Mise à jour des templates de VM et nettoyage des images inutilisées

This commit is contained in:
wpetit 2018-05-16 15:08:27 +02:00
parent 8ac2048fd6
commit 9cd443bef6
3 changed files with 497 additions and 76 deletions

View File

@ -1,10 +1,14 @@
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"
@ -28,6 +32,8 @@ type Config struct {
User string
Group string
} `mapstructure:"owner"`
AutoUpdateVMTemplates bool `mapstructure:"auto_update_vm_templates"`
CleanupUnusedImages bool `mapstructure:"cleanup_unused_images"`
ctx interpolate.Context
}
@ -53,139 +59,368 @@ func (pp *PostProcessor) Configure(raws ...interface{}) error {
// 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) {
// Initialize OpenNebula XML-RPC API client
config := goca.NewConfig(pp.Conf.User, pp.Conf.Password, pp.Conf.Endpoint)
if err := goca.SetClient(config); err != nil {
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
}
ui.Say(fmt.Sprintf("Connecting to OpenNebula RPC endpoint '%s' with provided credentials...", pp.Conf.Endpoint))
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(pp.Conf.ImageName)
img, err := goca.NewImageFromName(state.FullImageName)
if err != nil && err.Error() != "resource not found" {
return a, true, err
return err
}
if img != nil {
// Retreive info about the template
if err := img.Info(); err != nil {
return a, true, err
return err
}
state, err := img.State()
imgState, err := img.State()
if err != nil {
return a, true, err
return err
}
inUse := state == goca.ImageUsed || state == goca.ImageLockUsed
inUse := isImageUsed(imgState)
if inUse {
ui.Say(fmt.Sprintf("Template '%s' is in use. Cannot delete it.", pp.Conf.ImageName))
state.UI.Say(fmt.Sprintf("Image '%s' is in use. Cannot delete it.", state.FullImageName))
}
if pp.Conf.DeleteIfExists && !inUse {
ui.Say(fmt.Sprintf("Deleting template '%s'...", pp.Conf.ImageName))
if state.Config.DeleteIfExists && !inUse {
state.UI.Say(fmt.Sprintf("Deleting image '%s'...", state.FullImageName))
if err := img.Delete(); err != nil {
return a, true, err
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(pp.Conf.ImageTemplate)
tmplStr := serializeImageTemplate(state.FullImageName, state.Config.ImageTemplate)
// If the image template can not be found, we create it
if img == nil {
if state.Image == nil {
ui.Say(fmt.Sprintf("Creating template '%s'...", pp.Conf.ImageName))
state.UI.Say(fmt.Sprintf("Creating image '%s'...", state.FullImageName))
// Search image datastore's ID
datastore, err := goca.NewDatastoreFromName(pp.Conf.DatastoreName)
datastore, err := goca.NewDatastoreFromName(state.Config.DatastoreName)
if err != nil {
return a, true, err
return err
}
// Create image template
imageID, err := goca.CreateImage(tmplStr, datastore.ID)
if err != nil {
return a, true, err
return err
}
img = goca.NewImage(imageID)
state.Image = goca.NewImage(imageID)
// Retreive info about the template
if err := img.Info(); err != nil {
return a, true, err
if err := state.Image.Info(); err != nil {
return err
}
} else {
ui.Say(fmt.Sprintf("Updating template '%s'...", pp.Conf.ImageName))
state.UI.Say(fmt.Sprintf("Updating image '%s'...", state.FullImageName))
// Update image template
if err := img.Update(tmplStr, pp.Conf.MergeTemplate); err != nil {
return a, true, err
if err := state.Image.Update(tmplStr, state.Config.MergeTemplate); err != nil {
return err
}
}
if pp.Conf.Owner != nil {
return nil
currentUserName, userFound := img.XPath("/IMAGE/UNAME")
currentGroupName, groupFound := img.XPath("/IMAGE/GNAME")
}
isSameUser := userFound && currentUserName == pp.Conf.Owner.User
isSameGroup := groupFound && currentGroupName == pp.Conf.Owner.Group
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 pp.Conf.Owner.User != "" && !isSameUser {
user, err := goca.NewUserFromName(pp.Conf.Owner.User)
if owner.User != "" && !isSameUser {
user, err := goca.NewUserFromName(owner.User)
if err != nil {
return a, true, err
return err
}
userID = int(user.ID)
}
if pp.Conf.Owner.Group != "" && !isSameGroup {
group, err := goca.NewGroupFromName(pp.Conf.Owner.Group)
if owner.Group != "" && !isSameGroup {
group, err := goca.NewGroupFromName(owner.Group)
if err != nil {
return a, true, err
return err
}
groupID = int(group.ID)
}
ui.Say(fmt.Sprintf("Updating template '%s' owner...", pp.Conf.ImageName))
if err := img.Chown(userID, groupID); err != nil {
return a, true, err
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
}
}
if pp.Conf.ACL != nil {
ui.Say(fmt.Sprintf("Updating template '%s' ACL...", pp.Conf.ImageName))
chmodOptions := aclToChmodOptions(pp.Conf.ACL)
if err := img.Chmod(chmodOptions); err != nil {
return a, true, err
}
}
ui.Say("Operation completed.")
return a, true, nil
return nil
}
func serializeImageTemplate(tmpl []string) string {
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")
}

40
stepper/stepper.go Normal file
View File

@ -0,0 +1,40 @@
package stepper
import (
"context"
)
const (
Error Status = iota
Completed Status = iota
Canceled Status = iota
)
var (
ErrUnsupportedState = "unsupported state"
)
type Status int
type Step func(ctx context.Context, cancelFunc context.CancelFunc, state interface{}) error
func Run(ctx context.Context, state interface{}, steps ...Step) (Status, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
for _, s := range steps {
select {
case <-ctx.Done():
return Canceled, nil
default:
err := s(ctx, cancel, state)
if err != nil {
return Error, err
}
}
}
return Completed, nil
}

146
stepper/stepper_test.go Normal file
View File

@ -0,0 +1,146 @@
package stepper
import (
"context"
"errors"
"testing"
)
func TestBasicSteps(t *testing.T) {
ctx := context.Background()
state := make(map[string]bool)
step1 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
st, ok := state.(map[string]bool)
if !ok {
t.Error(ErrUnsupportedState)
}
st["step1"] = true
return nil
}
step2 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
st, ok := state.(map[string]bool)
if !ok {
t.Error(ErrUnsupportedState)
}
st["step2"] = true
return nil
}
status, err := Run(ctx, state, step1, step2)
if err != nil {
t.Error(err)
}
if g, e := status, Completed; g != e {
t.Errorf("status: expected '%v', got '%v'", e, g)
}
if g, e := state["step1"], true; g != e {
t.Errorf("state[\"step1\"]: expected '%v', got '%v'", e, g)
}
if g, e := state["step2"], true; g != e {
t.Errorf("state[\"step2\"]: expected '%v', got '%v'", e, g)
}
}
func TestStepsCancel(t *testing.T) {
ctx := context.Background()
state := make(map[string]bool)
step1 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
st, ok := state.(map[string]bool)
if !ok {
t.Error(ErrUnsupportedState)
}
st["step1"] = true
return nil
}
step2 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
cancel()
return nil
}
step3 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
st, ok := state.(map[string]bool)
if !ok {
t.Error(ErrUnsupportedState)
}
st["step3"] = true
return nil
}
status, err := Run(ctx, state, step1, step2, step3)
if err != nil {
t.Error(err)
}
if g, e := status, Canceled; g != e {
t.Errorf("status: expected '%v', got '%v'", e, g)
}
if g, e := state["step1"], true; g != e {
t.Errorf("state[\"step1\"]: expected '%v', got '%v'", e, g)
}
if g, e := state["step3"], false; g != e {
t.Errorf("state[\"step3\"]: expected '%v', got '%v'", e, g)
}
}
func TestStepsError(t *testing.T) {
stepErr := errors.New("test error")
ctx := context.Background()
state := make(map[string]bool)
step1 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
st, ok := state.(map[string]bool)
if !ok {
t.Error(ErrUnsupportedState)
}
st["step1"] = true
return nil
}
step2 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
return stepErr
}
step3 := func(ctx context.Context, cancel context.CancelFunc, state interface{}) error {
st, ok := state.(map[string]bool)
if !ok {
t.Error(ErrUnsupportedState)
}
st["step3"] = true
return nil
}
status, err := Run(ctx, state, step1, step2, step3)
if err == nil {
t.Errorf("err should not be nil")
}
if g, e := err, stepErr; g != e {
t.Errorf("err: expected '%v', got '%v'", e, g)
}
if g, e := status, Error; g != e {
t.Errorf("status: expected '%v', got '%v'", e, g)
}
if g, e := state["step1"], true; g != e {
t.Errorf("state[\"step1\"]: expected '%v', got '%v'", e, g)
}
if g, e := state["step3"], false; g != e {
t.Errorf("state[\"step3\"]: expected '%v', got '%v'", e, g)
}
}