Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not write status file on Disable, Update. Create dummy status files during update if needed. #37

Merged
merged 3 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
131 changes: 115 additions & 16 deletions internal/cmds/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"bufio"
"bytes"
"compress/gzip"
"container/list"
"context"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -334,21 +336,29 @@ func checkAndSaveSeqNum(ctx log.Logger, seq int, mrseqPath string) (shouldExit b
// Copy state of the extension from old version to new version during update (.mrseq files, .status files)
func CopyStateForUpdate(ctx log.Logger) error {
// Copy .mrseq files (Most Recently executed Sequence number) that helps determine whether a sequence number of Run Command has been previously executed or not.
err := copyFiles(ctx, ".mrseq", "")
if err != nil {
return err
mrseqFilesNameList, mrseqFileCopyErr := copyFiles(ctx, ".mrseq", "")
if mrseqFileCopyErr != nil {
return mrseqFileCopyErr
}

// Copy .status files of already executed sequence numbers
err = copyFiles(ctx, ".status", "status")
if err != nil {
return err
_, statusFileCopyErr := copyFiles(ctx, ".status", constants.StatusFileDirectory)
if statusFileCopyErr != nil {
return statusFileCopyErr
}

// If status file corresponding to a .mrseq file does not exist, create a dummy status file to prevent poll status timeouts for already executed Run Commands after upgrade.
if mrseqFilesNameList != nil && mrseqFilesNameList.Len() > 0 {
// This is best effort - Do not return error if any case of failures.
// Worst case that could happen is poll status timeouts for those few cases where creating dummy status file failed for some reason.
createDummyStatusFilesIfNeeded(ctx, mrseqFilesNameList)
}

return nil
}

// Copy *.mrseq (Most Recently executed Sequence number) files from old extension version to new extension version during update.
func copyFiles(ctx log.Logger, fileExtensionSuffix string, extensionSubdirectory string) error {
// Copy files like *.mrseq (Most Recently executed Sequence number), .status files from old extension version to new extension version during update.
func copyFiles(ctx log.Logger, fileExtensionSuffix string, extensionSubdirectory string) (*list.List, error) {

newExtensionVersion := os.Getenv(constants.ExtensionVersionEnvName)
oldExtensionVersion := os.Getenv(constants.ExtensionVersionUpdatingFromEnvName)
Expand All @@ -368,31 +378,32 @@ func copyFiles(ctx log.Logger, fileExtensionSuffix string, extensionSubdirectory
if err != nil {
errr := os.Mkdir(newExtensionDirectory, 0700)
if errr != nil {
return errors.Wrap(errr, fmt.Sprintf("Failed to create directory '%s'", newExtensionDirectory))
return nil, errors.Wrap(errr, fmt.Sprintf("Failed to create directory '%s'", newExtensionDirectory))
}
}
}

if oldExtensionDirectory == "" || newExtensionDirectory == "" {
return errors.New("oldExtesionDirectory or newExtensionDirectory is empty")
return nil, errors.New("oldExtesionDirectory or newExtensionDirectory is empty")
}

// Check if the directory exists
sourceDirectoryFDRef, err := os.Open(oldExtensionDirectory)
if err != nil {
errMessage := fmt.Sprintf("could not open sourceDirectory %s", oldExtensionDirectory)
ctx.Log("message", errMessage)
return errors.Wrap(err, errMessage)
return nil, errors.Wrap(err, errMessage)
}

directoryEntries, err := sourceDirectoryFDRef.ReadDir(0)
if err != nil {
errMessage := fmt.Sprintf("could not read directory entries from sourceDirectory %s", oldExtensionDirectory)
ctx.Log("message", errMessage)
return errors.Wrap(err, errMessage)
return nil, errors.Wrap(err, errMessage)
}

numberOfFilesMigrated := 0
fileNamesMigrated := list.New()

for _, dirEntry := range directoryEntries {
fileName := dirEntry.Name()
Expand All @@ -405,15 +416,15 @@ func copyFiles(ctx log.Logger, fileExtensionSuffix string, extensionSubdirectory
if sourceFileOpenError != nil {
errMessage := "Failed to open '%s' file '%s' for reading. Contact ICM team AzureRT\\Extensions for this service error."
ctx.Log("message", fmt.Sprintf(errMessage, fileExtensionSuffix, sourceFileFullPath))
return errors.Wrapf(sourceFileOpenError, errMessage)
return fileNamesMigrated, errors.Wrapf(sourceFileOpenError, errMessage)
}
defer sourceFile.Close()

destFile, destFileCreateError := os.Create(destinationFileFullPath)
if destFileCreateError != nil {
errMessage := "Failed to create '%s' file '%s'. Contact ICM team AzureRT\\Extensions for this service error."
ctx.Log("message", fmt.Sprintf(errMessage, fileExtensionSuffix, destinationFileFullPath))
return errors.Wrapf(destFileCreateError, errMessage)
return fileNamesMigrated, errors.Wrapf(destFileCreateError, errMessage)
}
defer destFile.Close()

Expand All @@ -422,17 +433,105 @@ func copyFiles(ctx log.Logger, fileExtensionSuffix string, extensionSubdirectory
errMessage := fmt.Sprintf("Failed to copy '%s' file '%s' to path '%s'. Contact ICM team AzureRT\\Extensions for this service error.",
fileExtensionSuffix, sourceFileFullPath, destinationFileFullPath)
ctx.Log("message", errMessage)
return errors.Wrapf(copyError, errMessage)
return fileNamesMigrated, errors.Wrapf(copyError, errMessage)
} else {
ctx.Log("message", fmt.Sprintf("File '%s' was copied successfully to '%s'", sourceFileFullPath, destinationFileFullPath))
numberOfFilesMigrated++
fileNamesMigrated.PushBack(fileName)
}
}
}

ctx.Log("message", fmt.Sprintf("Migrated %d '%s' files from extension version '%s' to '%s'", numberOfFilesMigrated, fileExtensionSuffix, oldExtensionVersion, newExtensionVersion))

return nil
return fileNamesMigrated, nil
}

// This need to be only executed by Update operation
func createDummyStatusFilesIfNeeded(ctx log.Logger, mrseqFilesNameList *list.List) error {
if mrseqFilesNameList == nil || mrseqFilesNameList.Len() <= 0 {
return nil
}

// Create dummy status file for .mrseq file if status file is not available.
newExtensionDirectory := os.Getenv(constants.ExtensionPathEnvName)
statusFileDirectoryPath := filepath.Join(newExtensionDirectory, constants.StatusFileDirectory)

var mrSeqFileName string
var mrSeqFileFullPath string
var extensionName string
var mrSeqFileExtensionIndex int
var statusFileName string
var statusFilePath string
var errorMessage string
var err error
var content []byte
var allErr error = errors.New("Refer to all error messages above.")

for mreSeqFileNameElement := mrseqFilesNameList.Front(); mreSeqFileNameElement != nil; mreSeqFileNameElement = mreSeqFileNameElement.Next() {
mrSeqFileName = (mreSeqFileNameElement.Value).(string)
vivlingaiah marked this conversation as resolved.
Show resolved Hide resolved

// Read the most recently executed sequence number from the .mrseq file
mrSeqFileFullPath = filepath.Join(newExtensionDirectory, mrSeqFileName)
content, err = os.ReadFile(mrSeqFileFullPath)
if err != nil {
errorMessage = fmt.Sprintf("Reading mrseq (Most Recently executed Sequence number) from file '%s' failed with error '%s'", mrSeqFileFullPath, err.Error())
ctx.Log("error", errorMessage)
allErr = errors.Wrap(allErr, errorMessage)
continue
}

var mrseqNumber int
if content != nil {
mrseqNumberString := string(content)
mrseqNumber, err = strconv.Atoi(mrseqNumberString)
if err != nil {
errorMessage = fmt.Sprintf("mrseqNumberString to mrseqNumber conversion (string to int) of '%s' failed with error '%s'", mrseqNumberString, err.Error())
ctx.Log("error", errorMessage)
allErr = errors.Wrap(allErr, errorMessage)
continue
}
} else {
errorMessage = fmt.Sprintf("Empty .mrseq file content. No sequence number was found inside file '%s' ", mrSeqFileFullPath)
ctx.Log("error", errorMessage)
allErr = errors.Wrap(allErr, errorMessage)
continue
}

// Find extension name from the .mrseq file
mrSeqFileExtensionIndex = strings.Index(mrSeqFileName, constants.MrSeqFileExtension)
if mrSeqFileExtensionIndex == -1 {
errorMessage = fmt.Sprintf("Invalid mrseq file '%s'", mrSeqFileName)
ctx.Log("error", errorMessage)
allErr = errors.Wrap(allErr, errorMessage)
continue
}
extensionName = mrSeqFileName[0:mrSeqFileExtensionIndex]

// Determine status file name and status file path
statusFileName = fmt.Sprintf("%s.%d.status", extensionName, mrseqNumber)
statusFilePath = filepath.Join(statusFileDirectoryPath, statusFileName)

var rootStatusJson []byte
// If status file path does not exist, create a dummy status file to prevent poll status timeouts for already executed Run Commands after upgrade.
if !handlersettings.DoesFileExist(statusFilePath) {
statusReport := types.NewStatusReport(types.StatusWarning, "Enable", "The script has been executed. However, the execution state, output, error are unknown.")
rootStatusJson, err = status.MarshalStatusReportIntoJson(statusReport, true)
if err != nil {
errorMessage = fmt.Sprintf("failed to marshal status report into json for status file '%s' with error '%s'", statusFilePath, err.Error())
allErr = errors.Wrap(allErr, errorMessage)
continue
}

err = status.SaveStatusReport(statusFileDirectoryPath, extensionName, mrseqNumber, rootStatusJson)
if err != nil {
errorMessage = fmt.Sprintf("Failed to create a dummy status file '%s' as it was not existing for .mrseq file '%s' with error '%s'", statusFilePath, mrSeqFileFullPath, err.Error())
allErr = errors.Wrap(allErr, errorMessage)
continue
}
}
}
return allErr
}

// downloadScript downloads the script file specified in cfg into dir (creates if does
Expand Down
34 changes: 19 additions & 15 deletions internal/cmds/cmds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,23 @@ func Test_CopyMrseqFiles_MrseqFilesAreCopied(t *testing.T) {
files, _ := ioutil.ReadDir(currentExtensionVersionDirectory)
require.Equal(t, 0, len(files))

os.Create(filepath.Join(previousExtensionVersionDirectory, "1.mrseq"))
os.Create(filepath.Join(previousExtensionVersionDirectory, "ABCD.mrseq"))
os.Create(filepath.Join(previousExtensionVersionDirectory, "2345.mrseq"))
os.Create(filepath.Join(previousExtensionVersionDirectory, "RC0804_0.mrseq"))
os.Create(filepath.Join(previousExtensionVersionDirectory, "asdfsad.mrseq"))
createMrseqFile(filepath.Join(previousExtensionVersionDirectory, "1.mrseq"), "0", t)
createMrseqFile(filepath.Join(previousExtensionVersionDirectory, "ABCD.mrseq"), "1", t)
createMrseqFile(filepath.Join(previousExtensionVersionDirectory, "2345.mrseq"), "0", t)
createMrseqFile(filepath.Join(previousExtensionVersionDirectory, "RC0804_0.mrseq"), "5", t)
createMrseqFile(filepath.Join(previousExtensionVersionDirectory, "asdfsad.mrseq"), "20", t)
os.Create(filepath.Join(previousExtensionVersionDirectory, "abc.txt")) // this should not be copied to currentExtensionVersionDirectory

statusSubdirectory := "status"
previousStatusDirectory := filepath.Join(previousExtensionVersionDirectory, statusSubdirectory)
// Create previousStatusDirectory
err = os.Mkdir(previousStatusDirectory, 0777)
require.Nil(t, err)
os.Create(filepath.Join(previousStatusDirectory, "1.status"))
os.Create(filepath.Join(previousStatusDirectory, "ABCD.status"))
os.Create(filepath.Join(previousStatusDirectory, "2345.status"))
os.Create(filepath.Join(previousStatusDirectory, "RC0804_0.status"))
os.Create(filepath.Join(previousStatusDirectory, "asdfsad.status"))
os.Create(filepath.Join(previousStatusDirectory, "xyusfd.status"))
os.Create(filepath.Join(previousStatusDirectory, "234434534.status"))

// Only two status files are available. The rest of the 3 status files should be created during Update operation which would have dummy status.
// Dummy status files would be created to prevent poll status timeouts for already executed Run Commands after upgrade.
os.Create(filepath.Join(previousStatusDirectory, "1.0.status"))
os.Create(filepath.Join(previousStatusDirectory, "ABCD.1.status"))
os.Create(filepath.Join(previousStatusDirectory, "abc.cs")) // this should not be copied to currentExtensionVersionDirectory

err = CopyStateForUpdate(log.NewContext(log.NewNopLogger()))
Expand All @@ -93,12 +91,18 @@ func Test_CopyMrseqFiles_MrseqFilesAreCopied(t *testing.T) {

currentStatusDirectory := filepath.Join(currentExtensionVersionDirectory, statusSubdirectory)
files, _ = ioutil.ReadDir(currentStatusDirectory)
require.Equal(t, 7, len(files))
require.Equal(t, 5, len(files))
for _, file := range files {
require.True(t, strings.HasSuffix(file.Name(), ".status"))
}
}

func createMrseqFile(mrseqFilePath string, mrseqNum string, t *testing.T) {
os.Create(mrseqFilePath)
err := os.WriteFile(mrseqFilePath, []byte(mrseqNum), 0644)
require.Nil(t, err)
}

func Test_commandsExist(t *testing.T) {
// we expect these subcommands to be handled
expect := []string{"install", "enable", "disable", "uninstall", "update"}
Expand All @@ -119,8 +123,8 @@ func Test_commands_shouldReportStatus(t *testing.T) {

// these subcommands SHOULD report status
require.True(t, Cmds["enable"].ShouldReportStatus, "enable should report status")
require.True(t, Cmds["disable"].ShouldReportStatus, "disable should report status")
require.True(t, Cmds["update"].ShouldReportStatus, "update should report status")
require.False(t, Cmds["disable"].ShouldReportStatus, "disable should report status")
require.False(t, Cmds["update"].ShouldReportStatus, "update should report status")
}

func Test_checkAndSaveSeqNum_fails(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const (

ConfigFileExtension = ".settings"

MrSeqFileExtension = ".mrseq"

StatusFileDirectory = "status"

// General failed exit code when extension provisioning fails due to service errors.
FailedExitCodeGeneral = -1

Expand Down
19 changes: 12 additions & 7 deletions internal/status/status.go
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func ReportStatusToLocalFile(ctx *log.Context, hEnv types.HandlerEnvironment, me
}

ctx.Log("message", "reporting status by writing status file locally")
err = saveStatusReport(hEnv.HandlerEnvironment.StatusFolder, metadata.ExtName, metadata.SeqNum, rootStatusJson)
err = SaveStatusReport(hEnv.HandlerEnvironment.StatusFolder, metadata.ExtName, metadata.SeqNum, rootStatusJson)
if err != nil {
ctx.Log("event", "failed to save handler status", "error", err)
return errors.Wrap(err, "failed to save handler status")
Expand All @@ -48,7 +48,7 @@ func ReportStatusToLocalFile(ctx *log.Context, hEnv types.HandlerEnvironment, me
// SaveStatusReport persists the status message to the specified status folder using the
// sequence number. The operation consists of writing to a temporary file in the
// same folder and moving it to the final destination for atomicity.
func saveStatusReport(statusFolder string, extName string, seqNo int, rootStatusJson []byte) error {
func SaveStatusReport(statusFolder string, extName string, seqNo int, rootStatusJson []byte) error {
fn := fmt.Sprintf("%d.status", seqNo)
// Support multiconfig extensions where status file name should be: extName.seqNo.status
if extName != "" {
Expand Down Expand Up @@ -77,6 +77,15 @@ func getRootStatusJson(ctx *log.Context, statusType types.StatusType, c types.Cm
ctx.Log("message", "creating json to report status")
statusReport := types.NewStatusReport(statusType, c.Name, msg)

b, err := MarshalStatusReportIntoJson(statusReport, indent)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal status report into json")
}

return b, nil
}

func MarshalStatusReportIntoJson(statusReport types.StatusReport, indent bool) ([]byte, error) {
var b []byte
var err error
if indent {
Expand All @@ -85,11 +94,7 @@ func getRootStatusJson(ctx *log.Context, statusType types.StatusType, c types.Cm
b, err = json.Marshal(statusReport)
}

if err != nil {
return nil, errors.Wrap(err, "failed to marshal status report into json")
}

return b, nil
return b, err
}

func reportStatusToEndpoint(ctx *log.Context, hEnv types.HandlerEnvironment, metadata types.RCMetadata, statusType types.StatusType, c types.Cmd, msg string, reporter statusreporter.IGuestInformationServiceClient) error {
Expand Down
4 changes: 2 additions & 2 deletions internal/types/commands.go
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func (command Cmd) InitializeFunctions(input CmdFunctions) Cmd {
var (
CmdInstallTemplate = Cmd{Name: "Install", ShouldReportStatus: false, FailExitCode: 52}
CmdEnableTemplate = Cmd{Name: "Enable", ShouldReportStatus: true, FailExitCode: 3}
CmdDisableTemplate = Cmd{Name: "Disable", ShouldReportStatus: true, FailExitCode: 3}
CmdUpdateTemplate = Cmd{Name: "Update", ShouldReportStatus: true, FailExitCode: 3}
CmdDisableTemplate = Cmd{Name: "Disable", ShouldReportStatus: false, FailExitCode: 3}
CmdUpdateTemplate = Cmd{Name: "Update", ShouldReportStatus: false, FailExitCode: 3}
CmdUninstallTemplate = Cmd{Name: "Uninstall", ShouldReportStatus: false, FailExitCode: 3}
CmdRunServiceTemplate = Cmd{Name: "RunService", ShouldReportStatus: true, FailExitCode: 3}

Expand Down
4 changes: 4 additions & 0 deletions internal/types/status.go
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const (

// StatusSuccess indicates the operation succeeded
StatusSuccess StatusType = "success"

// StatusWarning indicates the operation was executed, but with one of the below conditions:
// 1) Status files have been lost. So, exact execution status (error or success), output and error are not known.
StatusWarning StatusType = "warning"
)

// Status is used for serializing status in a manner the server understands
Expand Down
Loading