diff options
Diffstat (limited to 'cloudinit/sources/helpers')
| -rwxr-xr-x[-rw-r--r--] | cloudinit/sources/helpers/azure.py | 345 | ||||
| -rw-r--r-- | cloudinit/sources/helpers/openstack.py | 40 | ||||
| -rw-r--r-- | cloudinit/sources/helpers/vmware/imc/config_custom_script.py | 143 | ||||
| -rw-r--r-- | cloudinit/sources/helpers/vmware/imc/guestcust_error.py | 1 | ||||
| -rw-r--r-- | cloudinit/sources/helpers/vmware/imc/guestcust_util.py | 37 | 
5 files changed, 404 insertions, 162 deletions
| diff --git a/cloudinit/sources/helpers/azure.py b/cloudinit/sources/helpers/azure.py index e5696b1f..fc760581 100644..100755 --- a/cloudinit/sources/helpers/azure.py +++ b/cloudinit/sources/helpers/azure.py @@ -7,6 +7,7 @@ import re  import socket  import struct  import time +import textwrap  from cloudinit.net import dhcp  from cloudinit import stages @@ -16,9 +17,162 @@ from xml.etree import ElementTree  from cloudinit import url_helper  from cloudinit import util +from cloudinit import version +from cloudinit import distros +from cloudinit.reporting import events +from cloudinit.net.dhcp import EphemeralDHCPv4 +from datetime import datetime  LOG = logging.getLogger(__name__) +# This endpoint matches the format as found in dhcp lease files, since this +# value is applied if the endpoint can't be found within a lease file +DEFAULT_WIRESERVER_ENDPOINT = "a8:3f:81:10" + +BOOT_EVENT_TYPE = 'boot-telemetry' +SYSTEMINFO_EVENT_TYPE = 'system-info' +DIAGNOSTIC_EVENT_TYPE = 'diagnostic' + +azure_ds_reporter = events.ReportEventStack( +    name="azure-ds", +    description="initialize reporter for azure ds", +    reporting_enabled=True) + + +def azure_ds_telemetry_reporter(func): +    def impl(*args, **kwargs): +        with events.ReportEventStack( +                name=func.__name__, +                description=func.__name__, +                parent=azure_ds_reporter): +            return func(*args, **kwargs) +    return impl + + +def is_byte_swapped(previous_id, current_id): +    """ +    Azure stores the instance ID with an incorrect byte ordering for the +    first parts. This corrects the byte order such that it is consistent with +    that returned by the metadata service. +    """ +    if previous_id == current_id: +        return False + +    def swap_bytestring(s, width=2): +        dd = [byte for byte in textwrap.wrap(s, 2)] +        dd.reverse() +        return ''.join(dd) + +    parts = current_id.split('-') +    swapped_id = '-'.join([ +            swap_bytestring(parts[0]), +            swap_bytestring(parts[1]), +            swap_bytestring(parts[2]), +            parts[3], +            parts[4] +        ]) + +    return previous_id == swapped_id + + +@azure_ds_telemetry_reporter +def get_boot_telemetry(): +    """Report timestamps related to kernel initialization and systemd +       activation of cloud-init""" +    if not distros.uses_systemd(): +        raise RuntimeError( +            "distro not using systemd, skipping boot telemetry") + +    LOG.debug("Collecting boot telemetry") +    try: +        kernel_start = float(time.time()) - float(util.uptime()) +    except ValueError: +        raise RuntimeError("Failed to determine kernel start timestamp") + +    try: +        out, _ = util.subp(['/bin/systemctl', +                            'show', '-p', +                            'UserspaceTimestampMonotonic'], +                           capture=True) +        tsm = None +        if out and '=' in out: +            tsm = out.split("=")[1] + +        if not tsm: +            raise RuntimeError("Failed to parse " +                               "UserspaceTimestampMonotonic from systemd") + +        user_start = kernel_start + (float(tsm) / 1000000) +    except util.ProcessExecutionError as e: +        raise RuntimeError("Failed to get UserspaceTimestampMonotonic: %s" +                           % e) +    except ValueError as e: +        raise RuntimeError("Failed to parse " +                           "UserspaceTimestampMonotonic from systemd: %s" +                           % e) + +    try: +        out, _ = util.subp(['/bin/systemctl', 'show', +                            'cloud-init-local', '-p', +                            'InactiveExitTimestampMonotonic'], +                           capture=True) +        tsm = None +        if out and '=' in out: +            tsm = out.split("=")[1] +        if not tsm: +            raise RuntimeError("Failed to parse " +                               "InactiveExitTimestampMonotonic from systemd") + +        cloudinit_activation = kernel_start + (float(tsm) / 1000000) +    except util.ProcessExecutionError as e: +        raise RuntimeError("Failed to get InactiveExitTimestampMonotonic: %s" +                           % e) +    except ValueError as e: +        raise RuntimeError("Failed to parse " +                           "InactiveExitTimestampMonotonic from systemd: %s" +                           % e) + +    evt = events.ReportingEvent( +        BOOT_EVENT_TYPE, 'boot-telemetry', +        "kernel_start=%s user_start=%s cloudinit_activation=%s" % +        (datetime.utcfromtimestamp(kernel_start).isoformat() + 'Z', +         datetime.utcfromtimestamp(user_start).isoformat() + 'Z', +         datetime.utcfromtimestamp(cloudinit_activation).isoformat() + 'Z'), +        events.DEFAULT_EVENT_ORIGIN) +    events.report_event(evt) + +    # return the event for unit testing purpose +    return evt + + +@azure_ds_telemetry_reporter +def get_system_info(): +    """Collect and report system information""" +    info = util.system_info() +    evt = events.ReportingEvent( +        SYSTEMINFO_EVENT_TYPE, 'system information', +        "cloudinit_version=%s, kernel_version=%s, variant=%s, " +        "distro_name=%s, distro_version=%s, flavor=%s, " +        "python_version=%s" % +        (version.version_string(), info['release'], info['variant'], +         info['dist'][0], info['dist'][1], info['dist'][2], +         info['python']), events.DEFAULT_EVENT_ORIGIN) +    events.report_event(evt) + +    # return the event for unit testing purpose +    return evt + + +def report_diagnostic_event(str): +    """Report a diagnostic event""" +    evt = events.ReportingEvent( +        DIAGNOSTIC_EVENT_TYPE, 'diagnostic message', +        str, events.DEFAULT_EVENT_ORIGIN) +    events.report_event(evt) + +    # return the event for unit testing purpose +    return evt +  @contextmanager  def cd(newdir): @@ -56,14 +210,16 @@ class AzureEndpointHttpClient(object):          if secure:              headers = self.headers.copy()              headers.update(self.extra_secure_headers) -        return url_helper.read_file_or_url(url, headers=headers) +        return url_helper.read_file_or_url(url, headers=headers, timeout=5, +                                           retries=10)      def post(self, url, data=None, extra_headers=None):          headers = self.headers          if extra_headers is not None:              headers = self.headers.copy()              headers.update(extra_headers) -        return url_helper.read_file_or_url(url, data=data, headers=headers) +        return url_helper.read_file_or_url(url, data=data, headers=headers, +                                           timeout=5, retries=10)  class GoalState(object): @@ -119,6 +275,7 @@ class OpenSSLManager(object):      def clean_up(self):          util.del_dir(self.tmpdir) +    @azure_ds_telemetry_reporter      def generate_certificate(self):          LOG.debug('Generating certificate for communication with fabric...')          if self.certificate is not None: @@ -138,9 +295,40 @@ class OpenSSLManager(object):              self.certificate = certificate          LOG.debug('New certificate generated.') -    def parse_certificates(self, certificates_xml): -        tag = ElementTree.fromstring(certificates_xml).find( -            './/Data') +    @staticmethod +    @azure_ds_telemetry_reporter +    def _run_x509_action(action, cert): +        cmd = ['openssl', 'x509', '-noout', action] +        result, _ = util.subp(cmd, data=cert) +        return result + +    @azure_ds_telemetry_reporter +    def _get_ssh_key_from_cert(self, certificate): +        pub_key = self._run_x509_action('-pubkey', certificate) +        keygen_cmd = ['ssh-keygen', '-i', '-m', 'PKCS8', '-f', '/dev/stdin'] +        ssh_key, _ = util.subp(keygen_cmd, data=pub_key) +        return ssh_key + +    @azure_ds_telemetry_reporter +    def _get_fingerprint_from_cert(self, certificate): +        """openssl x509 formats fingerprints as so: +        'SHA1 Fingerprint=07:3E:19:D1:4D:1C:79:92:24:C6:A0:FD:8D:DA:\ +        B6:A8:BF:27:D4:73\n' + +        Azure control plane passes that fingerprint as so: +        '073E19D14D1C799224C6A0FD8DDAB6A8BF27D473' +        """ +        raw_fp = self._run_x509_action('-fingerprint', certificate) +        eq = raw_fp.find('=') +        octets = raw_fp[eq+1:-1].split(':') +        return ''.join(octets) + +    @azure_ds_telemetry_reporter +    def _decrypt_certs_from_xml(self, certificates_xml): +        """Decrypt the certificates XML document using the our private key; +           return the list of certs and private keys contained in the doc. +        """ +        tag = ElementTree.fromstring(certificates_xml).find('.//Data')          certificates_content = tag.text          lines = [              b'MIME-Version: 1.0', @@ -151,32 +339,31 @@ class OpenSSLManager(object):              certificates_content.encode('utf-8'),          ]          with cd(self.tmpdir): -            with open('Certificates.p7m', 'wb') as f: -                f.write(b'\n'.join(lines))              out, _ = util.subp( -                'openssl cms -decrypt -in Certificates.p7m -inkey' +                'openssl cms -decrypt -in /dev/stdin -inkey'                  ' {private_key} -recip {certificate} | openssl pkcs12 -nodes'                  ' -password pass:'.format(**self.certificate_names), -                shell=True) -        private_keys, certificates = [], [] +                shell=True, data=b'\n'.join(lines)) +        return out + +    @azure_ds_telemetry_reporter +    def parse_certificates(self, certificates_xml): +        """Given the Certificates XML document, return a dictionary of +           fingerprints and associated SSH keys derived from the certs.""" +        out = self._decrypt_certs_from_xml(certificates_xml)          current = [] +        keys = {}          for line in out.splitlines():              current.append(line)              if re.match(r'[-]+END .*?KEY[-]+$', line): -                private_keys.append('\n'.join(current)) +                # ignore private_keys                  current = []              elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line): -                certificates.append('\n'.join(current)) +                certificate = '\n'.join(current) +                ssh_key = self._get_ssh_key_from_cert(certificate) +                fingerprint = self._get_fingerprint_from_cert(certificate) +                keys[fingerprint] = ssh_key                  current = [] -        keys = [] -        for certificate in certificates: -            with cd(self.tmpdir): -                public_key, _ = util.subp( -                    'openssl x509 -noout -pubkey |' -                    'ssh-keygen -i -m PKCS8 -f /dev/stdin', -                    data=certificate, -                    shell=True) -            keys.append(public_key)          return keys @@ -206,7 +393,6 @@ class WALinuxAgentShim(object):          self.dhcpoptions = dhcp_options          self._endpoint = None          self.openssl_manager = None -        self.values = {}          self.lease_file = fallback_lease_file      def clean_up(self): @@ -241,14 +427,21 @@ class WALinuxAgentShim(object):          return socket.inet_ntoa(packed_bytes)      @staticmethod +    @azure_ds_telemetry_reporter      def _networkd_get_value_from_leases(leases_d=None):          return dhcp.networkd_get_option_from_leases(              'OPTION_245', leases_d=leases_d)      @staticmethod +    @azure_ds_telemetry_reporter      def _get_value_from_leases_file(fallback_lease_file):          leases = [] -        content = util.load_file(fallback_lease_file) +        try: +            content = util.load_file(fallback_lease_file) +        except IOError as ex: +            LOG.error("Failed to read %s: %s", fallback_lease_file, ex) +            return None +          LOG.debug("content is %s", content)          option_name = _get_dhcp_endpoint_option_name()          for line in content.splitlines(): @@ -263,6 +456,7 @@ class WALinuxAgentShim(object):              return leases[-1]      @staticmethod +    @azure_ds_telemetry_reporter      def _load_dhclient_json():          dhcp_options = {}          hooks_dir = WALinuxAgentShim._get_hooks_dir() @@ -281,6 +475,7 @@ class WALinuxAgentShim(object):          return dhcp_options      @staticmethod +    @azure_ds_telemetry_reporter      def _get_value_from_dhcpoptions(dhcp_options):          if dhcp_options is None:              return None @@ -294,22 +489,26 @@ class WALinuxAgentShim(object):          return _value      @staticmethod +    @azure_ds_telemetry_reporter      def find_endpoint(fallback_lease_file=None, dhcp245=None):          value = None          if dhcp245 is not None:              value = dhcp245              LOG.debug("Using Azure Endpoint from dhcp options")          if value is None: +            report_diagnostic_event("No Azure endpoint from dhcp options")              LOG.debug('Finding Azure endpoint from networkd...')              value = WALinuxAgentShim._networkd_get_value_from_leases()          if value is None:              # Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json              # a dhclient exit hook that calls cloud-init-dhclient-hook +            report_diagnostic_event("No Azure endpoint from networkd")              LOG.debug('Finding Azure endpoint from hook json...')              dhcp_options = WALinuxAgentShim._load_dhclient_json()              value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options)          if value is None:              # Fallback and check the leases file if unsuccessful +            report_diagnostic_event("No Azure endpoint from dhclient logs")              LOG.debug("Unable to find endpoint in dhclient logs. "                        " Falling back to check lease files")              if fallback_lease_file is None: @@ -320,16 +519,22 @@ class WALinuxAgentShim(object):                            fallback_lease_file)                  value = WALinuxAgentShim._get_value_from_leases_file(                      fallback_lease_file) -          if value is None: -            raise ValueError('No endpoint found.') +            msg = "No lease found; using default endpoint" +            report_diagnostic_event(msg) +            LOG.warning(msg) +            value = DEFAULT_WIRESERVER_ENDPOINT          endpoint_ip_address = WALinuxAgentShim.get_ip_from_lease_value(value) -        LOG.debug('Azure endpoint found at %s', endpoint_ip_address) +        msg = 'Azure endpoint found at %s' % endpoint_ip_address +        report_diagnostic_event(msg) +        LOG.debug(msg)          return endpoint_ip_address -    def register_with_azure_and_fetch_data(self): -        self.openssl_manager = OpenSSLManager() +    @azure_ds_telemetry_reporter +    def register_with_azure_and_fetch_data(self, pubkey_info=None): +        if self.openssl_manager is None: +            self.openssl_manager = OpenSSLManager()          http_client = AzureEndpointHttpClient(self.openssl_manager.certificate)          LOG.info('Registering with Azure...')          attempts = 0 @@ -337,27 +542,52 @@ class WALinuxAgentShim(object):              try:                  response = http_client.get(                      'http://{0}/machine/?comp=goalstate'.format(self.endpoint)) -            except Exception: +            except Exception as e:                  if attempts < 10:                      time.sleep(attempts + 1)                  else: +                    report_diagnostic_event( +                        "failed to register with Azure: %s" % e)                      raise              else:                  break              attempts += 1          LOG.debug('Successfully fetched GoalState XML.')          goal_state = GoalState(response.contents, http_client) -        public_keys = [] -        if goal_state.certificates_xml is not None: +        report_diagnostic_event("container_id %s" % goal_state.container_id) +        ssh_keys = [] +        if goal_state.certificates_xml is not None and pubkey_info is not None:              LOG.debug('Certificate XML found; parsing out public keys.') -            public_keys = self.openssl_manager.parse_certificates( +            keys_by_fingerprint = self.openssl_manager.parse_certificates(                  goal_state.certificates_xml) -        data = { -            'public-keys': public_keys, -        } +            ssh_keys = self._filter_pubkeys(keys_by_fingerprint, pubkey_info)          self._report_ready(goal_state, http_client) -        return data +        return {'public-keys': ssh_keys} + +    def _filter_pubkeys(self, keys_by_fingerprint, pubkey_info): +        """cloud-init expects a straightforward array of keys to be dropped +           into the user's authorized_keys file. Azure control plane exposes +           multiple public keys to the VM via wireserver. Select just the +           user's key(s) and return them, ignoring any other certs. +        """ +        keys = [] +        for pubkey in pubkey_info: +            if 'value' in pubkey and pubkey['value']: +                keys.append(pubkey['value']) +            elif 'fingerprint' in pubkey and pubkey['fingerprint']: +                fingerprint = pubkey['fingerprint'] +                if fingerprint in keys_by_fingerprint: +                    keys.append(keys_by_fingerprint[fingerprint]) +                else: +                    LOG.warning("ovf-env.xml specified PublicKey fingerprint " +                                "%s not found in goalstate XML", fingerprint) +            else: +                LOG.warning("ovf-env.xml specified PublicKey with neither " +                            "value nor fingerprint: %s", pubkey) +        return keys + +    @azure_ds_telemetry_reporter      def _report_ready(self, goal_state, http_client):          LOG.debug('Reporting ready to Azure fabric.')          document = self.REPORT_READY_XML_TEMPLATE.format( @@ -365,20 +595,49 @@ class WALinuxAgentShim(object):              container_id=goal_state.container_id,              instance_id=goal_state.instance_id,          ) -        http_client.post( -            "http://{0}/machine?comp=health".format(self.endpoint), -            data=document, -            extra_headers={'Content-Type': 'text/xml; charset=utf-8'}, -        ) +        # Host will collect kvps when cloud-init reports ready. +        # some kvps might still be in the queue. We yield the scheduler +        # to make sure we process all kvps up till this point. +        time.sleep(0) +        try: +            http_client.post( +                "http://{0}/machine?comp=health".format(self.endpoint), +                data=document, +                extra_headers={'Content-Type': 'text/xml; charset=utf-8'}, +            ) +        except Exception as e: +            report_diagnostic_event("exception while reporting ready: %s" % e) +            raise +          LOG.info('Reported ready to Azure fabric.') -def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None): +@azure_ds_telemetry_reporter +def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None, +                             pubkey_info=None):      shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,                              dhcp_options=dhcp_opts)      try: -        return shim.register_with_azure_and_fetch_data() +        return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info)      finally:          shim.clean_up() + +class EphemeralDHCPv4WithReporting(object): +    def __init__(self, reporter, nic=None): +        self.reporter = reporter +        self.ephemeralDHCPv4 = EphemeralDHCPv4(iface=nic) + +    def __enter__(self): +        with events.ReportEventStack( +                name="obtain-dhcp-lease", +                description="obtain dhcp lease", +                parent=self.reporter): +            return self.ephemeralDHCPv4.__enter__() + +    def __exit__(self, excp_type, excp_value, excp_traceback): +        self.ephemeralDHCPv4.__exit__( +            excp_type, excp_value, excp_traceback) + +  # vi: ts=4 expandtab diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py index 9c29ceac..441db506 100644 --- a/cloudinit/sources/helpers/openstack.py +++ b/cloudinit/sources/helpers/openstack.py @@ -12,15 +12,12 @@ import copy  import functools  import os -import six -  from cloudinit import ec2_utils  from cloudinit import log as logging  from cloudinit import net  from cloudinit import sources  from cloudinit import url_helper  from cloudinit import util -  from cloudinit.sources import BrokenMetadata  # See https://docs.openstack.org/user-guide/cli-config-drive.html @@ -67,7 +64,7 @@ OS_VERSIONS = (      OS_ROCKY,  ) -PHYSICAL_TYPES = ( +KNOWN_PHYSICAL_TYPES = (      None,      'bgpovs',  # not present in OpenStack upstream but used on OVH cloud.      'bridge', @@ -163,8 +160,7 @@ class SourceMixin(object):              return device -@six.add_metaclass(abc.ABCMeta) -class BaseReader(object): +class BaseReader(metaclass=abc.ABCMeta):      def __init__(self, base_path):          self.base_path = base_path @@ -227,7 +223,7 @@ class BaseReader(object):          """          load_json_anytype = functools.partial( -            util.load_json, root_types=(dict, list) + six.string_types) +            util.load_json, root_types=(dict, list, str))          def datafiles(version):              files = {} @@ -584,25 +580,31 @@ def convert_net_json(network_json=None, known_macs=None):                          if n['link'] == link['id']]:              subnet = dict((k, v) for k, v in network.items()                            if k in valid_keys['subnet']) -            if 'dhcp' in network['type']: -                t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4' -                subnet.update({ -                    'type': t, -                }) -            else: + +            if network['type'] == 'ipv4_dhcp': +                subnet.update({'type': 'dhcp4'}) +            elif network['type'] == 'ipv6_dhcp': +                subnet.update({'type': 'dhcp6'}) +            elif network['type'] in ['ipv6_slaac', 'ipv6_dhcpv6-stateless', +                                     'ipv6_dhcpv6-stateful']: +                subnet.update({'type': network['type']}) +            elif network['type'] in ['ipv4', 'ipv6']:                  subnet.update({                      'type': 'static',                      'address': network.get('ip_address'),                  }) + +            # Enable accept_ra for stateful and legacy ipv6_dhcp types +            if network['type'] in ['ipv6_dhcpv6-stateful', 'ipv6_dhcp']: +                cfg.update({'accept-ra': True}) +              if network['type'] == 'ipv4':                  subnet['ipv4'] = True              if network['type'] == 'ipv6':                  subnet['ipv6'] = True              subnets.append(subnet)          cfg.update({'subnets': subnets}) -        if link['type'] in PHYSICAL_TYPES: -            cfg.update({'type': 'physical', 'mac_address': link_mac_addr}) -        elif link['type'] in ['bond']: +        if link['type'] in ['bond']:              params = {}              if link_mac_addr:                  params['mac_address'] = link_mac_addr @@ -641,8 +643,10 @@ def convert_net_json(network_json=None, known_macs=None):              curinfo.update({'mac': link['vlan_mac_address'],                              'name': name})          else: -            raise ValueError( -                'Unknown network_data link type: %s' % link['type']) +            if link['type'] not in KNOWN_PHYSICAL_TYPES: +                LOG.warning('Unknown network_data link type (%s); treating as' +                            ' physical', link['type']) +            cfg.update({'type': 'physical', 'mac_address': link_mac_addr})          config.append(cfg)          link_id_info[curinfo['id']] = curinfo diff --git a/cloudinit/sources/helpers/vmware/imc/config_custom_script.py b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py index a7d4ad91..9f14770e 100644 --- a/cloudinit/sources/helpers/vmware/imc/config_custom_script.py +++ b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py @@ -1,5 +1,5 @@  # Copyright (C) 2017 Canonical Ltd. -# Copyright (C) 2017 VMware Inc. +# Copyright (C) 2017-2019 VMware Inc.  #  # Author: Maitreyee Saikia <msaikia@vmware.com>  # @@ -8,7 +8,6 @@  import logging  import os  import stat -from textwrap import dedent  from cloudinit import util @@ -20,12 +19,15 @@ class CustomScriptNotFound(Exception):  class CustomScriptConstant(object): -    RC_LOCAL = "/etc/rc.local" -    POST_CUST_TMP_DIR = "/root/.customization" -    POST_CUST_RUN_SCRIPT_NAME = "post-customize-guest.sh" -    POST_CUST_RUN_SCRIPT = os.path.join(POST_CUST_TMP_DIR, -                                        POST_CUST_RUN_SCRIPT_NAME) -    POST_REBOOT_PENDING_MARKER = "/.guest-customization-post-reboot-pending" +    CUSTOM_TMP_DIR = "/root/.customization" + +    # The user defined custom script +    CUSTOM_SCRIPT_NAME = "customize.sh" +    CUSTOM_SCRIPT = os.path.join(CUSTOM_TMP_DIR, +                                 CUSTOM_SCRIPT_NAME) +    POST_CUSTOM_PENDING_MARKER = "/.guest-customization-post-reboot-pending" +    # The cc_scripts_per_instance script to launch custom script +    POST_CUSTOM_SCRIPT_NAME = "post-customize-guest.sh"  class RunCustomScript(object): @@ -39,10 +41,19 @@ class RunCustomScript(object):              raise CustomScriptNotFound("Script %s not found!! "                                         "Cannot execute custom script!"                                         % self.scriptpath) + +        util.ensure_dir(CustomScriptConstant.CUSTOM_TMP_DIR) + +        LOG.debug("Copying custom script to %s", +                  CustomScriptConstant.CUSTOM_SCRIPT) +        util.copy(self.scriptpath, CustomScriptConstant.CUSTOM_SCRIPT) +          # Strip any CR characters from the decoded script -        util.load_file(self.scriptpath).replace("\r", "") -        st = os.stat(self.scriptpath) -        os.chmod(self.scriptpath, st.st_mode | stat.S_IEXEC) +        content = util.load_file( +            CustomScriptConstant.CUSTOM_SCRIPT).replace("\r", "") +        util.write_file(CustomScriptConstant.CUSTOM_SCRIPT, +                        content, +                        mode=0o544)  class PreCustomScript(RunCustomScript): @@ -50,104 +61,34 @@ class PreCustomScript(RunCustomScript):          """Executing custom script with precustomization argument."""          LOG.debug("Executing pre-customization script")          self.prepare_script() -        util.subp(["/bin/sh", self.scriptpath, "precustomization"]) +        util.subp([CustomScriptConstant.CUSTOM_SCRIPT, "precustomization"])  class PostCustomScript(RunCustomScript): -    def __init__(self, scriptname, directory): +    def __init__(self, scriptname, directory, ccScriptsDir):          super(PostCustomScript, self).__init__(scriptname, directory) -        # Determine when to run custom script. When postreboot is True, -        # the user uploaded script will run as part of rc.local after -        # the machine reboots. This is determined by presence of rclocal. -        # When postreboot is False, script will run as part of cloud-init. -        self.postreboot = False - -    def _install_post_reboot_agent(self, rclocal): -        """ -        Install post-reboot agent for running custom script after reboot. -        As part of this process, we are editing the rclocal file to run a -        VMware script, which in turn is resposible for handling the user -        script. -        @param: path to rc local. -        """ -        LOG.debug("Installing post-reboot customization from %s to %s", -                  self.directory, rclocal) -        if not self.has_previous_agent(rclocal): -            LOG.info("Adding post-reboot customization agent to rc.local") -            new_content = dedent(""" -                # Run post-reboot guest customization -                /bin/sh %s -                exit 0 -                """) % CustomScriptConstant.POST_CUST_RUN_SCRIPT -            existing_rclocal = util.load_file(rclocal).replace('exit 0\n', '') -            st = os.stat(rclocal) -            # "x" flag should be set -            mode = st.st_mode | stat.S_IEXEC -            util.write_file(rclocal, existing_rclocal + new_content, mode) - -        else: -            # We don't need to update rclocal file everytime a customization -            # is requested. It just needs to be done for the first time. -            LOG.info("Post-reboot guest customization agent is already " -                     "registered in rc.local") -        LOG.debug("Installing post-reboot customization agent finished: %s", -                  self.postreboot) - -    def has_previous_agent(self, rclocal): -        searchstring = "# Run post-reboot guest customization" -        if searchstring in open(rclocal).read(): -            return True -        return False - -    def find_rc_local(self): -        """ -        Determine if rc local is present. -        """ -        rclocal = "" -        if os.path.exists(CustomScriptConstant.RC_LOCAL): -            LOG.debug("rc.local detected.") -            # resolving in case of symlink -            rclocal = os.path.realpath(CustomScriptConstant.RC_LOCAL) -            LOG.debug("rc.local resolved to %s", rclocal) -        else: -            LOG.warning("Can't find rc.local, post-customization " -                        "will be run before reboot") -        return rclocal - -    def install_agent(self): -        rclocal = self.find_rc_local() -        if rclocal: -            self._install_post_reboot_agent(rclocal) -            self.postreboot = True +        self.ccScriptsDir = ccScriptsDir +        self.ccScriptPath = os.path.join( +            ccScriptsDir, +            CustomScriptConstant.POST_CUSTOM_SCRIPT_NAME)      def execute(self):          """ -        This method executes post-customization script before or after reboot -        based on the presence of rc local. +        This method copy the post customize run script to +        cc_scripts_per_instance directory and let this +        module to run post custom script.          """          self.prepare_script() -        self.install_agent() -        if not self.postreboot: -            LOG.warning("Executing post-customization script inline") -            util.subp(["/bin/sh", self.scriptpath, "postcustomization"]) -        else: -            LOG.debug("Scheduling custom script to run post reboot") -            if not os.path.isdir(CustomScriptConstant.POST_CUST_TMP_DIR): -                os.mkdir(CustomScriptConstant.POST_CUST_TMP_DIR) -            # Script "post-customize-guest.sh" and user uploaded script are -            # are present in the same directory and needs to copied to a temp -            # directory to be executed post reboot. User uploaded script is -            # saved as customize.sh in the temp directory. -            # post-customize-guest.sh excutes customize.sh after reboot. -            LOG.debug("Copying post-customization script") -            util.copy(self.scriptpath, -                      CustomScriptConstant.POST_CUST_TMP_DIR + "/customize.sh") -            LOG.debug("Copying script to run post-customization script") -            util.copy( -                os.path.join(self.directory, -                             CustomScriptConstant.POST_CUST_RUN_SCRIPT_NAME), -                CustomScriptConstant.POST_CUST_RUN_SCRIPT) -            LOG.info("Creating post-reboot pending marker") -            util.ensure_file(CustomScriptConstant.POST_REBOOT_PENDING_MARKER) + +        LOG.debug("Copying post customize run script to %s", +                  self.ccScriptPath) +        util.copy( +            os.path.join(self.directory, +                         CustomScriptConstant.POST_CUSTOM_SCRIPT_NAME), +            self.ccScriptPath) +        st = os.stat(self.ccScriptPath) +        os.chmod(self.ccScriptPath, st.st_mode | stat.S_IEXEC) +        LOG.info("Creating post customization pending marker") +        util.ensure_file(CustomScriptConstant.POST_CUSTOM_PENDING_MARKER)  # vi: ts=4 expandtab diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py index db5a00dc..65ae7390 100644 --- a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py +++ b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py @@ -10,5 +10,6 @@ class GuestCustErrorEnum(object):      """Specifies different errors of Guest Customization engine"""      GUESTCUST_ERROR_SUCCESS = 0 +    GUESTCUST_ERROR_SCRIPT_DISABLED = 6  # vi: ts=4 expandtab diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py index a590f323..3d369d04 100644 --- a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py +++ b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py @@ -7,6 +7,7 @@  import logging  import os +import re  import time  from cloudinit import util @@ -117,4 +118,40 @@ def enable_nics(nics):      logger.warning("Can't connect network interfaces after %d attempts",                     enableNicsWaitRetries) + +def get_tools_config(section, key, defaultVal): +    """ Return the value of [section] key from VMTools configuration. + +        @param section: String of section to read from VMTools config +        @returns: String value from key in [section] or defaultVal if +                  [section] is not present or vmware-toolbox-cmd is +                  not installed. +    """ + +    if not util.which('vmware-toolbox-cmd'): +        logger.debug( +            'vmware-toolbox-cmd not installed, returning default value') +        return defaultVal + +    retValue = defaultVal +    cmd = ['vmware-toolbox-cmd', 'config', 'get', section, key] + +    try: +        (outText, _) = util.subp(cmd) +        m = re.match(r'([^=]+)=(.*)', outText) +        if m: +            retValue = m.group(2).strip() +            logger.debug("Get tools config: [%s] %s = %s", +                         section, key, retValue) +        else: +            logger.debug( +                "Tools config: [%s] %s is not found, return default value: %s", +                section, key, retValue) +    except util.ProcessExecutionError as e: +        logger.error("Failed running %s[%s]", cmd, e.exit_code) +        logger.exception(e) + +    return retValue + +  # vi: ts=4 expandtab | 
