From 8844ffb5988bcfbb8cfbe57d9139c3dcb8b429cc Mon Sep 17 00:00:00 2001 From: Sankar Tanguturi Date: Wed, 18 Nov 2015 16:03:15 -0800 Subject: Add Image Customization Parser for VMware vSphere Hypervisor Support. This is the first changeset submitted as a part of project to add cloud-init support for VMware vSphere Hypervisor. This changeset contains _only_ the changes for a simple python parser for a Image Customization Specification file pushed by VMware vSphere hypervisor into the guest VMs. In a later changeset, will be submitting another patch to actually detect the underlying VMware vSphere hypervisor and do the necessary customization. --- cloudinit/sources/helpers/vmware/__init__.py | 13 ++ cloudinit/sources/helpers/vmware/imc/__init__.py | 13 ++ cloudinit/sources/helpers/vmware/imc/boot_proto.py | 11 + cloudinit/sources/helpers/vmware/imc/config.py | 125 ++++++++++++ .../sources/helpers/vmware/imc/config_file.py | 221 +++++++++++++++++++++ .../sources/helpers/vmware/imc/config_namespace.py | 5 + .../sources/helpers/vmware/imc/config_source.py | 2 + cloudinit/sources/helpers/vmware/imc/ipv4_mode.py | 29 +++ cloudinit/sources/helpers/vmware/imc/nic.py | 107 ++++++++++ 9 files changed, 526 insertions(+) create mode 100644 cloudinit/sources/helpers/vmware/__init__.py create mode 100644 cloudinit/sources/helpers/vmware/imc/__init__.py create mode 100644 cloudinit/sources/helpers/vmware/imc/boot_proto.py create mode 100644 cloudinit/sources/helpers/vmware/imc/config.py create mode 100644 cloudinit/sources/helpers/vmware/imc/config_file.py create mode 100644 cloudinit/sources/helpers/vmware/imc/config_namespace.py create mode 100644 cloudinit/sources/helpers/vmware/imc/config_source.py create mode 100644 cloudinit/sources/helpers/vmware/imc/ipv4_mode.py create mode 100644 cloudinit/sources/helpers/vmware/imc/nic.py (limited to 'cloudinit/sources/helpers') diff --git a/cloudinit/sources/helpers/vmware/__init__.py b/cloudinit/sources/helpers/vmware/__init__.py new file mode 100644 index 00000000..386225d5 --- /dev/null +++ b/cloudinit/sources/helpers/vmware/__init__.py @@ -0,0 +1,13 @@ +# vi: ts=4 expandtab +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/cloudinit/sources/helpers/vmware/imc/__init__.py b/cloudinit/sources/helpers/vmware/imc/__init__.py new file mode 100644 index 00000000..386225d5 --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/__init__.py @@ -0,0 +1,13 @@ +# vi: ts=4 expandtab +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/cloudinit/sources/helpers/vmware/imc/boot_proto.py b/cloudinit/sources/helpers/vmware/imc/boot_proto.py new file mode 100644 index 00000000..6c3b070a --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/boot_proto.py @@ -0,0 +1,11 @@ +# from enum import Enum + +class BootProto: + DHCP = 'dhcp' + STATIC = 'static' + +# def __eq__(self, other): +# return self.name == other.name and self.value == other.value +# +# def __ne__(self, other): +# return not self.__eq__(other) diff --git a/cloudinit/sources/helpers/vmware/imc/config.py b/cloudinit/sources/helpers/vmware/imc/config.py new file mode 100644 index 00000000..ea0873fb --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/config.py @@ -0,0 +1,125 @@ +from cloudinit.sources.helpers.vmware.imc.nic import Nic + + +class Config: + DNS = 'DNS|NAMESERVER|' + SUFFIX = 'DNS|SUFFIX|' + PASS = 'PASSWORD|-PASS' + TIMEZONE = 'DATETIME|TIMEZONE' + UTC = 'DATETIME|UTC' + HOSTNAME = 'NETWORK|HOSTNAME' + OMAINNAME = 'NETWORK|DOMAINNAME' + + def __init__(self, configFile): + self._configFile = configFile + + # Retrieves hostname. + # + # Args: + # None + # Results: + # string: hostname + # Throws: + # None + @property + def hostName(self): + return self._configFile.get(Config.HOSTNAME, None) + + # Retrieves domainName. + # + # Args: + # None + # Results: + # string: domainName + # Throws: + # None + @property + def domainName(self): + return self._configFile.get(Config.DOMAINNAME, None) + + # Retrieves timezone. + # + # Args: + # None + # Results: + # string: timezone + # Throws: + # None + @property + def timeZone(self): + return self._configFile.get(Config.TIMEZONE, None) + + # Retrieves whether to set time to UTC or Local. + # + # Args: + # None + # Results: + # boolean: True for yes/YES, True for no/NO, otherwise - None + # Throws: + # None + @property + def utc(self): + return self._configFile.get(Config.UTC, None) + + # Retrieves root password to be set. + # + # Args: + # None + # Results: + # string: base64-encoded root password or None + # Throws: + # None + @property + def adminPassword(self): + return self._configFile.get(Config.PASS, None) + + # Retrieves DNS Servers. + # + # Args: + # None + # Results: + # integer: count or 0 + # Throws: + # None + @property + def nameServers(self): + res = [] + for i in range(1, self._configFile.getCnt(Config.DNS) + 1): + key = Config.DNS + str(i) + res.append(self._configFile[key]) + + return res + + # Retrieves DNS Suffixes. + # + # Args: + # None + # Results: + # integer: count or 0 + # Throws: + # None + @property + def dnsSuffixes(self): + res = [] + for i in range(1, self._configFile.getCnt(Config.SUFFIX) + 1): + key = Config.SUFFIX + str(i) + res.append(self._configFile[key]) + + return res + + # Retrieves NICs. + # + # Args: + # None + # Results: + # integer: count + # Throws: + # None + @property + def nics(self): + res = [] + nics = self._configFile['NIC-CONFIG|NICS'] + for nic in nics.split(','): + res.append(Nic(nic, self._configFile)) + + return res diff --git a/cloudinit/sources/helpers/vmware/imc/config_file.py b/cloudinit/sources/helpers/vmware/imc/config_file.py new file mode 100644 index 00000000..3f9938da --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/config_file.py @@ -0,0 +1,221 @@ +import logging +import re + +from cloudinit.sources.helpers.vmware.imc.config_source import ConfigSource + +logger = logging.getLogger(__name__) + + +class ConfigFile(ConfigSource): + def __init__(self): + self._configData = {} + + def __getitem__(self, key): + return self._configData[key] + + def get(self, key, default=None): + return self._configData.get(key, default) + + # Removes all the properties. + # + # Args: + # None + # Results: + # None + # Throws: + # None + def clear(self): + self._configData.clear() + + # Inserts k/v pair. + # + # Does not do any key/cross-key validation. + # + # Args: + # key: string: key + # val: string: value + # Results: + # None + # Throws: + # None + def _insertKey(self, key, val): + # cleaning up on all "input" path + + # remove end char \n (chomp) + key = key.strip() + val = val.strip() + + if key.startswith('-') or '|-' in key: + canLog = 0 + else: + canLog = 1 + + # "sensitive" settings shall not be logged + if canLog: + logger.debug("ADDED KEY-VAL :: '%s' = '%s'" % (key, val)) + else: + logger.debug("ADDED KEY-VAL :: '%s' = '*****************'" % key) + + self._configData[key] = val + + # Determines properties count. + # + # Args: + # None + # Results: + # integer: properties count + # Throws: + # None + def size(self): + return len(self._configData) + + # Parses properties from a .cfg file content. + # + # Any previously available properties will be removed. + # + # Sensitive data will not be logged in case key starts from '-'. + # + # Args: + # content: string: e.g. content of config/cust.cfg + # Results: + # None + # Throws: + # None + def loadConfigContent(self, content): + self.clear() + + # remove end char \n (chomp) + for line in content.split('\n'): + # TODO validate against allowed characters (not done in Perl) + + # spaces at the end are not allowed, things like passwords must be + # at least base64-encoded + line = line.strip() + + # "sensitive" settings shall not be logged + if line.startswith('-'): + canLog = 0 + else: + canLog = 1 + + if canLog: + logger.debug("Processing line: '%s'" % line) + else: + logger.debug("Processing line: '***********************'") + + if not line: + logger.debug("Empty line. Ignored.") + continue + + if line.startswith('#'): + logger.debug("Comment found. Line ignored.") + continue + + matchObj = re.match(r'\[(.+)\]', line) + if matchObj: + category = matchObj.group(1) + logger.debug("FOUND CATEGORY = '%s'" % category) + else: + # POSIX.2 regex doesn't support non-greedy like in (.+?)=(.*) + # key value pair (non-eager '=' for base64) + matchObj = re.match(r'([^=]+)=(.*)', line) + if matchObj: + # cleaning up on all "input" paths + key = category + "|" + matchObj.group(1).strip() + val = matchObj.group(2).strip() + + self._insertKey(key, val) + else: + # TODO document + raise Exception("Unrecognizable line: '%s'" % line) + + self.validate() + + # Parses properties from a .cfg file + # + # Any previously available properties will be removed. + # + # Sensitive data will not be logged in case key starts from '-'. + # + # Args: + # filename: string: full path to a .cfg file + # Results: + # None + # Throws: + # None + def loadConfigFile(self, filename): + logger.info("Opening file name %s." % filename) + # TODO what throws? + with open(filename, "r") as myfile: + self.loadConfigContent(myfile.read()) + + # Determines whether a property with a given key exists. + # + # Args: + # key: string: key + # Results: + # boolean: True if such property exists, otherwise - False. + # Throws: + # None + def hasKey(self, key): + return key in self._configData + + # Determines whether a value for a property must be kept. + # + # If the property is missing, it's treated as it should be not changed by + # the engine. + # + # Args: + # key: string: key + # Results: + # boolean: True if property must be kept, otherwise - False. + # Throws: + # None + def keepCurrentValue(self, key): + # helps to distinguish from "empty" value which is used to indicate + # "removal" + return not self.hasKey(key) + + # Determines whether a value for a property must be removed. + # + # If the property is empty, it's treated as it should be removed by the + # engine. + # + # Args: + # key: string: key + # Results: + # boolean: True if property must be removed, otherwise - False. + # Throws: + # None + def removeCurrentValue(self, key): + # helps to distinguish from "missing" value which is used to indicate + # "keeping unchanged" + if self.hasKey(key): + return not bool(self._configData[key]) + else: + return False + + # TODO + def getCnt(self, prefix): + res = 0 + for key in self._configData.keys(): + if key.startswith(prefix): + res += 1 + + return res + + # TODO + # TODO pass base64 + # Throws: + # Dies in case timezone is present but empty. + # Dies in case password is present but empty. + # Dies in case hostname is present but empty or greater than 63 chars. + # Dies in case UTC is present, but is not yes/YES or no/NO. + # Dies in case NICS is not present. + def validate(self): + # TODO must log all the errors + keyValidators = {'NIC1|IPv6GATEWAY|': None} + crossValidators = {} + + for key in self._configData.keys(): + pass diff --git a/cloudinit/sources/helpers/vmware/imc/config_namespace.py b/cloudinit/sources/helpers/vmware/imc/config_namespace.py new file mode 100644 index 00000000..7f76ac8b --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/config_namespace.py @@ -0,0 +1,5 @@ +from cloudinit.sources.helpers.vmware.imc.config_source import ConfigSource + + +class ConfigNamespace(ConfigSource): + pass diff --git a/cloudinit/sources/helpers/vmware/imc/config_source.py b/cloudinit/sources/helpers/vmware/imc/config_source.py new file mode 100644 index 00000000..fad3a389 --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/config_source.py @@ -0,0 +1,2 @@ +class ConfigSource: + pass diff --git a/cloudinit/sources/helpers/vmware/imc/ipv4_mode.py b/cloudinit/sources/helpers/vmware/imc/ipv4_mode.py new file mode 100644 index 00000000..66b4fad7 --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/ipv4_mode.py @@ -0,0 +1,29 @@ +# from enum import Enum + + +# The IPv4 configuration mode which directly represents the user's goal. +# +# This mode effectively acts as a contract of the inguest customization engine. +# It must be set based on what the user has requested via VMODL/generators API +# and should not be changed by those layers. It's up to the in-guest engine to +# interpret and materialize the user's request. +# +# Also defined in linuxconfiggenerator.h. +class Ipv4Mode: + # The legacy mode which only allows dhcp/static based on whether IPv4 + # addresses list is empty or not + IPV4_MODE_BACKWARDS_COMPATIBLE = 'BACKWARDS_COMPATIBLE' + # IPv4 must use static address. Reserved for future use + IPV4_MODE_STATIC = 'STATIC' + # IPv4 must use DHCPv4. Reserved for future use + IPV4_MODE_DHCP = 'DHCP' + # IPv4 must be disabled + IPV4_MODE_DISABLED = 'DISABLED' + # IPv4 settings should be left untouched. Reserved for future use + IPV4_MODE_AS_IS = 'AS_IS' + + # def __eq__(self, other): + # return self.name == other.name and self.value == other.value + # + # def __ne__(self, other): + # return not self.__eq__(other) diff --git a/cloudinit/sources/helpers/vmware/imc/nic.py b/cloudinit/sources/helpers/vmware/imc/nic.py new file mode 100644 index 00000000..b90a5640 --- /dev/null +++ b/cloudinit/sources/helpers/vmware/imc/nic.py @@ -0,0 +1,107 @@ +from cloudinit.sources.helpers.vmware.imc.boot_proto import BootProto + + +class Nic: + def __init__(self, name, configFile): + self._name = name + self._configFile = configFile + + def _get(self, what): + return self._configFile.get(self.name + what, None) + + def _getCnt(self, prefix): + return self._configFile.getCnt(self.name + prefix) + + @property + def name(self): + return self._name + + @property + def mac(self): + return self._get('|MACADDR').lower() + + @property + def bootProto(self): + return self._get('|BOOTPROTO').lower() + + @property + def ipv4(self): + # TODO implement NONE + if self.bootProto == BootProto.STATIC: + return StaticIpv4Conf(self) + + return DhcpIpv4Conf(self) + + @property + def ipv6(self): + # TODO implement NONE + cnt = self._getCnt("|IPv6ADDR|") + + if cnt != 0: + return StaticIpv6Conf(self) + + return DhcpIpv6Conf(self) + + +class DhcpIpv4Conf: + def __init__(self, nic): + self._nic = nic + + +class StaticIpv4Addr: + def __init__(self, nic): + self._nic = nic + + @property + def ip(self): + return self._nic._get('|IPADDR') + + @property + def netmask(self): + return self._nic._get('|NETMASK') + + @property + def gateway(self): + return self._nic._get('|GATEWAY') + + +class StaticIpv4Conf(DhcpIpv4Conf): + @property + def addrs(self): + return [StaticIpv4Addr(self._nic)] + + +class DhcpIpv6Conf: + def __init__(self, nic): + self._nic = nic + + +class StaticIpv6Addr: + def __init__(self, nic, index): + self._nic = nic + self._index = index + + @property + def ip(self): + return self._nic._get("|IPv6ADDR|" + str(self._index)) + + @property + def prefix(self): + return self._nic._get("|IPv6NETMASK|" + str(self._index)) + + @property + def gateway(self): + return self._nic._get("|IPv6GATEWAY|" + str(self._index)) + + +class StaticIpv6Conf(DhcpIpv6Conf): + @property + def addrs(self): + cnt = self._nic._getCnt("|IPv6ADDR|") + + res = [] + + for i in range(1, cnt + 1): + res.append(StaticIpv6Addr(self._nic, i)) + + return res -- cgit v1.2.3