Repository: Azure/WALinuxAgent
Pull request #2051 (2020-10-23 00:15:56)

Fix timestamp for goal state archive
<!-- DO NOT DELETE THIS TEMPLATE -->

## Description

Today, we archive the last processed goal state when a new goal state comes in. However, we erroneously use the *new* goal state timestamp for that archive instead of when we received the *last* processed goal state. This PR fixed the archive timestamp.

<!--
Please add an informative description that covers that changes made by the pull request. 
This checklist is used to make sure that common issues in a pull request are addressed.
This will expedite the process of getting your pull request merged and avoid extra work on your part to fix issues discovered during the review process.
-->

---

### PR information
- [ ] The title of the PR is clear and informative.
- [ ] There are a small number of commits, each of which has an informative message. This means that previously merged commits do not appear in the history of the PR. For information on cleaning up the commits in your pull request, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md).
- [ ] Except for special cases involving multiple contributors, the PR is started from a fork of the main repository, not a branch.
- [ ] If applicable, the PR references the bug/issue that it fixes in the description.
- [ ] New Unit tests were added for the changes made and Travis.CI is passing.

### Quality of Code and Contribution Guidelines
- [ ] I have read the [contribution guidelines](https://github.com/Azure/WALinuxAgent/blob/master/.github/CONTRIBUTING.md).


Patch:
diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py
index 12286e3b..722531dc 100644
--- a/azurelinuxagent/common/protocol/goal_state.py
+++ b/azurelinuxagent/common/protocol/goal_state.py
@@ -25,12 +25,12 @@ import azurelinuxagent.common.logger as logger
 from azurelinuxagent.common.AgentGlobals import AgentGlobals
 from azurelinuxagent.common.datacontract import set_properties
 from azurelinuxagent.common.event import add_event, WALAEventOperation
+from azurelinuxagent.common.exception import IncompleteGoalStateError
 from azurelinuxagent.common.exception import ProtocolError
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.protocol.restapi import Cert, CertList, Extension, ExtHandler, ExtHandlerList, \
     ExtHandlerVersionUri, RemoteAccessUser, RemoteAccessUsersList, \
     VMAgentManifest, VMAgentManifestList, VMAgentManifestUri
-from azurelinuxagent.common.exception import IncompleteGoalStateError
 from azurelinuxagent.common.utils import fileutil
 from azurelinuxagent.common.utils.cryptutil import CryptUtil
 from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib, gettext
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index 005d95da..b788feb9 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -16,14 +16,12 @@
 #
 # Requires Python 2.6+ and Openssl 1.0+
 
-import datetime
 import json
 import os
 import random
 import time
 import traceback
 import xml.sax.saxutils as saxutils
-from datetime import datetime # pylint: disable=ungrouped-imports
 
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.logger as logger
@@ -759,8 +757,7 @@ class WireClient(object): # pylint: disable=R0904
 
     def _save_goal_state(self):
         try:
-            self.goal_state_flusher.flush(datetime.utcnow())
-
+            self.goal_state_flusher.flush()
         except Exception as e: # pylint: disable=C0103
             logger.warn("Failed to save the previous goal state to the history folder: {0}", ustr(e))
 
diff --git a/azurelinuxagent/common/utils/archive.py b/azurelinuxagent/common/utils/archive.py
index 05145ec6..b87f337d 100644
--- a/azurelinuxagent/common/utils/archive.py
+++ b/azurelinuxagent/common/utils/archive.py
@@ -5,13 +5,13 @@ import os
 import re
 import shutil
 import zipfile
-
-from azurelinuxagent.common.utils import fileutil
+from datetime import datetime
 
 import azurelinuxagent.common.logger as logger
-
+from azurelinuxagent.common.utils import fileutil
 
 # pylint: disable=W0105
+
 """
 archive.py
 
@@ -36,76 +36,108 @@ The timestamp is an ISO8601 formatted value.
 """
 # pylint: enable=W0105
 
-ARCHIVE_DIRECTORY_NAME = 'history'
+_ARCHIVE_DIRECTORY_NAME = 'history'
 
-MAX_ARCHIVED_STATES = 50
+_MAX_ARCHIVED_STATES = 50
 
-CACHE_PATTERNS = [
-    re.compile("^(.*)\.(\d+)\.(agentsManifest)$", re.IGNORECASE), # pylint: disable=W1401
-    re.compile("^(.*)\.(\d+)\.(manifest\.xml)$", re.IGNORECASE), # pylint: disable=W1401
-    re.compile("^(.*)\.(\d+)\.(xml)$", re.IGNORECASE) # pylint: disable=W1401
+_CACHE_PATTERNS = [
+    re.compile(r"^(.*)\.(\d+)\.(agentsManifest)$", re.IGNORECASE),
+    re.compile(r"^(.*)\.(\d+)\.(manifest\.xml)$", re.IGNORECASE),
+    re.compile(r"^(.*)\.(\d+)\.(xml)$", re.IGNORECASE)
 ]
 
-# 2018-04-06T08:21:37.142697
-# 2018-04-06T08:21:37.142697.zip
-ARCHIVE_PATTERNS_DIRECTORY = re.compile('^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d+$') # pylint: disable=W1401
-ARCHIVE_PATTERNS_ZIP       = re.compile('^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d+\.zip$') # pylint: disable=W1401
+_GOAL_STATE_PATTERN = re.compile(r"^(.*)/GoalState\.(\d+)\.xml$", re.IGNORECASE)
+
+# Old names didn't have incarnation, new ones do. Ensure the regex captures both cases.
+# 2018-04-06T08:21:37.142697_incarnation_N
+# 2018-04-06T08:21:37.142697_incarnation_N.zip
+_ARCHIVE_PATTERNS_DIRECTORY = re.compile(r"^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(_incarnation_(\d+))?$$")
+_ARCHIVE_PATTERNS_ZIP = re.compile(r"^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(_incarnation_(\d+))?\.zip$")
 
 
 class StateFlusher(object):
     def __init__(self, lib_dir):
         self._source = lib_dir
 
-        d = os.path.join(self._source, ARCHIVE_DIRECTORY_NAME) # pylint: disable=C0103
-        if not os.path.exists(d):
+        directory = os.path.join(self._source, _ARCHIVE_DIRECTORY_NAME)
+        if not os.path.exists(directory):
             try:
-                fileutil.mkdir(d)
-            except OSError as e: # pylint: disable=C0103
-                if e.errno != errno.EEXIST:
-                    logger.error("{0} : {1}", self._source, e.strerror)
+                fileutil.mkdir(directory)
+            except OSError as exception:
+                if exception.errno != errno.EEXIST:
+                    logger.error("{0} : {1}", self._source, exception.strerror)
 
-    def flush(self, timestamp):
+    def flush(self):
         files = self._get_files_to_archive()
-        if len(files) == 0: # pylint: disable=len-as-condition
+        if not files:
+            return
+
+        archive_name = self._get_archive_name(files)
+        if archive_name is None:
             return
 
-        if self._mkdir(timestamp):
-            self._archive(files, timestamp)
+        if self._mkdir(archive_name):
+            self._archive(files, archive_name)
         else:
             self._purge(files)
 
-    def history_dir(self, timestamp):
-        return os.path.join(self._source, ARCHIVE_DIRECTORY_NAME, timestamp.isoformat())
+    def history_dir(self, name):
+        return os.path.join(self._source, _ARCHIVE_DIRECTORY_NAME, name)
+
+    @staticmethod
+    def _get_archive_name(files):
+        """
+        Gets the most recently modified GoalState.*.xml and uses that timestamp and incarnation for the archive name.
+        In a normal workflow, we expect there to be only one GoalState.*.xml at a time, but if the previous one
+        wasn't purged for whatever reason, we take the most recently modified goal state file.
+        If there are no GoalState.*.xml files, we return None.
+        """
+        latest_timestamp_ms = None
+        incarnation = None
+
+        for current_file in files:
+            match = _GOAL_STATE_PATTERN.match(current_file)
+            if not match:
+                continue
+
+            modification_time_ms = os.path.getmtime(current_file)
+            if latest_timestamp_ms is None or latest_timestamp_ms < modification_time_ms:
+                latest_timestamp_ms = modification_time_ms
+                incarnation = match.groups()[1]
+
+        if latest_timestamp_ms is not None and incarnation is not None:
+            return datetime.utcfromtimestamp(latest_timestamp_ms).isoformat() + "_incarnation_{0}".format(incarnation)
+        return None
 
     def _get_files_to_archive(self):
         files = []
-        for f in os.listdir(self._source): # pylint: disable=C0103
-            full_path = os.path.join(self._source, f)
-            for pattern in CACHE_PATTERNS:
-                m = pattern.match(f) # pylint: disable=C0103
-                if m is not None:
+        for current_file in os.listdir(self._source):
+            full_path = os.path.join(self._source, current_file)
+            for pattern in _CACHE_PATTERNS:
+                match = pattern.match(current_file)
+                if match is not None:
                     files.append(full_path)
                     break
 
         return files
 
     def _archive(self, files, timestamp):
-        for f in files: # pylint: disable=C0103
-            dst = os.path.join(self.history_dir(timestamp), os.path.basename(f))
-            shutil.move(f, dst)
+        for current_file in files:
+            dst = os.path.join(self.history_dir(timestamp), os.path.basename(current_file))
+            shutil.move(current_file, dst)
 
     def _purge(self, files):
-        for f in files: # pylint: disable=C0103
-            os.remove(f)
+        for current_file in files:
+            os.remove(current_file)
 
-    def _mkdir(self, timestamp):
-        d = self.history_dir(timestamp) # pylint: disable=C0103
+    def _mkdir(self, name):
+        directory = self.history_dir(name)
 
         try:
-            fileutil.mkdir(d, mode=0o700)
+            fileutil.mkdir(directory, mode=0o700)
             return True
-        except IOError as e: # pylint: disable=C0103
-            logger.error("{0} : {1}".format(d, e.strerror))
+        except IOError as exception:
+            logger.error("{0} : {1}".format(directory, exception.strerror))
             return False
 
 
@@ -148,45 +180,39 @@ class State(object):
 
 
 class StateZip(State):
-    def __init__(self, path, timestamp): # pylint: disable=W0235
-        super(StateZip,self).__init__(path, timestamp)
-
     def delete(self):
         os.remove(self._path)
 
 
 class StateDirectory(State):
-    def __init__(self, path, timestamp): # pylint: disable=W0235
-        super(StateDirectory, self).__init__(path, timestamp)
-
     def delete(self):
         shutil.rmtree(self._path)
 
     def archive(self):
         fn_tmp = "{0}.zip.tmp".format(self._path)
-        fn = "{0}.zip".format(self._path) # pylint: disable=C0103
+        filename = "{0}.zip".format(self._path)
 
         ziph = zipfile.ZipFile(fn_tmp, 'w')
-        for f in os.listdir(self._path): # pylint: disable=C0103
-            full_path = os.path.join(self._path, f)
-            ziph.write(full_path, f, zipfile.ZIP_DEFLATED)
+        for current_file in os.listdir(self._path):
+            full_path = os.path.join(self._path, current_file)
+            ziph.write(full_path, current_file, zipfile.ZIP_DEFLATED)
 
         ziph.close()
 
-        os.rename(fn_tmp, fn)
+        os.rename(fn_tmp, filename)
         shutil.rmtree(self._path)
 
 
 class StateArchiver(object):
     def __init__(self, lib_dir):
-        self._source = os.path.join(lib_dir, ARCHIVE_DIRECTORY_NAME)
+        self._source = os.path.join(lib_dir, _ARCHIVE_DIRECTORY_NAME)
 
         if not os.path.isdir(self._source):
             try:
                 fileutil.mkdir(self._source, mode=0o700)
-            except IOError as e: # pylint: disable=C0103
-                if e.errno != errno.EEXIST:
-                    logger.error("{0} : {1}", self._source, e.strerror)
+            except IOError as exception:
+                if exception.errno != errno.EEXIST:
+                    logger.error("{0} : {1}", self._source, exception.strerror)
 
     def purge(self):
         """
@@ -197,7 +223,7 @@ class StateArchiver(object):
         states = self._get_archive_states()
         states.sort(reverse=True)
 
-        for state in states[MAX_ARCHIVED_STATES:]:
+        for state in states[_MAX_ARCHIVED_STATES:]:
             state.delete()
 
     def archive(self):
@@ -207,14 +233,14 @@ class StateArchiver(object):
 
     def _get_archive_states(self):
         states = []
-        for f in os.listdir(self._source): # pylint: disable=C0103
-            full_path = os.path.join(self._source, f)
-            m = ARCHIVE_PATTERNS_DIRECTORY.match(f) # pylint: disable=C0103
-            if m is not None:
-                states.append(StateDirectory(full_path, m.group(0)))
-
-            m = ARCHIVE_PATTERNS_ZIP.match(f) # pylint: disable=C0103
-            if m is not None:
-                states.append(StateZip(full_path, m.group(0)))
+        for current_file in os.listdir(self._source):
+            full_path = os.path.join(self._source, current_file)
+            match = _ARCHIVE_PATTERNS_DIRECTORY.match(current_file)
+            if match is not None:
+                states.append(StateDirectory(full_path, match.group(0)))
+
+            match = _ARCHIVE_PATTERNS_ZIP.match(current_file)
+            if match is not None:
+                states.append(StateZip(full_path, match.group(0)))
 
         return states


Test patch:
diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py
index ab8ddfd2..3918c68f 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -24,7 +24,9 @@ import socket
 import time
 import unittest
 import uuid
+from datetime import datetime, timedelta
 
+import azurelinuxagent.common.conf as conf
 from azurelinuxagent.common.exception import IncompleteGoalStateError
 from azurelinuxagent.common.exception import InvalidContainerError, ResourceGoneError, ProtocolError, \
     ExtensionDownloadError, HttpError
@@ -1053,8 +1055,47 @@ class UpdateGoalStateTestCase(AgentTestCase):
 
             self.assertEqual(protocol.client.get_host_plugin().container_id, new_container_id)
             self.assertEqual(protocol.client.get_host_plugin().role_config_name, new_role_config_name)
+
+    def test_update_goal_state_should_archive_last_goal_state(self):
+        # We use the last modified timestamp of the goal state to be archived to determine the archive's name.
+        mock_mtime = os.path.getmtime(self.tmp_dir)
+        with patch("azurelinuxagent.common.utils.archive.os.path.getmtime") as patch_mtime:
+            first_gs_ms = mock_mtime + timedelta(minutes=5).seconds
+            second_gs_ms = mock_mtime + timedelta(minutes=10).seconds
+            third_gs_ms = mock_mtime + timedelta(minutes=15).seconds
+
+            patch_mtime.side_effect = [first_gs_ms, second_gs_ms, third_gs_ms]
+
+            # The first goal state is created when we instantiate the protocol
+            with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol:
+                history_dir = os.path.join(conf.get_lib_dir(), "history")
+                archives = os.listdir(history_dir)
+                self.assertEqual(len(archives), 0, "The goal state archive should have been empty since this is the first goal state")
+
+                # Create the second new goal state, so the initial one should be archived
+                protocol.mock_wire_data.set_incarnation("2")
+                protocol.client.update_goal_state()
+
+                # The initial goal state should be in the archive
+                first_archive_name = datetime.utcfromtimestamp(first_gs_ms).isoformat() + "_incarnation_1"
+                archives = os.listdir(history_dir)
+                self.assertEqual(len(archives), 1, "Only one goal state should have been archived")
+                self.assertEqual(archives[0], first_archive_name, "The name of goal state archive should match the first goal state timestamp and incarnation")
+
+                # Create the third goal state, so the second one should be archived too
+                protocol.mock_wire_data.set_incarnation("3")
+                protocol.client.update_goal_state()
+
+                # The second goal state should be in the archive
+                second_archive_name = datetime.utcfromtimestamp(second_gs_ms).isoformat() + "_incarnation_2"
+                archives = os.listdir(history_dir)
+                archives.sort()
+                self.assertEqual(len(archives), 2, "Two goal states should have been archived")
+                self.assertEqual(archives[1], second_archive_name, "The name of goal state archive should match the second goal state timestamp and incarnation")
+
 # pylint: enable=too-many-public-methods
 
+
 class TryUpdateGoalStateTestCase(HttpRequestPredicates, AgentTestCase):
     """
     Tests for WireClient.try_update_goal_state()
diff --git a/tests/utils/test_archive.py b/tests/utils/test_archive.py
index 40e9f103..818a4ea4 100644
--- a/tests/utils/test_archive.py
+++ b/tests/utils/test_archive.py
@@ -1,16 +1,15 @@
 # Copyright (c) Microsoft Corporation. All rights reserved.
 # Licensed under the Apache License.
-from datetime import datetime, timedelta
-
-import zipfile
 import os
 import shutil
 import tempfile
+import zipfile
+from datetime import datetime, timedelta
 
 import azurelinuxagent.common.logger as logger
 from azurelinuxagent.common.utils import fileutil
-from azurelinuxagent.common.utils.archive import StateFlusher, StateArchiver, MAX_ARCHIVED_STATES
-from tests.tools import AgentTestCase
+from azurelinuxagent.common.utils.archive import StateFlusher, StateArchiver, _MAX_ARCHIVED_STATES
+from tests.tools import AgentTestCase, patch
 
 debug = False # pylint: disable=invalid-name
 if os.environ.get('DEBUG') == '1':
@@ -32,35 +31,45 @@ class TestArchive(AgentTestCase):
         if not debug and self.tmp_dir is not None:
             shutil.rmtree(self.tmp_dir)
 
-    def _write_file(self, fn, contents=None): # pylint: disable=invalid-name
-        full_name = os.path.join(self.tmp_dir, fn)
+    def _write_file(self, filename, contents=None):
+        full_name = os.path.join(self.tmp_dir, filename)
         fileutil.mkdir(os.path.dirname(full_name))
 
-        with open(full_name, 'w') as fh: # pylint: disable=invalid-name
-            data = contents if contents is not None else fn
-            fh.write(data)
+        with open(full_name, 'w') as file_handler:
+            data = contents if contents is not None else filename
+            file_handler.write(data)
             return full_name
 
     @property
     def history_dir(self):
         return os.path.join(self.tmp_dir, 'history')
 
+    @staticmethod
+    def _parse_archive_name(name):
+        # Name can be a directory or a zip
+        # '0000-00-00T00:00:00.000000_incarnation_0'
+        # '0000-00-00T00:00:00.000000_incarnation_0.zip'
+        timestamp_str, incarnation_ext = name.split("_incarnation_")
+        incarnation_no_ext = os.path.splitext(incarnation_ext)[0]
+        return timestamp_str, incarnation_no_ext
+
     def test_archive00(self):
         """
         StateFlusher should move all 'goal state' files to a new directory
         under the history folder that is timestamped.
         """
         temp_files = [
+            'GoalState.0.xml',
             'Prod.0.manifest.xml',
             'Prod.0.agentsManifest',
             'Microsoft.Azure.Extensions.CustomScript.0.xml'
         ]
 
-        for f in temp_files: # pylint: disable=invalid-name
-            self._write_file(f)
+        for temp_file in temp_files:
+            self._write_file(temp_file)
 
         test_subject = StateFlusher(self.tmp_dir)
-        test_subject.flush(datetime.utcnow())
+        test_subject.flush()
 
         self.assertTrue(os.path.exists(self.history_dir))
         self.assertTrue(os.path.isdir(self.history_dir))
@@ -68,12 +77,14 @@ class TestArchive(AgentTestCase):
         timestamp_dirs = os.listdir(self.history_dir)
         self.assertEqual(1, len(timestamp_dirs))
 
-        self.assertIsIso8601(timestamp_dirs[0])
-        ts = self.parse_isoformat(timestamp_dirs[0]) # pylint: disable=invalid-name
-        self.assertDateTimeCloseTo(ts, datetime.utcnow(), timedelta(seconds=30))
+        timestamp_str, incarnation = self._parse_archive_name(timestamp_dirs[0])
+        self.assert_is_iso8601(timestamp_str)
+        timestamp = self.parse_isoformat(timestamp_str)
+        self.assert_datetime_close_to(timestamp, datetime.utcnow(), timedelta(seconds=30))
+        self.assertEqual("0", incarnation)
 
-        for f in temp_files: # pylint: disable=invalid-name
-            history_path = os.path.join(self.history_dir, timestamp_dirs[0], f)
+        for temp_file in temp_files:
+            history_path = os.path.join(self.history_dir, timestamp_dirs[0], temp_file)
             msg = "expected the temp file {0} to exist".format(history_path)
             self.assertTrue(os.path.exists(history_path), msg)
 
@@ -86,16 +97,17 @@ class TestArchive(AgentTestCase):
           2. Deleting the timestamped directory
         """
         temp_files = [
+            'GoalState.0.xml',
             'Prod.0.manifest.xml',
             'Prod.0.agentsManifest',
             'Microsoft.Azure.Extensions.CustomScript.0.xml'
         ]
 
-        for f in temp_files: # pylint: disable=invalid-name
-            self._write_file(f)
+        for current_file in temp_files:
+            self._write_file(current_file)
 
         flusher = StateFlusher(self.tmp_dir)
-        flusher.flush(datetime.utcnow())
+        flusher.flush()
 
         test_subject = StateArchiver(self.tmp_dir)
         test_subject.archive()
@@ -103,15 +115,16 @@ class TestArchive(AgentTestCase):
         timestamp_zips = os.listdir(self.history_dir)
         self.assertEqual(1, len(timestamp_zips))
 
-        zip_fn = timestamp_zips[0]          # 2000-01-01T00:00:00.000000.zip
-        ts_s = os.path.splitext(zip_fn)[0]  # 2000-01-01T00:00:00.000000
+        zip_fn = timestamp_zips[0]  # 2000-01-01T00:00:00.000000_incarnation_N.zip
+        timestamp_str, incarnation = self._parse_archive_name(zip_fn)
 
-        self.assertIsIso8601(ts_s)
-        ts = self.parse_isoformat(ts_s) # pylint: disable=invalid-name
-        self.assertDateTimeCloseTo(ts, datetime.utcnow(), timedelta(seconds=30))
+        self.assert_is_iso8601(timestamp_str)
+        timestamp = self.parse_isoformat(timestamp_str)
+        self.assert_datetime_close_to(timestamp, datetime.utcnow(), timedelta(seconds=30))
+        self.assertEqual("0", incarnation)
 
         zip_full = os.path.join(self.history_dir, zip_fn)
-        self.assertZipContains(zip_full, temp_files)
+        self.assert_zip_contains(zip_full, temp_files)
 
     def test_archive02(self):
         """
@@ -124,21 +137,21 @@ class TestArchive(AgentTestCase):
         directories are properly deleted from the disk.
         """
         count = 6
-        total = MAX_ARCHIVED_STATES + count
+        total = _MAX_ARCHIVED_STATES + count
 
         start = datetime.now()
         timestamps = []
 
         for i in range(0, total):
-            ts = start + timedelta(seconds=i) # pylint: disable=invalid-name
-            timestamps.append(ts)
+            timestamp = start + timedelta(seconds=i)
+            timestamps.append(timestamp)
 
             if i % 2 == 0:
-                fn = os.path.join('history', ts.isoformat(), 'Prod.0.manifest.xml') # pylint: disable=invalid-name
+                filename = os.path.join('history', "{0}_incarnation_0".format(timestamp.isoformat()), 'Prod.0.manifest.xml')
             else:
-                fn = os.path.join('history', "{0}.zip".format(ts.isoformat())) # pylint: disable=invalid-name
+                filename = os.path.join('history', "{0}_incarnation_0.zip".format(timestamp.isoformat()))
 
-            self._write_file(fn)
+            self._write_file(filename)
 
         self.assertEqual(total, len(os.listdir(self.history_dir)))
 
@@ -146,69 +159,45 @@ class TestArchive(AgentTestCase):
         test_subject.purge()
 
         archived_entries = os.listdir(self.history_dir)
-        self.assertEqual(MAX_ARCHIVED_STATES, len(archived_entries))
+        self.assertEqual(_MAX_ARCHIVED_STATES, len(archived_entries))
 
         archived_entries.sort()
 
-        for i in range(0, MAX_ARCHIVED_STATES):
-            ts = timestamps[i + count].isoformat() # pylint: disable=invalid-name
+        for i in range(0, _MAX_ARCHIVED_STATES):
+            timestamp = timestamps[i + count].isoformat()
             if i % 2 == 0:
-                fn = ts # pylint: disable=invalid-name
+                filename = "{0}_incarnation_0".format(timestamp)
             else:
-                fn = "{0}.zip".format(ts) # pylint: disable=invalid-name
-            self.assertTrue(fn in archived_entries, "'{0}' is not in the list of unpurged entires".format(fn))
+                filename = "{0}_incarnation_0.zip".format(timestamp)
+            self.assertTrue(filename in archived_entries, "'{0}' is not in the list of unpurged entires".format(filename))
 
     def test_archive03(self):
         """
-        If the StateFlusher has to flush the same file, it should
-        overwrite the existing one.
+        All archives should be purged, both with the new naming (with incarnation number) and with the old naming.
         """
-        temp_files = [
-            'Prod.0.manifest.xml',
-            'Prod.0.agentsManifest',
-            'Microsoft.Azure.Extensions.CustomScript.0.xml'
-        ]
-
-        def _write_goal_state_files(temp_files, content=None):
-            for f in temp_files: # pylint: disable=invalid-name
-                self._write_file(f, content)
+        start = datetime.now()
+        timestamp1 = start + timedelta(seconds=5)
+        timestamp2 = start + timedelta(seconds=10)
 
-        def _check_history_files(timestamp_dir, files, content=None):
-            for f in files: # pylint: disable=invalid-name
-                history_path = os.path.join(self.history_dir, timestamp_dir, f)
-                msg = "expected the temp file {0} to exist".format(history_path)
-                self.assertTrue(os.path.exists(history_path), msg)
-                expected_content = f if content is None else content
-                actual_content = fileutil.read_file(history_path)
-                self.assertEqual(expected_content, actual_content)
+        dir_old = timestamp1.isoformat()
+        dir_new = "{0}_incarnation_1".format(timestamp2.isoformat())
 
-        timestamp = datetime.utcnow()
+        archive_old = "{0}.zip".format(timestamp1.isoformat())
+        archive_new = "{0}_incarnation_1.zip".format(timestamp2.isoformat())
 
-        _write_goal_state_files(temp_files)
-        test_subject = StateFlusher(self.tmp_dir)
-        test_subject.flush(timestamp)
+        self._write_file(os.path.join("history", dir_old, "Prod.0.manifest.xml"))
+        self._write_file(os.path.join("history", dir_new, "Prod.1.manifest.xml"))
+        self._write_file(os.path.join("history", archive_old))
+        self._write_file(os.path.join("history", archive_new))
 
-        # Ensure history directory exists, has proper timestamped-based name,
-        self.assertTrue(os.path.exists(self.history_dir))
-        self.assertTrue(os.path.isdir(self.history_dir))
+        self.assertEqual(4, len(os.listdir(self.history_dir)), "Not all entries were archived!")
 
-        timestamp_dirs = os.listdir(self.history_dir)
-        self.assertEqual(1, len(timestamp_dirs))
-
-        self.assertIsIso8601(timestamp_dirs[0])
-        ts = self.parse_isoformat(timestamp_dirs[0]) # pylint: disable=invalid-name
-        self.assertDateTimeCloseTo(ts, datetime.utcnow(), timedelta(seconds=30))
-
-        # Ensure saved files contain the right content
-        _check_history_files(timestamp_dirs[0], temp_files)
-
-        # re-write all of the same files with different content, and flush again.
-        # .flush() should overwrite the existing ones
-        _write_goal_state_files(temp_files, "--this-has-been-changed--")
-        test_subject.flush(timestamp)
+        test_subject = StateArchiver(self.tmp_dir)
+        with patch("azurelinuxagent.common.utils.archive._MAX_ARCHIVED_STATES", 0):
+            test_subject.purge()
 
-        # The contents of the saved files were overwritten as a result of the flush.
-        _check_history_files(timestamp_dirs[0], temp_files, "--this-has-been-changed--")
+        archived_entries = os.listdir(self.history_dir)
+        self.assertEqual(0, len(archived_entries), "Not all entries were purged!")
 
     def test_archive04(self):
         """
@@ -219,35 +208,38 @@ class TestArchive(AgentTestCase):
         test_subject = StateArchiver(os.path.join(self.tmp_dir, 'does-not-exist'))
         test_subject.purge()
 
-    def parse_isoformat(self, s): # pylint: disable=invalid-name
-        return datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f')
+    @staticmethod
+    def parse_isoformat(timestamp_str):
+        return datetime.strptime(timestamp_str, '%Y-%m-%dT%H:%M:%S.%f')
 
-    def assertIsIso8601(self, s): # pylint: disable=invalid-name
+    @staticmethod
+    def assert_is_iso8601(timestamp_str):
         try:
-            self.parse_isoformat(s)
+            TestArchive.parse_isoformat(timestamp_str)
         except:
-            raise AssertionError("the value '{0}' is not an ISO8601 formatted timestamp".format(s))
+            raise AssertionError("the value '{0}' is not an ISO8601 formatted timestamp".format(timestamp_str))
 
-    def _total_seconds(self, td): # pylint: disable=invalid-name
+    @staticmethod
+    def _total_seconds(delta):
         """
         Compute the total_seconds for a timedelta because 2.6 does not have total_seconds.
         """
-        return (0.0 + td.microseconds + (td.seconds + td.days * 24 * 60 * 60) * 10 ** 6) / 10 ** 6
+        return (0.0 + delta.microseconds + (delta.seconds + delta.days * 24 * 60 * 60) * 10 ** 6) / 10 ** 6
 
-    def assertDateTimeCloseTo(self, t1, t2, within): # pylint: disable=invalid-name
-        if t1 <= t2:
-            diff = t2 -t1
+    def assert_datetime_close_to(self, time1, time2, within):
+        if time1 <= time2:
+            diff = time2 - time1
         else:
-            diff = t1 - t2
+            diff = time1 - time2
 
         secs = self._total_seconds(within - diff)
         if secs < 0:
             self.fail("the timestamps are outside of the tolerance of by {0} seconds".format(secs))
 
-    def assertZipContains(self, zip_fn, files): # pylint: disable=invalid-name
-        ziph = zipfile.ZipFile(zip_fn, 'r')
+    def assert_zip_contains(self, zip_filename, files):
+        ziph = zipfile.ZipFile(zip_filename, 'r')
         zip_files = [x.filename for x in ziph.filelist]
-        for f in files: # pylint: disable=invalid-name
-            self.assertTrue(f in zip_files, "'{0}' was not found in {1}".format(f, zip_fn))
+        for current_file in files:
+            self.assertTrue(current_file in zip_files, "'{0}' was not found in {1}".format(current_file, zip_filename))
 
         ziph.close()