package main import ( "context" "fmt" "regexp" "strconv" "strings" "time" "forge.cadoles.com/wpetit/packer-opennebula/stepper" "github.com/Cadoles/goca" "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" ) // Config handles configuration options of the OpenNebula post-processor type Config struct { common.PackerConfig `mapstructure:",squash"` User string Password string Endpoint string ImageName string `mapstructure:"image_name"` ImageTemplate []string `mapstructure:"image_template"` DatastoreName string `mapstructure:"datastore_name"` MergeTemplate int `mapstructure:"merge_template"` DeleteIfExists bool `mapstructure:"delete_if_exists"` ACL map[string]int `mapstructure:"acl"` Owner *struct { User string Group string } `mapstructure:"owner"` AutoUpdateVMTemplates bool `mapstructure:"auto_update_vm_templates"` CleanupUnusedImages bool `mapstructure:"cleanup_unused_images"` ctx interpolate.Context } // PostProcessor is an OpenNebula post processor for packer type PostProcessor struct { Conf *Config } // Configure configures the packer OpenNebula post-processor func (pp *PostProcessor) Configure(raws ...interface{}) error { conf := &Config{} err := config.Decode(conf, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &conf.ctx, }, raws...) if err != nil { return err } pp.Conf = conf return nil } // PostProcess creates/updates your configured image to OpenNebula using the XML-RPC API func (pp *PostProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.Artifact, bool, error) { ctx := context.Background() state := &postProcessorState{ Config: pp.Conf, UI: ui, } status, err := stepper.Run( ctx, state, stepInitializeGocaClient, stepGenerateFullImageName, stepRetrieveImage, stepUpsertImage, stepUpdateImageOwnership, stepUpdateImageACL, stepUpdateVMTemplates, stepCleanupUnusedImages, ) if err != nil { return a, true, err } switch status { case stepper.Completed: ui.Say("Operation completed.") case stepper.Canceled: ui.Say("Operation canceled.") } return a, true, nil } type postProcessorState struct { Config *Config UI packer.Ui Image *goca.Image FullImageName string } func stepInitializeGocaClient(ctx context.Context, cancelFunc context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) // Initialize OpenNebula XML-RPC API client config := goca.NewConfig(state.Config.User, state.Config.Password, state.Config.Endpoint) if err := goca.SetClient(config); err != nil { return err } return nil } func stepGenerateFullImageName(ctx context.Context, cancelFunc context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) if !state.Config.AutoUpdateVMTemplates { state.FullImageName = state.Config.ImageName return nil } state.UI.Say("VM templates auto update activated. Adding timestamp to image's name.") now := time.Now().Format("200601021504") state.FullImageName = fmt.Sprintf("%s-%s", state.Config.ImageName, now) return nil } func stepRetrieveImage(ctx context.Context, cancelFunc context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) state.UI.Say(fmt.Sprintf("Connecting to OpenNebula RPC endpoint '%s' with provided credentials...", state.Config.Endpoint)) // Search image template by its name img, err := goca.NewImageFromName(state.FullImageName) if err != nil && err.Error() != "resource not found" { return err } if img != nil { // Retreive info about the template if err := img.Info(); err != nil { return err } imgState, err := img.State() if err != nil { return err } inUse := isImageUsed(imgState) if inUse { state.UI.Say(fmt.Sprintf("Image '%s' is in use. Cannot delete it.", state.FullImageName)) } if state.Config.DeleteIfExists && !inUse { state.UI.Say(fmt.Sprintf("Deleting image '%s'...", state.FullImageName)) if err := img.Delete(); err != nil { return err } time.Sleep(1 * time.Second) img = nil } } state.Image = img return nil } func stepUpsertImage(ctx context.Context, cancelFunc context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) // Generate image template tmplStr := serializeImageTemplate(state.FullImageName, state.Config.ImageTemplate) // If the image template can not be found, we create it if state.Image == nil { state.UI.Say(fmt.Sprintf("Creating image '%s'...", state.FullImageName)) // Search image datastore's ID datastore, err := goca.NewDatastoreFromName(state.Config.DatastoreName) if err != nil { return err } // Create image template imageID, err := goca.CreateImage(tmplStr, datastore.ID) if err != nil { return err } state.Image = goca.NewImage(imageID) // Retreive info about the template if err := state.Image.Info(); err != nil { return err } } else { state.UI.Say(fmt.Sprintf("Updating image '%s'...", state.FullImageName)) // Update image template if err := state.Image.Update(tmplStr, state.Config.MergeTemplate); err != nil { return err } } return nil } func stepUpdateImageOwnership(ctx context.Context, cancelFunc context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) if state.Config.Owner == nil { return nil } owner := state.Config.Owner currentUserName, userFound := state.Image.XPath("/IMAGE/UNAME") currentGroupName, groupFound := state.Image.XPath("/IMAGE/GNAME") isSameUser := userFound && currentUserName == owner.User isSameGroup := groupFound && currentGroupName == owner.Group userID := -1 groupID := -1 if owner.User != "" && !isSameUser { user, err := goca.NewUserFromName(owner.User) if err != nil { return err } userID = int(user.ID) } if owner.Group != "" && !isSameGroup { group, err := goca.NewGroupFromName(owner.Group) if err != nil { return err } groupID = int(group.ID) } state.UI.Say(fmt.Sprintf("Updating image '%s' owner...", state.FullImageName)) if err := state.Image.Chown(userID, groupID); err != nil { return err } return nil } func stepUpdateImageACL(ctx context.Context, cancelFunc context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) if state.Config.ACL == nil { return nil } state.UI.Say(fmt.Sprintf("Updating template '%s' ACL...", state.FullImageName)) chmodOptions := aclToChmodOptions(state.Config.ACL) if err := state.Image.Chmod(chmodOptions); err != nil { return err } return nil } func stepUpdateVMTemplates(ctx context.Context, cancel context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) if !state.Config.AutoUpdateVMTemplates { return nil } state.UI.Say("Searching VM templates using old images...") templatePool, err := goca.NewTemplatePool(goca.PoolWhoAll, -1, -1) if err != nil { return err } imageNameRegExp, err := getFullImageNamePattern(state.Config.ImageName) if err != nil { return err } xPathIter := templatePool.XPathIter("/VMTEMPLATE_POOL/VMTEMPLATE") for xPathIter.Next() { node := xPathIter.Node() imageName, _ := node.XPath("TEMPLATE/DISK/IMAGE") if !imageNameRegExp.MatchString(imageName) || imageName == state.FullImageName { continue } templateName, _ := node.XPath("NAME") state.UI.Say(fmt.Sprintf("Found VM template '%s' using image '%s' ", templateName, imageName)) rawTemplateID, _ := node.XPath("ID") templateID, err := strconv.ParseUint(rawTemplateID, 10, 32) if err != nil { return err } template := goca.NewTemplate(uint(templateID)) imageUser, _ := state.Image.XPath("/IMAGE/UNAME") templateUpdate := fmt.Sprintf(` DISK = [ IMAGE = "%s", IMAGE_UNAME = "%s" ] `, state.FullImageName, imageUser) state.UI.Say(fmt.Sprintf("Updating VM template '%s'", templateName)) if err := template.Update(templateUpdate, 1); err != nil { return err } } return nil } func stepCleanupUnusedImages(ctx context.Context, cancel context.CancelFunc, st interface{}) error { state := toPostProcessorState(st) if !state.Config.CleanupUnusedImages { return nil } state.UI.Say("Cleaning old unused images...") imageNameRegExp, err := getFullImageNamePattern(state.Config.ImageName) if err != nil { return err } imagePool, err := goca.NewImagePool(goca.PoolWhoAll, -1, -1) if err != nil { return err } xPathIter := imagePool.XPathIter("/IMAGE_POOL/IMAGE") for xPathIter.Next() { node := xPathIter.Node() imageName, _ := node.XPath("NAME") if !imageNameRegExp.MatchString(imageName) || imageName == state.FullImageName { continue } rawImgState, _ := node.XPath("STATE") imgState, err := strconv.Atoi(rawImgState) if err != nil { return err } if isImageUsed(goca.ImageState(imgState)) { continue } state.UI.Say(fmt.Sprintf("Found unused image '%s'. Deleting it.", imageName)) rawImageID, _ := node.XPath("ID") imageID, err := strconv.ParseUint(rawImageID, 10, 32) if err != nil { return err } image := goca.NewImage(uint(imageID)) if err := image.Delete(); err != nil { return err } } return nil } func toPostProcessorState(st interface{}) *postProcessorState { state, ok := st.(*postProcessorState) if !ok { panic("couldnt cast state to the expected type") } return state } func isImageUsed(imgState goca.ImageState) bool { return imgState == goca.ImageUsed || imgState == goca.ImageLockUsed } func getFullImageNamePattern(imageName string) (*regexp.Regexp, error) { return regexp.Compile(fmt.Sprintf(`^%s-\d+$`, imageName)) } func serializeImageTemplate(imageName string, tmpl []string) string { tmpl = append(tmpl, fmt.Sprintf("NAME = \"%s\"", imageName)) return strings.Join(tmpl, "\n") } func aclToChmodOptions(acl map[string]int) goca.ImageChmodOptions { chmodOptions := goca.NewImageChmodOptions() for key, val := range acl { switch key { case "user_use": chmodOptions.UserUse = val case "user_manage": chmodOptions.UserManage = val case "user_admin": chmodOptions.UserAdmin = val case "group_use": chmodOptions.GroupUse = val case "group_manage": chmodOptions.GroupManage = val case "group_admin": chmodOptions.GroupAdmin = val case "other_use": chmodOptions.OtherUse = val case "other_manage": chmodOptions.OtherManage = val case "other_admin": chmodOptions.OtherAdmin = val } } return chmodOptions }