packer-opennebula/cmd/post-processor/image-template/image_template.go

453 lines
10 KiB
Go

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
}