diff options
author | Ben Howard <ben.howard@ubuntu.com> | 2015-08-14 16:40:41 -0600 |
---|---|---|
committer | usd-importer <ubuntu-server@lists.ubuntu.com> | 2015-08-15 14:33:21 +0000 |
commit | f78b9650d0e7b008d430673a075aad95dda863be (patch) | |
tree | a6749619e78483d45a66d4bad4d6e922391541fc /azurelinuxagent | |
parent | 0afc048f2a6ff3638ecfa33e7ded5dc8dddf041a (diff) | |
download | vyos-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')
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) + + |