Merge lp:~sankaraditya/cloud-init/topic-stanguturi-vmware-support into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Sankar Tanguturi on 2015-11-19
Status: Merged
Merged at revision: 1166
Proposed branch: lp:~sankaraditya/cloud-init/topic-stanguturi-vmware-support
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 1300 lines (+1168/-3)
15 files modified
cloudinit/sources/DataSourceOVF.py (+76/-3)
cloudinit/sources/helpers/vmware/__init__.py (+13/-0)
cloudinit/sources/helpers/vmware/imc/__init__.py (+13/-0)
cloudinit/sources/helpers/vmware/imc/boot_proto.py (+25/-0)
cloudinit/sources/helpers/vmware/imc/config.py (+95/-0)
cloudinit/sources/helpers/vmware/imc/config_file.py (+129/-0)
cloudinit/sources/helpers/vmware/imc/config_namespace.py (+25/-0)
cloudinit/sources/helpers/vmware/imc/config_nic.py (+247/-0)
cloudinit/sources/helpers/vmware/imc/config_source.py (+23/-0)
cloudinit/sources/helpers/vmware/imc/ipv4_mode.py (+45/-0)
cloudinit/sources/helpers/vmware/imc/nic.py (+147/-0)
cloudinit/sources/helpers/vmware/imc/nic_base.py (+154/-0)
tests/data/vmware/cust-dhcp-2nic.cfg (+34/-0)
tests/data/vmware/cust-static-2nic.cfg (+39/-0)
tests/unittests/test_vmware_config_file.py (+103/-0)
To merge this branch: bzr merge lp:~sankaraditya/cloud-init/topic-stanguturi-vmware-support
Reviewer Review Type Date Requested Status
Dan Watkins Approve on 2016-02-22
Scott Moser 2015-11-19 Pending
Review via email: mp+277933@code.launchpad.net

Description of the Change

  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.

To post a comment you must log in.
Dan Watkins (daniel-thewatkins) wrote :

Hi Sankar,

Thanks for submitting this change!

I have a few high-level stylistic comments; it would be good if you could address these so it's easier for us to undertake a more in-depth code review:

- there is a decent chunk of commented code in here; this should be removed
- the cloud-init code-base (and Python best practice in general) uses underscored_method_names instead of camelCasedMethodNames
- the documentation for methods and functions should be in a docstring within the code block, not a comment above it
  - additionally, I would suggest that you don't need to document when a callable takes no arguments, throws nothing or returns None; these are reasonable default assumptions

Thanks,

Dan

review: Needs Fixing
Dan Watkins (daniel-thewatkins) wrote :

Hi Sankar,

On 21/12/15 21:20, Sankar Tanguturi wrote:
> Thanks a lot for your valuable inputs. Is there any python coding style guide that is followed by all cloudinit developers? I can follow that guide and make necessary changes.

Generally, we follow PEP-8[0], which is the generally accepted style
guide for Python code. I would suggest using the pep8 checker[1] to
check if your code meets the requirements. As with all linters, you
don't necessarily need to fix 100% of the problems it raises, but fixing
most of them should get you there. :)

Dan

[0] https://www.python.org/dev/peps/pep-0008/
[1] https://pypi.python.org/pypi/pep8

1158. By Sankar Tanguturi <stanguturi@stanguturi-rhel> on 2016-01-05

 Fixed all the styling nits.
 Used proper naming convention for the methods.
 Added proper documentation.
 Checked pep8 and flake8 output and no issues were reported.

Sankar Tanguturi (sankaraditya) wrote :

Updated the branch with the new code. Waiting for review comments.

Thanks a lot for the help.

Dan Watkins (daniel-thewatkins) wrote :

Hi Sankar,

Do you have any examples/documentations of the Image Customization Specification that I could refer to while reviewing this? I've tried Googling for it, but there isn't anything obviously canonical, and I want to make sure that I'm looking at the correct thing. :)

In the meantime, I'll do a high-level review to get a handle on everything hangs together.

Thanks,

Dan

Dan Watkins (daniel-thewatkins) wrote :

OK, here are some more implementation comments. I'll do another pass once I have some specification documentation to compare against. :)

Sankar Tanguturi (sankaraditya) wrote :

Thanks Daniel. I have updated the code with your review comments. Will update the diff.

Sankar Tanguturi (sankaraditya) wrote :

Hi,

Thanks a lot for the review comments.

I will update the diff in few minutes. I am attaching an example for the 'customization specification' file (cust.cfg). Can you please check it out.

Thanks
SAT

________________________________________
From: <email address hidden> <email address hidden> on behalf of Dan Watkins <email address hidden>
Sent: Tuesday, January 19, 2016 3:16 AM
To: Sankar Tanguturi
Subject: Re: [Merge] lp:~sankaraditya/cloud-init/topic-stanguturi-vmware-support into lp:cloud-init

Hi Sankar,

Do you have any examples/documentations of the Image Customization Specification that I could refer to while reviewing this? I've tried Googling for it, but there isn't anything obviously canonical, and I want to make sure that I'm looking at the correct thing. :)

In the meantime, I'll do a high-level review to get a handle on everything hangs together.

Thanks,

Dan
--
https://code.launchpad.net/~sankaraditya/cloud-init/topic-stanguturi-vmware-support/+merge/277933
You are the owner of lp:~sankaraditya/cloud-init/topic-stanguturi-vmware-support.

1159. By Sankar Tanguturi <stanguturi@stanguturi-rhel> on 2016-01-20

  Fixed all the review comments from Daniel.
  Added a new file i.e. nic_base.py which will be used a base calls for all
  NIC related configuration.
  Modified some code in nic.py.

Sankar Tanguturi (sankaraditya) wrote :

I couldn't add the 'customization specification' file. I have updated the diff and am pasting the content of a sample 'customization specification' file. Please check it out.

""" cust.cfg file"""

[NETWORK]
NETWORKING = yes
BOOTPROTO = dhcp
HOSTNAME = CENTOS510OSPX64
DOMAINNAME = google.com

[NIC-CONFIG]
NICS = NIC1

[NIC1]
MACADDR = 00:50:56:a7:00:5e
ONBOOT = yes
IPv4_MODE = BACKWARDS_COMPATIBLE
BOOTPROTO = dhcp
GATEWAY = 192.4.5.6
# This is a comment
-abc = def

[DNS]
DNSFROMDHCP=no
SUFFIX|1 = vmware.com
NAMESERVER|1 = 10.20.20.1

[DATETIME]
TIMEZONE = Africa/Abidjan
UTC = yes

"""

Scott Moser (smoser) wrote :

Sankar,
Thanks for your patience and for your work.

A few minor things:
a.) any information on this format (where it came from, where else it is used... would be good, links to doc would be great)
b.) there is one comment inline.

The next thing is a request.
What you're providing us here is a way to seed networking configuration into cloud-init.
We have other work coming where cloud-init will be able to do this itself so that the user/host/*something* can seed networking information into the system.

So, the request is that
1.) you would in the future when we have that help to port any network-config-rendering code that you provide shortly to re-work onto what we have later.
2.) you can help with that network-config-rendering stuff also.

The last request is just for unit tests.
I realize that this is mostly infrastructure but would be good to have unit tests on it.

Thanks.

Sankar Tanguturi (sankaraditya) wrote :

Thanks a lot for the review comments.

> a.) any information on this format (where it came from, where else it is used... would be good, links to doc would be great)

I don't have any official documentation to share.

If there is any Guest Customization on a VMware VM, then a specific customization file is pushed inside the guest after the VM comes up. It is usually used only the guest customization and nowhere else. After this patch, I am going to publish another patch which will have all the code to extract, parse and apply the customization.

> b.) there is one comment inline.
Replied to the comment.

> 1.) you would in the future when we have that help to port any network-config-rendering code that you provide shortly to re-work onto what we have later.
> 2.) you can help with that network-config-rendering stuff also.

Sure.

> The last request is just for unit tests.
Can you please let me know in which directory I should include the unit tests?

Sankar Tanguturi (sankaraditya) wrote :

> canLog depends on the key starting with a '-' or having '|-' ?

This is our way to deliver additional "metadata" with the key. "-" means "confidential". This is a legacy format.

1160. By Sankar Tanguturi <stanguturi@stanguturi-rhel> on 2016-02-09

  - Added new Unittests for VMware support.

Sankar Tanguturi (sankaraditya) wrote :

Thanks for the review. Added new unittests.

1161. By Sankar Tanguturi <stanguturi@stanguturi-rhel> on 2016-02-10

  - Added the code to configure the NICs.
  - Added the code to detect VMware Virtual Platform and apply the
    customization based on the 'Customization Specification File' Pushed
    into the guest VM.

review: Needs Fixing
Sankar Tanguturi (sankaraditya) wrote :

Thanks for the review comments. Will publish the new diff soon.

1162. By Sankar Tanguturi <stanguturi@stanguturi-rhel> on 2016-02-17

 - Used proper 4 space indentations for config_nic.py and nic.py
 - Implemented the 'search_file' function using 'os.walk()'
 - Fixed few variable names.
 - Removed size() function in config_file.py
 - Updated the test_config_file.py to use len() instead of .size()

Sankar Tanguturi (sankaraditya) wrote :

Published the new diff addressing all review comments from Dan Watkins.

Thanks.

Dan Watkins (daniel-thewatkins) wrote :

A few more notes. Sorry that we're iterating so much on this!

(For future reference; more, smaller merge proposals are much easier and quicker to review :)

review: Needs Fixing
Sankar Tanguturi (sankaraditya) wrote :

Thanks for the comments. Will update the new diff in few minutes.

1163. By Sankar Tanguturi <stanguturi@stanguturi-rhel> on 2016-02-19

 - Removed dmi_data function.
 - Fixed few variable names.
 - Used util.subp methods for process related manipulations.

Dan Watkins (daniel-thewatkins) wrote :

I'm pretty much happy with this. Thanks for all your work on it!

(I'll wait for smoser to approve before merging :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sources/DataSourceOVF.py'
2--- cloudinit/sources/DataSourceOVF.py 2015-01-21 22:56:53 +0000
3+++ cloudinit/sources/DataSourceOVF.py 2016-02-19 02:34:07 +0000
4@@ -24,11 +24,16 @@
5
6 import base64
7 import os
8+import shutil
9 import re
10+import time
11
12 from cloudinit import log as logging
13 from cloudinit import sources
14 from cloudinit import util
15+from cloudinit.sources.helpers.vmware.imc.config import Config
16+from cloudinit.sources.helpers.vmware.imc.config_file import ConfigFile
17+from cloudinit.sources.helpers.vmware.imc.config_nic import NicConfigurator
18
19 LOG = logging.getLogger(__name__)
20
21@@ -50,13 +55,46 @@
22 found = []
23 md = {}
24 ud = ""
25+ vmwarePlatformFound = False
26+ vmwareImcConfigFilePath = ''
27
28 defaults = {
29 "instance-id": "iid-dsovf",
30 }
31
32 (seedfile, contents) = get_ovf_env(self.paths.seed_dir)
33- if seedfile:
34+
35+ system_type = util.read_dmi_data("system-product-name")
36+ if system_type is None:
37+ LOG.debug("No system-product-name found")
38+ elif 'vmware' in system_type.lower():
39+ LOG.debug("VMware Virtual Platform found")
40+ deployPkgPluginPath = search_file("/usr/lib/vmware-tools", "libdeployPkgPlugin.so")
41+ if deployPkgPluginPath:
42+ vmwareImcConfigFilePath = util.log_time(logfunc=LOG.debug,
43+ msg="waiting for configuration file",
44+ func=wait_for_imc_cfg_file,
45+ args=("/tmp", "cust.cfg"))
46+
47+ if vmwareImcConfigFilePath:
48+ LOG.debug("Found VMware DeployPkg Config File Path at %s" % vmwareImcConfigFilePath)
49+ else:
50+ LOG.debug("Didn't find VMware DeployPkg Config File Path")
51+
52+ if vmwareImcConfigFilePath:
53+ try:
54+ cf = ConfigFile(vmwareImcConfigFilePath)
55+ conf = Config(cf)
56+ (md, ud, cfg) = read_vmware_imc(conf)
57+ nicConfigurator = NicConfigurator(conf.nics)
58+ nicConfigurator.configure()
59+ vmwarePlatformFound = True
60+ except Exception as inst:
61+ LOG.debug("Error while parsing the Customization Config File")
62+ finally:
63+ dirPath = os.path.dirname(vmwareImcConfigFilePath)
64+ shutil.rmtree(dirPath)
65+ elif seedfile:
66 # Found a seed dir
67 seed = os.path.join(self.paths.seed_dir, seedfile)
68 (md, ud, cfg) = read_ovf_environment(contents)
69@@ -76,7 +114,7 @@
70 found.append(name)
71
72 # There was no OVF transports found
73- if len(found) == 0:
74+ if len(found) == 0 and not vmwarePlatformFound:
75 return False
76
77 if 'seedfrom' in md and md['seedfrom']:
78@@ -108,7 +146,7 @@
79
80 def get_public_ssh_keys(self):
81 if 'public-keys' not in self.metadata:
82- return []
83+ return []
84 pks = self.metadata['public-keys']
85 if isinstance(pks, (list)):
86 return pks
87@@ -129,6 +167,31 @@
88 self.supported_seed_starts = ("http://", "https://", "ftp://")
89
90
91+def wait_for_imc_cfg_file(dirpath, filename, maxwait=180, naplen=5):
92+ waited = 0
93+
94+ while waited < maxwait:
95+ fileFullPath = search_file(dirpath, filename)
96+ if fileFullPath:
97+ return fileFullPath
98+ time.sleep(naplen)
99+ waited += naplen
100+ return None
101+
102+# This will return a dict with some content
103+# meta-data, user-data, some config
104+def read_vmware_imc(config):
105+ md = {}
106+ cfg = {}
107+ ud = ""
108+ if config.host_name:
109+ if config.domain_name:
110+ md['local-hostname'] = config.host_name + "." + config.domain_name
111+ else:
112+ md['local-hostname'] = config.host_name
113+
114+ return (md, ud, cfg)
115+
116 # This will return a dict with some content
117 # meta-data, user-data, some config
118 def read_ovf_environment(contents):
119@@ -281,6 +344,16 @@
120 return props
121
122
123+def search_file(dirpath, filename):
124+ if not dirpath or not filename:
125+ return None
126+
127+ for root, dirs, files in os.walk(dirpath):
128+ if filename in files:
129+ return os.path.join(root, filename)
130+
131+ return None
132+
133 class XmlError(Exception):
134 pass
135
136
137=== added directory 'cloudinit/sources/helpers/vmware'
138=== added file 'cloudinit/sources/helpers/vmware/__init__.py'
139--- cloudinit/sources/helpers/vmware/__init__.py 1970-01-01 00:00:00 +0000
140+++ cloudinit/sources/helpers/vmware/__init__.py 2016-02-19 02:34:07 +0000
141@@ -0,0 +1,13 @@
142+# vi: ts=4 expandtab
143+#
144+# This program is free software: you can redistribute it and/or modify
145+# it under the terms of the GNU General Public License version 3, as
146+# published by the Free Software Foundation.
147+#
148+# This program is distributed in the hope that it will be useful,
149+# but WITHOUT ANY WARRANTY; without even the implied warranty of
150+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
151+# GNU General Public License for more details.
152+#
153+# You should have received a copy of the GNU General Public License
154+# along with this program. If not, see <http://www.gnu.org/licenses/>.
155
156=== added directory 'cloudinit/sources/helpers/vmware/imc'
157=== added file 'cloudinit/sources/helpers/vmware/imc/__init__.py'
158--- cloudinit/sources/helpers/vmware/imc/__init__.py 1970-01-01 00:00:00 +0000
159+++ cloudinit/sources/helpers/vmware/imc/__init__.py 2016-02-19 02:34:07 +0000
160@@ -0,0 +1,13 @@
161+# vi: ts=4 expandtab
162+#
163+# This program is free software: you can redistribute it and/or modify
164+# it under the terms of the GNU General Public License version 3, as
165+# published by the Free Software Foundation.
166+#
167+# This program is distributed in the hope that it will be useful,
168+# but WITHOUT ANY WARRANTY; without even the implied warranty of
169+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
170+# GNU General Public License for more details.
171+#
172+# You should have received a copy of the GNU General Public License
173+# along with this program. If not, see <http://www.gnu.org/licenses/>.
174
175=== added file 'cloudinit/sources/helpers/vmware/imc/boot_proto.py'
176--- cloudinit/sources/helpers/vmware/imc/boot_proto.py 1970-01-01 00:00:00 +0000
177+++ cloudinit/sources/helpers/vmware/imc/boot_proto.py 2016-02-19 02:34:07 +0000
178@@ -0,0 +1,25 @@
179+# vi: ts=4 expandtab
180+#
181+# Copyright (C) 2015 Canonical Ltd.
182+# Copyright (C) 2015 VMware Inc.
183+#
184+# Author: Sankar Tanguturi <stanguturi@vmware.com>
185+#
186+# This program is free software: you can redistribute it and/or modify
187+# it under the terms of the GNU General Public License version 3, as
188+# published by the Free Software Foundation.
189+#
190+# This program is distributed in the hope that it will be useful,
191+# but WITHOUT ANY WARRANTY; without even the implied warranty of
192+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
193+# GNU General Public License for more details.
194+#
195+# You should have received a copy of the GNU General Public License
196+# along with this program. If not, see <http://www.gnu.org/licenses/>.
197+
198+
199+class BootProtoEnum:
200+ """Specifies the NIC Boot Settings."""
201+
202+ DHCP = 'dhcp'
203+ STATIC = 'static'
204
205=== added file 'cloudinit/sources/helpers/vmware/imc/config.py'
206--- cloudinit/sources/helpers/vmware/imc/config.py 1970-01-01 00:00:00 +0000
207+++ cloudinit/sources/helpers/vmware/imc/config.py 2016-02-19 02:34:07 +0000
208@@ -0,0 +1,95 @@
209+# vi: ts=4 expandtab
210+#
211+# Copyright (C) 2015 Canonical Ltd.
212+# Copyright (C) 2015 VMware Inc.
213+#
214+# Author: Sankar Tanguturi <stanguturi@vmware.com>
215+#
216+# This program is free software: you can redistribute it and/or modify
217+# it under the terms of the GNU General Public License version 3, as
218+# published by the Free Software Foundation.
219+#
220+# This program is distributed in the hope that it will be useful,
221+# but WITHOUT ANY WARRANTY; without even the implied warranty of
222+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
223+# GNU General Public License for more details.
224+#
225+# You should have received a copy of the GNU General Public License
226+# along with this program. If not, see <http://www.gnu.org/licenses/>.
227+
228+from .nic import Nic
229+
230+
231+class Config:
232+ """
233+ Stores the Contents specified in the Customization
234+ Specification file.
235+ """
236+
237+ DNS = 'DNS|NAMESERVER|'
238+ SUFFIX = 'DNS|SUFFIX|'
239+ PASS = 'PASSWORD|-PASS'
240+ TIMEZONE = 'DATETIME|TIMEZONE'
241+ UTC = 'DATETIME|UTC'
242+ HOSTNAME = 'NETWORK|HOSTNAME'
243+ DOMAINNAME = 'NETWORK|DOMAINNAME'
244+
245+ def __init__(self, configFile):
246+ self._configFile = configFile
247+
248+ @property
249+ def host_name(self):
250+ """Return the hostname."""
251+ return self._configFile.get(Config.HOSTNAME, None)
252+
253+ @property
254+ def domain_name(self):
255+ """Return the domain name."""
256+ return self._configFile.get(Config.DOMAINNAME, None)
257+
258+ @property
259+ def timezone(self):
260+ """Return the timezone."""
261+ return self._configFile.get(Config.TIMEZONE, None)
262+
263+ @property
264+ def utc(self):
265+ """Retrieves whether to set time to UTC or Local."""
266+ return self._configFile.get(Config.UTC, None)
267+
268+ @property
269+ def admin_password(self):
270+ """Return the root password to be set."""
271+ return self._configFile.get(Config.PASS, None)
272+
273+ @property
274+ def name_servers(self):
275+ """Return the list of DNS servers."""
276+ res = []
277+ cnt = self._configFile.get_count_with_prefix(Config.DNS)
278+ for i in range(1, cnt + 1):
279+ key = Config.DNS + str(i)
280+ res.append(self._configFile[key])
281+
282+ return res
283+
284+ @property
285+ def dns_suffixes(self):
286+ """Return the list of DNS Suffixes."""
287+ res = []
288+ cnt = self._configFile.get_count_with_prefix(Config.SUFFIX)
289+ for i in range(1, cnt + 1):
290+ key = Config.SUFFIX + str(i)
291+ res.append(self._configFile[key])
292+
293+ return res
294+
295+ @property
296+ def nics(self):
297+ """Return the list of associated NICs."""
298+ res = []
299+ nics = self._configFile['NIC-CONFIG|NICS']
300+ for nic in nics.split(','):
301+ res.append(Nic(nic, self._configFile))
302+
303+ return res
304
305=== added file 'cloudinit/sources/helpers/vmware/imc/config_file.py'
306--- cloudinit/sources/helpers/vmware/imc/config_file.py 1970-01-01 00:00:00 +0000
307+++ cloudinit/sources/helpers/vmware/imc/config_file.py 2016-02-19 02:34:07 +0000
308@@ -0,0 +1,129 @@
309+# vi: ts=4 expandtab
310+#
311+# Copyright (C) 2015 Canonical Ltd.
312+# Copyright (C) 2015 VMware Inc.
313+#
314+# Author: Sankar Tanguturi <stanguturi@vmware.com>
315+#
316+# This program is free software: you can redistribute it and/or modify
317+# it under the terms of the GNU General Public License version 3, as
318+# published by the Free Software Foundation.
319+#
320+# This program is distributed in the hope that it will be useful,
321+# but WITHOUT ANY WARRANTY; without even the implied warranty of
322+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
323+# GNU General Public License for more details.
324+#
325+# You should have received a copy of the GNU General Public License
326+# along with this program. If not, see <http://www.gnu.org/licenses/>.
327+
328+import logging
329+
330+try:
331+ import configparser
332+except ImportError:
333+ import ConfigParser as configparser
334+
335+from .config_source import ConfigSource
336+
337+logger = logging.getLogger(__name__)
338+
339+
340+class ConfigFile(ConfigSource, dict):
341+ """ConfigFile module to load the content from a specified source."""
342+
343+ def __init__(self, filename):
344+ self._loadConfigFile(filename)
345+ pass
346+
347+ def _insertKey(self, key, val):
348+ """
349+ Inserts a Key Value pair.
350+
351+ Keyword arguments:
352+ key -- The key to insert
353+ val -- The value to insert for the key
354+
355+ """
356+ key = key.strip()
357+ val = val.strip()
358+
359+ if key.startswith('-') or '|-' in key:
360+ canLog = False
361+ else:
362+ canLog = True
363+
364+ # "sensitive" settings shall not be logged
365+ if canLog:
366+ logger.debug("ADDED KEY-VAL :: '%s' = '%s'" % (key, val))
367+ else:
368+ logger.debug("ADDED KEY-VAL :: '%s' = '*****************'" % key)
369+
370+ self[key] = val
371+
372+ def _loadConfigFile(self, filename):
373+ """
374+ Parses properties from the specified config file.
375+
376+ Any previously available properties will be removed.
377+ Sensitive data will not be logged in case the key starts
378+ from '-'.
379+
380+ Keyword arguments:
381+ filename - The full path to the config file.
382+ """
383+ logger.info('Parsing the config file %s.' % filename)
384+
385+ config = configparser.ConfigParser()
386+ config.optionxform = str
387+ config.read(filename)
388+
389+ self.clear()
390+
391+ for category in config.sections():
392+ logger.debug("FOUND CATEGORY = '%s'" % category)
393+
394+ for (key, value) in config.items(category):
395+ self._insertKey(category + '|' + key, value)
396+
397+ def should_keep_current_value(self, key):
398+ """
399+ Determines whether a value for a property must be kept.
400+
401+ If the propery is missing, it is treated as it should be not
402+ changed by the engine.
403+
404+ Keyword arguments:
405+ key -- The key to search for.
406+ """
407+ # helps to distinguish from "empty" value which is used to indicate
408+ # "removal"
409+ return key not in self
410+
411+ def should_remove_current_value(self, key):
412+ """
413+ Determines whether a value for the property must be removed.
414+
415+ If the specified key is empty, it is treated as it should be
416+ removed by the engine.
417+
418+ Return true if the value can be removed, false otherwise.
419+
420+ Keyword arguments:
421+ key -- The key to search for.
422+ """
423+ # helps to distinguish from "missing" value which is used to indicate
424+ # "keeping unchanged"
425+ if key in self:
426+ return not bool(self[key])
427+ else:
428+ return False
429+
430+ def get_count_with_prefix(self, prefix):
431+ """
432+ Return the total count of keys that start with the specified prefix.
433+
434+ Keyword arguments:
435+ prefix -- prefix of the key
436+ """
437+ return len([key for key in self if key.startswith(prefix)])
438
439=== added file 'cloudinit/sources/helpers/vmware/imc/config_namespace.py'
440--- cloudinit/sources/helpers/vmware/imc/config_namespace.py 1970-01-01 00:00:00 +0000
441+++ cloudinit/sources/helpers/vmware/imc/config_namespace.py 2016-02-19 02:34:07 +0000
442@@ -0,0 +1,25 @@
443+# vi: ts=4 expandtab
444+#
445+# Copyright (C) 2015 Canonical Ltd.
446+# Copyright (C) 2015 VMware Inc.
447+#
448+# Author: Sankar Tanguturi <stanguturi@vmware.com>
449+#
450+# This program is free software: you can redistribute it and/or modify
451+# it under the terms of the GNU General Public License version 3, as
452+# published by the Free Software Foundation.
453+#
454+# This program is distributed in the hope that it will be useful,
455+# but WITHOUT ANY WARRANTY; without even the implied warranty of
456+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
457+# GNU General Public License for more details.
458+#
459+# You should have received a copy of the GNU General Public License
460+# along with this program. If not, see <http://www.gnu.org/licenses/>.
461+
462+from .config_source import ConfigSource
463+
464+
465+class ConfigNamespace(ConfigSource):
466+ """Specifies the Config Namespace."""
467+ pass
468
469=== added file 'cloudinit/sources/helpers/vmware/imc/config_nic.py'
470--- cloudinit/sources/helpers/vmware/imc/config_nic.py 1970-01-01 00:00:00 +0000
471+++ cloudinit/sources/helpers/vmware/imc/config_nic.py 2016-02-19 02:34:07 +0000
472@@ -0,0 +1,247 @@
473+# vi: ts=4 expandtab
474+#
475+# Copyright (C) 2015 Canonical Ltd.
476+# Copyright (C) 2016 VMware INC.
477+#
478+# Author: Sankar Tanguturi <stanguturi@vmware.com>
479+#
480+# This program is free software: you can redistribute it and/or modify
481+# it under the terms of the GNU General Public License version 3, as
482+# published by the Free Software Foundation.
483+#
484+# This program is distributed in the hope that it will be useful,
485+# but WITHOUT ANY WARRANTY; without even the implied warranty of
486+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
487+# GNU General Public License for more details.
488+#
489+# You should have received a copy of the GNU General Public License
490+# along with this program. If not, see <http://www.gnu.org/licenses/>.
491+
492+import logging
493+import os
494+import subprocess
495+import re
496+
497+from cloudinit import util
498+
499+logger = logging.getLogger(__name__)
500+
501+
502+class NicConfigurator:
503+ def __init__(self, nics):
504+ """
505+ Initialize the Nic Configurator
506+ @param nics (list) an array of nics to configure
507+ """
508+ self.nics = nics
509+ self.mac2Name = {}
510+ self.ipv4PrimaryGateway = None
511+ self.ipv6PrimaryGateway = None
512+ self.find_devices()
513+ self._primaryNic = self.get_primary_nic()
514+
515+ def get_primary_nic(self):
516+ """
517+ Retrieve the primary nic if it exists
518+ @return (NicBase): the primary nic if exists, None otherwise
519+ """
520+ primary_nics = [nic for nic in self.nics if nic.primary]
521+ if not primary_nics:
522+ return None
523+ elif len(primary_nics) > 1:
524+ raise Exception('There can only be one primary nic',
525+ [nic.mac for nic in primary_nics])
526+ else:
527+ return primary_nics[0]
528+
529+ def find_devices(self):
530+ """
531+ Create the mac2Name dictionary
532+ The mac address(es) are in the lower case
533+ """
534+ cmd = ['ip', 'addr', 'show']
535+ (output, err) = util.subp(cmd)
536+ sections = re.split(r'\n\d+: ', '\n' + output)[1:]
537+
538+ macPat = r'link/ether (([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2}))'
539+ for section in sections:
540+ match = re.search(macPat, section)
541+ if not match: # Only keep info about nics
542+ continue
543+ mac = match.group(1).lower()
544+ name = section.split(':', 1)[0]
545+ self.mac2Name[mac] = name
546+
547+ def gen_one_nic(self, nic):
548+ """
549+ Return the lines needed to configure a nic
550+ @return (str list): the string list to configure the nic
551+ @param nic (NicBase): the nic to configure
552+ """
553+ lines = []
554+ name = self.mac2Name.get(nic.mac.lower())
555+ if not name:
556+ raise ValueError('No known device has MACADDR: %s' % nic.mac)
557+
558+ if nic.onboot:
559+ lines.append('auto %s' % name)
560+
561+ # Customize IPv4
562+ lines.extend(self.gen_ipv4(name, nic))
563+
564+ # Customize IPv6
565+ lines.extend(self.gen_ipv6(name, nic))
566+
567+ lines.append('')
568+
569+ return lines
570+
571+ def gen_ipv4(self, name, nic):
572+ """
573+ Return the lines needed to configure the IPv4 setting of a nic
574+ @return (str list): the string list to configure the gateways
575+ @param name (str): name of the nic
576+ @param nic (NicBase): the nic to configure
577+ """
578+ lines = []
579+
580+ bootproto = nic.bootProto.lower()
581+ if nic.ipv4_mode.lower() == 'disabled':
582+ bootproto = 'manual'
583+ lines.append('iface %s inet %s' % (name, bootproto))
584+
585+ if bootproto != 'static':
586+ return lines
587+
588+ # Static Ipv4
589+ v4 = nic.staticIpv4
590+ if v4.ip:
591+ lines.append(' address %s' % v4.ip)
592+ if v4.netmask:
593+ lines.append(' netmask %s' % v4.netmask)
594+
595+ # Add the primary gateway
596+ if nic.primary and v4.gateways:
597+ self.ipv4PrimaryGateway = v4.gateways[0]
598+ lines.append(' gateway %s metric 0' % self.ipv4PrimaryGateway)
599+ return lines
600+
601+ # Add routes if there is no primary nic
602+ if not self._primaryNic:
603+ lines.extend(self.gen_ipv4_route(nic, v4.gateways))
604+
605+ return lines
606+
607+ def gen_ipv4_route(self, nic, gateways):
608+ """
609+ Return the lines needed to configure additional Ipv4 route
610+ @return (str list): the string list to configure the gateways
611+ @param nic (NicBase): the nic to configure
612+ @param gateways (str list): the list of gateways
613+ """
614+ lines = []
615+
616+ for gateway in gateways:
617+ lines.append(' up route add default gw %s metric 10000' %
618+ gateway)
619+
620+ return lines
621+
622+ def gen_ipv6(self, name, nic):
623+ """
624+ Return the lines needed to configure the gateways for a nic
625+ @return (str list): the string list to configure the gateways
626+ @param name (str): name of the nic
627+ @param nic (NicBase): the nic to configure
628+ """
629+ lines = []
630+
631+ if not nic.staticIpv6:
632+ return lines
633+
634+ # Static Ipv6
635+ addrs = nic.staticIpv6
636+ lines.append('iface %s inet6 static' % name)
637+ lines.append(' address %s' % addrs[0].ip)
638+ lines.append(' netmask %s' % addrs[0].netmask)
639+
640+ for addr in addrs[1:]:
641+ lines.append(' up ifconfig %s inet6 add %s/%s' % (name, addr.ip,
642+ addr.netmask))
643+ # Add the primary gateway
644+ if nic.primary:
645+ for addr in addrs:
646+ if addr.gateway:
647+ self.ipv6PrimaryGateway = addr.gateway
648+ lines.append(' gateway %s' % self.ipv6PrimaryGateway)
649+ return lines
650+
651+ # Add routes if there is no primary nic
652+ if not self._primaryNic:
653+ lines.extend(self._genIpv6Route(name, nic, addrs))
654+
655+ return lines
656+
657+ def _genIpv6Route(self, name, nic, addrs):
658+ lines = []
659+
660+ for addr in addrs:
661+ lines.append(' up route -A inet6 add default gw %s metric 10000' %
662+ addr.gateway)
663+
664+ return lines
665+
666+ def generate(self):
667+ """Return the lines that is needed to configure the nics"""
668+ lines = []
669+ lines.append('iface lo inet loopback')
670+ lines.append('auto lo')
671+ lines.append('')
672+
673+ for nic in self.nics:
674+ lines.extend(self.gen_one_nic(nic))
675+
676+ return lines
677+
678+ def clear_dhcp(self):
679+ logger.info('Clearing DHCP leases')
680+
681+ util.subp(["pkill", "dhclient"])
682+ util.subp(["rm", "-f", "/var/lib/dhcp/*"])
683+
684+ def if_down_up(self):
685+ names = []
686+ for nic in self.nics:
687+ name = self.mac2Name.get(nic.mac.lower())
688+ names.append(name)
689+
690+ for name in names:
691+ logger.info('Bring down interface %s' % name)
692+ util.subp(["ifdown", "%s" % name])
693+
694+ self.clear_dhcp()
695+
696+ for name in names:
697+ logger.info('Bring up interface %s' % name)
698+ util.subp(["ifup", "%s" % name])
699+
700+ def configure(self):
701+ """
702+ Configure the /etc/network/intefaces
703+ Make a back up of the original
704+ """
705+ containingDir = '/etc/network'
706+
707+ interfaceFile = os.path.join(containingDir, 'interfaces')
708+ originalFile = os.path.join(containingDir,
709+ 'interfaces.before_vmware_customization')
710+
711+ if not os.path.exists(originalFile) and os.path.exists(interfaceFile):
712+ os.rename(interfaceFile, originalFile)
713+
714+ lines = self.generate()
715+ with open(interfaceFile, 'w') as fp:
716+ for line in lines:
717+ fp.write('%s\n' % line)
718+
719+ self.if_down_up()
720
721=== added file 'cloudinit/sources/helpers/vmware/imc/config_source.py'
722--- cloudinit/sources/helpers/vmware/imc/config_source.py 1970-01-01 00:00:00 +0000
723+++ cloudinit/sources/helpers/vmware/imc/config_source.py 2016-02-19 02:34:07 +0000
724@@ -0,0 +1,23 @@
725+# vi: ts=4 expandtab
726+#
727+# Copyright (C) 2015 Canonical Ltd.
728+# Copyright (C) 2015 VMware Inc.
729+#
730+# Author: Sankar Tanguturi <stanguturi@vmware.com>
731+#
732+# This program is free software: you can redistribute it and/or modify
733+# it under the terms of the GNU General Public License version 3, as
734+# published by the Free Software Foundation.
735+#
736+# This program is distributed in the hope that it will be useful,
737+# but WITHOUT ANY WARRANTY; without even the implied warranty of
738+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
739+# GNU General Public License for more details.
740+#
741+# You should have received a copy of the GNU General Public License
742+# along with this program. If not, see <http://www.gnu.org/licenses/>.
743+
744+
745+class ConfigSource:
746+ """Specifies a source for the Config Content."""
747+ pass
748
749=== added file 'cloudinit/sources/helpers/vmware/imc/ipv4_mode.py'
750--- cloudinit/sources/helpers/vmware/imc/ipv4_mode.py 1970-01-01 00:00:00 +0000
751+++ cloudinit/sources/helpers/vmware/imc/ipv4_mode.py 2016-02-19 02:34:07 +0000
752@@ -0,0 +1,45 @@
753+# vi: ts=4 expandtab
754+#
755+# Copyright (C) 2015 Canonical Ltd.
756+# Copyright (C) 2015 VMware Inc.
757+#
758+# Author: Sankar Tanguturi <stanguturi@vmware.com>
759+#
760+# This program is free software: you can redistribute it and/or modify
761+# it under the terms of the GNU General Public License version 3, as
762+# published by the Free Software Foundation.
763+#
764+# This program is distributed in the hope that it will be useful,
765+# but WITHOUT ANY WARRANTY; without even the implied warranty of
766+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
767+# GNU General Public License for more details.
768+#
769+# You should have received a copy of the GNU General Public License
770+# along with this program. If not, see <http://www.gnu.org/licenses/>.
771+
772+
773+class Ipv4ModeEnum:
774+ """
775+ The IPv4 configuration mode which directly represents the user's goal.
776+
777+ This mode effectively acts as a contract of the in-guest customization
778+ engine. It must be set based on what the user has requested and should
779+ not be changed by those layers. It's up to the in-guest engine to
780+ interpret and materialize the user's request.
781+ """
782+
783+ # The legacy mode which only allows dhcp/static based on whether IPv4
784+ # addresses list is empty or not
785+ IPV4_MODE_BACKWARDS_COMPATIBLE = 'BACKWARDS_COMPATIBLE'
786+
787+ # IPv4 must use static address. Reserved for future use
788+ IPV4_MODE_STATIC = 'STATIC'
789+
790+ # IPv4 must use DHCPv4. Reserved for future use
791+ IPV4_MODE_DHCP = 'DHCP'
792+
793+ # IPv4 must be disabled
794+ IPV4_MODE_DISABLED = 'DISABLED'
795+
796+ # IPv4 settings should be left untouched. Reserved for future use
797+ IPV4_MODE_AS_IS = 'AS_IS'
798
799=== added file 'cloudinit/sources/helpers/vmware/imc/nic.py'
800--- cloudinit/sources/helpers/vmware/imc/nic.py 1970-01-01 00:00:00 +0000
801+++ cloudinit/sources/helpers/vmware/imc/nic.py 2016-02-19 02:34:07 +0000
802@@ -0,0 +1,147 @@
803+# vi: ts=4 expandtab
804+#
805+# Copyright (C) 2015 Canonical Ltd.
806+# Copyright (C) 2015 VMware Inc.
807+#
808+# Author: Sankar Tanguturi <stanguturi@vmware.com>
809+#
810+# This program is free software: you can redistribute it and/or modify
811+# it under the terms of the GNU General Public License version 3, as
812+# published by the Free Software Foundation.
813+#
814+# This program is distributed in the hope that it will be useful,
815+# but WITHOUT ANY WARRANTY; without even the implied warranty of
816+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
817+# GNU General Public License for more details.
818+#
819+# You should have received a copy of the GNU General Public License
820+# along with this program. If not, see <http://www.gnu.org/licenses/>.
821+
822+from .boot_proto import BootProtoEnum
823+from .nic_base import NicBase, StaticIpv4Base, StaticIpv6Base
824+
825+
826+class Nic(NicBase):
827+ """
828+ Holds the information about each NIC specified
829+ in the customization specification file
830+ """
831+
832+ def __init__(self, name, configFile):
833+ self._name = name
834+ self._configFile = configFile
835+
836+ def _get(self, what):
837+ return self._configFile.get(self.name + '|' + what, None)
838+
839+ def _get_count_with_prefix(self, prefix):
840+ return self._configFile.get_count_with_prefix(self.name + prefix)
841+
842+ @property
843+ def name(self):
844+ return self._name
845+
846+ @property
847+ def mac(self):
848+ return self._get('MACADDR').lower()
849+
850+ @property
851+ def primary(self):
852+ value = self._get('PRIMARY')
853+ if value:
854+ value = value.lower()
855+ return value == 'yes' or value == 'true'
856+ else:
857+ return False
858+
859+ @property
860+ def onboot(self):
861+ value = self._get('ONBOOT')
862+ if value:
863+ value = value.lower()
864+ return value == 'yes' or value == 'true'
865+ else:
866+ return False
867+
868+ @property
869+ def bootProto(self):
870+ value = self._get('BOOTPROTO')
871+ if value:
872+ return value.lower()
873+ else:
874+ return ""
875+
876+ @property
877+ def ipv4_mode(self):
878+ value = self._get('IPv4_MODE')
879+ if value:
880+ return value.lower()
881+ else:
882+ return ""
883+
884+ @property
885+ def staticIpv4(self):
886+ """
887+ Checks the BOOTPROTO property and returns StaticIPv4Addr
888+ configuration object if STATIC configuration is set.
889+ """
890+ if self.bootProto == BootProtoEnum.STATIC:
891+ return [StaticIpv4Addr(self)]
892+ else:
893+ return None
894+
895+ @property
896+ def staticIpv6(self):
897+ cnt = self._get_count_with_prefix('|IPv6ADDR|')
898+
899+ if not cnt:
900+ return None
901+
902+ result = []
903+ for index in range(1, cnt + 1):
904+ result.append(StaticIpv6Addr(self, index))
905+
906+ return result
907+
908+
909+class StaticIpv4Addr(StaticIpv4Base):
910+ """Static IPV4 Setting."""
911+
912+ def __init__(self, nic):
913+ self._nic = nic
914+
915+ @property
916+ def ip(self):
917+ return self._nic._get('IPADDR')
918+
919+ @property
920+ def netmask(self):
921+ return self._nic._get('NETMASK')
922+
923+ @property
924+ def gateways(self):
925+ value = self._nic._get('GATEWAY')
926+ if value:
927+ return [x.strip() for x in value.split(',')]
928+ else:
929+ return None
930+
931+
932+class StaticIpv6Addr(StaticIpv6Base):
933+ """Static IPV6 Address."""
934+
935+ def __init__(self, nic, index):
936+ self._nic = nic
937+ self._index = index
938+
939+ @property
940+ def ip(self):
941+ return self._nic._get('IPv6ADDR|' + str(self._index))
942+
943+ @property
944+ def netmask(self):
945+ return self._nic._get('IPv6NETMASK|' + str(self._index))
946+
947+ @property
948+ def gateway(self):
949+ return self._nic._get('IPv6GATEWAY|' + str(self._index))
950
951=== added file 'cloudinit/sources/helpers/vmware/imc/nic_base.py'
952--- cloudinit/sources/helpers/vmware/imc/nic_base.py 1970-01-01 00:00:00 +0000
953+++ cloudinit/sources/helpers/vmware/imc/nic_base.py 2016-02-19 02:34:07 +0000
954@@ -0,0 +1,154 @@
955+# vi: ts=4 expandtab
956+#
957+# Copyright (C) 2015 Canonical Ltd.
958+# Copyright (C) 2015 VMware Inc.
959+#
960+# Author: Sankar Tanguturi <stanguturi@vmware.com>
961+#
962+# This program is free software: you can redistribute it and/or modify
963+# it under the terms of the GNU General Public License version 3, as
964+# published by the Free Software Foundation.
965+#
966+# This program is distributed in the hope that it will be useful,
967+# but WITHOUT ANY WARRANTY; without even the implied warranty of
968+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
969+# GNU General Public License for more details.
970+#
971+# You should have received a copy of the GNU General Public License
972+# along with this program. If not, see <http://www.gnu.org/licenses/>.
973+
974+
975+class NicBase:
976+ """
977+ Define what are expected of each nic.
978+ The following properties should be provided in an implementation class.
979+ """
980+
981+ @property
982+ def mac(self):
983+ """
984+ Retrieves the mac address of the nic
985+ @return (str) : the MACADDR setting
986+ """
987+ raise NotImplementedError('MACADDR')
988+
989+ @property
990+ def primary(self):
991+ """
992+ Retrieves whether the nic is the primary nic
993+ Indicates whether NIC will be used to define the default gateway.
994+ If none of the NICs is configured to be primary, default gateway won't
995+ be set.
996+ @return (bool): the PRIMARY setting
997+ """
998+ raise NotImplementedError('PRIMARY')
999+
1000+ @property
1001+ def onboot(self):
1002+ """
1003+ Retrieves whether the nic should be up at the boot time
1004+ @return (bool) : the ONBOOT setting
1005+ """
1006+ raise NotImplementedError('ONBOOT')
1007+
1008+ @property
1009+ def bootProto(self):
1010+ """
1011+ Retrieves the boot protocol of the nic
1012+ @return (str): the BOOTPROTO setting, valid values: dhcp and static.
1013+ """
1014+ raise NotImplementedError('BOOTPROTO')
1015+
1016+ @property
1017+ def ipv4_mode(self):
1018+ """
1019+ Retrieves the IPv4_MODE
1020+ @return (str): the IPv4_MODE setting, valid values:
1021+ backwards_compatible, static, dhcp, disabled, as_is
1022+ """
1023+ raise NotImplementedError('IPv4_MODE')
1024+
1025+ @property
1026+ def staticIpv4(self):
1027+ """
1028+ Retrieves the static IPv4 configuration of the nic
1029+ @return (StaticIpv4Base list): the static ipv4 setting
1030+ """
1031+ raise NotImplementedError('Static IPv4')
1032+
1033+ @property
1034+ def staticIpv6(self):
1035+ """
1036+ Retrieves the IPv6 configuration of the nic
1037+ @return (StaticIpv6Base list): the static ipv6 setting
1038+ """
1039+ raise NotImplementedError('Static Ipv6')
1040+
1041+ def validate(self):
1042+ """
1043+ Validate the object
1044+ For example, the staticIpv4 property is required and should not be
1045+ empty when ipv4Mode is STATIC
1046+ """
1047+ raise NotImplementedError('Check constraints on properties')
1048+
1049+
1050+class StaticIpv4Base:
1051+ """
1052+ Define what are expected of a static IPv4 setting
1053+ The following properties should be provided in an implementation class.
1054+ """
1055+
1056+ @property
1057+ def ip(self):
1058+ """
1059+ Retrieves the Ipv4 address
1060+ @return (str): the IPADDR setting
1061+ """
1062+ raise NotImplementedError('Ipv4 Address')
1063+
1064+ @property
1065+ def netmask(self):
1066+ """
1067+ Retrieves the Ipv4 NETMASK setting
1068+ @return (str): the NETMASK setting
1069+ """
1070+ raise NotImplementedError('Ipv4 NETMASK')
1071+
1072+ @property
1073+ def gateways(self):
1074+ """
1075+ Retrieves the gateways on this Ipv4 subnet
1076+ @return (str list): the GATEWAY setting
1077+ """
1078+ raise NotImplementedError('Ipv4 GATEWAY')
1079+
1080+
1081+class StaticIpv6Base:
1082+ """Define what are expected of a static IPv6 setting
1083+ The following properties should be provided in an implementation class.
1084+ """
1085+
1086+ @property
1087+ def ip(self):
1088+ """
1089+ Retrieves the Ipv6 address
1090+ @return (str): the IPv6ADDR setting
1091+ """
1092+ raise NotImplementedError('Ipv6 Address')
1093+
1094+ @property
1095+ def netmask(self):
1096+ """
1097+ Retrieves the Ipv6 NETMASK setting
1098+ @return (str): the IPv6NETMASK setting
1099+ """
1100+ raise NotImplementedError('Ipv6 NETMASK')
1101+
1102+ @property
1103+ def gateway(self):
1104+ """
1105+ Retrieves the Ipv6 GATEWAY setting
1106+ @return (str): the IPv6GATEWAY setting
1107+ """
1108+ raise NotImplementedError('Ipv6 GATEWAY')
1109
1110=== added directory 'tests/data/vmware'
1111=== added file 'tests/data/vmware/cust-dhcp-2nic.cfg'
1112--- tests/data/vmware/cust-dhcp-2nic.cfg 1970-01-01 00:00:00 +0000
1113+++ tests/data/vmware/cust-dhcp-2nic.cfg 2016-02-19 02:34:07 +0000
1114@@ -0,0 +1,34 @@
1115+[NETWORK]
1116+NETWORKING = yes
1117+BOOTPROTO = dhcp
1118+HOSTNAME = myhost1
1119+DOMAINNAME = eng.vmware.com
1120+
1121+[NIC-CONFIG]
1122+NICS = NIC1,NIC2
1123+
1124+[NIC1]
1125+MACADDR = 00:50:56:a6:8c:08
1126+ONBOOT = yes
1127+IPv4_MODE = BACKWARDS_COMPATIBLE
1128+BOOTPROTO = dhcp
1129+
1130+[NIC2]
1131+MACADDR = 00:50:56:a6:5a:de
1132+ONBOOT = yes
1133+IPv4_MODE = BACKWARDS_COMPATIBLE
1134+BOOTPROTO = dhcp
1135+
1136+# some random comment
1137+
1138+[PASSWORD]
1139+# secret
1140+-PASS = c2VjcmV0Cg==
1141+
1142+[DNS]
1143+DNSFROMDHCP=yes
1144+SUFFIX|1 = eng.vmware.com
1145+
1146+[DATETIME]
1147+TIMEZONE = Africa/Abidjan
1148+UTC = yes
1149
1150=== added file 'tests/data/vmware/cust-static-2nic.cfg'
1151--- tests/data/vmware/cust-static-2nic.cfg 1970-01-01 00:00:00 +0000
1152+++ tests/data/vmware/cust-static-2nic.cfg 2016-02-19 02:34:07 +0000
1153@@ -0,0 +1,39 @@
1154+[NETWORK]
1155+NETWORKING = yes
1156+BOOTPROTO = dhcp
1157+HOSTNAME = myhost1
1158+DOMAINNAME = eng.vmware.com
1159+
1160+[NIC-CONFIG]
1161+NICS = NIC1,NIC2
1162+
1163+[NIC1]
1164+MACADDR = 00:50:56:a6:8c:08
1165+ONBOOT = yes
1166+IPv4_MODE = BACKWARDS_COMPATIBLE
1167+BOOTPROTO = static
1168+IPADDR = 10.20.87.154
1169+NETMASK = 255.255.252.0
1170+GATEWAY = 10.20.87.253, 10.20.87.105
1171+IPv6ADDR|1 = fc00:10:20:87::154
1172+IPv6NETMASK|1 = 64
1173+IPv6GATEWAY|1 = fc00:10:20:87::253
1174+[NIC2]
1175+MACADDR = 00:50:56:a6:ef:7d
1176+ONBOOT = yes
1177+IPv4_MODE = BACKWARDS_COMPATIBLE
1178+BOOTPROTO = static
1179+IPADDR = 192.168.6.102
1180+NETMASK = 255.255.0.0
1181+GATEWAY = 192.168.0.10
1182+
1183+[DNS]
1184+DNSFROMDHCP=no
1185+SUFFIX|1 = eng.vmware.com
1186+SUFFIX|2 = proxy.vmware.com
1187+NAMESERVER|1 = 10.20.145.1
1188+NAMESERVER|2 = 10.20.145.2
1189+
1190+[DATETIME]
1191+TIMEZONE = Africa/Abidjan
1192+UTC = yes
1193
1194=== added file 'tests/unittests/test_vmware_config_file.py'
1195--- tests/unittests/test_vmware_config_file.py 1970-01-01 00:00:00 +0000
1196+++ tests/unittests/test_vmware_config_file.py 2016-02-19 02:34:07 +0000
1197@@ -0,0 +1,103 @@
1198+# vi: ts=4 expandtab
1199+#
1200+# Copyright (C) 2015 Canonical Ltd.
1201+# Copyright (C) 2016 VMware INC.
1202+#
1203+# Author: Sankar Tanguturi <stanguturi@vmware.com>
1204+#
1205+# This program is free software: you can redistribute it and/or modify
1206+# it under the terms of the GNU General Public License version 3, as
1207+# published by the Free Software Foundation.
1208+#
1209+# This program is distributed in the hope that it will be useful,
1210+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1211+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1212+# GNU General Public License for more details.
1213+#
1214+# You should have received a copy of the GNU General Public License
1215+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1216+
1217+import logging
1218+import sys
1219+import unittest
1220+
1221+from cloudinit.sources.helpers.vmware.imc.boot_proto import BootProtoEnum
1222+from cloudinit.sources.helpers.vmware.imc.config import Config
1223+from cloudinit.sources.helpers.vmware.imc.config_file import ConfigFile
1224+
1225+logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
1226+logger = logging.getLogger(__name__)
1227+
1228+
1229+class TestVmwareConfigFile(unittest.TestCase):
1230+
1231+ def test_utility_methods(self):
1232+ cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg")
1233+
1234+ cf.clear()
1235+
1236+ self.assertEqual(0, len(cf), "clear size")
1237+
1238+ cf._insertKey(" PASSWORD|-PASS ", " foo ")
1239+ cf._insertKey("BAR", " ")
1240+
1241+ self.assertEqual(2, len(cf), "insert size")
1242+ self.assertEqual('foo', cf["PASSWORD|-PASS"], "password")
1243+ self.assertTrue("PASSWORD|-PASS" in cf, "hasPassword")
1244+ self.assertFalse(cf.should_keep_current_value("PASSWORD|-PASS"),
1245+ "keepPassword")
1246+ self.assertFalse(cf.should_remove_current_value("PASSWORD|-PASS"),
1247+ "removePassword")
1248+ self.assertFalse("FOO" in cf, "hasFoo")
1249+ self.assertTrue(cf.should_keep_current_value("FOO"), "keepFoo")
1250+ self.assertFalse(cf.should_remove_current_value("FOO"), "removeFoo")
1251+ self.assertTrue("BAR" in cf, "hasBar")
1252+ self.assertFalse(cf.should_keep_current_value("BAR"), "keepBar")
1253+ self.assertTrue(cf.should_remove_current_value("BAR"), "removeBar")
1254+
1255+ def test_configfile_static_2nics(self):
1256+ cf = ConfigFile("tests/data/vmware/cust-static-2nic.cfg")
1257+
1258+ conf = Config(cf)
1259+
1260+ self.assertEqual('myhost1', conf.host_name, "hostName")
1261+ self.assertEqual('Africa/Abidjan', conf.timezone, "tz")
1262+ self.assertTrue(conf.utc, "utc")
1263+
1264+ self.assertEqual(['10.20.145.1', '10.20.145.2'],
1265+ conf.name_servers,
1266+ "dns")
1267+ self.assertEqual(['eng.vmware.com', 'proxy.vmware.com'],
1268+ conf.dns_suffixes,
1269+ "suffixes")
1270+
1271+ nics = conf.nics
1272+ ipv40 = nics[0].staticIpv4
1273+
1274+ self.assertEqual(2, len(nics), "nics")
1275+ self.assertEqual('NIC1', nics[0].name, "nic0")
1276+ self.assertEqual('00:50:56:a6:8c:08', nics[0].mac, "mac0")
1277+ self.assertEqual(BootProtoEnum.STATIC, nics[0].bootProto, "bootproto0")
1278+ self.assertEqual('10.20.87.154', ipv40[0].ip, "ipv4Addr0")
1279+ self.assertEqual('255.255.252.0', ipv40[0].netmask, "ipv4Mask0")
1280+ self.assertEqual(2, len(ipv40[0].gateways), "ipv4Gw0")
1281+ self.assertEqual('10.20.87.253', ipv40[0].gateways[0], "ipv4Gw0_0")
1282+ self.assertEqual('10.20.87.105', ipv40[0].gateways[1], "ipv4Gw0_1")
1283+
1284+ self.assertEqual(1, len(nics[0].staticIpv6), "ipv6Cnt0")
1285+ self.assertEqual('fc00:10:20:87::154',
1286+ nics[0].staticIpv6[0].ip,
1287+ "ipv6Addr0")
1288+
1289+ self.assertEqual('NIC2', nics[1].name, "nic1")
1290+ self.assertTrue(not nics[1].staticIpv6, "ipv61 dhcp")
1291+
1292+ def test_config_file_dhcp_2nics(self):
1293+ cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg")
1294+
1295+ conf = Config(cf)
1296+ nics = conf.nics
1297+ self.assertEqual(2, len(nics), "nics")
1298+ self.assertEqual('NIC1', nics[0].name, "nic0")
1299+ self.assertEqual('00:50:56:a6:8c:08', nics[0].mac, "mac0")
1300+ self.assertEqual(BootProtoEnum.DHCP, nics[0].bootProto, "bootproto0")