summaryrefslogtreecommitdiff
path: root/azurelinuxagent
diff options
context:
space:
mode:
authorBen Howard <ben.howard@ubuntu.com>2015-08-14 16:40:41 -0600
committerusd-importer <ubuntu-server@lists.ubuntu.com>2015-08-15 14:33:21 +0000
commitf78b9650d0e7b008d430673a075aad95dda863be (patch)
treea6749619e78483d45a66d4bad4d6e922391541fc /azurelinuxagent
parent0afc048f2a6ff3638ecfa33e7ded5dc8dddf041a (diff)
downloadvyos-walinuxagent-f78b9650d0e7b008d430673a075aad95dda863be.tar.gz
vyos-walinuxagent-f78b9650d0e7b008d430673a075aad95dda863be.zip
Import patches-unapplied version 2.1.1-0ubuntu1 to ubuntu/wily-proposed
Imported using git-ubuntu import. Changelog parent: 0afc048f2a6ff3638ecfa33e7ded5dc8dddf041a New changelog entries: * New upstream release for Ubuntu. - Switch to Python3 - Applies Ubuntu specific patches
Diffstat (limited to 'azurelinuxagent')
-rw-r--r--azurelinuxagent/__init__.py17
-rw-r--r--azurelinuxagent/agent.py141
-rw-r--r--azurelinuxagent/conf.py109
-rw-r--r--azurelinuxagent/distro/__init__.py19
-rw-r--r--azurelinuxagent/distro/centos/__init__.py19
-rw-r--r--azurelinuxagent/distro/centos/loader.py25
-rw-r--r--azurelinuxagent/distro/coreos/__init__.py18
-rw-r--r--azurelinuxagent/distro/coreos/deprovision.py30
-rw-r--r--azurelinuxagent/distro/coreos/handlerFactory.py27
-rw-r--r--azurelinuxagent/distro/coreos/loader.py28
-rw-r--r--azurelinuxagent/distro/coreos/osutil.py90
-rw-r--r--azurelinuxagent/distro/debian/__init__.py19
-rw-r--r--azurelinuxagent/distro/debian/loader.py24
-rw-r--r--azurelinuxagent/distro/debian/osutil.py47
-rw-r--r--azurelinuxagent/distro/default/__init__.py19
-rw-r--r--azurelinuxagent/distro/default/deprovision.py117
-rw-r--r--azurelinuxagent/distro/default/dhcp.py330
-rw-r--r--azurelinuxagent/distro/default/env.py115
-rw-r--r--azurelinuxagent/distro/default/extension.py647
-rw-r--r--azurelinuxagent/distro/default/handlerFactory.py40
-rw-r--r--azurelinuxagent/distro/default/init.py49
-rw-r--r--azurelinuxagent/distro/default/loader.py28
-rw-r--r--azurelinuxagent/distro/default/osutil.py657
-rw-r--r--azurelinuxagent/distro/default/provision.py165
-rw-r--r--azurelinuxagent/distro/default/resourceDisk.py166
-rw-r--r--azurelinuxagent/distro/default/run.py86
-rw-r--r--azurelinuxagent/distro/default/scvmm.py47
-rw-r--r--azurelinuxagent/distro/loader.py46
-rw-r--r--azurelinuxagent/distro/oracle/__init__.py19
-rw-r--r--azurelinuxagent/distro/oracle/loader.py25
-rw-r--r--azurelinuxagent/distro/redhat/__init__.py19
-rw-r--r--azurelinuxagent/distro/redhat/loader.py28
-rw-r--r--azurelinuxagent/distro/redhat/osutil.py148
-rw-r--r--azurelinuxagent/distro/suse/__init__.py19
-rw-r--r--azurelinuxagent/distro/suse/loader.py29
-rw-r--r--azurelinuxagent/distro/suse/osutil.py88
-rw-r--r--azurelinuxagent/distro/ubuntu/__init__.py19
-rw-r--r--azurelinuxagent/distro/ubuntu/deprovision.py43
-rw-r--r--azurelinuxagent/distro/ubuntu/handlerFactory.py29
-rw-r--r--azurelinuxagent/distro/ubuntu/loader.py36
-rw-r--r--azurelinuxagent/distro/ubuntu/osutil.py65
-rw-r--r--azurelinuxagent/distro/ubuntu/provision.py72
-rw-r--r--azurelinuxagent/event.py188
-rw-r--r--azurelinuxagent/exception.py65
-rw-r--r--azurelinuxagent/future.py19
-rw-r--r--azurelinuxagent/handler.py28
-rw-r--r--azurelinuxagent/logger.py158
-rw-r--r--azurelinuxagent/metadata.py93
-rw-r--r--azurelinuxagent/protocol/__init__.py23
-rw-r--r--azurelinuxagent/protocol/common.py245
-rw-r--r--azurelinuxagent/protocol/ovfenv.py146
-rw-r--r--azurelinuxagent/protocol/protocolFactory.py114
-rw-r--r--azurelinuxagent/protocol/v1.py964
-rw-r--r--azurelinuxagent/protocol/v2.py122
-rw-r--r--azurelinuxagent/utils/__init__.py19
-rw-r--r--azurelinuxagent/utils/fileutil.py186
-rw-r--r--azurelinuxagent/utils/osutil.py27
-rw-r--r--azurelinuxagent/utils/restutil.py154
-rw-r--r--azurelinuxagent/utils/shellutil.py85
-rw-r--r--azurelinuxagent/utils/textutil.py228
60 files changed, 6628 insertions, 0 deletions
diff --git a/azurelinuxagent/__init__.py b/azurelinuxagent/__init__.py
new file mode 100644
index 0000000..1ea2f38
--- /dev/null
+++ b/azurelinuxagent/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/agent.py b/azurelinuxagent/agent.py
new file mode 100644
index 0000000..5e61a6c
--- /dev/null
+++ b/azurelinuxagent/agent.py
@@ -0,0 +1,141 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+"""
+Module agent
+"""
+
+import os
+import sys
+import re
+import subprocess
+from azurelinuxagent.metadata import AGENT_NAME, AGENT_LONG_VERSION, \
+ DISTRO_NAME, DISTRO_VERSION, \
+ PY_VERSION_MAJOR, PY_VERSION_MINOR, \
+ PY_VERSION_MICRO
+from azurelinuxagent.utils.osutil import OSUTIL
+from azurelinuxagent.handler import HANDLERS
+
+
+def init(verbose):
+ """
+ Initialize agent running environment.
+ """
+ HANDLERS.init_handler.init(verbose)
+
+def run():
+ """
+ Run agent daemon
+ """
+ HANDLERS.main_handler.run()
+
+def deprovision(force=False, deluser=False):
+ """
+ Run deprovision command
+ """
+ HANDLERS.deprovision_handler.deprovision(force=force, deluser=deluser)
+
+def parse_args(sys_args):
+ """
+ Parse command line arguments
+ """
+ cmd = "help"
+ force = False
+ verbose = False
+ for a in sys_args:
+ if re.match("^([-/]*)deprovision\\+user", a):
+ cmd = "deprovision+user"
+ elif re.match("^([-/]*)deprovision", a):
+ cmd = "deprovision"
+ elif re.match("^([-/]*)daemon", a):
+ cmd = "daemon"
+ elif re.match("^([-/]*)start", a):
+ cmd = "start"
+ elif re.match("^([-/]*)register-service", a):
+ cmd = "register-service"
+ elif re.match("^([-/]*)version", a):
+ cmd = "version"
+ elif re.match("^([-/]*)verbose", a):
+ verbose = True
+ elif re.match("^([-/]*)force", a):
+ force = True
+ elif re.match("^([-/]*)(help|usage|\\?)", a):
+ cmd = "help"
+ else:
+ cmd = "help"
+ break
+ return cmd, force, verbose
+
+def version():
+ """
+ Show agent version
+ """
+ print(("{0} running on {1} {2}".format(AGENT_LONG_VERSION, DISTRO_NAME,
+ DISTRO_VERSION)))
+ print("Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR,
+ PY_VERSION_MICRO))
+def usage():
+ """
+ Show agent usage
+ """
+ print("")
+ print((("usage: {0} [-verbose] [-force] [-help]"
+ "-deprovision[+user]|-register-service|-version|-daemon|-start]"
+ "").format(sys.argv[0])))
+ print("")
+
+def start():
+ """
+ Start agent daemon in a background process and set stdout/stderr to
+ /dev/null
+ """
+ devnull = open(os.devnull, 'w')
+ subprocess.Popen([sys.argv[0], '-daemon'], stdout=devnull, stderr=devnull)
+
+def register_service():
+ """
+ Register agent as a service
+ """
+ print("Register {0} service".format(AGENT_NAME))
+ OSUTIL.register_agent_service()
+ print("Start {0} service".format(AGENT_NAME))
+ OSUTIL.start_agent_service()
+
+def main():
+ """
+ Parse command line arguments, exit with usage() on error.
+ Invoke different methods according to different command
+ """
+ command, force, verbose = parse_args(sys.argv[1:])
+ if command == "version":
+ version()
+ elif command == "help":
+ usage()
+ else:
+ init(verbose)
+ if command == "deprovision+user":
+ deprovision(force, deluser=True)
+ elif command == "deprovision":
+ deprovision(force, deluser=False)
+ elif command == "start":
+ start()
+ elif command == "register-service":
+ register_service()
+ elif command == "daemon":
+ run()
diff --git a/azurelinuxagent/conf.py b/azurelinuxagent/conf.py
new file mode 100644
index 0000000..3185d99
--- /dev/null
+++ b/azurelinuxagent/conf.py
@@ -0,0 +1,109 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+"""
+Module conf loads and parses configuration file
+"""
+import os
+import azurelinuxagent.utils.fileutil as fileutil
+from azurelinuxagent.exception import AgentConfigError
+
+class ConfigurationProvider(object):
+ """
+ Parse amd store key:values in /etc/waagent.conf.
+ """
+ def __init__(self):
+ self.values = dict()
+
+ def load(self, content):
+ if not content:
+ raise AgentConfigError("Can't not parse empty configuration")
+ for line in content.split('\n'):
+ if not line.startswith("#") and "=" in line:
+ parts = line.split()[0].split('=')
+ value = parts[1].strip("\" ")
+ if value != "None":
+ self.values[parts[0]] = value
+ else:
+ self.values[parts[0]] = None
+
+ def get(self, key, default_val=None):
+ val = self.values.get(key)
+ return val if val is not None else default_val
+
+ def get_switch(self, key, default_val=False):
+ val = self.values.get(key)
+ if val is not None and val.lower() == 'y':
+ return True
+ elif val is not None and val.lower() == 'n':
+ return False
+ return default_val
+
+ def get_int(self, key, default_val=-1):
+ try:
+ return int(self.values.get(key))
+ except TypeError:
+ return default_val
+ except ValueError:
+ return default_val
+
+
+__config__ = ConfigurationProvider()
+
+def load_conf(conf_file_path, conf=__config__):
+ """
+ Load conf file from: conf_file_path
+ """
+ if os.path.isfile(conf_file_path) == False:
+ raise AgentConfigError(("Missing configuration in {0}"
+ "").format(conf_file_path))
+ try:
+ content = fileutil.read_file(conf_file_path)
+ conf.load(content)
+ except IOError as err:
+ raise AgentConfigError(("Failed to load conf file:{0}, {1}"
+ "").format(conf_file_path, err))
+
+def get(key, default_val=None, conf=__config__):
+ """
+ Get option value by key, return default_val if not found
+ """
+ if conf is not None:
+ return conf.get(key, default_val)
+ else:
+ return default_val
+
+def get_switch(key, default_val=None, conf=__config__):
+ """
+ Get bool option value by key, return default_val if not found
+ """
+ if conf is not None:
+ return conf.get_switch(key, default_val)
+ else:
+ return default_val
+
+def get_int(key, default_val=None, conf=__config__):
+ """
+ Get int option value by key, return default_val if not found
+ """
+ if conf is not None:
+ return conf.get_int(key, default_val)
+ else:
+ return default_val
+
diff --git a/azurelinuxagent/distro/__init__.py b/azurelinuxagent/distro/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/centos/__init__.py b/azurelinuxagent/distro/centos/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/centos/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/centos/loader.py b/azurelinuxagent/distro/centos/loader.py
new file mode 100644
index 0000000..379f027
--- /dev/null
+++ b/azurelinuxagent/distro/centos/loader.py
@@ -0,0 +1,25 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from azurelinuxagent.metadata import DISTRO_NAME, DISTRO_VERSION
+import azurelinuxagent.distro.redhat.loader as redhat
+
+def get_osutil():
+ return redhat.get_osutil()
+
diff --git a/azurelinuxagent/distro/coreos/__init__.py b/azurelinuxagent/distro/coreos/__init__.py
new file mode 100644
index 0000000..7a4980e
--- /dev/null
+++ b/azurelinuxagent/distro/coreos/__init__.py
@@ -0,0 +1,18 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
diff --git a/azurelinuxagent/distro/coreos/deprovision.py b/azurelinuxagent/distro/coreos/deprovision.py
new file mode 100644
index 0000000..f0ff604
--- /dev/null
+++ b/azurelinuxagent/distro/coreos/deprovision.py
@@ -0,0 +1,30 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import azurelinuxagent.utils.fileutil as fileutil
+from azurelinuxagent.distro.default.deprovision import DeprovisionHandler, DeprovisionAction
+
+class CoreOSDeprovisionHandler(DeprovisionHandler):
+ def setup(self, deluser):
+ warnings, actions = super(CoreOSDeprovisionHandler, self).setup(deluser)
+ warnings.append("WARNING! /etc/machine-id will be removed.")
+ files_to_del = ['/etc/machine-id']
+ actions.append(DeprovisionAction(fileutil.rm_files, files_to_del))
+ return warnings, actions
+
diff --git a/azurelinuxagent/distro/coreos/handlerFactory.py b/azurelinuxagent/distro/coreos/handlerFactory.py
new file mode 100644
index 0000000..f0490e8
--- /dev/null
+++ b/azurelinuxagent/distro/coreos/handlerFactory.py
@@ -0,0 +1,27 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from .deprovision import CoreOSDeprovisionHandler
+from azurelinuxagent.distro.default.handlerFactory import DefaultHandlerFactory
+
+class CoreOSHandlerFactory(DefaultHandlerFactory):
+ def __init__(self):
+ super(CoreOSHandlerFactory, self).__init__()
+ self.deprovision_handler = CoreOSDeprovisionHandler()
+
diff --git a/azurelinuxagent/distro/coreos/loader.py b/azurelinuxagent/distro/coreos/loader.py
new file mode 100644
index 0000000..ec009ef
--- /dev/null
+++ b/azurelinuxagent/distro/coreos/loader.py
@@ -0,0 +1,28 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+
+def get_osutil():
+ from azurelinuxagent.distro.coreos.osutil import CoreOSUtil
+ return CoreOSUtil()
+
+def get_handlers():
+ from azurelinuxagent.distro.coreos.handlerFactory import CoreOSHandlerFactory
+ return CoreOSHandlerFactory()
+
diff --git a/azurelinuxagent/distro/coreos/osutil.py b/azurelinuxagent/distro/coreos/osutil.py
new file mode 100644
index 0000000..6dfba64
--- /dev/null
+++ b/azurelinuxagent/distro/coreos/osutil.py
@@ -0,0 +1,90 @@
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import pwd
+import shutil
+import socket
+import array
+import struct
+import fcntl
+import time
+import base64
+import azurelinuxagent.logger as logger
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.textutil as textutil
+from azurelinuxagent.distro.default.osutil import DefaultOSUtil
+
+class CoreOSUtil(DefaultOSUtil):
+ def __init__(self):
+ super(CoreOSUtil, self).__init__()
+ self.waagent_path='/usr/share/oem/bin/waagent'
+ self.python_path='/usr/share/oem/python/bin'
+ self.conf_path = '/usr/share/oem/waagent.conf'
+ if 'PATH' in os.environ:
+ path = "{0}:{1}".format(os.environ['PATH'], self.python_path)
+ else:
+ path = self.python_path
+ os.environ['PATH'] = path
+
+ if 'PYTHONPATH' in os.environ:
+ py_path = os.environ['PYTHONPATH']
+ py_path = "{0}:{1}".format(py_path, self.waagent_path)
+ else:
+ py_path = self.waagent_path
+ os.environ['PYTHONPATH'] = py_path
+
+ def is_sys_user(self, username):
+ #User 'core' is not a sysuser
+ if username == 'core':
+ return False
+ return super(CoreOSUtil, self).IsSysUser(username)
+
+ def is_dhcp_enabled(self):
+ return True
+
+ def start_network(self) :
+ return shellutil.run("systemctl start systemd-networkd", chk_err=False)
+
+ def restart_if(self, iface):
+ shellutil.run("systemctl restart systemd-networkd")
+
+ def restart_ssh_service(self):
+ return shellutil.run("systemctl restart sshd", chk_err=False)
+
+ def stop_dhcp_service(self):
+ return shellutil.run("systemctl stop systemd-networkd", chk_err=False)
+
+ def start_dhcp_service(self):
+ return shellutil.run("systemctl start systemd-networkd", chk_err=False)
+
+ def start_agent_service(self):
+ return shellutil.run("systemctl start wagent", chk_err=False)
+
+ def stop_agent_service(self):
+ return shellutil.run("systemctl stop wagent", chk_err=False)
+
+ def get_dhcp_pid(self):
+ ret= shellutil.run_get_output("pidof systemd-networkd")
+ return ret[1] if ret[0] == 0 else None
+
+ def decode_customdata(self, data):
+ return base64.b64decode(data)
+
diff --git a/azurelinuxagent/distro/debian/__init__.py b/azurelinuxagent/distro/debian/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/debian/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/debian/loader.py b/azurelinuxagent/distro/debian/loader.py
new file mode 100644
index 0000000..0787758
--- /dev/null
+++ b/azurelinuxagent/distro/debian/loader.py
@@ -0,0 +1,24 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+
+def get_osutil():
+ from azurelinuxagent.distro.debian.osutil import DebianOSUtil
+ return DebianOSUtil()
+
diff --git a/azurelinuxagent/distro/debian/osutil.py b/azurelinuxagent/distro/debian/osutil.py
new file mode 100644
index 0000000..a40c1de
--- /dev/null
+++ b/azurelinuxagent/distro/debian/osutil.py
@@ -0,0 +1,47 @@
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import pwd
+import shutil
+import socket
+import array
+import struct
+import fcntl
+import time
+import base64
+import azurelinuxagent.logger as logger
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.textutil as textutil
+from azurelinuxagent.distro.default.osutil import DefaultOSUtil
+
+class DebianOSUtil(DefaultOSUtil):
+ def __init__(self):
+ super(DebianOSUtil, self).__init__()
+
+ def restart_ssh_service(self):
+ return shellutil.run("service sshd restart", chk_err=False)
+
+ def stop_agent_service(self):
+ return shellutil.run("service azurelinuxagent stop", chk_err=False)
+
+ def start_agent_service(self):
+ return shellutil.run("service azurelinuxagent start", chk_err=False)
+
diff --git a/azurelinuxagent/distro/default/__init__.py b/azurelinuxagent/distro/default/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/default/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/default/deprovision.py b/azurelinuxagent/distro/default/deprovision.py
new file mode 100644
index 0000000..231f4eb
--- /dev/null
+++ b/azurelinuxagent/distro/default/deprovision.py
@@ -0,0 +1,117 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import azurelinuxagent.conf as conf
+from azurelinuxagent.utils.osutil import OSUTIL
+import azurelinuxagent.protocol as prot
+import azurelinuxagent.protocol.ovfenv as ovf
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+
+class DeprovisionAction(object):
+ def __init__(self, func, args=[], kwargs={}):
+ self.func = func
+ self.args = args
+ self.kwargs = kwargs
+
+ def invoke(self):
+ self.func(*self.args, **self.kwargs)
+
+class DeprovisionHandler(object):
+
+ def del_root_password(self, warnings, actions):
+ warnings.append("WARNING! root password will be disabled. "
+ "You will not be able to login as root.")
+
+ actions.append(DeprovisionAction(OSUTIL.del_root_password))
+
+ def del_user(self, warnings, actions):
+
+ try:
+ ovfenv = ovf.get_ovf_env()
+ except prot.ProtocolError:
+ warnings.append("WARNING! ovf-env.xml is not found.")
+ warnings.append("WARNING! Skip delete user.")
+ return
+
+ username = ovfenv.username
+ warnings.append(("WARNING! {0} account and entire home directory "
+ "will be deleted.").format(username))
+ actions.append(DeprovisionAction(OSUTIL.del_account, [username]))
+
+
+ def regen_ssh_host_key(self, warnings, actions):
+ warnings.append("WARNING! All SSH host key pairs will be deleted.")
+ actions.append(DeprovisionAction(OSUTIL.set_hostname,
+ ['localhost.localdomain']))
+ actions.append(DeprovisionAction(shellutil.run,
+ ['rm -f /etc/ssh/ssh_host_*key*']))
+
+ def stop_agent_service(self, warnings, actions):
+ warnings.append("WARNING! The waagent service will be stopped.")
+ actions.append(DeprovisionAction(OSUTIL.stop_agent_service))
+
+ def del_files(self, warnings, actions):
+ files_to_del = ['/root/.bash_history', '/var/log/waagent.log']
+ actions.append(DeprovisionAction(fileutil.rm_files, files_to_del))
+
+ def del_dhcp_lease(self, warnings, actions):
+ warnings.append("WARNING! Cached DHCP leases will be deleted.")
+ dirs_to_del = ["/var/lib/dhclient", "/var/lib/dhcpcd", "/var/lib/dhcp"]
+ actions.append(DeprovisionAction(fileutil.rm_dirs, dirs_to_del))
+
+ def del_lib_dir(self, warnings, actions):
+ dirs_to_del = [OSUTIL.get_lib_dir()]
+ actions.append(DeprovisionAction(fileutil.rm_dirs, dirs_to_del))
+
+ def setup(self, deluser):
+ warnings = []
+ actions = []
+
+ self.stop_agent_service(warnings, actions)
+ if conf.get_switch("Provisioning.RegenerateSshHostkey", False):
+ self.regen_ssh_host_key(warnings, actions)
+
+ self.del_dhcp_lease(warnings, actions)
+
+ if conf.get_switch("Provisioning.DeleteRootPassword", False):
+ self.del_root_password(warnings, actions)
+
+ self.del_lib_dir(warnings, actions)
+ self.del_files(warnings, actions)
+
+ if deluser:
+ self.del_user(warnings, actions)
+
+ return warnings, actions
+
+ def deprovision(self, force=False, deluser=False):
+ warnings, actions = self.setup(deluser)
+ for warning in warnings:
+ print(warning)
+
+ if not force:
+ confirm = input("Do you want to proceed (y/n)")
+ if not confirm.lower().startswith('y'):
+ return
+
+ for action in actions:
+ action.invoke()
+
+
diff --git a/azurelinuxagent/distro/default/dhcp.py b/azurelinuxagent/distro/default/dhcp.py
new file mode 100644
index 0000000..574ebd4
--- /dev/null
+++ b/azurelinuxagent/distro/default/dhcp.py
@@ -0,0 +1,330 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+import os
+import socket
+import array
+import time
+import azurelinuxagent.logger as logger
+from azurelinuxagent.utils.osutil import OSUTIL
+from azurelinuxagent.exception import AgentNetworkError
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+from azurelinuxagent.utils.textutil import *
+
+WIRE_SERVER_ADDR_FILE_NAME="WireServer"
+
+class DhcpHandler(object):
+ def __init__(self):
+ self.endpoint = None
+ self.gateway = None
+ self.routes = None
+
+ def wait_for_network(self):
+ ipv4 = OSUTIL.get_ip4_addr()
+ while ipv4 == '' or ipv4 == '0.0.0.0':
+ logger.info("Waiting for network.")
+ time.sleep(10)
+ OSUTIL.start_network()
+ ipv4 = OSUTIL.get_ip4_addr()
+
+ def probe(self):
+ logger.info("Send dhcp request")
+ self.wait_for_network()
+ mac_addr = OSUTIL.get_mac_addr()
+ req = build_dhcp_request(mac_addr)
+ resp = send_dhcp_request(req)
+ if resp is None:
+ logger.warn("Failed to detect wire server.")
+ return
+ endpoint, gateway, routes = parse_dhcp_resp(resp)
+ self.endpoint = endpoint
+ logger.info("Wire server endpoint:{0}", endpoint)
+ logger.info("Gateway:{0}", gateway)
+ logger.info("Routes:{0}", routes)
+ if endpoint is not None:
+ path = os.path.join(OSUTIL.get_lib_dir(), WIRE_SERVER_ADDR_FILE_NAME)
+ fileutil.write_file(path, endpoint)
+ self.gateway = gateway
+ self.routes = routes
+ self.conf_routes()
+
+ def get_endpoint(self):
+ return self.endpoint
+
+ def conf_routes(self):
+ logger.info("Configure routes")
+ #Add default gateway
+ if self.gateway is not None:
+ OSUTIL.route_add(0 , 0, self.gateway)
+ if self.routes is not None:
+ for route in self.routes:
+ OSUTIL.route_add(route[0], route[1], route[2])
+
+def validate_dhcp_resp(request, response):
+ bytes_recv = len(response)
+ if bytes_recv < 0xF6:
+ logger.error("HandleDhcpResponse: Too few bytes received:{0}",
+ bytes_recv)
+ return False
+
+ logger.verb("BytesReceived:{0}", hex(bytes_recv))
+ logger.verb("DHCP response:{0}", hex_dump(response, bytes_recv))
+
+ # check transactionId, cookie, MAC address cookie should never mismatch
+ # transactionId and MAC address may mismatch if we see a response
+ # meant from another machine
+ if not compare_bytes(request, response, 0xEC, 4):
+ logger.verb("Cookie not match:\nsend={0},\nreceive={1}",
+ hex_dump3(request, 0xEC, 4),
+ hex_dump3(response, 0xEC, 4))
+ raise AgentNetworkError("Cookie in dhcp respones "
+ "doesn't match the request")
+
+ if not compare_bytes(request, response, 4, 4):
+ logger.verb("TransactionID not match:\nsend={0},\nreceive={1}",
+ hex_dump3(request, 4, 4),
+ hex_dump3(response, 4, 4))
+ raise AgentNetworkError("TransactionID in dhcp respones "
+ "doesn't match the request")
+
+ if not compare_bytes(request, response, 0x1C, 6):
+ logger.verb("Mac Address not match:\nsend={0},\nreceive={1}",
+ hex_dump3(request, 0x1C, 6),
+ hex_dump3(response, 0x1C, 6))
+ raise AgentNetworkError("Mac Addr in dhcp respones "
+ "doesn't match the request")
+
+def parse_route(response, option, i, length, bytes_recv):
+ # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx
+ logger.verb("Routes at offset: {0} with length:{1}",
+ hex(i),
+ hex(length))
+ routes = []
+ if length < 5:
+ logger.error("Data too small for option:{0}", option)
+ j = i + 2
+ while j < (i + length + 2):
+ mask_len_bits = str_to_ord(response[j])
+ mask_len_bytes = (((mask_len_bits + 7) & ~7) >> 3)
+ mask = 0xFFFFFFFF & (0xFFFFFFFF << (32 - mask_len_bits))
+ j += 1
+ net = unpack_big_endian(response, j, mask_len_bytes)
+ net <<= (32 - mask_len_bytes * 8)
+ net &= mask
+ j += mask_len_bytes
+ gateway = unpack_big_endian(response, j, 4)
+ j += 4
+ routes.append((net, mask, gateway))
+ if j != (i + length + 2):
+ logger.error("Unable to parse routes")
+ return routes
+
+def parse_ip_addr(response, option, i, length, bytes_recv):
+ if i + 5 < bytes_recv:
+ if length != 4:
+ logger.error("Endpoint or Default Gateway not 4 bytes")
+ return None
+ addr = unpack_big_endian(response, i + 2, 4)
+ ip_addr = int_to_ip4_addr(addr)
+ return ip_addr
+ else:
+ logger.error("Data too small for option:{0}", option)
+ return None
+
+def parse_dhcp_resp(response):
+ """
+ Parse DHCP response:
+ Returns endpoint server or None on error.
+ """
+ logger.verb("parse Dhcp Response")
+ bytes_recv = len(response)
+ endpoint = None
+ gateway = None
+ routes = None
+
+ # Walk all the returned options, parsing out what we need, ignoring the
+ # others. We need the custom option 245 to find the the endpoint we talk to,
+ # as well as, to handle some Linux DHCP client incompatibilities,
+ # options 3 for default gateway and 249 for routes. And 255 is end.
+
+ i = 0xF0 # offset to first option
+ while i < bytes_recv:
+ option = str_to_ord(response[i])
+ length = 0
+ if (i + 1) < bytes_recv:
+ length = str_to_ord(response[i + 1])
+ logger.verb("DHCP option {0} at offset:{1} with length:{2}",
+ hex(option),
+ hex(i),
+ hex(length))
+ if option == 255:
+ logger.verb("DHCP packet ended at offset:{0}", hex(i))
+ break
+ elif option == 249:
+ routes = parse_route(response, option, i, length, bytes_recv)
+ elif option == 3:
+ gateway = parse_ip_addr(response, option, i, length, bytes_recv)
+ logger.verb("Default gateway:{0}, at {1}",
+ gateway,
+ hex(i))
+ elif option == 245:
+ endpoint = parse_ip_addr(response, option, i, length, bytes_recv)
+ logger.verb("Azure wire protocol endpoint:{0}, at {1}",
+ gateway,
+ hex(i))
+ else:
+ logger.verb("Skipping DHCP option:{0} at {1} with length {2}",
+ hex(option),
+ hex(i),
+ hex(length))
+ i += length + 2
+ return endpoint, gateway, routes
+
+
+def allow_dhcp_broadcast(func):
+ """
+ Temporary allow broadcase for dhcp. Remove the route when done.
+ """
+ def wrapper(*args, **kwargs):
+ missing_default_route = OSUTIL.is_missing_default_route()
+ ifname = OSUTIL.get_if_name()
+ if missing_default_route:
+ OSUTIL.set_route_for_dhcp_broadcast(ifname)
+ result = func(*args, **kwargs)
+ if missing_default_route:
+ OSUTIL.remove_route_for_dhcp_broadcast(ifname)
+ return result
+ return wrapper
+
+def disable_dhcp_service(func):
+ """
+ In some distros, dhcp service needs to be shutdown before agent probe
+ endpoint through dhcp.
+ """
+ def wrapper(*args, **kwargs):
+ if OSUTIL.is_dhcp_enabled():
+ OSUTIL.stop_dhcp_service()
+ result = func(*args, **kwargs)
+ OSUTIL.start_dhcp_service()
+ return result
+ else:
+ return func(*args, **kwargs)
+ return wrapper
+
+
+@allow_dhcp_broadcast
+@disable_dhcp_service
+def send_dhcp_request(request):
+ __waiting_duration__ = [0, 10, 30, 60, 60]
+ for duration in __waiting_duration__:
+ try:
+ OSUTIL.allow_dhcp_broadcast()
+ response = socket_send(request)
+ validate_dhcp_resp(request, response)
+ return response
+ except AgentNetworkError as e:
+ logger.warn("Failed to send DHCP request: {0}", e)
+ time.sleep(duration)
+ return None
+
+def socket_send(request):
+ sock = None
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
+ socket.IPPROTO_UDP)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(("0.0.0.0", 68))
+ sock.sendto(request, ("<broadcast>", 67))
+ sock.settimeout(10)
+ logger.verb("Send DHCP request: Setting socket.timeout=10, "
+ "entering recv")
+ response = sock.recv(1024)
+ return response
+ except IOError as e:
+ raise AgentNetworkError("{0}".format(e))
+ finally:
+ if sock is not None:
+ sock.close()
+
+def build_dhcp_request(mac_addr):
+ """
+ Build DHCP request string.
+ """
+ #
+ # typedef struct _DHCP {
+ # UINT8 Opcode; /* op: BOOTREQUEST or BOOTREPLY */
+ # UINT8 HardwareAddressType; /* htype: ethernet */
+ # UINT8 HardwareAddressLength; /* hlen: 6 (48 bit mac address) */
+ # UINT8 Hops; /* hops: 0 */
+ # UINT8 TransactionID[4]; /* xid: random */
+ # UINT8 Seconds[2]; /* secs: 0 */
+ # UINT8 Flags[2]; /* flags: 0 or 0x8000 for broadcast */
+ # UINT8 ClientIpAddress[4]; /* ciaddr: 0 */
+ # UINT8 YourIpAddress[4]; /* yiaddr: 0 */
+ # UINT8 ServerIpAddress[4]; /* siaddr: 0 */
+ # UINT8 RelayAgentIpAddress[4]; /* giaddr: 0 */
+ # UINT8 ClientHardwareAddress[16]; /* chaddr: 6 byte eth MAC address */
+ # UINT8 ServerName[64]; /* sname: 0 */
+ # UINT8 BootFileName[128]; /* file: 0 */
+ # UINT8 MagicCookie[4]; /* 99 130 83 99 */
+ # /* 0x63 0x82 0x53 0x63 */
+ # /* options -- hard code ours */
+ #
+ # UINT8 MessageTypeCode; /* 53 */
+ # UINT8 MessageTypeLength; /* 1 */
+ # UINT8 MessageType; /* 1 for DISCOVER */
+ # UINT8 End; /* 255 */
+ # } DHCP;
+ #
+
+ # tuple of 244 zeros
+ # (struct.pack_into would be good here, but requires Python 2.5)
+ request = [0] * 244
+
+ trans_id = gen_trans_id()
+
+ # Opcode = 1
+ # HardwareAddressType = 1 (ethernet/MAC)
+ # HardwareAddressLength = 6 (ethernet/MAC/48 bits)
+ for a in range(0, 3):
+ request[a] = [1, 1, 6][a]
+
+ # fill in transaction id (random number to ensure response matches request)
+ for a in range(0, 4):
+ request[4 + a] = str_to_ord(trans_id[a])
+
+ logger.verb("BuildDhcpRequest: transactionId:%s,%04X" % (
+ hex_dump2(trans_id),
+ unpack_big_endian(request, 4, 4)))
+
+ # fill in ClientHardwareAddress
+ for a in range(0, 6):
+ request[0x1C + a] = str_to_ord(mac_addr[a])
+
+ # DHCP Magic Cookie: 99, 130, 83, 99
+ # MessageTypeCode = 53 DHCP Message Type
+ # MessageTypeLength = 1
+ # MessageType = DHCPDISCOVER
+ # End = 255 DHCP_END
+ for a in range(0, 8):
+ request[0xEC + a] = [99, 130, 83, 99, 53, 1, 1, 255][a]
+ return array.array("B", request)
+
+def gen_trans_id():
+ return os.urandom(4)
diff --git a/azurelinuxagent/distro/default/env.py b/azurelinuxagent/distro/default/env.py
new file mode 100644
index 0000000..6a67113
--- /dev/null
+++ b/azurelinuxagent/distro/default/env.py
@@ -0,0 +1,115 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import socket
+import threading
+import time
+import azurelinuxagent.logger as logger
+import azurelinuxagent.conf as conf
+from azurelinuxagent.utils.osutil import OSUTIL
+
+class EnvHandler(object):
+ """
+ Monitor changes to dhcp and hostname.
+ If dhcp clinet process re-start has occurred, reset routes, dhcp with fabric.
+
+ Monitor scsi disk.
+ If new scsi disk found, set
+ """
+ def __init__(self, handlers):
+ self.monitor = EnvMonitor(handlers.dhcp_handler)
+
+ def start(self):
+ self.monitor.start()
+
+ def stop(self):
+ self.monitor.stop()
+
+class EnvMonitor(object):
+
+ def __init__(self, dhcp_handler):
+ self.dhcp_handler = dhcp_handler
+ self.stopped = True
+ self.hostname = None
+ self.dhcpid = None
+ self.server_thread=None
+
+ def start(self):
+ if not self.stopped:
+ logger.info("Stop existing env monitor service.")
+ self.stop()
+
+ self.stopped = False
+ logger.info("Start env monitor service.")
+ self.hostname = socket.gethostname()
+ self.dhcpid = OSUTIL.get_dhcp_pid()
+ self.server_thread = threading.Thread(target = self.monitor)
+ self.server_thread.setDaemon(True)
+ self.server_thread.start()
+
+ def monitor(self):
+ """
+ Monitor dhcp client pid and hostname.
+ If dhcp clinet process re-start has occurred, reset routes.
+ """
+ while not self.stopped:
+ OSUTIL.remove_rules_files()
+ timeout = conf.get("OS.RootDeviceScsiTimeout", None)
+ if timeout is not None:
+ OSUTIL.set_scsi_disks_timeout(timeout)
+ if conf.get_switch("Provisioning.MonitorHostName", False):
+ self.handle_hostname_update()
+ self.handle_dhclient_restart()
+ time.sleep(5)
+
+ def handle_hostname_update(self):
+ curr_hostname = socket.gethostname()
+ if curr_hostname != self.hostname:
+ logger.info("EnvMonitor: Detected host name change: {0} -> {1}",
+ self.hostname, curr_hostname)
+ OSUTIL.set_hostname(curr_hostname)
+ OSUTIL.publish_hostname(curr_hostname)
+ self.hostname = curr_hostname
+
+ def handle_dhclient_restart(self):
+ if self.dhcpid is None:
+ logger.warn("Dhcp client is not running. ")
+ self.dhcpid = OSUTIL.get_dhcp_pid()
+ return
+
+ #The dhcp process hasn't changed since last check
+ if os.path.isdir(os.path.join('/proc', self.dhcpid.strip())):
+ return
+
+ newpid = OSUTIL.get_dhcp_pid()
+ if newpid is not None and newpid != self.dhcpid:
+ logger.info("EnvMonitor: Detected dhcp client restart. "
+ "Restoring routing table.")
+ self.dhcp_handler.conf_routes()
+ self.dhcpid = newpid
+
+ def stop(self):
+ """
+ Stop server comminucation and join the thread to main thread.
+ """
+ self.stopped = True
+ if self.server_thread is not None:
+ self.server_thread.join()
+
diff --git a/azurelinuxagent/distro/default/extension.py b/azurelinuxagent/distro/default/extension.py
new file mode 100644
index 0000000..58ba84e
--- /dev/null
+++ b/azurelinuxagent/distro/default/extension.py
@@ -0,0 +1,647 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+import os
+import zipfile
+import time
+import json
+import subprocess
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+from azurelinuxagent.utils.osutil import OSUTIL
+import azurelinuxagent.protocol as prot
+from azurelinuxagent.event import add_event, WALAEventOperation
+from azurelinuxagent.exception import ExtensionError
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.restutil as restutil
+import azurelinuxagent.utils.shellutil as shellutil
+
+VALID_EXTENSION_STATUS = ['transitioning', 'error', 'success', 'warning']
+
+def validate_has_key(obj, key, fullname):
+ if key not in obj:
+ raise ExtensionError("Missing: {0}".format(fullname))
+
+def validate_in_range(val, valid_range, name):
+ if val not in valid_range:
+ raise ExtensionError("Invalid {0}: {1}".format(name, val))
+
+def try_get(dictionary, key, default=None):
+ try:
+ return dictionary[key]
+ except KeyError:
+ return default
+
+def extension_sub_status_to_v2(substatus):
+ #Check extension sub status format
+ validate_has_key(substatus, 'name', 'substatus/name')
+ validate_has_key(substatus, 'status', 'substatus/status')
+ validate_has_key(substatus, 'code', 'substatus/code')
+ validate_has_key(substatus, 'formattedMessage', 'substatus/formattedMessage')
+ validate_has_key(substatus['formattedMessage'], 'lang',
+ 'substatus/formattedMessage/lang')
+ validate_has_key(substatus['formattedMessage'], 'message',
+ 'substatus/formattedMessage/message')
+
+ validate_in_range(substatus['status'], VALID_EXTENSION_STATUS,
+ 'substatus/status')
+ status = prot.ExtensionSubStatus()
+ status.name = try_get(substatus, 'name')
+ status.status = try_get(substatus, 'status')
+ status.code = try_get(substatus, 'code')
+ status.message = try_get(substatus['formattedMessage'], 'message')
+ return status
+
+def ext_status_to_v2(ext_status, seq_no):
+ #Check extension status format
+ validate_has_key(ext_status, 'status', 'status')
+ validate_has_key(ext_status['status'], 'status', 'status/status')
+ validate_has_key(ext_status['status'], 'operation', 'status/operation')
+ validate_has_key(ext_status['status'], 'code', 'status/code')
+ validate_has_key(ext_status['status'], 'name', 'status/name')
+ validate_has_key(ext_status['status'], 'formattedMessage',
+ 'status/formattedMessage')
+ validate_has_key(ext_status['status']['formattedMessage'], 'lang',
+ 'status/formattedMessage/lang')
+ validate_has_key(ext_status['status']['formattedMessage'], 'message',
+ 'status/formattedMessage/message')
+
+ validate_in_range(ext_status['status']['status'], VALID_EXTENSION_STATUS,
+ 'status/status')
+
+ status = prot.ExtensionStatus()
+ status.name = try_get(ext_status['status'], 'name')
+ status.configurationAppliedTime = try_get(ext_status['status'],
+ 'configurationAppliedTime')
+ status.operation = try_get(ext_status['status'], 'operation')
+ status.status = try_get(ext_status['status'], 'status')
+ status.code = try_get(ext_status['status'], 'code')
+ status.message = try_get(ext_status['status']['formattedMessage'], 'message')
+ status.sequenceNumber = seq_no
+
+ substatus_list = try_get(ext_status['status'], 'substatus', [])
+ for substatus in substatus_list:
+ status.substatusList.extend(extension_sub_status_to_v2(substatus))
+ return status
+
+class ExtensionsHandler(object):
+
+ def process(self):
+ protocol = prot.FACTORY.get_default_protocol()
+ ext_list = protocol.get_extensions()
+
+ h_status_list = []
+ for extension in ext_list.extensions:
+ #TODO handle extension in parallel
+ pkg_list = protocol.get_extension_pkgs(extension)
+ h_status = self.process_extension(extension, pkg_list)
+ h_status_list.append(h_status)
+
+ return h_status_list
+
+ def process_extension(self, extension, pkg_list):
+ installed_version = get_installed_version(extension.name)
+ if installed_version is not None:
+ ext = ExtensionInstance(extension, pkg_list,
+ installed_version, installed=True)
+ else:
+ ext = ExtensionInstance(extension, pkg_list,
+ extension.properties.version)
+ try:
+ ext.init_logger()
+ ext.handle()
+ status = ext.collect_handler_status()
+ except ExtensionError as e:
+ logger.error("Failed to handle extension: {0}-{1}\n {2}",
+ ext.get_name(), ext.get_version(), e)
+ add_event(name=ext.get_name(), is_success=False,
+ op=ext.get_curr_op(), message = text(e))
+ ext_status = prot.ExtensionStatus(status='error', code='-1',
+ operation = ext.get_curr_op(),
+ message = text(e),
+ seq_no = ext.get_seq_no())
+ status = ext.create_handler_status(ext_status)
+ status.status = "Ready"
+ return status
+
+def parse_extension_dirname(dirname):
+ """
+ Parse installed extension dir name. Sample: ExtensionName-Version/
+ """
+ seprator = dirname.rfind('-')
+ if seprator < 0:
+ raise ExtensionError("Invalid extenation dir name")
+ return dirname[0:seprator], dirname[seprator + 1:]
+
+def get_installed_version(target_name):
+ """
+ Return the highest version instance with the same name
+ """
+ installed_version = None
+ lib_dir = OSUTIL.get_lib_dir()
+ for dir_name in os.listdir(lib_dir):
+ path = os.path.join(lib_dir, dir_name)
+ if os.path.isdir(path) and dir_name.startswith(target_name):
+ name, version = parse_extension_dirname(dir_name)
+ #Here we need to ensure names are exactly the same.
+ if name == target_name:
+ if installed_version is None or installed_version < version:
+ installed_version = version
+ return installed_version
+
+class ExtensionInstance(object):
+ def __init__(self, extension, pkg_list, curr_version, installed=False):
+ self.extension = extension
+ self.pkg_list = pkg_list
+ self.curr_version = curr_version
+ self.lib_dir = OSUTIL.get_lib_dir()
+ self.installed = installed
+ self.settings = None
+
+ #Extension will have no more than 1 settings instance
+ if len(extension.properties.extensions) > 0:
+ self.settings = extension.properties.extensions[0]
+ self.enabled = False
+ self.curr_op = None
+
+ prefix = "[{0}]".format(self.get_full_name())
+ self.logger = logger.Logger(logger.DEFAULT_LOGGER, prefix)
+
+ def init_logger(self):
+ #Init logger appender for extension
+ fileutil.mkdir(self.get_log_dir(), mode=0o700)
+ log_file = os.path.join(self.get_log_dir(), "CommandExecution.log")
+ self.logger.add_appender(logger.AppenderType.FILE,
+ logger.LogLevel.INFO, log_file)
+
+ def handle(self):
+ self.logger.info("Process extension settings:")
+ self.logger.info(" Name: {0}", self.get_name())
+ self.logger.info(" Version: {0}", self.get_version())
+
+ if self.installed:
+ self.logger.info("Installed version:{0}", self.curr_version)
+ h_status = self.get_handler_status()
+ self.enabled = (h_status == "Ready")
+
+ state = self.get_state()
+ if state == 'enabled':
+ self.handle_enable()
+ elif state == 'disabled':
+ self.handle_disable()
+ elif state == 'uninstall':
+ self.handle_disable()
+ self.handle_uninstall()
+ else:
+ raise ExtensionError("Unknown extension state:{0}".format(state))
+
+ def handle_enable(self):
+ target_version = self.get_target_version()
+ if self.installed:
+ if target_version > self.curr_version:
+ self.upgrade(target_version)
+ elif target_version == self.curr_version:
+ self.enable()
+ else:
+ raise ExtensionError("A newer version has already been installed")
+ else:
+ if target_version > self.get_version():
+ #This will happen when auto upgrade policy is enabled
+ self.logger.info("Auto upgrade to new version:{0}",
+ target_version)
+ self.curr_version = target_version
+ self.download()
+ self.init_dir()
+ self.install()
+ self.enable()
+
+ def handle_disable(self):
+ if not self.installed or not self.enabled:
+ return
+ self.disable()
+
+ def handle_uninstall(self):
+ if not self.installed:
+ return
+ self.uninstall()
+
+ def upgrade(self, target_version):
+ self.logger.info("Upgrade from: {0} to {1}", self.curr_version,
+ target_version)
+ self.curr_op=WALAEventOperation.Upgrade
+ old = self
+ new = ExtensionInstance(self.extension, self.pkg_list, target_version)
+ self.logger.info("Download new extension package")
+ new.init_logger()
+ new.download()
+ self.logger.info("Initialize new extension directory")
+ new.init_dir()
+
+ old.disable()
+ self.logger.info("Update new extension")
+ new.update()
+ old.uninstall()
+ man = new.load_manifest()
+ if man.is_update_with_install():
+ self.logger.info("Install new extension")
+ new.install()
+ self.logger.info("Enable new extension")
+ new.enable()
+ add_event(name=self.get_name(), is_success=True,
+ op=self.curr_op, message="")
+
+ def download(self):
+ self.logger.info("Download extension package")
+ self.curr_op=WALAEventOperation.Download
+ uris = self.get_package_uris()
+ package = None
+ for uri in uris:
+ try:
+ resp = restutil.http_get(uri.uri, chk_proxy=True)
+ package = resp.read()
+ break
+ except restutil.HttpError as e:
+ self.logger.warn("Failed download extension from: {0}", uri.uri)
+
+ if package is None:
+ raise ExtensionError("Download extension failed")
+
+ self.logger.info("Unpack extension package")
+ pkg_file = os.path.join(self.lib_dir, os.path.basename(uri.uri) + ".zip")
+ fileutil.write_file(pkg_file, bytearray(package), asbin=True)
+ zipfile.ZipFile(pkg_file).extractall(self.get_base_dir())
+ chmod = "find {0} -type f | xargs chmod u+x".format(self.get_base_dir())
+ shellutil.run(chmod)
+ add_event(name=self.get_name(), is_success=True,
+ op=self.curr_op, message="")
+
+ def init_dir(self):
+ self.logger.info("Initialize extension directory")
+ #Save HandlerManifest.json
+ man_file = fileutil.search_file(self.get_base_dir(),
+ 'HandlerManifest.json')
+ man = fileutil.read_file(man_file, remove_bom=True)
+ fileutil.write_file(self.get_manifest_file(), man)
+
+ #Create status and config dir
+ status_dir = self.get_status_dir()
+ fileutil.mkdir(status_dir, mode=0o700)
+ conf_dir = self.get_conf_dir()
+ fileutil.mkdir(conf_dir, mode=0o700)
+
+ #Init handler state to uninstall
+ self.set_handler_status("NotReady")
+
+ #Save HandlerEnvironment.json
+ self.create_handler_env()
+
+ def enable(self):
+ self.logger.info("Enable extension.")
+ self.curr_op=WALAEventOperation.Enable
+ man = self.load_manifest()
+ self.launch_command(man.get_enable_command())
+ self.set_handler_status("Ready")
+ add_event(name=self.get_name(), is_success=True,
+ op=self.curr_op, message="")
+
+ def disable(self):
+ self.logger.info("Disable extension.")
+ self.curr_op=WALAEventOperation.Disable
+ man = self.load_manifest()
+ self.launch_command(man.get_disable_command(), timeout=900)
+ self.set_handler_status("Ready")
+ add_event(name=self.get_name(), is_success=True,
+ op=self.curr_op, message="")
+
+ def install(self):
+ self.logger.info("Install extension.")
+ self.curr_op=WALAEventOperation.Install
+ man = self.load_manifest()
+ self.set_handler_status("Installing")
+ self.launch_command(man.get_install_command(), timeout=900)
+ self.set_handler_status("Ready")
+ add_event(name=self.get_name(), is_success=True,
+ op=self.curr_op, message="")
+
+ def uninstall(self):
+ self.logger.info("Uninstall extension.")
+ self.curr_op=WALAEventOperation.UnInstall
+ man = self.load_manifest()
+ self.launch_command(man.get_uninstall_command())
+ self.set_handler_status("NotReady")
+ add_event(name=self.get_name(), is_success=True,
+ op=self.curr_op, message="")
+
+ def update(self):
+ self.logger.info("Update extension.")
+ self.curr_op=WALAEventOperation.Update
+ man = self.load_manifest()
+ self.launch_command(man.get_update_command(), timeout=900)
+ add_event(name=self.get_name(), is_success=True,
+ op=self.curr_op, message="")
+
+ def create_handler_status(self, ext_status, heartbeat=None):
+ status = prot.ExtensionHandlerStatus()
+ status.handlerName = self.get_name()
+ status.handlerVersion = self.get_version()
+ status.status = self.get_handler_status()
+ status.extensionStatusList.append(ext_status)
+ return status
+
+ def collect_handler_status(self):
+ man = self.load_manifest()
+ heartbeat=None
+ if man.is_report_heartbeat():
+ heartbeat = self.collect_heartbeat()
+ ext_status = self.collect_extension_status()
+ status= self.create_handler_status(ext_status, heartbeat)
+ status.status = self.get_handler_status()
+ if heartbeat is not None:
+ status.status = heartbeat['status']
+ status.extensionStatusList.append(ext_status)
+ return status
+
+ def collect_extension_status(self):
+ ext_status_file = self.get_status_file()
+ try:
+ ext_status_str = fileutil.read_file(ext_status_file)
+ ext_status = json.loads(ext_status_str)
+ except IOError as e:
+ raise ExtensionError("Failed to get status file: {0}".format(e))
+ except ValueError as e:
+ raise ExtensionError("Malformed status file: {0}".format(e))
+ return ext_status_to_v2(ext_status[0],
+ self.settings.sequenceNumber)
+
+ def get_handler_status(self):
+ h_status = "uninstalled"
+ h_status_file = self.get_handler_state_file()
+ try:
+ h_status = fileutil.read_file(h_status_file)
+ return h_status
+ except IOError as e:
+ raise ExtensionError("Failed to get handler status: {0}".format(e))
+
+ def set_handler_status(self, status):
+ h_status_file = self.get_handler_state_file()
+ try:
+ fileutil.write_file(h_status_file, status)
+ except IOError as e:
+ raise ExtensionError("Failed to set handler status: {0}".format(e))
+
+ def collect_heartbeat(self):
+ self.logger.info("Collect heart beat")
+ heartbeat_file = os.path.join(OSUTIL.get_lib_dir(),
+ self.get_heartbeat_file())
+ if not os.path.isfile(heartbeat_file):
+ raise ExtensionError("Failed to get heart beat file")
+ if not self.is_responsive(heartbeat_file):
+ return {
+ "status": "Unresponsive",
+ "code": -1,
+ "message": "Extension heartbeat is not responsive"
+ }
+ try:
+ heartbeat_json = fileutil.read_file(heartbeat_file)
+ heartbeat = json.loads(heartbeat_json)[0]['heartbeat']
+ except IOError as e:
+ raise ExtensionError("Failed to get heartbeat file:{0}".format(e))
+ except ValueError as e:
+ raise ExtensionError("Malformed heartbeat file: {0}".format(e))
+ return heartbeat
+
+ def is_responsive(self, heartbeat_file):
+ last_update=int(time.time()-os.stat(heartbeat_file).st_mtime)
+ return last_update > 600 # not updated for more than 10 min
+
+ def launch_command(self, cmd, timeout=300):
+ self.logger.info("Launch command:{0}", cmd)
+ base_dir = self.get_base_dir()
+ self.update_settings()
+ try:
+ devnull = open(os.devnull, 'w')
+ child = subprocess.Popen(base_dir + "/" + cmd, shell=True,
+ cwd=base_dir, stdout=devnull)
+ except Exception as e:
+ #TODO do not catch all exception
+ raise ExtensionError("Failed to launch: {0}, {1}".format(cmd, e))
+
+ retry = timeout / 5
+ while retry > 0 and child.poll == None:
+ time.sleep(5)
+ retry -= 1
+ if retry == 0:
+ os.kill(child.pid, 9)
+ raise ExtensionError("Timeout({0}): {1}".format(timeout, cmd))
+
+ ret = child.wait()
+ if ret == None or ret != 0:
+ raise ExtensionError("Non-zero exit code: {0}, {1}".format(ret, cmd))
+
+ def load_manifest(self):
+ man_file = self.get_manifest_file()
+ try:
+ data = json.loads(fileutil.read_file(man_file))
+ except IOError as e:
+ raise ExtensionError('Failed to load manifest file.')
+ except ValueError as e:
+ raise ExtensionError('Malformed manifest file.')
+
+ return HandlerManifest(data[0])
+
+
+ def update_settings(self):
+ if self.settings is None:
+ self.logger.verbose("Extension has no settings")
+ return
+
+ settings = {
+ 'publicSettings': self.settings.publicSettings,
+ 'protectedSettings': self.settings.privateSettings,
+ 'protectedSettingsCertThumbprint': self.settings.certificateThumbprint
+ }
+ ext_settings = {
+ "runtimeSettings":[{
+ "handlerSettings": settings
+ }]
+ }
+ fileutil.write_file(self.get_settings_file(), json.dumps(ext_settings))
+
+ latest = os.path.join(self.get_conf_dir(), "latest")
+ fileutil.write_file(latest, self.settings.sequenceNumber)
+
+ def create_handler_env(self):
+ env = [{
+ "name": self.get_name(),
+ "version" : self.get_version(),
+ "handlerEnvironment" : {
+ "logFolder" : self.get_log_dir(),
+ "configFolder" : self.get_conf_dir(),
+ "statusFolder" : self.get_status_dir(),
+ "heartbeatFile" : self.get_heartbeat_file()
+ }
+ }]
+ fileutil.write_file(self.get_env_file(),
+ json.dumps(env))
+
+ def get_target_version(self):
+ version = self.get_version()
+ update_policy = self.get_upgrade_policy()
+ if update_policy is None or update_policy.lower() != 'auto':
+ return version
+
+ major = version.split('.')[0]
+ if major is None:
+ raise ExtensionError("Wrong version format: {0}".format(version))
+
+ packages = [x for x in self.pkg_list.versions if x.version.startswith(major + ".")]
+ packages = sorted(packages, key=lambda x: x.version, reverse=True)
+ if len(packages) <= 0:
+ raise ExtensionError("Can't find version: {0}.*".format(major))
+
+ return packages[0].version
+
+ def get_package_uris(self):
+ version = self.get_version()
+ packages = self.pkg_list.versions
+ if packages is None:
+ raise ExtensionError("Package uris is None.")
+
+ for package in packages:
+ if package.version == version:
+ return package.uris
+
+ raise ExtensionError("Can't get package uris for {0}.".format(version))
+
+ def get_curr_op(self):
+ return self.curr_op
+
+ def get_name(self):
+ return self.extension.name
+
+ def get_version(self):
+ return self.extension.properties.version
+
+ def get_state(self):
+ return self.extension.properties.state
+
+ def get_seq_no(self):
+ return self.settings.sequenceNumber
+
+ def get_upgrade_policy(self):
+ return self.extension.properties.upgradePolicy
+
+ def get_full_name(self):
+ return "{0}-{1}".format(self.get_name(), self.curr_version)
+
+ def get_base_dir(self):
+ return os.path.join(OSUTIL.get_lib_dir(), self.get_full_name())
+
+ def get_status_dir(self):
+ return os.path.join(self.get_base_dir(), "status")
+
+ def get_status_file(self):
+ return os.path.join(self.get_status_dir(),
+ "{0}.status".format(self.settings.sequenceNumber))
+
+ def get_conf_dir(self):
+ return os.path.join(self.get_base_dir(), 'config')
+
+ def get_settings_file(self):
+ return os.path.join(self.get_conf_dir(),
+ "{0}.settings".format(self.settings.sequenceNumber))
+
+ def get_handler_state_file(self):
+ return os.path.join(self.get_conf_dir(), 'HandlerState')
+
+ def get_heartbeat_file(self):
+ return os.path.join(self.get_base_dir(), 'heartbeat.log')
+
+ def get_manifest_file(self):
+ return os.path.join(self.get_base_dir(), 'HandlerManifest.json')
+
+ def get_env_file(self):
+ return os.path.join(self.get_base_dir(), 'HandlerEnvironment.json')
+
+ def get_log_dir(self):
+ return os.path.join(OSUTIL.get_ext_log_dir(), self.get_name(),
+ self.curr_version)
+
+class HandlerEnvironment(object):
+ def __init__(self, data):
+ self.data = data
+
+ def get_version(self):
+ return self.data["version"]
+
+ def get_log_dir(self):
+ return self.data["handlerEnvironment"]["logFolder"]
+
+ def get_conf_dir(self):
+ return self.data["handlerEnvironment"]["configFolder"]
+
+ def get_status_dir(self):
+ return self.data["handlerEnvironment"]["statusFolder"]
+
+ def get_heartbeat_file(self):
+ return self.data["handlerEnvironment"]["heartbeatFile"]
+
+class HandlerManifest(object):
+ def __init__(self, data):
+ if data is None or data['handlerManifest'] is None:
+ raise ExtensionError('Malformed manifest file.')
+ self.data = data
+
+ def get_name(self):
+ return self.data["name"]
+
+ def get_version(self):
+ return self.data["version"]
+
+ def get_install_command(self):
+ return self.data['handlerManifest']["installCommand"]
+
+ def get_uninstall_command(self):
+ return self.data['handlerManifest']["uninstallCommand"]
+
+ def get_update_command(self):
+ return self.data['handlerManifest']["updateCommand"]
+
+ def get_enable_command(self):
+ return self.data['handlerManifest']["enableCommand"]
+
+ def get_disable_command(self):
+ return self.data['handlerManifest']["disableCommand"]
+
+ def is_reboot_after_install(self):
+ #TODO handle reboot after install
+ if "rebootAfterInstall" not in self.data['handlerManifest']:
+ return False
+ return self.data['handlerManifest']["rebootAfterInstall"]
+
+ def is_report_heartbeat(self):
+ if "reportHeartbeat" not in self.data['handlerManifest']:
+ return False
+ return self.data['handlerManifest']["reportHeartbeat"]
+
+ def is_update_with_install(self):
+ if "updateMode" not in self.data['handlerManifest']:
+ return False
+ if "updateMode" in self.data:
+ return self.data['handlerManifest']["updateMode"].lower() == "updatewithinstall"
+ return False
diff --git a/azurelinuxagent/distro/default/handlerFactory.py b/azurelinuxagent/distro/default/handlerFactory.py
new file mode 100644
index 0000000..98b2380
--- /dev/null
+++ b/azurelinuxagent/distro/default/handlerFactory.py
@@ -0,0 +1,40 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+from .init import InitHandler
+from .run import MainHandler
+from .scvmm import ScvmmHandler
+from .dhcp import DhcpHandler
+from .env import EnvHandler
+from .provision import ProvisionHandler
+from .resourceDisk import ResourceDiskHandler
+from .extension import ExtensionsHandler
+from .deprovision import DeprovisionHandler
+
+class DefaultHandlerFactory(object):
+ def __init__(self):
+ self.init_handler = InitHandler()
+ self.main_handler = MainHandler(self)
+ self.scvmm_handler = ScvmmHandler()
+ self.dhcp_handler = DhcpHandler()
+ self.env_handler = EnvHandler(self)
+ self.provision_handler = ProvisionHandler()
+ self.resource_disk_handler = ResourceDiskHandler()
+ self.extension_handler = ExtensionsHandler()
+ self.deprovision_handler = DeprovisionHandler()
+
diff --git a/azurelinuxagent/distro/default/init.py b/azurelinuxagent/distro/default/init.py
new file mode 100644
index 0000000..337fdea
--- /dev/null
+++ b/azurelinuxagent/distro/default/init.py
@@ -0,0 +1,49 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import azurelinuxagent.conf as conf
+import azurelinuxagent.logger as logger
+from azurelinuxagent.utils.osutil import OSUTIL
+import azurelinuxagent.utils.fileutil as fileutil
+
+
+class InitHandler(object):
+ def init(self, verbose):
+ #Init stdout log
+ level = logger.LogLevel.VERBOSE if verbose else logger.LogLevel.INFO
+ logger.add_logger_appender(logger.AppenderType.STDOUT, level)
+
+ #Init config
+ conf_file_path = OSUTIL.get_conf_file_path()
+ conf.load_conf(conf_file_path)
+
+ #Init log
+ verbose = verbose or conf.get_switch("Logs.Verbose", False)
+ level = logger.LogLevel.VERBOSE if verbose else logger.LogLevel.INFO
+ logger.add_logger_appender(logger.AppenderType.FILE, level,
+ path="/var/log/waagent.log")
+ logger.add_logger_appender(logger.AppenderType.CONSOLE, level,
+ path="/dev/console")
+
+ #Create lib dir
+ fileutil.mkdir(OSUTIL.get_lib_dir(), mode=0o700)
+ os.chdir(OSUTIL.get_lib_dir())
+
+
diff --git a/azurelinuxagent/distro/default/loader.py b/azurelinuxagent/distro/default/loader.py
new file mode 100644
index 0000000..d7dbe87
--- /dev/null
+++ b/azurelinuxagent/distro/default/loader.py
@@ -0,0 +1,28 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+def get_osutil():
+ from azurelinuxagent.distro.default.osutil import DefaultOSUtil
+ return DefaultOSUtil()
+
+def get_handlers():
+ from azurelinuxagent.distro.default.handlerFactory import DefaultHandlerFactory
+ return DefaultHandlerFactory()
+
+
diff --git a/azurelinuxagent/distro/default/osutil.py b/azurelinuxagent/distro/default/osutil.py
new file mode 100644
index 0000000..8e3fb77
--- /dev/null
+++ b/azurelinuxagent/distro/default/osutil.py
@@ -0,0 +1,657 @@
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import shutil
+import socket
+import array
+import struct
+import time
+import pwd
+import fcntl
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.textutil as textutil
+
+__RULES_FILES__ = [ "/lib/udev/rules.d/75-persistent-net-generator.rules",
+ "/etc/udev/rules.d/70-persistent-net.rules" ]
+
+"""
+Define distro specific behavior. OSUtil class defines default behavior
+for all distros. Each concrete distro classes could overwrite default behavior
+if needed.
+"""
+
+class OSUtilError(Exception):
+ pass
+
+class DefaultOSUtil(object):
+
+ def __init__(self):
+ self.lib_dir = "/var/lib/waagent"
+ self.ext_log_dir = "/var/log/azure"
+ self.dvd_mount_point = "/mnt/cdrom/secure"
+ self.ovf_env_file_path = "/mnt/cdrom/secure/ovf-env.xml"
+ self.agent_pid_file_path = "/var/run/waagent.pid"
+ self.passwd_file_path = "/etc/shadow"
+ self.home = '/home'
+ self.sshd_conf_file_path = '/etc/ssh/sshd_config'
+ self.openssl_cmd = '/usr/bin/openssl'
+ self.conf_file_path = '/etc/waagent.conf'
+ self.selinux=None
+
+ def get_lib_dir(self):
+ return self.lib_dir
+
+ def get_ext_log_dir(self):
+ return self.ext_log_dir
+
+ def get_dvd_mount_point(self):
+ return self.dvd_mount_point
+
+ def get_conf_file_path(self):
+ return self.conf_file_path
+
+ def get_ovf_env_file_path_on_dvd(self):
+ return self.ovf_env_file_path
+
+ def get_agent_pid_file_path(self):
+ return self.agent_pid_file_path
+
+ def get_openssl_cmd(self):
+ return self.openssl_cmd
+
+ def get_userentry(self, username):
+ try:
+ return pwd.getpwnam(username)
+ except KeyError:
+ return None
+
+ def is_sys_user(self, username):
+ userentry = self.get_userentry(username)
+ uidmin = None
+ try:
+ uidmin_def = fileutil.get_line_startingwith("UID_MIN",
+ "/etc/login.defs")
+ if uidmin_def is not None:
+ uidmin = int(uidmin_def.split()[1])
+ except IOError as e:
+ pass
+ if uidmin == None:
+ uidmin = 100
+ if userentry != None and userentry[2] < uidmin:
+ return True
+ else:
+ return False
+
+ def useradd(self, username, expiration=None):
+ """
+ Update password and ssh key for user account.
+ New account will be created if not exists.
+ """
+ if expiration is not None:
+ cmd = "useradd -m {0} -e {1}".format(username, expiration)
+ else:
+ cmd = "useradd -m {0}".format(username)
+ retcode, out = shellutil.run_get_output(cmd)
+ if retcode != 0:
+ raise OSUtilError(("Failed to create user account:{0}, "
+ "retcode:{1}, "
+ "output:{2}").format(username, retcode, out))
+
+ def chpasswd(self, username, password, use_salt=True, salt_type=6,
+ salt_len=10):
+ if self.is_sys_user(username):
+ raise OSUtilError(("User {0} is a system user. "
+ "Will not set passwd.").format(username))
+ passwd_hash = textutil.gen_password_hash(password, use_salt, salt_type,
+ salt_len)
+ try:
+ passwd_content = fileutil.read_file(self.passwd_file_path)
+ passwd = passwd_content.split("\n")
+ new_passwd = [x for x in passwd if not x.startswith(username)]
+ new_passwd.append("{0}:{1}:14600::::::".format(username, passwd_hash))
+ fileutil.write_file(self.passwd_file_path, "\n".join(new_passwd))
+ except IOError as e:
+ raise OSUtilError(("Failed to set password for {0}: {1}"
+ "").format(username, e))
+
+ def conf_sudoer(self, username, nopasswd):
+ # for older distros create sudoers.d
+ if not os.path.isdir('/etc/sudoers.d/'):
+ # create the /etc/sudoers.d/ directory
+ os.mkdir('/etc/sudoers.d/')
+ # add the include of sudoers.d to the /etc/sudoers
+ sudoers = '\n' + '#includedir /etc/sudoers.d/\n'
+ fileutil.append_file('/etc/sudoers', sudoers)
+ sudoer = None
+ if nopasswd:
+ sudoer = "{0} ALL = (ALL) NOPASSWD\n".format(username)
+ else:
+ sudoer = "{0} ALL = (ALL) ALL\n".format(username)
+ fileutil.append_file('/etc/sudoers.d/waagent', sudoer)
+ fileutil.chmod('/etc/sudoers.d/waagent', 0o440)
+
+ def del_root_password(self):
+ try:
+ passwd_content = fileutil.read_file(self.passwd_file_path)
+ passwd = passwd_content.split('\n')
+ new_passwd = [x for x in passwd if not x.startswith("root:")]
+ new_passwd.insert(0, "root:*LOCK*:14600::::::")
+ fileutil.write_file(self.passwd_file_path, "\n".join(new_passwd))
+ except IOError as e:
+ raise OSUtilError("Failed to delete root password:{0}".format(e))
+
+ def get_home(self):
+ return self.home
+
+ def get_pubkey_from_prv(self, file_name):
+ cmd = "{0} rsa -in {1} -pubout 2>/dev/null".format(self.openssl_cmd,
+ file_name)
+ pub = shellutil.run_get_output(cmd)[1]
+ return pub
+
+ def get_pubkey_from_crt(self, file_name):
+ cmd = "{0} x509 -in {1} -pubkey -noout".format(self.openssl_cmd,
+ file_name)
+ pub = shellutil.run_get_output(cmd)[1]
+ return pub
+
+ def _norm_path(self, filepath):
+ home = self.get_home()
+ # Expand HOME variable if present in path
+ path = os.path.normpath(filepath.replace("$HOME", home))
+ return path
+
+ def get_thumbprint_from_crt(self, file_name):
+ cmd="{0} x509 -in {1} -fingerprint -noout".format(self.openssl_cmd,
+ file_name)
+ thumbprint = shellutil.run_get_output(cmd)[1]
+ thumbprint = thumbprint.rstrip().split('=')[1].replace(':', '').upper()
+ return thumbprint
+
+ def deploy_ssh_keypair(self, username, keypair):
+ """
+ Deploy id_rsa and id_rsa.pub
+ """
+ path, thumbprint = keypair
+ path = self._norm_path(path)
+ dir_path = os.path.dirname(path)
+ fileutil.mkdir(dir_path, mode=0o700, owner=username)
+ lib_dir = self.get_lib_dir()
+ prv_path = os.path.join(lib_dir, thumbprint + '.prv')
+ if not os.path.isfile(prv_path):
+ raise OSUtilError("Can't find {0}.prv".format(thumbprint))
+ shutil.copyfile(prv_path, path)
+ pub_path = path + '.pub'
+ pub = self.get_pubkey_from_prv(prv_path)
+ fileutil.write_file(pub_path, pub)
+ self.set_selinux_context(pub_path, 'unconfined_u:object_r:ssh_home_t:s0')
+ self.set_selinux_context(path, 'unconfined_u:object_r:ssh_home_t:s0')
+ os.chmod(path, 0o644)
+ os.chmod(pub_path, 0o600)
+
+ def openssl_to_openssh(self, input_file, output_file):
+ shellutil.run("ssh-keygen -i -m PKCS8 -f {0} >> {1}".format(input_file,
+ output_file))
+
+ def deploy_ssh_pubkey(self, username, pubkey):
+ """
+ Deploy authorized_key
+ """
+ path, thumbprint, value = pubkey
+ if path is None:
+ raise OSUtilError("Publich key path is None")
+
+ path = self._norm_path(path)
+ dir_path = os.path.dirname(path)
+ fileutil.mkdir(dir_path, mode=0o700, owner=username)
+ if value is not None:
+ if not value.startswith("ssh-"):
+ raise OSUtilError("Bad public key: {0}".format(value))
+ fileutil.write_file(path, value)
+ elif thumbprint is not None:
+ lib_dir = self.get_lib_dir()
+ crt_path = os.path.join(lib_dir, thumbprint + '.crt')
+ if not os.path.isfile(crt_path):
+ raise OSUtilError("Can't find {0}.crt".format(thumbprint))
+ pub_path = os.path.join(lib_dir, thumbprint + '.pub')
+ pub = self.get_pubkey_from_crt(crt_path)
+ fileutil.write_file(pub_path, pub)
+ self.set_selinux_context(pub_path,
+ 'unconfined_u:object_r:ssh_home_t:s0')
+ self.openssl_to_openssh(pub_path, path)
+ fileutil.chmod(pub_path, 0o600)
+ else:
+ raise OSUtilError("SSH public key Fingerprint and Value are None")
+
+ self.set_selinux_context(path, 'unconfined_u:object_r:ssh_home_t:s0')
+ fileutil.chowner(path, username)
+ fileutil.chmod(path, 0o644)
+
+ def is_selinux_system(self):
+ """
+ Checks and sets self.selinux = True if SELinux is available on system.
+ """
+ if self.selinux == None:
+ if shellutil.run("which getenforce", chk_err=False) == 0:
+ self.selinux = True
+ else:
+ self.selinux = False
+ return self.selinux
+
+ def is_selinux_enforcing(self):
+ """
+ Calls shell command 'getenforce' and returns True if 'Enforcing'.
+ """
+ if self.is_selinux_system():
+ output = shellutil.run_get_output("getenforce")[1]
+ return output.startswith("Enforcing")
+ else:
+ return False
+
+ def set_selinux_enforce(self, state):
+ """
+ Calls shell command 'setenforce' with 'state'
+ and returns resulting exit code.
+ """
+ if self.is_selinux_system():
+ if state: s = '1'
+ else: s='0'
+ return shellutil.run("setenforce "+s)
+
+ def set_selinux_context(self, path, con):
+ """
+ Calls shell 'chcon' with 'path' and 'con' context.
+ Returns exit result.
+ """
+ if self.is_selinux_system():
+ return shellutil.run('chcon ' + con + ' ' + path)
+
+ def get_sshd_conf_file_path(self):
+ return self.sshd_conf_file_path
+
+ def set_ssh_client_alive_interval(self):
+ conf_file_path = self.get_sshd_conf_file_path()
+ conf = fileutil.read_file(conf_file_path).split("\n")
+ textutil.set_ssh_config(conf, "ClientAliveInterval", "180")
+ fileutil.write_file(conf_file_path, '\n'.join(conf))
+ logger.info("Configured SSH client probing to keep connections alive.")
+
+ def conf_sshd(self, disable_password):
+ option = "no" if disable_password else "yes"
+ conf_file_path = self.get_sshd_conf_file_path()
+ conf = fileutil.read_file(conf_file_path).split("\n")
+ textutil.set_ssh_config(conf, "PasswordAuthentication", option)
+ textutil.set_ssh_config(conf, "ChallengeResponseAuthentication", option)
+ fileutil.write_file(conf_file_path, "\n".join(conf))
+ logger.info("Disabled SSH password-based authentication methods.")
+
+
+ def get_dvd_device(self, dev_dir='/dev'):
+ patten=r'(sr[0-9]|hd[c-z]|cdrom[0-9])'
+ for dvd in [re.match(patten, dev) for dev in os.listdir(dev_dir)]:
+ if dvd is not None:
+ return "/dev/{0}".format(dvd.group(0))
+ raise OSUtilError("Failed to get dvd device")
+
+ def mount_dvd(self, max_retry=6, chk_err=True):
+ dvd = self.get_dvd_device()
+ mount_point = self.get_dvd_mount_point()
+ mountlist = shellutil.run_get_output("mount")[1]
+ existing = self.get_mount_point(mountlist, dvd)
+ if existing is not None: #Already mounted
+ logger.info("{0} is already mounted at {1}", dvd, existing)
+ return
+ if not os.path.isdir(mount_point):
+ os.makedirs(mount_point)
+
+ for retry in range(0, max_retry):
+ retcode = self.mount(dvd, mount_point, option="-o ro -t iso9660,udf",
+ chk_err=chk_err)
+ if retcode == 0:
+ logger.info("Successfully mounted dvd")
+ return
+ if retry < max_retry - 1:
+ logger.warn("Mount dvd failed: retry={0}, ret={1}", retry,
+ retcode)
+ time.sleep(5)
+ if chk_err:
+ raise OSUtilError("Failed to mount dvd.")
+
+ def umount_dvd(self, chk_err=True):
+ mount_point = self.get_dvd_mount_point()
+ retcode = self.umount(mount_point, chk_err=chk_err)
+ if chk_err and retcode != 0:
+ raise OSUtilError("Failed to umount dvd.")
+
+ def eject_dvd(self, chk_err=True):
+ retcode = shellutil.run("eject")
+ if chk_err and retcode != 0:
+ raise OSUtilError("Failed to eject dvd")
+
+ def load_atappix_mod(self):
+ if self.is_atapiix_mod_loaded():
+ return
+ ret, kern_version = shellutil.run_get_output("uname -r")
+ if ret != 0:
+ raise Exception("Failed to call uname -r")
+ mod_path = os.path.join('/lib/modules',
+ kern_version.strip('\n'),
+ 'kernel/drivers/ata/ata_piix.ko')
+ if not os.path.isfile(mod_path):
+ raise Exception("Can't find module file:{0}".format(mod_path))
+
+ ret, output = shellutil.run_get_output("insmod " + mod_path)
+ if ret != 0:
+ raise Exception("Error calling insmod for ATAPI CD-ROM driver")
+ if not self.is_atapiix_mod_loaded(max_retry=3):
+ raise Exception("Failed to load ATAPI CD-ROM driver")
+
+ def is_atapiix_mod_loaded(self, max_retry=1):
+ for retry in range(0, max_retry):
+ ret = shellutil.run("lsmod | grep ata_piix", chk_err=False)
+ if ret == 0:
+ logger.info("Module driver for ATAPI CD-ROM is already present.")
+ return True
+ if retry < max_retry - 1:
+ time.sleep(1)
+ return False
+
+ def mount(self, dvd, mount_point, option="", chk_err=True):
+ cmd = "mount {0} {1} {2}".format(dvd, option, mount_point)
+ return shellutil.run_get_output(cmd, chk_err)[0]
+
+ def umount(self, mount_point, chk_err=True):
+ return shellutil.run("umount {0}".format(mount_point), chk_err=chk_err)
+
+ def allow_dhcp_broadcast(self):
+ #Open DHCP port if iptables is enabled.
+ # We supress error logging on error.
+ shellutil.run("iptables -D INPUT -p udp --dport 68 -j ACCEPT",
+ chk_err=False)
+ shellutil.run("iptables -I INPUT -p udp --dport 68 -j ACCEPT",
+ chk_err=False)
+
+ def gen_transport_cert(self):
+ """
+ Create ssl certificate for https communication with endpoint server.
+ """
+ cmd = ("{0} req -x509 -nodes -subj /CN=LinuxTransport -days 32768 "
+ "-newkey rsa:2048 -keyout TransportPrivate.pem "
+ "-out TransportCert.pem").format(self.openssl_cmd)
+ shellutil.run(cmd)
+
+ def remove_rules_files(self, rules_files=__RULES_FILES__):
+ lib_dir = self.get_lib_dir()
+ for src in rules_files:
+ file_name = fileutil.base_name(src)
+ dest = os.path.join(lib_dir, file_name)
+ if os.path.isfile(dest):
+ os.remove(dest)
+ if os.path.isfile(src):
+ logger.warn("Move rules file {0} to {1}", file_name, dest)
+ shutil.move(src, dest)
+
+ def restore_rules_files(self, rules_files=__RULES_FILES__):
+ lib_dir = self.get_lib_dir()
+ for dest in rules_files:
+ filename = fileutil.base_name(dest)
+ src = os.path.join(lib_dir, filename)
+ if os.path.isfile(dest):
+ continue
+ if os.path.isfile(src):
+ logger.warn("Move rules file {0} to {1}", filename, dest)
+ shutil.move(src, dest)
+
+ def get_mac_addr(self):
+ """
+ Convienience function, returns mac addr bound to
+ first non-loobback interface.
+ """
+ ifname=''
+ while len(ifname) < 2 :
+ ifname=self.get_first_if()[0]
+ addr = self.get_if_mac(ifname)
+ return textutil.hexstr_to_bytearray(addr)
+
+ def get_if_mac(self, ifname):
+ """
+ Return the mac-address bound to the socket.
+ """
+ sock = socket.socket(socket.AF_INET,
+ socket.SOCK_DGRAM,
+ socket.IPPROTO_UDP)
+ param = struct.pack('256s', (ifname[:15]+('\0'*241)).encode('latin-1'))
+ info = fcntl.ioctl(sock.fileno(), 0x8927, param)
+ return ''.join(['%02X' % textutil.str_to_ord(char) for char in info[18:24]])
+
+ def get_first_if(self):
+ """
+ Return the interface name, and ip addr of the
+ first active non-loopback interface.
+ """
+ iface=''
+ expected=16 # how many devices should I expect...
+ struct_size=40 # for 64bit the size is 40 bytes
+ sock = socket.socket(socket.AF_INET,
+ socket.SOCK_DGRAM,
+ socket.IPPROTO_UDP)
+ buff=array.array('B', b'\0' * (expected * struct_size))
+ param = struct.pack('iL',
+ expected*struct_size,
+ buff.buffer_info()[0])
+ ret = fcntl.ioctl(sock.fileno(), 0x8912, param)
+ retsize=(struct.unpack('iL', ret)[0])
+ if retsize == (expected * struct_size):
+ logger.warn(('SIOCGIFCONF returned more than {0} up '
+ 'network interfaces.'), expected)
+ sock = buff.tostring()
+ for i in range(0, struct_size * expected, struct_size):
+ iface=sock[i:i+16].split(b'\0', 1)[0]
+ if iface == b'lo':
+ continue
+ else:
+ break
+ return iface.decode('latin-1'), socket.inet_ntoa(sock[i+20:i+24])
+
+ def is_missing_default_route(self):
+ routes = shellutil.run_get_output("route -n")[1]
+ for route in routes.split("\n"):
+ if route.startswith("0.0.0.0 ") or route.startswith("default "):
+ return False
+ return True
+
+ def get_if_name(self):
+ return self.get_first_if()[0]
+
+ def get_ip4_addr(self):
+ return self.get_first_if()[1]
+
+ def set_route_for_dhcp_broadcast(self, ifname):
+ return shellutil.run("route add 255.255.255.255 dev {0}".format(ifname),
+ chk_err=False)
+
+ def remove_route_for_dhcp_broadcast(self, ifname):
+ shellutil.run("route del 255.255.255.255 dev {0}".format(ifname),
+ chk_err=False)
+
+ def is_dhcp_enabled(self):
+ return False
+
+ def stop_dhcp_service(self):
+ pass
+
+ def start_dhcp_service(self):
+ pass
+
+ def start_network(self):
+ pass
+
+ def start_agent_service(self):
+ pass
+
+ def stop_agent_service(self):
+ pass
+
+ def register_agent_service(self):
+ pass
+
+ def unregister_agent_service(self):
+ pass
+
+ def restart_ssh_service(self):
+ pass
+
+ def route_add(self, net, mask, gateway):
+ """
+ Add specified route using /sbin/route add -net.
+ """
+ cmd = ("/sbin/route add -net "
+ "{0} netmask {1} gw {2}").format(net, mask, gateway)
+ return shellutil.run(cmd, chk_err=False)
+
+ def get_dhcp_pid(self):
+ ret= shellutil.run_get_output("pidof dhclient")
+ return ret[1] if ret[0] == 0 else None
+
+ def set_hostname(self, hostname):
+ fileutil.write_file('/etc/hostname', hostname)
+ shellutil.run("hostname {0}".format(hostname), chk_err=False)
+
+ def set_dhcp_hostname(self, hostname):
+ autosend = r'^[^#]*?send\s*host-name.*?(<hostname>|gethostname[(,)])'
+ dhclient_files = ['/etc/dhcp/dhclient.conf', '/etc/dhcp3/dhclient.conf']
+ for conf_file in dhclient_files:
+ if not os.path.isfile(conf_file):
+ continue
+ if fileutil.findstr_in_file(conf_file, autosend):
+ #Return if auto send host-name is configured
+ return
+ fileutil.update_conf_file(conf_file,
+ 'send host-name',
+ 'send host-name {0}'.format(hostname))
+
+ def restart_if(self, ifname):
+ shellutil.run("ifdown {0} && ifup {1}".format(ifname, ifname))
+
+ def publish_hostname(self, hostname):
+ self.set_dhcp_hostname(hostname)
+ ifname = self.get_if_name()
+ self.restart_if(ifname)
+
+ def set_scsi_disks_timeout(self, timeout):
+ for dev in os.listdir("/sys/block"):
+ if dev.startswith('sd'):
+ self.set_block_device_timeout(dev, timeout)
+
+ def set_block_device_timeout(self, dev, timeout):
+ if dev is not None and timeout is not None:
+ file_path = "/sys/block/{0}/device/timeout".format(dev)
+ content = fileutil.read_file(file_path)
+ original = content.splitlines()[0].rstrip()
+ if original != timeout:
+ fileutil.write_file(file_path, timeout)
+ logger.info("Set block dev timeout: {0} with timeout: {1}",
+ dev, timeout)
+
+ def get_mount_point(self, mountlist, device):
+ """
+ Example of mountlist:
+ /dev/sda1 on / type ext4 (rw)
+ proc on /proc type proc (rw)
+ sysfs on /sys type sysfs (rw)
+ devpts on /dev/pts type devpts (rw,gid=5,mode=620)
+ tmpfs on /dev/shm type tmpfs
+ (rw,rootcontext="system_u:object_r:tmpfs_t:s0")
+ none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw)
+ /dev/sdb1 on /mnt/resource type ext4 (rw)
+ """
+ if (mountlist and device):
+ for entry in mountlist.split('\n'):
+ if(re.search(device, entry)):
+ tokens = entry.split()
+ #Return the 3rd column of this line
+ return tokens[2] if len(tokens) > 2 else None
+ return None
+
+ def device_for_ide_port(self, port_id):
+ """
+ Return device name attached to ide port 'n'.
+ """
+ if port_id > 3:
+ return None
+ g0 = "00000000"
+ if port_id > 1:
+ g0 = "00000001"
+ port_id = port_id - 2
+ device = None
+ path = "/sys/bus/vmbus/devices/"
+ for vmbus in os.listdir(path):
+ deviceid = fileutil.read_file(os.path.join(path, vmbus, "device_id"))
+ guid = deviceid.lstrip('{').split('-')
+ if guid[0] == g0 and guid[1] == "000" + text(port_id):
+ for root, dirs, files in os.walk(path + vmbus):
+ if root.endswith("/block"):
+ device = dirs[0]
+ break
+ else : #older distros
+ for d in dirs:
+ if ':' in d and "block" == d.split(':')[0]:
+ device = d.split(':')[1]
+ break
+ break
+ return device
+
+ def del_account(self, username):
+ if self.is_sys_user(username):
+ logger.error("{0} is a system user. Will not delete it.", username)
+ shellutil.run("> /var/run/utmp")
+ shellutil.run("userdel -f -r " + username)
+ #Remove user from suders
+ if os.path.isfile("/etc/suders.d/waagent"):
+ try:
+ content = fileutil.read_file("/etc/sudoers.d/waagent")
+ sudoers = content.split("\n")
+ sudoers = [x for x in sudoers if username not in x]
+ fileutil.write_file("/etc/sudoers.d/waagent",
+ "\n".join(sudoers))
+ except IOError as e:
+ raise OSUtilError("Failed to remove sudoer: {0}".format(e))
+
+ def decode_customdata(self, data):
+ return data
+
+ def get_total_mem(self):
+ cmd = "grep MemTotal /proc/meminfo |awk '{print $2}'"
+ ret = shellutil.run_get_output(cmd)
+ if ret[0] == 0:
+ return int(ret[1])/1024
+ else:
+ raise OSUtilError("Failed to get total memory: {0}".format(ret[1]))
+
+ def get_processor_cores(self):
+ ret = shellutil.run_get_output("grep 'processor.*:' /proc/cpuinfo |wc -l")
+ if ret[0] == 0:
+ return int(ret[1])
+ else:
+ raise OSUtilError("Failed to get procerssor cores")
+
diff --git a/azurelinuxagent/distro/default/provision.py b/azurelinuxagent/distro/default/provision.py
new file mode 100644
index 0000000..1e9c459
--- /dev/null
+++ b/azurelinuxagent/distro/default/provision.py
@@ -0,0 +1,165 @@
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+"""
+Provision handler
+"""
+
+import os
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.conf as conf
+from azurelinuxagent.event import add_event, WALAEventOperation
+from azurelinuxagent.exception import *
+from azurelinuxagent.utils.osutil import OSUTIL, OSUtilError
+import azurelinuxagent.protocol as prot
+import azurelinuxagent.protocol.ovfenv as ovf
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.fileutil as fileutil
+
+CUSTOM_DATA_FILE="CustomData"
+
+class ProvisionHandler(object):
+
+ def process(self):
+ #If provision is not enabled, return
+ if not conf.get_switch("Provisioning.Enabled", True):
+ logger.info("Provisioning is disabled. Skip.")
+ return
+
+ provisioned = os.path.join(OSUTIL.get_lib_dir(), "provisioned")
+ if os.path.isfile(provisioned):
+ return
+
+ logger.info("run provision handler.")
+ protocol = prot.FACTORY.get_default_protocol()
+ try:
+ status = prot.ProvisionStatus(status="NotReady",
+ subStatus="Provision started")
+ protocol.report_provision_status(status)
+
+ self.provision()
+ fileutil.write_file(provisioned, "")
+ thumbprint = self.reg_ssh_host_key()
+
+ logger.info("Finished provisioning")
+ status = prot.ProvisionStatus(status="Ready")
+ status.properties.certificateThumbprint = thumbprint
+ protocol.report_provision_status(status)
+
+ add_event(name="WALA", is_success=True, message="",
+ op=WALAEventOperation.Provision)
+ except ProvisionError as e:
+ logger.error("Provision failed: {0}", e)
+ status = prot.ProvisionStatus(status="NotReady",
+ subStatus= text(e))
+ protocol.report_provision_status(status)
+ add_event(name="WALA", is_success=False, message=text(e),
+ op=WALAEventOperation.Provision)
+
+ def reg_ssh_host_key(self):
+ keypair_type = conf.get("Provisioning.SshHostKeyPairType", "rsa")
+ if conf.get_switch("Provisioning.RegenerateSshHostKeyPair"):
+ shellutil.run("rm -f /etc/ssh/ssh_host_*key*")
+ shellutil.run(("ssh-keygen -N '' -t {0} -f /etc/ssh/ssh_host_{1}_key"
+ "").format(keypair_type, keypair_type))
+ thumbprint = self.get_ssh_host_key_thumbprint(keypair_type)
+ return thumbprint
+
+ def get_ssh_host_key_thumbprint(self, keypair_type):
+ cmd = "ssh-keygen -lf /etc/ssh/ssh_host_{0}_key.pub".format(keypair_type)
+ ret = shellutil.run_get_output(cmd)
+ if ret[0] == 0:
+ return ret[1].rstrip().split()[1].replace(':', '')
+ else:
+ raise ProvisionError(("Failed to generate ssh host key: "
+ "ret={0}, out= {1}").format(ret[0], ret[1]))
+
+
+ def provision(self):
+ logger.info("Copy ovf-env.xml.")
+ try:
+ ovfenv = ovf.copy_ovf_env()
+ except prot.ProtocolError as e:
+ raise ProvisionError("Failed to copy ovf-env.xml: {0}".format(e))
+
+ logger.info("Handle ovf-env.xml.")
+ try:
+ logger.info("Set host name.")
+ OSUTIL.set_hostname(ovfenv.hostname)
+
+ logger.info("Publish host name.")
+ OSUTIL.publish_hostname(ovfenv.hostname)
+
+ self.config_user_account(ovfenv)
+
+ self.save_customdata(ovfenv)
+
+ if conf.get_switch("Provisioning.DeleteRootPassword"):
+ OSUTIL.del_root_password()
+ except OSUtilError as e:
+ raise ProvisionError("Failed to handle ovf-env.xml: {0}".format(e))
+
+ def config_user_account(self, ovfenv):
+ logger.info("Create user account if not exists")
+ OSUTIL.useradd(ovfenv.username)
+
+ if ovfenv.user_password is not None:
+ logger.info("Set user password.")
+ use_salt = conf.get_switch("Provision.UseSalt", True)
+ salt_type = conf.get_switch("Provision.SaltType", 6)
+ OSUTIL.chpasswd(ovfenv.username, ovfenv.user_password,
+ use_salt,salt_type)
+
+ logger.info("Configure sudoer")
+ OSUTIL.conf_sudoer(ovfenv.username, ovfenv.user_password is None)
+
+ logger.info("Configure sshd")
+ OSUTIL.conf_sshd(ovfenv.disable_ssh_password_auth)
+
+ #Disable selinux temporary
+ sel = OSUTIL.is_selinux_enforcing()
+ if sel:
+ OSUTIL.set_selinux_enforce(0)
+
+ self.deploy_ssh_pubkeys(ovfenv)
+ self.deploy_ssh_keypairs(ovfenv)
+
+ if sel:
+ OSUTIL.set_selinux_enforce(1)
+
+ OSUTIL.restart_ssh_service()
+
+ def save_customdata(self, ovfenv):
+ logger.info("Save custom data")
+ customdata = ovfenv.customdata
+ if customdata is None:
+ return
+ lib_dir = OSUTIL.get_lib_dir()
+ fileutil.write_file(os.path.join(lib_dir, CUSTOM_DATA_FILE),
+ OSUTIL.decode_customdata(customdata))
+
+ def deploy_ssh_pubkeys(self, ovfenv):
+ for pubkey in ovfenv.ssh_pubkeys:
+ logger.info("Deploy ssh public key.")
+ OSUTIL.deploy_ssh_pubkey(ovfenv.username, pubkey)
+
+ def deploy_ssh_keypairs(self, ovfenv):
+ for keypair in ovfenv.ssh_keypairs:
+ logger.info("Deploy ssh key pairs.")
+ OSUTIL.deploy_ssh_keypair(ovfenv.username, keypair)
+
diff --git a/azurelinuxagent/distro/default/resourceDisk.py b/azurelinuxagent/distro/default/resourceDisk.py
new file mode 100644
index 0000000..d4ef1c9
--- /dev/null
+++ b/azurelinuxagent/distro/default/resourceDisk.py
@@ -0,0 +1,166 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import threading
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.conf as conf
+from azurelinuxagent.utils.osutil import OSUTIL
+from azurelinuxagent.event import add_event, WALAEventOperation
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+from azurelinuxagent.exception import ResourceDiskError
+
+DATALOSS_WARNING_FILE_NAME="DATALOSS_WARNING_README.txt"
+DATA_LOSS_WARNING="""\
+WARNING: THIS IS A TEMPORARY DISK.
+
+Any data stored on this drive is SUBJECT TO LOSS and THERE IS NO WAY TO RECOVER IT.
+
+Please do not use this disk for storing any personal or application data.
+
+For additional details to please refer to the MSDN documentation at : http://msdn.microsoft.com/en-us/library/windowsazure/jj672979.aspx
+"""
+
+class ResourceDiskHandler(object):
+
+ def start_activate_resource_disk(self):
+ disk_thread = threading.Thread(target = self.run)
+ disk_thread.start()
+
+ def run(self):
+ mount_point = None
+ if conf.get_switch("ResourceDisk.Format", False):
+ mount_point = self.activate_resource_disk()
+ if mount_point is not None and \
+ conf.get_switch("ResourceDisk.EnableSwap", False):
+ self.enable_swap(mount_point)
+
+ def activate_resource_disk(self):
+ logger.info("Activate resource disk")
+ try:
+ mount_point = conf.get("ResourceDisk.MountPoint", "/mnt/resource")
+ fs = conf.get("ResourceDisk.Filesystem", "ext3")
+ mount_point = self.mount_resource_disk(mount_point, fs)
+ warning_file = os.path.join(mount_point, DATALOSS_WARNING_FILE_NAME)
+ try:
+ fileutil.write_file(warning_file, DATA_LOSS_WARNING)
+ except IOError as e:
+ logger.warn("Failed to write data loss warnning:{0}", e)
+ return mount_point
+ except ResourceDiskError as e:
+ logger.error("Failed to mount resource disk {0}", e)
+ add_event(name="WALA", is_success=False, message=text(e),
+ op=WALAEventOperation.ActivateResourceDisk)
+
+ def enable_swap(self, mount_point):
+ logger.info("Enable swap")
+ try:
+ size_mb = conf.get_int("ResourceDisk.SwapSizeMB", 0)
+ self.create_swap_space(mount_point, size_mb)
+ except ResourceDiskError as e:
+ logger.error("Failed to enable swap {0}", e)
+
+ def mount_resource_disk(self, mount_point, fs):
+ device = OSUTIL.device_for_ide_port(1)
+ if device is None:
+ raise ResourceDiskError("unable to detect disk topology")
+
+ device = "/dev/" + device
+ mountlist = shellutil.run_get_output("mount")[1]
+ existing = OSUTIL.get_mount_point(mountlist, device)
+
+ if(existing):
+ logger.info("Resource disk {0}1 is already mounted", device)
+ return existing
+
+ fileutil.mkdir(mount_point, mode=0o755)
+
+ logger.info("Detect GPT...")
+ partition = device + "1"
+ ret = shellutil.run_get_output("parted {0} print".format(device))
+ if ret[0]:
+ raise ResourceDiskError("({0}) {1}".format(device, ret[1]))
+
+ if "gpt" in ret[1]:
+ logger.info("GPT detected")
+ logger.info("Get GPT partitions")
+ parts = [x for x in ret[1].split("\n") if re.match("^\s*[0-9]+", x)]
+ logger.info("Found more than {0} GPT partitions.", len(parts))
+ if len(parts) > 1:
+ logger.info("Remove old GPT partitions")
+ for i in range(1, len(parts) + 1):
+ logger.info("Remove partition: {0}", i)
+ shellutil.run("parted {0} rm {1}".format(device, i))
+
+ logger.info("Create a new GPT partition using entire disk space")
+ shellutil.run("parted {0} mkpart primary 0% 100%".format(device))
+
+ logger.info("Format partition: {0} with fstype {1}",partition,fs)
+ shellutil.run("mkfs." + fs + " " + partition + " -F")
+ else:
+ logger.info("GPT not detected")
+ logger.info("Check fstype")
+ ret = shellutil.run_get_output("sfdisk -q -c {0} 1".format(device))
+ if ret[1].rstrip() == "7" and fs != "ntfs":
+ logger.info("The partition is formatted with ntfs")
+ logger.info("Format partition: {0} with fstype {1}",partition,fs)
+ shellutil.run("sfdisk -c {0} 1 83".format(device))
+ shellutil.run("mkfs." + fs + " " + partition + " -F")
+
+ logger.info("Mount resource disk")
+ ret = shellutil.run("mount {0} {1}".format(partition, mount_point),
+ chk_err=False)
+ if ret:
+ logger.warn("Failed to mount resource disk. Retry mounting")
+ shellutil.run("mkfs." + fs + " " + partition + " -F")
+ ret = shellutil.run("mount {0} {1}".format(partition, mount_point))
+ if ret:
+ raise ResourceDiskError("({0}) {1}".format(partition, ret))
+
+ logger.info("Resource disk ({0}) is mounted at {1} with fstype {2}",
+ device, mount_point, fs)
+ return mount_point
+
+ def create_swap_space(self, mount_point, size_mb):
+ size_kb = size_mb * 1024
+ size = size_kb * 1024
+ swapfile = os.path.join(mount_point, 'swapfile')
+ swaplist = shellutil.run_get_output("swapon -s")[1]
+
+ if swapfile in swaplist and os.path.getsize(swapfile) == size:
+ logger.info("Swap already enabled")
+ return
+
+ if os.path.isfile(swapfile) and os.path.getsize(swapfile) != size:
+ logger.info("Remove old swap file")
+ shellutil.run("swapoff -a", chk_err=False)
+ os.remove(swapfile)
+
+ if not os.path.isfile(swapfile):
+ logger.info("Create swap file")
+ shellutil.run(("dd if=/dev/zero of={0} bs=1024 "
+ "count={1}").format(swapfile, size_kb))
+ shellutil.run("mkswap {0}".format(swapfile))
+ if shellutil.run("swapon {0}".format(swapfile)):
+ raise ResourceDiskError("{0}".format(swapfile))
+ logger.info("Enabled {0}KB of swap at {1}".format(size_kb, swapfile))
+
diff --git a/azurelinuxagent/distro/default/run.py b/azurelinuxagent/distro/default/run.py
new file mode 100644
index 0000000..13880b4
--- /dev/null
+++ b/azurelinuxagent/distro/default/run.py
@@ -0,0 +1,86 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import time
+import sys
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.conf as conf
+from azurelinuxagent.metadata import AGENT_LONG_NAME, AGENT_VERSION, \
+ DISTRO_NAME, DISTRO_VERSION, \
+ DISTRO_FULL_NAME, PY_VERSION_MAJOR, \
+ PY_VERSION_MINOR, PY_VERSION_MICRO
+import azurelinuxagent.protocol as prot
+import azurelinuxagent.event as event
+from azurelinuxagent.utils.osutil import OSUTIL
+import azurelinuxagent.utils.fileutil as fileutil
+
+
+class MainHandler(object):
+ def __init__(self, handlers):
+ self.handlers = handlers
+
+ def run(self):
+ logger.info("{0} Version:{1}", AGENT_LONG_NAME, AGENT_VERSION)
+ logger.info("OS: {0} {1}", DISTRO_NAME, DISTRO_VERSION)
+ logger.info("Python: {0}.{1}.{2}", PY_VERSION_MAJOR, PY_VERSION_MINOR,
+ PY_VERSION_MICRO)
+
+ event.enable_unhandled_err_dump("Azure Linux Agent")
+ fileutil.write_file(OSUTIL.get_agent_pid_file_path(), text(os.getpid()))
+
+ if conf.get_switch("DetectScvmmEnv", False):
+ if self.handlers.scvmm_handler.detect_scvmm_env():
+ return
+
+ self.handlers.dhcp_handler.probe()
+
+ prot.detect_default_protocol()
+
+ event.EventMonitor().start()
+
+ self.handlers.provision_handler.process()
+
+ if conf.get_switch("ResourceDisk.Format", False):
+ self.handlers.resource_disk_handler.start_activate_resource_disk()
+
+ self.handlers.env_handler.start()
+
+ protocol = prot.FACTORY.get_default_protocol()
+ while True:
+
+ #Handle extensions
+ h_status_list = self.handlers.extension_handler.process()
+
+ #Report status
+ vm_status = prot.VMStatus()
+ vm_status.vmAgent.agentVersion = AGENT_LONG_NAME
+ vm_status.vmAgent.status = "Ready"
+ vm_status.vmAgent.message = "Guest Agent is running"
+ for h_status in h_status_list:
+ vm_status.extensionHandlers.append(h_status)
+ try:
+ logger.info("Report vm status")
+ protocol.report_status(vm_status)
+ except prot.ProtocolError as e:
+ logger.error("Failed to report vm status: {0}", e)
+
+ time.sleep(25)
+
diff --git a/azurelinuxagent/distro/default/scvmm.py b/azurelinuxagent/distro/default/scvmm.py
new file mode 100644
index 0000000..18fad4b
--- /dev/null
+++ b/azurelinuxagent/distro/default/scvmm.py
@@ -0,0 +1,47 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import subprocess
+import azurelinuxagent.logger as logger
+from azurelinuxagent.utils.osutil import OSUTIL
+
+VMM_CONF_FILE_NAME = "linuxosconfiguration.xml"
+VMM_STARTUP_SCRIPT_NAME= "install"
+
+class ScvmmHandler(object):
+
+ def detect_scvmm_env(self):
+ logger.info("Detecting Microsoft System Center VMM Environment")
+ OSUTIL.mount_dvd(max_retry=1, chk_err=False)
+ mount_point = OSUTIL.get_dvd_mount_point()
+ found = os.path.isfile(os.path.join(mount_point, VMM_CONF_FILE_NAME))
+ if found:
+ self.start_scvmm_agent()
+ else:
+ OSUTIL.umount_dvd(chk_err=False)
+ return found
+
+ def start_scvmm_agent(self):
+ logger.info("Starting Microsoft System Center VMM Initialization "
+ "Process")
+ mount_point = OSUTIL.get_dvd_mount_point()
+ startup_script = os.path.join(mount_point, VMM_STARTUP_SCRIPT_NAME)
+ subprocess.Popen(["/bin/bash", startup_script, "-p " + mount_point])
+
diff --git a/azurelinuxagent/distro/loader.py b/azurelinuxagent/distro/loader.py
new file mode 100644
index 0000000..0060a7f
--- /dev/null
+++ b/azurelinuxagent/distro/loader.py
@@ -0,0 +1,46 @@
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import azurelinuxagent.logger as logger
+from azurelinuxagent.metadata import DISTRO_NAME
+import azurelinuxagent.distro.default.loader as default_loader
+
+
+def get_distro_loader():
+ try:
+ logger.verb("Loading distro implemetation from: {0}", DISTRO_NAME)
+ pkg_name = "azurelinuxagent.distro.{0}.loader".format(DISTRO_NAME)
+ return __import__(pkg_name, fromlist="loader")
+ except ImportError as e:
+ logger.warn("Unable to load distro implemetation for {0}.", DISTRO_NAME)
+ logger.warn("Use default distro implemetation instead.")
+ return default_loader
+
+DISTRO_LOADER = get_distro_loader()
+
+def get_osutil():
+ try:
+ return DISTRO_LOADER.get_osutil()
+ except AttributeError:
+ return default_loader.get_osutil()
+
+def get_handlers():
+ try:
+ return DISTRO_LOADER.get_handlers()
+ except AttributeError:
+ return default_loader.get_handlers()
+
diff --git a/azurelinuxagent/distro/oracle/__init__.py b/azurelinuxagent/distro/oracle/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/oracle/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/oracle/loader.py b/azurelinuxagent/distro/oracle/loader.py
new file mode 100644
index 0000000..379f027
--- /dev/null
+++ b/azurelinuxagent/distro/oracle/loader.py
@@ -0,0 +1,25 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from azurelinuxagent.metadata import DISTRO_NAME, DISTRO_VERSION
+import azurelinuxagent.distro.redhat.loader as redhat
+
+def get_osutil():
+ return redhat.get_osutil()
+
diff --git a/azurelinuxagent/distro/redhat/__init__.py b/azurelinuxagent/distro/redhat/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/redhat/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/redhat/loader.py b/azurelinuxagent/distro/redhat/loader.py
new file mode 100644
index 0000000..911e74d
--- /dev/null
+++ b/azurelinuxagent/distro/redhat/loader.py
@@ -0,0 +1,28 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from azurelinuxagent.metadata import DISTRO_NAME, DISTRO_VERSION
+
+def get_osutil():
+ from azurelinuxagent.distro.redhat.osutil import Redhat6xOSUtil, RedhatOSUtil
+ if DISTRO_VERSION < "7":
+ return Redhat6xOSUtil()
+ else:
+ return RedhatOSUtil()
+
diff --git a/azurelinuxagent/distro/redhat/osutil.py b/azurelinuxagent/distro/redhat/osutil.py
new file mode 100644
index 0000000..c6c3016
--- /dev/null
+++ b/azurelinuxagent/distro/redhat/osutil.py
@@ -0,0 +1,148 @@
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import pwd
+import shutil
+import socket
+import array
+import struct
+import fcntl
+import time
+import base64
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text, bytebuffer
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.textutil as textutil
+from azurelinuxagent.distro.default.osutil import DefaultOSUtil, OSUtilError
+
+class Redhat6xOSUtil(DefaultOSUtil):
+ def __init__(self):
+ super(Redhat6xOSUtil, self).__init__()
+ self.sshd_conf_file_path = '/etc/ssh/sshd_config'
+ self.openssl_cmd = '/usr/bin/openssl'
+ self.conf_file_path = '/etc/waagent.conf'
+ self.selinux=None
+
+ def start_network(self):
+ return shellutil.run("/sbin/service networking start", chk_err=False)
+
+ def restart_ssh_service(self):
+ return shellutil.run("/sbin/service sshd condrestart", chk_err=False)
+
+ def stop_agent_service(self):
+ return shellutil.run("/sbin/service waagent stop", chk_err=False)
+
+ def start_agent_service(self):
+ return shellutil.run("/sbin/service waagent start", chk_err=False)
+
+ def register_agent_service(self):
+ return shellutil.run("chkconfig --add waagent", chk_err=False)
+
+ def unregister_agent_service(self):
+ return shellutil.run("chkconfig --del waagent", chk_err=False)
+
+ def asn1_to_ssh_rsa(self, pubkey):
+ lines = pubkey.split("\n")
+ lines = [x for x in lines if not x.startswith("----")]
+ base64_encoded = "".join(lines)
+ try:
+ #TODO remove pyasn1 dependency
+ from pyasn1.codec.der import decoder as der_decoder
+ der_encoded = base64.b64decode(base64_encoded)
+ der_encoded = der_decoder.decode(der_encoded)[0][1]
+ key = der_decoder.decode(self.bits_to_bytes(der_encoded))[0]
+ n=key[0]
+ e=key[1]
+ keydata = bytearray()
+ keydata.extend(struct.pack('>I', len("ssh-rsa")))
+ keydata.extend(b"ssh-rsa")
+ keydata.extend(struct.pack('>I', len(self.num_to_bytes(e))))
+ keydata.extend(self.num_to_bytes(e))
+ keydata.extend(struct.pack('>I', len(self.num_to_bytes(n)) + 1))
+ keydata.extend(b"\0")
+ keydata.extend(self.num_to_bytes(n))
+ keydata_base64 = base64.b64encode(bytebuffer(keydata))
+ return text(b"ssh-rsa " + keydata_base64 + b"\n",
+ encoding='utf-8')
+ except ImportError as e:
+ raise OSUtilError("Failed to load pyasn1.codec.der")
+
+ def num_to_bytes(self, num):
+ """
+ Pack number into bytes. Retun as string.
+ """
+ result = bytearray()
+ while num:
+ result.append(num & 0xFF)
+ num >>= 8
+ result.reverse()
+ return result
+
+ def bits_to_bytes(self, bits):
+ """
+ Convert an array contains bits, [0,1] to a byte array
+ """
+ index = 7
+ byte_array = bytearray()
+ curr = 0
+ for bit in bits:
+ curr = curr | (bit << index)
+ index = index - 1
+ if index == -1:
+ byte_array.append(curr)
+ curr = 0
+ index = 7
+ return bytes(byte_array)
+
+ def openssl_to_openssh(self, input_file, output_file):
+ pubkey = fileutil.read_file(input_file)
+ ssh_rsa_pubkey = self.asn1_to_ssh_rsa(pubkey)
+ fileutil.write_file(output_file, ssh_rsa_pubkey)
+
+ #Override
+ def get_dhcp_pid(self):
+ ret= shellutil.run_get_output("pidof dhclient")
+ return ret[1] if ret[0] == 0 else None
+
+class RedhatOSUtil(Redhat6xOSUtil):
+ def __init__(self):
+ super(RedhatOSUtil, self).__init__()
+
+ def set_hostname(self, hostname):
+ super(RedhatOSUtil, self).set_hostname(hostname)
+ fileutil.update_conf_file('/etc/sysconfig/network',
+ 'HOSTNAME',
+ 'HOSTNAME={0}'.format(hostname))
+
+ def set_dhcp_hostname(self, hostname):
+ ifname = self.get_if_name()
+ filepath = "/etc/sysconfig/network-scripts/ifcfg-{0}".format(ifname)
+ fileutil.update_conf_file(filepath,
+ 'DHCP_HOSTNAME',
+ 'DHCP_HOSTNAME={0}'.format(hostname))
+
+ def register_agent_service(self):
+ return shellutil.run("systemctl enable waagent", chk_err=False)
+
+ def unregister_agent_service(self):
+ return shellutil.run("systemctl disable waagent", chk_err=False)
+
+
diff --git a/azurelinuxagent/distro/suse/__init__.py b/azurelinuxagent/distro/suse/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/suse/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/suse/loader.py b/azurelinuxagent/distro/suse/loader.py
new file mode 100644
index 0000000..e38aa17
--- /dev/null
+++ b/azurelinuxagent/distro/suse/loader.py
@@ -0,0 +1,29 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from azurelinuxagent.metadata import DISTRO_NAME, DISTRO_VERSION, DISTRO_FULL_NAME
+
+def get_osutil():
+ from azurelinuxagent.distro.suse.osutil import SUSE11OSUtil, SUSEOSUtil
+ if DISTRO_FULL_NAME=='SUSE Linux Enterprise Server' and DISTRO_VERSION < '12' \
+ or DISTRO_FULL_NAME == 'openSUSE' and DISTRO_VERSION < '13.2':
+ return SUSE11OSUtil()
+ else:
+ return SUSEOSUtil()
+
diff --git a/azurelinuxagent/distro/suse/osutil.py b/azurelinuxagent/distro/suse/osutil.py
new file mode 100644
index 0000000..870e0b7
--- /dev/null
+++ b/azurelinuxagent/distro/suse/osutil.py
@@ -0,0 +1,88 @@
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import pwd
+import shutil
+import socket
+import array
+import struct
+import fcntl
+import time
+import azurelinuxagent.logger as logger
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.textutil as textutil
+from azurelinuxagent.metadata import DISTRO_NAME, DISTRO_VERSION, DISTRO_FULL_NAME
+from azurelinuxagent.distro.default.osutil import DefaultOSUtil
+
+class SUSE11OSUtil(DefaultOSUtil):
+ def __init__(self):
+ super(SUSE11OSUtil, self).__init__()
+ self.dhclient_name='dhcpcd'
+
+ def set_hostname(self, hostname):
+ fileutil.write_file('/etc/HOSTNAME', hostname)
+ shellutil.run("hostname {0}".format(hostname), chk_err=False)
+
+ def get_dhcp_pid(self):
+ ret= shellutil.run_get_output("pidof {0}".format(self.dhclient_name))
+ return ret[1] if ret[0] == 0 else None
+
+ def is_dhcp_enabled(self):
+ return True
+
+ def stop_dhcp_service(self):
+ cmd = "/sbin/service {0} stop".format(self.dhclient_name)
+ return shellutil.run(cmd, chk_err=False)
+
+ def start_dhcp_service(self):
+ cmd = "/sbin/service {0} start".format(self.dhclient_name)
+ return shellutil.run(cmd, chk_err=False)
+
+ def start_network(self) :
+ return shellutil.run("/sbin/service start network", chk_err=False)
+
+ def restart_ssh_service(self):
+ return shellutil.run("/sbin/service sshd restart", chk_err=False)
+
+ def stop_agent_service(self):
+ return shellutil.run("/sbin/service waagent stop", chk_err=False)
+
+ def start_agent_service(self):
+ return shellutil.run("/sbin/service waagent start", chk_err=False)
+
+ def register_agent_service(self):
+ return shellutil.run("/sbin/insserv waagent", chk_err=False)
+
+ def unregister_agent_service(self):
+ return shellutil.run("/sbin/insserv -r waagent", chk_err=False)
+
+class SUSEOSUtil(SUSE11OSUtil):
+ def __init__(self):
+ super(SUSEOSUtil, self).__init__()
+ self.dhclient_name = 'wickedd-dhcp4'
+
+ def register_agent_service(self):
+ return shellutil.run("systemctl enable waagent", chk_err=False)
+
+ def unregister_agent_service(self):
+ return shellutil.run("systemctl disable waagent", chk_err=False)
+
+
diff --git a/azurelinuxagent/distro/ubuntu/__init__.py b/azurelinuxagent/distro/ubuntu/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/distro/ubuntu/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/distro/ubuntu/deprovision.py b/azurelinuxagent/distro/ubuntu/deprovision.py
new file mode 100644
index 0000000..10fa123
--- /dev/null
+++ b/azurelinuxagent/distro/ubuntu/deprovision.py
@@ -0,0 +1,43 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import azurelinuxagent.logger as logger
+import azurelinuxagent.utils.fileutil as fileutil
+from azurelinuxagent.distro.default.deprovision import DeprovisionHandler, DeprovisionAction
+
+def del_resolv():
+ if os.path.realpath('/etc/resolv.conf') != '/run/resolvconf/resolv.conf':
+ logger.info("resolvconf is not configured. Removing /etc/resolv.conf")
+ fileutil.rm_files('/etc/resolv.conf')
+ else:
+ logger.info("resolvconf is enabled; leaving /etc/resolv.conf intact")
+ fileutil.rm_files('/etc/resolvconf/resolv.conf.d/tail',
+ '/etc/resolvconf/resolv.conf.d/originial')
+
+
+class UbuntuDeprovisionHandler(DeprovisionHandler):
+ def setup(self, deluser):
+ warnings, actions = super(UbuntuDeprovisionHandler, self).setup(deluser)
+ warnings.append("WARNING! Nameserver configuration in "
+ "/etc/resolvconf/resolv.conf.d/{tail,originial} "
+ "will be deleted.")
+ actions.append(DeprovisionAction(del_resolv))
+ return warnings, actions
+
diff --git a/azurelinuxagent/distro/ubuntu/handlerFactory.py b/azurelinuxagent/distro/ubuntu/handlerFactory.py
new file mode 100644
index 0000000..c8d0906
--- /dev/null
+++ b/azurelinuxagent/distro/ubuntu/handlerFactory.py
@@ -0,0 +1,29 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from azurelinuxagent.distro.ubuntu.provision import UbuntuProvisionHandler
+from azurelinuxagent.distro.ubuntu.deprovision import UbuntuDeprovisionHandler
+from azurelinuxagent.distro.default.handlerFactory import DefaultHandlerFactory
+
+class UbuntuHandlerFactory(DefaultHandlerFactory):
+ def __init__(self):
+ super(UbuntuHandlerFactory, self).__init__()
+ self.provision_handler = UbuntuProvisionHandler()
+ self.deprovision_handler = UbuntuDeprovisionHandler()
+
diff --git a/azurelinuxagent/distro/ubuntu/loader.py b/azurelinuxagent/distro/ubuntu/loader.py
new file mode 100644
index 0000000..26db4fa
--- /dev/null
+++ b/azurelinuxagent/distro/ubuntu/loader.py
@@ -0,0 +1,36 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from azurelinuxagent.metadata import DISTRO_NAME, DISTRO_VERSION
+
+def get_osutil():
+ from azurelinuxagent.distro.ubuntu.osutil import Ubuntu1204OSUtil, \
+ UbuntuOSUtil, \
+ Ubuntu14xOSUtil
+ if DISTRO_VERSION == "12.04":
+ return Ubuntu1204OSUtil()
+ elif DISTRO_VERSION == "14.04" or DISTRO_VERSION == "14.10":
+ return Ubuntu14xOSUtil()
+ else:
+ return UbuntuOSUtil()
+
+def get_handlers():
+ from azurelinuxagent.distro.ubuntu.handlerFactory import UbuntuHandlerFactory
+ return UbuntuHandlerFactory()
+
diff --git a/azurelinuxagent/distro/ubuntu/osutil.py b/azurelinuxagent/distro/ubuntu/osutil.py
new file mode 100644
index 0000000..1e51c2a
--- /dev/null
+++ b/azurelinuxagent/distro/ubuntu/osutil.py
@@ -0,0 +1,65 @@
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import pwd
+import shutil
+import socket
+import array
+import struct
+import fcntl
+import time
+import azurelinuxagent.logger as logger
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.textutil as textutil
+from azurelinuxagent.distro.default.osutil import DefaultOSUtil
+
+class Ubuntu14xOSUtil(DefaultOSUtil):
+ def __init__(self):
+ super(Ubuntu14xOSUtil, self).__init__()
+
+ def start_network(self):
+ return shellutil.run("service networking start", chk_err=False)
+
+ def stop_agent_service(self):
+ return shellutil.run("service walinuxagent stop", chk_err=False)
+
+ def start_agent_service(self):
+ return shellutil.run("service walinuxagent start", chk_err=False)
+
+class Ubuntu1204OSUtil(Ubuntu14xOSUtil):
+ def __init__(self):
+ super(Ubuntu1204OSUtil, self).__init__()
+
+ #Override
+ def get_dhcp_pid(self):
+ ret= shellutil.run_get_output("pidof dhclient3")
+ return ret[1] if ret[0] == 0 else None
+
+class UbuntuOSUtil(Ubuntu14xOSUtil):
+ def __init__(self):
+ super(UbuntuOSUtil, self).__init__()
+
+ def register_agent_service(self):
+ return shellutil.run("systemctl unmask walinuxagent", chk_err=False)
+
+ def unregister_agent_service(self):
+ return shellutil.run("systemctl mask walinuxagent", chk_err=False)
+
diff --git a/azurelinuxagent/distro/ubuntu/provision.py b/azurelinuxagent/distro/ubuntu/provision.py
new file mode 100644
index 0000000..7551074
--- /dev/null
+++ b/azurelinuxagent/distro/ubuntu/provision.py
@@ -0,0 +1,72 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import time
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.conf as conf
+import azurelinuxagent.protocol as prot
+from azurelinuxagent.exception import *
+from azurelinuxagent.utils.osutil import OSUTIL
+import azurelinuxagent.utils.shellutil as shellutil
+import azurelinuxagent.utils.fileutil as fileutil
+from azurelinuxagent.distro.default.provision import ProvisionHandler
+
+"""
+On ubuntu image, provision could be disabled.
+"""
+class UbuntuProvisionHandler(ProvisionHandler):
+ def process(self):
+ #If provision is enabled, run default provision handler
+ if conf.get_switch("Provisioning.Enabled", False):
+ super(UbuntuProvisionHandler, self).process()
+ return
+
+ logger.info("run Ubuntu provision handler")
+ provisioned = os.path.join(OSUTIL.get_lib_dir(), "provisioned")
+ if os.path.isfile(provisioned):
+ return
+
+ logger.info("Waiting cloud-init to finish provisioning.")
+ protocol = prot.FACTORY.get_default_protocol()
+ try:
+ logger.info("Wait for ssh host key to be generated.")
+ thumbprint = self.wait_for_ssh_host_key()
+ fileutil.write_file(provisioned, "")
+
+ logger.info("Finished provisioning")
+ status = prot.ProvisionStatus(status="Ready")
+ status.properties.certificateThumbprint = thumbprint
+ protocol.report_provision_status(status)
+
+ except ProvisionError as e:
+ logger.error("Provision failed: {0}", e)
+ protocol.report_provision_status(status="NotReady", subStatus=text(e))
+
+ def wait_for_ssh_host_key(self, max_retry=60):
+ kepair_type = conf.get("Provisioning.SshHostKeyPairType", "rsa")
+ path = '/etc/ssh/ssh_host_{0}_key'.format(kepair_type)
+ for retry in range(0, max_retry):
+ if os.path.isfile(path):
+ return self.get_ssh_host_key_thumbprint(kepair_type)
+ if retry < max_retry - 1:
+ logger.info("Wait for ssh host key be generated: {0}", path)
+ time.sleep(5)
+ raise ProvisionError("Ssh hsot key is not generated.")
diff --git a/azurelinuxagent/event.py b/azurelinuxagent/event.py
new file mode 100644
index 0000000..f866a22
--- /dev/null
+++ b/azurelinuxagent/event.py
@@ -0,0 +1,188 @@
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import sys
+import traceback
+import atexit
+import json
+import time
+import datetime
+import threading
+import platform
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.protocol as prot
+from azurelinuxagent.metadata import DISTRO_NAME, DISTRO_VERSION, \
+ DISTRO_CODE_NAME, AGENT_VERSION
+from azurelinuxagent.utils.osutil import OSUTIL
+
+class EventError(Exception):
+ pass
+
+class WALAEventOperation:
+ HeartBeat="HeartBeat"
+ Provision = "Provision"
+ Install = "Install"
+ UnInstall = "UnInstall"
+ Disable = "Disable"
+ Enable = "Enable"
+ Download = "Download"
+ Upgrade = "Upgrade"
+ Update = "Update"
+ ActivateResourceDisk="ActivateResourceDisk"
+ UnhandledError="UnhandledError"
+
+class EventMonitor(object):
+ def __init__(self):
+ self.sysinfo = []
+ self.event_dir = os.path.join(OSUTIL.get_lib_dir(), "events")
+
+ def init_sysinfo(self):
+ osversion = "{0}:{1}-{2}-{3}:{4}".format(platform.system(),
+ DISTRO_NAME,
+ DISTRO_VERSION,
+ DISTRO_CODE_NAME,
+ platform.release())
+
+ self.sysinfo.append(prot.TelemetryEventParam("OSVersion", osversion))
+ self.sysinfo.append(prot.TelemetryEventParam("GAVersion",
+ AGENT_VERSION))
+ self.sysinfo.append(prot.TelemetryEventParam("RAM",
+ OSUTIL.get_total_mem()))
+ self.sysinfo.append(prot.TelemetryEventParam("Processors",
+ OSUTIL.get_processor_cores()))
+ try:
+ protocol = prot.FACTORY.get_default_protocol()
+ vminfo = protocol.get_vminfo()
+ self.sysinfo.append(prot.TelemetryEventParam("VMName",
+ vminfo.vmName))
+ #TODO add other system info like, subscription id, etc.
+ except prot.ProtocolError as e:
+ logger.warn("Failed to get vm info: {0}", e)
+
+ def start(self):
+ event_thread = threading.Thread(target = self.run)
+ event_thread.setDaemon(True)
+ event_thread.start()
+
+ def collect_event(self, evt_file_name):
+ try:
+ with open(evt_file_name, "rb") as evt_file:
+ #if fail to open or delete the file, throw exception
+ json_str = evt_file.read().decode("utf-8",'ignore')
+ os.remove(evt_file_name)
+ return json_str
+ except IOError as e:
+ msg = "Failed to process {0}, {1}".format(evt_file_name, e)
+ raise EventError(msg)
+
+ def collect_and_send_events(self):
+ event_list = prot.TelemetryEventList()
+ event_files = os.listdir(self.event_dir)
+ for event_file in event_files:
+ if not event_file.endswith(".tld"):
+ continue
+ event_file_path = os.path.join(self.event_dir, event_file)
+ try:
+ data_str = self.collect_event(event_file_path)
+ except EventError as e:
+ logger.error("{0}", e)
+ continue
+ try:
+ data = json.loads(data_str)
+ except ValueError as e:
+ logger.verb(data_str)
+ logger.error("Failed to decode json event file{0}", e)
+ continue
+
+ event = prot.TelemetryEvent()
+ prot.set_properties(event, data)
+ event.parameters.extend(self.sysinfo)
+ event_list.events.append(event)
+ if len(event_list.events) == 0:
+ return
+
+ try:
+ protocol = prot.FACTORY.get_default_protocol()
+ protocol.report_event(event_list)
+ except prot.ProtocolError as e:
+ logger.error("{0}", e)
+
+ def run(self):
+ self.init_sysinfo()
+ last_heartbeat = datetime.datetime.min
+ period = datetime.timedelta(hours = 12)
+ while(True):
+ if (datetime.datetime.now()-last_heartbeat) > period:
+ last_heartbeat = datetime.datetime.now()
+ add_event(op=WALAEventOperation.HeartBeat,
+ name="WALA",is_success=True)
+ self.collect_and_send_events()
+ time.sleep(60)
+
+def save_event(data):
+ event_dir = os.path.join(OSUTIL.get_lib_dir(), 'events')
+ if not os.path.exists(event_dir):
+ os.mkdir(event_dir)
+ os.chmod(event_dir,0o700)
+ if len(os.listdir(event_dir)) > 1000:
+ raise EventError("Too many files under: {0}", event_dir)
+
+ filename = os.path.join(event_dir, text(int(time.time()*1000000)))
+ try:
+ with open(filename+".tmp",'wb+') as hfile:
+ hfile.write(data.encode("utf-8"))
+ os.rename(filename+".tmp", filename+".tld")
+ except IOError as e:
+ raise EventError("Failed to write events to file:{0}", e)
+
+def add_event(name, op, is_success, duration=0, version="1.0",
+ message="", evt_type="", is_internal=False):
+ event = prot.TelemetryEvent(1, "69B669B9-4AF8-4C50-BDC4-6006FA76E975")
+ event.parameters.append(prot.TelemetryEventParam('Name', name))
+ event.parameters.append(prot.TelemetryEventParam('Version', version))
+ event.parameters.append(prot.TelemetryEventParam('IsInternal', is_internal))
+ event.parameters.append(prot.TelemetryEventParam('Operation', op))
+ event.parameters.append(prot.TelemetryEventParam('OperationSuccess',
+ is_success))
+ event.parameters.append(prot.TelemetryEventParam('Message', message))
+ event.parameters.append(prot.TelemetryEventParam('Duration', duration))
+ event.parameters.append(prot.TelemetryEventParam('ExtensionType', evt_type))
+
+ data = prot.get_properties(event)
+ try:
+ save_event(json.dumps(data))
+ except EventError as e:
+ logger.error("{0}", e)
+
+def dump_unhandled_err(name):
+ if hasattr(sys, 'last_type') and hasattr(sys, 'last_value') and \
+ hasattr(sys, 'last_traceback'):
+ last_type = getattr(sys, 'last_type')
+ last_value = getattr(sys, 'last_value')
+ last_traceback = getattr(sys, 'last_traceback')
+ error = traceback.format_exception(last_type, last_value,
+ last_traceback)
+ message= "".join(error)
+ logger.error(message)
+ add_event(name, is_success=False, message=message,
+ op=WALAEventOperation.UnhandledError)
+
+def enable_unhandled_err_dump(name):
+ atexit.register(dump_unhandled_err, name)
+
diff --git a/azurelinuxagent/exception.py b/azurelinuxagent/exception.py
new file mode 100644
index 0000000..7c31394
--- /dev/null
+++ b/azurelinuxagent/exception.py
@@ -0,0 +1,65 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+"""
+Defines all exceptions
+"""
+
+class AgentError(Exception):
+ """
+ Base class of agent error.
+ """
+ def __init__(self, errno, msg):
+ msg = "({0}){1}".format(errno, msg)
+ super(AgentError, self).__init__(msg)
+
+class AgentConfigError(AgentError):
+ """
+ When configure file is not found or malformed.
+ """
+ def __init__(self, msg):
+ super(AgentConfigError, self).__init__('000001', msg)
+
+class AgentNetworkError(AgentError):
+ """
+ When network is not avaiable.
+ """
+ def __init__(self, msg):
+ super(AgentNetworkError, self).__init__('000002', msg)
+
+class ExtensionError(AgentError):
+ """
+ When failed to execute an extension
+ """
+ def __init__(self, msg):
+ super(ExtensionError, self).__init__('000003', msg)
+
+class ProvisionError(AgentError):
+ """
+ When provision failed
+ """
+ def __init__(self, msg):
+ super(ProvisionError, self).__init__('000004', msg)
+
+class ResourceDiskError(AgentError):
+ """
+ Mount resource disk failed
+ """
+ def __init__(self, msg):
+ super(ResourceDiskError, self).__init__('000005', msg)
+
diff --git a/azurelinuxagent/future.py b/azurelinuxagent/future.py
new file mode 100644
index 0000000..8186fcf
--- /dev/null
+++ b/azurelinuxagent/future.py
@@ -0,0 +1,19 @@
+import sys
+
+"""
+Add alies for python2 and python3 libs and fucntions.
+"""
+
+if sys.version_info[0]== 3:
+ import http.client as httpclient
+ from urllib.parse import urlparse
+ text = str
+ bytebuffer = memoryview
+elif sys.version_info[0] == 2:
+ import httplib as httpclient
+ from urlparse import urlparse
+ text = unicode
+ bytebuffer = buffer
+else:
+ raise ImportError("Unknown python version:{0}".format(sys.version_info))
+
diff --git a/azurelinuxagent/handler.py b/azurelinuxagent/handler.py
new file mode 100644
index 0000000..c180112
--- /dev/null
+++ b/azurelinuxagent/handler.py
@@ -0,0 +1,28 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+"""
+Handler handles different tasks like, provisioning, deprovisioning etc.
+The handlers could be extended for different distros. The default
+implementation is under azurelinuxagent.distros.default
+"""
+import azurelinuxagent.distro.loader as loader
+
+HANDLERS = loader.get_handlers()
+
diff --git a/azurelinuxagent/logger.py b/azurelinuxagent/logger.py
new file mode 100644
index 0000000..126d6bc
--- /dev/null
+++ b/azurelinuxagent/logger.py
@@ -0,0 +1,158 @@
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and openssl_bin 1.0+
+#
+# Implements parts of RFC 2131, 1541, 1497 and
+# http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx
+# http://msdn.microsoft.com/en-us/library/cc227259%28PROT.13%29.aspx
+"""
+Log utils
+"""
+
+import sys
+from azurelinuxagent.future import text
+import azurelinuxagent.utils.textutil as textutil
+from datetime import datetime
+
+class Logger(object):
+ """
+ Logger class
+ """
+ def __init__(self, logger=None, prefix=None):
+ self.appenders = []
+ if logger is not None:
+ self.appenders.extend(logger.appenders)
+ self.prefix = prefix
+
+ def verbose(self, msg_format, *args):
+ self.log(LogLevel.VERBOSE, msg_format, *args)
+
+ def info(self, msg_format, *args):
+ self.log(LogLevel.INFO, msg_format, *args)
+
+ def warn(self, msg_format, *args):
+ self.log(LogLevel.WARNING, msg_format, *args)
+
+ def error(self, msg_format, *args):
+ self.log(LogLevel.ERROR, msg_format, *args)
+
+ def log(self, level, msg_format, *args):
+ if len(args) > 0:
+ msg = msg_format.format(*args)
+ else:
+ msg = msg_format
+ time = datetime.now().strftime(u'%Y/%m/%d %H:%M:%S.%f')
+ level_str = LogLevel.STRINGS[level]
+ if self.prefix is not None:
+ log_item = u"{0} {1} {2} {3}\n".format(time, level_str, self.prefix,
+ msg)
+ else:
+ log_item = u"{0} {1} {2}\n".format(time, level_str, msg)
+ log_item = text(log_item.encode("ascii", "backslashreplace"), encoding='ascii')
+ for appender in self.appenders:
+ appender.write(level, log_item)
+
+ def add_appender(self, appender_type, level, path):
+ appender = _create_logger_appender(appender_type, level, path)
+ self.appenders.append(appender)
+
+class ConsoleAppender(object):
+ def __init__(self, level, path):
+ self.level = LogLevel.INFO
+ if level >= LogLevel.INFO:
+ self.level = level
+ self.path = path
+
+ def write(self, level, msg):
+ if self.level <= level:
+ try:
+ with open(self.path, "w") as console:
+ console.write(msg)
+ except IOError:
+ pass
+
+class FileAppender(object):
+ def __init__(self, level, path):
+ self.level = level
+ self.path = path
+
+ def write(self, level, msg):
+ if self.level <= level:
+ try:
+ with open(self.path, "a+") as log_file:
+ log_file.write(msg)
+ except IOError:
+ pass
+
+class StdoutAppender(object):
+ def __init__(self, level):
+ self.level = level
+
+ def write(self, level, msg):
+ if self.level <= level:
+ try:
+ sys.stdout.write(msg)
+ except IOError:
+ pass
+
+
+#Initialize logger instance
+DEFAULT_LOGGER = Logger()
+
+class LogLevel(object):
+ VERBOSE = 0
+ INFO = 1
+ WARNING = 2
+ ERROR = 3
+ STRINGS = [
+ "VERBOSE",
+ "INFO",
+ "WARNING",
+ "ERROR"
+ ]
+
+class AppenderType(object):
+ FILE = 0
+ CONSOLE = 1
+ STDOUT = 2
+
+def add_logger_appender(appender_type, level=LogLevel.INFO, path=None):
+ DEFAULT_LOGGER.add_appender(appender_type, level, path)
+
+def verb(msg_format, *args):
+ DEFAULT_LOGGER.verbose(msg_format, *args)
+
+def info(msg_format, *args):
+ DEFAULT_LOGGER.info(msg_format, *args)
+
+def warn(msg_format, *args):
+ DEFAULT_LOGGER.warn(msg_format, *args)
+
+def error(msg_format, *args):
+ DEFAULT_LOGGER.error(msg_format, *args)
+
+def log(level, msg_format, *args):
+ DEFAULT_LOGGER.log(level, msg_format, args)
+
+def _create_logger_appender(appender_type, level=LogLevel.INFO, path=None):
+ if appender_type == AppenderType.CONSOLE:
+ return ConsoleAppender(level, path)
+ elif appender_type == AppenderType.FILE:
+ return FileAppender(level, path)
+ elif appender_type == AppenderType.STDOUT:
+ return StdoutAppender(level)
+ else:
+ raise ValueError("Unknown appender type")
+
diff --git a/azurelinuxagent/metadata.py b/azurelinuxagent/metadata.py
new file mode 100644
index 0000000..83d4676
--- /dev/null
+++ b/azurelinuxagent/metadata.py
@@ -0,0 +1,93 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import os
+import re
+import platform
+import sys
+from azurelinuxagent.future import text
+
+def get_distro():
+ if 'FreeBSD' in platform.system():
+ release = re.sub('\-.*\Z', '', text(platform.release()))
+ osinfo = ['freebsd', release, '', 'freebsd']
+ if 'linux_distribution' in dir(platform):
+ osinfo = list(platform.linux_distribution(full_distribution_name=0))
+ full_name = platform.linux_distribution()[0].strip()
+ osinfo.append(full_name)
+ else:
+ osinfo = platform.dist()
+
+ #The platform.py lib has issue with detecting oracle linux distribution.
+ #Merge the following patch provided by oracle as a temparory fix.
+ if os.path.exists("/etc/oracle-release"):
+ osinfo[2] = "oracle"
+ osinfo[3] = "Oracle Linux"
+
+ #Remove trailing whitespace and quote in distro name
+ osinfo[0] = osinfo[0].strip('"').strip(' ').lower()
+ return osinfo
+
+AGENT_NAME = "WALinuxAgent"
+AGENT_LONG_NAME = "Azure Linux Agent"
+AGENT_VERSION = '2.1.1'
+AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION)
+AGENT_DESCRIPTION = """\
+The Azure Linux Agent supports the provisioning and running of Linux
+VMs in the Azure cloud. This package should be installed on Linux disk
+images that are built to run in the Azure environment.
+"""
+
+__distro__ = get_distro()
+DISTRO_NAME = __distro__[0]
+DISTRO_VERSION = __distro__[1]
+DISTRO_CODE_NAME = __distro__[2]
+DISTRO_FULL_NAME = __distro__[3]
+
+PY_VERSION = sys.version_info
+PY_VERSION_MAJOR = sys.version_info[0]
+PY_VERSION_MINOR = sys.version_info[1]
+PY_VERSION_MICRO = sys.version_info[2]
+
+
+"""
+Add this walk arround for detecting Snappy Ubuntu Core temporarily, until ubuntu
+fixed this bug: https://bugs.launchpad.net/snappy/+bug/1481086
+"""
+def which(program):
+ # Return path of program for execution if found in path
+ def is_exe(fpath):
+ return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
+ _fpath, _ = os.path.split(program)
+ if _fpath:
+ if is_exe(program):
+ return program
+ else:
+ for path in os.environ.get("PATH", "").split(os.pathsep):
+ path = path.strip('"')
+ exe_file = os.path.join(path, program)
+ if is_exe(exe_file):
+ return exe_file
+ return None
+
+def is_snappy():
+ return which("snappy")
+
+if is_snappy():
+ DISTRO_FULL_NAME = "Snappy Ubuntu Core"
diff --git a/azurelinuxagent/protocol/__init__.py b/azurelinuxagent/protocol/__init__.py
new file mode 100644
index 0000000..65d8a5d
--- /dev/null
+++ b/azurelinuxagent/protocol/__init__.py
@@ -0,0 +1,23 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+from azurelinuxagent.protocol.common import *
+from azurelinuxagent.protocol.protocolFactory import FACTORY, \
+ detect_default_protocol
+
diff --git a/azurelinuxagent/protocol/common.py b/azurelinuxagent/protocol/common.py
new file mode 100644
index 0000000..77247ab
--- /dev/null
+++ b/azurelinuxagent/protocol/common.py
@@ -0,0 +1,245 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+import os
+import copy
+import re
+import json
+import xml.dom.minidom
+import azurelinuxagent.logger as logger
+import azurelinuxagent.utils.fileutil as fileutil
+
+class ProtocolError(Exception):
+ pass
+
+class ProtocolNotFound(Exception):
+ pass
+
+def validata_param(name, val, expected_type):
+ if val is None:
+ raise ProtocolError("Param {0} is None".format(name))
+ if not isinstance(val, expected_type):
+ raise ProtocolError("Param {0} type should be {1}".format(name,
+ expected_type))
+
+def set_properties(obj, data):
+ validata_param("obj", obj, DataContract)
+ validata_param("data", data, dict)
+
+ props = vars(obj)
+ for name, val in list(props.items()):
+ try:
+ new_val = data[name]
+ except KeyError:
+ continue
+
+ if isinstance(new_val, dict):
+ set_properties(val, new_val)
+ elif isinstance(new_val, list):
+ validata_param("list", val, DataContractList)
+ for data_item in new_val:
+ item = val.item_cls()
+ set_properties(item, data_item)
+ val.append(item)
+ else:
+ setattr(obj, name, new_val)
+
+def get_properties(obj):
+ validata_param("obj", obj, DataContract)
+
+ data = {}
+ props = vars(obj)
+ for name, val in list(props.items()):
+ if isinstance(val, DataContract):
+ data[name] = get_properties(val)
+ elif isinstance(val, DataContractList):
+ if len(val) == 0:
+ continue
+ data[name] = []
+ for item in val:
+ date_item = get_properties(item)
+ data[name].append(date_item)
+ elif val is not None:
+ data[name] = val
+ return data
+
+class DataContract(object):
+ pass
+
+class DataContractList(list):
+ def __init__(self, item_cls):
+ self.item_cls = item_cls
+
+class VMInfo(DataContract):
+ def __init__(self, subscriptionId=None, vmName=None):
+ self.subscriptionId = subscriptionId
+ self.vmName = vmName
+
+class Cert(DataContract):
+ def __init__(self, name=None, thumbprint=None, certificateDataUri=None):
+ self.name = name
+ self.thumbprint = thumbprint
+ self.certificateDataUri = certificateDataUri
+
+class CertList(DataContract):
+ def __init__(self):
+ self.certificates = DataContractList(Cert)
+
+class ExtensionSettings(DataContract):
+ def __init__(self, name=None, sequenceNumber=None, publicSettings=None,
+ privateSettings=None, certificateThumbprint=None):
+ self.name = name
+ self.sequenceNumber = sequenceNumber
+ self.publicSettings = publicSettings
+ self.privateSettings = privateSettings
+ self.certificateThumbprint = certificateThumbprint
+
+class ExtensionProperties(DataContract):
+ def __init__(self):
+ self.version = None
+ self.upgradePolicy = None
+ self.state = None
+ self.extensions = DataContractList(ExtensionSettings)
+
+class ExtensionVersionUri(DataContract):
+ def __init__(self):
+ self.uri = None
+
+class Extension(DataContract):
+ def __init__(self, name=None):
+ self.name = name
+ self.properties = ExtensionProperties()
+ self.version_uris = DataContractList(ExtensionVersionUri)
+
+class ExtensionList(DataContract):
+ def __init__(self):
+ self.extensions = DataContractList(Extension)
+
+class ExtensionPackageUri(DataContract):
+ def __init__(self, uri=None):
+ self.uri = uri
+
+class ExtensionPackage(DataContract):
+ def __init__(self, version = None):
+ self.version = version
+ self.uris = DataContractList(ExtensionPackageUri)
+
+class ExtensionPackageList(DataContract):
+ def __init__(self):
+ self.versions = DataContractList(ExtensionPackage)
+
+class InstanceMetadata(DataContract):
+ def __init__(self, deploymentName=None, roleName=None, roleInstanceId=None,
+ containerId=None):
+ self.deploymentName = deploymentName
+ self.roleName = roleName
+ self.roleInstanceId = roleInstanceId
+ self.containerId = containerId
+
+class VMProperties(DataContract):
+ def __init__(self, certificateThumbprint=None):
+ #TODO need to confirm the property name
+ self.certificateThumbprint = certificateThumbprint
+
+class ProvisionStatus(DataContract):
+ def __init__(self, status=None, subStatus=None, description=None):
+ self.status = status
+ self.subStatus = subStatus
+ self.description = description
+ self.properties = VMProperties()
+
+class VMAgentStatus(DataContract):
+ def __init__(self, agentVersion=None, status=None, message=None):
+ self.agentVersion = agentVersion
+ self.status = status
+ self.message = message
+
+class ExtensionSubStatus(DataContract):
+ def __init__(self, name=None, status=None, code=None, message=None):
+ self.name = name
+ self.status = status
+ self.code = code
+ self.message = message
+
+class ExtensionStatus(DataContract):
+ def __init__(self, name=None, configurationAppliedTime=None, operation=None,
+ status=None, code=None, message=None, seq_no=None):
+ self.name = name
+ self.configurationAppliedTime = configurationAppliedTime
+ self.operation = operation
+ self.status = status
+ self.code = code
+ self.message = message
+ self.sequenceNumber = seq_no
+ self.substatusList = DataContractList(ExtensionSubStatus)
+
+class ExtensionHandlerStatus(DataContract):
+ def __init__(self, handlerName=None, handlerVersion=None, status=None,
+ message=None):
+ self.handlerName = handlerName
+ self.handlerVersion = handlerVersion
+ self.status = status
+ self.message = message
+ self.extensionStatusList = DataContractList(ExtensionStatus)
+
+class VMStatus(DataContract):
+ def __init__(self):
+ self.vmAgent = VMAgentStatus()
+ self.extensionHandlers = DataContractList(ExtensionHandlerStatus)
+
+class TelemetryEventParam(DataContract):
+ def __init__(self, name=None, value=None):
+ self.name = name
+ self.value = value
+
+class TelemetryEvent(DataContract):
+ def __init__(self, eventId=None, providerId=None):
+ self.eventId = eventId
+ self.providerId = providerId
+ self.parameters = DataContractList(TelemetryEventParam)
+
+class TelemetryEventList(DataContract):
+ def __init__(self):
+ self.events = DataContractList(TelemetryEvent)
+
+class Protocol(DataContract):
+
+ def initialize(self):
+ raise NotImplementedError()
+
+ def get_vminfo(self):
+ raise NotImplementedError()
+
+ def get_certs(self):
+ raise NotImplementedError()
+
+ def get_extensions(self):
+ raise NotImplementedError()
+
+ def get_extension_pkgs(self, extension):
+ raise NotImplementedError()
+
+ def report_provision_status(self, status):
+ raise NotImplementedError()
+
+ def report_status(self, status):
+ raise NotImplementedError()
+
+ def report_event(self, event):
+ raise NotImplementedError()
+
diff --git a/azurelinuxagent/protocol/ovfenv.py b/azurelinuxagent/protocol/ovfenv.py
new file mode 100644
index 0000000..2e0411d
--- /dev/null
+++ b/azurelinuxagent/protocol/ovfenv.py
@@ -0,0 +1,146 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+"""
+Copy and parse ovf-env.xml from provisiong ISO and local cache
+"""
+import os
+import re
+import xml.dom.minidom as minidom
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.utils.fileutil as fileutil
+from azurelinuxagent.utils.textutil import parse_doc, findall, find, findtext
+from azurelinuxagent.utils.osutil import OSUTIL, OSUtilError
+from azurelinuxagent.protocol import ProtocolError
+
+OVF_FILE_NAME = "ovf-env.xml"
+OVF_VERSION = "1.0"
+OVF_NAME_SPACE = "http://schemas.dmtf.org/ovf/environment/1"
+WA_NAME_SPACE = "http://schemas.microsoft.com/windowsazure"
+
+def get_ovf_env():
+ """
+ Load saved ovf-env.xml
+ """
+ ovf_file_path = os.path.join(OSUTIL.get_lib_dir(), OVF_FILE_NAME)
+ if os.path.isfile(ovf_file_path):
+ xml_text = fileutil.read_file(ovf_file_path)
+ return OvfEnv(xml_text)
+ else:
+ raise ProtocolError("ovf-env.xml is missing.")
+
+def copy_ovf_env():
+ """
+ Copy ovf env file from dvd to hard disk.
+ Remove password before save it to the disk
+ """
+ try:
+ OSUTIL.mount_dvd()
+ ovf_file_path_on_dvd = OSUTIL.get_ovf_env_file_path_on_dvd()
+ ovfxml = fileutil.read_file(ovf_file_path_on_dvd, remove_bom=True)
+ ovfenv = OvfEnv(ovfxml)
+ ovfxml = re.sub("<UserPassword>.*?<", "<UserPassword>*<", ovfxml)
+ ovf_file_path = os.path.join(OSUTIL.get_lib_dir(), OVF_FILE_NAME)
+ fileutil.write_file(ovf_file_path, ovfxml)
+ OSUTIL.umount_dvd()
+ OSUTIL.eject_dvd()
+ except IOError as e:
+ raise ProtocolError(text(e))
+ except OSUtilError as e:
+ raise ProtocolError(text(e))
+ return ovfenv
+
+def _validate_ovf(val, msg):
+ if val is None:
+ raise ProtocolError("Failed to parse OVF XML: {0}".format(msg))
+
+class OvfEnv(object):
+ """
+ Read, and process provisioning info from provisioning file OvfEnv.xml
+ """
+ def __init__(self, xml_text):
+ if xml_text is None:
+ raise ValueError("ovf-env is None")
+ logger.verb("Load ovf-env.xml")
+ self.hostname = None
+ self.username = None
+ self.user_password = None
+ self.customdata = None
+ self.disable_ssh_password_auth = True
+ self.ssh_pubkeys = []
+ self.ssh_keypairs = []
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ """
+ Parse xml tree, retreiving user and ssh key information.
+ Return self.
+ """
+ wans = WA_NAME_SPACE
+ ovfns = OVF_NAME_SPACE
+
+ xml_doc = parse_doc(xml_text)
+
+ environment = find(xml_doc, "Environment", namespace=ovfns)
+ _validate_ovf(environment, "Environment not found")
+
+ section = find(environment, "ProvisioningSection", namespace=wans)
+ _validate_ovf(section, "ProvisioningSection not found")
+
+ version = findtext(environment, "Version", namespace=wans)
+ _validate_ovf(version, "Version not found")
+
+ if version > OVF_VERSION:
+ logger.warn("Newer provisioning configuration detected. "
+ "Please consider updating waagent")
+
+ conf_set = find(section, "LinuxProvisioningConfigurationSet",
+ namespace=wans)
+ _validate_ovf(conf_set, "LinuxProvisioningConfigurationSet not found")
+
+ self.hostname = findtext(conf_set, "HostName", namespace=wans)
+ _validate_ovf(self.hostname, "HostName not found")
+
+ self.username = findtext(conf_set, "UserName", namespace=wans)
+ _validate_ovf(self.username, "UserName not found")
+
+ self.user_password = findtext(conf_set, "UserPassword", namespace=wans)
+
+ self.customdata = findtext(conf_set, "CustomData", namespace=wans)
+
+ auth_option = findtext(conf_set, "DisableSshPasswordAuthentication",
+ namespace=wans)
+ if auth_option is not None and auth_option.lower() == "true":
+ self.disable_ssh_password_auth = True
+ else:
+ self.disable_ssh_password_auth = False
+
+ public_keys = findall(conf_set, "PublicKey", namespace=wans)
+ for public_key in public_keys:
+ path = findtext(public_key, "Path", namespace=wans)
+ fingerprint = findtext(public_key, "Fingerprint", namespace=wans)
+ value = findtext(public_key, "Value", namespace=wans)
+ self.ssh_pubkeys.append((path, fingerprint, value))
+
+ keypairs = findall(conf_set, "KeyPair", namespace=wans)
+ for keypair in keypairs:
+ path = findtext(keypair, "Path", namespace=wans)
+ fingerprint = findtext(keypair, "Fingerprint", namespace=wans)
+ self.ssh_keypairs.append((path, fingerprint))
+
diff --git a/azurelinuxagent/protocol/protocolFactory.py b/azurelinuxagent/protocol/protocolFactory.py
new file mode 100644
index 0000000..d2ca201
--- /dev/null
+++ b/azurelinuxagent/protocol/protocolFactory.py
@@ -0,0 +1,114 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+import os
+import traceback
+import threading
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.utils.fileutil as fileutil
+from azurelinuxagent.utils.osutil import OSUTIL
+from azurelinuxagent.protocol.common import *
+from azurelinuxagent.protocol.v1 import WireProtocol
+from azurelinuxagent.protocol.v2 import MetadataProtocol
+
+WIRE_SERVER_ADDR_FILE_NAME = "WireServer"
+
+def get_wire_protocol_endpoint():
+ path = os.path.join(OSUTIL.get_lib_dir(), WIRE_SERVER_ADDR_FILE_NAME)
+ try:
+ endpoint = fileutil.read_file(path)
+ except IOError as e:
+ raise ProtocolNotFound("Wire server endpoint not found: {0}".format(e))
+
+ if endpoint is None:
+ raise ProtocolNotFound("Wire server endpoint is None")
+
+ return endpoint
+
+def detect_wire_protocol():
+ endpoint = get_wire_protocol_endpoint()
+
+ OSUTIL.gen_transport_cert()
+ protocol = WireProtocol(endpoint)
+ protocol.initialize()
+ logger.info("Protocol V1 found.")
+ return protocol
+
+def detect_metadata_protocol():
+ protocol = MetadataProtocol()
+ protocol.initialize()
+
+ logger.info("Protocol V2 found.")
+ return protocol
+
+def detect_available_protocols(prob_funcs=[detect_wire_protocol,
+ detect_metadata_protocol]):
+ available_protocols = []
+ for probe_func in prob_funcs:
+ try:
+ protocol = probe_func()
+ available_protocols.append(protocol)
+ except ProtocolNotFound as e:
+ logger.info(text(e))
+ return available_protocols
+
+def detect_default_protocol():
+ logger.info("Detect default protocol.")
+ available_protocols = detect_available_protocols()
+ return choose_default_protocol(available_protocols)
+
+def choose_default_protocol(protocols):
+ if len(protocols) > 0:
+ return protocols[0]
+ else:
+ raise ProtocolNotFound("No available protocol detected.")
+
+def get_wire_protocol():
+ endpoint = get_wire_protocol_endpoint()
+ return WireProtocol(endpoint)
+
+def get_metadata_protocol():
+ return MetadataProtocol()
+
+def get_available_protocols(getters=[get_wire_protocol, get_metadata_protocol]):
+ available_protocols = []
+ for getter in getters:
+ try:
+ protocol = getter()
+ available_protocols.append(protocol)
+ except ProtocolNotFound as e:
+ logger.info(text(e))
+ return available_protocols
+
+class ProtocolFactory(object):
+ def __init__(self):
+ self._protocol = None
+ self._lock = threading.Lock()
+
+ def get_default_protocol(self):
+ if self._protocol is None:
+ self._lock.acquire()
+ if self._protocol is None:
+ available_protocols = get_available_protocols()
+ self._protocol = choose_default_protocol(available_protocols)
+ self._lock.release()
+
+ return self._protocol
+
+FACTORY = ProtocolFactory()
diff --git a/azurelinuxagent/protocol/v1.py b/azurelinuxagent/protocol/v1.py
new file mode 100644
index 0000000..54a80b6
--- /dev/null
+++ b/azurelinuxagent/protocol/v1.py
@@ -0,0 +1,964 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+
+import os
+import json
+import re
+import time
+import traceback
+import xml.sax.saxutils as saxutils
+import xml.etree.ElementTree as ET
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text, httpclient
+import azurelinuxagent.utils.restutil as restutil
+from azurelinuxagent.utils.textutil import parse_doc, findall, find, findtext, \
+ getattrib, gettext, remove_bom
+from azurelinuxagent.utils.osutil import OSUTIL
+import azurelinuxagent.utils.fileutil as fileutil
+import azurelinuxagent.utils.shellutil as shellutil
+from azurelinuxagent.protocol.common import *
+
+VERSION_INFO_URI = "http://{0}/?comp=versions"
+GOAL_STATE_URI = "http://{0}/machine/?comp=goalstate"
+HEALTH_REPORT_URI = "http://{0}/machine?comp=health"
+ROLE_PROP_URI = "http://{0}/machine?comp=roleProperties"
+TELEMETRY_URI = "http://{0}/machine?comp=telemetrydata"
+
+WIRE_SERVER_ADDR_FILE_NAME = "WireServer"
+INCARNATION_FILE_NAME = "Incarnation"
+GOAL_STATE_FILE_NAME = "GoalState.{0}.xml"
+HOSTING_ENV_FILE_NAME = "HostingEnvironmentConfig.xml"
+SHARED_CONF_FILE_NAME = "SharedConfig.xml"
+CERTS_FILE_NAME = "Certificates.xml"
+P7M_FILE_NAME = "Certificates.p7m"
+PEM_FILE_NAME = "Certificates.pem"
+EXT_CONF_FILE_NAME = "ExtensionsConfig.{0}.xml"
+MANIFEST_FILE_NAME = "{0}.{1}.manifest.xml"
+TRANSPORT_CERT_FILE_NAME = "TransportCert.pem"
+TRANSPORT_PRV_FILE_NAME = "TransportPrivate.pem"
+
+PROTOCOL_VERSION = "2012-11-30"
+
+class WireProtocolResourceGone(ProtocolError):
+ pass
+
+class WireProtocol(Protocol):
+
+ def __init__(self, endpoint):
+ self.client = WireClient(endpoint)
+
+ def initialize(self):
+ self.client.check_wire_protocol_version()
+ self.client.update_goal_state(forced=True)
+
+ def get_vminfo(self):
+ hosting_env = self.client.get_hosting_env()
+ vminfo = VMInfo()
+ vminfo.subscriptionId = None
+ vminfo.vmName = hosting_env.vm_name
+ return vminfo
+
+ def get_certs(self):
+ certificates = self.client.get_certs()
+ return certificates.cert_list
+
+ def get_extensions(self):
+ #Update goal state to get latest extensions config
+ self.client.update_goal_state()
+ ext_conf = self.client.get_ext_conf()
+ return ext_conf.ext_list
+
+ def get_extension_pkgs(self, extension):
+ goal_state = self.client.get_goal_state()
+ man = self.client.get_ext_manifest(extension, goal_state)
+ return man.pkg_list
+
+ def report_provision_status(self, provisionStatus):
+ validata_param("provisionStatus", provisionStatus, ProvisionStatus)
+
+ if provisionStatus.status is not None:
+ self.client.report_health(provisionStatus.status,
+ provisionStatus.subStatus,
+ provisionStatus.description)
+ if provisionStatus.properties.certificateThumbprint is not None:
+ thumbprint = provisionStatus.properties.certificateThumbprint
+ self.client.report_role_prop(thumbprint)
+
+ def report_status(self, vmStatus):
+ validata_param("vmStatus", vmStatus, VMStatus)
+ self.client.upload_status_blob(vmStatus)
+
+ def report_event(self, events):
+ validata_param("events", events, TelemetryEventList)
+ self.client.report_event(events)
+
+def _fetch_cache(local_file):
+ if not os.path.isfile(local_file):
+ raise ProtocolError("{0} is missing.".format(local_file))
+ return fileutil.read_file(local_file)
+
+def _fetch_uri(uri, headers, chk_proxy=False):
+ try:
+ resp = restutil.http_get(uri, headers, chk_proxy=chk_proxy)
+ except restutil.HttpError as e:
+ raise ProtocolError(text(e))
+
+ if(resp.status == httpclient.GONE):
+ raise WireProtocolResourceGone(uri)
+ if(resp.status != httpclient.OK):
+ raise ProtocolError("{0} - {1}".format(resp.status, uri))
+ data = resp.read()
+ if data is None:
+ return None
+ data = remove_bom(data)
+ xml_text = text(data, encoding='utf-8')
+ return xml_text
+
+def _fetch_manifest(version_uris):
+ for version_uri in version_uris:
+ try:
+ xml_text = _fetch_uri(version_uri.uri, None, chk_proxy=True)
+ return xml_text
+ except IOError as e:
+ logger.warn("Failed to fetch ExtensionManifest: {0}, {1}", e,
+ version_uri.uri)
+ raise ProtocolError("Failed to fetch ExtensionManifest from all sources")
+
+def _build_role_properties(container_id, role_instance_id, thumbprint):
+ xml = ("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<RoleProperties>"
+ "<Container>"
+ "<ContainerId>{0}</ContainerId>"
+ "<RoleInstances>"
+ "<RoleInstance>"
+ "<Id>{1}</Id>"
+ "<Properties>"
+ "<Property name=\"CertificateThumbprint\" value=\"{2}\" />"
+ "</Properties>"
+ "</RoleInstance>"
+ "</RoleInstances>"
+ "</Container>"
+ "</RoleProperties>"
+ "").format(container_id, role_instance_id, thumbprint)
+ return xml
+
+def _build_health_report(incarnation, container_id, role_instance_id,
+ status, substatus, description):
+ detail = ''
+ if substatus is not None:
+ detail = ("<Details>"
+ "<SubStatus>{0}</SubStatus>"
+ "<Description>{1}</Description>"
+ "</Details>").format(substatus, description)
+ xml = ("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<Health "
+ "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+ " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
+ "<GoalStateIncarnation>{0}</GoalStateIncarnation>"
+ "<Container>"
+ "<ContainerId>{1}</ContainerId>"
+ "<RoleInstanceList>"
+ "<Role>"
+ "<InstanceId>{2}</InstanceId>"
+ "<Health>"
+ "<State>{3}</State>"
+ "{4}"
+ "</Health>"
+ "</Role>"
+ "</RoleInstanceList>"
+ "</Container>"
+ "</Health>"
+ "").format(incarnation,
+ container_id,
+ role_instance_id,
+ status,
+ detail)
+ return xml
+
+"""
+Convert VMStatus object to status blob format
+"""
+def guest_agent_status_to_v1(ga_status):
+ formatted_msg = {
+ 'lang' : 'en-US',
+ 'message' : ga_status.message
+ }
+ v1_ga_status = {
+ 'version' : ga_status.agentVersion,
+ 'status' : ga_status.status,
+ 'formattedMessage' : formatted_msg
+ }
+ return v1_ga_status
+
+def extension_substatus_to_v1(sub_status_list):
+ status_list = []
+ for substatus in sub_status_list:
+ status = {
+ "name": substatus.name,
+ "status": substatus.status,
+ "code": substatus.code,
+ "formattedMessage":{
+ "lang": "en-US",
+ "message": substatus.message
+ }
+ }
+ status_list.append(status)
+ return status_list
+
+def extension_handler_status_to_v1(handler_status, timestamp):
+ if handler_status is None or len(handler_status.extensionStatusList) == 0:
+ return
+ ext_status = handler_status.extensionStatusList[0]
+ sub_status = extension_substatus_to_v1(ext_status.substatusList)
+ ext_in_status = {
+ "status":{
+ "name": ext_status.name,
+ "configurationAppliedTime": ext_status.configurationAppliedTime,
+ "operation": ext_status.operation,
+ "status": ext_status.status,
+ "code": ext_status.code,
+ "formattedMessage": {
+ "lang":"en-US",
+ "message": ext_status.message
+ }
+ },
+ "timestampUTC": timestamp
+ }
+
+ if len(sub_status) != 0:
+ ext_in_status['substatus'] = sub_status
+
+ v1_handler_status = {
+ 'handlerVersion' : handler_status.handlerVersion,
+ 'handlerName' : handler_status.handlerName,
+ 'status' : handler_status.status,
+ 'runtimeSettingsStatus' : {
+ 'settingsStatus' : ext_in_status,
+ 'sequenceNumber' : ext_status.sequenceNumber
+ }
+ }
+ return v1_handler_status
+
+
+def vm_status_to_v1(vm_status):
+ timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+
+ v1_ga_status = guest_agent_status_to_v1(vm_status.vmAgent)
+ v1_handler_status_list = []
+ for handler_status in vm_status.extensionHandlers:
+ v1_handler_status = extension_handler_status_to_v1(handler_status,
+ timestamp)
+ v1_handler_status_list.append(v1_handler_status)
+
+ v1_agg_status = {
+ 'guestAgentStatus': v1_ga_status,
+ 'handlerAggregateStatus' : v1_handler_status_list
+ }
+ v1_vm_status = {
+ 'version' : '1.0',
+ 'timestampUTC' : timestamp,
+ 'aggregateStatus' : v1_agg_status
+ }
+ return v1_vm_status
+
+
+class StatusBlob(object):
+ def __init__(self, vm_status):
+ self.vm_status = vm_status
+
+ def to_json(self):
+ report = vm_status_to_v1(self.vm_status)
+ return json.dumps(report)
+
+ __storage_version__ = "2014-02-14"
+
+ def upload(self, url):
+ logger.info("Upload status blob")
+ blob_type = self.get_blob_type(url)
+
+ data = self.to_json()
+ if blob_type == "BlockBlob":
+ self.put_block_blob(url, data)
+ elif blob_type == "PageBlob":
+ self.put_page_blob(url, data)
+ else:
+ raise ProtocolError("Unknown blob type: {0}".format(blob_type))
+
+ def get_blob_type(self, url):
+ #Check blob type
+ logger.verb("Check blob type.")
+ timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+ resp = restutil.http_head(url, {
+ "x-ms-date" : timestamp,
+ 'x-ms-version' : self.__class__.__storage_version__
+ })
+ if resp is None or resp.status != httpclient.OK:
+ raise ProtocolError(("Failed to get status blob type: {0}"
+ "").format(resp.status))
+
+ blob_type = resp.getheader("x-ms-blob-type")
+ logger.verb("Blob type={0}".format(blob_type))
+ return blob_type
+
+ def put_block_blob(self, url, data):
+ logger.verb("Upload block blob")
+ timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+ resp = restutil.http_put(url, data, {
+ "x-ms-date" : timestamp,
+ "x-ms-blob-type" : "BlockBlob",
+ "Content-Length": text(len(data)),
+ "x-ms-version" : self.__class__.__storage_version__
+ })
+ if resp is None or resp.status != httpclient.CREATED:
+ raise ProtocolError(("Failed to upload block blob: {0}"
+ "").format(resp.status))
+
+ def put_page_blob(self, url, data):
+ logger.verb("Replace old page blob")
+ timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+ #Align to 512 bytes
+ page_blob_size = ((len(data) + 511) / 512) * 512
+ resp = restutil.http_put(url, "", {
+ "x-ms-date" : timestamp,
+ "x-ms-blob-type" : "PageBlob",
+ "Content-Length": "0",
+ "x-ms-blob-content-length" : text(page_blob_size),
+ "x-ms-version" : self.__class__.__storage_version__
+ })
+ if resp is None or resp.status != httpclient.CREATED:
+ raise ProtocolError(("Failed to clean up page blob: {0}"
+ "").format(resp.status))
+
+ if '?' in url < 0:
+ url = "{0}?comp=page".format(url)
+ else:
+ url = "{0}&comp=page".format(url)
+
+ logger.verb("Upload page blob")
+ page_max = 4 * 1024 * 1024 #Max page size: 4MB
+ start = 0
+ end = 0
+ while end < len(data):
+ end = min(len(data), start + page_max)
+ content_size = end - start
+ #Align to 512 bytes
+ page_end = int((end + 511) / 512) * 512
+ buf_size = page_end - start
+ buf = bytearray(source=data[start:end], encoding="utf-8")
+ #TODO buffer is not defined in python3, however we need this to make httplib to work on python 2.6
+ resp = restutil.http_put(url, buf, {
+ "x-ms-date" : timestamp,
+ "x-ms-range" : "bytes={0}-{1}".format(start, page_end - 1),
+ "x-ms-page-write" : "update",
+ "x-ms-version" : self.__class__.__storage_version__,
+ "Content-Length": text(page_end - start)
+ })
+ if resp is None or resp.status != httpclient.CREATED:
+ raise ProtocolError(("Failed to upload page blob: {0}"
+ "").format(resp.status))
+ start = end
+
+def event_param_to_v1(param):
+ param_format = '<Param Name="{0}" Value={1} T="{2}" />'
+ param_type = type(param.value)
+ attr_type = ""
+ if param_type is int:
+ attr_type = 'mt:uint64'
+ elif param_type is str:
+ attr_type = 'mt:wstr'
+ elif text(param_type).count("'unicode'") > 0:
+ attr_type = 'mt:wstr'
+ elif param_type is bool:
+ attr_type = 'mt:bool'
+ elif param_type is float:
+ attr_type = 'mt:float64'
+ return param_format.format(param.name, saxutils.quoteattr(text(param.value)),
+ attr_type)
+
+def event_to_v1(event):
+ params = ""
+ for param in event.parameters:
+ params += event_param_to_v1(param)
+ event_str = ('<Event id="{0}">'
+ '<![CDATA[{1}]]>'
+ '</Event>').format(event.eventId, params)
+ return event_str
+
+class WireClient(object):
+ def __init__(self, endpoint):
+ self.endpoint = endpoint
+ self.goal_state = None
+ self.updated = None
+ self.hosting_env = None
+ self.shared_conf = None
+ self.certs = None
+ self.ext_conf = None
+ self.req_count = 0
+
+ def update_hosting_env(self, goal_state):
+ if goal_state.hosting_env_uri is None:
+ raise ProtocolError("HostingEnvironmentConfig uri is empty")
+ local_file = HOSTING_ENV_FILE_NAME
+ xml_text = _fetch_uri(goal_state.hosting_env_uri, self.get_header())
+ fileutil.write_file(local_file, xml_text)
+ self.hosting_env = HostingEnv(xml_text)
+
+ def update_shared_conf(self, goal_state):
+ if goal_state.shared_conf_uri is None:
+ raise ProtocolError("SharedConfig uri is empty")
+ local_file = SHARED_CONF_FILE_NAME
+ xml_text = _fetch_uri(goal_state.shared_conf_uri, self.get_header())
+ fileutil.write_file(local_file, xml_text)
+ self.shared_conf = SharedConfig(xml_text)
+
+ def update_certs(self, goal_state):
+ if goal_state.certs_uri is None:
+ return
+ local_file = CERTS_FILE_NAME
+ xml_text = _fetch_uri(goal_state.certs_uri, self.get_header_for_cert())
+ fileutil.write_file(local_file, xml_text)
+ self.certs = Certificates(xml_text)
+
+ def update_ext_conf(self, goal_state):
+ if goal_state.ext_uri is None:
+ raise ProtocolError("ExtensionsConfig uri is empty")
+ incarnation = goal_state.incarnation
+ local_file = EXT_CONF_FILE_NAME.format(incarnation)
+ xml_text = _fetch_uri(goal_state.ext_uri,
+ self.get_header())
+ fileutil.write_file(local_file, xml_text)
+ self.ext_conf = ExtensionsConfig(xml_text)
+ for extension in self.ext_conf.ext_list.extensions:
+ self.update_ext_manifest(extension, goal_state)
+
+ def update_ext_manifest(self, extension, goal_state):
+ local_file = MANIFEST_FILE_NAME.format(extension.name,
+ goal_state.incarnation)
+ xml_text = _fetch_manifest(extension.version_uris)
+ fileutil.write_file(local_file, xml_text)
+
+ def update_goal_state(self, forced=False, max_retry=3):
+ uri = GOAL_STATE_URI.format(self.endpoint)
+ xml_text = _fetch_uri(uri, self.get_header())
+ goal_state = GoalState(xml_text)
+
+ if not forced:
+ last_incarnation = None
+ if(os.path.isfile(INCARNATION_FILE_NAME)):
+ last_incarnation = fileutil.read_file(INCARNATION_FILE_NAME)
+ new_incarnation = goal_state.incarnation
+ if last_incarnation is not None and \
+ last_incarnation == new_incarnation:
+ #Goalstate is not updated.
+ return
+
+ #Start updating goalstate, retry on 410
+ for retry in range(0, max_retry):
+ try:
+ self.goal_state = goal_state
+ goal_state_file = GOAL_STATE_FILE_NAME.format(goal_state.incarnation)
+ fileutil.write_file(goal_state_file, xml_text)
+ fileutil.write_file(INCARNATION_FILE_NAME,
+ goal_state.incarnation)
+ self.update_hosting_env(goal_state)
+ self.update_shared_conf(goal_state)
+ self.update_certs(goal_state)
+ self.update_ext_conf(goal_state)
+ return
+ except WireProtocolResourceGone:
+ logger.info("Incarnation is out of date. Update goalstate.")
+ xml_text = _fetch_uri(GOAL_STATE_URI, self.get_header())
+ goal_state = GoalState(xml_text)
+
+ raise ProtocolError("Exceeded max retry updating goal state")
+
+ def get_goal_state(self):
+ if(self.goal_state is None):
+ incarnation = _fetch_cache(INCARNATION_FILE_NAME)
+ goal_state_file = GOAL_STATE_FILE_NAME.format(incarnation)
+ xml_text = _fetch_cache(goal_state_file)
+ self.goal_state = GoalState(xml_text)
+ return self.goal_state
+
+ def get_hosting_env(self):
+ if(self.hosting_env is None):
+ xml_text = _fetch_cache(HOSTING_ENV_FILE_NAME)
+ self.hosting_env = HostingEnv(xml_text)
+ return self.hosting_env
+
+ def get_shared_conf(self):
+ if(self.shared_conf is None):
+ xml_text = _fetch_cache(SHARED_CONF_FILE_NAME)
+ self.shared_conf = SharedConfig(xml_text)
+ return self.shared_conf
+
+ def get_certs(self):
+ if(self.certs is None):
+ xml_text = _fetch_cache(Certificates)
+ self.certs = Certificates(xml_text)
+ if self.certs is None:
+ return None
+ return self.certs
+
+ def get_ext_conf(self):
+ if(self.ext_conf is None):
+ goal_state = self.get_goal_state()
+ local_file = EXT_CONF_FILE_NAME.format(goal_state.incarnation)
+ xml_text = _fetch_cache(local_file)
+ self.ext_conf = ExtensionsConfig(xml_text)
+ return self.ext_conf
+
+ def get_ext_manifest(self, extension, goal_state):
+ local_file = MANIFEST_FILE_NAME.format(extension.name,
+ goal_state.incarnation)
+ xml_text = _fetch_cache(local_file)
+ return ExtensionManifest(xml_text)
+
+ def check_wire_protocol_version(self):
+ uri = VERSION_INFO_URI.format(self.endpoint)
+ version_info_xml = _fetch_uri(uri, None)
+ version_info = VersionInfo(version_info_xml)
+
+ preferred = version_info.get_preferred()
+ if PROTOCOL_VERSION == preferred:
+ logger.info("Wire protocol version:{0}", PROTOCOL_VERSION)
+ elif PROTOCOL_VERSION in version_info.get_supported():
+ logger.info("Wire protocol version:{0}", PROTOCOL_VERSION)
+ logger.warn("Server prefered version:{0}", preferred)
+ else:
+ error = ("Agent supported wire protocol version: {0} was not "
+ "advised by Fabric.").format(PROTOCOL_VERSION)
+ raise ProtocolNotFound(error)
+
+ def upload_status_blob(self, vm_status):
+ ext_conf = self.get_ext_conf()
+ status_blob = StatusBlob(vm_status)
+ status_blob.upload(ext_conf.status_upload_blob)
+
+ def report_role_prop(self, thumbprint):
+ goal_state = self.get_goal_state()
+ role_prop = _build_role_properties(goal_state.container_id,
+ goal_state.role_instance_id,
+ thumbprint)
+ role_prop_uri = ROLE_PROP_URI.format(self.endpoint)
+ ret = restutil.http_post(role_prop_uri,
+ role_prop,
+ headers=self.get_header_for_xml_content())
+
+
+ def report_health(self, status, substatus, description):
+ goal_state = self.get_goal_state()
+ health_report = _build_health_report(goal_state.incarnation,
+ goal_state.container_id,
+ goal_state.role_instance_id,
+ status,
+ substatus,
+ description)
+ health_report_uri = HEALTH_REPORT_URI.format(self.endpoint)
+ headers = self.get_header_for_xml_content()
+ resp = restutil.http_post(health_report_uri,
+ health_report,
+ headers=headers)
+ def prevent_throttling(self):
+ self.req_count += 1
+ if self.req_count % 3 == 0:
+ logger.info("Sleep 15 before sending event to avoid throttling.")
+ self.req_count = 0
+ time.sleep(15)
+
+ def send_event(self, provider_id, event_str):
+ uri = TELEMETRY_URI.format(self.endpoint)
+ data_format = ('<?xml version="1.0"?>'
+ '<TelemetryData version="1.0">'
+ '<Provider id="{0}">{1}'
+ '</Provider>'
+ '</TelemetryData>')
+ data = data_format.format(provider_id, event_str)
+ try:
+ self.prevent_throttling()
+ header = self.get_header_for_xml_content()
+ resp = restutil.http_post(uri, data, header)
+ except restutil.HttpError as e:
+ raise ProtocolError("Failed to send events:{0}".format(e))
+
+ if resp.status != httpclient.OK:
+ logger.verb(resp.read())
+ raise ProtocolError("Failed to send events:{0}".format(resp.status))
+
+ def report_event(self, event_list):
+ buf = {}
+ #Group events by providerId
+ for event in event_list.events:
+ if event.providerId not in buf:
+ buf[event.providerId] = ""
+ event_str = event_to_v1(event)
+ if len(event_str) >= 63 * 1024:
+ logger.warn("Single event too large: {0}", event_str[300:])
+ continue
+ if len(buf[event.providerId] + event_str) >= 63 * 1024:
+ self.send_event(event.providerId, buf[event.providerId])
+ buf[event.providerId] = ""
+ buf[event.providerId] = buf[event.providerId] + event_str
+
+ #Send out all events left in buffer.
+ for provider_id in list(buf.keys()):
+ if len(buf[provider_id]) > 0:
+ self.send_event(provider_id, buf[provider_id])
+
+ def get_header(self):
+ return {
+ "x-ms-agent-name":"WALinuxAgent",
+ "x-ms-version":PROTOCOL_VERSION
+ }
+
+ def get_header_for_xml_content(self):
+ return {
+ "x-ms-agent-name":"WALinuxAgent",
+ "x-ms-version":PROTOCOL_VERSION,
+ "Content-Type":"text/xml;charset=utf-8"
+ }
+
+ def get_header_for_cert(self):
+ cert = ""
+ content = _fetch_cache(TRANSPORT_CERT_FILE_NAME)
+ for line in content.split('\n'):
+ if "CERTIFICATE" not in line:
+ cert += line.rstrip()
+ return {
+ "x-ms-agent-name":"WALinuxAgent",
+ "x-ms-version":PROTOCOL_VERSION,
+ "x-ms-cipher-name": "DES_EDE3_CBC",
+ "x-ms-guest-agent-public-x509-cert":cert
+ }
+
+class VersionInfo(object):
+ def __init__(self, xml_text):
+ """
+ Query endpoint server for wire protocol version.
+ Fail if our desired protocol version is not seen.
+ """
+ logger.verb("Load Version.xml")
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ xml_doc = parse_doc(xml_text)
+ preferred = find(xml_doc, "Preferred")
+ self.preferred = findtext(preferred, "Version")
+ logger.info("Fabric preferred wire protocol version:{0}", self.preferred)
+
+ self.supported = []
+ supported = find(xml_doc, "Supported")
+ supported_version = findall(supported, "Version")
+ for node in supported_version:
+ version = gettext(node)
+ logger.verb("Fabric supported wire protocol version:{0}", version)
+ self.supported.append(version)
+
+ def get_preferred(self):
+ return self.preferred
+
+ def get_supported(self):
+ return self.supported
+
+
+class GoalState(object):
+
+ def __init__(self, xml_text):
+ if xml_text is None:
+ raise ValueError("GoalState.xml is None")
+ logger.verb("Load GoalState.xml")
+ self.incarnation = None
+ self.expected_state = None
+ self.hosting_env_uri = None
+ self.shared_conf_uri = None
+ self.certs_uri = None
+ self.ext_uri = None
+ self.role_instance_id = None
+ self.container_id = None
+ self.load_balancer_probe_port = None
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ """
+ Request configuration data from endpoint server.
+ """
+ self.xml_text = xml_text
+ xml_doc = parse_doc(xml_text)
+ self.incarnation = findtext(xml_doc, "Incarnation")
+ self.expected_state = findtext(xml_doc, "ExpectedState")
+ self.hosting_env_uri = findtext(xml_doc, "HostingEnvironmentConfig")
+ self.shared_conf_uri = findtext(xml_doc, "SharedConfig")
+ self.certs_uri = findtext(xml_doc, "Certificates")
+ self.ext_uri = findtext(xml_doc, "ExtensionsConfig")
+ role_instance = find(xml_doc, "RoleInstance")
+ self.role_instance_id = findtext(role_instance, "InstanceId")
+ container = find(xml_doc, "Container")
+ self.container_id = findtext(container, "ContainerId")
+ lbprobe_ports = find(xml_doc, "LBProbePorts")
+ self.load_balancer_probe_port = findtext(lbprobe_ports, "Port")
+ return self
+
+
+class HostingEnv(object):
+ """
+ parse Hosting enviromnet config and store in
+ HostingEnvironmentConfig.xml
+ """
+ def __init__(self, xml_text):
+ if xml_text is None:
+ raise ValueError("HostingEnvironmentConfig.xml is None")
+ logger.verb("Load HostingEnvironmentConfig.xml")
+ self.vm_name = None
+ self.role_name = None
+ self.deployment_name = None
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ """
+ parse and create HostingEnvironmentConfig.xml.
+ """
+ self.xml_text = xml_text
+ xml_doc = parse_doc(xml_text)
+ incarnation = find(xml_doc, "Incarnation")
+ self.vm_name = getattrib(incarnation, "instance")
+ role = find(xml_doc, "Role")
+ self.role_name = getattrib(role, "name")
+ deployment = find(xml_doc, "Deployment")
+ self.deployment_name = getattrib(deployment, "name")
+ return self
+
+class SharedConfig(object):
+ """
+ parse role endpoint server and goal state config.
+ """
+ def __init__(self, xml_text):
+ logger.verb("Load SharedConfig.xml")
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ """
+ parse and write configuration to file SharedConfig.xml.
+ """
+ #Not used currently
+ return self
+
+class Certificates(object):
+
+ """
+ Object containing certificates of host and provisioned user.
+ """
+ def __init__(self, xml_text=None):
+ if xml_text is None:
+ raise ValueError("Certificates.xml is None")
+ logger.verb("Load Certificates.xml")
+ self.lib_dir = OSUTIL.get_lib_dir()
+ self.openssl_cmd = OSUTIL.get_openssl_cmd()
+ self.cert_list = CertList()
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ """
+ Parse multiple certificates into seperate files.
+ """
+ xml_doc = parse_doc(xml_text)
+ data = findtext(xml_doc, "Data")
+ if data is None:
+ return
+
+ p7m = ("MIME-Version:1.0\n"
+ "Content-Disposition: attachment; filename=\"{0}\"\n"
+ "Content-Type: application/x-pkcs7-mime; name=\"{1}\"\n"
+ "Content-Transfer-Encoding: base64\n"
+ "\n"
+ "{2}").format(P7M_FILE_NAME, P7M_FILE_NAME, data)
+
+ fileutil.write_file(os.path.join(self.lib_dir, P7M_FILE_NAME), p7m)
+ #decrypt certificates
+ cmd = ("{0} cms -decrypt -in {1} -inkey {2} -recip {3}"
+ "| {4} pkcs12 -nodes -password pass: -out {5}"
+ "").format(self.openssl_cmd, P7M_FILE_NAME,
+ TRANSPORT_PRV_FILE_NAME, TRANSPORT_CERT_FILE_NAME,
+ self.openssl_cmd, PEM_FILE_NAME)
+ shellutil.run(cmd)
+
+ #The parsing process use public key to match prv and crt.
+ buf = []
+ begin_crt = False
+ begin_prv = False
+ prvs = {}
+ thumbprints = {}
+ index = 0
+ v1_cert_list = []
+ with open(PEM_FILE_NAME) as pem:
+ for line in pem.readlines():
+ buf.append(line)
+ if re.match(r'[-]+BEGIN.*KEY[-]+', line):
+ begin_prv = True
+ elif re.match(r'[-]+BEGIN.*CERTIFICATE[-]+', line):
+ begin_crt = True
+ elif re.match(r'[-]+END.*KEY[-]+', line):
+ tmp_file = self.write_to_tmp_file(index, 'prv', buf)
+ pub = OSUTIL.get_pubkey_from_prv(tmp_file)
+ prvs[pub] = tmp_file
+ buf = []
+ index += 1
+ begin_prv = False
+ elif re.match(r'[-]+END.*CERTIFICATE[-]+', line):
+ tmp_file = self.write_to_tmp_file(index, 'crt', buf)
+ pub = OSUTIL.get_pubkey_from_crt(tmp_file)
+ thumbprint = OSUTIL.get_thumbprint_from_crt(tmp_file)
+ thumbprints[pub] = thumbprint
+ #Rename crt with thumbprint as the file name
+ crt = "{0}.crt".format(thumbprint)
+ v1_cert_list.append({
+ "name":None,
+ "thumbprint":thumbprint
+ })
+ os.rename(tmp_file, os.path.join(self.lib_dir, crt))
+ buf = []
+ index += 1
+ begin_crt = False
+
+ #Rename prv key with thumbprint as the file name
+ for pubkey in prvs:
+ thumbprint = thumbprints[pubkey]
+ if thumbprint:
+ tmp_file = prvs[pubkey]
+ prv = "{0}.prv".format(thumbprint)
+ os.rename(tmp_file, os.path.join(self.lib_dir, prv))
+
+ for v1_cert in v1_cert_list:
+ cert = Cert()
+ set_properties(cert, v1_cert)
+ self.cert_list.certificates.append(cert)
+
+ def write_to_tmp_file(self, index, suffix, buf):
+ file_name = os.path.join(self.lib_dir, "{0}.{1}".format(index, suffix))
+ with open(file_name, 'w') as tmp:
+ tmp.writelines(buf)
+ return file_name
+
+
+class ExtensionsConfig(object):
+ """
+ parse ExtensionsConfig, downloading and unpacking them to /var/lib/waagent.
+ Install if <enabled>true</enabled>, remove if it is set to false.
+ """
+
+ def __init__(self, xml_text):
+ if xml_text is None:
+ raise ValueError("ExtensionsConfig is None")
+ logger.verb("Load ExtensionsConfig.xml")
+ self.ext_list = ExtensionList()
+ self.status_upload_blob = None
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ """
+ Write configuration to file ExtensionsConfig.xml.
+ """
+ xml_doc = parse_doc(xml_text)
+ plugins_list = find(xml_doc, "Plugins")
+ plugins = findall(plugins_list, "Plugin")
+ plugin_settings_list = find(xml_doc, "PluginSettings")
+ plugin_settings = findall(plugin_settings_list, "Plugin")
+
+ for plugin in plugins:
+ ext = self.parse_ext(plugin)
+ self.ext_list.extensions.append(ext)
+ self.parse_ext_settings(ext, plugin_settings)
+
+ self.status_upload_blob = findtext(xml_doc, "StatusUploadBlob")
+
+ def parse_ext(self, plugin):
+ ext = Extension()
+ ext.name = getattrib(plugin, "name")
+ ext.properties.version = getattrib(plugin, "version")
+ ext.properties.state = getattrib(plugin, "state")
+
+ auto_upgrade = getattrib(plugin, "autoUpgrade")
+ if auto_upgrade is not None and auto_upgrade.lower() == "true":
+ ext.properties.upgradePolicy = "auto"
+ else:
+ ext.properties.upgradePolicy = "manual"
+
+ location = getattrib(plugin, "location")
+ failover_location = getattrib(plugin, "failoverlocation")
+ for uri in [location, failover_location]:
+ version_uri = ExtensionVersionUri()
+ version_uri.uri = uri
+ ext.version_uris.append(version_uri)
+ return ext
+
+ def parse_ext_settings(self, ext, plugin_settings):
+ if plugin_settings is None:
+ return
+
+ name = ext.name
+ version = ext.properties.version
+ settings = [x for x in plugin_settings \
+ if getattrib(x, "name") == name and \
+ getattrib(x ,"version") == version]
+
+ if settings is None or len(settings) == 0:
+ return
+
+ runtime_settings = None
+ runtime_settings_node = find(settings[0], "RuntimeSettings")
+ seqNo = getattrib(runtime_settings_node, "seqNo")
+ runtime_settings_str = gettext(runtime_settings_node)
+ try:
+ runtime_settings = json.loads(runtime_settings_str)
+ except ValueError as e:
+ logger.error("Invalid extension settings")
+ return
+
+ for plugin_settings_list in runtime_settings["runtimeSettings"]:
+ handler_settings = plugin_settings_list["handlerSettings"]
+ ext_settings = ExtensionSettings()
+ ext_settings.sequenceNumber = seqNo
+ ext_settings.publicSettings = handler_settings.get("publicSettings", None)
+ ext_settings.privateSettings = handler_settings.get("protectedSettings", None)
+ thumbprint = handler_settings.get("protectedSettingsCertThumbprint", None)
+ ext_settings.certificateThumbprint = thumbprint
+ ext.properties.extensions.append(ext_settings)
+
+class ExtensionManifest(object):
+ def __init__(self, xml_text):
+ if xml_text is None:
+ raise ValueError("ExtensionManifest is None")
+ logger.verb("Load ExtensionManifest.xml")
+ self.pkg_list = ExtensionPackageList()
+ self.parse(xml_text)
+
+ def parse(self, xml_text):
+ xml_doc = parse_doc(xml_text)
+ packages = findall(xml_doc, "Plugin")
+ for package in packages:
+ version = findtext(package, "Version")
+ uris = find(package, "Uris")
+ uri_list = findall(uris, "Uri")
+ uri_list = [gettext(x) for x in uri_list]
+ package = ExtensionPackage()
+ package.version = version
+ for uri in uri_list:
+ pkg_uri = ExtensionPackageUri()
+ pkg_uri.uri = uri
+ package.uris.append(pkg_uri)
+ self.pkg_list.versions.append(package)
+
diff --git a/azurelinuxagent/protocol/v2.py b/azurelinuxagent/protocol/v2.py
new file mode 100644
index 0000000..d7c9143
--- /dev/null
+++ b/azurelinuxagent/protocol/v2.py
@@ -0,0 +1,122 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+
+import json
+from azurelinuxagent.future import httpclient, text
+import azurelinuxagent.utils.restutil as restutil
+from azurelinuxagent.protocol.common import *
+
+ENDPOINT='169.254.169.254'
+#TODO use http for azure pack test
+#ENDPOINT='localhost'
+APIVERSION='2015-05-01-preview'
+BASE_URI = "http://{0}/Microsoft.Compute/{1}?api-version={{{2}}}{3}"
+
+def _add_content_type(headers):
+ if headers is None:
+ headers = {}
+ headers["content-type"] = "application/json"
+ return headers
+
+class MetadataProtocol(Protocol):
+
+ def __init__(self, apiversion=APIVERSION, endpoint=ENDPOINT):
+ self.apiversion = apiversion
+ self.endpoint = endpoint
+ self.identity_uri = BASE_URI.format(self.endpoint, "identity",
+ self.apiversion, "&$expand=*")
+ self.cert_uri = BASE_URI.format(self.endpoint, "certificates",
+ self.apiversion, "&$expand=*")
+ self.ext_uri = BASE_URI.format(self.endpoint, "extensionHandlers",
+ self.apiversion, "&$expand=*")
+ self.provision_status_uri = BASE_URI.format(self.endpoint,
+ "provisioningStatus",
+ self.apiversion, "")
+ self.status_uri = BASE_URI.format(self.endpoint, "status",
+ self.apiversion, "")
+ self.event_uri = BASE_URI.format(self.endpoint, "status/telemetry",
+ self.apiversion, "")
+
+ def _get_data(self, data_type, url, headers=None):
+ try:
+ resp = restutil.http_get(url, headers=headers)
+ except restutil.HttpError as e:
+ raise ProtocolError(text(e))
+
+ if resp.status != httpclient.OK:
+ raise ProtocolError("{0} - GET: {1}".format(resp.status, url))
+ try:
+ data = resp.read()
+ if data is None:
+ return None
+ data = json.loads(text(data, encoding="utf-8"))
+ except ValueError as e:
+ raise ProtocolError(text(e))
+ obj = data_type()
+ set_properties(obj, data)
+ return obj
+
+ def _put_data(self, url, obj, headers=None):
+ headers = _add_content_type(headers)
+ data = get_properties(obj)
+ try:
+ resp = restutil.http_put(url, json.dumps(data), headers=headers)
+ except restutil.HttpError as e:
+ raise ProtocolError(text(e))
+ if resp.status != httpclient.OK:
+ raise ProtocolError("{0} - PUT: {1}".format(resp.status, url))
+
+ def _post_data(self, url, obj, headers=None):
+ headers = _add_content_type(headers)
+ data = get_properties(obj)
+ try:
+ resp = restutil.http_post(url, json.dumps(data), headers=headers)
+ except restutil.HttpError as e:
+ raise ProtocolError(text(e))
+ if resp.status != httpclient.CREATED:
+ raise ProtocolError("{0} - POST: {1}".format(resp.status, url))
+
+ def initialize(self):
+ pass
+
+ def get_vminfo(self):
+ return self._get_data(VMInfo, self.identity_uri)
+
+ def get_certs(self):
+ #TODO walk arround for azure pack test
+ return CertList()
+
+ certs = self._get_data(CertList, self.cert_uri)
+ #TODO download pfx and convert to pem
+ return certs
+
+ def get_extensions(self):
+ return self._get_data(ExtensionList, self.ext_uri)
+
+ def report_provision_status(self, status):
+ validata_param('status', status, ProvisionStatus)
+ self._put_data(self.provision_status_uri, status)
+
+ def report_status(self, status):
+ validata_param('status', status, VMStatus)
+ self._put_data(self.status_uri, status)
+
+ def report_event(self, events):
+ validata_param('events', events, TelemetryEventList)
+ self._post_data(self.event_uri, events)
+
diff --git a/azurelinuxagent/utils/__init__.py b/azurelinuxagent/utils/__init__.py
new file mode 100644
index 0000000..4b2b9e1
--- /dev/null
+++ b/azurelinuxagent/utils/__init__.py
@@ -0,0 +1,19 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
diff --git a/azurelinuxagent/utils/fileutil.py b/azurelinuxagent/utils/fileutil.py
new file mode 100644
index 0000000..5e7fecf
--- /dev/null
+++ b/azurelinuxagent/utils/fileutil.py
@@ -0,0 +1,186 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+"""
+File operation util functions
+"""
+
+import os
+import re
+import shutil
+import pwd
+import tempfile
+import azurelinuxagent.logger as logger
+from azurelinuxagent.future import text
+import azurelinuxagent.utils.textutil as textutil
+
+def read_file(filepath, asbin=False, remove_bom=False, encoding='utf-8'):
+ """
+ Read and return contents of 'filepath'.
+ """
+ mode = 'rb'
+ with open(filepath, mode) as in_file:
+ data = in_file.read()
+ if data is None:
+ return None
+
+ if asbin:
+ return data
+
+ if remove_bom:
+ #Remove bom on bytes data before it is converted into string.
+ data = textutil.remove_bom(data)
+ data = text(data, encoding=encoding)
+ return data
+
+def write_file(filepath, contents, asbin=False, encoding='utf-8', append=False):
+ """
+ Write 'contents' to 'filepath'.
+ """
+ mode = "ab" if append else "wb"
+ data = contents
+ if not asbin:
+ data = contents.encode(encoding)
+ with open(filepath, mode) as out_file:
+ out_file.write(data)
+
+def append_file(filepath, contents, asbin=False, encoding='utf-8'):
+ """
+ Append 'contents' to 'filepath'.
+ """
+ write_file(filepath, contents, asbin=asbin, encoding=encoding, append=True)
+
+def replace_file(filepath, contents):
+ """
+ Write 'contents' to 'filepath' by creating a temp file,
+ and replacing original.
+ """
+ handle, temp = tempfile.mkstemp(dir=os.path.dirname(filepath))
+ #if type(contents) == str:
+ #contents=contents.encode('latin-1')
+ try:
+ os.write(handle, contents)
+ except IOError as err:
+ logger.error('Write to file {0}, Exception is {1}', filepath, err)
+ return 1
+ finally:
+ os.close(handle)
+
+ try:
+ os.rename(temp, filepath)
+ except IOError as err:
+ logger.info('Rename {0} to {1}, Exception is {2}', temp, filepath, err)
+ logger.info('Remove original file and retry')
+ try:
+ os.remove(filepath)
+ except IOError as err:
+ logger.error('Remove {0}, Exception is {1}', temp, filepath, err)
+
+ try:
+ os.rename(temp, filepath)
+ except IOError as err:
+ logger.error('Rename {0} to {1}, Exception is {2}', temp, filepath,
+ err)
+ return 1
+ return 0
+
+def base_name(path):
+ head, tail = os.path.split(path)
+ return tail
+
+def get_line_startingwith(prefix, filepath):
+ """
+ Return line from 'filepath' if the line startswith 'prefix'
+ """
+ for line in read_file(filepath).split('\n'):
+ if line.startswith(prefix):
+ return line
+ return None
+
+#End File operation util functions
+
+def mkdir(dirpath, mode=None, owner=None):
+ if not os.path.isdir(dirpath):
+ os.makedirs(dirpath)
+ if mode is not None:
+ chmod(dirpath, mode)
+ if owner is not None:
+ chowner(dirpath, owner)
+
+def chowner(path, owner):
+ owner_info = pwd.getpwnam(owner)
+ os.chown(path, owner_info[2], owner_info[3])
+
+def chmod(path, mode):
+ os.chmod(path, mode)
+
+def rm_files(*args):
+ for path in args:
+ if os.path.isfile(path):
+ os.remove(path)
+
+def rm_dirs(*args):
+ """
+ Remove all the contents under the directry
+ """
+ for dir_name in args:
+ if os.path.isdir(dir_name):
+ for item in os.listdir(dir_name):
+ path = os.path.join(dir_name, item)
+ if os.path.isfile(path):
+ os.remove(path)
+ elif os.path.isdir(path):
+ shutil.rmtree(path)
+
+def update_conf_file(path, line_start, val, chk_err=False):
+ conf = []
+ if not os.path.isfile(path) and chk_err:
+ raise Exception("Can't find config file:{0}".format(path))
+ conf = read_file(path).split('\n')
+ conf = [x for x in conf if not x.startswith(line_start)]
+ conf.append(val)
+ replace_file(path, '\n'.join(conf))
+
+def search_file(target_dir_name, target_file_name):
+ for root, dirs, files in os.walk(target_dir_name):
+ for file_name in files:
+ if file_name == target_file_name:
+ return os.path.join(root, file_name)
+ return None
+
+def chmod_tree(path, mode):
+ for root, dirs, files in os.walk(path):
+ for file_name in files:
+ os.chmod(os.path.join(root, file_name), mode)
+
+def findstr_in_file(file_path, pattern_str):
+ """
+ Return match object if found in file.
+ """
+ try:
+ pattern = re.compile(pattern_str)
+ for line in (open(file_path, 'r')).readlines():
+ match = re.search(pattern, line)
+ if match:
+ return match
+ except:
+ raise
+
+ return None
+
diff --git a/azurelinuxagent/utils/osutil.py b/azurelinuxagent/utils/osutil.py
new file mode 100644
index 0000000..756400c
--- /dev/null
+++ b/azurelinuxagent/utils/osutil.py
@@ -0,0 +1,27 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+"""
+Load OSUtil implementation from azurelinuxagent.distro
+"""
+from azurelinuxagent.distro.default.osutil import OSUtilError
+import azurelinuxagent.distro.loader as loader
+
+OSUTIL = loader.get_osutil()
+
diff --git a/azurelinuxagent/utils/restutil.py b/azurelinuxagent/utils/restutil.py
new file mode 100644
index 0000000..1015f71
--- /dev/null
+++ b/azurelinuxagent/utils/restutil.py
@@ -0,0 +1,154 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import time
+import platform
+import os
+import subprocess
+import azurelinuxagent.logger as logger
+import azurelinuxagent.conf as conf
+from azurelinuxagent.future import httpclient, urlparse
+
+"""
+REST api util functions
+"""
+
+RETRY_WAITING_INTERVAL = 10
+
+class HttpError(Exception):
+ pass
+
+def _parse_url(url):
+ o = urlparse(url)
+ rel_uri = o.path
+ if o.fragment:
+ rel_uri = "{0}#{1}".format(rel_uri, o.fragment)
+ if o.query:
+ rel_uri = "{0}?{1}".format(rel_uri, o.query)
+ secure = False
+ if o.scheme.lower() == "https":
+ secure = True
+ return o.hostname, o.port, secure, rel_uri
+
+def get_http_proxy():
+ """
+ Get http_proxy and https_proxy from environment variables.
+ Username and password is not supported now.
+ """
+ host = conf.get("HttpProxy.Host", None)
+ port = conf.get("HttpProxy.Port", None)
+ return (host, port)
+
+def _http_request(method, host, rel_uri, port=None, data=None, secure=False,
+ headers=None, proxy_host=None, proxy_port=None):
+ url, conn = None, None
+ if secure:
+ port = 443 if port is None else port
+ if proxy_host is not None and proxy_port is not None:
+ conn = httpclient.HTTPSConnection(proxy_host, proxy_port)
+ conn.set_tunnel(host, port)
+ #If proxy is used, full url is needed.
+ url = "https://{0}:{1}{2}".format(host, port, rel_uri)
+ else:
+ conn = httpclient.HTTPSConnection(host, port)
+ url = rel_uri
+ else:
+ port = 80 if port is None else port
+ if proxy_host is not None and proxy_port is not None:
+ conn = httpclient.HTTPConnection(proxy_host, proxy_port)
+ #If proxy is used, full url is needed.
+ url = "http://{0}:{1}{2}".format(host, port, rel_uri)
+ else:
+ conn = httpclient.HTTPConnection(host, port)
+ url = rel_uri
+ if headers == None:
+ conn.request(method, url, data)
+ else:
+ conn.request(method, url, data, headers)
+ resp = conn.getresponse()
+ return resp
+
+def http_request(method, url, data, headers=None, max_retry=3, chk_proxy=False):
+ """
+ Sending http request to server
+ On error, sleep 10 and retry max_retry times.
+ """
+ logger.verb("HTTP Req: {0} {1}", method, url)
+ logger.verb(" Data={0}", data)
+ logger.verb(" Header={0}", headers)
+ host, port, secure, rel_uri = _parse_url(url)
+
+ #Check proxy
+ proxy_host, proxy_port = (None, None)
+ if chk_proxy:
+ proxy_host, proxy_port = get_http_proxy()
+
+ #If httplib module is not built with ssl support. Fallback to http
+ if secure and not hasattr(httpclient, "HTTPSConnection"):
+ logger.warn("httplib is not built with ssl support")
+ secure = False
+
+ #If httplib module doesn't support https tunnelling. Fallback to http
+ if secure and \
+ proxy_host is not None and \
+ proxy_port is not None and \
+ not hasattr(httpclient.HTTPSConnection, "set_tunnel"):
+ logger.warn("httplib doesn't support https tunnelling(new in python 2.7)")
+ secure = False
+
+ for retry in range(0, max_retry):
+ try:
+ resp = _http_request(method, host, rel_uri, port=port, data=data,
+ secure=secure, headers=headers,
+ proxy_host=proxy_host, proxy_port=proxy_port)
+ logger.verb("HTTP Resp: Status={0}", resp.status)
+ logger.verb(" Header={0}", resp.getheaders())
+ return resp
+ except httpclient.HTTPException as e:
+ logger.warn('HTTPException {0}, args:{1}', e, repr(e.args))
+ except IOError as e:
+ logger.warn('Socket IOError {0}, args:{1}', e, repr(e.args))
+
+ if retry < max_retry - 1:
+ logger.info("Retry={0}, {1} {2}", retry, method, url)
+ time.sleep(RETRY_WAITING_INTERVAL)
+
+ raise HttpError("HTTP Err: {0} {1}".format(method, url))
+
+def http_get(url, headers=None, max_retry=3, chk_proxy=False):
+ return http_request("GET", url, data=None, headers=headers,
+ max_retry=max_retry, chk_proxy=chk_proxy)
+
+def http_head(url, headers=None, max_retry=3, chk_proxy=False):
+ return http_request("HEAD", url, None, headers=headers,
+ max_retry=max_retry, chk_proxy=chk_proxy)
+
+def http_post(url, data, headers=None, max_retry=3, chk_proxy=False):
+ return http_request("POST", url, data, headers=headers,
+ max_retry=max_retry, chk_proxy=chk_proxy)
+
+def http_put(url, data, headers=None, max_retry=3, chk_proxy=False):
+ return http_request("PUT", url, data, headers=headers,
+ max_retry=max_retry, chk_proxy=chk_proxy)
+
+def http_delete(url, headers=None, max_retry=3, chk_proxy=False):
+ return http_request("DELETE", url, None, headers=headers,
+ max_retry=max_retry, chk_proxy=chk_proxy)
+
+#End REST api util functions
diff --git a/azurelinuxagent/utils/shellutil.py b/azurelinuxagent/utils/shellutil.py
new file mode 100644
index 0000000..f4305d9
--- /dev/null
+++ b/azurelinuxagent/utils/shellutil.py
@@ -0,0 +1,85 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+#
+
+import platform
+import os
+import subprocess
+from azurelinuxagent.future import text
+import azurelinuxagent.logger as logger
+
+if not hasattr(subprocess,'check_output'):
+ def check_output(*popenargs, **kwargs):
+ r"""Backport from subprocess module from python 2.7"""
+ if 'stdout' in kwargs:
+ raise ValueError('stdout argument not allowed, '
+ 'it will be overridden.')
+ process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
+ output, unused_err = process.communicate()
+ retcode = process.poll()
+ if retcode:
+ cmd = kwargs.get("args")
+ if cmd is None:
+ cmd = popenargs[0]
+ raise subprocess.CalledProcessError(retcode, cmd, output=output)
+ return output
+
+ # Exception classes used by this module.
+ class CalledProcessError(Exception):
+ def __init__(self, returncode, cmd, output=None):
+ self.returncode = returncode
+ self.cmd = cmd
+ self.output = output
+ def __str__(self):
+ return ("Command '{0}' returned non-zero exit status {1}"
+ "").format(self.cmd, self.returncode)
+
+ subprocess.check_output=check_output
+ subprocess.CalledProcessError=CalledProcessError
+
+
+"""
+Shell command util functions
+"""
+def run(cmd, chk_err=True):
+ """
+ Calls run_get_output on 'cmd', returning only the return code.
+ If chk_err=True then errors will be reported in the log.
+ If chk_err=False then errors will be suppressed from the log.
+ """
+ retcode,out=run_get_output(cmd,chk_err)
+ return retcode
+
+def run_get_output(cmd, chk_err=True):
+ """
+ Wrapper for subprocess.check_output.
+ Execute 'cmd'. Returns return code and STDOUT, trapping expected exceptions.
+ Reports exceptions to Error if chk_err parameter is True
+ """
+ logger.verb("run cmd '{0}'", cmd)
+ try:
+ output=subprocess.check_output(cmd,stderr=subprocess.STDOUT,shell=True)
+ except subprocess.CalledProcessError as e :
+ if chk_err :
+ logger.error("run cmd '{0}' failed", e.cmd)
+ logger.error("Error Code:{0}", e.returncode)
+ logger.error("Result:{0}", e.output[:-1].decode('latin-1'))
+ return e.returncode, e.output.decode('latin-1')
+ return 0, text(output, encoding="utf-8")
+
+#End shell command util functions
diff --git a/azurelinuxagent/utils/textutil.py b/azurelinuxagent/utils/textutil.py
new file mode 100644
index 0000000..2e66b0e
--- /dev/null
+++ b/azurelinuxagent/utils/textutil.py
@@ -0,0 +1,228 @@
+# Windows Azure Linux Agent
+#
+# Copyright 2014 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.4+ and Openssl 1.0+
+
+import crypt
+import random
+import string
+import struct
+import xml.dom.minidom as minidom
+import sys
+
+def parse_doc(xml_text):
+ """
+ Parse xml document from string
+ """
+ #The minidom lib has some issue with unicode in python2.
+ #Encode the string into utf-8 first
+ xml_text = xml_text.encode('utf-8')
+ return minidom.parseString(xml_text)
+
+def findall(root, tag, namespace=None):
+ """
+ Get all nodes by tag and namespace under Node root.
+ """
+ if root is None:
+ return []
+
+ if namespace is None:
+ return root.getElementsByTagName(tag)
+ else:
+ return root.getElementsByTagNameNS(namespace, tag)
+
+def find(root, tag, namespace=None):
+ """
+ Get first node by tag and namespace under Node root.
+ """
+ nodes = findall(root, tag, namespace=namespace)
+ if nodes is not None and len(nodes) >= 1:
+ return nodes[0]
+ else:
+ return None
+
+def gettext(node):
+ """
+ Get node text
+ """
+ if node is None:
+ return None
+
+ for child in node.childNodes:
+ if child.nodeType == child.TEXT_NODE:
+ return child.data
+ return None
+
+def findtext(root, tag, namespace=None):
+ """
+ Get text of node by tag and namespace under Node root.
+ """
+ node = find(root, tag, namespace=namespace)
+ return gettext(node)
+
+def getattrib(node, attr_name):
+ """
+ Get attribute of xml node
+ """
+ if node is not None:
+ return node.getAttribute(attr_name)
+ else:
+ return None
+
+def unpack(buf, offset, range):
+ """
+ Unpack bytes into python values.
+ """
+ result = 0
+ for i in range:
+ result = (result << 8) | str_to_ord(buf[offset + i])
+ return result
+
+def unpack_little_endian(buf, offset, length):
+ """
+ Unpack little endian bytes into python values.
+ """
+ return unpack(buf, offset, list(range(length - 1, -1, -1)))
+
+def unpack_big_endian(buf, offset, length):
+ """
+ Unpack big endian bytes into python values.
+ """
+ return unpack(buf, offset, list(range(0, length)))
+
+def hex_dump3(buf, offset, length):
+ """
+ Dump range of buf in formatted hex.
+ """
+ return ''.join(['%02X' % str_to_ord(char) for char in buf[offset:offset + length]])
+
+def hex_dump2(buf):
+ """
+ Dump buf in formatted hex.
+ """
+ return hex_dump3(buf, 0, len(buf))
+
+def is_in_range(a, low, high):
+ """
+ Return True if 'a' in 'low' <= a >= 'high'
+ """
+ return (a >= low and a <= high)
+
+def is_printable(ch):
+ """
+ Return True if character is displayable.
+ """
+ return (is_in_range(ch, str_to_ord('A'), str_to_ord('Z'))
+ or is_in_range(ch, str_to_ord('a'), str_to_ord('z'))
+ or is_in_range(ch, str_to_ord('0'), str_to_ord('9')))
+
+def hex_dump(buffer, size):
+ """
+ Return Hex formated dump of a 'buffer' of 'size'.
+ """
+ if size < 0:
+ size = len(buffer)
+ result = ""
+ for i in range(0, size):
+ if (i % 16) == 0:
+ result += "%06X: " % i
+ byte = buffer[i]
+ if type(byte) == str:
+ byte = ord(byte.decode('latin1'))
+ result += "%02X " % byte
+ if (i & 15) == 7:
+ result += " "
+ if ((i + 1) % 16) == 0 or (i + 1) == size:
+ j = i
+ while ((j + 1) % 16) != 0:
+ result += " "
+ if (j & 7) == 7:
+ result += " "
+ j += 1
+ result += " "
+ for j in range(i - (i % 16), i + 1):
+ byte=buffer[j]
+ if type(byte) == str:
+ byte = str_to_ord(byte.decode('latin1'))
+ k = '.'
+ if is_printable(byte):
+ k = chr(byte)
+ result += k
+ if (i + 1) != size:
+ result += "\n"
+ return result
+
+def str_to_ord(a):
+ """
+ Allows indexing into a string or an array of integers transparently.
+ Generic utility function.
+ """
+ if type(a) == type(b'') or type(a) == type(u''):
+ a = ord(a)
+ return a
+
+def compare_bytes(a, b, start, length):
+ for offset in range(start, start + length):
+ if str_to_ord(a[offset]) != str_to_ord(b[offset]):
+ return False
+ return True
+
+def int_to_ip4_addr(a):
+ """
+ Build DHCP request string.
+ """
+ return "%u.%u.%u.%u" % ((a >> 24) & 0xFF,
+ (a >> 16) & 0xFF,
+ (a >> 8) & 0xFF,
+ (a) & 0xFF)
+
+def hexstr_to_bytearray(a):
+ """
+ Return hex string packed into a binary struct.
+ """
+ b = b""
+ for c in range(0, len(a) // 2):
+ b += struct.pack("B", int(a[c * 2:c * 2 + 2], 16))
+ return b
+
+def set_ssh_config(config, name, val):
+ notfound = True
+ for i in range(0, len(config)):
+ if config[i].startswith(name):
+ config[i] = "{0} {1}".format(name, val)
+ notfound = False
+ elif config[i].startswith("Match"):
+ #Match block must be put in the end of sshd config
+ break
+ if notfound:
+ config.insert(i, "{0} {1}".format(name, val))
+ return config
+
+def remove_bom(c):
+ if str_to_ord(c[0]) > 128 and str_to_ord(c[1]) > 128 and \
+ str_to_ord(c[2]) > 128:
+ c = c[3:]
+ return c
+
+def gen_password_hash(password, use_salt, salt_type, salt_len):
+ salt="$6$"
+ if use_salt:
+ collection = string.ascii_letters + string.digits
+ salt = ''.join(random.choice(collection) for _ in range(salt_len))
+ salt = "${0}${1}".format(salt_type, salt)
+ return crypt.crypt(password, salt)
+
+