453 lines
10 KiB
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
|
|
}
|