diff options
-rw-r--r-- | Changelog | 6 | ||||
-rw-r--r-- | README | 13 | ||||
-rw-r--r-- | config/waagent.conf | 2 | ||||
-rw-r--r-- | debian/changelog | 7 | ||||
-rw-r--r-- | debian/patches/disable-udev-rules.patch | 2 | ||||
-rw-r--r-- | rpm/walinuxagent.spec | 12 | ||||
-rwxr-xr-x | waagent | 523 |
7 files changed, 454 insertions, 111 deletions
@@ -1,5 +1,11 @@ WALinuxAgent Changelog ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +02 Apr 2014, WALinuxAgent 2.0.4 + . Fix encoding issue in LogToFile() & LogToCon() + . Add support for parsing ExtensionsConfiguration from GoalState document + . Add support for the Fedora distribution + . Several fixes to FreeBSDDistro class + 16 Jan 2014, WALinuxAgent 2.0.3 . Add exception awareness to GetFileContents, SetFileContents, and AppendFileContents @@ -22,9 +22,9 @@ functionality for Linux and FreeBSD IaaS deployments: - Ensures the stability of the network interface name * Kernel - - Configuring virtual NUMA + - Configure virtual NUMA (disable for kernel <2.6.37) - Consume Hyper-V entropy for /dev/random - - Configuring SCSI timeouts for the root device (which could be remote) + - Configure SCSI timeouts for the root device (which could be remote) * Diagnostics - Console redirection to the serial port @@ -50,7 +50,7 @@ REQUIREMENTS The following systems have been tested and are known to work with the Windows Azure Linux Agent. Please note that this list may differ from the official -list of supported systems on the Windows Azure Platform described here: +list of supported systems on the Windows Azure Platform as described here: http://support.microsoft.com/kb/2805216 Supported Linux Distributions: @@ -66,7 +66,7 @@ http://support.microsoft.com/kb/2805216 Waagent depends on some system packages in order to function properly: - * Python 2.4+ + * Python 2.5+ * OpenSSL 1.0+ * OpenSSH 5.3+ * Filesystem utilities: sfdisk, fdisk, mkfs @@ -201,7 +201,7 @@ Role.TopologyConsumer: Type: String Default: None If a path to an executable program is specified, the program is invoked when the -Fabric indicates that a new network topology layout is available for the VM.The +Fabric indicates that a new network topology layout is available for the VM. The path to the XML configuration file is provided as an argument to the executable. This may be invoked multiple times whenever the network topology changes (due to service healing for example). A sample file is provided in the Appendix. Please @@ -264,7 +264,8 @@ Type: String Default: ext4 This specifies the filesystem type for the resource disk. Supported values vary by Linux distribution. If the string is X, then mkfs.X should be present on the -Linux image. FreeBSD images should use 'ufs2' here. +Linux image. SLES 11 images should typically use 'ext3'. FreeBSD images should +use 'ufs2' here. ResourceDisk.MountPoint: Type: String Default: /mnt/resource diff --git a/config/waagent.conf b/config/waagent.conf index 364c2c1..c7cb96a 100644 --- a/config/waagent.conf +++ b/config/waagent.conf @@ -47,7 +47,7 @@ ResourceDisk.SwapSizeMB=0 # Respond to load balancer probes if requested by Windows Azure. LBProbeResponder=y -# Logging level +# Enable verbose logging (y|n) Logs.Verbose=n # Root device timeout in seconds. diff --git a/debian/changelog b/debian/changelog index 6cd9f8b..c42e5f4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +walinuxagent (2.0.4-0ubuntu1) trusty; urgency=medium + + * Update to latest upstream version 2.0.4 (LP: #1304023). + - Includes ability for agent injection. + + -- Ben Howard <ben.howard@ubuntu.com> Mon, 07 Apr 2014 16:48:16 -0600 + walinuxagent (2.0.3-0ubuntu2) trusty; urgency=low * Change /etc/dhcp/dhclient.conf 'send host-name' during post-inst to diff --git a/debian/patches/disable-udev-rules.patch b/debian/patches/disable-udev-rules.patch index ba96a2d..56c5acf 100644 --- a/debian/patches/disable-udev-rules.patch +++ b/debian/patches/disable-udev-rules.patch @@ -5,7 +5,7 @@ Author: Ben Howard Last-Update: 2013-07-11 --- a/waagent +++ b/waagent -@@ -87,8 +87,7 @@ +@@ -90,8 +90,7 @@ VMM_STARTUP_SCRIPT_NAME='install' VMM_CONFIG_FILE_NAME='linuxosconfiguration.xml' global RulesFiles diff --git a/rpm/walinuxagent.spec b/rpm/walinuxagent.spec index 0ba8cec..f20cb64 100644 --- a/rpm/walinuxagent.spec +++ b/rpm/walinuxagent.spec @@ -2,18 +2,18 @@ # Name: walinuxagent.spec #------------------------------------------------------------------------------- # Purpose : RPM Spec file for Python script packaging -# Version : 2.0.0 +# Version : 2.0.4 # Created : April 20 2012 #=============================================================================== Name: WALinuxAgent Summary: The Windows Azure Linux Agent -Version: 2.0.3 +Version: 2.0.4 Release: 1 License: Apache License Version 2.0 Group: System/Daemons Url: http://go.microsoft.com/fwlink/?LinkId=250998 -Source0: WALinuxAgent-2.0.3.tar.gz +Source0: WALinuxAgent-2.0.4.tar.gz Requires: python python-pyasn1 openssh openssl util-linux sed grep sudo iptables Conflicts: NetworkManager BuildRoot: %{_tmppath}/%{name}-%{version}-build @@ -39,6 +39,7 @@ find . -type f -exec chmod 0644 {} + %install python setup.py install --prefix=%{_prefix} --lnx-distro='redhat' --init-system='sysV' --root=%{buildroot} mkdir -p %{buildroot}/%{_localstatedir}/log +mkdir -p -m 0700 %{buildroot}/%{_sharedstatedir}/waagent touch %{buildroot}/%{_localstatedir}/log/waagent.log %post @@ -64,9 +65,14 @@ fi %config(noreplace) %{_sysconfdir}/logrotate.d/waagent %config %{_sysconfdir}/waagent.conf %ghost %{_localstatedir}/log/waagent.log +%dir %attr(0700, root, root) %{_sharedstatedir}/waagent %changelog +* Thu Mar 25 2014 - walinuxagent@microsoft.com +- Create directory /var/lib/waagent +- Updated version to 2.0.4 for release + * Thu Jan 16 2014 - walinuxagent@microsoft.com - Updated version to 2.0.3 for release @@ -2,7 +2,7 @@ # # Windows Azure Linux Agent # -# Copyright 2012 Microsoft Corporation +# Copyright 2014 Microsoft Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -45,6 +45,9 @@ import time import traceback import xml.dom.minidom import fcntl +import inspect +import zipfile +import json if not hasattr(subprocess,'check_output'): def check_output(*popenargs, **kwargs): @@ -75,7 +78,7 @@ if not hasattr(subprocess,'check_output'): GuestAgentName = "WALinuxAgent" GuestAgentLongName = "Windows Azure Linux Agent" -GuestAgentVersion = "WALinuxAgent-2.0.3" +GuestAgentVersion = "WALinuxAgent-2.0.4" ProtocolVersion = "2012-11-30" #WARNING this value is used to confirm the correct fabric protocol. Config = None @@ -83,7 +86,7 @@ WaAgent = None DiskActivated = False Openssl = "openssl" Children = [] - +ExtensionChildren = [] VMM_STARTUP_SCRIPT_NAME='install' VMM_CONFIG_FILE_NAME='linuxosconfiguration.xml' global RulesFiles @@ -118,7 +121,7 @@ ResourceDisk.SwapSizeMB=0 # Size of the swapfile. LBProbeResponder=y # Respond to load balancer probes if requested by Windows Azure. -Logs.Verbose=n # +Logs.Verbose=n # Enable verbose logs OS.RootDeviceScsiTimeout=300 # Root device timeout in seconds. OS.OpensslPath=None # If "None", the system default version is used. @@ -404,6 +407,7 @@ class AbstractDistro(object): Return the ip of the first active non-loopback interface. """ + addr='' iface,addr=GetFirstActiveNetworkInterfaceNonLoopback() return addr @@ -471,8 +475,10 @@ class AbstractDistro(object): def Install(self): return Install() - def dvdHasMedia(self,dvd): - if Run("LC_ALL=C fdisk -l " + dvd + " | grep Disk"): + def mediaHasFilesystem(self,dsk): + if len(dsk) == 0 : + return False + if Run("LC_ALL=C fdisk -l " + dsk + " | grep Disk"): return False return True @@ -1156,6 +1162,18 @@ class LinuxMintDistro(UbuntuDistro): super(LinuxMintDistro,self).__init__() ############################################################ +# fedoraDistro +############################################################ + +class fedoraDistro(redhatDistro): + """ + FedoraDistro concrete class + Put Fedora specific behavior here... + """ + def __init__(self): + super(fedoraDistro,self).__init__() + +############################################################ # FreeBSD ############################################################ FreeBSDWaagentConf = """\ @@ -1182,7 +1200,7 @@ ResourceDisk.SwapSizeMB=0 # Size of the swapfile. LBProbeResponder=y # Respond to load balancer probes if requested by Windows Azure. -Logs.Verbose=n # +Logs.Verbose=n # Enable verbose logs OS.RootDeviceScsiTimeout=300 # Root device timeout in seconds. OS.OpensslPath=None # If "None", the system default version is used. @@ -1197,7 +1215,7 @@ bsd_init_file="""\ # KEYWORD: nojail . /etc/rc.subr - +export PATH=$PATH:/usr/local/bin name="waagent" rcvar="waagent_enable" command="/usr/sbin/${name}" @@ -1210,6 +1228,54 @@ load_rc_config $name run_rc_command "$1" """ +bsd_activate_resource_disk_txt="""\ +#!/usr/bin/env python + +import os +import sys +import imp + +# waagent has no '.py' therefore create waagent module import manually. +__name__='setupmain' #prevent waagent.__main__ from executing +waagent=imp.load_source('waagent','/tmp/waagent') +waagent.LoggerInit('/var/log/waagent.log','/dev/console') +from waagent import RunGetOutput,Run +Config=waagent.ConfigurationProvider() +format = Config.get("ResourceDisk.Format") +if format == None or format.lower().startswith("n"): + sys.exit(0) +device_base = 'da1' +device = "/dev/" + device_base +for entry in RunGetOutput("mount")[1].split(): + if entry.startswith(device + "s1"): + waagent.Log("ActivateResourceDisk: " + device + "s1 is already mounted.") + sys.exit(0) +mountpoint = Config.get("ResourceDisk.MountPoint") +if mountpoint == None: + mountpoint = "/mnt/resource" +waagent.CreateDir(mountpoint, "root", 0755) +fs = Config.get("ResourceDisk.Filesystem") +if waagent.FreeBSDDistro().mediaHasFilesystem(device) == False : + Run("newfs " + device + "s1") +if Run("mount " + device + "s1 " + mountpoint): + waagent.Error("ActivateResourceDisk: Failed to mount resource disk (" + device + "s1).") + sys.exit(0) +waagent.Log("Resource disk (" + device + "s1) is mounted at " + mountpoint + " with fstype " + fs) +swap = Config.get("ResourceDisk.EnableSwap") +if swap == None or swap.lower().startswith("n"): + sys.exit(0) +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"): + waagent.Error("ActivateResourceDisk: Configuring swap - Failed to create md0") +if not Run("swapon /dev/md0"): + waagent.Log("Enabled " + str(sizeKB) + " KB of swap at " + mountpoint + "/swapfile") +else: + waagent.Error("ActivateResourceDisk: Failed to activate swap at " + mountpoint + "/swapfile") +""" class FreeBSDDistro(AbstractDistro): """ @@ -1219,6 +1285,7 @@ class FreeBSDDistro(AbstractDistro): Generic Attributes go here. These are based on 'majority rules'. This __init__() may be called or overriden by the child. """ + super(FreeBSDDistro,self).__init__() self.agent_service_name = os.path.basename(sys.argv[0]) self.selinux=False self.ssh_service_name='sshd' @@ -1235,7 +1302,7 @@ class FreeBSDDistro(AbstractDistro): self.grubKernelBootOptionsFile = '/boot/loader.conf' self.grubKernelBootOptionsLine = '' self.getpidcmd = 'pgrep -n' - self.mount_dvd_cmd = 'dd bs=2048 count=1 skip=295 if=' + self.mount_dvd_cmd = 'dd bs=2048 count=33 skip=295 if=' # custom data max len is 64k self.sudoers_dir_base = '/usr/local/etc' self.waagent_conf_file = FreeBSDWaagentConf @@ -1249,13 +1316,6 @@ class FreeBSDDistro(AbstractDistro): 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 @@ -1359,6 +1419,14 @@ class FreeBSDDistro(AbstractDistro): retries-=1 if code > 0 and retries > 0 : Log("GetFreeBSDEthernetInfo - Error: retry ethernet detection " + str(retries)) + if retries == 9 : + c,o=RunGetOutput("ifconfig | grep -A1 -B2 ether",chk_err=False) + if c == 0: + t=o.replace('\n',' ') + t=t.split() + i=t[0][:-1] + Log(RunGetOutput('id')[1]) + Run('dhclient '+i) time.sleep(10) j=output.replace('\n',' ') @@ -1469,52 +1537,19 @@ class FreeBSDDistro(AbstractDistro): pass return - def ActivateResourceDisk(self): + def ActivateResourceDiskNoThread(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) + Run('cp /usr/sbin/waagent /tmp/') + SetFileContents('/tmp/bsd_activate_resource_disk.py',bsd_activate_resource_disk_txt) + Run('chmod +x /tmp/bsd_activate_resource_disk.py') + pid = subprocess.Popen(["/tmp/bsd_activate_resource_disk.py", ""]).pid + Log("Spawning bsd_activate_resource_disk.py") 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") + return def Install(self): """ @@ -1559,8 +1594,8 @@ class FreeBSDDistro(AbstractDistro): #ApplyVNUMAWorkaround() return 0 - def dvdHasMedia(self,dvd): - if Run('LC_ALL=C fdisk -p ' + dvd + ' | grep "invalid fdisk partition table found" '): + def mediaHasFilesystem(self,dsk): + if Run('LC_ALL=C fdisk -p ' + dsk + ' | grep "invalid fdisk partition table found" ',False): return False return True @@ -1950,22 +1985,20 @@ class Logger(object): """ if self.file_path: with open(self.file_path, "a") as F : - F.write(message + "\n") + F.write(message.encode('ascii','ignore') + "\n") F.close() 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() - + if self.con_path: + with open(self.con_path, "w") as C : + C.write(message.encode('ascii','ignore') + "\n") + C.close() + def Log(self,message): """ Standard Log function. @@ -1994,15 +2027,12 @@ class Logger(object): 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: @@ -2012,7 +2042,8 @@ class Logger(object): for line in message.split('\n'): line = t + line self.LogToFile(line) - + self.LogToCon(line) + def Warn(self,message): """ Prepend the text "WARNING:" to the prefix for each line in 'message'. @@ -2053,11 +2084,11 @@ def GetFirstActiveNetworkInterfaceNonLoopback(): Return the interface name, and ip addr of the first active non-loopback interface. """ + iface='' expected=16 # how many devices should I expect... struct_size=40 # for 64bit the size is 40 bytes 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.') @@ -2070,7 +2101,6 @@ def GetFirstActiveNetworkInterfaceNonLoopback(): break return iface.decode('latin-1'), socket.inet_ntoa(s[i+20:i+24]) - def GetIpv4Address(): """ Return the ip of the @@ -2093,8 +2123,10 @@ def GetMacAddress(): Convienience function, returns mac addr bound to first non-loobback interface. """ - ifname=GetFirstActiveNetworkInterfaceNonLoopback()[0] - a = Linux_ioctl_GetInterfaceMac(ifname) + ifname='' + while len(ifname) < 2 : + ifname=GetFirstActiveNetworkInterfaceNonLoopback()[0] + a = Linux_ioctl_GetInterfaceMac(ifname) return HexStringToByteArray(a) def DeviceForIdePort(n): @@ -2559,7 +2591,270 @@ class SharedConfig(object): except OSError, e : ErrorWithPrefix('Agent.Run','Exception: '+ str(e) +' occured launching ' + program ) return self + +class ExtensionsConfig(object): + """ + Parse ExtensionsConfig, downloading and unpacking them to /var/lib/waagent. + Install if <enabled>true</enabled>, remove if it is set to false. + """ + #<?xml version="1.0" encoding="utf-8"?> + #<Extensions version="1.0.0.0" goalStateIncarnation="6"><Plugins> + # <Plugin name="OSTCExtensions.ExampleHandlerLinux" version="1.5" + #location="http://previewusnorthcache.blob.core.test-cint.azure-test.net/d84b216d00bf4d96982be531539e1513/OSTCExtensions_ExampleHandlerLinux_usnorth_manifest.xml" + #config="" state="enabled" autoUpgrade="false" runAsStartupTask="false" isJson="true" /> + #</Plugins> + #<PluginSettings> + # <Plugin name="OSTCExtensions.ExampleHandlerLinux" version="1.5"> + # <RuntimeSettings seqNo="2">{"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"1BE9A13AA1321C7C515EF109746998BAB6D86FD1", + #"protectedSettings":"MIIByAYJKoZIhvcNAQcDoIIBuTCCAbUCAQAxggFxMIIBbQIBADBVMEExPzA9BgoJkiaJk/IsZAEZFi9XaW5kb3dzIEF6dXJlIFNlcnZpY2UgTWFuYWdlbWVudCBmb3IgR + #Xh0ZW5zaW9ucwIQZi7dw+nhc6VHQTQpCiiV2zANBgkqhkiG9w0BAQEFAASCAQCKr09QKMGhwYe+O4/a8td+vpB4eTR+BQso84cV5KCAnD6iUIMcSYTrn9aveY6v6ykRLEw8GRKfri2d6 + #tvVDggUrBqDwIgzejGTlCstcMJItWa8Je8gHZVSDfoN80AEOTws9Fp+wNXAbSuMJNb8EnpkpvigAWU2v6pGLEFvSKC0MCjDTkjpjqciGMcbe/r85RG3Zo21HLl0xNOpjDs/qqikc/ri43Y76E/X + #v1vBSHEGMFprPy/Hwo3PqZCnulcbVzNnaXN3qi/kxV897xGMPPC3IrO7Nc++AT9qRLFI0841JLcLTlnoVG1okPzK9w6ttksDQmKBSHt3mfYV+skqs+EOMDsGCSqGSIb3DQEHATAUBggqh + #kiG9w0DBwQITgu0Nu3iFPuAGD6/QzKdtrnCI5425fIUy7LtpXJGmpWDUA==","publicSettings":{"port":"3000"}}}]}</RuntimeSettings> + # </Plugin> + #</PluginSettings> + #<StatusUploadBlob>https://ostcextensions.blob.core.test-cint.azure-test.net/vhds/eg-plugin7-vm.eg-plugin7-vm.eg-plugin7-vm.status?sr=b&sp=rw& + #se=9999-01-01&sk=key1&sv=2012-02-12&sig=wRUIDN1x2GC06FWaetBP9sjjifOWvRzS2y2XBB4qoBU%3D</StatusUploadBlob></Extensions> + + def __init__(self): + self.reinitialize() + + def reinitialize(self): + """ + Reset members. + """ + self.Extensions = None + self.Plugins = None + self.Util = None + def Parse(self, xmlText): + """ + Write configuration to file ExtensionsConfig.xml. + If state is enabled: + if the plugin is installed: + if the new plugin's version is higher: + download the new archive + do the updateCommand. + + if the version is the same or lower: + create the new .settings file from the configuration received + do the enableCommand + if the plugin is not installed: + download/unpack archive and call the installCommand + + if state is disabled: + call disableCommand + Create execuatable shell script containig 'installCommand'. + Spawn the script and report the PID to the Log. + """ + self.reinitialize() + self.Util=Util() + dom = xml.dom.minidom.parseString(xmlText) + LogIfVerbose(xmlText) + self.Extensions=dom.getElementsByTagName("Extensions") + self.Plugins = dom.getElementsByTagName("Plugin") + incarnation=self.Extensions[0].getAttribute("goalStateIncarnation") + SetFileContents('ExtensionsConfig.'+incarnation+'.xml', xmlText) + for p in self.Plugins: + if len(p.getAttribute("location"))<1: # this plugin is inside the PluginSettings + continue + previous_version = None + version=p.getAttribute("version") + name=p.getAttribute("name") + Log("Found Plugin: " + name + ' version: ' + version) + if p.getAttribute("state") == 'disabled' : + #disable + if self.launchCommand(name,version,'disableCommand') == None : + Error('Unable to disable '+name) + continue + else : + Log(name+' is disabled') + continue + # state is enabled + # if the same plugin exists and the version is newer or + # does not exist then download and unzip the new plugin + plg_dir=None + for root, dirs, files in os.walk(LibDir): + for d in dirs: + if name in d: + plg_dir=os.path.join(root,d) + if plg_dir != None: + break + if plg_dir != None : + previous_version=plg_dir.rsplit('-')[-1] + if plg_dir == None or version > previous_version : + location=p.getAttribute("location") + Log("Downloading plugin manifest: " + name + " from " + location) + self.Util.Endpoint=location.split('/')[2] + Log("Plugin server is: " + self.Util.Endpoint) + manifest=self.Util.HttpGetWithoutHeaders(location) + if manifest == None: + Error("Unable to download plugin manifest" + name + " from primary location. Attempting with failover location.") + failoverlocation=p.getAttribute("failoverlocation") + self.Util.Endpoint=failoverlocation.split('/')[2] + Log("Plugin failover server is: " + self.Util.Endpoint) + manifest=self.Util.HttpGetWithoutHeaders(failoverlocation) + if manifest == None: + Error("Unable to download plugin manifest" + name + " from failover location") + continue + Log("Plugin manifest" + name + "downloaded successfully length = " + str(len(manifest))) + filepath=LibDir+"/" + name + '.' + incarnation + '.manifest' + if os.path.splitext(location)[-1] == '.xml' : #if this is an xml file we may have a BOM + if ord(manifest[0]) > 128 and ord(manifest[1]) > 128 and ord(manifest[2]) > 128: + manifest=manifest[3:] + SetFileContents(filepath,manifest) + #Get the bundle url from the manifest + man_dom = xml.dom.minidom.parseString(manifest) + bundle_uri = GetNodeTextData(man_dom.getElementsByTagName("Uri")[0]) + Log("Bundle URI = " + bundle_uri) + # Download the zipfile archive and save as '.zip' + bundle=self.Util.HttpGetWithoutHeaders(bundle_uri) + if bundle == None: + Error("Unable to download plugin bundle" + bundle_uri ) + continue + b=bytearray(bundle) + filepath=LibDir+"/" + os.path.basename(bundle_uri) + '.zip' + SetFileContents(filepath,b) + Log("Plugin bundle" + bundle_uri + "downloaded successfully length = " + str(len(bundle))) + # unpack the archive + z=zipfile.ZipFile(filepath) + zip_dir=LibDir+"/" + name + '-' + version + z.extractall(zip_dir) + Log('Extracted ' + bundle_uri + ' to ' + zip_dir) + # zip no file perms in .zip so set all the scripts to +x + Run( "find " + zip_dir +" | egrep '.sh|.py' | xargs chmod 0700 ") + #write out the base64 config data so the plugin can process it. + mfile=None + for root, dirs, files in os.walk(zip_dir): + for f in files: + if f in ('HandlerManifest.json'): + mfile=os.path.join(root,f) + if mfile != None: + break + if mfile == None : + Error('HandlerManifest.json not found.') + continue + manifest = GetFileContents(mfile) + # create the status and config dirs + Run('mkdir -p ' + root + '/status') + Run('mkdir -p ' + root + '/config') + # write out the configuration data to goalStateIncarnation.settings file in the config path. + config='' + pslist=dom.getElementsByTagName("PluginSettings")[0].getElementsByTagName("Plugin") + for ps in pslist: + if name == ps.getAttribute("name") and version == ps.getAttribute("version"): + Log("Found RuntimeSettings for " + name + " V " + version) + config=GetNodeTextData(ps.getElementsByTagName("RuntimeSettings")[0]) + if config == '': + Error("No RuntimeSettings for " + name + " V " + version) + SetFileContents(root +"/config/" + incarnation +".settings", config ) + #create HandlerEnvironment.json + handler_env='{ "name": "'+name+'", "version": 1.0, "handlerEnvironment": { "logFolder": "/var/log", "configFolder": "' + root + '/config", "statusFolder": "' + root + '/status", "heartbeatFile": "'+ root + '/heartbeat.log"}}' + SetFileContents(root+'/HandlerEnvironment.json',handler_env) + cmd = '' + getcmd='installCommand' + if plg_dir != None and version > plg_dir.rsplit('-')[-1]: + getcmd='updateCommand' + # disable the old plugin if it exists + if previous_version != None: + if self.launchCommand(name,previous_version,'disableCommand') == None : + Error('Unable to disable old plugin '+name+' version ' + previous_version) + else : + Log(name+' version ' + previous_version + ' is disabled') + + if getcmd=='updateCommand': + if self.launchCommand(name,version,getcmd,previous_version) == None : + Error('Update failed for '+name+'-'+version) + else : + Log('Update complete'+name+'-'+version) + # if we updated - call unistall for the old plugin - remove old plugin dir and zipfile + if self.launchCommand(name,previous_version,'uninstallCommand') == None : + Error('Uninstall failed for '+name+'-'+previous_version) + else : + Log('Uninstall complete'+ name +'-' + previous_version) + # remove the old plugin + Run('rm -rf ' + LibDir + '/' + name +'-'+ previous_version + '*') + else : # run install + if self.launchCommand(name,version,getcmd) == None : + Error('Installation failed for '+name+'-'+version) + else : + Log('Installation completed for '+name+'-'+version) + #end if zip_dir == none or version > = prev + # state is still enable + if self.launchCommand(name,version,'enableCommand') == None : + Error('Enable failed for '+name+'-'+version) + else : + Log('Enable completed for '+name+'-'+version) + # this plugin processing is complete + Log('Processing completed for '+name+'-'+version) + #end plugin processing loop + Log('Finished processing ExtensionsConfig.xml') + return self + + def launchCommand(self,name,version,command,prev_version=None): + # get the manifest and read the command + mfile=None + zip_dir=LibDir+"/" + name + '-' + version + for root, dirs, files in os.walk(zip_dir): + for f in files: + if f in ('HandlerManifest.json'): + mfile=os.path.join(root,f) + if mfile != None: + break + if mfile == None : + Error('HandlerManifest.json not found.') + return None + manifest = GetFileContents(mfile) + try: + jsn = json.loads(manifest) + except: + Error('Error parsing HandlerManifest.json.') + return None + if jsn.has_key('handlerManifest') : + cmd = jsn['handlerManifest'][command] + else : + Error('Key handlerManifest not found. Handler cannot be installed.') + if len(cmd) == 0 : + Error('Unable to read ' + command ) + return None + # for update we send the path of the old installation + arg='' + if prev_version != None : + arg=' ' + LibDir+'/' + name + '-' + prev_version + filepath='./'+os.path.basename(cmd) + dirpath=zip_dir+'/'+os.path.dirname(cmd).split('/')[1] + Log(command+ ' = ' + dirpath+'/'+filepath + arg) + # launch + pid=None + try: + pid = subprocess.Popen(filepath+arg,shell=True,cwd=dirpath).pid + except Exception as e: + Error('Exception launching ' + filepath + str(e)) + if pid == None or pid < 1 : + ExtensionChildren.append((-1,root)) + Error('Error launching ' + filepath + '.') + else : + ExtensionChildren.append((pid,root)) + Log("Spawned "+ filepath + " PID " + str(pid)) + # wait until install/upgrade is finished + retry = 5*60/10 + while pid !=None and pid > 0: + if retry==0: + break + if Run("ps " + str(pid)) != 0: + Log(filepath + ' still running with PID ' + str(pid)) + time.sleep(10) + else : + pid=0 + retry-=1 + + if retry==0: + Error('More than five minutes has passed. Killing ' + str(pid)) + os.kill(pid,9) + return None + Log(command + ' completed.') + return 0 + class HostingEnvironmentConfig(object): """ Parse Hosting enviromnet config and store in @@ -2584,12 +2879,7 @@ class HostingEnvironmentConfig(object): # <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" /> @@ -2653,9 +2943,17 @@ class HostingEnvironmentConfig(object): Create the user account. Launch ConfigurationConsumer if specified in the config. """ + no_thread = False if DiskActivated == False: - diskThread = threading.Thread(target = self.ActivateResourceDisk) - diskThread.start() + for m in inspect.getmembers(MyDistro): + if 'ActivateResourceDiskNoThread' in m: + no_thread = True + break + if no_thread == True : + MyDistro.ActivateResourceDiskNoThread() + else : + diskThread = threading.Thread(target = self.ActivateResourceDisk) + diskThread.start() User = None Pass = None Expiration = None @@ -2663,14 +2961,6 @@ class HostingEnvironmentConfig(object): for b in self.ApplicationSettings: sname = b.getAttribute("name") svalue = b.getAttribute("value") - if sname == "Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountEncryptedPassword": - Pass = self.DecryptPassword(svalue) - elif sname == "Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountUsername": - User = svalue - elif sname == "Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountExpiration": - Expiration = svalue - elif sname == "Certificate|Microsoft.WindowsAzure.Plugins.RemoteAccess.PasswordEncryption": - Thumbprint = svalue.split(':')[1].upper() if User != None and Pass != None: if User != "root" and User != "" and Pass != "": CreateAccount(User, Pass, Expiration, Thumbprint) @@ -2696,6 +2986,7 @@ class GoalState(Util): Initializes and populates: self.HostingEnvironmentConfig self.SharedConfig + self.ExtensionsConfig self.Certificates """ # @@ -2718,6 +3009,9 @@ class GoalState(Util): # <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> + # <ExtensionsConfig>http://100.67.238.230:80/machine/9c87aa94-3bda-45e3-b2b7-0eb0fca7baff/1552dd64dc254e6884f8d5b8b68aa18f.eg%2Dplug%2Dvm?comp=config&type=extensionsConfig&incarnation=2</ExtensionsConfig> + # <FullConfig>http://100.67.238.230:80/machine/9c87aa94-3bda-45e3-b2b7-0eb0fca7baff/1552dd64dc254e6884f8d5b8b68aa18f.eg%2Dplug%2Dvm?comp=config&type=fullConfig&incarnation=2</FullConfig> + # </Configuration> # </RoleInstance> # </RoleInstanceList> @@ -2749,6 +3043,9 @@ class GoalState(Util): self.CertificatesUrl = None self.CertificatesXml = None self.Certificates = None + self.ExtensionsConfigUrl = None + self.ExtensionsConfigXml = None + self.ExtensionsConfig = None self.RoleInstanceId = None self.ContainerId = None self.LoadBalancerProbePort = None # integer, ?list of integers @@ -2759,9 +3056,11 @@ class GoalState(Util): Parse and populate contained configuration objects. Calls Certificates().Parse() Calls SharedConfig().Parse + Calls ExtensionsConfig().Parse Calls HostingEnvironmentConfig().Parse """ self.reinitialize() + LogIfVerbose(xmlText) node = xml.dom.minidom.parseString(xmlText).childNodes[0] if node.localName != "GoalState": Error("GoalState.Parse: root not GoalState") @@ -2799,6 +3098,7 @@ class GoalState(Util): elif d.localName == "Configuration": for e in d.childNodes: if e.nodeType == node.ELEMENT_NODE: + LogIfVerbose(e.localName) if e.localName == "HostingEnvironmentConfig": self.HostingEnvironmentConfigUrl = GetNodeTextData(e) LogIfVerbose("HostingEnvironmentConfigUrl:" + self.HostingEnvironmentConfigUrl) @@ -2809,6 +3109,10 @@ class GoalState(Util): LogIfVerbose("SharedConfigUrl:" + self.SharedConfigUrl) self.SharedConfigXml = self.HttpGetWithHeaders(self.SharedConfigUrl) self.SharedConfig = SharedConfig().Parse(self.SharedConfigXml) + elif e.localName == "ExtensionsConfig": + self.ExtensionsConfigUrl = GetNodeTextData(e) + LogIfVerbose("ExtensionsConfigUrl:" + self.ExtensionsConfigUrl) + self.ExtensionsConfigXml = self.HttpGetWithHeaders(self.ExtensionsConfigUrl) elif e.localName == "Certificates": self.CertificatesUrl = GetNodeTextData(e) LogIfVerbose("CertificatesUrl:" + self.CertificatesUrl) @@ -2894,6 +3198,7 @@ class OvfEnv(object): Return self. """ self.reinitialize() + LogIfVerbose(xmlText) dom = xml.dom.minidom.parseString(xmlText) if len(dom.getElementsByTagNameNS(self.OvfNs, "Environment")) != 1: Error("Unable to parse OVF XML.") @@ -2939,22 +3244,28 @@ class OvfEnv(object): if len(disableSshPass) != 0: self.DisableSshPasswordAuthentication = (GetNodeTextData(disableSshPass[0]).lower() == "true") for pkey in section.getElementsByTagNameNS(self.WaNs, "PublicKey"): + LogIfVerbose(repr(pkey)) fp = None path = None for c in pkey.childNodes: if c.localName == "Fingerprint": fp = GetNodeTextData(c).upper() + LogIfVerbose(fp) if c.localName == "Path": path = GetNodeTextData(c) + LogIfVerbose(path) self.SshPublicKeys += [[fp, path]] for keyp in section.getElementsByTagNameNS(self.WaNs, "KeyPair"): fp = None path = None + LogIfVerbose(repr(keyp)) for c in keyp.childNodes: if c.localName == "Fingerprint": fp = GetNodeTextData(c).upper() + LogIfVerbose(fp) if c.localName == "Path": path = GetNodeTextData(c) + LogIfVerbose(path) self.SshKeyPairs += [[fp, path]] return self @@ -3658,11 +3969,12 @@ class Agent(Util): Run("ssh-keygen -N '' -t " + type + " -f /etc/ssh/ssh_host_" + type + "_key") MyDistro.restartSshService() #SetFileContents(LibDir + "/provisioned", "") + dvd = None 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: + if dvds == None : continue dvd = '/dev/'+dvds.group(0) - if MyDistro.dvdHasMedia(dvd) is False : + if MyDistro.mediaHasFilesystem(dvd) is False : out=MyDistro.load_ata_piix() if out: return out @@ -3694,6 +4006,7 @@ class Agent(Util): 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. + ovfxml=ovfxml[ovfxml.find('<?'):] # chop leading text if present SetFileContents("ovf-env.xml", re.sub("<UserPassword>.*?<", "<UserPassword>*<", ovfxml)) Run("umount " + dvd,chk_err=False) MyDistro.unload_ata_piix() @@ -3739,15 +4052,19 @@ class Agent(Util): # Determine if we are in VMM. Spawn VMM_STARTUP_SCRIPT_NAME if found. self.SearchForVMMStartup() - - if MyDistro.GetIpv4Address() == None: - Log("Waiting for network.") - while(MyDistro.GetIpv4Address() == None): + ipv4='' + while ipv4 == '' or ipv4 == '0.0.0.0' : + ipv4=MyDistro.GetIpv4Address() + if ipv4 == '' or ipv4 == '0.0.0.0' : + Log("Waiting for network.") time.sleep(10) - Log("IPv4 address: " + MyDistro.GetIpv4Address()) - Log("MAC address: " + ":".join(["%02X" % Ord(a) for a in MyDistro.GetMacAddress()])) - + Log("IPv4 address: " + ipv4) + mac='' + mac=MyDistro.GetMacAddress() + if len(mac)>0 : + Log("MAC address: " + ":".join(["%02X" % Ord(a) for a in mac])) + # Consume Entropy in ACPI table provided by Hyper-V try: SetFileContents("/dev/random", GetFileContents("/sys/firmware/acpi/tables/OEM0")) @@ -3837,7 +4154,11 @@ class Agent(Util): if provisionError != None: incarnation = self.ReportNotReady("ProvisioningFailed", provisionError) else: - incarnation = self.ReportReady() + incarnation = self.ReportReady() + # Process our extensions. + if goalState.ExtensionsConfig == None and goalState.ExtensionsConfigXml != None : + goalState.ExtensionsConfig = ExtensionsConfig().Parse(goalState.ExtensionsConfigXml) + # TODO report the status/heartbeat results of extension processing time.sleep(25 - sleepToReduceAccessDenied) WaagentLogrotate = """\ @@ -4003,6 +4324,8 @@ def GetMyDistro(dist_class_name=''): else : # I know this is not Linux! if 'FreeBSD' in platform.system(): Distro=platform.system() + Distro=Distro.strip('"') + Distro=Distro.strip(' ') dist_class_name=Distro+'Distro' else: Distro=dist_class_name |