Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add networkupdate support for VM, introduce forced guest customization function #229

Merged
merged 20 commits into from
Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* Added method `VCDClient.QueryProviderVdcStorageProfiles`
* Added method `VCDClient.QueryNetworkPools`
* Added get/add/delete metadata functions for vApp template and media item [#225](https://github.com/vmware/go-vcloud-director/pull/225).
* Added `UpdateNetworkConnectionSection` for updating VM network configuration [#229](https://github.com/vmware/go-vcloud-director/pull/229)
* Added `PowerOnAndForceCustomization`, `GetGuestCustomizationStatus`, `BlockWhileGuestCustomizationStatus` [#229](https://github.com/vmware/go-vcloud-director/pull/229)
* Deprecated methods `AdminOrg.GetAdminVdcByName`, `AdminOrg.GetVdcByName`, `AdminOrg.FindAdminCatalog`, `AdminOrg.FindCatalog`
* Deprecated methods `Catalog.FindCatalogItem`, `Org.FindCatalog`, `Org.GetVdcByName`
* Deprecated function `GetExternalNetwork`
Expand Down
81 changes: 44 additions & 37 deletions govcd/lb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,61 +250,68 @@ func buildLb(edge EdgeGateway, node1Ip, node2Ip string, vcd *TestVCD, check *C)
// checkLb queries specified endpoint until it gets all responses in expectedResponses slice
func checkLb(queryUrl string, expectedResponses []string, maxRetryTimeout int) error {
var err error
var iterations int
if len(expectedResponses) == 0 {
return fmt.Errorf("no expected responses specified")
}

retryTimeout := maxRetryTimeout
// due to the VMs taking long time to boot it needs to be at least 5 minutes
// may be even more in slower environments
sleepInterval := 5
sleepIntervalDuration := time.Duration(sleepInterval) * time.Second
if maxRetryTimeout < 5*60 { // 5 minutes
iterations = 60
} else {
iterations = maxRetryTimeout / 5
retryTimeout = 5 * 60 // 5 minutes
}

fmt.Printf("# Waiting for the virtual server to accept responses (%s interval x %d iterations)"+
timeOutAfterInterval := time.Duration(retryTimeout) * time.Second
timeoutAfter := time.After(timeOutAfterInterval)
tick := time.NewTicker(time.Duration(5) * time.Second)

httpClient := &http.Client{Timeout: 5 * time.Second}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment explaining how this timeout affects the expected duration of the test?

I am talking about the estimate given with Waiting for the virtual server to accept responses (%s interval x %d iterations)"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. This simple loop was actually quite unstable in guessing timing. I put in a time.Ticker so that it does not lie and actually work as the time was set.


fmt.Printf("# Waiting for the virtual server to accept responses (timeout after %s)"+
"\n[_ = timeout, x = connection refused, ?(err) = unknown error, / = no nodes are up yet, "+
". = no response from all nodes yet]: ", sleepIntervalDuration.String(), iterations)
for i := 1; i <= iterations; i++ {
var resp *http.Response
resp, err = http.Get(queryUrl)
if err != nil {
switch {
case strings.Contains(err.Error(), "i/o timeout"):
fmt.Printf("_")
case strings.Contains(err.Error(), "connect: connection refused"):
fmt.Printf("x")
case strings.Contains(err.Error(), "connect: network is unreachable"):
fmt.Printf("/")
default:
fmt.Printf("?(%s)", err.Error())
". = no response from all nodes yet]: ", timeOutAfterInterval.String())

for {
select {
case <-timeoutAfter:
return fmt.Errorf("timed out waiting for all nodes to be up: %s", err)
case <-tick.C:
var resp *http.Response
resp, err = httpClient.Get(queryUrl)
if err != nil {
switch {
case strings.Contains(err.Error(), "i/o timeout"):
fmt.Printf("_")
case strings.Contains(err.Error(), "connect: connection refused"):
fmt.Printf("x")
case strings.Contains(err.Error(), "connect: network is unreachable"):
fmt.Printf("/")
default:
fmt.Printf("?(%s)", err.Error())
}
}
}

if err == nil {
fmt.Printf(".") // progress bar when waiting for responses from all nodes
body, _ := ioutil.ReadAll(resp.Body)
// check if the element is in the list
for index, value := range expectedResponses {
if value == string(body) {
expectedResponses = append(expectedResponses[:index], expectedResponses[index+1:]...)
if len(expectedResponses) > 0 {
fmt.Printf("\n# '%s' responded. Waiting for node(s) '%s': ",
value, strings.Join(expectedResponses, ","))
} else {
fmt.Printf("\n# Last node '%s' responded. Exiting\n", value)
return nil
if err == nil {
fmt.Printf(".") // progress bar when waiting for responses from all nodes
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
// check if the element is in the list
for index, value := range expectedResponses {
if value == string(body) {
expectedResponses = append(expectedResponses[:index], expectedResponses[index+1:]...)
if len(expectedResponses) > 0 {
fmt.Printf("\n# '%s' responded. Waiting for node(s) '%s': ",
value, strings.Join(expectedResponses, ","))
} else {
fmt.Printf("\n# Last node '%s' responded. Exiting\n", value)
return nil
}
}
}
}
}
time.Sleep(sleepIntervalDuration)
}

return fmt.Errorf("timed out waiting for all nodes to be up: %s", err)
}

// addFirewallRule adds a firewall rule needed to access virtual server port on edge gateway
Expand Down
4 changes: 2 additions & 2 deletions govcd/vapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,14 @@ func (vapp *VApp) GetStatus() (string, error) {
// of seconds.
func (vapp *VApp) BlockWhileStatus(unwantedStatus string, timeOutAfterSeconds int) error {
timeoutAfter := time.After(time.Duration(timeOutAfterSeconds) * time.Second)
tick := time.Tick(200 * time.Millisecond)
tick := time.NewTicker(200 * time.Millisecond)

for {
select {
case <-timeoutAfter:
return fmt.Errorf("timed out waiting for vApp to exit state %s after %d seconds",
unwantedStatus, timeOutAfterSeconds)
case <-tick:
case <-tick.C:
currentStatus, err := vapp.GetStatus()

if err != nil {
Expand Down
129 changes: 127 additions & 2 deletions govcd/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"strconv"
"time"

"github.com/vmware/go-vcloud-director/v2/types/v56"
"github.com/vmware/go-vcloud-director/v2/util"
Expand Down Expand Up @@ -48,6 +49,15 @@ func (vm *VM) GetStatus() (string, error) {
return types.VAppStatuses[vm.VM.Status], nil
}

// IsDeployed checks if the VM is deployed or not
func (vm *VM) IsDeployed() (bool, error) {
err := vm.Refresh()
if err != nil {
return false, fmt.Errorf("error refreshing VM: %v", err)
}
return vm.VM.Deployed, nil
}

func (vm *VM) Refresh() error {

if vm.VM.HREF == "" {
Expand Down Expand Up @@ -75,7 +85,7 @@ func (vm *VM) GetNetworkConnectionSection() (*types.NetworkConnectionSection, er
networkConnectionSection := &types.NetworkConnectionSection{}

if vm.VM.HREF == "" {
return networkConnectionSection, fmt.Errorf("cannot refresh, Object is empty")
return networkConnectionSection, fmt.Errorf("cannot retrieve network when VM HREF is unset")
}

_, err := vm.client.ExecuteRequest(vm.VM.HREF+"/networkConnectionSection/", http.MethodGet,
Expand All @@ -85,6 +95,35 @@ func (vm *VM) GetNetworkConnectionSection() (*types.NetworkConnectionSection, er
return networkConnectionSection, err
}

// UpdateNetworkConnectionSection applies network configuration of types.NetworkConnectionSection for the VM
// Runs synchronously, VM is ready for another operation after this function returns.
func (vm *VM) UpdateNetworkConnectionSection(networks *types.NetworkConnectionSection) error {
vbauzys marked this conversation as resolved.
Show resolved Hide resolved
if vm.VM.HREF == "" {
return fmt.Errorf("cannot update network connection when VM HREF is unset")
}

// Retrieve current network configuration so that we are not altering any other internal fields
updateNetwork, err := vm.GetNetworkConnectionSection()
if err != nil {
return fmt.Errorf("cannot read network section for update: %s", err)
}
updateNetwork.PrimaryNetworkConnectionIndex = networks.PrimaryNetworkConnectionIndex
updateNetwork.NetworkConnection = networks.NetworkConnection
updateNetwork.Ovf = types.XMLNamespaceOVF

task, err := vm.client.ExecuteTaskRequest(vm.VM.HREF+"/networkConnectionSection/", http.MethodPut,
types.MimeNetworkConnectionSection, "error updating network connection: %s", updateNetwork)
if err != nil {
return err
}
err = task.WaitTaskCompletion()
if err != nil {
return fmt.Errorf("error waiting for task completion after network update for vm %s: %s", vm.VM.Name, err)
}

return nil
}

func (cli *Client) FindVMByHREF(vmHREF string) (VM, error) {

newVm := NewVM(cli)
Expand All @@ -107,6 +146,46 @@ func (vm *VM) PowerOn() (Task, error) {

}

// PowerOnAndForceCustomization is a synchronous function which is equivalent to the functionality
// one has in UI. It triggers customization which may be useful in some cases (like altering NICs)
//
// The VM _must_ be un-deployed for this action to actually work.
func (vm *VM) PowerOnAndForceCustomization() error {
vbauzys marked this conversation as resolved.
Show resolved Hide resolved
// PowerOnAndForceCustomization only works if the VM was previously un-deployed
vmIsDeployed, err := vm.IsDeployed()
if err != nil {
return fmt.Errorf("unable to check if VM %s is un-deployed forcing customization: %s",
vm.VM.Name, err)
}

if vmIsDeployed {
return fmt.Errorf("VM %s must be undeployed before forcing customization", vm.VM.Name)
}

apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF)
apiEndpoint.Path += "/action/deploy"

powerOnAndCustomize := &types.DeployVAppParams{
Xmlns: types.XMLNamespaceVCloud,
PowerOn: true,
ForceCustomization: true,
}

task, err := vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost,
"", "error powering on VM with customization: %s", powerOnAndCustomize)

if err != nil {
return err
}

err = task.WaitTaskCompletion()
if err != nil {
return fmt.Errorf("error waiting for task completion after power on with customization %s: %s", vm.VM.Name, err)
}

return nil
}

func (vm *VM) PowerOff() (Task, error) {

apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF)
Expand Down Expand Up @@ -302,6 +381,51 @@ func (vm *VM) RunCustomizationScript(computername, script string) (Task, error)
return vm.Customize(computername, script, false)
}

// GetGuestCustomizationStatus retrieves guest customization status.
// It can be one of "GC_PENDING", "REBOOT_PENDING", "GC_FAILED", "POST_GC_PENDING", "GC_COMPLETE"
func (vm *VM) GetGuestCustomizationStatus() (string, error) {
vbauzys marked this conversation as resolved.
Show resolved Hide resolved
guestCustomizationStatus := &types.GuestCustomizationStatusSection{}

if vm.VM.HREF == "" {
return "", fmt.Errorf("cannot retrieve guest customization, VM HREF is empty")
}

_, err := vm.client.ExecuteRequest(vm.VM.HREF+"/guestcustomizationstatus", http.MethodGet,
types.MimeGuestCustomizationStatus, "error retrieving guest customization status: %s", nil, guestCustomizationStatus)

// The request was successful
return guestCustomizationStatus.GuestCustStatus, err
}

// BlockWhileGuestCustomizationStatus blocks until the customization status of VM exits unwantedStatus.
// It sleeps 3 seconds between iterations and times out after timeOutAfterSeconds of seconds.
//
// timeOutAfterSeconds must be more than 4 and less than 2 hours (60s*120)
func (vm *VM) BlockWhileGuestCustomizationStatus(unwantedStatus string, timeOutAfterSeconds int) error {
vbauzys marked this conversation as resolved.
Show resolved Hide resolved
if timeOutAfterSeconds < 5 || timeOutAfterSeconds > 60*120 {
return fmt.Errorf("timeOutAfterSeconds must be in range 4<X<7200")
}

timeoutAfter := time.After(time.Duration(timeOutAfterSeconds) * time.Second)
tick := time.NewTicker(3 * time.Second)

for {
select {
case <-timeoutAfter:
return fmt.Errorf("timed out waiting for VM guest customization status to exit state %s after %d seconds",
unwantedStatus, timeOutAfterSeconds)
case <-tick.C:
currentStatus, err := vm.GetGuestCustomizationStatus()
if err != nil {
return fmt.Errorf("could not get VM customization status %s", err)
}
if currentStatus != unwantedStatus {
return nil
}
}
}
}

func (vm *VM) Customize(computername, script string, changeSid bool) (Task, error) {
err := vm.Refresh()
if err != nil {
Expand Down Expand Up @@ -330,6 +454,7 @@ func (vm *VM) Customize(computername, script string, changeSid bool) (Task, erro
types.MimeGuestCustomizationSection, "error customizing VM: %s", vu)
}

// Undeploy triggers a VM undeploy and power off action. "Power off" action in UI behaves this way.
func (vm *VM) Undeploy() (Task, error) {

vu := &types.UndeployVAppParams{
Expand All @@ -342,7 +467,7 @@ func (vm *VM) Undeploy() (Task, error) {

// Return the task
return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost,
types.MimeUndeployVappParams, "error undeploy vApp: %s", vu)
types.MimeUndeployVappParams, "error undeploy VM: %s", vu)
}

// Attach or detach an independent disk
Expand Down
Loading