nautobot-app-ssot
nautobot-app-ssot copied to clipboard
role__name returns None using contrib
Environment
- Python version: 3.9.18
- Nautobot version: 2.1.4
- nautobot-ssot version: 2.2.0
Related Slack: https://networktocode.slack.com/archives/C01NWPK6WHL/p1708560079971619
Expected Behavior
Utilize role__name in my model that lets me add a role to my IPAddresses
Observed Behavior
The role would always return None using a created role or a default role like "Loopback"
Steps to Reproduce
- Add role__name to my attributes/data-types of my model using contrib
- Using the model with self.ipaddress (from nautobot.ipam.models import IPAddress) in this case. Added role__name="Loopback" for example.
- Run my SSOT job and I get back my dict but role is always
None
Thanks for submitting! Can you provide a full model and adapter to reference?
I believe this highlights what you are looking for (I didn't 1:1 copy and paste):
class IPAddressModel(NautobotModel):
# Metadata about this model
_model = IPAddress
_modelname = "ipaddress"
_identifiers = ("host",)
_attributes = (
"mask_length",
"status__name",
"role__name",
"dns_name",
"description",
"parent__network",
)
# Data type declarations for all identifiers and attributes
host: str
mask_length: int
status__name: str
role__name: str
dns_name: str
description: Optional[str]
parent__network: str
class NBAdapter(NautobotAdapter):
ipaddress = IPAddressModel
top_level = ["ipaddress"]
class Adapter(DiffSync):
ipaddress = IPAddressModel
top_level = ["ipaddress"]
def __init__(self, job: object, sync: object, client: object, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job = job
self.client = client
self.sync = sync
def load(self):
ip_data = [{
"host": "10.1.1.2",
"mask_length": "32",
"status__name": "Reserved",
"dns_name": "john.com",
"description": "Jon Doe",
"parent__network": "10.1.1.0",
"role__name": "Loopback", # What it doesn't like
}]
for data in ip_data:
ip_addr = self.ipaddress(
host=data.get("host"),
mask_length=data.get("mask_length"),
status__name=data.get("status__name"),
dns_name=data.get("dns_name"),
description=data.get("description"),
parent__network=data.get("parent__network"),
role__name=data.get("role__name"), # What it doesn't like
)
self.add(ip_addr)
If you can give me something I can run as is then I can reproduce the issue much quicker, i.e. flesh out
# GETS IP DATA AND ASSIGNS TO VARIABLES
with a list of dictionaries or something like this, and then include the actual operation that shows the faulty behaviour.
Thanks!
@Kircheneer I updated my comment from before. Let me know if this is sufficient.
This looks good, thanks.
@rifen
I've been looking into this and I was able to recreate the described issue. It occurs when an IP Address exists in Nautobot without a role and the SSoT contrib adapter tries to load it. It returns a NoneType object which is not allowed by the DiffSync model you provided so it raises an error.
I also found a config-based fix by wrapping the role__name typing declaration in an Optional[] tag or adding |None to the allowed types (see below). This resolved the issue entirely. If you require the role__name in your source adapter, you can always add a check for it in there.
Since None is a valid value for ipaddress.role, I'm inclined to think this is not a bug and the role__name typing config should reflect that scenario.
@Kircheneer would you still consider this a bug requiring a fix or would one of the below methods be a preferred approach?
class IPAddressModel(NautobotModel):
# Metadata about this model
_model = IPAddress
_modelname = "ipaddress"
_identifiers = ("host",)
_attributes = (
"mask_length",
"status__name",
"role__name",
"dns_name",
"description",
"parent__network",
)
# Data type declarations for all identifiers and attributes
host: str
mask_length: int
status__name: str
role__name: Optional[str]
dns_name: str
description: Optional[str]
parent__network: str
Or you can use:
class IPAddressModel(NautobotModel):
# Metadata about this model
_model = IPAddress
_modelname = "ipaddress"
_identifiers = ("host",)
_attributes = (
"mask_length",
"status__name",
"role__name",
"dns_name",
"description",
"parent__network",
)
# Data type declarations for all identifiers and attributes
host: str
mask_length: int
status__name: str
role__name: str|None
dns_name: str
description: Optional[str]
parent__network: str