Repository: Azure/WALinuxAgent
Pull request #910 (2017-10-17 16:25:13)

Address various issues
[#908] -- Set Files to 0400 in /var/lib/waagent
[#906] -- Hardcoded value for sshd's ClientAliveInterval (180)
[#899] -- Improve HeartBeat Event
[#898] -- Send dummy status if extension fails to write a #.status file

Signed-off-by: Brendan Dixon <brendandixon@me.com>


Patch:
diff --git a/README.md b/README.md
index 88ad37a5..5611643b 100644
--- a/README.md
+++ b/README.md
@@ -187,6 +187,7 @@ OS.AllowHTTP=n
 OS.RootDeviceScsiTimeout=300
 OS.EnableFIPS=n
 OS.OpensslPath=None
+OS.SshClientAliveInterval=180
 OS.SshDir=/etc/ssh
 HttpProxy.Host=None
 HttpProxy.Port=None
@@ -383,6 +384,12 @@ _Default: None_
 This can be used to specify an alternate path for the openssl binary to use for
 cryptographic operations.
 
+* __OS.SshClientAliveInterval__
+_Type: Integer_
+_Default: 180_
+
+This values sets the number of seconds the agent uses for the SSH ClientAliveInterval configuration option.
+
 * __OS.SshDir__   
 _Type: String_   
 _Default: `/etc/ssh`_   
diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py
index 75a02486..df1ca5b4 100644
--- a/azurelinuxagent/common/conf.py
+++ b/azurelinuxagent/common/conf.py
@@ -131,6 +131,7 @@ __STRING_OPTIONS__ = {
 }
 
 __INTEGER_OPTIONS__ = {
+    "OS.SshClientAliveInterval" : 180,
     "Provisioning.PasswordCryptSaltLength" : 10,
     "HttpProxy.Port" : None,
     "ResourceDisk.SwapSizeMB" : 0,
@@ -190,6 +191,9 @@ def get_fips_enabled(conf=__conf__):
 def get_openssl_cmd(conf=__conf__):
     return conf.get("OS.OpensslPath", "/usr/bin/openssl")
 
+def get_ssh_client_alive_interval(conf=__conf__):
+    return conf.get("OS.SshClientAliveInterval", 180)
+
 def get_ssh_dir(conf=__conf__):
     return conf.get("OS.SshDir", "/etc/ssh")
 
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 8a34b923..dbaf70a7 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -440,7 +440,7 @@ class DefaultOSUtil(object):
         conf_file = fileutil.read_file(conf_file_path).split("\n")
         textutil.set_ssh_config(conf_file, "PasswordAuthentication", option)
         textutil.set_ssh_config(conf_file, "ChallengeResponseAuthentication", option)
-        textutil.set_ssh_config(conf_file, "ClientAliveInterval", "180")
+        textutil.set_ssh_config(conf_file, "ClientAliveInterval", str(conf.get_ssh_client_alive_interval()))
         fileutil.write_file(conf_file_path, "\n".join(conf_file))
         logger.info("{0} SSH password-based authentication methods."
                     .format("Disabled" if disable_password else "Enabled"))
diff --git a/azurelinuxagent/common/protocol/metadata.py b/azurelinuxagent/common/protocol/metadata.py
index 4de7ecfd..ed4fff98 100644
--- a/azurelinuxagent/common/protocol/metadata.py
+++ b/azurelinuxagent/common/protocol/metadata.py
@@ -173,6 +173,11 @@ class MetadataProtocol(Protocol):
             return None
         return self.certs
 
+    def get_incarnation(self):
+        # Always return 0 since Azure Stack does not maintain goal state
+        # incarnation identifiers
+        return 0
+
     def get_vmagent_manifests(self):
         self.update_goal_state()
 
diff --git a/azurelinuxagent/common/protocol/restapi.py b/azurelinuxagent/common/protocol/restapi.py
index 1ec3e211..9f2ccc1c 100644
--- a/azurelinuxagent/common/protocol/restapi.py
+++ b/azurelinuxagent/common/protocol/restapi.py
@@ -300,6 +300,9 @@ class Protocol(DataContract):
     def get_certs(self):
         raise NotImplementedError()
 
+    def get_incarnation(self):
+        raise NotImplementedError()
+
     def get_vmagent_manifests(self):
         raise NotImplementedError()
 
diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py
index 2b4e71af..f4e382fa 100644
--- a/azurelinuxagent/common/protocol/wire.py
+++ b/azurelinuxagent/common/protocol/wire.py
@@ -120,6 +120,13 @@ class WireProtocol(Protocol):
         certificates = self.client.get_certs()
         return certificates.cert_list
 
+    def get_incarnation(self):
+        path = os.path.join(conf.get_lib_dir(), INCARNATION_FILE_NAME)
+        if os.path.exists(path):
+            return fileutil.read_file(path)
+        else:
+            return 0
+
     def get_vmagent_manifests(self):
         # Update goal state to get latest extensions config
         self.update_goal_state()
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index f0a3b098..fae0c666 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -17,6 +17,7 @@
 # Requires Python 2.4+ and Openssl 1.0+
 #
 
+import datetime
 import glob
 import json
 import os
@@ -64,7 +65,6 @@ HANDLER_PKG_EXT = ".zip"
 HANDLER_PKG_PATTERN = re.compile(HANDLER_PATTERN+"\\"+HANDLER_PKG_EXT+"$",
                                 re.IGNORECASE)
 
-
 def validate_has_key(obj, key, fullname):
     if key not in obj:
         raise ExtensionError("Missing: {0}".format(fullname))
@@ -585,7 +585,7 @@ class ExtHandlerInstance(object):
                 status_file = os.path.join(old_ext_status_dir, status_file)
                 if os.path.isfile(status_file):
                     shutil.copy2(status_file, new_ext_status_dir)
-    
+
     def set_operation(self, op):
         self.operation = op
 
@@ -649,8 +649,25 @@ class ExtHandlerInstance(object):
         try:
             status_dir = self.get_status_dir()
             fileutil.mkdir(status_dir, mode=0o700)
+
+            seq_no, status_path = self.get_status_file_path()
+            if seq_no > -1:
+                now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
+                status = {
+                    "version": 1.0,
+                    "timestampUTC" : now,
+                    "status" : {
+                        "name" : self.ext_handler.name,
+                        "operation" : "Enabling Handler",
+                        "status" : "transitioning",
+                        "code" : 0
+                    }
+                }
+                fileutil.write_file(json.dumps(status), status_path)
+
             conf_dir = self.get_conf_dir()
             fileutil.mkdir(conf_dir, mode=0o700)
+
         except IOError as e:
             fileutil.clean_ioerror(e,
                 paths=[self.get_base_dir(), self.pkg_file])
@@ -740,17 +757,24 @@ class ExtHandlerInstance(object):
                     continue
         return seq_no
 
+    def get_status_file_path(self):
+        seq_no = self.get_largest_seq_no()
+        path = None
+
+        if seq_no > -1:
+            path = os.path.join(
+                        self.get_status_dir(),
+                        "{0}.status".format(seq_no))
+
+        return seq_no, path
+
     def collect_ext_status(self, ext):
         self.logger.verbose("Collect extension status")
 
-        seq_no = self.get_largest_seq_no()
+        seq_no, ext_status_file = self.get_status_file_path()
         if seq_no == -1:
             return None
 
-        status_dir = self.get_status_dir()
-        ext_status_file = "{0}.status".format(seq_no)
-        ext_status_file = os.path.join(status_dir, ext_status_file)
-
         ext_status = ExtensionStatus(seq_no=seq_no)
         try:
             data_str = fileutil.read_file(ext_status_file)
diff --git a/azurelinuxagent/ga/monitor.py b/azurelinuxagent/ga/monitor.py
index 307a514a..7304ff1c 100644
--- a/azurelinuxagent/ga/monitor.py
+++ b/azurelinuxagent/ga/monitor.py
@@ -21,8 +21,10 @@ import os
 import platform
 import time
 import threading
+import uuid
 
 import azurelinuxagent.common.conf as conf
+import azurelinuxagent.common.utils.fileutil as fileutil
 import azurelinuxagent.common.logger as logger
 
 from azurelinuxagent.common.event import add_event, WALAEventOperation
@@ -179,15 +181,24 @@ class MonitorHandler(object):
 
     def daemon(self):
         period = datetime.timedelta(minutes=30)
+        protocol = self.protocol_util.get_protocol()        
         last_heartbeat = datetime.datetime.utcnow() - period
+
+        # Create a new identifier on each restart and reset the counter
+        heartbeat_id = str(uuid.uuid4()).upper()
+        counter = 0
         while True:
             if datetime.datetime.utcnow() >= (last_heartbeat + period):
                 last_heartbeat = datetime.datetime.utcnow()
+                incarnation = protocol.get_incarnation()
+                msg = "{0};{1};{2}".format(incarnation, counter, heartbeat_id)
                 add_event(
                     name=AGENT_NAME,
                     version=CURRENT_VERSION,
                     op=WALAEventOperation.HeartBeat,
-                    is_success=True)
+                    is_success=True,
+                    message=msg)
+                counter += 1
             try:
                 self.collect_and_send_events()
             except Exception as e:
diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py
index 467a11cd..79860698 100644
--- a/azurelinuxagent/ga/update.py
+++ b/azurelinuxagent/ga/update.py
@@ -24,6 +24,7 @@ import platform
 import re
 import shutil
 import signal
+import stat
 import subprocess
 import sys
 import time
@@ -76,6 +77,14 @@ ORPHAN_WAIT_INTERVAL = 15 * 60
 
 AGENT_SENTINAL_FILE = "current_version"
 
+READONLY_FILE_GLOBS = [
+    "*.crt",
+    "*.p7m",
+    "*.pem",
+    "*.prv",
+    "ovf-env.xml"
+]
+
 def get_update_handler():
     return UpdateHandler()
 
@@ -251,6 +260,7 @@ class UpdateHandler(object):
             self._ensure_no_orphans()
             self._emit_restart_event()
             self._ensure_partition_assigned()
+            self._ensure_readonly_files()
 
             while self.running:
                 if self._is_orphaned:
@@ -277,6 +287,7 @@ class UpdateHandler(object):
                 exthandlers_handler.run()
 
                 if last_etag != exthandlers_handler.last_etag:
+                    self._ensure_readonly_files()
                     add_event(
                         AGENT_NAME,
                         version=CURRENT_VERSION,
@@ -425,6 +436,11 @@ class UpdateHandler(object):
                 is_success=True,
                 message=partition)
 
+    def _ensure_readonly_files(self):
+        for g in READONLY_FILE_GLOBS:
+            for path in glob.iglob(os.path.join(conf.get_lib_dir(), g)):
+                os.chmod(path, stat.S_IRUSR)
+
     def _evaluate_agent_health(self, latest_agent):
         """
         Evaluate the health of the selected agent: If it is restarting
diff --git a/azurelinuxagent/pa/deprovision/default.py b/azurelinuxagent/pa/deprovision/default.py
index e2c56132..895264a5 100644
--- a/azurelinuxagent/pa/deprovision/default.py
+++ b/azurelinuxagent/pa/deprovision/default.py
@@ -123,6 +123,7 @@ class DeprovisionHandler(object):
         known_files = [
             'HostingEnvironmentConfig.xml',
             'Incarnation',
+            'partition',
             'Protocol',
             'SharedConfig.xml',
             'WireServerEndpoint'
diff --git a/config/waagent.conf b/config/waagent.conf
index 99f54d67..bdba297a 100644
--- a/config/waagent.conf
+++ b/config/waagent.conf
@@ -66,6 +66,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the SSH ClientAliveInterval
+# OS.SshClientAliveInterval=180
+
 # Set the path to SSH keys and configuration files
 OS.SshDir=/etc/ssh
 


Test patch:
diff --git a/tests/common/test_conf.py b/tests/common/test_conf.py
index 93759de1..a2c65733 100644
--- a/tests/common/test_conf.py
+++ b/tests/common/test_conf.py
@@ -48,6 +48,7 @@ class TestConf(AgentTestCase):
         "OS.EnableFIPS" : True,
         "OS.RootDeviceScsiTimeout" : '300',
         "OS.OpensslPath" : '/usr/bin/openssl',
+        "OS.SshClientAliveInterval" : 42,
         "OS.SshDir" : "/notareal/path",
         "HttpProxy.Host" : None,
         "HttpProxy.Port" : None,
diff --git a/tests/data/test_waagent.conf b/tests/data/test_waagent.conf
index edc36769..e35f8c41 100644
--- a/tests/data/test_waagent.conf
+++ b/tests/data/test_waagent.conf
@@ -71,6 +71,9 @@ OS.RootDeviceScsiTimeout=300
 # If "None", the system default version is used.
 OS.OpensslPath=None
 
+# Set the SSH ClientAliveInterval
+OS.SshClientAliveInterval=42
+
 # Set the path to SSH keys and configuration files
 OS.SshDir=/notareal/path
 
diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py
index ceefb8dd..2f9fb400 100644
--- a/tests/ga/test_update.py
+++ b/tests/ga/test_update.py
@@ -21,6 +21,7 @@ from datetime import datetime
 
 import json
 import shutil
+import stat
 
 from azurelinuxagent.common.event import *
 from azurelinuxagent.common.protocol.hostplugin import *
@@ -921,6 +922,47 @@ class TestUpdate(UpdateTestCase):
             self.assertEqual(n, int(s))
             os.remove(path)
 
+    def test_ensure_readonly_sets_readonly(self):
+        test_files = [
+            os.path.join(conf.get_lib_dir(), "faux_certificate.crt"),
+            os.path.join(conf.get_lib_dir(), "faux_certificate.p7m"),
+            os.path.join(conf.get_lib_dir(), "faux_certificate.pem"),
+            os.path.join(conf.get_lib_dir(), "faux_certificate.prv"),
+            os.path.join(conf.get_lib_dir(), "ovf-env.xml")
+        ]
+        for path in test_files:
+            fileutil.write_file(path, "Faux content")
+            os.chmod(path,
+                stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
+
+        self.update_handler._ensure_readonly_files()
+
+        for path in test_files:
+            mode = os.stat(path).st_mode
+            mode &= (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+            self.assertEqual(0, mode ^ stat.S_IRUSR)
+
+    def test_ensure_readonly_leaves_unmodified(self):
+        test_files = [
+            os.path.join(conf.get_lib_dir(), "faux.xml"),
+            os.path.join(conf.get_lib_dir(), "faux.json"),
+            os.path.join(conf.get_lib_dir(), "faux.txt"),
+            os.path.join(conf.get_lib_dir(), "faux")
+        ]
+        for path in test_files:
+            fileutil.write_file(path, "Faux content")
+            os.chmod(path,
+                stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
+
+        self.update_handler._ensure_readonly_files()
+
+        for path in test_files:
+            mode = os.stat(path).st_mode
+            mode &= (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+            self.assertEqual(
+                stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH,
+                mode)
+
     def _test_evaluate_agent_health(self, child_agent_index=0):
         self.prepare_agents()
 
diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py
index d19bab19..93b2c955 100644
--- a/tests/protocol/test_wire.py
+++ b/tests/protocol/test_wire.py
@@ -54,6 +54,8 @@ class TestWireProtocolGetters(AgentTestCase):
             self.assertTrue(os.path.isfile(crt2))
             self.assertTrue(os.path.isfile(prv2))
 
+            self.assertEqual("1", protocol.get_incarnation())
+
     def test_getters(self, *args):
         """Normal case"""
         test_data = WireProtocolData(DATA_FILE)
diff --git a/tests/test_agent.py b/tests/test_agent.py
index 42edda29..51b157dd 100644
--- a/tests/test_agent.py
+++ b/tests/test_agent.py
@@ -45,6 +45,7 @@ OS.HomeDir = /home
 OS.OpensslPath = /usr/bin/openssl
 OS.PasswordPath = /etc/shadow
 OS.RootDeviceScsiTimeout = 300
+OS.SshClientAliveInterval = 42
 OS.SshDir = /notareal/path
 OS.SudoersDir = /etc/sudoers.d
 OS.UpdateRdmaDriver = False