Merge ~ma-brothier/cloud-init:cs-multi-dhcp into cloud-init:master

Proposed by Marc-Aurele Brothier
Status: Work in progress
Proposed branch: ~ma-brothier/cloud-init:cs-multi-dhcp
Merge into: cloud-init:master
Diff against target: 275 lines (+63/-60)
2 files modified
cloudinit/sources/DataSourceCloudStack.py (+44/-42)
tests/unittests/test_datasource/test_cloudstack.py (+19/-18)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Needs Fixing
Review via email: mp+336145@code.launchpad.net

Description of the change

CloudStack changes on DHCP leases crawling mechanism for VR address

Currently the DHCP leases are search in reverse chronological order to look for the virtual router IP inside the lease information. In case more than one interface is added to a VM using a DHCP service to configure it, the latest DHCP lease will be linked to this new interface instead of the default one, thus waiting for the fault back method to fetch the default gateway address.

This change looks for all DHCP leases, grab all potential IP addresses (removing potential ducplicate due to multiple lease information in the same file) for the VR and try all of them in alphabetical order of the interface names until a good match is found (correct response), with a last default to the default gateway IP as originally.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/697/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/697/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/698/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/698/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Marc,

Thanks for this MP.

I have two thoughts:
a.) we should probably look into using the output of 'cloud-init dhclient-hook' which gets called by tools/hook-dhclient. That writes files in /run/cloud-init/dhclient.hooks/eth0.json which look like this below. I think its just a matter of then reading those files and looking for a dhcp_server_identifier key.

{
 "broadcast_address": "10.75.205.255",
 "dhcp_lease_time": "3600",
 "dhcp_message_type": "5",
 "dhcp_rebinding_time": "3150",
 "dhcp_renewal_time": "1800",
 "dhcp_server_identifier": "10.75.205.1",
 "domain_name": "lxd",
 "domain_name_servers": "10.75.205.1",
 "expiry": "1516216015",
 "host_name": "x1",
 "ip_address": "10.75.205.55",
 "network_number": "10.75.205.0",
 "next_server": "10.75.205.1",
 "routers": "10.75.205.1",
 "subnet_mask": "255.255.255.0"
}

b. I think that cloudstack is probably broken on Ubuntu 17.10 and bionic (18.04) and anywhere else that uses systemd-networkd. systemd-networkd doesnt' write the same leases files as dhclient, nor do the hooks get called. It does though provide info like:
$ cat /run/systemd/netif/leases/41
# This is private data. Do not parse.
ADDRESS=10.75.205.120
NETMASK=255.255.255.0
ROUTER=10.75.205.1
SERVER_ADDRESS=10.75.205.1
NEXT_SERVER=10.75.205.1
BROADCAST=10.75.205.255
T1=1609
T2=2959
LIFETIME=3600
DNS=10.75.205.1
DOMAINNAME=lxd
HOSTNAME=b1
CLIENTID=ffc011a99100020000ab118d8462b1b543ef0c

(Yes, i'm aware of the 'Do not parse' comment in the file, but there is currently no other way to get this information out of the networkd-systemd built in dhcp client).

Revision history for this message
Scott Moser (smoser) wrote :

Oh, also, if we *were* to need to parse dhcp leases files, there is now some code in cloudinit/net/dhcp.py that could be re-used or improved on.

Revision history for this message
Scott Moser (smoser) wrote :

So I was wrong about my 'b' above. that was fixed already. for systemd systems it currently just walks the leases dir in sorted order. these are numbers of the iface... not sorted like 'eth0', 'eth1' or any other mreaningful manner.

then the first entry that has that key it will return the value.

Revision history for this message
Marc-Aurele Brothier (ma-brothier) wrote :

Hi Scott,

If cloud-init creates a file for the lease of the interface it's taking
care, it's much better than using the default system location. What if
cloud-init manage multiple interface as part of the cloud-config? I don't
think it's the case now to say that eth1 should be confogured through DHCP?

Does the failed tests are expected? Localy I had some failing on another
part of the code I didn't touch (or at least thought it's not related).

On Thu, Jan 18, 2018 at 7:33 PM, Scott Moser <email address hidden>
wrote:

> So I was wrong about my 'b' above. that was fixed already. for systemd
> systems it currently just walks the leases dir in sorted order. these are
> numbers of the iface... not sorted like 'eth0', 'eth1' or any other
> mreaningful manner.
>
> then the first entry that has that key it will return the value.
>
> --
> https://code.launchpad.net/~ma-brothier/cloud-init/+git/
> cloud-init/+merge/336145
> You are the owner of ~ma-brothier/cloud-init:cs-multi-dhcp.
>

Revision history for this message
Scott Moser (smoser) wrote :

Marc,
cloud-init only currently creates the files in /run/ on azure. We can change that to do it also on CloudStack. It is optimized to only run on azure just to stop cloud-init (and python) in the hook path for dhcp and nic bringup everywhere.

I'm not sure I follow "What if cloud-init mange multiple interface..."

Tests should pass on trunk. nightly runs of tip can be seen:
 https://jenkins.ubuntu.com/server/view/cloud-init/job/cloud-init-ci-nightly/

its possible that there are some bad tests that interact poorly with the environment they're running in. unittests fail for you, please paste those failures somewhere and I'll look at helping out (feel free to ping in #cloud-init also).

Revision history for this message
Marc-Aurele Brothier (ma-brothier) wrote :

I've pushed a fixed for the failed test, it's running now locally.

Sorry for the confusion on my previous mail, it was unclear.Since
cloud-init does change the configuration of the first interface to be
configured through a DHCP, it would be great to read the JSON file to grab
the cloudstack password/metadata server address. Then if another interface
is configured through DHCP, cloud-init won't create the related json file,
am I right? Or would it be possible to pass a cloud-init config to state a
list of interface to be configured through a DHCP ?

On Fri, Jan 19, 2018 at 8:03 PM, Scott Moser <email address hidden>
wrote:

> Marc,
> cloud-init only currently creates the files in /run/ on azure. We can
> change that to do it also on CloudStack. It is optimized to only run on
> azure just to stop cloud-init (and python) in the hook path for dhcp and
> nic bringup everywhere.
>
> I'm not sure I follow "What if cloud-init mange multiple interface..."
>
> Tests should pass on trunk. nightly runs of tip can be seen:
> https://jenkins.ubuntu.com/server/view/cloud-init/job/
> cloud-init-ci-nightly/
>
> its possible that there are some bad tests that interact poorly with the
> environment they're running in. unittests fail for you, please paste those
> failures somewhere and I'll look at helping out (feel free to ping in
> #cloud-init also).
>
>
> --
> https://code.launchpad.net/~ma-brothier/cloud-init/+git/
> cloud-init/+merge/336145
> You are the owner of ~ma-brothier/cloud-init:cs-multi-dhcp.
>

Revision history for this message
Marc-Aurele Brothier (ma-brothier) wrote :

I'm force pushing the change to force a rebuild of the CI bot, but it does seem to work. How should I do?

1832cc7... by Marc-Aurele Brothier

CloudStack: go through all potential DHCP lease to find the meta/user-data server

Signed-off-by: Marc-Aurèle Brothier <email address hidden>

Revision history for this message
Scott Moser (smoser) wrote :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/879/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/879/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

@Marc,

Can you rebase and push --force again ? it looks like you have one failing test. possibly it is just notn completely mocked. i didnt' look much further, but below is how it fails on my system.

$ tox-venv py27 - tests/unittests/test_datasource/test_cloudstack.py
envpython=python2.7
inside tox:py27 running: python -m nose tests/unittests/test_datasource/test_cloudstack.py
.........F...
======================================================================
FAIL: If multiple files match, all should be used.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/smoser-public/src/cloud-init/cloud-init/tests/unittests/test_datasource/test_cloudstack.py", line 173, in test_selects_all_matching
    get_leases(lease_d))
AssertionError: Lists differ: ['/tm[29 chars]lient.leases', '/tmp/ci-TestGetLeases.LS7lQh/d[62 chars]ase'] != ['/tm[29 chars]lient-eth3.lease', '/tmp/ci-TestGetLeases.LS7l[62 chars]ase']

First differing element 0:
/tmp/ci-TestGetLeases.LS7lQh/dhclient.leases
/tmp/ci-TestGetLeases.LS7lQh/dhclient-eth3.lease

- ['/tmp/ci-TestGetLeases.LS7lQh/dhclient.leases',
? -

+ ['/tmp/ci-TestGetLeases.LS7lQh/dhclient-eth3.lease',
? +++++

- '/tmp/ci-TestGetLeases.LS7lQh/dhclient.lease',
+ '/tmp/ci-TestGetLeases.LS7lQh/dhclient.leases',
? +

- '/tmp/ci-TestGetLeases.LS7lQh/dhclient-eth3.lease']
? -----

+ '/tmp/ci-TestGetLeases.LS7lQh/dhclient.lease']
-------------------- >> begin captured logging << --------------------
cloudinit.util: DEBUG: Writing to /tmp/ci-TestGetLeases.LS7lQh/dhclient.leases - wb: [644] 15 bytes
cloudinit.util: DEBUG: Writing to /tmp/ci-TestGetLeases.LS7lQh/dhclient.lease - wb: [644] 14 bytes
cloudinit.util: DEBUG: Writing to /tmp/ci-TestGetLeases.LS7lQh/dhclient.leases - wb: [644] 15 bytes
cloudinit.util: DEBUG: Writing to /tmp/ci-TestGetLeases.LS7lQh/dhclient.lease - wb: [644] 14 bytes
cloudinit.util: DEBUG: Writing to /tmp/ci-TestGetLeases.LS7lQh/dhclient-eth3.lease - wb: [644] 19 bytes
--------------------- >> end captured logging << ---------------------

----------------------------------------------------------------------
Ran 13 tests in 0.259s

FAILED (failures=1)

Revision history for this message
Scott Moser (smoser) wrote :

Bump.

@Marc,
I had a request above.

Revision history for this message
Ryan Harper (raharper) wrote :

Hi Marc,

I've marked this branch Work in Progress. Scott had left you some review comments that need applied before we can land this branch. Once you've done so (and likely rebase on master) please push and then update this merge proposal back to Needs Review and we'll pick things back up.

Unmerged commits

1832cc7... by Marc-Aurele Brothier

CloudStack: go through all potential DHCP lease to find the meta/user-data server

Signed-off-by: Marc-Aurèle Brothier <email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py
index 0df545f..e3e922b 100644
--- a/cloudinit/sources/DataSourceCloudStack.py
+++ b/cloudinit/sources/DataSourceCloudStack.py
@@ -13,23 +13,22 @@
13# This file is part of cloud-init. See LICENSE file for license information.13# This file is part of cloud-init. See LICENSE file for license information.
1414
15import os15import os
16import time
16from socket import inet_ntoa17from socket import inet_ntoa
17from struct import pack18from struct import pack
18import time
1919
20from cloudinit import ec2_utils as ec220from cloudinit import ec2_utils as ec2
21from cloudinit import log as logging21from cloudinit import log as logging
22from cloudinit.net import dhcp
23from cloudinit import sources22from cloudinit import sources
24from cloudinit import url_helper as uhelp23from cloudinit import url_helper as uhelp
25from cloudinit import util24from cloudinit import util
25from cloudinit.net import dhcp
2626
27LOG = logging.getLogger(__name__)27LOG = logging.getLogger(__name__)
2828
2929
30class CloudStackPasswordServerClient(object):30class CloudStackPasswordServerClient(object):
31 """31 """Implements password fetching from the CloudStack password server.
32 Implements password fetching from the CloudStack password server.
3332
34 http://cloudstack-administration.readthedocs.org/33 http://cloudstack-administration.readthedocs.org/
35 en/latest/templates.html#adding-password-management-to-your-templates34 en/latest/templates.html#adding-password-management-to-your-templates
@@ -74,10 +73,9 @@ class DataSourceCloudStack(sources.DataSource):
74 # Cloudstack has its metadata/userdata URLs located at73 # Cloudstack has its metadata/userdata URLs located at
75 # http://<virtual-router-ip>/latest/74 # http://<virtual-router-ip>/latest/
76 self.api_ver = 'latest'75 self.api_ver = 'latest'
77 self.vr_addr = get_vr_address()76 self.vr_addresses = get_vr_addresses()
78 if not self.vr_addr:77 if not self.vr_addresses:
79 raise RuntimeError("No virtual router found!")78 raise RuntimeError("No virtual router found!")
80 self.metadata_address = "http://%s/" % (self.vr_addr,)
81 self.cfg = {}79 self.cfg = {}
8280
83 def _get_url_settings(self):81 def _get_url_settings(self):
@@ -102,14 +100,17 @@ class DataSourceCloudStack(sources.DataSource):
102 def wait_for_metadata_service(self):100 def wait_for_metadata_service(self):
103 (max_wait, timeout) = self._get_url_settings()101 (max_wait, timeout) = self._get_url_settings()
104102
105 urls = [uhelp.combine_url(self.metadata_address,103 urls = [uhelp.combine_url("http://%s/" % vr_potential_ip,
106 'latest/meta-data/instance-id')]104 'latest/meta-data/instance-id')
105 for vr_potential_ip in self.vr_addresses]
107 start_time = time.time()106 start_time = time.time()
108 url = uhelp.wait_for_url(urls=urls, max_wait=max_wait,107 url = uhelp.wait_for_url(urls=urls, max_wait=max_wait,
109 timeout=timeout, status_cb=LOG.warn)108 timeout=timeout, status_cb=LOG.warn)
110109
111 if url:110 if url:
112 LOG.debug("Using metadata source: '%s'", url)111 LOG.debug("Using metadata source: '%s'", url)
112 self.vr_addr = url.split('/')[2]
113 self.metadata_address = "http://%s/" % (self.vr_addr,)
113 else:114 else:
114 LOG.critical(("Giving up on waiting for the metadata from %s"115 LOG.critical(("Giving up on waiting for the metadata from %s"
115 " after %s seconds"),116 " after %s seconds"),
@@ -168,7 +169,7 @@ class DataSourceCloudStack(sources.DataSource):
168169
169170
170def get_default_gateway():171def get_default_gateway():
171 # Returns the default gateway ip address in the dotted format.172 """Returns the default gateway ip address in the dotted format."""
172 lines = util.load_file("/proc/net/route").splitlines()173 lines = util.load_file("/proc/net/route").splitlines()
173 for line in lines:174 for line in lines:
174 items = line.split("\t")175 items = line.split("\t")
@@ -181,7 +182,7 @@ def get_default_gateway():
181182
182183
183def get_dhclient_d():184def get_dhclient_d():
184 # find lease files directory185 """Find lease files directory."""
185 supported_dirs = ["/var/lib/dhclient", "/var/lib/dhcp",186 supported_dirs = ["/var/lib/dhclient", "/var/lib/dhcp",
186 "/var/lib/NetworkManager"]187 "/var/lib/NetworkManager"]
187 for d in supported_dirs:188 for d in supported_dirs:
@@ -191,15 +192,14 @@ def get_dhclient_d():
191 return None192 return None
192193
193194
194def get_latest_lease(lease_d=None):195def get_leases(lease_d=None):
195 # find latest lease file196 """Find all lease files."""
196 if lease_d is None:197 if lease_d is None:
197 lease_d = get_dhclient_d()198 lease_d = get_dhclient_d()
198 if not lease_d:199 if not lease_d:
199 return None200 return None
200 lease_files = os.listdir(lease_d)201 lease_files = os.listdir(lease_d)
201 latest_mtime = -1202 valid_lease_files = []
202 latest_file = None
203203
204 # lease files are named inconsistently across distros.204 # lease files are named inconsistently across distros.
205 # We assume that 'dhclient6' indicates ipv6 and ignore it.205 # We assume that 'dhclient6' indicates ipv6 and ignore it.
@@ -215,18 +215,17 @@ def get_latest_lease(lease_d=None):
215 continue215 continue
216 if not (fname.endswith(".lease") or fname.endswith(".leases")):216 if not (fname.endswith(".lease") or fname.endswith(".leases")):
217 continue217 continue
218218 valid_lease_files.append(os.path.join(lease_d, fname))
219 abs_path = os.path.join(lease_d, fname)219 return valid_lease_files
220 mtime = os.path.getmtime(abs_path)
221 if mtime > latest_mtime:
222 latest_mtime = mtime
223 latest_file = abs_path
224 return latest_file
225220
226221
227def get_vr_address():222def get_vr_addresses():
228 # Get the address of the virtual router via dhcp leases223 """Get a list of potential addresses for the virtual router via DHCP
229 # If no virtual router is detected, fallback on default gateway.224 leases. This overcomes the situation when multiple interfaces are
225 configured through DHCP, one of them will come from CloudStack VR and
226 all addresses should be tested until the meta-data server responds. The
227 latest entry will always be the default gateway.
228 """
230 # See http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/4.8/virtual_machines/user-data.html # noqa229 # See http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/4.8/virtual_machines/user-data.html # noqa
231230
232 # Try networkd first...231 # Try networkd first...
@@ -234,27 +233,30 @@ def get_vr_address():
234 if latest_address:233 if latest_address:
235 LOG.debug("Found SERVER_ADDRESS '%s' via networkd_leases",234 LOG.debug("Found SERVER_ADDRESS '%s' via networkd_leases",
236 latest_address)235 latest_address)
237 return latest_address236 return [latest_address]
238237
239 # Try dhcp lease files next...238 # Try dhcp lease files next...
240 lease_file = get_latest_lease()239 lease_files = get_leases()
241 if not lease_file:240 if not lease_files:
242 LOG.debug("No lease file found, using default gateway")241 LOG.debug("No lease file found, using default gateway")
243 return get_default_gateway()242 return [get_default_gateway()]
244243
245 with open(lease_file, "r") as fd:244 potential_addresses = []
246 for line in fd:245 for lease_file in lease_files:
247 if "dhcp-server-identifier" in line:246 with open(lease_file, "r") as fd:
248 words = line.strip(" ;\r\n").split(" ")247 for line in fd:
249 if len(words) > 2:248 if "dhcp-server-identifier" in line:
250 dhcptok = words[2]249 words = line.strip(" ;\r\n").split(" ")
251 LOG.debug("Found DHCP identifier %s", dhcptok)250 if len(words) > 2:
252 latest_address = dhcptok251 dhcptok = words[2]
253 if not latest_address:252 if dhcptok not in potential_addresses:
253 LOG.debug("Found DHCP identifier %s", dhcptok)
254 potential_addresses.append(dhcptok)
255 if not potential_addresses:
254 # No virtual router found, fallback on default gateway256 # No virtual router found, fallback on default gateway
255 LOG.debug("No DHCP found, using default gateway")257 LOG.debug("No DHCP found, using default gateway")
256 return get_default_gateway()258 return [get_default_gateway()]
257 return latest_address259 return potential_addresses
258260
259261
260# Used to match classes to dependencies262# Used to match classes to dependencies
@@ -263,8 +265,8 @@ datasources = [
263]265]
264266
265267
266# Return a list of data sources that match this set of dependencies
267def get_datasource_list(depends):268def get_datasource_list(depends):
269 """Return a list of data sources that match this set of dependencies."""
268 return sources.list_from_depends(depends, datasources)270 return sources.list_from_depends(depends, datasources)
269271
270# vi: ts=4 expandtab272# vi: ts=4 expandtab
diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/test_datasource/test_cloudstack.py
index d6d2d6b..29de7e4 100644
--- a/tests/unittests/test_datasource/test_cloudstack.py
+++ b/tests/unittests/test_datasource/test_cloudstack.py
@@ -1,15 +1,14 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import os
4import time
5
3from cloudinit import helpers6from cloudinit import helpers
4from cloudinit import util7from cloudinit import util
5from cloudinit.sources.DataSourceCloudStack import (8from cloudinit.sources.DataSourceCloudStack import (
6 DataSourceCloudStack, get_latest_lease)9 DataSourceCloudStack, get_leases)
7
8from cloudinit.tests.helpers import CiTestCase, ExitStack, mock10from cloudinit.tests.helpers import CiTestCase, ExitStack, mock
911
10import os
11import time
12
1312
14class TestCloudStackPasswordFetching(CiTestCase):13class TestCloudStackPasswordFetching(CiTestCase):
1514
@@ -21,9 +20,9 @@ class TestCloudStackPasswordFetching(CiTestCase):
21 self.patches.enter_context(mock.patch('{0}.ec2'.format(mod_name)))20 self.patches.enter_context(mock.patch('{0}.ec2'.format(mod_name)))
22 self.patches.enter_context(mock.patch('{0}.uhelp'.format(mod_name)))21 self.patches.enter_context(mock.patch('{0}.uhelp'.format(mod_name)))
23 default_gw = "192.201.20.0"22 default_gw = "192.201.20.0"
24 get_latest_lease = mock.MagicMock(return_value=None)23 get_leases = mock.MagicMock(return_value=None)
25 self.patches.enter_context(mock.patch(24 self.patches.enter_context(mock.patch(
26 mod_name + '.get_latest_lease', get_latest_lease))25 mod_name + '.get_leases', get_leases))
2726
28 get_default_gw = mock.MagicMock(return_value=default_gw)27 get_default_gw = mock.MagicMock(return_value=default_gw)
29 self.patches.enter_context(mock.patch(28 self.patches.enter_context(mock.patch(
@@ -105,7 +104,7 @@ class TestCloudStackPasswordFetching(CiTestCase):
105 self._check_password_not_saved_for('bad_request')104 self._check_password_not_saved_for('bad_request')
106105
107106
108class TestGetLatestLease(CiTestCase):107class TestGetLeases(CiTestCase):
109108
110 def _populate_dir_list(self, bdir, files):109 def _populate_dir_list(self, bdir, files):
111 """populate_dir_list([(name, data), (name, data)])110 """populate_dir_list([(name, data), (name, data)])
@@ -122,8 +121,8 @@ class TestGetLatestLease(CiTestCase):
122 def _pop_and_test(self, files, expected):121 def _pop_and_test(self, files, expected):
123 lease_d = self.tmp_dir()122 lease_d = self.tmp_dir()
124 self._populate_dir_list(lease_d, files)123 self._populate_dir_list(lease_d, files)
125 self.assertEqual(self.tmp_path(expected, lease_d),124 self.assertEqual([self.tmp_path(expected, lease_d)],
126 get_latest_lease(lease_d))125 get_leases(lease_d))
127126
128 def test_skips_dhcpv6_files(self):127 def test_skips_dhcpv6_files(self):
129 """files started with dhclient6 should be skipped."""128 """files started with dhclient6 should be skipped."""
@@ -154,22 +153,24 @@ class TestGetLatestLease(CiTestCase):
154 "dhclient.lease-old", "dhclient.leaselease"],153 "dhclient.lease-old", "dhclient.leaselease"],
155 "dhclient.lease")154 "dhclient.lease")
156155
157 def test_selects_newest_matching(self):156 def test_selects_all_matching(self):
158 """If multiple files match, the newest written should be used."""157 """If multiple files match, all should be used."""
159 lease_d = self.tmp_dir()158 lease_d = self.tmp_dir()
160 valid_1 = "dhclient.leases"159 valid_1 = "dhclient.leases"
161 valid_2 = "dhclient.lease"160 valid_2 = "dhclient.lease"
162 valid_1_path = self.tmp_path(valid_1, lease_d)161 valid_1_path = self.tmp_path(valid_1, lease_d)
163 valid_2_path = self.tmp_path(valid_2, lease_d)162 valid_2_path = self.tmp_path(valid_2, lease_d)
164163
165 self._populate_dir_list(lease_d, [valid_1, valid_2])164 self._populate_dir_list(lease_d, [valid_2, valid_1])
166 self.assertEqual(valid_2_path, get_latest_lease(lease_d))165 self.assertEqual([valid_1_path, valid_2_path], get_leases(lease_d))
167166
168 # now update mtime on valid_2 to be older than valid_1 and re-check.167 # now add another lease file
169 mtime = int(os.path.getmtime(valid_1_path)) - 1168 valid_3 = "dhclient-eth3.lease"
170 os.utime(valid_2_path, (mtime, mtime))169 valid_3_path = self.tmp_path(valid_3, lease_d)
170 self._populate_dir_list(lease_d, [valid_3, valid_2, valid_1])
171171
172 self.assertEqual(valid_1_path, get_latest_lease(lease_d))172 self.assertEqual([valid_1_path, valid_2_path, valid_3_path],
173 get_leases(lease_d))
173174
174175
175# vi: ts=4 expandtab176# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches