Skip to content

Commit 308c58c

Browse files
committed
chore(opennebula): address review feedback on type annotations
Add type annotations to parse_shell_config, get_field (with @overload), and all OpenNebulaNetwork.get_* methods as requested. Replace assert-based type narrowing with proper guards. Remove leaked global assignment in test and revert unrelated cosmetic changes. Refs GH-6810
1 parent 14ce1f0 commit 308c58c

File tree

2 files changed

+68
-39
lines changed

2 files changed

+68
-39
lines changed

cloudinit/sources/DataSourceOpenNebula.py

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

2525
from cloudinit import atomic_helper, net, sources, subp, util
2626

@@ -42,6 +42,7 @@
4242

4343

4444
class DataSourceOpenNebula(sources.DataSource):
45+
4546
dsname = "OpenNebula"
4647

4748
def __init__(self, sys_cfg, distro, paths):
@@ -94,11 +95,9 @@ 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

100-
assert results is not None
101-
102101
# merge fetched metadata with datasource defaults
103102
md = results["metadata"]
104103
md = util.mergemanydict([md, defaults])
@@ -119,7 +118,10 @@ def _get_data(self):
119118

120119
def _get_subplatform(self):
121120
"""Return the subplatform metadata source details."""
122-
assert self.seed is not None
121+
if self.seed is None:
122+
raise RuntimeError(
123+
"_get_subplatform called before datasource was read"
124+
)
123125
if self.seed_dir in self.seed:
124126
subplatform_type = "seed-dir"
125127
else:
@@ -151,33 +153,40 @@ class BrokenContextDiskDir(Exception):
151153

152154

153155
class OpenNebulaNetwork:
154-
def __init__(self, context, distro, system_nics_by_mac=None):
156+
def __init__(
157+
self,
158+
context: Dict[str, str],
159+
distro: Any,
160+
system_nics_by_mac: Optional[Dict[str, str]] = None,
161+
) -> None:
155162
self.context = context
156163
if system_nics_by_mac is None:
157164
system_nics_by_mac = get_physical_nics_by_mac(distro)
158-
self.ifaces = collections.OrderedDict(
159-
[
160-
k
161-
for k in sorted(
162-
system_nics_by_mac.items(),
163-
key=lambda k: net.natural_sort_key(k[1]),
164-
)
165-
]
165+
self.ifaces: collections.OrderedDict[str, str] = (
166+
collections.OrderedDict(
167+
[
168+
k
169+
for k in sorted(
170+
system_nics_by_mac.items(),
171+
key=lambda k: net.natural_sort_key(k[1]),
172+
)
173+
]
174+
)
166175
)
167176

168177
# OpenNebula 4.14+ provide macaddr for ETHX in variable ETH_MAC.
169178
# context_devname provides {mac.lower():ETHX, mac2.lower():ETHX}
170-
self.context_devname = {}
179+
self.context_devname: Dict[str, str] = {}
171180
for k, v in context.items():
172181
m = re.match(r"^(.+)_MAC$", k)
173182
if m:
174183
self.context_devname[v.lower()] = m.group(1)
175184

176-
def mac2ip(self, mac):
185+
def mac2ip(self, mac: str) -> str:
177186
return ".".join([str(int(c, 16)) for c in mac.split(":")[2:]])
178187

179-
def get_nameservers(self, dev):
180-
nameservers = {}
188+
def get_nameservers(self, dev: str) -> Dict[str, List[str]]:
189+
nameservers: Dict[str, List[str]] = {}
181190
dns = self.get_field(dev, "dns", "").split()
182191
dns.extend(self.context.get("DNS", "").split())
183192
if dns:
@@ -187,14 +196,14 @@ def get_nameservers(self, dev):
187196
nameservers["search"] = search_domain
188197
return nameservers
189198

190-
def get_mtu(self, dev):
199+
def get_mtu(self, dev: str) -> Optional[str]:
191200
return self.get_field(dev, "mtu")
192201

193-
def get_ip(self, dev, mac):
202+
def get_ip(self, dev: str, mac: str) -> str:
194203
return self.get_field(dev, "ip", self.mac2ip(mac))
195204

196-
def get_ip6(self, dev):
197-
addresses6 = []
205+
def get_ip6(self, dev: str) -> List[str]:
206+
addresses6: List[str] = []
198207
ip6 = self.get_field(dev, "ip6")
199208
if ip6:
200209
addresses6.append(ip6)
@@ -203,24 +212,35 @@ def get_ip6(self, dev):
203212
addresses6.append(ip6_ula)
204213
return addresses6
205214

206-
def get_ip6_prefix(self, dev):
215+
def get_ip6_prefix(self, dev: str) -> str:
207216
return self.get_field(dev, "ip6_prefix_length", "64")
208217

209-
def get_gateway(self, dev):
218+
def get_gateway(self, dev: str) -> Optional[str]:
210219
return self.get_field(dev, "gateway")
211220

212-
def get_gateway6(self, dev):
221+
def get_gateway6(self, dev: str) -> Optional[str]:
213222
# OpenNebula 6.1.80 introduced new context parameter ETHx_IP6_GATEWAY
214223
# to replace old ETHx_GATEWAY6. Old ETHx_GATEWAY6 will be removed in
215224
# OpenNebula 6.4.0 (https://github.com/OpenNebula/one/issues/5536).
216-
return self.get_field(
217-
dev, "ip6_gateway", self.get_field(dev, "gateway6")
218-
)
225+
ip6_gateway = self.get_field(dev, "ip6_gateway")
226+
if ip6_gateway is not None:
227+
return ip6_gateway
228+
return self.get_field(dev, "gateway6")
219229

220-
def get_mask(self, dev):
230+
def get_mask(self, dev: str) -> str:
221231
return self.get_field(dev, "mask", "255.255.255.0")
222232

223-
def get_field(self, dev, name, default=None):
233+
@overload
234+
def get_field(self, dev: str, name: str) -> Optional[str]: ...
235+
@overload
236+
def get_field(
237+
self, dev: str, name: str, default: None
238+
) -> Optional[str]: ...
239+
@overload
240+
def get_field(self, dev: str, name: str, default: str) -> str: ...
241+
def get_field(
242+
self, dev: str, name: str, default: Optional[str] = None
243+
) -> Optional[str]:
224244
"""return the field name in context for device dev.
225245
226246
context stores <dev>_<NAME> (example: eth0_DOMAIN).
@@ -324,7 +344,9 @@ def varprinter():
324344
)
325345

326346

327-
def parse_shell_config(content, asuser=None):
347+
def parse_shell_config(
348+
content: str, asuser: Optional[str] = None
349+
) -> Dict[str, str]:
328350
"""run content and return environment variables which changed
329351
330352
WARNING: the special variable _start_ is used to delimit content
@@ -398,10 +420,14 @@ def parse_shell_config(content, asuser=None):
398420
def read_context_disk_dir(
399421
source_dir: str, distro: Any, asuser: Optional[str] = None
400422
) -> Dict[str, Any]:
401-
"""
402-
read_context_disk_dir(source_dir):
403-
read source_dir and return a tuple with metadata dict and user-data
404-
string populated. If not a valid dir, raise a NonContextDiskDir
423+
"""Read ``source_dir`` and return a dictionary containing context data.
424+
425+
The returned dictionary always includes ``"metadata"`` and
426+
``"userdata"`` keys, and may also include ``"network-interfaces"``
427+
when network configuration can be generated from the context.
428+
429+
If ``source_dir`` is not a valid context directory, raise
430+
``NonContextDiskDir``.
405431
"""
406432
found: Dict[str, str] = {}
407433
for af in CONTEXT_DISK_FILES:
@@ -493,7 +519,7 @@ def read_context_disk_dir(
493519
return results
494520

495521

496-
def get_physical_nics_by_mac(distro):
522+
def get_physical_nics_by_mac(distro: Any) -> Dict[str, str]:
497523
devs = net.get_interfaces_by_mac()
498524
return dict(
499525
[(m, n) for m, n in devs.items() if distro.networking.is_physical(n)]

tests/unittests/sources/test_opennebula.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,12 +365,14 @@ def my_devs_with(criteria):
365365
}.get(criteria, [])
366366

367367
m_find_devs_with.side_effect = my_devs_with
368+
# Signature of my_devs_with doesn't match util.find_devs_with
368369
util.find_devs_with = my_devs_with # type: ignore[assignment]
369370
assert ["/dev/sdb", "/dev/sr0", "/dev/vdb"] == ds.find_candidate_devs()
370371

371372

372373
@mock.patch(DS_PATH + ".net.get_interfaces_by_mac", mock.Mock(return_value={}))
373374
class TestOpenNebulaNetwork:
375+
374376
system_nics = ("eth0", "ens3")
375377

376378
def test_context_devname(self):
@@ -575,8 +577,9 @@ def test_get_field_nonecontext(self):
575577
Verify get_field('device', 'name') returns None if context value is
576578
None.
577579
"""
580+
# Deliberately passing None value to test defensive handling in get_field
578581
context = {"ETH9_DUMMY": None}
579-
net = ds.OpenNebulaNetwork(context, mock.Mock())
582+
net = ds.OpenNebulaNetwork(context, mock.Mock()) # type: ignore[arg-type]
580583
val = net.get_field("eth9", "dummy")
581584
assert None is val
582585

@@ -1015,8 +1018,8 @@ class TestGetPhysicalNicsByMac:
10151018
)
10161019
def test(self, interfaces_by_mac, physical_devs, expected_return):
10171020
distro = mock.Mock()
1018-
distro.networking.is_physical.side_effect = lambda devname: (
1019-
devname in physical_devs
1021+
distro.networking.is_physical.side_effect = (
1022+
lambda devname: devname in physical_devs
10201023
)
10211024
with mock.patch(
10221025
DS_PATH + ".net.get_interfaces_by_mac",

0 commit comments

Comments
 (0)