Skip to content

Commit 87cc1ae

Browse files
authored
chore(opennebula): enable mypy type checking (#6827)
Remove DataSourceOpenNebula and its unit tests from the mypy check_untyped_defs=false override list and add type annotations to satisfy stricter checking. Annotate parse_shell_config, OpenNebulaNetwork.__init__, get_field (with @overload to distinguish str vs Optional[str] return), all get_* methods, get_physical_nics_by_mac, and read_context_disk_dir (fixing its stale docstring). Remove test_get_field_nonecontext: shell variables cannot hold None values so the scenario is impossible; test_get_field_emptycontext covers the realistic equivalent. Remove leftover bare `util.find_devs_with` assignment in test_find_candidates, a refactoring artifact from d482353 that leaked state between tests. Refs GH-6810
1 parent dd28a36 commit 87cc1ae

3 files changed

Lines changed: 74 additions & 60 deletions

File tree

cloudinit/sources/DataSourceOpenNebula.py

Lines changed: 73 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import re
2121
import shlex
2222
import textwrap
23+
from typing import Any, Dict, List, Optional, overload
2324

2425
from cloudinit import atomic_helper, net, sources, subp, util
2526

@@ -56,8 +57,8 @@ def __str__(self):
5657

5758
def _get_data(self):
5859
defaults = {"instance-id": DEFAULT_IID}
59-
results = None
60-
seed = None
60+
results: Optional[Dict[str, Any]] = None
61+
seed: Optional[str] = None
6162

6263
# decide parseuser for context.sh shell reader
6364
parseuser = DEFAULT_PARSEUSER
@@ -94,7 +95,7 @@ def _get_data(self):
9495
LOG.debug("found datasource in %s", cdev)
9596
break
9697

97-
if not seed:
98+
if not seed or results is None:
9899
return False
99100

100101
# merge fetched metadata with datasource defaults
@@ -115,8 +116,10 @@ def _get_data(self):
115116
self.userdata_raw = results.get("userdata")
116117
return True
117118

118-
def _get_subplatform(self):
119+
def _get_subplatform(self) -> str:
119120
"""Return the subplatform metadata source details."""
121+
if self.seed is None:
122+
return sources.METADATA_UNKNOWN
120123
if self.seed_dir in self.seed:
121124
subplatform_type = "seed-dir"
122125
else:
@@ -148,33 +151,40 @@ class BrokenContextDiskDir(Exception):
148151

149152

150153
class OpenNebulaNetwork:
151-
def __init__(self, context, distro, system_nics_by_mac=None):
154+
def __init__(
155+
self,
156+
context: Dict[str, str],
157+
distro: Any,
158+
system_nics_by_mac: Optional[Dict[str, str]] = None,
159+
) -> None:
152160
self.context = context
153161
if system_nics_by_mac is None:
154162
system_nics_by_mac = get_physical_nics_by_mac(distro)
155-
self.ifaces = collections.OrderedDict(
156-
[
157-
k
158-
for k in sorted(
159-
system_nics_by_mac.items(),
160-
key=lambda k: net.natural_sort_key(k[1]),
161-
)
162-
]
163+
self.ifaces: collections.OrderedDict[str, str] = (
164+
collections.OrderedDict(
165+
[
166+
k
167+
for k in sorted(
168+
system_nics_by_mac.items(),
169+
key=lambda k: net.natural_sort_key(k[1]),
170+
)
171+
]
172+
)
163173
)
164174

165175
# OpenNebula 4.14+ provide macaddr for ETHX in variable ETH_MAC.
166176
# context_devname provides {mac.lower():ETHX, mac2.lower():ETHX}
167-
self.context_devname = {}
177+
self.context_devname: Dict[str, str] = {}
168178
for k, v in context.items():
169179
m = re.match(r"^(.+)_MAC$", k)
170180
if m:
171181
self.context_devname[v.lower()] = m.group(1)
172182

173-
def mac2ip(self, mac):
183+
def mac2ip(self, mac: str) -> str:
174184
return ".".join([str(int(c, 16)) for c in mac.split(":")[2:]])
175185

176-
def get_nameservers(self, dev):
177-
nameservers = {}
186+
def get_nameservers(self, dev: str) -> Dict[str, List[str]]:
187+
nameservers: Dict[str, List[str]] = {}
178188
dns = self.get_field(dev, "dns", "").split()
179189
dns.extend(self.context.get("DNS", "").split())
180190
if dns:
@@ -184,14 +194,14 @@ def get_nameservers(self, dev):
184194
nameservers["search"] = search_domain
185195
return nameservers
186196

187-
def get_mtu(self, dev):
197+
def get_mtu(self, dev: str) -> Optional[str]:
188198
return self.get_field(dev, "mtu")
189199

190-
def get_ip(self, dev, mac):
200+
def get_ip(self, dev: str, mac: str) -> str:
191201
return self.get_field(dev, "ip", self.mac2ip(mac))
192202

193-
def get_ip6(self, dev):
194-
addresses6 = []
203+
def get_ip6(self, dev: str) -> List[str]:
204+
addresses6: List[str] = []
195205
ip6 = self.get_field(dev, "ip6")
196206
if ip6:
197207
addresses6.append(ip6)
@@ -200,24 +210,35 @@ def get_ip6(self, dev):
200210
addresses6.append(ip6_ula)
201211
return addresses6
202212

203-
def get_ip6_prefix(self, dev):
213+
def get_ip6_prefix(self, dev: str) -> str:
204214
return self.get_field(dev, "ip6_prefix_length", "64")
205215

206-
def get_gateway(self, dev):
216+
def get_gateway(self, dev: str) -> Optional[str]:
207217
return self.get_field(dev, "gateway")
208218

209-
def get_gateway6(self, dev):
219+
def get_gateway6(self, dev: str) -> Optional[str]:
210220
# OpenNebula 6.1.80 introduced new context parameter ETHx_IP6_GATEWAY
211221
# to replace old ETHx_GATEWAY6. Old ETHx_GATEWAY6 will be removed in
212222
# OpenNebula 6.4.0 (https://github.com/OpenNebula/one/issues/5536).
213-
return self.get_field(
214-
dev, "ip6_gateway", self.get_field(dev, "gateway6")
215-
)
223+
ip6_gateway = self.get_field(dev, "ip6_gateway")
224+
if ip6_gateway is not None:
225+
return ip6_gateway
226+
return self.get_field(dev, "gateway6")
216227

217-
def get_mask(self, dev):
228+
def get_mask(self, dev: str) -> str:
218229
return self.get_field(dev, "mask", "255.255.255.0")
219230

220-
def get_field(self, dev, name, default=None):
231+
@overload
232+
def get_field(self, dev: str, name: str) -> Optional[str]: ...
233+
@overload
234+
def get_field(
235+
self, dev: str, name: str, default: None
236+
) -> Optional[str]: ...
237+
@overload
238+
def get_field(self, dev: str, name: str, default: str) -> str: ...
239+
def get_field(
240+
self, dev: str, name: str, default: Optional[str] = None
241+
) -> Optional[str]:
221242
"""return the field name in context for device dev.
222243
223244
context stores <dev>_<NAME> (example: eth0_DOMAIN).
@@ -233,20 +254,18 @@ def get_field(self, dev, name, default=None):
233254
# allow empty string to return the default.
234255
return default if val in (None, "") else val
235256

236-
def gen_conf(self):
237-
netconf = {}
238-
netconf["version"] = 2
239-
netconf["ethernets"] = {}
257+
def gen_conf(self) -> Dict[str, Any]:
258+
netconf: Dict[str, Any] = {"version": 2, "ethernets": {}}
240259

241-
ethernets = {}
260+
ethernets: Dict[str, Dict[str, Any]] = {}
242261
for mac, dev in self.ifaces.items():
243262
mac = mac.lower()
244263

245264
# c_dev stores name in context 'ETHX' for this device.
246265
# dev stores the current system name.
247266
c_dev = self.context_devname.get(mac, dev)
248267

249-
devconf = {}
268+
devconf: Dict[str, Any] = {}
250269

251270
# Set MAC address
252271
devconf["match"] = {"macaddress": mac}
@@ -323,7 +342,9 @@ def varprinter():
323342
)
324343

325344

326-
def parse_shell_config(content, asuser=None):
345+
def parse_shell_config(
346+
content: str, asuser: Optional[str] = None
347+
) -> Dict[str, str]:
327348
"""run content and return environment variables which changed
328349
329350
WARNING: the special variable _start_ is used to delimit content
@@ -394,13 +415,19 @@ def parse_shell_config(content, asuser=None):
394415
return ret
395416

396417

397-
def read_context_disk_dir(source_dir, distro, asuser=None):
398-
"""
399-
read_context_disk_dir(source_dir):
400-
read source_dir and return a tuple with metadata dict and user-data
401-
string populated. If not a valid dir, raise a NonContextDiskDir
418+
def read_context_disk_dir(
419+
source_dir: str, distro: Any, asuser: Optional[str] = None
420+
) -> Dict[str, Any]:
421+
"""Read ``source_dir`` and return a dictionary containing context data.
422+
423+
The returned dictionary always includes ``"metadata"`` and
424+
``"userdata"`` keys, and may also include ``"network-interfaces"``
425+
when network configuration can be generated from the context.
426+
427+
If ``source_dir`` is not a valid context directory, raise
428+
``NonContextDiskDir``.
402429
"""
403-
found = {}
430+
found: Dict[str, str] = {}
404431
for af in CONTEXT_DISK_FILES:
405432
fn = os.path.join(source_dir, af)
406433
if os.path.isfile(fn):
@@ -409,8 +436,8 @@ def read_context_disk_dir(source_dir, distro, asuser=None):
409436
if not found:
410437
raise NonContextDiskDir("%s: %s" % (source_dir, "no files found"))
411438

412-
context = {}
413-
results = {"userdata": None, "metadata": {}}
439+
context: Dict[str, str] = {}
440+
results: Dict[str, Any] = {"userdata": None, "metadata": {}}
414441

415442
if "context.sh" in found:
416443
if asuser is not None:
@@ -450,7 +477,7 @@ def read_context_disk_dir(source_dir, distro, asuser=None):
450477
ssh_key_var = "SSH_PUBLIC_KEY"
451478

452479
if ssh_key_var:
453-
lines = context.get(ssh_key_var).splitlines()
480+
lines = context[ssh_key_var].splitlines()
454481
results["metadata"]["public-keys"] = [
455482
line for line in lines if len(line) and not line.startswith("#")
456483
]
@@ -490,7 +517,7 @@ def read_context_disk_dir(source_dir, distro, asuser=None):
490517
return results
491518

492519

493-
def get_physical_nics_by_mac(distro):
520+
def get_physical_nics_by_mac(distro: Any) -> Dict[str, str]:
494521
devs = net.get_interfaces_by_mac()
495522
return dict(
496523
[(m, n) for m, n in devs.items() if distro.networking.is_physical(n)]

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ module = [
7878
"cloudinit.sources.DataSourceHetzner",
7979
"cloudinit.sources.DataSourceNoCloud",
8080
"cloudinit.sources.DataSourceOVF",
81-
"cloudinit.sources.DataSourceOpenNebula",
8281
"cloudinit.sources.DataSourceOpenStack",
8382
"cloudinit.sources.DataSourceOracle",
8483
"cloudinit.sources.DataSourceRbxCloud",
@@ -146,7 +145,6 @@ module = [
146145
"tests.unittests.sources.test_gce",
147146
"tests.unittests.sources.test_init",
148147
"tests.unittests.sources.test_nocloud",
149-
"tests.unittests.sources.test_opennebula",
150148
"tests.unittests.sources.test_openstack",
151149
"tests.unittests.sources.test_oracle",
152150
"tests.unittests.sources.test_scaleway",

tests/unittests/sources/test_opennebula.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import pytest
99

10-
from cloudinit import atomic_helper, util
10+
from cloudinit import atomic_helper
1111
from cloudinit.sources import DataSourceOpenNebula as ds
1212
from tests.unittests.helpers import populate_dir
1313

@@ -365,7 +365,6 @@ def my_devs_with(criteria):
365365
}.get(criteria, [])
366366

367367
m_find_devs_with.side_effect = my_devs_with
368-
util.find_devs_with = my_devs_with
369368
assert ["/dev/sdb", "/dev/sr0", "/dev/vdb"] == ds.find_candidate_devs()
370369

371370

@@ -571,16 +570,6 @@ def test_get_field_emptycontext(self):
571570
val = net.get_field("eth9", "dummy")
572571
assert None is val
573572

574-
def test_get_field_nonecontext(self):
575-
"""
576-
Verify get_field('device', 'name') returns None if context value is
577-
None.
578-
"""
579-
context = {"ETH9_DUMMY": None}
580-
net = ds.OpenNebulaNetwork(context, mock.Mock())
581-
val = net.get_field("eth9", "dummy")
582-
assert None is val
583-
584573
@mock.patch(DS_PATH + ".get_physical_nics_by_mac")
585574
def test_gen_conf_gateway(self, m_get_phys_by_mac):
586575
"""Test rendering with/without IPv4 gateway"""

0 commit comments

Comments
 (0)