Page MenuHomePhabricator
Paste P59136

experiment: modifying spicerack netbox and gnmi modules to work with Juniper devices using Juniper specific yang models
ActivePublic

Authored by ayounsi on Apr 2 2024, 8:00 AM.
diff --git spicerack/__init__.py spicerack/__init__.py
index 3ff1d0e..e571cb9 100644
--- spicerack/__init__.py
+++ spicerack/__init__.py
@@ -755,13 +755,14 @@ class Spicerack: # pylint: disable=too-many-instance-attributes
"""
return AptGetHosts(remote_hosts)
- def gnmi(self, *, device_fqdn: str) -> GnmiBase:
+ def gnmi(self, *, device_fqdn: str, port: int = 8080) -> GnmiBase:
"""Get a gNMI instance to interact with a network device."""
config = load_yaml_config(self._spicerack_config_dir / "gnmi" / "config.yaml")
return GnmiBase(
device_fqdn=device_fqdn,
username=config["username"],
password=config["password"],
+ port=port,
dry_run=self._dry_run,
verbose=self._verbose,
)
diff --git spicerack/gnmi.py spicerack/gnmi.py
index 2d094ff..6e1ef23 100644
--- spicerack/gnmi.py
+++ spicerack/gnmi.py
@@ -17,7 +17,7 @@ class GnmiBase:
"""Class which wraps gNMI operations."""
def __init__(
- self, device_fqdn: str, username: str, password: str, dry_run: bool = False, verbose: bool = False
+ self, device_fqdn: str, username: str, password: str, port: int = 8080, dry_run: bool = False, verbose: bool = False
) -> None:
"""Create gNMI instance.
@@ -32,7 +32,7 @@ class GnmiBase:
self._dry_run = dry_run
self._verbose = verbose
self._device_fqdn = device_fqdn
- target = (device_fqdn, "8080")
+ target = (device_fqdn, port)
show_diff = "print" if verbose else ""
self._client = gNMIclient(
target=target, username=username, password=password, debug=verbose, show_diff=show_diff
@@ -87,8 +87,8 @@ class GnmiBase:
# a client desiring a set of changes to be applied together
# MUST ensure that they are encapsulated within a single SetRequest message.
change_pending = False
- for action in ("delete", "replace", "update"):
- if actions.get(action, None):
+ for operation in ("delete", "replace", "update"):
+ if actions.get(operation, None):
change_pending = True
if not change_pending:
logger.info("No changes to push to the device.")
diff --git spicerack/netbox.py spicerack/netbox.py
index 47eaa2b..4945a1e 100644
--- spicerack/netbox.py
+++ spicerack/netbox.py
@@ -383,9 +383,18 @@ class NetboxServer:
if self.virtual:
raise NetboxError("Server is a virtual machine, can't return a switch interface.")
primary_ip = self._server.primary_ip
- if not primary_ip:
- raise NetboxError("No primary IP, needed to find the primary interface.")
- netbox_iface = primary_ip.assigned_object
+ # First through the primary IP (eg. host is live)
+ # secondly thtough the connected interfaced (eg. )
+ if primary_ip:
+ netbox_iface = primary_ip.assigned_object
+ else:
+ # Workaround for Netbox REST API not having a __isnull filter (eg. cable__isnull=False)
+ netbox_iface = None
+ for iface in self._api.dcim.interfaces.filter(device=self._server.name, mgmt_only=False):
+ if iface.cable:
+ if netbox_iface:
+ raise NetboxError("More than 1 potential primary interface.")
+ netbox_iface = iface
if not netbox_iface:
raise NetboxError("Primary IP not assigned to an interface.")
netbox_switch_iface = netbox_iface.connected_endpoint
@@ -440,21 +449,32 @@ class NetboxServer:
"Updated Netbox switchport access vlan from %s to %s for device %s", current, value, self._server.name
)
- @property
- def fqdn(self) -> str:
- """Return the FQDN of the device.
+ def _fqdn_finder(self, device) -> str:
+ """Return the primary FQDN of a device.
Raises:
- spicerack.netbox.NetboxError: if the server has no FQDN defined in Netbox.
+ spicerack.netbox.NetboxError: if the device has no FQDN defined in Netbox.
"""
- # Until https://phabricator.wikimedia.org/T253173 is fixed we can't use the primary_ip attribute
+ # Workaround for a bug where address.dns_name returns
+ # AttributeError: type object 'IpAddresses' has no attribute 'dns_name'
+ device.full_details()
+ # Until https://phabricator.wikimedia.org/T253173 is fixed we can't use the primary_ip attribute
for attr_name in ("primary_ip4", "primary_ip6"):
- address = getattr(self._server, attr_name)
+ address = getattr(device, attr_name)
if address is not None and address.dns_name:
return address.dns_name
+ raise NetboxError(f"Device {device.name} does not have any primary IP with a DNS name set.")
- raise NetboxError(f"Server {self._server.name} does not have any primary IP with a DNS name set.")
+ @property
+ def fqdn(self) -> str:
+ """Return the FQDN of the device.
+
+ Raises:
+ spicerack.netbox.NetboxError: if the server has no FQDN defined in Netbox.
+
+ """
+ return self._fqdn_finder(self._server)
@property
def switch_fqdn(self) -> str:
@@ -464,15 +484,7 @@ class NetboxServer:
spicerack.netbox.NetboxError: if the switch has no FQDN defined in Netbox.
"""
- netbox_switch_iface = self._find_primary_switch_iface()
- netbox_switch = netbox_switch_iface.device
- # Until https://phabricator.wikimedia.org/T253173 is fixed we can't use the primary_ip attribute
- for attr_name in ("primary_ip4", "primary_ip6"):
- address = getattr(netbox_switch, attr_name)
- if address is not None and address.dns_name:
- return address.dns_name
-
- raise NetboxError(f"Switch {netbox_switch.name} does not have any primary IP with a DNS name set.")
+ return self._fqdn_finder(self._find_primary_switch_iface().device)
@property
def mgmt_fqdn(self) -> str:
@@ -511,23 +523,33 @@ class NetboxServer:
@property
def switch_interface_config(self) -> list:
- """Return the server's switch side config in an openconfig-interfaces format.
+ """Return the server's switch side config in an openconfig-interfaces or Juniper format.
Raises:
spicerack.netbox.NetboxError: for virtual servers or the server has no management FQDN defined in Netbox.
"""
nb_switch_interface = self._find_primary_switch_iface()
- interface_path = f"openconfig-interfaces:interfaces/interface[name={nb_switch_interface}]"
- interface_config_path = f"{interface_path}/config"
- vlan_config_path = f"{interface_path}/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
- # MTU: SONiC bug, `no mtu` sets it to 9100 in the API, to be adapted when/if we add Juniper support
- interface_config = {
- "enabled": nb_switch_interface.enabled,
- "mtu": nb_switch_interface.mtu if nb_switch_interface.mtu else 9100,
- "name": str(nb_switch_interface),
- "type": "iana-if-type:ethernetCsmacd", # Default, to prevent it showing up in diff
- }
+ nb_switch_interface.device.full_details()
+ vendor = nb_switch_interface.device.device_type.manufacturer.slug
+ if vendor == 'juniper':
+ interface_path = f"juniper:interfaces/interface[name={nb_switch_interface}]"
+ interface_config = {"name": str(nb_switch_interface)}
+ if not nb_switch_interface.enabled:
+ interface_config['disable'] = [None]
+ if nb_switch_interface.mtu:
+ interface_config['mtu'] = nb_switch_interface.mtu
+ else:
+ interface_path = f"openconfig-interfaces:interfaces/interface[name={nb_switch_interface}]"
+ interface_config_path = f"{interface_path}/config"
+ vlan_config_path = f"{interface_path}/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
+ # MTU: SONiC bug, `no mtu` sets it to 9100 in the API, to be adapted when/if we add Juniper support
+ interface_config = {
+ "enabled": nb_switch_interface.enabled,
+ "mtu": nb_switch_interface.mtu if nb_switch_interface.mtu else 9100,
+ "name": str(nb_switch_interface),
+ "type": "iana-if-type:ethernetCsmacd", # Default, to prevent it showing up in diff
+ }
vlan_config = {}
if not nb_switch_interface.enabled:
interface_config["description"] = "DISABLED"
@@ -539,17 +561,32 @@ class NetboxServer:
# VLAN
if not nb_switch_interface.mode:
raise NetboxError(f"Switch interface for server {self._server.name} with no vlan configured.")
- # Interface mode
- vlan_config["interface-mode"] = "ACCESS" if nb_switch_interface.mode.value == "access" else "TRUNK"
- # Native vlan
- if nb_switch_interface.untagged_vlan:
- vlan_config["access-vlan"] = nb_switch_interface.untagged_vlan.vid
- vlan_config["trunk-vlans"] = [tagged_vlan.vid for tagged_vlan in nb_switch_interface.tagged_vlans]
-
- return [
- (interface_config_path, {"openconfig-interfaces:config": interface_config}),
- (vlan_config_path, {"openconfig-vlan:config": vlan_config} if vlan_config else {}),
- ]
+ if vendor == 'juniper':
+ vlan_config = {'vlan': {'members': []}}
+ if nb_switch_interface.mode.value == "access":
+ vlan_config["interface-mode"] = "access"
+ vlan_config["vlan"]['members'] = [nb_switch_interface.untagged_vlan.name]
+ else:
+ vlan_config["interface-mode"] = "trunk"
+ vlan_config["vlan"]['members'].extend([tagged_vlan.name for tagged_vlan in nb_switch_interface.tagged_vlans])
+ if nb_switch_interface.untagged_vlan:
+ vlan_config["native-vlan-id"] = nb_switch_interface.untagged_vlan.vid
+ interface_config['unit'] = [{'name': 0, 'family': {'ethernet-switching': vlan_config}}]
+ else:
+ # Interface mode
+ vlan_config["interface-mode"] = "ACCESS" if nb_switch_interface.mode.value == "access" else "TRUNK"
+ # Native vlan
+ if nb_switch_interface.untagged_vlan:
+ vlan_config["access-vlan"] = nb_switch_interface.untagged_vlan.vid
+ vlan_config["trunk-vlans"] = [tagged_vlan.vid for tagged_vlan in nb_switch_interface.tagged_vlans]
+
+ if vendor == 'juniper':
+ return [(interface_path, interface_config)]
+ else:
+ return [
+ (interface_config_path, {"openconfig-interfaces:config": interface_config}),
+ (vlan_config_path, {"openconfig-vlan:config": vlan_config} if vlan_config else {}),
+ ]
def as_dict(self) -> dict:
"""Return a dict containing details about the server."""

Event Timeline

ayounsi changed the title of this paste from experiment: modifying spiecrack gnmi module to work with Juniper devices using Juniper specific yang models to experiment: modifying spicerack netbox and gnmi modules to work with Juniper devices using Juniper specific yang models.