diff options
Diffstat (limited to 'waagent')
-rwxr-xr-x[-rw-r--r--] | waagent | 3803 |
1 files changed, 2710 insertions, 1093 deletions
@@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # # Windows Azure Linux Agent # @@ -35,6 +35,7 @@ import shutil import socket import SocketServer import struct +import string import subprocess import sys import tempfile @@ -43,29 +44,8 @@ import threading import time import traceback import xml.dom.minidom - -GuestAgentName = "WALinuxAgent" -GuestAgentLongName = "Windows Azure Linux Agent" -GuestAgentVersion = "WALinuxAgent-1.3.2" -ProtocolVersion = "2011-12-31" - -Config = None -LinuxDistro = "UNKNOWN" -PackagedForDistro = "UNKNOWN" -Verbose = False -WaAgent = None -DiskActivated = False -Openssl = "openssl" -Children = [] +import fcntl -PossibleEthernetInterfaces = ["seth0", "seth1", "eth0", "eth1"] -RulesFiles = [ "/lib/udev/rules.d/75-persistent-net-generator.rules", - "/etc/udev/rules.d/70-persistent-net.rules" ] -VarLibDhcpDirectories = ["/var/lib/dhclient", "/var/lib/dhcpcd", "/var/lib/dhcp"] -EtcDhcpClientConfFiles = ["/etc/dhcp/dhclient.conf", "/etc/dhcp3/dhclient.conf"] -LibDir = "/var/lib/waagent" - -# backport subprocess.check_output if not defined ( for python version < 2.7) if not hasattr(subprocess,'check_output'): def check_output(*popenargs, **kwargs): r"""Backport from subprocess module from python 2.7""" @@ -92,164 +72,1678 @@ if not hasattr(subprocess,'check_output'): subprocess.check_output=check_output subprocess.CalledProcessError=CalledProcessError + +GuestAgentName = "WALinuxAgent" +GuestAgentLongName = "Windows Azure Linux Agent" +GuestAgentVersion = "WALinuxAgent-2.0.3" +ProtocolVersion = "2012-11-30" #WARNING this value is used to confirm the correct fabric protocol. -# This lets us index into a string or an array of integers transparently. -def Ord(a): - if type(a) == type("a"): - a = ord(a) - return a +Config = None +WaAgent = None +DiskActivated = False +Openssl = "openssl" +Children = [] -def IsWindows(): - return (platform.uname()[0] == "Windows") +VMM_STARTUP_SCRIPT_NAME='install' +VMM_CONFIG_FILE_NAME='linuxosconfiguration.xml' +global RulesFiles +RulesFiles = [ "/lib/udev/rules.d/75-persistent-net-generator.rules", + "/etc/udev/rules.d/70-persistent-net.rules" ] +VarLibDhcpDirectories = ["/var/lib/dhclient", "/var/lib/dhcpcd", "/var/lib/dhcp"] +EtcDhcpClientConfFiles = ["/etc/dhcp/dhclient.conf", "/etc/dhcp3/dhclient.conf"] +global LibDir +LibDir = "/var/lib/waagent" -def IsLinux(): - return (platform.uname()[0] == "Linux") +WaagentConf = """\ +# +# Windows Azure Linux Agent Configuration +# -def DetectLinuxDistro(): - global LinuxDistro - global PackagedForDistro - if os.path.isfile("/etc/redhat-release"): - LinuxDistro = "RedHat" - return True - if os.path.isfile("/etc/lsb-release") and "Ubuntu" in GetFileContents("/etc/lsb-release"): - LinuxDistro = "Ubuntu" +Role.StateConsumer=None # Specified program is invoked with the argument "Ready" when we report ready status + # to the endpoint server. +Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role configuration. +Role.TopologyConsumer=None # Specified program is invoked with XML file argument specifying role topology. + +Provisioning.Enabled=y # +Provisioning.DeleteRootPassword=y # Password authentication for root account will be unavailable. +Provisioning.RegenerateSshHostKeyPair=y # Generate fresh host key pair. +Provisioning.SshHostKeyPairType=rsa # Supported values are "rsa", "dsa" and "ecdsa". +Provisioning.MonitorHostName=y # Monitor host name changes and publish changes via DHCP requests. + +ResourceDisk.Format=y # Format if unformatted. If 'n', resource disk will not be mounted. +ResourceDisk.Filesystem=ext4 # Typically ext3 or ext4. FreeBSD images should use 'ufs2' here. +ResourceDisk.MountPoint=/mnt/resource # +ResourceDisk.EnableSwap=n # Create and use swapfile on resource disk. +ResourceDisk.SwapSizeMB=0 # Size of the swapfile. - # Should this run as if it is packaged Ubuntu? +LBProbeResponder=y # Respond to load balancer probes if requested by Windows Azure. + +Logs.Verbose=n # + +OS.RootDeviceScsiTimeout=300 # Root device timeout in seconds. +OS.OpensslPath=None # If "None", the system default version is used. +""" + +############################################################ +# BEGIN DISTRO CLASS DEFS +############################################################ +############################################################ +# AbstractDistro +############################################################ +class AbstractDistro(object): + """ + AbstractDistro defines a skeleton neccesary for a concrete Distro class. + + Generic methods and attributes are kept here, distribution specific attributes + and behavior are to be placed in the concrete child named distroDistro, where + distro is the string returned by calling python platform.dist()[0]. So for CentOS + the derived class is called 'centosDistro'. + """ + + def __init__(self): + """ + Generic Attributes go here. These are based on 'majority rules'. + This __init__() may be called or overriden by the child. + """ + self.agent_service_name = os.path.basename(sys.argv[0]) + self.selinux=None + self.service_cmd='/usr/sbin/service' + self.ssh_service_name='ssh' + self.ssh_config_file='/etc/ssh/sshd_config' + self.hostname_file_path='/etc/hostname' + self.dhcp_client_name='dhclient' + self.requiredDeps = [ 'route', 'shutdown', 'ssh-keygen', 'useradd' + , 'openssl', 'sfdisk', 'fdisk', 'mkfs', 'chpasswd', 'sed', 'grep', 'sudo' ] + self.init_script_file='/etc/init.d/waagent' + self.agent_package_name='WALinuxAgent' + self.fileBlackList = [ "/root/.bash_history", "/var/log/waagent.log",'/etc/resolv.conf' ] + self.agent_files_to_uninstall = ["/etc/waagent.conf", "/etc/logrotate.d/waagent", "/etc/sudoers.d/waagent"] + self.grubKernelBootOptionsFile = '/etc/default/grub' + self.grubKernelBootOptionsLine = 'GRUB_CMDLINE_LINUX_DEFAULT=' + self.getpidcmd = 'pidof' + self.mount_dvd_cmd = 'mount' + self.sudoers_dir_base = '/etc' + self.waagent_conf_file = WaagentConf + self.shadow_file_mode=0600 + + def isSelinuxSystem(self): + """ + Checks and sets self.selinux = True if SELinux is available on system. + """ + if self.selinux == None: + if Run("which getenforce",chk_err=False): + self.selinux = False + else: + self.selinux = True + return self.selinux + + def isSelinuxRunning(self): + """ + Calls shell command 'getenforce' and returns True if 'Enforcing'. + """ + if self.isSelinuxSystem(): + return RunGetOutput("getenforce")[1].startswith("Enforcing") + else: + return False + + def setSelinuxEnforce(self,state): + """ + Calls shell command 'setenforce' with 'state' and returns resulting exit code. + """ + if self.isSelinuxSystem(): + if state: s = '1' + else: s='0' + return Run("setenforce "+s) + + def setSelinuxContext(self,path,cn): + """ + Calls shell 'chcon' with 'path' and 'cn' context. + Returns exit result. + """ + if self.isSelinuxSystem(): + return Run('chcon ' + cn + ' ' + path) + + def setHostname(self,name): + """ + Shell call to hostname. + Returns resulting exit code. + """ + return Run('hostname ' + name) + + def publishHostname(self,name): + """ + Set the contents of the hostname file to 'name'. + Return 1 on failure. + """ try: - cmd="dpkg -S %s" % os.path.basename(__file__) - retcode, krn = RunGetOutput(cmd,chk_err=False) - if not retcode: - PackagedForDistro = "Ubuntu" + r=SetFileContents(self.hostname_file_path, name) + for f in EtcDhcpClientConfFiles: + if os.path.exists(f) and FindStringInFile(f,r'^[^#]*?send\s*host-name.*?(<hostname>|gethostname[(,)])') == None : + r=ReplaceFileContentsAtomic('/etc/dhcp/dhclient.conf', "send host-name \"" + name + "\";\n" + + "\n".join(filter(lambda a: not a.startswith("send host-name"), GetFileContents('/etc/dhcp/dhclient.conf').split('\n')))) + except: + return 1 + return r + + def installAgentServiceScriptFiles(self): + """ + Create the waagent support files for service installation. + Called by registerAgentService() + Abstract Virtual Function. Over-ridden in concrete Distro classes. + """ + pass - except IOError as e: - pass + def registerAgentService(self): + """ + Calls installAgentService to create service files. + Shell exec service registration commands. (e.g. chkconfig --add waagent) + Abstract Virtual Function. Over-ridden in concrete Distro classes. + """ + pass + + def uninstallAgentService(self): + """ + Call service subsystem to remove waagent script. + Abstract Virtual Function. Over-ridden in concrete Distro classes. + """ + pass + + def unregisterAgentService(self): + """ + Calls self.stopAgentService and call self.uninstallAgentService() + """ + self.stopAgentService() + self.uninstallAgentService() + + def startAgentService(self): + """ + Service call to start the Agent service + """ + return Run(self.service_cmd + ' ' + self.agent_service_name + ' start') + + def stopAgentService(self): + """ + Service call to stop the Agent service + """ + return Run(self.service_cmd + ' ' + self.agent_service_name + ' stop',False) + + def restartSshService(self): + """ + Service call to re(start) the SSH service + """ + if not Run(self.service_cmd + " " + self.ssh_service_name + " status | grep running"): + return Run(self.service_cmd + " " + self.ssh_service_name + " reload") + else: + return 0 + + def sshDeployPublicKey(self,fprint,path): + """ + Generic sshDeployPublicKey - over-ridden in some concrete Distro classes due to minor differences in openssl packages deployed + """ + error=0 + SshPubKey = OvfEnv().OpensslToSsh(fprint) + if SshPubKey != None: + AppendFileContents(path, SshPubKey) + else: + Error("Failed: " + fprint + ".crt -> " + path) + error = 1 + return error + + def checkPackageInstalled(self,p): + """ + Query package database for prescence of an installed package. + Abstract Virtual Function. Over-ridden in concrete Distro classes. + """ + pass + def checkPackageUpdateable(self,p): + """ + Online check if updated package of walinuxagent is available. + Abstract Virtual Function. Over-ridden in concrete Distro classes. + """ + pass + def deleteRootPassword(self): + """ + Generic root password removal. + """ + filepath="/etc/shadow" + ReplaceFileContentsAtomic(filepath,"root:*LOCK*:14600::::::\n" + + "\n".join(filter(lambda a: not a.startswith("root:"),GetFileContents(filepath).split('\n')))) + os.chmod(filepath,self.shadow_file_mode) + if self.isSelinuxSystem(): + self.setSelinuxContext(filepath,'system_u:object_r:shadow_t:s0') + Log("Root password deleted.") + return 0 + + def changePass(self,user,password): + return RunSendStdin("chpasswd",(user + ":" + password + "\n")) + + def load_ata_piix(self): + return WaAgent.TryLoadAtapiix() + + def unload_ata_piix(self): + """ + Generic function to remove ata_piix.ko. + """ + return WaAgent.TryUnloadAtapiix() + + def deprovisionWarnUser(self): + """ + Generic user warnings used at deprovision. + """ + print("WARNING! Nameserver configuration in /etc/resolv.conf will be deleted.") + + def deprovisionDeleteFiles(self): + """ + Files to delete when VM is deprovisioned + """ + for a in VarLibDhcpDirectories: + Run("rm -f " + a + "/*") + + # Clear LibDir, remove nameserver and root bash history + + for f in os.listdir(LibDir) + self.fileBlackList: + try: + os.remove(f) + except: + pass + return 0 + + def uninstallDeleteFiles(self): + """ + Files to delete when agent is uninstalled. + """ + for f in self.agent_files_to_uninstall: + try: + os.remove(f) + except: + pass + return 0 + + def checkDependencies(self): + """ + Generic dependency check. + Return 1 unless all dependencies are satisfied. + """ + if self.checkPackageInstalled('NetworkManager'): + Error(GuestAgentLongName + " is not compatible with network-manager.") + return 1 + try: + m= __import__('pyasn1') + except ImportError: + Error(GuestAgentLongName + " requires python-pyasn1 for your Linux distribution.") + return 1 + for a in self.requiredDeps: + if Run("which " + a + " > /dev/null 2>&1",chk_err=False): + Error("Missing required dependency: " + a) + return 1 + return 0 + + def packagedInstall(self,buildroot): + """ + Called from setup.py for use by RPM. + Copies generated files waagent.conf, under the buildroot. + """ + if not os.path.exists(buildroot+'/etc'): + os.mkdir(buildroot+'/etc') + SetFileContents(buildroot+'/etc/waagent.conf', MyDistro.waagent_conf_file) + + if not os.path.exists(buildroot+'/etc/logrotate.d'): + os.mkdir(buildroot+'/etc/logrotate.d') + SetFileContents(buildroot+'/etc/logrotate.d/waagent', WaagentLogrotate) + + self.init_script_file=buildroot+self.init_script_file + # this allows us to call installAgentServiceScriptFiles() + if not os.path.exists(os.path.dirname(self.init_script_file)): + os.mkdir(os.path.dirname(self.init_script_file)) + self.installAgentServiceScriptFiles() + + def GetIpv4Address(self): + """ + Return the ip of the + first active non-loopback interface. + """ + iface,addr=GetFirstActiveNetworkInterfaceNonLoopback() + return addr + + def GetMacAddress(self): + return GetMacAddress() + + def GetInterfaceName(self): + return GetFirstActiveNetworkInterfaceNonLoopback()[0] + + def CreateAccount(self,user, password, expiration, thumbprint): + return CreateAccount(user, password, expiration, thumbprint) + + def DeleteAccount(self,user): + return DeleteAccount(user) + + def ActivateResourceDisk(self): + """ + Format, mount, and if specified in the configuration + set resource disk as swap. + """ + global DiskActivated + format = Config.get("ResourceDisk.Format") + if format == None or format.lower().startswith("n"): + DiskActivated = True + return + device = DeviceForIdePort(1) + if device == None: + Error("ActivateResourceDisk: Unable to detect disk topology.") + return + device = "/dev/" + device + for entry in RunGetOutput("mount")[1].split(): + if entry.startswith(device + "1"): + Log("ActivateResourceDisk: " + device + "1 is already mounted.") + DiskActivated = True + return + mountpoint = Config.get("ResourceDisk.MountPoint") + if mountpoint == None: + mountpoint = "/mnt/resource" + CreateDir(mountpoint, "root", 0755) + fs = Config.get("ResourceDisk.Filesystem") + if fs == None: + fs = "ext3" + if RunGetOutput("sfdisk -q -c " + device + " 1")[1].rstrip() == "7" and fs != "ntfs": + Run("sfdisk -c " + device + " 1 83") + Run("mkfs." + fs + " " + device + "1") + if Run("mount " + device + "1 " + mountpoint): + Error("ActivateResourceDisk: Failed to mount resource disk (" + device + "1).") + return + Log("Resource disk (" + device + "1) is mounted at " + mountpoint + " with fstype " + fs) + DiskActivated = True + swap = Config.get("ResourceDisk.EnableSwap") + if swap == None or swap.lower().startswith("n"): + return + sizeKB = int(Config.get("ResourceDisk.SwapSizeMB")) * 1024 + if os.path.isfile(mountpoint + "/swapfile") and os.path.getsize(mountpoint + "/swapfile") != (sizeKB * 1024): + os.remove(mountpoint + "/swapfile") + if not os.path.isfile(mountpoint + "/swapfile"): + Run("dd if=/dev/zero of=" + mountpoint + "/swapfile bs=1024 count=" + str(sizeKB)) + Run("mkswap " + mountpoint + "/swapfile") + if not Run("swapon " + mountpoint + "/swapfile"): + Log("Enabled " + str(sizeKB) + " KB of swap at " + mountpoint + "/swapfile") + else: + Error("ActivateResourceDisk: Failed to activate swap at " + mountpoint + "/swapfile") + + def Install(self): + return Install() + + def dvdHasMedia(self,dvd): + if Run("LC_ALL=C fdisk -l " + dvd + " | grep Disk"): + return False return True - if os.path.isfile("/etc/debian_version"): - LinuxDistro = "Debian" - return True - if os.path.isfile("/etc/SuSE-release"): - LinuxDistro = "Suse" - return True - return False + + def mountDVD(self,dvd,location): + return RunGetOutput(self.mount_dvd_cmd + ' ' + dvd + ' ' + location) + + def GetHome(self): + return GetHome() + + def getDhcpClientName(self): + return self.dhcp_client_name + +############################################################ +# SuSEDistro +############################################################ +suse_init_file = """\ +#! /bin/sh +# +# Windows Azure Linux Agent sysV init script +# +# Copyright 2013 Microsoft Corporation +# Copyright SUSE LLC +# +# 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. +# +# /etc/init.d/waagent +# +# and symbolic link +# +# /usr/sbin/rcwaagent +# +# System startup script for the waagent +# +### BEGIN INIT INFO +# Provides: WindowsAzureLinuxAgent +# Required-Start: $network sshd +# Required-Stop: $network sshd +# Default-Start: 3 5 +# Default-Stop: 0 1 2 6 +# Description: Start the WindowsAzureLinuxAgent +### END INIT INFO + +PYTHON=/usr/bin/python +WAZD_BIN=/usr/sbin/waagent +WAZD_CONF=/etc/waagent.conf +WAZD_PIDFILE=/var/run/waagent.pid + +test -x "$WAZD_BIN" || { echo "$WAZD_BIN not installed"; exit 5; } +test -e "$WAZD_CONF" || { echo "$WAZD_CONF not found"; exit 6; } + +. /etc/rc.status + +# First reset status of this service +rc_reset + +# Return values acc. to LSB for all commands but status: +# 0 - success +# 1 - misc error +# 2 - invalid or excess args +# 3 - unimplemented feature (e.g. reload) +# 4 - insufficient privilege +# 5 - program not installed +# 6 - program not configured +# +# Note that starting an already running service, stopping +# or restarting a not-running service as well as the restart +# with force-reload (in case signalling is not supported) are +# considered a success. + + +case "$1" in + start) + echo -n "Starting WindowsAzureLinuxAgent" + ## Start daemon with startproc(8). If this fails + ## the echo return value is set appropriate. + startproc -f ${PYTHON} ${WAZD_BIN} -daemon + rc_status -v + ;; + stop) + echo -n "Shutting down WindowsAzureLinuxAgent" + ## Stop daemon with killproc(8) and if this fails + ## set echo the echo return value. + killproc -p ${WAZD_PIDFILE} ${PYTHON} ${WAZD_BIN} + rc_status -v + ;; + try-restart) + ## Stop the service and if this succeeds (i.e. the + ## service was running before), start it again. + $0 status >/dev/null && $0 restart + rc_status + ;; + restart) + ## Stop the service and regardless of whether it was + ## running or not, start it again. + $0 stop + sleep 1 + $0 start + rc_status + ;; + force-reload|reload) + rc_status + ;; + status) + echo -n "Checking for service WindowsAzureLinuxAgent " + ## Check status with checkproc(8), if process is running + ## checkproc will return with exit status 0. -def IsRedHat(): - return "RedHat" in LinuxDistro + checkproc -p ${WAZD_PIDFILE} ${PYTHON} ${WAZD_BIN} + rc_status -v + ;; + probe) + ;; + *) + echo "Usage: $0 {start|stop|status|try-restart|restart|force-reload|reload}" + exit 1 + ;; +esac +rc_exit +""" +class SuSEDistro(AbstractDistro): + """ + SuSE Distro concrete class + Put SuSE specific behavior here... + """ + def __init__(self): + super(SuSEDistro,self).__init__() + self.service_cmd='/sbin/service' + self.ssh_service_name='sshd' + self.kernel_boot_options_file='/boot/grub/menu.lst' + self.hostname_file_path='/etc/HOSTNAME' + self.requiredDeps += [ "/sbin/insserv" ] + self.init_file=suse_init_file + self.dhcp_client_name='dhcpcd' + self.grubKernelBootOptionsFile = '/boot/grub/menu.lst' + self.grubKernelBootOptionsLine = 'kernel' + self.getpidcmd='pidof ' + + def checkPackageInstalled(self,p): + if Run("rpm -q " + p,chk_err=False): + return 0 + else: + return 1 -def IsUbuntu(): - return "Ubuntu" in LinuxDistro + def checkPackageUpdateable(self,p): + if Run("zypper list-updates | grep " + p,chk_err=False): + return 1 + else: + return 0 + -def IsPackagedUbuntu(): - return "Ubuntu" in PackagedForDistro + def installAgentServiceScriptFiles(self): + try: + SetFileContents(self.init_script_file, self.init_file) + os.chmod(self.init_script_file, 0744) + except: + pass + + def registerAgentService(self): + self.installAgentServiceScriptFiles() + return Run('insserv ' + self.agent_service_name) + + def uninstallAgentService(self): + return Run('insserv -r ' + self.agent_service_name) + + def unregisterAgentService(self): + self.stopAgentService() + return self.uninstallAgentService() + +############################################################ +# redhatDistro +############################################################ -def IsDebian(): - return IsUbuntu() or "Debian" in LinuxDistro +redhat_init_file= """\ +#!/bin/bash +# +# Init file for WindowsAzureLinuxAgent. +# +# chkconfig: 2345 60 80 +# description: WindowsAzureLinuxAgent +# -def IsSuse(): - return "Suse" in LinuxDistro +# source function library +. /etc/rc.d/init.d/functions -def IsPackaged(): - if PackagedForDistro == "UNKNOWN": - return False +RETVAL=0 +FriendlyName="WindowsAzureLinuxAgent" +WAZD_BIN=/usr/sbin/waagent - return True +start() +{ + echo -n $"Starting $FriendlyName: " + $WAZD_BIN -daemon & +} -def UsesRpm(): - return IsRedHat() or IsSuse() +stop() +{ + echo -n $"Stopping $FriendlyName: " + killproc -p /var/run/waagent.pid $WAZD_BIN + RETVAL=$? + echo + return $RETVAL +} -def UsesDpkg(): - return IsDebian() +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + reload) + ;; + report) + ;; + status) + status $WAZD_BIN + RETVAL=$? + ;; + *) + echo $"Usage: $0 {start|stop|restart|status}" + RETVAL=1 +esac +exit $RETVAL +""" + +class redhatDistro(AbstractDistro): + """ + Redhat Distro concrete class + Put Redhat specific behavior here... + """ + def __init__(self): + super(redhatDistro,self).__init__() + self.service_cmd='/sbin/service' + self.ssh_service_name='sshd' + self.hostname_file_path=None + self.init_file=redhat_init_file + self.grubKernelBootOptionsFile = '/boot/grub/menu.lst' + self.grubKernelBootOptionsLine = 'kernel' + + def publishHostname(self,name): + super(redhatDistro,self).publishHostname(name) + filepath = "/etc/sysconfig/network" + if os.path.isfile(filepath): + ReplaceFileContentsAtomic(filepath, "HOSTNAME=" + name + "\n" + + "\n".join(filter(lambda a: not a.startswith("HOSTNAME"), GetFileContents(filepath).split('\n')))) + + ethernetInterface = MyDistro.GetInterfaceName() + filepath = "/etc/sysconfig/network-scripts/ifcfg-" + ethernetInterface + if os.path.isfile(filepath): + ReplaceFileContentsAtomic(filepath, "DHCP_HOSTNAME=" + name + "\n" + + "\n".join(filter(lambda a: not a.startswith("DHCP_HOSTNAME"), GetFileContents(filepath).split('\n')))) + return 0 + + def installAgentServiceScriptFiles(self): + SetFileContents(self.init_script_file, self.init_file) + os.chmod(self.init_script_file, 0744) + return 0 + + def registerAgentService(self): + self.installAgentServiceScriptFiles() + return Run('chkconfig --add waagent') + + def uninstallAgentService(self): + return Run('chkconfig --del ' + self.agent_service_name) + + def unregisterAgentService(self): + self.stopAgentService() + return self.uninstallAgentService() + + def checkPackageInstalled(self,p): + if Run("yum list installed " + p,chk_err=False): + return 0 + else: + return 1 + + def checkPackageUpdateable(self,p): + if Run("yum check-update | grep "+ p,chk_err=False): + return 1 + else: + return 0 + + + +############################################################ +# centosDistro +############################################################ + +centos_init_file= """\ +#!/bin/bash +# +# Init file for WindowsAzureLinuxAgent. +# +# chkconfig: 2345 60 80 +# description: WindowsAzureLinuxAgent +# + +# source function library +. /etc/rc.d/init.d/functions + +RETVAL=0 +FriendlyName="WindowsAzureLinuxAgent" +WAZD_BIN=/usr/sbin/waagent + +start() +{ + echo -n $"Starting $FriendlyName: " + $WAZD_BIN -daemon & +} + +stop() +{ + echo -n $"Stopping $FriendlyName: " + killproc -p /var/run/waagent.pid $WAZD_BIN + RETVAL=$? + echo + return $RETVAL +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + reload) + ;; + report) + ;; + status) + status $WAZD_BIN + RETVAL=$? + ;; + *) + echo $"Usage: $0 {start|stop|restart|status}" + RETVAL=1 +esac +exit $RETVAL +""" + +class centosDistro(AbstractDistro): + """ + CentOS Distro concrete class + Put CentOS specific behavior here... + """ + def __init__(self): + super(centosDistro,self).__init__() + self.service_cmd='/sbin/service' + self.ssh_service_name='sshd' + self.hostname_file_path=None + self.init_file=centos_init_file + self.grubKernelBootOptionsFile = '/boot/grub/menu.lst' + self.grubKernelBootOptionsLine = 'kernel' + + def publishHostname(self,name): + super(centosDistro,self).publishHostname(name) + filepath = "/etc/sysconfig/network" + if os.path.isfile(filepath): + ReplaceFileContentsAtomic(filepath, "HOSTNAME=" + name + "\n" + + "\n".join(filter(lambda a: not a.startswith("HOSTNAME"), GetFileContents(filepath).split('\n')))) + ethernetInterface = MyDistro.GetInterfaceName() + filepath = "/etc/sysconfig/network-scripts/ifcfg-" + ethernetInterface + if os.path.isfile(filepath): + ReplaceFileContentsAtomic(filepath, "DHCP_HOSTNAME=" + name + "\n" + + "\n".join(filter(lambda a: not a.startswith("DHCP_HOSTNAME"), GetFileContents(filepath).split('\n')))) + return 0 + + def installAgentServiceScriptFiles(self): + SetFileContents(self.init_script_file, self.init_file) + os.chmod(self.init_script_file, 0744) + return 0 + + def registerAgentService(self): + self.installAgentServiceScriptFiles() + return Run('chkconfig --add waagent') + + def uninstallAgentService(self): + return Run('chkconfig --del ' + self.agent_service_name) + + def unregisterAgentService(self): + self.stopAgentService() + return self.uninstallAgentService() + + def checkPackageInstalled(self,p): + if Run("yum list installed " + p,chk_err=False): + return 0 + else: + return 1 + + def checkPackageUpdateable(self,p): + if Run("yum check-update | grep "+ p,chk_err=False): + return 1 + else: + return 0 + +############################################################ +# debianDistro +############################################################ +debian_init_file = """\ +#!/bin/sh +### BEGIN INIT INFO +# Provides: WindowsAzureLinuxAgent +# Required-Start: $network $syslog +# Required-Stop: $network $syslog +# Should-Start: $network $syslog +# Should-Stop: $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: WindowsAzureLinuxAgent +# Description: WindowsAzureLinuxAgent +### END INIT INFO + +. /lib/lsb/init-functions + +OPTIONS="-daemon" +WAZD_BIN=/usr/sbin/waagent +WAZD_PID=/var/run/waagent.pid + +case "$1" in + start) + log_begin_msg "Starting WindowsAzureLinuxAgent..." + pid=$( pidofproc $WAZD_BIN ) + if [ -n "$pid" ] ; then + log_begin_msg "Already running." + log_end_msg 0 + exit 0 + fi + start-stop-daemon --start --quiet --oknodo --background --exec $WAZD_BIN -- $OPTIONS + log_end_msg $? + ;; + + stop) + log_begin_msg "Stopping WindowsAzureLinuxAgent..." + start-stop-daemon --stop --quiet --oknodo --pidfile $WAZD_PID + ret=$? + rm -f $WAZD_PID + log_end_msg $ret + ;; + force-reload) + $0 restart + ;; + restart) + $0 stop + $0 start + ;; + status) + status_of_proc $WAZD_BIN && exit 0 || exit $? + ;; + *) + log_success_msg "Usage: /etc/init.d/waagent {start|stop|force-reload|restart|status}" + exit 1 + ;; +esac + +exit 0 +""" + +class debianDistro(AbstractDistro): + """ + debian Distro concrete class + Put debian specific behavior here... + """ + def __init__(self): + super(debianDistro,self).__init__() + self.requiredDeps += [ "/usr/sbin/update-rc.d" ] + self.init_file=debian_init_file + self.agent_package_name='walinuxagent' + self.dhcp_client_name='dhclient' + self.getpidcmd='pidof ' + self.shadow_file_mode=0640 + + def checkPackageInstalled(self,p): + """ + Check that the package is installed. + Return 1 if installed, 0 if not installed. + This method of using dpkg-query + allows wildcards to be present in the + package name. + """ + if not Run("dpkg-query -W -f='${Status}\n' '" + p + "' | grep ' installed' 2>&1",chk_err=False): + return 1 + else: + return 0 + + def checkDependencies(self): + """ + Debian dependency check. python-pyasn1 is NOT needed. + Return 1 unless all dependencies are satisfied. + NOTE: using network*manager will catch either package name in Ubuntu or debian. + """ + if self.checkPackageInstalled('network*manager'): + Error(GuestAgentLongName + " is not compatible with network-manager.") + return 1 + for a in self.requiredDeps: + if Run("which " + a + " > /dev/null 2>&1",chk_err=False): + Error("Missing required dependency: " + a) + return 1 + return 0 + + def checkPackageUpdateable(self,p): + if Run("apt-get update ; apt-get upgrade -us | grep " + p,chk_err=False): + return 1 + else: + return 0 + + def installAgentServiceScriptFiles(self): + """ + If we are packaged - the service name is walinuxagent, do nothing. + """ + if self.agent_service_name == 'walinuxagent': + return 0 + try: + SetFileContents(self.init_script_file, self.init_file) + os.chmod(self.init_script_file, 0744) + except OSError, e: + ErrorWithPrefix('installAgentServiceScriptFiles','Exception: '+str(e)+' occured creating ' + self.init_script_file) + return 1 + return 0 + + def registerAgentService(self): + if self.installAgentServiceScriptFiles() == 0: + return Run('update-rc.d waagent defaults') + else : + return 1 + + def uninstallAgentService(self): + return Run('update-rc.d -f ' + self.agent_service_name + ' remove') + + def unregisterAgentService(self): + self.stopAgentService() + return self.uninstallAgentService() + + def sshDeployPublicKey(self,fprint,path): + """ + We support PKCS8. + """ + if Run("ssh-keygen -i -m PKCS8 -f " + fprint + " >> " + path): + return 1 + else : + return 0 + +############################################################ +# UbuntuDistro +############################################################ +ubuntu_upstart_file = """\ +#walinuxagent - start Windows Azure agent + +description "walinuxagent" +author "Ben Howard <ben.howard@canonical.com>" + +start on (filesystem and started rsyslog) + +pre-start script + + WALINUXAGENT_ENABLED=1 + [ -r /etc/default/walinuxagent ] && . /etc/default/walinuxagent + + if [ "$WALINUXAGENT_ENABLED" != "1" ]; then + exit 1 + fi + + if [ ! -x /usr/sbin/waagent ]; then + exit 1 + fi + + #Load the udf module + modprobe -b udf +end script + +exec /usr/sbin/waagent -daemon +""" + +class UbuntuDistro(debianDistro): + """ + Ubuntu Distro concrete class + Put Ubuntu specific behavior here... + """ + def __init__(self): + super(UbuntuDistro,self).__init__() + self.init_script_file='/etc/init/waagent.conf' + self.init_file=ubuntu_upstart_file + self.fileBlackList = [ "/root/.bash_history", "/var/log/waagent.log"] + self.dhcp_client_name=None + self.getpidcmd='pidof ' + + def registerAgentService(self): + return self.installAgentServiceScriptFiles() + + def startAgentService(self): + """ + Use upstart syntax. + """ + return Run('start ' + self.agent_service_name) + + def stopAgentService(self): + """ + Use upstart syntax. + """ + return Run('stop ' + self.agent_service_name) + + def uninstallAgentService(self): + """ + If we are packaged - the service name is walinuxagent, do nothing. + """ + if self.agent_service_name == 'walinuxagent': + return 0 + os.remove('/etc/init/' + self.agent_service_name + '.conf') + + def unregisterAgentService(self): + """ + If we are packaged - the service name is walinuxagent, do nothing. + """ + if self.agent_service_name == 'walinuxagent': + return + self.stopAgentService() + return self.uninstallAgentService() + + def deprovisionWarnUser(self): + """ + Ubuntu specific warning string from Deprovision. + """ + print("WARNING! Nameserver configuration in /etc/resolvconf/resolv.conf.d/{tail,originial} will be deleted.") + + def deprovisionDeleteFiles(self): + """ + Ubuntu uses resolv.conf by default, so removing /etc/resolv.conf will + break resolvconf. Therefore, we check to see if resolvconf is in use, + and if so, we remove the resolvconf artifacts. + """ + if os.path.realpath('/etc/resolv.conf') != '/run/resolvconf/resolv.conf': + Log("resolvconf is not configured. Removing /etc/resolv.conf") + self.fileBlackList.append('/etc/resolv.conf') + else: + Log("resolvconf is enabled; leaving /etc/resolv.conf intact") + resolvConfD = '/etc/resolvconf/resolv.conf.d/' + self.fileBlackList.extend([resolvConfD + 'tail', resolvConfD + 'originial']) + for f in os.listdir(LibDir)+self.fileBlackList: + try: + os.remove(f) + except: + pass + return 0 + + def getDhcpClientName(self): + if self.dhcp_client_name != None : + return self.dhcp_client_name + if platform.dist()[1] == '12.04' : + self.dhcp_client_name='dhclient3' + else : + self.dhcp_client_name='dhclient' + return self.dhcp_client_name + +############################################################ +# LinuxMintDistro +############################################################ + +class LinuxMintDistro(UbuntuDistro): + """ + LinuxMint Distro concrete class + Put LinuxMint specific behavior here... + """ + def __init__(self): + super(LinuxMintDistro,self).__init__() + +############################################################ +# FreeBSD +############################################################ +FreeBSDWaagentConf = """\ +# +# Windows Azure Linux Agent Configuration +# + +Role.StateConsumer=None # Specified program is invoked with the argument "Ready" when we report ready status + # to the endpoint server. +Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role configuration. +Role.TopologyConsumer=None # Specified program is invoked with XML file argument specifying role topology. + +Provisioning.Enabled=y # +Provisioning.DeleteRootPassword=y # Password authentication for root account will be unavailable. +Provisioning.RegenerateSshHostKeyPair=y # Generate fresh host key pair. +Provisioning.SshHostKeyPairType=rsa # Supported values are "rsa", "dsa" and "ecdsa". +Provisioning.MonitorHostName=y # Monitor host name changes and publish changes via DHCP requests. + +ResourceDisk.Format=y # Format if unformatted. If 'n', resource disk will not be mounted. +ResourceDisk.Filesystem=ufs2 # +ResourceDisk.MountPoint=/mnt/resource # +ResourceDisk.EnableSwap=n # Create and use swapfile on resource disk. +ResourceDisk.SwapSizeMB=0 # Size of the swapfile. + +LBProbeResponder=y # Respond to load balancer probes if requested by Windows Azure. + +Logs.Verbose=n # + +OS.RootDeviceScsiTimeout=300 # Root device timeout in seconds. +OS.OpensslPath=None # If "None", the system default version is used. +""" + +bsd_init_file="""\ +#! /bin/sh + +# PROVIDE: waagent +# REQUIRE: DAEMON cleanvar sshd +# BEFORE: LOGIN +# KEYWORD: nojail + +. /etc/rc.subr + +name="waagent" +rcvar="waagent_enable" +command="/usr/sbin/${name}" +command_interpreter="/usr/local/bin/python" +waagent_flags=" daemon &" + +pidfile="/var/run/waagent.pid" + +load_rc_config $name +run_rc_command "$1" + +""" + +class FreeBSDDistro(AbstractDistro): + """ + """ + def __init__(self): + """ + Generic Attributes go here. These are based on 'majority rules'. + This __init__() may be called or overriden by the child. + """ + self.agent_service_name = os.path.basename(sys.argv[0]) + self.selinux=False + self.ssh_service_name='sshd' + self.ssh_config_file='/etc/ssh/sshd_config' + self.hostname_file_path='/etc/hostname' + self.dhcp_client_name='dhclient' + self.requiredDeps = [ 'route', 'shutdown', 'ssh-keygen', 'pw' + , 'openssl', 'fdisk', 'sed', 'grep' , 'sudo'] + self.init_script_file='/etc/rc.d/waagent' + self.init_file=bsd_init_file + self.agent_package_name='WALinuxAgent' + self.fileBlackList = [ "/root/.bash_history", "/var/log/waagent.log",'/etc/resolv.conf' ] + self.agent_files_to_uninstall = ["/etc/waagent.conf", "/usr/local/etc/sudoers.d/waagent"] + self.grubKernelBootOptionsFile = '/boot/loader.conf' + self.grubKernelBootOptionsLine = '' + self.getpidcmd = 'pgrep -n' + self.mount_dvd_cmd = 'dd bs=2048 count=1 skip=295 if=' + self.sudoers_dir_base = '/usr/local/etc' + self.waagent_conf_file = FreeBSDWaagentConf + + def installAgentServiceScriptFiles(self): + SetFileContents(self.init_script_file, self.init_file) + os.chmod(self.init_script_file, 0777) + AppendFileContents("/etc/rc.conf","waagent_enable='YES'\n") + return 0 + + def registerAgentService(self): + self.installAgentServiceScriptFiles() + return Run("services_mkdb " + self.init_script_file) + +# def uninstallAgentService(self): +# return Run('chkconfig --del ' + self.agent_service_name) + +# def unregisterAgentService(self): +# self.stopAgentService() +# return self.uninstallAgentService() + + def restartSshService(self): + """ + Service call to re(start) the SSH service + """ + return Run(self.service_cmd + " " + self.ssh_service_name + " restart") + + def sshDeployPublicKey(self,fprint,path): + """ + We support PKCS8. + """ + if Run("ssh-keygen -i -m PKCS8 -f " + fprint + " >> " + path): + return 1 + else : + return 0 + + def deleteRootPassword(self): + """ + BSD root password removal. + """ + filepath="/etc/master.passwd" + ReplaceStringInFile(filepath,r'root:.*?:','root::') + #ReplaceFileContentsAtomic(filepath,"root:*LOCK*:14600::::::\n" + # + "\n".join(filter(lambda a: not a.startswith("root:"),GetFileContents(filepath).split('\n')))) + os.chmod(filepath,self.shadow_file_mode) + if self.isSelinuxSystem(): + self.setSelinuxContext(filepath,'system_u:object_r:shadow_t:s0') + RunGetOutput("pwd_mkdb -u root /etc/master.passwd") + Log("Root password deleted.") + return 0 + + def changePass(self,user,password): + return RunSendStdin("pw usermod " + user + " -h 0 ",password) + + def load_ata_piix(self): + return 0 + + def unload_ata_piix(self): + return 0 + + def checkDependencies(self): + """ + FreeBSD dependency check. + Return 1 unless all dependencies are satisfied. + """ + for a in self.requiredDeps: + if Run("which " + a + " > /dev/null 2>&1",chk_err=False): + Error("Missing required dependency: " + a) + return 1 + return 0 + + def packagedInstall(self,buildroot): + pass + + def GetInterfaceName(self): + """ + Return the ip of the + active ethernet interface. + """ + iface,inet,mac=self.GetFreeBSDEthernetInfo() + return iface + + def GetIpv4Address(self): + """ + Return the ip of the + active ethernet interface. + """ + iface,inet,mac=self.GetFreeBSDEthernetInfo() + return inet + + def GetMacAddress(self): + """ + Return the ip of the + active ethernet interface. + """ + iface,inet,mac=self.GetFreeBSDEthernetInfo() + l=mac.split(':') + r=[] + for i in l: + r.append(string.atoi(i,16)) + return r + + def GetFreeBSDEthernetInfo(self): + """ + There is no SIOCGIFCONF + on freeBSD - just parse ifconfig. + Returns strings: iface, inet4_addr, and mac + or 'None,None,None' if unable to parse. + We will sleep and retry as the network must be up. + """ + code,output=RunGetOutput("ifconfig",chk_err=False) + Log(output) + retries=10 + cmd='ifconfig | grep -A1 -B2 ether | grep -B3 inet | grep -A3 UP ' + code=1 + + while code > 0 : + if code > 0 and retries == 0: + Error("GetFreeBSDEthernetInfo - Failed to detect ethernet interface") + return None, None, None + code,output=RunGetOutput(cmd,chk_err=False) + retries-=1 + if code > 0 and retries > 0 : + Log("GetFreeBSDEthernetInfo - Error: retry ethernet detection " + str(retries)) + time.sleep(10) + + j=output.replace('\n',' ') + j=j.split() + iface=j[0][:-1] + + for i in range(len(j)): + if j[i] == 'inet' : + inet=j[i+1] + elif j[i] == 'ether' : + mac=j[i+1] + + return iface, inet, mac + + def CreateAccount(self,user, password, expiration, thumbprint): + """ + Create a user account, with 'user', 'password', 'expiration', ssh keys + and sudo permissions. + Returns None if successful, error string on failure. + """ + userentry = None + try: + userentry = pwd.getpwnam(user) + except: + pass + uidmin = None + try: + uidmin = int(GetLineStartingWith("UID_MIN", "/etc/login.defs").split()[1]) + except: + pass + if uidmin == None: + uidmin = 100 + if userentry != None and userentry[2] < uidmin: + Error("CreateAccount: " + user + " is a system user. Will not set password.") + return "Failed to set password for system user: " + user + " (0x06)." + if userentry == None: + command = "pw useradd " + user + " -m" + if expiration != None: + command += " -e " + expiration.split('.')[0] + if Run(command): + Error("Failed to create user account: " + user) + return "Failed to create user account: " + user + " (0x07)." + else: + Log("CreateAccount: " + user + " already exists. Will update password.") + + if password != None: + self.changePass(user,password) + try: + # for older distros create sudoers.d + if not os.path.isdir(MyDistro.sudoers_dir_base+'/sudoers.d/'): + # create the /etc/sudoers.d/ directory + os.mkdir(MyDistro.sudoers_dir_base+'/sudoers.d') + # add the include of sudoers.d to the /etc/sudoers + SetFileContents(MyDistro.sudoers_dir_base+'/sudoers',GetFileContents(MyDistro.sudoers_dir_base+'/sudoers')+'\n#includedir ' + MyDistro.sudoers_dir_base + '/sudoers.d\n') + if password == None: + SetFileContents(MyDistro.sudoers_dir_base+"/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") + else: + SetFileContents(MyDistro.sudoers_dir_base+"/sudoers.d/waagent", user + " ALL = (ALL) ALL\n") + os.chmod(MyDistro.sudoers_dir_base+"/sudoers.d/waagent", 0440) + except: + Error("CreateAccount: Failed to configure sudo access for user.") + return "Failed to configure sudo privileges (0x08)." + home = MyDistro.GetHome() + if thumbprint != None: + dir = home + "/" + user + "/.ssh" + CreateDir(dir, user, 0700) + pub = dir + "/id_rsa.pub" + prv = dir + "/id_rsa" + Run("ssh-keygen -y -f " + thumbprint + ".prv > " + pub) + SetFileContents(prv, GetFileContents(thumbprint + ".prv")) + for f in [pub, prv]: + os.chmod(f, 0600) + ChangeOwner(f, user) + SetFileContents(dir + "/authorized_keys", GetFileContents(pub)) + ChangeOwner(dir + "/authorized_keys", user) + Log("Created user account: " + user) + return None + + def DeleteAccount(self,user): + """ + Delete the 'user'. + Clear utmp first, to avoid error. + Removes the /etc/sudoers.d/waagent file. + """ + userentry = None + try: + userentry = pwd.getpwnam(user) + except: + pass + if userentry == None: + Error("DeleteAccount: " + user + " not found.") + return + uidmin = None + try: + uidmin = int(GetLineStartingWith("UID_MIN", "/etc/login.defs").split()[1]) + except: + pass + if uidmin == None: + uidmin = 100 + if userentry[2] < uidmin: + Error("DeleteAccount: " + user + " is a system user. Will not delete account.") + return + Run("> /var/run/utmp") #Delete utmp to prevent error if we are the 'user' deleted + Run("rmuser -y " + user) + try: + os.remove(MyDistro.sudoers_dir_base+"/sudoers.d/waagent") + except: + pass + return + + def ActivateResourceDisk(self): + """ + Format, mount, and if specified in the configuration + set resource disk as swap. + """ + global DiskActivated + format = Config.get("ResourceDisk.Format") + if format == None or format.lower().startswith("n"): + DiskActivated = True + return + #device = DeviceForIdePort(1) + device_base = 'ada1' +# if device == None: +# Error("ActivateResourceDisk: Unable to detect disk topology.") +# return + device = "/dev/" + device_base + for entry in RunGetOutput("mount")[1].split(): + if entry.startswith(device + "s1"): + Log("ActivateResourceDisk: " + device + "s1 is already mounted.") + DiskActivated = True + return + mountpoint = Config.get("ResourceDisk.MountPoint") + if mountpoint == None: + mountpoint = "/mnt/resource" + CreateDir(mountpoint, "root", 0755) + fs = Config.get("ResourceDisk.Filesystem") + Run("newfs " + device + "s1") + if Run("mount " + device + "s1 " + mountpoint): + Error("ActivateResourceDisk: Failed to mount resource disk (" + device + "1).") + return + Log("Resource disk (" + device + "1) is mounted at " + mountpoint + " with fstype " + fs) + DiskActivated = True + swap = Config.get("ResourceDisk.EnableSwap") + if swap == None or swap.lower().startswith("n"): + return + sizeKB = int(Config.get("ResourceDisk.SwapSizeMB")) * 1024 + if os.path.isfile(mountpoint + "/swapfile") and os.path.getsize(mountpoint + "/swapfile") != (sizeKB * 1024): + os.remove(mountpoint + "/swapfile") + if not os.path.isfile(mountpoint + "/swapfile"): + Run("dd if=/dev/zero of=" + mountpoint + "/swapfile bs=1024 count=" + str(sizeKB)) + if Run("mdconfig -a -t vnode -f " + mountpoint + "/swapfile -u 0"): + Error("ActivateResourceDisk: Configuring swap - Failed to create md0") + if not Run("swapon /dev/md0"): + Log("Enabled " + str(sizeKB) + " KB of swap at " + mountpoint + "/swapfile") + else: + Error("ActivateResourceDisk: Failed to activate swap at " + mountpoint + "/swapfile") + + def Install(self): + """ + Install the agent service. + Check dependencies. + Create /etc/waagent.conf and move old version to + /etc/waagent.conf.old + Copy RulesFiles to /var/lib/waagent + Create /etc/logrotate.d/waagent + Set /etc/ssh/sshd_config ClientAliveInterval to 180 + Call ApplyVNUMAWorkaround() + """ + if MyDistro.checkDependencies(): + return 1 + os.chmod(sys.argv[0], 0755) + SwitchCwd() + for a in RulesFiles: + if os.path.isfile(a): + if os.path.isfile(GetLastPathElement(a)): + os.remove(GetLastPathElement(a)) + shutil.move(a, ".") + Warn("Moved " + a + " -> " + LibDir + "/" + GetLastPathElement(a) ) + MyDistro.registerAgentService() + if os.path.isfile("/etc/waagent.conf"): + try: + os.remove("/etc/waagent.conf.old") + except: + pass + try: + os.rename("/etc/waagent.conf", "/etc/waagent.conf.old") + Warn("Existing /etc/waagent.conf has been renamed to /etc/waagent.conf.old") + except: + pass + SetFileContents("/etc/waagent.conf", self.waagent_conf_file) + if os.path.exists('/usr/local/etc/logrotate.d/'): + SetFileContents("/usr/local/etc/logrotate.d/waagent", WaagentLogrotate) + filepath = "/etc/ssh/sshd_config" + ReplaceFileContentsAtomic(filepath, "\n".join(filter(lambda a: not + a.startswith("ClientAliveInterval"), + GetFileContents(filepath).split('\n'))) + "\nClientAliveInterval 180\n") + Log("Configured SSH client probing to keep connections alive.") + #ApplyVNUMAWorkaround() + return 0 + + def dvdHasMedia(self,dvd): + if Run('LC_ALL=C fdisk -p ' + dvd + ' | grep "invalid fdisk partition table found" '): + return False + return True + + def mountDVD(self,dvd,location): + #At this point we cannot read a joliet option udf DVD in freebsd10 - so we 'dd' it into our location + return RunGetOutput(self.mount_dvd_cmd + dvd + ' of=' + location + '/ovf-env.xml') + + def GetHome(self): + return '/home' + +############################################################ +# END DISTRO CLASS DEFS +############################################################ + +# This lets us index into a string or an array of integers transparently. +def Ord(a): + """ + Allows indexing into a string or an array of integers transparently. + Generic utility function. + """ + if type(a) == type("a"): + a = ord(a) + return a + +def IsLinux(): + """ + Returns True if platform is Linux. + Generic utility function. + """ + return (platform.uname()[0] == "Linux") def GetLastPathElement(path): + """ + Similar to basename. + Generic utility function. + """ return path.rsplit('/', 1)[1] -def GetFileContents(filepath): - file = None +def GetFileContents(filepath,asbin=False): + """ + Read and return contents of 'filepath'. + """ + mode='r' + if asbin: + mode+='b' + c=None try: - file = open(filepath) - except: - return None - if file == None: - return None - try: - return file.read() - finally: - file.close() + with open(filepath, mode) as F : + c=F.read() + except IOError, e: + ErrorWithPrefix('GetFileContents','Reading from file ' + filepath + ' Exception is ' + str(e)) + return None + return c def SetFileContents(filepath, contents): - file = open(filepath, "w") + """ + Write 'contents' to 'filepath'. + """ + if type(contents) == str : + contents=contents.encode('latin-1') try: - file.write(contents) - finally: - file.close() + with open(filepath, "wb+") as F : + F.write(contents) + except IOError, e: + ErrorWithPrefix('SetFileContents','Writing to file ' + filepath + ' Exception is ' + str(e)) + return None + return 0 def AppendFileContents(filepath, contents): - file = open(filepath, "a") - try: - file.write(contents) - finally: - file.close() + """ + Append 'contents' to 'filepath'. + """ + if type(contents) == str : + contents=contents.encode('latin-1') + try: + with open(filepath, "a+") as F : + F.write(contents) + except IOError, e: + ErrorWithPrefix('AppendFileContents','Appending to file ' + filepath + ' Exception is ' + str(e)) + return None + return 0 def ReplaceFileContentsAtomic(filepath, contents): + """ + Write 'contents' to 'filepath' by creating a temp file, and replacing original. + """ handle, temp = tempfile.mkstemp(dir = os.path.dirname(filepath)) + if type(contents) == str : + contents=contents.encode('latin-1') try: os.write(handle, contents) + except IOError, e: + ErrorWithPrefix('ReplaceFileContentsAtomic','Writing to file ' + filepath + ' Exception is ' + str(e)) + return None finally: os.close(handle) try: os.rename(temp, filepath) - return - except: - pass - os.remove(filepath) - os.rename(temp, filepath) + return None + except IOError, e: + ErrorWithPrefix('ReplaceFileContentsAtomic','Renaming ' + temp+ ' to ' + filepath + ' Exception is ' + str(e)) + try: + os.remove(filepath) + except IOError, e: + ErrorWithPrefix('ReplaceFileContentsAtomic','Removing '+ filepath + ' Exception is ' + str(e)) + try: + os.rename(temp,filepath) + except IOError, e: + ErrorWithPrefix('ReplaceFileContentsAtomic','Removing '+ filepath + ' Exception is ' + str(e)) + return 1 + return 0 def GetLineStartingWith(prefix, filepath): + """ + Return line from 'filepath' if the line startswith 'prefix' + """ for line in GetFileContents(filepath).split('\n'): if line.startswith(prefix): return line return None def Run(cmd,chk_err=True): + """ + Calls RunGetOutput on 'cmd', returning only the return code. + If chk_err=True then errors will be reported in the log. + If chk_err=False then errors will be suppressed from the log. + """ retcode,out=RunGetOutput(cmd,chk_err) return retcode def RunGetOutput(cmd,chk_err=True): + """ + Wrapper for subprocess.check_output. + Execute 'cmd'. Returns return code and STDOUT, trapping expected exceptions. + Reports exceptions to Error if chk_err parameter is True + """ LogIfVerbose(cmd) try: output=subprocess.check_output(cmd,stderr=subprocess.STDOUT,shell=True) except subprocess.CalledProcessError,e : if chk_err : - Error('CalledProcessError. Error Code: ' + str(e.returncode) ) - Error('CalledProcessError. Command string: "' + e.cmd + '"') - Error('CalledProcessError. Command result: "' + e.output[:-1] + '"') - return e.returncode,e.output - return 0,output + Error('CalledProcessError. Error Code is ' + str(e.returncode) ) + Error('CalledProcessError. Command string was ' + e.cmd ) + Error('CalledProcessError. Command result was ' + (e.output[:-1]).decode('latin-1')) + return e.returncode,e.output.decode('latin-1') + return 0,output.decode('latin-1') def RunSendStdin(cmd,input,chk_err=True): + """ + Wrapper for subprocess.Popen. + Execute 'cmd', sending 'input' to STDIN of 'cmd'. + Returns return code and STDOUT, trapping expected exceptions. + Reports exceptions to Error if chk_err parameter is True + """ LogIfVerbose(cmd+input) try: me=subprocess.Popen([cmd], shell=True, stdin=subprocess.PIPE,stderr=subprocess.STDOUT,stdout=subprocess.PIPE) output=me.communicate(input) - except OSError,e : + except OSError , e : if chk_err : - Error('CalledProcessError. Error Code:' + str(me.returncode)) - Error('CalledProcessError. Command string:"' + cmd + '"' ) - Error('CalledProcessError. Command result:"' + output[:-1] + '"') - return 1,output[0] + Error('CalledProcessError. Error Code is ' + str(me.returncode) ) + Error('CalledProcessError. Command string was ' + cmd ) + Error('CalledProcessError. Command result was ' + output[0].decode('latin-1')) + return 1,output[0].decode('latin-1') if me.returncode is not 0 and chk_err is True: - Error('CalledProcessError. Error Code:' + str(me.returncode)) - Error('CalledProcessError. Command string:"' + cmd + '"' ) - Error('CalledProcessError. Command result:"' + (output[0])[:-1] + '"') - return me.returncode,output[0] + Error('CalledProcessError. Error Code is ' + str(me.returncode) ) + Error('CalledProcessError. Command string was ' + cmd ) + Error('CalledProcessError. Command result was ' + output[0].decode('latin-1')) + return me.returncode,output[0].decode('latin-1') def GetNodeTextData(a): + """ + Filter non-text nodes from DOM tree + """ for b in a.childNodes: if b.nodeType == b.TEXT_NODE: return b.data def GetHome(): + """ + Attempt to guess the $HOME location. + Return the path string. + """ home = None try: home = GetLineStartingWith("HOME", "/etc/default/useradd").split('=')[1].strip() @@ -260,6 +1754,9 @@ def GetHome(): return home def ChangeOwner(filepath, user): + """ + Lookup user. Attempt chown 'filepath' to 'user'. + """ p = None try: p = pwd.getpwnam(user) @@ -269,6 +1766,10 @@ def ChangeOwner(filepath, user): os.chown(filepath, p[2], p[3]) def CreateDir(dirpath, user, mode): + """ + Attempt os.makedirs, catch all exceptions. + Call ChangeOwner afterwards. + """ try: os.makedirs(dirpath, mode) except: @@ -276,9 +1777,11 @@ def CreateDir(dirpath, user, mode): ChangeOwner(dirpath, user) def CreateAccount(user, password, expiration, thumbprint): - if IsWindows(): - Log("Skipping CreateAccount on Windows") - return None + """ + Create a user account, with 'user', 'password', 'expiration', ssh keys + and sudo permissions. + Returns None if successful, error string on failure. + """ userentry = None try: userentry = pwd.getpwnam(user) @@ -306,6 +1809,12 @@ def CreateAccount(user, password, expiration, thumbprint): if password != None: RunSendStdin("chpasswd",(user + ":" + password + "\n")) try: + # 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 + SetFileContents('/etc/sudoers',GetFileContents('/etc/sudoers')+'\n#includedir /etc/sudoers.d\n') if password == None: SetFileContents("/etc/sudoers.d/waagent", user + " ALL = (ALL) NOPASSWD: ALL\n") else: @@ -314,7 +1823,7 @@ def CreateAccount(user, password, expiration, thumbprint): except: Error("CreateAccount: Failed to configure sudo access for user.") return "Failed to configure sudo privileges (0x08)." - home = GetHome() + home = MyDistro.GetHome() if thumbprint != None: dir = home + "/" + user + "/.ssh" CreateDir(dir, user, 0700) @@ -331,9 +1840,11 @@ def CreateAccount(user, password, expiration, thumbprint): return None def DeleteAccount(user): - if IsWindows(): - Log("Skipping DeleteAccount on Windows") - return + """ + Delete the 'user'. + Clear utmp first, to avoid error. + Removes the /etc/sudoers.d/waagent file. + """ userentry = None try: userentry = pwd.getpwnam(user) @@ -360,31 +1871,31 @@ def DeleteAccount(user): pass return -def ReloadSshd(): - name = None - if IsRedHat() or IsSuse(): - name = "sshd" - if IsDebian(): - name = "ssh" - if name == None: - return - if not Run("service " + name + " status | grep running"): - Run("service " + name + " reload") - def IsInRangeInclusive(a, low, high): + """ + Return True if 'a' in 'low' <= a >= 'high' + """ return (a >= low and a <= high) def IsPrintable(ch): + """ + Return True if character is displayable. + """ return IsInRangeInclusive(ch, Ord('A'), Ord('Z')) or IsInRangeInclusive(ch, Ord('a'), Ord('z')) or IsInRangeInclusive(ch, Ord('0'), Ord('9')) def HexDump(buffer, size): + """ + Return Hex formated dump of a 'buffer' of 'size'. + """ if size < 0: size = len(buffer) result = "" for i in range(0, size): if (i % 16) == 0: result += "%06X: " % i - byte = struct.unpack("B", buffer[i])[0] + byte = buffer[i] + if type(byte) == str: + byte = ord(byte.decode('latin1')) result += "%02X " % byte if (i & 15) == 7: result += " " @@ -397,7 +1908,9 @@ def HexDump(buffer, size): j += 1 result += " " for j in range(i - (i % 16), i + 1): - byte = struct.unpack("B", buffer[j])[0] + byte=buffer[j] + if type(byte) == str: + byte = ord(byte.decode('latin1')) k = '.' if IsPrintable(byte): k = chr(byte) @@ -406,121 +1919,188 @@ def HexDump(buffer, size): result += "\n" return result -def ThrottleLog(counter): - # Log everything up to 10, every 10 up to 100, then every 100. - return (counter < 10) or ((counter < 100) and ((counter % 10) == 0)) or ((counter % 100) == 0) - -def Logger(): - class T(object): - def __init__(self): - self.File = None - self.Con = None +class Logger(object): + """ + The Agent's logging assumptions are: + For Log, and LogWithPrefix all messages are logged to the + self.file_path and to the self.con_path. Setting either path + parameter to None skips that log. If Verbose is enabled, messages + calling the LogIfVerbose method will be logged to file_path yet + not to con_path. Error and Warn messages are normal log messages + with the 'ERROR:' or 'WARNING:' prefix added. + """ + + def __init__(self,filepath,conpath,verbose=False): + """ + Construct an instance of Logger. + """ + self.file_path=filepath + self.con_path=conpath + self.verbose=verbose + + def ThrottleLog(self,counter): + """ + Log everything up to 10, every 10 up to 100, then every 100. + """ + return (counter < 10) or ((counter < 100) and ((counter % 10) == 0)) or ((counter % 100) == 0) + + def LogToFile(self,message): + """ + Write 'message' to logfile. + """ + if self.file_path: + with open(self.file_path, "a") as F : + F.write(message + "\n") + F.close() - self = T() - - def LogToFile(message): - FilePath = ["/var/log/waagent.log", "waagent.log"][IsWindows()] - if not os.path.isfile(FilePath) and self.File != None: - self.File.close() - self.File = None - if self.File == None: - self.File = open(FilePath, "a") - self.File.write(message + "\n") - self.File.flush() - - def LogToCon(message): - ConPath = '/dev/console' - if self.Con == None: - self.Con = open(ConPath, "a") - self.Con.write(message + "\n") - self.Con.flush() - - def Log(message): - LogWithPrefix("", message) - - def LogWithPrefix(prefix, message): + def LogToCon(self,message): + """ + Write 'message' to /dev/console. + This supports serial port logging if the /dev/console + is redirected to ttys0 in kernel boot options. + """ + if self.con_path: + with open(self.con_path, "w") as C : +# if isinstance(message,str): +# message=message.encode('latin-1')) + C.write(message + "\n") + C.close() + + def Log(self,message): + """ + Standard Log function. + Logs to self.file_path, and con_path + """ + self.LogWithPrefix("", message) + + def LogWithPrefix(self,prefix, message): + """ + Prefix each line of 'message' with current time+'prefix'. + """ t = time.localtime() t = "%04u/%02u/%02u %02u:%02u:%02u " % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) t += prefix for line in message.split('\n'): line = t + line - LogToFile(line) - LogToCon(line) + self.LogToFile(line) + self.LogToCon(line) - return Log, LogWithPrefix - -Log, LogWithPrefix = Logger() - -def NoLog(message): - pass - -def LogIfVerbose(message): - if Verbose == True: - LogFileWithPrefix('',message) - -def LogWithPrefixIfVerbose(prefix, message): - if Verbose == True: - LogWithPrefix(prefix, message) - -def Warn(message): - LogWithPrefix("WARNING:", message) - -def Error(message): - ErrorWithPrefix("", message) - -def ErrorWithPrefix(prefix, message): - LogWithPrefix("ERROR:", message) - -def Linux_ioctl_GetIpv4Address(ifname): - import fcntl - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - return socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24]) + def NoLog(self,message): + """ + Don't Log. + """ + pass + + def LogIfVerbose(self,message): + """ + Only log 'message' if global Verbose is True. + Verbose messages are assumed to be undesiarable in the + serial logs, so do not send the verbose logging to /dev/console + """ + self.LogWithPrefixIfVerbose('',message) + + def LogWithPrefixIfVerbose(self,prefix, message): + """ + Only log 'message' if global Verbose is True. + Log to logfile, ignoring /dev/console + Prefix each line of 'message' with current time+'prefix'. + """ + if self.verbose == True: + t = time.localtime() + t = "%04u/%02u/%02u %02u:%02u:%02u " % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) + t += prefix + for line in message.split('\n'): + line = t + line + self.LogToFile(line) + + def Warn(self,message): + """ + Prepend the text "WARNING:" to the prefix for each line in 'message'. + """ + self.LogWithPrefix("WARNING:", message) + + def Error(self,message): + """ + Call ErrorWithPrefix(message). + """ + ErrorWithPrefix("", message) + + def ErrorWithPrefix(self,prefix, message): + """ + Prepend the text "ERROR:" to the prefix for each line in 'message'. + Errors written to logfile, and /dev/console + """ + self.LogWithPrefix("ERROR:", message) + +def LoggerInit(log_file_path,log_con_path,verbose=False): + """ + Create log object and export its methods to global scope. + """ + global Log,LogWithPrefix,LogIfVerbose,LogWithPrefixIfVerbose,Error,ErrorWithPrefix,Warn,NoLog,ThrottleLog,myLogger + l=Logger(log_file_path,log_con_path,verbose) + Log,LogWithPrefix,LogIfVerbose,LogWithPrefixIfVerbose,Error,ErrorWithPrefix,Warn,NoLog,ThrottleLog,myLogger = l.Log,l.LogWithPrefix,l.LogIfVerbose,l.LogWithPrefixIfVerbose,l.Error,l.ErrorWithPrefix,l.Warn,l.NoLog,l.ThrottleLog,l def Linux_ioctl_GetInterfaceMac(ifname): - import fcntl + """ + Return the mac-address bound to the socket. + """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', ifname[:15])) + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', (ifname[:15]+('\0'*241)).encode('latin-1'))) return ''.join(['%02X' % Ord(char) for char in info[18:24]]) +def GetFirstActiveNetworkInterfaceNonLoopback(): + """ + Return the interface name, and ip addr of the + first active non-loopback interface. + """ + expected=16 # how many devices should I expect... + struct_size=40 # for 64bit the size is 40 bytes + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + buff=array.array('B', b'\0' * (expected*struct_size)) +# retsize=(struct.unpack('iL', fcntl.ioctl(s.fileno().to_bytes(1,'little'), 0x8912, struct.pack('iL',expected*struct_size,buff.buffer_info()[0]))))[0] + retsize=(struct.unpack('iL', fcntl.ioctl(s.fileno(), 0x8912, struct.pack('iL',expected*struct_size,buff.buffer_info()[0]))))[0] + if retsize == (expected*struct_size) : + Warn('SIOCGIFCONF returned more than ' + str(expected) + ' up network interfaces.') + s=buff.tostring() + for i in range(0,struct_size*expected,struct_size): + iface=s[i:i+16].split(b'\0', 1)[0] + if iface == b'lo': + continue + else : + break + return iface.decode('latin-1'), socket.inet_ntoa(s[i+20:i+24]) + + def GetIpv4Address(): - if IsLinux(): - for ifname in PossibleEthernetInterfaces: - try: - return Linux_ioctl_GetIpv4Address(ifname) - except IOError, e: - pass - else: - try: - return socket.gethostbyname(socket.gethostname()) - except Exception, e: - ErrorWithPrefix("GetIpv4Address:", str(e)) - ErrorWithPrefix("GetIpv4Address:", traceback.format_exc()) + """ + Return the ip of the + first active non-loopback interface. + """ + iface,addr=GetFirstActiveNetworkInterfaceNonLoopback() + return addr def HexStringToByteArray(a): - b = "" - for c in range(0, len(a) / 2): + """ + Return hex string packed into a binary struct. + """ + b = b"" + for c in range(0, len(a) // 2): b += struct.pack("B", int(a[c * 2:c * 2 + 2], 16)) return b def GetMacAddress(): - if IsWindows(): - # Windows: Physical Address. . . . . . . . . : 00-15-17-79-00-7F\n - a = "ipconfig /all | findstr /c:\"Physical Address\" | findstr /v \"00-00-00-00-00-00-00\"" - a = os.popen(a).read() # not re-implementing wirh RunGetOutput - not called unless we're in windows - a = re.sub("\s+$", "", a) - a = re.sub(".+ ", "", a) - a = re.sub(":", "", a) - a = re.sub("-", "", a) - else: - for ifname in PossibleEthernetInterfaces: - try: - a = Linux_ioctl_GetInterfaceMac(ifname) - break - except IOError, e: - pass + """ + Convienience function, returns mac addr bound to + first non-loobback interface. + """ + ifname=GetFirstActiveNetworkInterfaceNonLoopback()[0] + a = Linux_ioctl_GetInterfaceMac(ifname) return HexStringToByteArray(a) def DeviceForIdePort(n): + """ + Return device name attached to ide port 'n'. + """ if n > 3: return None g0 = "00000000" @@ -536,11 +2116,25 @@ def DeviceForIdePort(n): 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 class Util(object): + """ + Http communication class. + Base of GoalState, and Agent classes. + """ def _HttpGet(self, url, headers): + """ + Do HTTP get on 'url' with 'headers'. + On error, sleep 10 and maxRetry times. + Return the output buffer or None. + """ LogIfVerbose("HttpGet(" + url + ")") maxRetry = 2 if url.startswith("http://"): @@ -560,8 +2154,10 @@ class Util(object): request = httpConnection.request("GET", url, None, headers) response = httpConnection.getresponse() strStatus = str(response.status) - except httplib.HTTPException, e: + except httplib.HTTPException, e: Error('HTTPException ' + e.message + ' args: ' + repr(e.args)) + except IOError, e: + Error('socket IOError ' + e.message + ' args: ' + repr(e.args)) log("response HttpGet(" + url + "),retry=" + strRetry + ",status=" + strStatus) if response == None or response.status != httplib.OK: Error("HttpGet(" + url + "),retry=" + strRetry + ",status=" + strStatus) @@ -576,18 +2172,32 @@ class Util(object): return response.read() def HttpGetWithoutHeaders(self, url): + """ + Return data from an HTTP get on 'url'. + """ return self._HttpGet(url, None) def HttpGetWithHeaders(self, url): + """ + Return data from an HTTP get on 'url' with + x-ms-agent-name and x-ms-version + headers. + """ return self._HttpGet(url, {"x-ms-agent-name": GuestAgentName, "x-ms-version": ProtocolVersion}) def HttpSecureGetWithHeaders(self, url, transportCert): + """ + Return output of get using ssl cert. + """ return self._HttpGet(url, {"x-ms-agent-name": GuestAgentName, "x-ms-version": ProtocolVersion, "x-ms-cipher-name": "DES_EDE3_CBC", "x-ms-guest-agent-public-x509-cert": transportCert}) def HttpPost(self, url, data): + """ + Send http POST to server, sleeping 10 retrying maxRetry times upon error. + """ LogIfVerbose("HttpPost(" + url + ")") maxRetry = 2 for retry in range(0, maxRetry + 1): @@ -605,6 +2215,8 @@ class Util(object): strStatus = str(response.status) except httplib.HTTPException, e: Error('HTTPException ' + e.message + ' args: ' + repr(e.args)) + except IOError, e: + Error('socket IOError ' + e.message + ' args: ' + repr(e.args)) log("response HttpPost(" + url + "),retry=" + strRetry + ",status=" + strStatus) if response == None or (response.status != httplib.OK and response.status != httplib.ACCEPTED): Error("HttpPost(" + url + "),retry=" + strRetry + ",status=" + strStatus) @@ -618,50 +2230,64 @@ class Util(object): log("return HttpPost(" + url + "),retry=" + strRetry + ",status=" + strStatus) return response -def LoadBalancerProbeServer(port): - - class T(object): - def __init__(self, ip, port): - self.ProbeCounter = 0 - self.server = SocketServer.TCPServer((ip, port), TCPHandler) - self.server_thread = threading.Thread(target = self.server.serve_forever) - self.server_thread.setDaemon(True) - self.server_thread.start() - - def shutdown(self): - self.server.shutdown() - - class TCPHandler(SocketServer.BaseRequestHandler): - def GetHttpDateTimeNow(self): - # Date: Fri, 25 Mar 2011 04:53:10 GMT - return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) - - def handle(self): - context.ProbeCounter = (context.ProbeCounter + 1) % 1000000 - log = [NoLog, LogIfVerbose][ThrottleLog(context.ProbeCounter)] - strCounter = str(context.ProbeCounter) - if context.ProbeCounter == 1: - Log("Receiving LB probes.") - log("Received LB probe # " + strCounter) - self.request.recv(1024) - self.request.send("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: text/html\r\nDate: " + self.GetHttpDateTimeNow() + "\r\n\r\nOK") +class TCPHandler(SocketServer.BaseRequestHandler): + """ + Callback object for LoadBalancerProbeServer. + Recv and send LB probe messages. + """ + def __init__(self,lb_probe): + super(TCPHandler,self).__init__() + self.lb_probe=lb_probe + + def GetHttpDateTimeNow(self): + """ + Return formatted gmtime "Date: Fri, 25 Mar 2011 04:53:10 GMT" + """ + return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + + def handle(self): + """ + Log LB probe messages, read the socket buffer, + send LB probe response back to server. + """ + self.lb_probe.ProbeCounter = (self.lb_probe.ProbeCounter + 1) % 1000000 + log = [NoLog, LogIfVerbose][ThrottleLog(self.lb_probe.ProbeCounter)] + strCounter = str(self.lb_probe.ProbeCounter) + if self.lb_probe.ProbeCounter == 1: + Log("Receiving LB probes.") + log("Received LB probe # " + strCounter) + self.request.recv(1024) + self.request.send("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: text/html\r\nDate: " + self.GetHttpDateTimeNow() + "\r\n\r\nOK") - for retry in range(1,6): - context=None - ip = GetIpv4Address() - if ip == None : - Log("LoadBalancerProbeServer: GetIpv4Address() returned None, sleeping 10 before retry " + str(retry+1) ) - time.sleep(10) - else: - try: - context = T(ip,port) - break - except Exception, e: - Log("LoadBalancerProbeServer: Exception contructing socket server: " + str(e)) - Log("LoadBalancerProbeServer: Retry socket server construction #" + str(retry+1) ) - return context +class LoadBalancerProbeServer(object): + """ + Threaded object to receive and send LB probe messages. + Load Balancer messages but be recv'd by + the load balancing server, or this node may be shut-down. + """ + def __init__(self, port): + self.ProbeCounter = 0 + self.server = SocketServer.TCPServer((self.get_ip(), port), TCPHandler) + self.server_thread = threading.Thread(target = self.server.serve_forever) + self.server_thread.setDaemon(True) + self.server_thread.start() + + def shutdown(self): + self.server.shutdown() + + def get_ip(self): + for retry in range(1,6): + ip = MyDistro.GetIpv4Address() + if ip == None : + Log("LoadBalancerProbeServer: GetIpv4Address() returned None, sleeping 10 before retry " + str(retry+1) ) + time.sleep(10) + else: + return ip class ConfigurationProvider(object): + """ + Parse amd store key:values in /etc/waagent.conf. + """ def __init__(self): self.values = dict() if os.path.isfile("/etc/waagent.conf") == False: @@ -684,6 +2310,10 @@ class ConfigurationProvider(object): return self.values.get(key) class EnvMonitor(object): + """ + Montor changes to dhcp and hostname. + If dhcp clinet process re-start has occurred, reset routes, dhcp with fabric. + """ def __init__(self): self.shutdown = False self.HostName = socket.gethostname() @@ -693,13 +2323,13 @@ class EnvMonitor(object): self.published = False def monitor(self): - publish = Config.get("Provisioning.MonitorHostName") - dhcpcmd = "pidof dhclient" - if IsSuse(): - dhcpcmd = "pidof dhcpcd" - if IsDebian(): - dhcpcmd = "pidof dhclient3" - dhcppid = RunGetOutput(dhcpcmd,chk_err=False)[1] + """ + Monitor dhcp client pid and hostname. + If dhcp clinet process re-start has occurred, reset routes, dhcp with fabric. + """ + publish = ConfigurationProvider().get("Provisioning.MonitorHostName") + dhcpcmd = MyDistro.getpidcmd+ ' ' + MyDistro.getDhcpClientName() + dhcppid = RunGetOutput(dhcpcmd)[1] while not self.shutdown: for a in RulesFiles: if os.path.isfile(a): @@ -713,7 +2343,7 @@ class EnvMonitor(object): Log("EnvMonitor: Detected host name change: " + self.HostName + " -> " + socket.gethostname()) self.HostName = socket.gethostname() WaAgent.UpdateAndPublishHostName(self.HostName) - dhcppid = RunGetOutput(dhcpcmd,chk_err=False)[1] + dhcppid = RunGetOutput(dhcpcmd)[1] self.published = True except: pass @@ -721,7 +2351,7 @@ class EnvMonitor(object): self.published = True pid = "" if not os.path.isdir("/proc/" + dhcppid.strip()): - pid = RunGetOutput(dhcpcmd,chk_err=False)[1] + pid = RunGetOutput(dhcpcmd)[1] if pid != "" and pid != dhcppid: Log("EnvMonitor: Detected dhcp client restart. Restoring routing table.") WaAgent.RestoreRoutes() @@ -732,37 +2362,56 @@ class EnvMonitor(object): time.sleep(5) def SetHostName(self, name): + """ + Generic call to MyDistro.setHostname(name). + Complian to Log on error. + """ if socket.gethostname() == name: self.published = True - elif Run("hostname " + name): + elif MyDistro.setHostname(name): Error("Error: SetHostName: Cannot set hostname to " + name) return ("Error: SetHostName: Cannot set hostname to " + name) - def IsNamePublished(self): + def IsHostnamePublished(self): + """ + Return self.published + """ return self.published def ShutdownService(self): + """ + Stop server comminucation and join the thread to main thread. + """ self.shutdown = True self.server_thread.join() class Certificates(object): -# -# <CertificateFile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="certificates10.xsd"> -# <Version>2010-12-15</Version> -# <Incarnation>2</Incarnation> -# <Format>Pkcs7BlobWithPfxContents</Format> -# <Data>MIILTAY... -# </Data> -# </CertificateFile> -# + """ + Object containing certificates of host and provisioned user. + Parses and splits certificates into files. + """ + # <CertificateFile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="certificates10.xsd"> + # <Version>2010-12-15</Version> + # <Incarnation>2</Incarnation> + # <Format>Pkcs7BlobWithPfxContents</Format> + # <Data>MIILTAY... + # </Data> + # </CertificateFile> + def __init__(self): self.reinitialize() def reinitialize(self): + """ + Reset the Role, Incarnation + """ self.Incarnation = None self.Role = None def Parse(self, xmlText): + """ + Parse multiple certificates into seperate files. + """ self.reinitialize() SetFileContents("Certificates.xml", xmlText) dom = xml.dom.minidom.parseString(xmlText) @@ -811,71 +2460,75 @@ class Certificates(object): keys[pubkey] = thumbprint os.rename(filename, thumbprint + ".crt") os.chmod(thumbprint + ".crt", 0600) - if IsRedHat(): - Run("chcon unconfined_u:object_r:ssh_home_t:s0 " + thumbprint + ".crt") + MyDistro.setSelinuxContext(thumbprint + '.crt','unconfined_u:object_r:ssh_home_t:s0') index += 1 filename = str(index) + ".crt" index = 1 filename = str(index) + ".prv" while os.path.isfile(filename): - pubkey = RunGetOutput(Openssl + " rsa -in " + filename + " -pubout 2> /dev/null")[1] + pubkey = RunGetOutput(Openssl + " rsa -in " + filename + " -pubout 2> /dev/null ")[1] os.rename(filename, keys[pubkey] + ".prv") os.chmod(keys[pubkey] + ".prv", 0600) - if IsRedHat(): - Run("chcon unconfined_u:object_r:ssh_home_t:s0 " + keys[pubkey] + ".prv") + MyDistro.setSelinuxContext( keys[pubkey] + '.prv','unconfined_u:object_r:ssh_home_t:s0') index += 1 filename = str(index) + ".prv" return self class SharedConfig(object): -# -# <SharedConfig version="1.0.0.0" goalStateIncarnation="1"> -# <Deployment name="db00a7755a5e4e8a8fe4b19bc3b330c3" guid="{ce5a036f-5c93-40e7-8adf-2613631008ab}" incarnation="2"> -# <Service name="MyVMRoleService" guid="{00000000-0000-0000-0000-000000000000}" /> -# <ServiceInstance name="db00a7755a5e4e8a8fe4b19bc3b330c3.1" guid="{d113f4d7-9ead-4e73-b715-b724b5b7842c}" /> -# </Deployment> -# <Incarnation number="1" instance="MachineRole_IN_0" guid="{a0faca35-52e5-4ec7-8fd1-63d2bc107d9b}" /> -# <Role guid="{73d95f1c-6472-e58e-7a1a-523554e11d46}" name="MachineRole" settleTimeSeconds="10" /> -# <LoadBalancerSettings timeoutSeconds="0" waitLoadBalancerProbeCount="8"> -# <Probes> -# <Probe name="MachineRole" /> -# <Probe name="55B17C5E41A1E1E8FA991CF80FAC8E55" /> -# <Probe name="3EA4DBC19418F0A766A4C19D431FA45F" /> -# </Probes> -# </LoadBalancerSettings> -# <OutputEndpoints> -# <Endpoint name="MachineRole:Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" type="SFS"> -# <Target instance="MachineRole_IN_0" endpoint="Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" /> -# </Endpoint> -# </OutputEndpoints> -# <Instances> -# <Instance id="MachineRole_IN_0" address="10.115.153.75"> -# <FaultDomains randomId="0" updateId="0" updateCount="0" /> -# <InputEndpoints> -# <Endpoint name="a" address="10.115.153.75:80" protocol="http" isPublic="true" loadBalancedPublicAddress="70.37.106.197:80" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false"> -# <LocalPorts> -# <LocalPortRange from="80" to="80" /> -# </LocalPorts> -# </Endpoint> -# <Endpoint name="Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" address="10.115.153.75:3389" protocol="tcp" isPublic="false" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false"> -# <LocalPorts> -# <LocalPortRange from="3389" to="3389" /> -# </LocalPorts> -# </Endpoint> -# <Endpoint name="Microsoft.WindowsAzure.Plugins.RemoteForwarder.RdpInput" address="10.115.153.75:20000" protocol="tcp" isPublic="true" loadBalancedPublicAddress="70.37.106.197:3389" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false"> -# <LocalPorts> -# <LocalPortRange from="20000" to="20000" /> -# </LocalPorts> -# </Endpoint> -# </InputEndpoints> -# </Instance> -# </Instances> -# </SharedConfig> -# + """ + Parse role endpoint server and goal state config. + """ + # + # <SharedConfig version="1.0.0.0" goalStateIncarnation="1"> + # <Deployment name="db00a7755a5e4e8a8fe4b19bc3b330c3" guid="{ce5a036f-5c93-40e7-8adf-2613631008ab}" incarnation="2"> + # <Service name="MyVMRoleService" guid="{00000000-0000-0000-0000-000000000000}" /> + # <ServiceInstance name="db00a7755a5e4e8a8fe4b19bc3b330c3.1" guid="{d113f4d7-9ead-4e73-b715-b724b5b7842c}" /> + # </Deployment> + # <Incarnation number="1" instance="MachineRole_IN_0" guid="{a0faca35-52e5-4ec7-8fd1-63d2bc107d9b}" /> + # <Role guid="{73d95f1c-6472-e58e-7a1a-523554e11d46}" name="MachineRole" settleTimeSeconds="10" /> + # <LoadBalancerSettings timeoutSeconds="0" waitLoadBalancerProbeCount="8"> + # <Probes> + # <Probe name="MachineRole" /> + # <Probe name="55B17C5E41A1E1E8FA991CF80FAC8E55" /> + # <Probe name="3EA4DBC19418F0A766A4C19D431FA45F" /> + # </Probes> + # </LoadBalancerSettings> + # <OutputEndpoints> + # <Endpoint name="MachineRole:Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" type="SFS"> + # <Target instance="MachineRole_IN_0" endpoint="Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" /> + # </Endpoint> + # </OutputEndpoints> + # <Instances> + # <Instance id="MachineRole_IN_0" address="10.115.153.75"> + # <FaultDomains randomId="0" updateId="0" updateCount="0" /> + # <InputEndpoints> + # <Endpoint name="a" address="10.115.153.75:80" protocol="http" isPublic="true" loadBalancedPublicAddress="70.37.106.197:80" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false"> + # <LocalPorts> + # <LocalPortRange from="80" to="80" /> + # </LocalPorts> + # </Endpoint> + # <Endpoint name="Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" address="10.115.153.75:3389" protocol="tcp" isPublic="false" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false"> + # <LocalPorts> + # <LocalPortRange from="3389" to="3389" /> + # </LocalPorts> + # </Endpoint> + # <Endpoint name="Microsoft.WindowsAzure.Plugins.RemoteForwarder.RdpInput" address="10.115.153.75:20000" protocol="tcp" isPublic="true" loadBalancedPublicAddress="70.37.106.197:3389" enableDirectServerReturn="false" isDirectAddress="false" disableStealthMode="false"> + # <LocalPorts> + # <LocalPortRange from="20000" to="20000" /> + # </LocalPorts> + # </Endpoint> + # </InputEndpoints> + # </Instance> + # </Instances> + # </SharedConfig> + # def __init__(self): self.reinitialize() def reinitialize(self): + """ + Reset members. + """ self.Deployment = None self.Incarnation = None self.Role = None @@ -884,6 +2537,9 @@ class SharedConfig(object): self.Instances = None def Parse(self, xmlText): + """ + Parse and write configuration to file SharedConfig.xml. + """ self.reinitialize() SetFileContents("SharedConfig.xml", xmlText) dom = xml.dom.minidom.parseString(xmlText) @@ -898,45 +2554,55 @@ class SharedConfig(object): return None program = Config.get("Role.TopologyConsumer") if program != None: - Children.append(subprocess.Popen([program, LibDir + "/SharedConfig.xml"])) + try: + Children.append(subprocess.Popen([program, LibDir + "/SharedConfig.xml"])) + except OSError, e : + ErrorWithPrefix('Agent.Run','Exception: '+ str(e) +' occured launching ' + program ) return self - + class HostingEnvironmentConfig(object): -# -# <HostingEnvironmentConfig version="1.0.0.0" goalStateIncarnation="1"> -# <StoredCertificates> -# <StoredCertificate name="Stored0Microsoft.WindowsAzure.Plugins.RemoteAccess.PasswordEncryption" certificateId="sha1:C093FA5CD3AAE057CB7C4E04532B2E16E07C26CA" storeName="My" configurationLevel="System" /> -# </StoredCertificates> -# <Deployment name="db00a7755a5e4e8a8fe4b19bc3b330c3" guid="{ce5a036f-5c93-40e7-8adf-2613631008ab}" incarnation="2"> -# <Service name="MyVMRoleService" guid="{00000000-0000-0000-0000-000000000000}" /> -# <ServiceInstance name="db00a7755a5e4e8a8fe4b19bc3b330c3.1" guid="{d113f4d7-9ead-4e73-b715-b724b5b7842c}" /> -# </Deployment> -# <Incarnation number="1" instance="MachineRole_IN_0" guid="{a0faca35-52e5-4ec7-8fd1-63d2bc107d9b}" /> -# <Role guid="{73d95f1c-6472-e58e-7a1a-523554e11d46}" name="MachineRole" hostingEnvironmentVersion="1" software="" softwareType="ApplicationPackage" entryPoint="" parameters="" settleTimeSeconds="10" /> -# <HostingEnvironmentSettings name="full" Runtime="rd_fabric_stable.110217-1402.RuntimePackage_1.0.0.8.zip"> -# <CAS mode="full" /> -# <PrivilegeLevel mode="max" /> -# <AdditionalProperties><CgiHandlers></CgiHandlers></AdditionalProperties> -# </HostingEnvironmentSettings> -# <ApplicationSettings> -# <Setting name="__ModelData" value="<m role="MachineRole" xmlns="urn:azure:m:v1"><r name="MachineRole"><e name="a" /><e name="b" /><e name="Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" /><e name="Microsoft.WindowsAzure.Plugins.RemoteForwarder.RdpInput" /></r></m>" /> -# <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="DefaultEndpointsProtocol=http;AccountName=osimages;AccountKey=DNZQ..." /> -# <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountEncryptedPassword" value="MIIBnQYJKoZIhvcN..." /> -# <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountExpiration" value="2022-07-23T23:59:59.0000000-07:00" /> -# <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountUsername" value="test" /> -# <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.Enabled" value="true" /> -# <Setting name="Microsoft.WindowsAzure.Plugins.RemoteForwarder.Enabled" value="true" /> -# <Setting name="Certificate|Microsoft.WindowsAzure.Plugins.RemoteAccess.PasswordEncryption" value="sha1:C093FA5CD3AAE057CB7C4E04532B2E16E07C26CA" /> -# </ApplicationSettings> -# <ResourceReferences> -# <Resource name="DiagnosticStore" type="directory" request="Microsoft.Cis.Fabric.Controller.Descriptions.ServiceDescription.Data.Policy" sticky="true" size="1" path="db00a7755a5e4e8a8fe4b19bc3b330c3.MachineRole.DiagnosticStore\" disableQuota="false" /> -# </ResourceReferences> -# </HostingEnvironmentConfig> -# + """ + Parse Hosting enviromnet config and store in + HostingEnvironmentConfig.xml + """ + # + # <HostingEnvironmentConfig version="1.0.0.0" goalStateIncarnation="1"> + # <StoredCertificates> + # <StoredCertificate name="Stored0Microsoft.WindowsAzure.Plugins.RemoteAccess.PasswordEncryption" certificateId="sha1:C093FA5CD3AAE057CB7C4E04532B2E16E07C26CA" storeName="My" configurationLevel="System" /> + # </StoredCertificates> + # <Deployment name="db00a7755a5e4e8a8fe4b19bc3b330c3" guid="{ce5a036f-5c93-40e7-8adf-2613631008ab}" incarnation="2"> + # <Service name="MyVMRoleService" guid="{00000000-0000-0000-0000-000000000000}" /> + # <ServiceInstance name="db00a7755a5e4e8a8fe4b19bc3b330c3.1" guid="{d113f4d7-9ead-4e73-b715-b724b5b7842c}" /> + # </Deployment> + # <Incarnation number="1" instance="MachineRole_IN_0" guid="{a0faca35-52e5-4ec7-8fd1-63d2bc107d9b}" /> + # <Role guid="{73d95f1c-6472-e58e-7a1a-523554e11d46}" name="MachineRole" hostingEnvironmentVersion="1" software="" softwareType="ApplicationPackage" entryPoint="" parameters="" settleTimeSeconds="10" /> + # <HostingEnvironmentSettings name="full" Runtime="rd_fabric_stable.110217-1402.RuntimePackage_1.0.0.8.zip"> + # <CAS mode="full" /> + # <PrivilegeLevel mode="max" /> + # <AdditionalProperties><CgiHandlers></CgiHandlers></AdditionalProperties> + # </HostingEnvironmentSettings> + # <ApplicationSettings> + # <Setting name="__ModelData" value="<m role="MachineRole" xmlns="urn:azure:m:v1"><r name="MachineRole"><e name="a" /><e name="b" /><e name="Microsoft.WindowsAzure.Plugins.RemoteAccess.Rdp" /><e name="Microsoft.WindowsAzure.Plugins.RemoteForwarder.RdpInput" /></r></m>" /> + # <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="DefaultEndpointsProtocol=http;AccountName=osimages;AccountKey=DNZQ..." /> + # <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountEncryptedPassword" value="MIIBnQYJKoZIhvcN..." /> + # <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountExpiration" value="2022-07-23T23:59:59.0000000-07:00" /> + # <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountUsername" value="test" /> + # <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.Enabled" value="true" /> + # <Setting name="Microsoft.WindowsAzure.Plugins.RemoteForwarder.Enabled" value="true" /> + # <Setting name="Certificate|Microsoft.WindowsAzure.Plugins.RemoteAccess.PasswordEncryption" value="sha1:C093FA5CD3AAE057CB7C4E04532B2E16E07C26CA" /> + # </ApplicationSettings> + # <ResourceReferences> + # <Resource name="DiagnosticStore" type="directory" request="Microsoft.Cis.Fabric.Controller.Descriptions.ServiceDescription.Data.Policy" sticky="true" size="1" path="db00a7755a5e4e8a8fe4b19bc3b330c3.MachineRole.DiagnosticStore\" disableQuota="false" /> + # </ResourceReferences> + # </HostingEnvironmentConfig> + # def __init__(self): self.reinitialize() def reinitialize(self): + """ + Reset Members. + """ self.StoredCertificates = None self.Deployment = None self.Incarnation = None @@ -947,6 +2613,9 @@ class HostingEnvironmentConfig(object): self.ResourceReferences = None def Parse(self, xmlText): + """ + Parse and create HostingEnvironmentConfig.xml. + """ self.reinitialize() SetFileContents("HostingEnvironmentConfig.xml", xmlText) dom = xml.dom.minidom.parseString(xmlText) @@ -964,6 +2633,9 @@ class HostingEnvironmentConfig(object): return self def DecryptPassword(self, e): + """ + Return decrypted password. + """ SetFileContents("password.p7m", "MIME-Version: 1.0\n" + "Content-Disposition: attachment; filename=\"password.p7m\"\n" @@ -973,55 +2645,14 @@ class HostingEnvironmentConfig(object): return RunGetOutput(Openssl + " cms -decrypt -in password.p7m -inkey Certificates.pem -recip Certificates.pem")[1] def ActivateResourceDisk(self): - global DiskActivated - if IsWindows(): - DiskActivated = True - Log("Skipping ActivateResourceDisk on Windows") - return - format = Config.get("ResourceDisk.Format") - if format == None or format.lower().startswith("n"): - DiskActivated = True - return - device = DeviceForIdePort(1) - if device == None: - Error("ActivateResourceDisk: Unable to detect disk topology.") - return - device = "/dev/" + device - for entry in RunGetOutput("mount")[1].split(): - if entry.startswith(device + "1"): - Log("ActivateResourceDisk: " + device + "1 is already mounted.") - DiskActivated = True - return - mountpoint = Config.get("ResourceDisk.MountPoint") - if mountpoint == None: - mountpoint = "/mnt/resource" - CreateDir(mountpoint, "root", 0755) - fs = Config.get("ResourceDisk.Filesystem") - if fs == None: - fs = "ext3" - if RunGetOutput("sfdisk -q -c " + device + " 1")[1].rstrip() == "7" and fs != "ntfs": - Run("sfdisk -c " + device + " 1 83") - Run("mkfs." + fs + " " + device + "1") - if Run("mount " + device + "1 " + mountpoint): - Error("ActivateResourceDisk: Failed to mount resource disk (" + device + "1).") - return - Log("Resource disk (" + device + "1) is mounted at " + mountpoint + " with fstype " + fs) - DiskActivated = True - swap = Config.get("ResourceDisk.EnableSwap") - if swap == None or swap.lower().startswith("n"): - return - sizeKB = int(Config.get("ResourceDisk.SwapSizeMB")) * 1024 - if os.path.isfile(mountpoint + "/swapfile") and os.path.getsize(mountpoint + "/swapfile") != (sizeKB * 1024): - os.remove(mountpoint + "/swapfile") - if not os.path.isfile(mountpoint + "/swapfile"): - Run("dd if=/dev/zero of=" + mountpoint + "/swapfile bs=1024 count=" + str(sizeKB)) - Run("mkswap " + mountpoint + "/swapfile") - if not Run("swapon " + mountpoint + "/swapfile"): - Log("Enabled " + str(sizeKB) + " KB of swap at " + mountpoint + "/swapfile") - else: - Error("ActivateResourceDisk: Failed to activate swap at " + mountpoint + "/swapfile") + return MyDistro.ActivateResourceDisk() def Process(self): + """ + Execute ActivateResourceDisk in separate thread. + Create the user account. + Launch ConfigurationConsumer if specified in the config. + """ if DiskActivated == False: diskThread = threading.Thread(target = self.ActivateResourceDisk) diskThread.start() @@ -1046,53 +2677,60 @@ class HostingEnvironmentConfig(object): else: Error("Not creating user account: " + User) for c in self.Certificates: - cname = c.getAttribute("name") csha1 = c.getAttribute("certificateId").split(':')[1].upper() - cpath = c.getAttribute("storeName") - clevel = c.getAttribute("configurationLevel") if os.path.isfile(csha1 + ".prv"): Log("Private key with thumbprint: " + csha1 + " was retrieved.") if os.path.isfile(csha1 + ".crt"): Log("Public cert with thumbprint: " + csha1 + " was retrieved.") program = Config.get("Role.ConfigurationConsumer") if program != None: - Children.append(subprocess.Popen([program, LibDir + "/HostingEnvironmentConfig.xml"])) + try: + Children.append(subprocess.Popen([program, LibDir + "/HostingEnvironmentConfig.xml"])) + except OSError, e : + ErrorWithPrefix('HostingEnvironmentConfig.Process','Exception: '+ str(e) +' occured launching ' + program ) class GoalState(Util): -# -# <GoalState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="goalstate10.xsd"> -# <Version>2010-12-15</Version> -# <Incarnation>1</Incarnation> -# <Machine> -# <ExpectedState>Started</ExpectedState> -# <LBProbePorts> -# <Port>16001</Port> -# </LBProbePorts> -# </Machine> -# <Container> -# <ContainerId>c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2</ContainerId> -# <RoleInstanceList> -# <RoleInstance> -# <InstanceId>MachineRole_IN_0</InstanceId> -# <State>Started</State> -# <Configuration> -# <HostingEnvironmentConfig>http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=config&type=hostingEnvironmentConfig&incarnation=1</HostingEnvironmentConfig> -# <SharedConfig>http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=config&type=sharedConfig&incarnation=1</SharedConfig> -# <Certificates>http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=certificates&incarnation=1</Certificates> -# </Configuration> -# </RoleInstance> -# </RoleInstanceList> -# </Container> -# </GoalState> -# -# There is only one Role for VM images. -# -# Of primary interest is: -# Machine/ExpectedState -- this is how shutdown is requested -# LBProbePorts -- an http server needs to run here -# We also note Container/ContainerID and RoleInstance/InstanceId to form the health report. -# And of course, Incarnation -# + """ + Primary container for all configuration except OvfXml. + Encapsulates http communication with endpoint server. + Initializes and populates: + self.HostingEnvironmentConfig + self.SharedConfig + self.Certificates + """ + # + # <GoalState xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="goalstate10.xsd"> + # <Version>2010-12-15</Version> + # <Incarnation>1</Incarnation> + # <Machine> + # <ExpectedState>Started</ExpectedState> + # <LBProbePorts> + # <Port>16001</Port> + # </LBProbePorts> + # </Machine> + # <Container> + # <ContainerId>c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2</ContainerId> + # <RoleInstanceList> + # <RoleInstance> + # <InstanceId>MachineRole_IN_0</InstanceId> + # <State>Started</State> + # <Configuration> + # <HostingEnvironmentConfig>http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=config&type=hostingEnvironmentConfig&incarnation=1</HostingEnvironmentConfig> + # <SharedConfig>http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=config&type=sharedConfig&incarnation=1</SharedConfig> + # <Certificates>http://10.115.153.40:80/machine/c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2/MachineRole%5FIN%5F0?comp=certificates&incarnation=1</Certificates> + # </Configuration> + # </RoleInstance> + # </RoleInstanceList> + # </Container> + # </GoalState> + # + # There is only one Role for VM images. + # + # Of primary interest is: + # LBProbePorts -- an http server needs to run here + # We also note Container/ContainerID and RoleInstance/InstanceId to form the health report. + # And of course, Incarnation + # def __init__(self, Agent): self.Agent = Agent self.Endpoint = Agent.Endpoint @@ -1101,7 +2739,7 @@ class GoalState(Util): def reinitialize(self): self.Incarnation = None # integer - self.ExpectedState = None # "Started" or "Stopped" + self.ExpectedState = None # "Started" self.HostingEnvironmentConfigUrl = None self.HostingEnvironmentConfigXml = None self.HostingEnvironmentConfig = None @@ -1116,6 +2754,13 @@ class GoalState(Util): self.LoadBalancerProbePort = None # integer, ?list of integers def Parse(self, xmlText): + """ + Request configuration data from endpoint server. + Parse and populate contained configuration objects. + Calls Certificates().Parse() + Calls SharedConfig().Parse + Calls HostingEnvironmentConfig().Parse + """ self.reinitialize() node = xml.dom.minidom.parseString(xmlText).childNodes[0] if node.localName != "GoalState": @@ -1185,42 +2830,51 @@ class GoalState(Util): return self def Process(self): + """ + Calls HostingEnvironmentConfig.Process() + """ self.HostingEnvironmentConfig.Process() - + class OvfEnv(object): -# -# <?xml version="1.0" encoding="utf-8"?> -# <Environment xmlns="http://schemas.dmtf.org/ovf/environment/1" xmlns:oe="http://schemas.dmtf.org/ovf/environment/1" xmlns:wa="http://schemas.microsoft.com/windowsazure" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> -# <wa:ProvisioningSection> -# <wa:Version>1.0</wa:Version> -# <LinuxProvisioningConfigurationSet xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> -# <ConfigurationSetType>LinuxProvisioningConfiguration</ConfigurationSetType> -# <HostName>HostName</HostName> -# <UserName>UserName</UserName> -# <UserPassword>UserPassword</UserPassword> -# <DisableSshPasswordAuthentication>false</DisableSshPasswordAuthentication> -# <SSH> -# <PublicKeys> -# <PublicKey> -# <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint> -# <Path>$HOME/UserName/.ssh/authorized_keys</Path> -# </PublicKey> -# </PublicKeys> -# <KeyPairs> -# <KeyPair> -# <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint> -# <Path>$HOME/UserName/.ssh/id_rsa</Path> -# </KeyPair> -# </KeyPairs> -# </SSH> -# </LinuxProvisioningConfigurationSet> -# </wa:ProvisioningSection> -# </Environment> -# + """ + Read, and process provisioning info from provisioning file OvfEnv.xml + """ + # + # <?xml version="1.0" encoding="utf-8"?> + # <Environment xmlns="http://schemas.dmtf.org/ovf/environment/1" xmlns:oe="http://schemas.dmtf.org/ovf/environment/1" xmlns:wa="http://schemas.microsoft.com/windowsazure" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + # <wa:ProvisioningSection> + # <wa:Version>1.0</wa:Version> + # <LinuxProvisioningConfigurationSet xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> + # <ConfigurationSetType>LinuxProvisioningConfiguration</ConfigurationSetType> + # <HostName>HostName</HostName> + # <UserName>UserName</UserName> + # <UserPassword>UserPassword</UserPassword> + # <DisableSshPasswordAuthentication>false</DisableSshPasswordAuthentication> + # <SSH> + # <PublicKeys> + # <PublicKey> + # <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint> + # <Path>$HOME/UserName/.ssh/authorized_keys</Path> + # </PublicKey> + # </PublicKeys> + # <KeyPairs> + # <KeyPair> + # <Fingerprint>EB0C0AB4B2D5FC35F2F0658D19F44C8283E2DD62</Fingerprint> + # <Path>$HOME/UserName/.ssh/id_rsa</Path> + # </KeyPair> + # </KeyPairs> + # </SSH> + # </LinuxProvisioningConfigurationSet> + # </wa:ProvisioningSection> + # </Environment> + # def __init__(self): self.reinitialize() def reinitialize(self): + """ + Reset members. + """ self.WaNs = "http://schemas.microsoft.com/windowsazure" self.OvfNs = "http://schemas.dmtf.org/ovf/environment/1" self.MajorVersion = 1 @@ -1229,11 +2883,16 @@ class OvfEnv(object): self.AdminPassword = None self.UserName = None self.UserPassword = None + self.CustomData = None self.DisableSshPasswordAuthentication = True self.SshPublicKeys = [] self.SshKeyPairs = [] def Parse(self, xmlText): + """ + Parse xml tree, retreiving user and ssh key information. + Return self. + """ self.reinitialize() dom = xml.dom.minidom.parseString(xmlText) if len(dom.getElementsByTagNameNS(self.OvfNs, "Environment")) != 1: @@ -1264,6 +2923,18 @@ class OvfEnv(object): self.UserPassword = GetNodeTextData(section.getElementsByTagNameNS(self.WaNs, "UserPassword")[0]) except: pass + CDSection=None + try: + CDSection=section.getElementsByTagNameNS(self.WaNs, "CustomData") + if len(CDSection) > 0 : + self.CustomData=GetNodeTextData(CDSection[0]) + if len(self.CustomData)>0: + SetFileContents(LibDir + '/CustomData',self.CustomData) + Log('Wrote ' + LibDir + '/CustomData') + else : + Error('<CustomData> contains no data!') + except Exception, e: + Error( str(e)+' occured creating ' + LibDir + '/CustomData') disableSshPass = section.getElementsByTagNameNS(self.WaNs, "DisableSshPasswordAuthentication") if len(disableSshPass) != 0: self.DisableSshPasswordAuthentication = (GetNodeTextData(disableSshPass[0]).lower() == "true") @@ -1288,7 +2959,11 @@ class OvfEnv(object): return self def PrepareDir(self, filepath): - home = GetHome() + """ + Create home dir for self.UserName + Change owner and return path. + """ + home = MyDistro.GetHome() # Expand HOME variable if present in path path = os.path.normpath(filepath.replace("$HOME", home)) if (path.startswith("/") == False) or (path.endswith("/") == True): @@ -1301,6 +2976,9 @@ class OvfEnv(object): return path def NumberToBytes(self, i): + """ + Pack number into bytes. Retun as string. + """ result = [] while i: result.append(chr(i & 0xFF)) @@ -1309,6 +2987,9 @@ class OvfEnv(object): return ''.join(result) def BitsToString(self, a): + """ + Return string representation of bits in a. + """ index=7 s = "" c = 0 @@ -1322,6 +3003,9 @@ class OvfEnv(object): return s def OpensslToSsh(self, file): + """ + Return base-64 encoded key appropriate for ssh. + """ from pyasn1.codec.der import decoder as der_decoder try: f = open(file).read().replace('\n','').split("KEY-----")[1].split('-')[0] @@ -1342,6 +3026,13 @@ class OvfEnv(object): return "ssh-rsa " + base64.b64encode(keydata) + "\n" def Process(self): + """ + Process all certificate and key info. + DisableSshPasswordAuthentication if configured. + CreateAccount(user) + Wait for WaAgent.EnvMonitor.IsHostnamePublished(). + Restart ssh service. + """ error = None if self.ComputerName == None : return "Error: Hostname missing" @@ -1355,13 +3046,13 @@ class OvfEnv(object): GetFileContents(filepath).split('\n'))) + "PasswordAuthentication no\nChallengeResponseAuthentication no\n") Log("Disabled SSH password-based authentication methods.") if self.AdminPassword != None: - RunSendStdin("chpasswd",("root:" + self.AdminPassword + "\n")) + MyDistro.changePass('root',self.AdminPassword) if self.UserName != None: - error = CreateAccount(self.UserName, self.UserPassword, None, None) - sel = RunGetOutput("getenforce",chk_err=False)[1].startswith("Enforcing") - if sel == True and IsRedHat(): - Run("setenforce 0") - home = GetHome() + error = MyDistro.CreateAccount(self.UserName, self.UserPassword, None, None) + sel = MyDistro.isSelinuxRunning() + if sel : + MyDistro.setSelinuxEnforce(0) + home = MyDistro.GetHome() for pkey in self.SshPublicKeys: if not os.path.isfile(pkey[0] + ".crt"): Error("PublicKey not found: " + pkey[0]) @@ -1373,20 +3064,9 @@ class OvfEnv(object): error = "Invalid path for public key (0x03)." continue Run(Openssl + " x509 -in " + pkey[0] + ".crt -noout -pubkey > " + pkey[0] + ".pub") - if IsRedHat(): - Run("chcon unconfined_u:object_r:ssh_home_t:s0 " + pkey[0] + ".pub") - if IsUbuntu(): - # Only supported in new SSH releases - Run("ssh-keygen -i -m PKCS8 -f " + pkey[0] + ".pub >> " + path) - else: - SshPubKey = self.OpensslToSsh(pkey[0] + ".pub") - if SshPubKey != None: - AppendFileContents(path, SshPubKey) - else: - Error("Failed: " + pkey[0] + ".crt -> " + path) - error = "Failed to deploy public key (0x04)." - if IsRedHat(): - Run("chcon unconfined_u:object_r:ssh_home_t:s0 " + path) + MyDistro.setSelinuxContext(pkey[0] + '.pub','unconfined_u:object_r:ssh_home_t:s0') + MyDistro.sshDeployPublicKey(pkey[0] + '.pub',path) + MyDistro.setSelinuxContext(path,'unconfined_u:object_r:ssh_home_t:s0') if path.startswith(os.path.normpath(home + "/" + self.UserName + "/")): ChangeOwner(path, self.UserName) for keyp in self.SshKeyPairs: @@ -1402,47 +3082,23 @@ class OvfEnv(object): SetFileContents(path, GetFileContents(keyp[0] + ".prv")) os.chmod(path, 0600) Run("ssh-keygen -y -f " + keyp[0] + ".prv > " + path + ".pub") - if IsRedHat(): - Run("chcon unconfined_u:object_r:ssh_home_t:s0 " + path) - Run("chcon unconfined_u:object_r:ssh_home_t:s0 " + path + ".pub") + MyDistro.setSelinuxContext(path,'unconfined_u:object_r:ssh_home_t:s0') + MyDistro.setSelinuxContext(path + '.pub','unconfined_u:object_r:ssh_home_t:s0') if path.startswith(os.path.normpath(home + "/" + self.UserName + "/")): ChangeOwner(path, self.UserName) ChangeOwner(path + ".pub", self.UserName) - if sel == True and IsRedHat(): - Run("setenforce 1") - while not WaAgent.EnvMonitor.IsNamePublished(): + if sel : + MyDistro.setSelinuxEnforce(1) + while not WaAgent.EnvMonitor.IsHostnamePublished(): time.sleep(1) - ReloadSshd() + MyDistro.restartSshService() return error -def UpdateAndPublishHostNameCommon(name): - # RedHat - if IsRedHat(): - filepath = "/etc/sysconfig/network" - if os.path.isfile(filepath): - ReplaceFileContentsAtomic(filepath, "HOSTNAME=" + name + "\n" - + "\n".join(filter(lambda a: not a.startswith("HOSTNAME"), GetFileContents(filepath).split('\n')))) - - for ethernetInterface in PossibleEthernetInterfaces: - filepath = "/etc/sysconfig/network-scripts/ifcfg-" + ethernetInterface - if os.path.isfile(filepath): - ReplaceFileContentsAtomic(filepath, "DHCP_HOSTNAME=" + name + "\n" - + "\n".join(filter(lambda a: not a.startswith("DHCP_HOSTNAME"), GetFileContents(filepath).split('\n')))) - - # Debian - if IsDebian(): - SetFileContents("/etc/hostname", name) - - for filepath in EtcDhcpClientConfFiles: - if os.path.isfile(filepath): - ReplaceFileContentsAtomic(filepath, "send host-name \"" + name + "\";\n" - + "\n".join(filter(lambda a: not a.startswith("send host-name"), GetFileContents(filepath).split('\n')))) - - # Suse - if IsSuse(): - SetFileContents("/etc/HOSTNAME", name) - class Agent(Util): + """ + Primary object container for the provisioning process. + + """ def __init__(self): self.GoalState = None self.Endpoint = None @@ -1454,6 +3110,10 @@ class Agent(Util): self.DhcpResponse = None def CheckVersions(self): + """ + Query endpoint server for wire protocol version. + Fail if our desired protocol version is not seen. + """ #<?xml version="1.0" encoding="utf-8"?> #<Versions> # <Preferred> @@ -1480,34 +3140,50 @@ class Agent(Util): protocolVersionSeen = True if a.nodeType == node.ELEMENT_NODE and a.localName == "Preferred": v = GetNodeTextData(a.getElementsByTagName("Version")[0]) - LogIfVerbose("Fabric preferred wire protocol version: " + v) - if ProtocolVersion < v: - Warn("Newer wire protocol version detected. Please consider updating waagent.") + Log("Fabric preferred wire protocol version: " + v) if not protocolVersionSeen: Warn("Agent supported wire protocol version: " + ProtocolVersion + " was not advertised by Fabric.") - ProtocolVersion = "2011-08-31" - Log("Negotiated wire protocol version: " + ProtocolVersion) + else: + Log("Negotiated wire protocol version: " + ProtocolVersion) return True def Unpack(self, buffer, offset, range): + """ + Unpack bytes into python values. + """ result = 0 for i in range: result = (result << 8) | Ord(buffer[offset + i]) return result def UnpackLittleEndian(self, buffer, offset, length): - return self.Unpack(buffer, offset, range(length - 1, -1, -1)) + """ + Unpack little endian bytes into python values. + """ + return self.Unpack(buffer, offset, list(range(length - 1, -1, -1))) def UnpackBigEndian(self, buffer, offset, length): - return self.Unpack(buffer, offset, range(0, length)) + """ + Unpack big endian bytes into python values. + """ + return self.Unpack(buffer, offset, list(range(0, length))) def HexDump3(self, buffer, offset, length): + """ + Dump range of buffer in formatted hex. + """ return ''.join(['%02X' % Ord(char) for char in buffer[offset:offset + length]]) def HexDump2(self, buffer): + """ + Dump buffer in formatted hex. + """ return self.HexDump3(buffer, 0, len(buffer)) def BuildDhcpRequest(self): + """ + Build DHCP request string. + """ # # typedef struct _DHCP { # UINT8 Opcode; /* op: BOOTREQUEST or BOOTREPLY */ @@ -1540,7 +3216,7 @@ class Agent(Util): sendData = [0] * 244 transactionID = os.urandom(4) - macAddress = GetMacAddress() + macAddress = MyDistro.GetMacAddress() # Opcode = 1 # HardwareAddressType = 1 (ethernet/MAC) @@ -1565,20 +3241,31 @@ class Agent(Util): # End = 255 DHCP_END for a in range(0, 8): sendData[0xEC + a] = [99, 130, 83, 99, 53, 1, 1, 255][a] - return array.array("c", map(chr, sendData)) + return array.array("B", sendData) def IntegerToIpAddressV4String(self, a): + """ + Build DHCP request string. + """ return "%u.%u.%u.%u" % ((a >> 24) & 0xFF, (a >> 16) & 0xFF, (a >> 8) & 0xFF, a & 0xFF) def RouteAdd(self, net, mask, gateway): - if IsWindows(): - return + """ + Add specified route using /sbin/route add -net. + """ net = self.IntegerToIpAddressV4String(net) mask = self.IntegerToIpAddressV4String(mask) gateway = self.IntegerToIpAddressV4String(gateway) - Run("/sbin/route add -net " + net + " netmask " + mask + " gw " + gateway,chk_err=False) # We supress error logging on error. + Run("/sbin/route add -net " + net + " netmask " + mask + " gw " + gateway,chk_err=False) def HandleDhcpResponse(self, sendData, receiveBuffer): + """ + Parse DHCP response: + Set default gateway. + Set default routes. + Retrieve endpoint server. + Returns endpoint server or None on error. + """ LogIfVerbose("HandleDhcpResponse") bytesReceived = len(receiveBuffer) if bytesReceived < 0xF6: @@ -1592,7 +3279,7 @@ class Agent(Util): # cookie should never mismatch # transactionId and MAC address may mismatch if we see a response meant from another machine - for offsets in [range(4, 4 + 4), range(0x1C, 0x1C + 6), range(0xEC, 0xEC + 4)]: + for offsets in [list(range(4, 4 + 4)), list(range(0x1C, 0x1C + 6)), list(range(0xEC, 0xEC + 4))]: for offset in offsets: sentByte = Ord(sendData[offset]) receivedByte = Ord(receiveBuffer[offset]) @@ -1666,17 +3353,16 @@ class Agent(Util): return endpoint def DoDhcpWork(self): - # - # Discover the wire server via DHCP option 245. - # And workaround incompatibility with Windows Azure DHCP servers. - # + """ + Discover the wire server via DHCP option 245. + And workaround incompatibility with Windows Azure DHCP servers. + """ ShortSleep = False # Sleep 1 second before retrying DHCP queries. ifname=None - if not IsWindows(): - Run("iptables -D INPUT -p udp --dport 68 -j ACCEPT",chk_err=False) # We supress error logging on error. - Run("iptables -I INPUT -p udp --dport 68 -j ACCEPT",chk_err=False) # We supress error logging on error. + Run("iptables -D INPUT -p udp --dport 68 -j ACCEPT",chk_err=False) # We supress error logging on error. + Run("iptables -I INPUT -p udp --dport 68 -j ACCEPT",chk_err=False) # We supress error logging on error. - sleepDurations = [0, 5, 10, 30, 60, 60, 60, 60] + sleepDurations = [0, 10, 30, 60, 60] maxRetry = len(sleepDurations) lastTry = (maxRetry - 1) for retry in range(0, maxRetry): @@ -1699,12 +3385,7 @@ class Agent(Util): if missingDefaultRoute: # This is required because sending after binding to 0.0.0.0 fails with # network unreachable when the default gateway is not set up. - for i in PossibleEthernetInterfaces: - try: - if Linux_ioctl_GetIpv4Address(i): - ifname=i - except IOError, e: - pass + ifname=MyDistro.GetInterfaceName() Log("DoDhcpWork: Missing default route - adding broadcast route for DHCP.") Run("route add 255.255.255.255 dev " + ifname,chk_err=False) # We supress error logging on error. sock.bind(("0.0.0.0", 68)) @@ -1737,18 +3418,28 @@ class Agent(Util): return None def UpdateAndPublishHostName(self, name): - # Set hostname locally and publish to iDNS + """ + Set hostname locally and publish to iDNS + """ Log("Setting host name: " + name) - UpdateAndPublishHostNameCommon(name) - for ethernetInterface in PossibleEthernetInterfaces: - Run("ifdown " + ethernetInterface + " && ifup " + ethernetInterface,chk_err=False) # We supress error logging on error. + MyDistro.publishHostname(name) + ethernetInterface = MyDistro.GetInterfaceName() + Run("ifdown " + ethernetInterface + " && ifup " + ethernetInterface) self.RestoreRoutes() def RestoreRoutes(self): + """ + If there is a DHCP response, then call HandleDhcpResponse. + """ if self.SendData != None and self.DhcpResponse != None: self.HandleDhcpResponse(self.SendData, self.DhcpResponse) def UpdateGoalState(self): + """ + Retreive goal state information from endpoint server. + Parse xml and initialize Agent.GoalState object. + Return object or None on error. + """ goalStateXml = None maxRetry = 9 log = NoLog @@ -1768,6 +3459,11 @@ class Agent(Util): return self.GoalState def ReportReady(self): + """ + Send health report 'Ready' to server. + This signals the fabric that our provosion is completed, + and the host is ready for operation. + """ counter = (self.HealthReportCounter + 1) % 1000000 self.HealthReportCounter = counter healthReport = ("<?xml version=\"1.0\" encoding=\"utf-8\"?><Health xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><GoalStateIncarnation>" @@ -1783,6 +3479,10 @@ class Agent(Util): return None def ReportNotReady(self, status, desc): + """ + Send health report 'Provisioning' to server. + This signals the fabric that our provosion is starting. + """ healthReport = ("<?xml version=\"1.0\" encoding=\"utf-8\"?><Health xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><GoalStateIncarnation>" + self.GoalState.Incarnation + "</GoalStateIncarnation><Container><ContainerId>" @@ -1798,6 +3498,9 @@ class Agent(Util): return None def ReportRoleProperties(self, thumbprint): + """ + Send roleProperties and thumbprint to server. + """ roleProperties = ("<?xml version=\"1.0\" encoding=\"utf-8\"?><RoleProperties><Container>" + "<ContainerId>" + self.GoalState.ContainerId + "</ContainerId>" + "<RoleInstances><RoleInstance>" @@ -1809,11 +3512,17 @@ class Agent(Util): return a def LoadBalancerProbeServer_Shutdown(self): + """ + Shutdown the LoadBalancerProbeServer. + """ if self.LoadBalancerProbeServer != None: self.LoadBalancerProbeServer.shutdown() self.LoadBalancerProbeServer = None def GenerateTransportCert(self): + """ + Create ssl certificate for https communication with endpoint server. + """ Run(Openssl + " req -x509 -nodes -subj /CN=LinuxTransport -days 32768 -newkey rsa:2048 -keyout TransportPrivate.pem -out TransportCert.pem") cert = "" for line in GetFileContents("TransportCert.pem").split('\n'): @@ -1821,10 +3530,121 @@ class Agent(Util): cert += line.rstrip() return cert + def DoVmmStartup(self): + """ + Spawn the VMM startup script. + """ + Log("Starting Microsoft System Center VMM Initialization Process") + pid = subprocess.Popen(["/bin/bash","/mnt/cdrom/secure/"+VMM_STARTUP_SCRIPT_NAME,"-p /mnt/cdrom/secure/ "]).pid + time.sleep(5) + sys.exit(0) + + def TryUnloadAtapiix(self): + """ + If global modloaded is True, then we loaded the ata_piix kernel module, unload it. + """ + if modloaded: + Run("rmmod ata_piix.ko",chk_err=False) + Log("Unloaded ata_piix.ko driver for ATAPI CD-ROM") + + def TryLoadAtapiix(self): + """ + Load the ata_piix kernel module if it exists. + If successful, set global modloaded to True. + If unable to load module leave modloaded False. + """ + global modloaded + modloaded=False + retcode,krn=RunGetOutput('uname -r') + krn_pth='/lib/modules/'+krn.strip('\n')+'/kernel/drivers/ata/ata_piix.ko' + if Run("lsmod | grep ata_piix",chk_err=False) == 0 : + Log("Module " + krn_pth + " driver for ATAPI CD-ROM is already present.") + return 0 + if retcode: + Error("Unable to provision: Failed to call uname -r") + return "Unable to provision: Failed to call uname" + if os.path.isfile(krn_pth): + retcode,output=RunGetOutput("insmod " + krn_pth,chk_err=False) + else: + Log("Module " + krn_pth + " driver for ATAPI CD-ROM does not exist.") + return 1 + if retcode != 0: + Error('Error calling insmod for '+ krn_pth + ' driver for ATAPI CD-ROM') + return retcode + time.sleep(1) + # check 3 times if the mod is loaded + for i in range(3): + if Run('lsmod | grep ata_piix'): + continue + else : + modloaded=True + break + if not modloaded: + Error('Unable to load '+ krn_pth + ' driver for ATAPI CD-ROM') + return 1 + + Log("Loaded " + krn_pth + " driver for ATAPI CD-ROM") + + # we have succeeded loading the ata_piix mod if it can be done. + + def SearchForVMMStartup(self): + """ + Search for a DVD/CDROM containing VMM's VMM_CONFIG_FILE_NAME. + Call TryLoadAtapiix in case we must load the ata_piix module first. + + If VMM_CONFIG_FILE_NAME is found, call DoVmmStartup. + Else, return to Azure Provisioning process. + """ + self.TryLoadAtapiix() + if os.path.exists('/mnt/cdrom/secure') == False: + CreateDir("/mnt/cdrom/secure", "root", 0700) + mounted=False + for dvds in [re.match(r'(sr[0-9]|hd[c-z]|cdrom[0-9]|cd[0-9]?)',x) for x in os.listdir('/dev/')]: + if dvds == None: + continue + dvd = '/dev/'+dvds.group(0) + if Run("LC_ALL=C fdisk -l " + dvd + " | grep Disk",chk_err=False): + continue # Not mountable + else: + for retry in range(1,6): + retcode,output=RunGetOutput("mount -v " + dvd + " /mnt/cdrom/secure") + Log(output[:-1]) + if retcode == 0: + Log("mount succeeded on attempt #" + str(retry) ) + mounted=True + break + if 'is already mounted on /mnt/cdrom/secure' in output: + Log("Device " + dvd + " is already mounted on /mnt/cdrom/secure." + str(retry) ) + mounted=True + break + Log("mount failed on attempt #" + str(retry) ) + Log("mount loop sleeping 5...") + time.sleep(5) + if not mounted: + # unable to mount + continue + if not os.path.isfile("/mnt/cdrom/secure/"+VMM_CONFIG_FILE_NAME): + #nope - mount the next drive + if mounted: + Run("umount "+dvd,chk_err=False) + mounted=False + continue + else : # it is the vmm startup + self.DoVmmStartup() + + Log("VMM Init script not found. Provisioning for Azure") + return + def Provision(self): - if IsWindows(): - Log("Skipping Provision on Windows") - return + """ + Responible for: + Regenerate ssh keys, + Mount, read, and parse ovfenv.xml from provisioning dvd rom + Process the ovfenv.xml info + Call ReportRoleProperties + If configured, delete root password. + Return None on success, error string on error. + """ enabled = Config.get("Provisioning.Enabled") if enabled != None and enabled.lower().startswith("n"): return @@ -1836,56 +3656,47 @@ class Agent(Util): if regenerateKeys == None or regenerateKeys.lower().startswith("y"): Run("rm -f /etc/ssh/ssh_host_*key*") Run("ssh-keygen -N '' -t " + type + " -f /etc/ssh/ssh_host_" + type + "_key") - ReloadSshd() - SetFileContents(LibDir + "/provisioned", "") - dvd = "/dev/hdc" - if os.path.exists("/dev/sr0"): - dvd = "/dev/sr0" - modloaded=False - if Run("fdisk -l " + dvd + " | grep Disk",chk_err=False): - # Is it possible to load a module for ata_piix? - retcode,krn=RunGetOutput('uname -r') - if retcode: - Error("Unable to provision: Failed to call uname -a") - return "Unable to provision: Failed to mount DVD." - krn_pth='/lib/modules/'+krn.strip('\n')+'/kernel/drivers/ata/ata_piix.ko' - if not os.path.isfile(krn_pth): - Error("Unable to provision: Failed to locate ata_piix.ko") - return "Unable to provision: Failed to mount DVD." - retcode,output=RunGetOutput('insmod ' + krn_pth) - if retcode: - Error("Unable to provision: Failed to insmod " + krn+pth) - return "Failed to retrieve provisioning data (0x01)." - modloaded=True - Log("Provision: Loaded " + krn_pth + " driver for ATAPI CD-ROM") - # we have succeeded loading the ata_piix mod + MyDistro.restartSshService() + #SetFileContents(LibDir + "/provisioned", "") + for dvds in [re.match(r'(sr[0-9]|hd[c-z]|cdrom[0-9]|cd[0-9]?)',x) for x in os.listdir('/dev/')]: + if dvds == None: + continue + dvd = '/dev/'+dvds.group(0) + if MyDistro.dvdHasMedia(dvd) is False : + out=MyDistro.load_ata_piix() + if out: + return out for i in range(10): # we may have to wait - if os.path.exists("/dev/sr0"): - dvd = "/dev/sr0" + if os.path.exists(dvd): break Log("Waiting for DVD - sleeping 1 - "+str(i+1)+" try...") time.sleep(1) - CreateDir("/mnt/cdrom/secure", "root", 0700) - #begin mount loop - ten tries - 5 sec wait between - for retry in range(1,11): - retcode,output=RunGetOutput("mount -v " + dvd + " /mnt/cdrom/secure") + if os.path.exists('/mnt/cdrom/secure') == False: + CreateDir("/mnt/cdrom/secure", "root", 0700) + #begin mount loop - 5 tries - 5 sec wait between + for retry in range(1,6): + location='/mnt/cdrom/secure' + retcode,output=MyDistro.mountDVD(dvd,location) Log(output[:-1]) - if retcode: - Log("mount failed on attempt #" + str(retry) ) - else: + if retcode == 0: Log("mount succeeded on attempt #" + str(retry) ) break + if 'is already mounted on /mnt/cdrom/secure' in output: + Log("Device " + dvd + " is already mounted on /mnt/cdrom/secure." + str(retry) ) + break + Log("mount failed on attempt #" + str(retry) ) Log("mount loop sleeping 5...") time.sleep(5) - if not os.path.isfile("/mnt/cdrom/secure/ovf-env.xml"): Error("Unable to provision: Missing ovf-env.xml on DVD.") return "Failed to retrieve provisioning data (0x02)." - ovfxml = GetFileContents("/mnt/cdrom/secure/ovf-env.xml") + ovfxml = (GetFileContents(u"/mnt/cdrom/secure/ovf-env.xml",asbin=False)) # use unicode here to ensure correct codec gets used. + if ord(ovfxml[0]) > 128 and ord(ovfxml[1]) > 128 and ord(ovfxml[2]) > 128 : + ovfxml = ovfxml[3:] # BOM is not stripped. First three bytes are > 128 and not unicode chars so we ignore them. + ovfxml=ovfxml.strip(chr(0x00)) # we may have NULLs. SetFileContents("ovf-env.xml", re.sub("<UserPassword>.*?<", "<UserPassword>*<", ovfxml)) - Run("umount /mnt/cdrom/secure") - if modloaded: - Run('rmmod ' + krn_pth) + Run("umount " + dvd,chk_err=False) + MyDistro.unload_ata_piix() error = None if ovfxml != None: Log("Provisioning image using OVF settings in the DVD.") @@ -1900,21 +3711,42 @@ class Agent(Util): self.ReportRoleProperties(fingerprint) delRootPass = Config.get("Provisioning.DeleteRootPassword") if delRootPass != None and delRootPass.lower().startswith("y"): - DeleteRootPassword() + MyDistro.deleteRootPassword() Log("Provisioning image completed.") return error def Run(self): - if IsLinux(): - SetFileContents("/var/run/waagent.pid", str(os.getpid()) + "\n") - - if GetIpv4Address() == None: + """ + Called by 'waagent -daemon.' + Main loop to process the goal state. State is posted every 25 seconds + when provisioning has been completed. + + Search for VMM enviroment, start VMM script if found. + Perform DHCP and endpoint server discovery by calling DoDhcpWork(). + Check wire protocol versions. + Set SCSI timeout on root device. + Call GenerateTransportCert() to create ssl certs for server communication. + Call UpdateGoalState(). + If not provisioned, call ReportNotReady("Provisioning", "Starting") + Call Provision(), set global provisioned = True if successful. + Call goalState.Process() + Start LBProbeServer if indicated in waagent.conf. + Start the StateConsumer if indicated in waagent.conf. + ReportReady if provisioning is complete. + If provisioning failed, call ReportNotReady("ProvisioningFailed", provisionError) + """ + SetFileContents("/var/run/waagent.pid", str(os.getpid()) + "\n") + + # Determine if we are in VMM. Spawn VMM_STARTUP_SCRIPT_NAME if found. + self.SearchForVMMStartup() + + if MyDistro.GetIpv4Address() == None: Log("Waiting for network.") - while(GetIpv4Address() == None): + while(MyDistro.GetIpv4Address() == None): time.sleep(10) - Log("IPv4 address: " + GetIpv4Address()) - Log("MAC address: " + ":".join(["%02X" % Ord(a) for a in GetMacAddress()])) + Log("IPv4 address: " + MyDistro.GetIpv4Address()) + Log("MAC address: " + ":".join(["%02X" % Ord(a) for a in MyDistro.GetMacAddress()])) # Consume Entropy in ACPI table provided by Hyper-V try: @@ -1965,7 +3797,8 @@ class Agent(Util): while True: if (goalState == None) or (incarnation == None) or (goalState.Incarnation != incarnation): goalState = self.UpdateGoalState() - + if goalState == None : + continue if provisioned == False: self.ReportNotReady("Provisioning", "Starting") @@ -1973,7 +3806,9 @@ class Agent(Util): if provisioned == False: provisionError = self.Provision() - provisioned = True + if provisionError == None : + provisioned = True + SetFileContents(LibDir + "/provisioned", "") # # only one port supported @@ -1991,19 +3826,12 @@ class Agent(Util): Log("Unable to create LBProbeResponder.") if program != None and DiskActivated == True: - Children.append(subprocess.Popen([program, "Ready"])) + try: + Children.append(subprocess.Popen([program, "Ready"])) + except OSError, e : + ErrorWithPrefix('SharedConfig.Parse','Exception: '+ str(e) +' occured launching ' + program ) program = None - if goalState.ExpectedState == "Stopped": - program = Config.get("Role.StateConsumer") - if program != None: - Run(program + " Shutdown") - self.EnvMonitor.ShutdownService() - self.LoadBalancerProbeServer_Shutdown() - command = ["/sbin/shutdown -hP now", "shutdown /s /t 5"][IsWindows()] - Run(command) - return - sleepToReduceAccessDenied = 3 time.sleep(sleepToReduceAccessDenied) if provisionError != None: @@ -2011,244 +3839,7 @@ class Agent(Util): else: incarnation = self.ReportReady() time.sleep(25 - sleepToReduceAccessDenied) - -Init_Suse = """\ -#! /bin/sh - -### BEGIN INIT INFO -# Provides: WindowsAzureLinuxAgent -# Required-Start: $network sshd -# Required-Stop: $network sshd -# Default-Start: 3 5 -# Default-Stop: 0 1 2 6 -# Description: Start the WindowsAzureLinuxAgent -### END INIT INFO - -WAZD_BIN=/usr/sbin/waagent -test -x $WAZD_BIN || exit 5 - -case "$1" in - start) - echo "Starting WindowsAzureLinuxAgent" - ## Start daemon with startproc(8). If this fails - ## the echo return value is set appropriate. - - startproc -f $WAZD_BIN -daemon - exit $? - ;; - stop) - echo "Shutting down WindowsAzureLinuxAgent" - ## Stop daemon with killproc(8) and if this fails - ## set echo the echo return value. - - killproc -p /var/run/waagent.pid $WAZD_BIN - exit $? - ;; - try-restart) - ## Stop the service and if this succeeds (i.e. the - ## service was running before), start it again. - $0 status >/dev/null && $0 restart - ;; - restart) - ## Stop the service and regardless of whether it was - ## running or not, start it again. - $0 stop - $0 start - ;; - force-reload|reload) - ;; - status) - echo -n "Checking for service WindowsAzureLinuxAgent " - ## Check status with checkproc(8), if process is running - ## checkproc will return with exit status 0. - - checkproc -p $WAZD_PIDFILE $WAZD_BIN - exit $? - ;; - probe) - ;; - *) - echo "Usage: $0 {start|stop|status|try-restart|restart|force-reload|reload}" - exit 1 - ;; -esac -""" - -Init_RedHat = """\ -#!/bin/bash -# -# Init file for WindowsAzureLinuxAgent. -# -# chkconfig: 2345 60 80 -# description: WindowsAzureLinuxAgent -# - -# source function library -. /etc/rc.d/init.d/functions - -RETVAL=0 -FriendlyName="WindowsAzureLinuxAgent" -WAZD_BIN=/usr/sbin/waagent - -start() -{ - echo -n $"Starting $FriendlyName: " - $WAZD_BIN -daemon & -} - -stop() -{ - echo -n $"Stopping $FriendlyName: " - killproc -p /var/run/waagent.pid $WAZD_BIN - RETVAL=$? - echo - return $RETVAL -} - -case "$1" in - start) - start - ;; - stop) - stop - ;; - restart) - stop - start - ;; - reload) - ;; - report) - ;; - status) - status $WAZD_BIN - RETVAL=$? - ;; - *) - echo $"Usage: $0 {start|stop|restart|status}" - RETVAL=1 -esac -exit $RETVAL -""" - -Init_Ubuntu = """\ -#walinuxagent - start Windows Azure agent - -description "walinuxagent" -author "Ben Howard <ben.howard@canonical.com>" - -start on (filesystem and started rsyslog) - -pre-start script - - WALINUXAGENT_ENABLED=1 - [ -r /etc/default/walinuxagent ] && . /etc/default/walinuxagent - - if [ "$WALINUXAGENT_ENABLED" != "1" ]; then - exit 1 - fi - - if [ ! -x /usr/sbin/waagent ]; then - exit 1 - fi - - #Load the udf module - modprobe -b udf -end script - -exec /usr/sbin/waagent -daemon -""" - -Init_Debian = """\ -#!/bin/sh -### BEGIN INIT INFO -# Provides: WindowsAzureLinuxAgent -# Required-Start: $network $syslog -# Required-Stop: $network $syslog -# Should-Start: $network $syslog -# Should-Stop: $network $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: WindowsAzureLinuxAgent -# Description: WindowsAzureLinuxAgent -### END INIT INFO - -. /lib/lsb/init-functions - -OPTIONS="-daemon" -WAZD_BIN=/usr/sbin/waagent -WAZD_PID=/var/run/waagent.pid - -case "$1" in - start) - log_begin_msg "Starting WindowsAzureLinuxAgent..." - pid=$( pidofproc $WAZD_BIN ) - if [ -n "$pid" ] ; then - log_begin_msg "Already running." - log_end_msg 0 - exit 0 - fi - start-stop-daemon --start --quiet --oknodo --background --exec $WAZD_BIN -- $OPTIONS - log_end_msg $? - ;; - - stop) - log_begin_msg "Stopping WindowsAzureLinuxAgent..." - start-stop-daemon --stop --quiet --oknodo --pidfile $WAZD_PID - ret=$? - rm -f $WAZD_PID - log_end_msg $ret - ;; - force-reload) - $0 restart - ;; - restart) - $0 stop - $0 start - ;; - status) - status_of_proc $WAZD_BIN && exit 0 || exit $? - ;; - *) - log_success_msg "Usage: /etc/init.d/waagent {start|stop|force-reload|restart|status}" - exit 1 - ;; -esac - -exit 0 -""" - -WaagentConf = """\ -# -# Windows Azure Linux Agent Configuration -# - -Role.StateConsumer=None # Specified program is invoked with "Ready" or "Shutdown". - # Shutdown will be initiated only after the program returns. Windows Azure will - # power off the VM if shutdown is not completed within ?? minutes. -Role.ConfigurationConsumer=None # Specified program is invoked with XML file argument specifying role configuration. -Role.TopologyConsumer=None # Specified program is invoked with XML file argument specifying role topology. - -Provisioning.Enabled=y # -Provisioning.DeleteRootPassword=y # Password authentication for root account will be unavailable. -Provisioning.RegenerateSshHostKeyPair=y # Generate fresh host key pair. -Provisioning.SshHostKeyPairType=rsa # Supported values are "rsa", "dsa" and "ecdsa". -Provisioning.MonitorHostName=y # Monitor host name changes and publish changes via DHCP requests. - -ResourceDisk.Format=y # Format if unformatted. If 'n', resource disk will not be mounted. -ResourceDisk.Filesystem=ext4 # -ResourceDisk.MountPoint=/mnt/resource # -ResourceDisk.EnableSwap=n # Create and use swapfile on resource disk. -ResourceDisk.SwapSizeMB=0 # Size of the swapfile. - -LBProbeResponder=y # Respond to load balancer probes if requested by Windows Azure. - -Logs.Verbose=n # - -OS.RootDeviceScsiTimeout=300 # Root device timeout in seconds. -OS.OpensslPath=None # If "None", the system default version is used. -""" - + WaagentLogrotate = """\ /var/log/waagent.log { monthly @@ -2258,19 +3849,86 @@ WaagentLogrotate = """\ } """ -def AddToLinuxKernelCmdline(options): - if os.path.isfile("/boot/grub/menu.lst"): - Run("sed -i --follow-symlinks '/kernel/s|$| " + options + " |' /boot/grub/menu.lst") - filepath = "/etc/default/grub" - if os.path.isfile(filepath): - filecontents = GetFileContents(filepath).split('\n') - current = filter(lambda a: a.startswith("GRUB_CMDLINE_LINUX"), filecontents) - ReplaceFileContentsAtomic(filepath, - "\n".join(filter(lambda a: not a.startswith("GRUB_CMDLINE_LINUX"), filecontents)) - + current[0][:-1] + " " + options + "\"\n") - Run("update-grub") +def FindInLinuxKernelCmdline(option): + """ + Return match object if 'option' is present in the kernel boot options + of the grub configuration. + """ + m=None + matchs=r'^.*?'+MyDistro.grubKernelBootOptionsLine+r'.*?'+option+r'.*$' + try: + m=FindStringInFile(MyDistro.grubKernelBootOptionsFile,matchs) + except IOError, e: + Error('FindInLinuxKernelCmdline: Exception opening ' + MyDistro.grubKernelBootOptionsFile + 'Exception:' + str(e)) + + return m + +def AppendToLinuxKernelCmdline(option): + """ + Add 'option' to the kernel boot options of the grub configuration. + """ + if not FindInLinuxKernelCmdline(option): + src=r'^(.*?'+MyDistro.grubKernelBootOptionsLine+r')(.*?)("?)$' + rep=r'\1\2 '+ option + r'\3' + try: + ReplaceStringInFile(MyDistro.grubKernelBootOptionsFile,src,rep) + except IOError, e : + Error('AppendToLinuxKernelCmdline: Exception opening ' + MyDistro.grubKernelBootOptionsFile + 'Exception:' + str(e)) + return 1 + Run("update-grub",chk_err=False) + return 0 + +def RemoveFromLinuxKernelCmdline(option): + """ + Remove 'option' to the kernel boot options of the grub configuration. + """ + if FindInLinuxKernelCmdline(option): + src=r'^(.*?'+MyDistro.grubKernelBootOptionsLine+r'.*?)('+option+r')(.*?)("?)$' + rep=r'\1\3\4' + try: + ReplaceStringInFile(MyDistro.grubKernelBootOptionsFile,src,rep) + except IOError, e : + Error('RemoveFromLinuxKernelCmdline: Exception opening ' + MyDistro.grubKernelBootOptionsFile + 'Exception:' + str(e)) + return 1 + Run("update-grub",chk_err=False) + return 0 + +def FindStringInFile(fname,matchs): + """ + Return match object if found in file. + """ + try: + ms=re.compile(matchs) + for l in (open(fname,'r')).readlines(): + m=re.search(ms,l) + if m: + return m + except: + raise + + return None + +def ReplaceStringInFile(fname,src,repl): + """ + Replace 'src' with 'repl' in file. + """ + updated='' + try: + sr=re.compile(src) + if FindStringInFile(fname,src): + for l in (open(fname,'r')).readlines(): + n=re.sub(sr,repl,l) + updated+=n + ReplaceFileContentsAtomic(fname,updated) + except : + raise + return def ApplyVNUMAWorkaround(): + """ + If kernel version has NUMA bug, add 'numa=off' to + kernel boot options. + """ VersionParts = platform.release().replace('-', '.').split('.') if int(VersionParts[0]) > 2: return @@ -2278,75 +3936,42 @@ def ApplyVNUMAWorkaround(): return if int(VersionParts[2]) > 37: return - AddToLinuxKernelCmdline("numa=off") - # TODO: This is not ideal for offline installation. - print("Your kernel version " + platform.release() + " has a NUMA-related bug: NUMA has been disabled.") - + if AppendToLinuxKernelCmdline("numa=off") == 0 : + Log("Your kernel version " + platform.release() + " has a NUMA-related bug: NUMA has been disabled.") + else : + "Error adding 'numa=off'. NUMA has not been disabled." + def RevertVNUMAWorkaround(): - print("Automatic reverting of GRUB configuration is not yet supported. Please edit by hand.") + """ + Remove 'numa=off' from kernel boot options. + """ + if RemoveFromLinuxKernelCmdline("numa=off") == 0 : + Log('NUMA has been re-enabled') + else : + Log('NUMA has not been re-enabled') def Install(): - if IsWindows(): - print("ERROR: -install invalid for Windows.") + """ + Install the agent service. + Check dependencies. + Create /etc/waagent.conf and move old version to + /etc/waagent.conf.old + Copy RulesFiles to /var/lib/waagent + Create /etc/logrotate.d/waagent + Set /etc/ssh/sshd_config ClientAliveInterval to 180 + Call ApplyVNUMAWorkaround() + """ + if MyDistro.checkDependencies(): return 1 os.chmod(sys.argv[0], 0755) SwitchCwd() - requiredDeps = [ "/sbin/route", "/sbin/shutdown" ] - if IsDebian(): - requiredDeps += [ "/usr/sbin/update-rc.d" ] - if IsSuse(): - requiredDeps += [ "/sbin/insserv" ] - for a in requiredDeps: - if not os.path.isfile(a): - Error("Missing required dependency: " + a) - return 1 - missing = False - for a in [ "ssh-keygen", "useradd", "openssl", "sfdisk", - "fdisk", "mkfs", "chpasswd", "sed", "grep", "sudo" ]: - if Run("which " + a + " > /dev/null 2>&1"): - Warn("Missing dependency: " + a) - missing = True - if missing == True: - Warn("Please resolve missing dependencies listed for full functionality.") - if UsesRpm(): - if not Run("rpm --quiet -q NetworkManager",chk_err=False): # We want this to fail - supress error logging on error. - Error(GuestAgentLongName + " is not compatible with NetworkManager.") - return 1 - if Run("rpm --quiet -q python-pyasn1"): - Error(GuestAgentLongName + " requires python-pyasn1.") - return 1 - if UsesDpkg() and not Run("dpkg-query -s network-manager >/dev/null 2>&1",chk_err=False): # We want this to fail - supress error logging on error. - Error(GuestAgentLongName + " is not compatible with network-manager.") - return 1 for a in RulesFiles: if os.path.isfile(a): if os.path.isfile(GetLastPathElement(a)): os.remove(GetLastPathElement(a)) shutil.move(a, ".") Warn("Moved " + a + " -> " + LibDir + "/" + GetLastPathElement(a) ) - - if IsUbuntu() and not IsPackagedUbuntu(): - # Support for Ubuntu's upstart configuration - filename="waagent.conf" - filepath = "/etc/init/" + filename - SetFileContents(filepath, Init_Ubuntu) - os.chmod(filepath, 0644) - - elif not IsPackagedUbuntu(): - # Regular init.d configurations - filename = "waagent" - filepath = "/etc/init.d/" + filename - distro = IsRedHat() + IsDebian() * 2 + IsSuse() * 3 - if distro == 0: - Error("Unable to detect Linux Distribution.") - return 1 - init = [[Init_RedHat, "chkconfig --add " + filename], - [Init_Debian, "update-rc.d " + filename + " defaults"], - [Init_Suse, "insserv " + filename]][distro - 1] - SetFileContents(filepath, init[0]) - os.chmod(filepath, 0755) - Run(init[1]) - + MyDistro.registerAgentService() if os.path.isfile("/etc/waagent.conf"): try: os.remove("/etc/waagent.conf.old") @@ -2357,20 +3982,58 @@ def Install(): Warn("Existing /etc/waagent.conf has been renamed to /etc/waagent.conf.old") except: pass - SetFileContents("/etc/waagent.conf", WaagentConf) + SetFileContents("/etc/waagent.conf", MyDistro.waagent_conf_file) SetFileContents("/etc/logrotate.d/waagent", WaagentLogrotate) filepath = "/etc/ssh/sshd_config" ReplaceFileContentsAtomic(filepath, "\n".join(filter(lambda a: not a.startswith("ClientAliveInterval"), - GetFileContents(filepath).split('\n'))) + "ClientAliveInterval 180\n") + GetFileContents(filepath).split('\n'))) + "\nClientAliveInterval 180\n") Log("Configured SSH client probing to keep connections alive.") ApplyVNUMAWorkaround() return 0 +def GetMyDistro(dist_class_name=''): + """ + Return MyDistro object. + NOTE: Logging is not initialized at this point. + """ + if dist_class_name == '': + if 'Linux' in platform.system(): + Distro=platform.dist()[0] + else : # I know this is not Linux! + if 'FreeBSD' in platform.system(): + Distro=platform.system() + dist_class_name=Distro+'Distro' + else: + Distro=dist_class_name + if not globals().has_key(dist_class_name): + print Distro+' is not a supported distribution.' + return None + return globals()[dist_class_name]() # the distro class inside this module. + +def PackagedInstall(buildroot): + """ + Called from setup.py for use by RPM. + Generic implementation Creates directories and + files /etc/waagent.conf, /etc/init.d/waagent, /usr/sbin/waagent, + /etc/logrotate.d/waagent, /etc/sudoers.d/waagent under buildroot. + Copies generated files waagent.conf, into place and exits. + """ + MyDistro=GetMyDistro() + if MyDistro == None : + sys.exit(1) + MyDistro.packagedInstall(buildroot) + +def LibraryInstall(buildroot): + pass + def Uninstall(): - if IsWindows(): - print("ERROR: -uninstall invalid for windows, see waagent_service.exe") - return 1 + """ + Uninstall the agent service. + Copy RulesFiles back to original locations. + Delete agent-related files. + Call RevertVNUMAWorkaround(). + """ SwitchCwd() for a in RulesFiles: if os.path.isfile(GetLastPathElement(a)): @@ -2379,71 +4042,30 @@ def Uninstall(): Warn("Moved " + LibDir + "/" + GetLastPathElement(a) + " -> " + a ) except: pass - filename = "waagent" - a = IsRedHat() + IsDebian() * 2 + IsSuse() * 3 - if a == 0: - Error("Unable to detect Linux Distribution.") - return 1 - - # Managed by dpkg for Packaged Ubuntu - if not IsPackaged(): - Run("service " + filename + " stop") - cmd = ["chkconfig --del " + filename, - "update-rc.d -f " + filename + " remove", - "insserv -r " + filename][a - 1] - Run(cmd) - - remove_f = [ - "/etc/waagent.conf", - "/etc/logrotate.d/waagent", - "/etc/sudoers.d/waagent", - ] - - # For packaged Ubuntu, the script should let the packaging - # manage the removal of these files - if not IsPackagedUbuntu(): - remove_f.append([ - "/etc/init/waagent.conf", - "/etc/init.d/" + filename, - ]) - - for f in os.listdir(LibDir) + remove_f: - try: - os.remove(f) - except: - pass + MyDistro.unregisterAgentService() + MyDistro.uninstallDeleteFiles() RevertVNUMAWorkaround() return 0 -def DeleteRootPassword(): - filepath="/etc/shadow" - ReplaceFileContentsAtomic(filepath, "root:*LOCK*:14600::::::\n" + "\n".join(filter(lambda a: not - a.startswith("root:"), - GetFileContents(filepath).split('\n')))) - os.chmod(filepath, 0000) - if IsRedHat(): - Run("chcon system_u:object_r:shadow_t:s0 " + filepath) - Log("Root password deleted.") - def Deprovision(force, deluser): - if IsWindows(): - Run(os.environ["windir"] + "\\system32\\sysprep\\sysprep.exe /generalize") - return 0 - + """ + Remove user accounts created by provisioning. + Disables root password if Provisioning.DeleteRootPassword = 'y' + Stop agent service. + Remove SSH host keys if they were generated by the provision. + Set hostname to 'localhost.localdomain'. + Delete cached system configuration files in /var/lib and /var/lib/waagent. + """ SwitchCwd() - ovfxml = GetFileContents("ovf-env.xml") + ovfxml = GetFileContents(LibDir+"/ovf-env.xml") ovfobj = None if ovfxml != None: ovfobj = OvfEnv().Parse(ovfxml) print("WARNING! The waagent service will be stopped.") print("WARNING! All SSH host key pairs will be deleted.") - if IsUbuntu(): - print("WARNING! Nameserver configuration in /etc/resolvconf/resolv.conf.d/{tail,originial} will be deleted.") - else: - print("WARNING! Nameserver configuration in /etc/resolv.conf will be deleted.") print("WARNING! Cached DHCP leases will be deleted.") - + MyDistro.deprovisionWarnUser() delRootPass = Config.get("Provisioning.DeleteRootPassword") if delRootPass != None and delRootPass.lower().startswith("y"): print("WARNING! root password will be disabled. You will not be able to login as root.") @@ -2454,10 +4076,9 @@ def Deprovision(force, deluser): if force == False and not raw_input('Do you want to proceed (y/n)? ').startswith('y'): return 1 - Run("service waagent stop") - + MyDistro.stopAgentService() if deluser == True: - DeleteAccount(ovfobj.UserName) + MyDistro.DeleteAccount(ovfobj.UserName) # Remove SSH host keys regenerateKeys = Config.get("Provisioning.RegenerateSshHostKeyPair") @@ -2466,111 +4087,107 @@ def Deprovision(force, deluser): # Remove root password if delRootPass != None and delRootPass.lower().startswith("y"): - DeleteRootPassword() - + MyDistro.deleteRootPassword() # Remove distribution specific networking configuration - UpdateAndPublishHostNameCommon("localhost.localdomain") - - # RedHat, Suse, Debian - for a in VarLibDhcpDirectories: - Run("rm -f " + a + "/*") - - # Clear LibDir, remove nameserver and root bash history - fileBlackList = [ "/root/.bash_history", "/var/log/waagent.log" ] - - if IsUbuntu(): - # Ubuntu uses resolv.conf by default, so removing /etc/resolv.conf will - # break resolvconf. Therefore, we check to see if resolvconf is in use, - # and if so, we remove the resolvconf artifacts. - - Log("Deprovision: Ubuntu specific resolv.conf behavior selected.") - if os.path.realpath('/etc/resolv.conf') != '/run/resolvconf/resolv.conf': - Log("resolvconf is not configured. Removing /etc/resolv.conf") - fileBlackList.append('/etc/resolv.conf') - else: - Log("resolvconf is enabled; leaving /etc/resolv.conf intact") - resolvConfD = '/etc/resolvconf/resolv.conf.d/' - fileBlackList.extend([resolvConfD + 'tail', resolvConfD + 'originial' ]) - else: - fileBlackList.extend(os.listdir(LibDir) + ['/etc/resolv.conf']) - - for f in os.listdir(LibDir) + fileBlackList: - try: - os.remove(f) - except: - pass + MyDistro.publishHostname('localhost.localdomain') + MyDistro.deprovisionDeleteFiles() return 0 def SwitchCwd(): - if not IsWindows(): - CreateDir(LibDir, "root", 0700) - os.chdir(LibDir) + """ + Switch to cwd to /var/lib/waagent. + Create if not present. + """ + CreateDir(LibDir, "root", 0700) + os.chdir(LibDir) def Usage(): + """ + Print the arguments to waagent. + """ print("usage: " + sys.argv[0] + " [-verbose] [-force] [-help|-install|-uninstall|-deprovision[+user]|-version|-serialconsole|-daemon]") return 0 -if GuestAgentVersion == "": - print("WARNING! This is a non-standard agent that does not include a valid version string.") -if IsLinux() and not DetectLinuxDistro(): - print("WARNING! Unable to detect Linux distribution. Some functionality may be broken.") - -if len(sys.argv) == 1: - sys.exit(Usage()) +def main(): + """ + Instantiate MyDistro, exit if distro class is not defined. + Parse command-line arguments, exit with usage() on error. + Instantiate ConfigurationProvider. + Call appropriate non-daemon methods and exit. + If daemon mode, enter Agent.Run() loop. + """ + if GuestAgentVersion == "": + print("WARNING! This is a non-standard agent that does not include a valid version string.") + + if len(sys.argv) == 1: + sys.exit(Usage()) -args = [] -force = False -for a in sys.argv[1:]: - if re.match("^([-/]*)(help|usage|\?)", a): + LoggerInit('/var/log/waagent.log','/dev/console') + global LinuxDistro + LinuxDistro=platform.dist()[0] + global MyDistro + MyDistro=GetMyDistro() + if MyDistro == None : + sys.exit(1) + args = [] + global force + force = False + for a in sys.argv[1:]: + if re.match("^([-/]*)(help|usage|\?)", a): + sys.exit(Usage()) + elif re.match("^([-/]*)verbose", a): + myLogger.verbose = True + elif re.match("^([-/]*)force", a): + force = True + elif re.match("^([-/]*)(setup|install)", a): + sys.exit(MyDistro.Install()) + elif re.match("^([-/]*)(uninstall)", a): + sys.exit(Uninstall()) + else: + args.append(a) + global Config + Config = ConfigurationProvider() + + verbose = Config.get("Logs.Verbose") + if verbose != None and verbose.lower().startswith("y"): + myLogger.verbose=True + global daemon + daemon = False + for a in args: + if re.match("^([-/]*)deprovision\+user", a): + sys.exit(Deprovision(force, True)) + elif re.match("^([-/]*)deprovision", a): + sys.exit(Deprovision(force, False)) + elif re.match("^([-/]*)daemon", a): + daemon = True + elif re.match("^([-/]*)version", a): + print(GuestAgentVersion + " running on " + LinuxDistro) + sys.exit(0) + elif re.match("^([-/]*)serialconsole", a): + AppendToLinuxKernelCmdline("console=ttyS0 earlyprintk=ttyS0") + Log("Configured kernel to use ttyS0 as the boot console.") + sys.exit(0) + else: + print("Invalid command line parameter:" + a) + sys.exit(1) + + if daemon == False: sys.exit(Usage()) - elif re.match("^([-/]*)verbose", a): - Verbose = True - elif re.match("^([-/]*)force", a): - force = True - elif re.match("^([-/]*)(setup|install)", a): - sys.exit(Install()) - elif re.match("^([-/]*)(uninstall)", a): - sys.exit(Uninstall()) - else: - args.append(a) - -Config = ConfigurationProvider() - -verbose = Config.get("Logs.Verbose") -if verbose != None and verbose.lower().startswith("y"): - Verbose = True - -daemon = False -for a in args: - if re.match("^([-/]*)deprovision\+user", a): - sys.exit(Deprovision(force, True)) - elif re.match("^([-/]*)deprovision", a): - sys.exit(Deprovision(force, False)) - elif re.match("^([-/]*)daemon", a): - daemon = True - elif re.match("^([-/]*)version", a): - print(GuestAgentVersion + " running on " + LinuxDistro) - sys.exit(0) - elif re.match("^([-/]*)serialconsole", a): - AddToLinuxKernelCmdline("console=ttyS0 earlyprintk=ttyS0") - Log("Configured kernel to use ttyS0 as the boot console.") - sys.exit(0) - else: - print("Invalid command line parameter:" + a) + global modloaded + modloaded = False + try: + SwitchCwd() + Log(GuestAgentLongName + " Version: " + GuestAgentVersion) + if IsLinux(): + Log("Linux Distribution Detected : " + LinuxDistro) + global WaAgent + WaAgent = Agent() + WaAgent.Run() + except Exception, e: + Error(traceback.format_exc()) + Error("Exception: " + str(e)) sys.exit(1) - -if daemon == False: - sys.exit(Usage()) - -try: - SwitchCwd() - Log(GuestAgentLongName + " Version: " + GuestAgentVersion) - if IsLinux(): - Log("Linux Distribution Detected : " + LinuxDistro) - WaAgent = Agent() - WaAgent.Run() -except Exception, e: - Error(traceback.format_exc()) - Error("Exception: " + str(e)) - sys.exit(1) + +if __name__ == '__main__' : + main() |