diff options
Diffstat (limited to 'azurelinuxagent/distro')
39 files changed, 3497 insertions, 0 deletions
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.") |