Skip to content
This repository has been archived by the owner on Dec 18, 2020. It is now read-only.

Implemented AttachISO task #4

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 5 additions & 2 deletions pkg/model/vspheremodel/autoscalinggroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ func (b *AutoscalingGroupModelBuilder) Build(c *fi.ModelBuilderContext) error {
Name: &name,
VMTemplateName: fi.String(defaultVmTemplateName),
}

c.AddTask(createVmTask)

attachISOTaskName := "AttachISO-" + name
attachISOTask := &vspheretasks.AttachISO{
Name: &attachISOTaskName,
VM: createVmTask,
Name: &attachISOTaskName,
VM: createVmTask,
IG: ig,
BootstrapScript: b.BootstrapScript,
}
c.AddTask(attachISOTask)

Expand Down
7 changes: 1 addition & 6 deletions upup/pkg/fi/cloudup/apply_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,12 +450,6 @@ func (c *ApplyClusterCmd) Run() error {
//&model.SSHKeyModelBuilder{KopsModelContext: modelContext},
)
case fi.CloudProviderVSphere:
vsphereModelContext := &vspheremodel.VSphereModelContext{
KopsModelContext: modelContext,
}

l.Builders = append(l.Builders,
Copy link

Choose a reason for hiding this comment

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

wait we would need this builder for PKI model...

Copy link
Author

Choose a reason for hiding this comment

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

Depending on who checks in first, one of us can add it!

&vspheremodel.VirtualMachineModelBuilder{VSphereModelContext: vsphereModelContext})

default:
return fmt.Errorf("unknown cloudprovider %q", cluster.Spec.CloudProvider)
Expand All @@ -482,6 +476,7 @@ func (c *ApplyClusterCmd) Run() error {
}

role := ig.Spec.Role
glog.V(4).Infof("Generating renderNodeUpConfig for %s", role)

Choose a reason for hiding this comment

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

Nit: Please move this log statement after if-block. If role is nil then anyways the program is exiting with error.

"Generating Nodeup config for role..." would be a more appropriate message.

Copy link
Author

Choose a reason for hiding this comment

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

I did not intend to checkin this. It was added for debugging. Will remove this log,

if role == "" {
return nil, fmt.Errorf("cannot determine role for instance group: %v", ig.ObjectMeta.Name)
}
Expand Down
79 changes: 76 additions & 3 deletions upup/pkg/fi/cloudup/vsphere/vsphere_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import (
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/property"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/upup/pkg/fi"
Expand All @@ -49,9 +52,10 @@ type VSphereCloud struct {
}

const (
snapshotName string = "LinkCloneSnapshotPoint"
snapshotDesc string = "Snapshot created by kops"
privateDNS string = "coredns"
snapshotName string = "LinkCloneSnapshotPoint"
snapshotDesc string = "Snapshot created by kops"
privateDNS string = "coredns"
cloudInitFile string = "cloud-init.iso"
)

var _ fi.Cloud = &VSphereCloud{}
Expand Down Expand Up @@ -221,3 +225,72 @@ func (c *VSphereCloud) PowerOn(vm string) error {
task.Wait(ctx)
return nil
}

func (c *VSphereCloud) UploadAndAttachISO(vm *string, isoFile string) error {
f := find.NewFinder(c.Client.Client, true)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

dc, err := f.Datacenter(ctx, c.Datacenter)
if err != nil {
return err
}
f.SetDatacenter(dc)

vmRef, err := f.VirtualMachine(ctx, *vm)
if err != nil {
return err
}

var refs []types.ManagedObjectReference
refs = append(refs, vmRef.Reference())
var vmResult mo.VirtualMachine

pc := property.DefaultCollector(c.Client.Client)
err = pc.RetrieveOne(ctx, vmRef.Reference(), []string{"datastore"}, &vmResult)
if err != nil {
glog.Fatalf("Unable to retrieve VM summary for VM %s", *vm)
}
glog.V(4).Infof("vm property collector result :%+v\n", vmResult)

// We expect the VM to be on only 1 datastore
dsRef := vmResult.Datastore[0].Reference()
var dsResult mo.Datastore
err = pc.RetrieveOne(ctx, dsRef, []string{"summary"}, &dsResult)
if err != nil {
glog.Fatalf("Unable to retrieve datastore summary for datastore %s", dsRef)
}
glog.V(4).Infof("datastore property collector result :%+v\n", dsResult)
dsObj, err := f.Datastore(ctx, dsResult.Summary.Name)
if err != nil {
return err
}
p := soap.DefaultUpload
dstIsoFile := getCloudInitFileName(*vm)
glog.V(2).Infof("Uploading ISO file %s to datastore %+v, destination iso is %s\n", isoFile, dsObj, dstIsoFile)
err = dsObj.UploadFile(ctx, isoFile, dstIsoFile, &p)
if err != nil {
return err
}
glog.V(2).Infof("Uploaded ISO file %s", isoFile)

// Find the cd-rom devide and insert the cloud init iso file into it.
devices, err := vmRef.Device(ctx)
if err != nil {
return err
}

// passing empty cd-rom name so that the first one gets returned
cdrom, err := devices.FindCdrom("")

Choose a reason for hiding this comment

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

Can you please explain with does empty string argument signifies here?

Copy link
Author

Choose a reason for hiding this comment

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

Done

if err != nil {
return err
}
iso := dsObj.Path(dstIsoFile)
glog.V(2).Infof("Inserting ISO file %s into cd-rom", iso)
return vmRef.EditDevice(ctx, devices.InsertIso(cdrom, iso))

}

func getCloudInitFileName(vmName string) string {
return vmName + "/" + cloudInitFile
}
108 changes: 104 additions & 4 deletions upup/pkg/fi/cloudup/vspheretasks/attachiso.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,43 @@ limitations under the License.
package vspheretasks

import (
"bytes"
"fmt"
"github.com/golang/glog"
"github.com/pborman/uuid"
"io/ioutil"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/model"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/vsphere"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)

// AttachISO represents the cloud-init ISO file attached to a VMware VM
//go:generate fitask -type=AttachISO
type AttachISO struct {
Name *string
VM *VirtualMachine

Choose a reason for hiding this comment

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

Can you please explain why did you have to remove dependency on VirtualMachine task and add it explicitly via GetDependencies method?

It would be nice if we can utilize the dependency graph of topological sort, rather than having to implement explicit GetDependencies method for various vsphere tasks.

Also, it would make more sense to get the vm name from VirtualMachine, as opposed to setting it in various tasks that need it, eg: AttachISO and VMPowerOn.

Copy link
Author

@SandeepPissay SandeepPissay Mar 22, 2017

Choose a reason for hiding this comment

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

VirtualMachine is a task. I wanted to add more members to AttachISO. The way the reflection based task dependency works is that it thinks that every field in the task is a dependency. For example, when I added InstanceGroup and BootstratScript fields in AttachISO task, the reflection based task dependency thinks that InstanceGroup and BootstratScript are tasks and fails complaining that they are not tasks.

Basically if the tasks structs are simple, then the reflection based task dependency management is useful but in our case it is not simple. So, I had to explicitly call out the task dependency which is also fine. Basically there are 2 ways of expressing task dependency -

  1. Implicit task dependency by way of composition (works only for simple tasks)
  2. Explicit task dependency by implementing HasDependency interface

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

I believe I have found the problem that you are talking about. This is the code responsible for ignoring or accounting a dependency for reflect.Struct https://github.com/vmware/kops/blob/master/upup/pkg/fi/topological_sort.go#L98

The problem is that current code identifies only those structs that are listed in the if-else structure. The structs that you are adding to your task, like InstanceGroup and BootstratScript don't end up getting processed by last else-block and hence the error "Unhandled type for...".

This if-else structure is not the ideal way of defining dependencies. But implementing HasDepenency method is also bit of a redundant work.

I would recommend maintaining a set of types, like InstanceGroup and BootScript, which we don't want to include as a dependency. Check that in the above discussed if-else block.

Name *string

Choose a reason for hiding this comment

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

I see that you have updated code to use VM.Name. Do you still need this string 'Name' field?

Copy link
Author

Choose a reason for hiding this comment

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

I think every task struct need to have the task Name. This was added in my previous checkin and I remember that not having this field was having problems executing the tasks.

VM *VirtualMachine
IG *kops.InstanceGroup
BootstrapScript *model.BootstrapScript
}

var _ fi.HasName = &AttachISO{}
var _ fi.HasDependencies = &AttachISO{}

func (o *AttachISO) GetDependencies(tasks map[string]fi.Task) []fi.Task {
var deps []fi.Task
vmCreateTask := tasks["VirtualMachine/"+*o.VM.Name]
if vmCreateTask == nil {
glog.Fatalf("Unable to find create VM task %s dependency for AttachISO %s", *o.VM.Name, *o.Name)
}
deps = append(deps, vmCreateTask)
return deps
}

// GetName returns the Name of the object, implementing fi.HasName
func (o *AttachISO) GetName() *string {
Expand All @@ -56,7 +80,83 @@ func (_ *AttachISO) CheckChanges(a, e, changes *AttachISO) error {
return nil
}

func (_ *AttachISO) RenderVC(t *vsphere.VSphereAPITarget, a, e, changes *AttachISO) error {
glog.Info("AttachISO.RenderVC invoked!")
func (_ *AttachISO) RenderVSphere(t *vsphere.VSphereAPITarget, a, e, changes *AttachISO) error {
startupScript, err := changes.BootstrapScript.ResourceNodeUp(changes.IG)
startupStr, err := startupScript.AsString()
if err != nil {
return fmt.Errorf("error rendering startup script: %v", err)
}
dir, err := ioutil.TempDir("", *changes.VM.Name)
defer os.RemoveAll(dir)

isoFile := createISO(changes, startupStr, dir)

Choose a reason for hiding this comment

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

I see that createISO method is not returning any error. It would be good to either return an error or check isoFile to be not nil.

Copy link
Author

Choose a reason for hiding this comment

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

If there is an error in createISO we fail the execution by invoking glog.Fatalf method. Basically there is no point in continuing if they fail.

err = t.Cloud.UploadAndAttachISO(changes.VM.Name, isoFile)
if err != nil {
return err
}

return nil
}

func createUserData(startupStr string, dir string) {
// Update the startup script to add the extra spaces for
// indentation when copied to the user-data file.
strArray := strings.Split(startupStr, "\n")
for i, str := range strArray {
if len(str) > 0 {
strArray[i] = " " + str
}
}
startupStr = strings.Join(strArray, "\n")

data := strings.Replace(userDataTemplate, "$SCRIPT", startupStr, -1)
userDataFile := filepath.Join(dir, "user-data")
glog.V(4).Infof("User data file content: %s", data)

if err := ioutil.WriteFile(userDataFile, []byte(data), 0644); err != nil {
glog.Fatalf("Unable to write user-data into file %s", userDataFile)

Choose a reason for hiding this comment

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

Errors from createUserData and createMetaData are not getting propagated to createISO. How do you plan on handling them?

Copy link
Author

Choose a reason for hiding this comment

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

They fail the kops command. See the usage of glog.Fatalf method in error conditions in those methods.

}
return
}

func createMetaData(dir string, vmName string) {
data := strings.Replace(metaDataTemplate, "$INSTANCE_ID", uuid.NewUUID().String(), -1)
data = strings.Replace(data, "$LOCAL_HOST_NAME", vmName, -1)

glog.V(4).Infof("Meta data file content: %s", string(data))

metaDataFile := filepath.Join(dir, "meta-data")
if err := ioutil.WriteFile(metaDataFile, []byte(data), 0644); err != nil {
glog.Fatalf("Unable to write meta-data into file %s", metaDataFile)
}
return
}

func createISO(changes *AttachISO, startupStr string, dir string) string {
createUserData(startupStr, dir)
createMetaData(dir, *changes.VM.Name)

isoFile := filepath.Join(dir, *changes.VM.Name+".iso")
var cmd *exec.Cmd

switch os := runtime.GOOS; os {
case "darwin":
cmd = exec.Command("mkisofs", "-o", isoFile, "-volid", "cidata", "-joliet", "-rock", dir)
case "linux":
cmd = exec.Command("genisoimage", "-o", isoFile, "-volid", "cidata", "-joliet", "-rock", dir)
default:
glog.Fatalf("Cannot generate ISO file %s. Unsupported operation system!!!", isoFile)
}
var out bytes.Buffer
cmd.Stdout = &out
var stderr bytes.Buffer
cmd.Stderr = &stderr

err := cmd.Run()
if err != nil {
glog.Fatalf("Error %s occurred while executing command %+v", err, cmd)

Choose a reason for hiding this comment

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

Return here with nil?

Copy link
Author

Choose a reason for hiding this comment

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

Not needed as err not being nil is fatal and will stop kops.

}
glog.V(4).Infof("genisoimage std output : %s\n", out.String())
glog.V(4).Infof("genisoimage std error : %s\n", stderr.String())
return isoFile

Choose a reason for hiding this comment

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

Please save command name in a variable and use that for logging. It could be genisoimage or mkisofs.

Copy link
Author

Choose a reason for hiding this comment

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

Done

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package vspheremodel

import (
"fmt"
"k8s.io/kops/upup/pkg/fi"
)

// Do we need this model builder?

// AutoscalingGroupModelBuilder configures AutoscalingGroup objects
type VirtualMachineModelBuilder struct {
*VSphereModelContext
}

func (b *VirtualMachineModelBuilder) Build(c *fi.ModelBuilderContext) error {
fmt.Print("In VirtualMachineModelBuilder.Build function!!")
return nil
}
package vspheretasks

// Template for user-data file in the cloud-init ISO
const userDataTemplate = `#cloud-config
write_files:
- content: |
$SCRIPT
owner: root:root
path: /tmp/script.sh
permissions: "0644"

runcmd:
- bash /tmp/script.sh 2>&1 > /var/log/script.log`

// Template for meta-data file in the cloud-init ISO
const metaDataTemplate = `instance-id: $INSTANCE_ID
local-hostname: $LOCAL_HOST_NAME`
15 changes: 13 additions & 2 deletions upup/pkg/fi/cloudup/vspheretasks/vmpoweron.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ type VMPowerOn struct {
}

var _ fi.HasName = &VMPowerOn{}
var _ fi.HasDependencies = &VMPowerOn{}

func (o *VMPowerOn) GetDependencies(tasks map[string]fi.Task) []fi.Task {

Choose a reason for hiding this comment

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

Same question here, why do we need to implement explicit GetDependencies method?

var deps []fi.Task
attachISOTask := tasks["AttachISO/"+*o.AttachISO.Name]
if attachISOTask == nil {
glog.Fatalf("Unable to find attachISO task %s dependency for VMPowerOn %s", *o.AttachISO.Name, *o.Name)
}
deps = append(deps, attachISOTask)
return deps
}

// GetName returns the Name of the object, implementing fi.HasName
func (o *VMPowerOn) GetName() *string {
Expand All @@ -56,8 +67,8 @@ func (_ *VMPowerOn) CheckChanges(a, e, changes *VMPowerOn) error {
return nil
}

func (_ *VMPowerOn) RenderVC(t *vsphere.VSphereAPITarget, a, e, changes *VMPowerOn) error {
glog.V(2).Infof("VMPowerOn.RenderVC invoked for vm %s", *changes.AttachISO.VM.Name)
func (_ *VMPowerOn) RenderVSphere(t *vsphere.VSphereAPITarget, a, e, changes *VMPowerOn) error {
glog.V(2).Infof("VMPowerOn.RenderVSphere invoked for vm %s", *changes.AttachISO.VM.Name)
err := t.Cloud.PowerOn(*changes.AttachISO.VM.Name)
return err
}