Unlocking the Power of Network Automation—How Large Enterprises Can Stay Ahead

Blog Detail

In today’s digital transformation era, traditional manual network management processes fall short in managing the complexity and scale of modern networks.

Particularly for large enterprises operating expansive networks across multiple locations and serving vast user bases, automation presents a critical advantage.

Why? Because it offers a streamlined and efficient approach to network configuration, monitoring, and troubleshooting. Automation of repetitive tasks through intelligent algorithms not only enhances network reliability and operational efficiency but also frees up IT teams to concentrate on strategic initiatives, thus adding significant value to business operations.

The Value of Network Automation for Large Enterprises

Network automation is not just a luxury for large enterprises; it has become a necessity. With the ever-increasing demand for seamless connectivity to a growing number of users and devices, these organizations cannot afford unreliable networks, downtime, or delays in implementing changes. By automating network configuration and management, large enterprises can ensure faster provisioning, reduce human errors, and enhance overall performance.

Moreover, network automation enables large enterprises to adapt quickly to changing business needs. As new services and applications are introduced, network infrastructure must be agile enough to support them. Manual configuration processes can be time-consuming and error-prone, leading to delays in service deployment. With network automation, large enterprises can respond to business requirements promptly, gaining a competitive edge in the market.

Network automation is not just a buzzword; it is a growing trend in the industry. According to a recent Gartner study, a growing percentage of large enterprises now automate more than half of their network activities, reflecting a robust trend toward embracing automation technology.

One of the key drivers behind this trend is the increasing complexity of networks. As networks become more distributed and hybrid in nature, the inefficiency of manual management becomes a significant obstacle. Network automation provides a centralized and standardized approach to network configuration and monitoring, simplifying operations and improving overall network performance.

Understanding the Role of Network Source of Truth in Automation

In network automation, the concept of a network source of truth plays a crucial role. It refers to a centralized repository or database that holds accurate and up-to-date information about the network infrastructure. This source of truth serves as a single reference point for network engineers and automation tools, ensuring consistency and accuracy in network configurations.

By having a reliable source of truth, large enterprises can avoid configuration conflicts and minimize the risk of network outages. Network changes automatically synchronize with the source of truth, ensuring it remains the authoritative record of the network state. This not only simplifies troubleshooting but also enables efficient network auditing and compliance management.

Introducing Nautobot: A Powerful Network Automation Tool

Selecting the right tool is pivotal for network automation success. Among the leading solutions is Nautobot, an open source platform that has become a favorite among large enterprises for its robust capabilities in network management. It provides an excellent framework for managing network infrastructure, from device configuration to IP address management.

Nautobot offers a wide range of features that make network automation a breeze. It allows for the creation of standardized network templates, enabling consistent and error-free network configurations. With its intuitive user interface, network engineers can easily visualize and manage network devices, interfaces, and connections. Nautobot also integrates seamlessly with other automation tools and platforms, making it a versatile choice for large enterprises.

The Benefits of Using Nautobot for Large Enterprises

By using Nautobot for network automation, large organizations can more quickly provision and deploy network services. By automating the configuration process, network engineers can save valuable time and effort, allowing for rapid service delivery.

Nautobot also enhances network reliability and stability. With its centralized source of truth and standardized templates, it minimizes configuration errors and reduces the risk of network outages. This translates into improved user experience and increased customer satisfaction.

Additionally, Nautobot provides a scalable and flexible solution for large enterprises. As network infrastructure grows and evolves, Nautobot can easily adapt to accommodate new devices and technologies. Its modular architecture allows for seamless integration with existing systems, ensuring a smooth transition to network automation.

Calculating the ROI of Network Automation

Although implementing network automation demands substantial time, resources, and budget, the long-term ROI justifies the initial investment. To justify this investment, large enterprises need to calculate the return on investment (ROI) of network automation.

While the benefits of network automation are evident, quantifying the financial impact can help organizations make informed decisions.

By automating manual tasks, large enterprises can benefit in multiple ways:

  • Reduce labor costs and minimize human errors, resulting in significant cost savings
  • Focus on strategic initiatives, driving innovation and improving overall productivity
  • Provide faster response times, reduce network downtime, and deliver better quality services to end users
  • Increase customer satisfaction and loyalty, strengthening business outcomes

Best Practices for Implementing Network Automation in Large Enterprises

Implementing network automation in large enterprises requires careful planning and execution. Here are some best practices to consider:

  1. Start small, but think big. Begin by automating a few processes or tasks before scaling up. Starting with reporting or data sync tasks before impacting actual network operations is also a smart initial step. This allows time for learning and practice with the tools and concepts, creates a gradual transition, and minimizes disruption to existing operations.
  2. Involve stakeholders. Engage key stakeholders, including network engineers, IT teams, and business leaders, early in the process. Their input and support are crucial for successful implementation.
  3. Invest in training and upskilling. Network automation requires a new set of skills and knowledge. Provide training and resources to equip your IT teams with the necessary expertise to leverage automation tools effectively.
  4. Establish clear goals and metrics. Define clear objectives and key performance indicators for network automation. Regularly measure and track progress to ensure alignment with business goals.
  5. Leverage vendor support. Seek assistance from automation tool vendors or consultants who specialize in network automation. Their expertise can help streamline the implementation process and address any challenges.

Training and Resources for Network Automation

Various training and resources are available to empower large enterprises to adopt network automation.

Online courses, certifications, and workshops provide valuable insights into network automation best practices, tools, and implementation strategies. Additionally, vendor documentation, forums, and user communities offer a wealth of knowledge and support for those starting their network automation journey.

By investing in training and leveraging available resources, large enterprises can equip their IT teams with the necessary skills and knowledge to drive successful network automation initiatives.

Embracing the Future with Network Automation

Network automation is no longer a distant dream; it is the present and future of network management. Large enterprises cannot afford to overlook the benefits it brings in terms of efficiency, reliability, and scalability. By embracing network automation, these organizations can stay ahead of the competition, deliver better services to their customers, and achieve long-term success.

With tools such as Nautobot and a strategic approach to implementation, large enterprises can unlock the full potential of network automation. By investing in training and resources, organizations can empower their IT teams to become automation champions, driving innovation and transforming the way networks are managed.

Start harnessing the power of network automation today and unlock limitless possibilities for your large enterprise. Contact us to discover how our solutions can transform the way to build, manage, and operate your network.


Conclusion

Ready to take your network automation journey to the next level? Explore Nautobot and start streamlining your network operations today.

-Chris Murray


Tags :

ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Nautobot Cloud – Your Gateway to Network Automation

Blog Detail


Network to Code recently hosted a webinar on the future of network automation to demonstrate how you can elevate your network management experience with the leading SaaS network automation platform, Nautobot Cloud.

If your organization is ready to begin or improve your network automation journey with Nautobot, there are two deployment options to choose from: self-managed solutions (on-premises or within your established cloud assets) or cloud SaaS. Self-managed solutions refer to an approach where an organization’s IT infrastructure (whether physical or cloud-based) and resources are responsible for the deployment, operation, and maintenance of the enterprise software.

Embracing the future of network automation has never been easier since the introduction of Nautobot Cloud. Nautobot Cloud enables engineering teams to quickly operationalize a Network Source of Truth (NSoT) and network automation platform in a single open source SaaS platform.

Before we dive into the differences between a self-managed solution and Nautobot Cloud, it’s important to understand Nautobot and its core functionality.

What is Nautobot?

Nautobot is the most widely deployed open source Network SoT and network automation platform used by large enterprises today, offering a data-driven approach to network automation. It offers organizations superior flexibility, extensibility, and control while catering to any network design.

One of Nautobot’s greatest strengths is that data doesn’t have to reside within Nautobot; Nautobot offers a neutral model allowing the data to stay where it is most easily stored and accessed, but also ensuring all necessary data is identified and available. Nautobot enables bi-directional data flows so data can be enriched from each of the various sources, and combined as necessary to fulfill the data needs of the network automation tasks. With Nautobot as your NSoT, data about the network can be stored and exposed in a multitude of ways to support operations, including Rest APIs, JobHooks, GraphQL, Git Integration, and Webhooks. With Nautobot, even disparate data sources are unified, and compliance is more easily achieved, mitigating errors and security vulnerabilities.

Additionally, Nautobot maintains an intended network state that can be confidently documented, understood, and used as a baseline to compare current network behaviors against, empowering a more modern and data-driven NetDevOps approach to network configuration and management.

Exploring the Pros and Cons of Network Automation Deployment Options

There are several key pros and cons between a self-managed solution and a Cloud SaaS approach.

Deploying Nautobot through a self-managed approach is a more traditional method that many organizations are familiar with. However, these solutions come with their own challenges.

To start, a self-managed solution often requires major upfront costs for hardware (or cloud capacity), software, and setup, along with an ongoing need to pay for costly maintenance to maintain functionality and avoid obsolescence. Additionally this solution takes away time and effort that could otherwise be directed toward achieving business objectives, as team members must invest labor and hours towards learning to install, maintain, and use it.

On the other hand, Nautobot Cloud offers teams significant benefits when it comes to simplifying the adoption of a data-driven network automation platform.

First, while traditional self-managed solutions are notorious for lengthy installation times, Nautobot Cloud delivers an accelerated installation time. With Nautobot Cloud, installation is done in minutes, with a single click, saving your team countless hours and labor during the setup phase.

Nautobot Cloud is also an invaluable tool for optimizing your team. Thanks to the management of the infrastructure and Nautobot in the cloud, clients can access as many Nautobot instances as they need without investing time or training in any infrastructure development and maintenance.

Self-managed solutions also require multiple departments across the organization to function together, such as network automation teams, security teams, infrastructure teams, PMO teams, and network teams. With a cloud-based approach, a few dedicated team members handle NSoT population, configuration management, data access, automation jobs, and custom applications, leaving the rest of the organization to focus on their specialized roles and objectives.

Another major benefit of Nautobot Cloud is its comprehensive cloud native architecture features. With Nautobot Cloud, users gain access to:

  • Autoscaling of resources – easily scale and dynamically change your network maintenance, configuration, and security measures based on fluctuating traffic, data, or other needs
  • Secure multi-tenancy – segregate different lines of business, based on your specific requirements
  • Dedicated clusters – Nautobot Cloud uses a multi-cluster design, relieving worries about impact from parallel environment, unlike a traditional On-Prem solution
  • Highly Available – crucial for mission-critical applications, where system outages can have significant consequences.

By providing an easy-to-use interface to deploy scalable network automation, Nautobot Cloud enables teams to focus on boosting performance—not spending unnecessary time, effort, and budget on deployment complexities.

Nautobot Platform and Service Features

Now that we’ve discussed the benefits of choosing Cloud SaaS over an On-Prem solution for your network automation needs, let’s explore the key Nautobot features in a bit more detail.

Fast and easy installation and management

With Nautobot Cloud, cloud resources are created in minutes with a single click. Resource upgrades, cloning, and snapshots are also performed in one click. For organizations strapped on time and resources, Nautobot Cloud makes network automation and management faster, simpler, and more efficient.

Accelerated network automation

Nautobot is the key to accelerating your network automation adoption, thanks to its apps that solve common use cases. Each app is available in the marketplace and easily deployable via a 1-button integration model so you can quickly solve your most pressing challenges. Nautobot can also integrate with custom-made apps as needed.

Enterprise Grade Data Insights

Nautobot’s rich modern dashboard provides unique insights into your automation environment, including real-time dashboards that showcase data insights and trends in your network data and executed workflows.

Cloud Services Console

Another service feature of Nautobot Cloud is the ability for team members to access, use, and manage all cloud services through the web-based console. Cloud services include:

  • Create, clone, and upgrade Nautobot instances
  • Create and manage AWX deployments
  • Install Apps to Nautobot Instances
  • View dashboards

Full Automation Ecosystem

Nautobot Cloud is your full automation ecosystem, acting as a Network Source of Truth and your Network Automation platform. In addition to seamlessly working with Nautobot, Nautobot Cloud also enables efficient deployment and management of Ansible AWX. This means team members can truly install, manage, and maintain one of the most common network automation stacks, directly from Nautobot Cloud.

Learn More about Nautobot Cloud

Choosing between a self-managed solution and a cloud operational model is no longer a difficult choice with Nautobot Cloud. Nautobot Cloud is turbocharging the efficiency, productivity, and responsiveness of network teams across the globe.


Conclusion

Led by data-driven network automation, Nautobot Cloud is enabling engineering teams to quickly operationalize its Network Source of Truth and network automation platform in a single open source SaaS platform.

Ready to learn how Nabutobot Cloud can help streamline and improve your network automation journey?

Watch the full webinar here: https://www.youtube.com/watch?v=S8YhkV4oE1w

-Chris M.


Tags :

ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Nautobot and Django QuerySet Annotations – Part 3

Blog Detail

This is Part 3 of the series on Django QuerySet annotations. In Part 1 we talked about what the annotations are and why you would want to use them. In Part 2 we looked at subqueries and more annotation examples.

In this post, we’ll learn how to use annotations spanning multiple tables, and we will go through more advanced use cases.

In the first part of this series we looked at examples of using Count aggregator with annotations to count the number of related objects. We took advantage of reverse relationships1 created by Django and used value of the related_name argument for accessing related objects.

However, not all related objects have a reverse relationship. When a model is defined, it’s possible to explicitly disable the reverse relationship by using the value + (plus sign) for the related_name. Or perhaps our models are indirectly related, i.e., there is no ForeignKey pointing directly from one model to another.

In these cases we have to use subqueries2 in our annotations to process related objects. The resulting queries tend to be a bit more complex; but once you get the hang of it, it all starts to make sense.

Let’s go back to the example where we counted number of circuits for each provider. We’ll try to build an equivalent query that doesn’t use the explicit relationship but uses a subquery instead. This subquery will leverage the fact that the Circuit model has ForeignKey pointing to the Provider model.

This is the query we used previously:

from django.db.models import Count

providers = Provider.objects.annotate(circuit_count=Count("circuits"))

Let’s modify it. First, we build the skeleton of the outer query:

from django.db.models import Subquery, OuterRef

Provider.objects.annotate(circuit_count=Subquery(...))

We keep the same annotation field name but instead of using the Count aggregator we insert a result returned by the subquery.

We now need to focus on building that subquery:

circuits = Circuit.objects \
                  .filter(provider=OuterRef("pk")) \
                  .values("provider") \
                  .order_by() \
                  .annotate(c=Count("provider")) \
                  .values("c")

That’s a bit more complex than using Count aggregator, but sometimes this is the only available option.

Before we combine the subquery and the outer query to get our final result, let’s have a closer look at all of the components of the subquerry to make sure we understand what they do.

  • filter(provider=OuterRef("pk")) — We want only Circuit objects with provider equal to the provider in the outer query. Field pk in OuterRef3 refers to the primary key of the Provider model.
  • values("provider") — Using values clause before annotation will trigger grouping behavior of the annotation as discussed in Part 2 of this series. We will count number of circuits for given provider, so we specify provider field here.
  • order_by() — We clear the default ordering to ensure grouping operation returns correct results.
  • annotate(c=Count("provider")) — Now we apply our annotation with the Count aggregator. New field, named c, will hold number of circuits for each provider group. We applied filtering at the beginning, so there will be only one such group.
  • values("c") — Finally, we specify that only one column is returned, which is the column added by the annotation.

Our subquery must return only one row and one column so that it can be used in the subsequent annotation. This is why we had to use values("c"); otherwise two columns would be returned, and we would get an error.

In this particular subquery only one row will be returned because of the filtering we applied. That won’t always be the case, so we will talk later about how to explicitly force subquery to return one row only.

We built the subquery and talked through how it works. We now need to use it in the main query to get circuit count for each of the providers:

providers = Provider.objects \
                    .annotate(circuit_count=Subquery(circuits)) \
                    .values("name", "circuit_count")

That’s it. This should have done the job. Let’s investigate the resulting values:

>>> pprint(list(providers))
[{'circuit_count': None, 'name': 'AT&T'},
 {'circuit_count': None, 'name': 'Cogent'},
 {'circuit_count': None, 'name': 'Deutsche Telekom'},
 {'circuit_count': None, 'name': 'GTT'},
 {'circuit_count': None, 'name': 'Liberty Global'},
 {'circuit_count': None, 'name': 'Lumen Technologies'},
 {'circuit_count': 40, 'name': 'NTT'},
 {'circuit_count': None, 'name': 'Orange'},
 {'circuit_count': None, 'name': 'PCCW Global'},
 {'circuit_count': None, 'name': 'Sprint'},
 {'circuit_count': None, 'name': 'Tata Communications'},
 {'circuit_count': None, 'name': 'Telecom Italia'},
 {'circuit_count': 40, 'name': 'Telia Carrier'},
 {'circuit_count': None, 'name': 'Telxius'},
 {'circuit_count': None, 'name': 'Verizon'},
 {'circuit_count': None, 'name': 'Zayo'}]

Something is not quite right here. Why is None showing up for some of the providers? When we used Count with reverse relationships, the value 0 was shown for the providers that don’t have circuits.

We get None here because when the subquery doesn’t return any results for given provider, a value of None will be used when annotating the Provider objects.

Fortunately, Django provides a function that can help us fix this. We’ll look at it now.

Coalesce Function

We saw that when subquery does not return anything, the value assigned to the annotation field is None. This is generally not what we want. If subquery doesn’t return anything, we would like to define a default value that is assigned to a field instead.

It so happens that Django provides a function that does exactly that. It’s a function named Coalesce4. Imagine it working like a dictionary get() method in Python, with default return value provided.

Coalesce accepts a list of at least two field names or expressions and returns the first non-null value. This means we could use multiple subqueries and wrap them in the Coalesce function. The first non-empty result from the ones returned by subqueries will be used. The caveat here is that the arguments must be of similar type. You can’t mix numbers with text, for example.

So now we know we have to use Coalesce function to fix our query. Let’s go ahead and feed it the subquery we created and an integer 0, which will be our default value.

from django.db.models.functions import Coalesce

providers = Provider.objects \
                    .annotate(
                        circuit_count=Coalesce(Subquery(circuits), 0)
                    ) \
                    .values("name", "circuit_count")

Let’s investigate the resulting values:

>>> pprint(list(providers))
[{'circuit_count': 0, 'name': 'AT&T'},
 {'circuit_count': 0, 'name': 'Cogent'},
 {'circuit_count': 0, 'name': 'Deutsche Telekom'},
 {'circuit_count': 0, 'name': 'GTT'},
 {'circuit_count': 0, 'name': 'Liberty Global'},
 {'circuit_count': 0, 'name': 'Lumen Technologies'},
 {'circuit_count': 40, 'name': 'NTT'},
 {'circuit_count': 0, 'name': 'Orange'},
 {'circuit_count': 0, 'name': 'PCCW Global'},
 {'circuit_count': 0, 'name': 'Sprint'},
 {'circuit_count': 0, 'name': 'Tata Communications'},
 {'circuit_count': 0, 'name': 'Telecom Italia'},
 {'circuit_count': 40, 'name': 'Telia Carrier'},
 {'circuit_count': 0, 'name': 'Telxius'},
 {'circuit_count': 0, 'name': 'Verizon'},
 {'circuit_count': 0, 'name': 'Zayo'}]

Great! We fixed our problem, and now providers that don’t have any circuits have the value of the field circuit_count set to 0.

Limiting Subquery Results to One Column and One Row

As I mentioned in the previous example, a subquery will often return more than one row and column, which cannot be used in some annotations.

Consider the below example of annotating Location objects with the name of the most recently added device within each location:

recently_added = Device.objects \
                       .filter(location=OuterRef("pk")) \
                       .order_by("-last_updated") \
                       .values("name")

locations = Location.objects \
                    .annotate(newest_device=Subquery(recently_added))
>>> print(locations)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  ... (cut for brevity)
django.db.utils.ProgrammingError: more than one row returned by a subquery used as an expression

We defined subquery recently_added, which returns devices located in a given location, ordered by the date they were last updated.

When we try to use this subquery in an annotation, we get an error. Django helpfully tells us that subquery returned too many rows. This happens because there could be multiple devices located in any given location, and each row contains device record.

To fix this, we use one element slice [:1] on the QuerySet returned by the subquery. Subquery expects to receive a QuerySet that produces one row, which is why we are using a slice. Note that we cannot use [0] index or first() function here, as these return a single instance of an object, or a dictionary when used with values.

recently_added = Device.objects \
                       .filter(location=OuterRef("pk")) \
                       .order_by("-last_updated") \
                       .values("name")[:1]

locations = Location.objects \
                    .annotate(newest_device=Subquery(recently_added)) \
                    .values("name", "newest_device")
>>> pprint(list(locations))
[{'name': 'Jersey City', 'newest_device': 'jcy-spine-02.infra.ntc.com'},
 {'name': 'New York City', 'newest_device': 'nyc-spine-02.infra.ntc.com'},
 {'name': 'ams', 'newest_device': 'ams-leaf-08'},
 {'name': 'atl', 'newest_device': 'atl-leaf-08'},
 {'name': 'bkk', 'newest_device': 'bkk-leaf-08'},
 {'name': 'can', 'newest_device': 'can-leaf-08'},
 {'name': 'cdg', 'newest_device': 'cdg-leaf-08'},
 {'name': 'del', 'newest_device': 'del-leaf-10'},
 {'name': 'den', 'newest_device': 'den-leaf-08'},
 {'name': 'dfw', 'newest_device': 'dfw-leaf-08'},
 {'name': 'dxb', 'newest_device': 'dxb-leaf-08'},
 {'name': 'fra', 'newest_device': 'fra-leaf-08'},
 {'name': 'hkg', 'newest_device': 'hkg-leaf-08'},
 {'name': 'hnd', 'newest_device': 'hnd-leaf-08'},
 {'name': 'icn', 'newest_device': 'icn-leaf-04'},
 {'name': 'jfk', 'newest_device': 'jfk-leaf-08'},
 {'name': 'lax', 'newest_device': 'lax-leaf-10'},
 {'name': 'lhr', 'newest_device': 'lhr-leaf-08'},
 {'name': 'ord', 'newest_device': 'ord-leaf-08'},
 {'name': 'pek', 'newest_device': 'pek-leaf-08'},
 {'name': 'pvg', 'newest_device': 'pvg-leaf-04'},
 {'name': 'sin', 'newest_device': 'sin-leaf-08'}]

And there you have it — our annotation is now working as expected.

Annotations Across Many Tables

A word of warning when using annotations across more than two tables: Due to the way Django translates queries with annotations to underlying SQL, using aggregator functions like Count or Avg across multiple tables will give incorrect results.

For instance, if we try annotating Location records with the count of related prefixes and the count of related devices, we get incorrect numbers:

>>> locations = Location.objects.annotate(pfx_count=Count("prefixes"))
>>> locations.get(name="lax").pfx_count
5
>>> locations = Location.objects.annotate(device_count=Count("devices"))
>>> locations.get(name="lax").device_count
12
>>> locations = Location.objects.annotate(pfx_count=Count("prefixes"), device_count=Count("devices"))
>>> locations.get(name="lax").device_count
60
>>> locations.get(name="lax").pfx_count
60

As you can see above, when using a single annotation with Count, we get correct results. But when we try to annotate Location objects with related Prefix and Device objects in a single query, we get back an incorrect value for both fields5.

Note that the in the case of Count, we can address this problem by providing argument distinct=True when invoking Count. However, this will not work for other aggregation functions.

>>> locations = Location.objects \
                        .annotate(
                            pfx_count=Count("prefixes", distinct=True),
                            device_count=Count("devices", distinct=True)
                        )
>>> locations.get(name="lax").device_count
12
>>> locations.get(name="lax").pfx_count
5

We will see how we can solve this problem for the generic case by using our trusty subqueries. In the example below, each of the subqueries will be run independently behind the scenes, and the results of each will be assigned to the given field.

from django.db.models import Subquery, OuterRef

pfx_count_sq = Location.objects \
                       .filter(pk=OuterRef("pk")) \
                       .annotate(c=Count("prefixes")) \
                       .values("c")

device_count_sq = Location.objects \
                          .filter(pk=OuterRef("pk")) \
                          .annotate(c=Count("devices")) \
                          .values("c")

locations = Location.objects \
                    .annotate(
                        pfx_count=Subquery(pfx_count_sq), \
                        device_count=Subquery(device_count_sq) \
                    )
>>> locations.get(name="lax").pfx_count
5
>>> locations.get(name="lax").device_count
12

First, we defined two subqueries, pfx_count_sq and device_count_sq. These could be included directly inside of the annotate() method but defining them separately makes the code easier to read.

In each of the subqueries, we used OuterRef class, which allows us to use the pk field from the outer Location object when Subquery is run.

Each subquery uses annotation with a Count aggregator. We use values("c") here to return just one column c, otherwise we could not use the result of the subquery in the outer query annotation.

You also need to ensure only one row is returned. In our case, we are guaranteed to have only one match. If your subquery returns more than one row, you’d have to use slice [:1] to get back only one item.

Lastly, we wrap subqueries in SubQuery class and plug them into the annotate() method in the main query to get the final result.

As you can see, the resulting values now match the numbers we got when using two separate Count annotations.

Note that using Count and other Django aggregation functions is usually more performant than using subqueries because of the underlying database optimizations. However, subqueries are generally preferable to expressing the equivalent logic in Python code. In case of complex subqueries, you should profile underlying database queries to ensure their performance is acceptable.

More Examples

We’ll now have a look at some more examples of annotations, using different Django functions.

Using Length Function

Let’s compute the length of each of the device names and sort the results in descending order.

from django.db.models import F
from django.db.models.functions import Length

devices = Device.objects \
                .annotate(name_len=Length(F("name"))) \
                .order_by("-name_len")
>>> pprint(list(devices)[:20])
[<Device: jcy-spine-02.infra.ntc.com>,
 <Device: nyc-spine-01.infra.ntc.com>,
 <Device: jcy-spine-01.infra.ntc.com>,
 <Device: nyc-spine-02.infra.ntc.com>,
 <Device: nyc-leaf-02.infra.ntc.com>,
 <Device: nyc-leaf-01.infra.ntc.com>,
 <Device: jcy-rtr-01.infra.ntc.com>,
 <Device: nyc-rtr-01.infra.ntc.com>,
 <Device: jcy-rtr-02.infra.ntc.com>,
 <Device: nyc-rtr-02.infra.ntc.com>,
 <Device: jcy-bb-01.infra.ntc.com>,
 <Device: nyc-bb-01.infra.ntc.com>,
 <Device: den-leaf-02>,
 <Device: dfw-leaf-05>,
 <Device: dxb-leaf-02>,
 <Device: dxb-leaf-05>,
 <Device: fra-leaf-04>,
 <Device: fra-leaf-08>,
 <Device: hkg-edge-01>,
 <Device: hkg-leaf-06>]

We made use of Length6 function and F7 object. F object is used to pass value of the device’s name field to the Length function. Length function will return number of characters in the passed argument.

Filtering Values of the Annotation Field

In the below example, we use Count in the annotation. Then, we filter the values in the annotation field vlans_count to get back only locations that have at least one VLAN assigned to them.

locations = Location.objects \
                    .annotate(vlans_count=Count("vlans")) \
                    .filter(vlans_count__gt=1) \
                    .values("name", "vlans_count")
>>> pprint(list(locations))
[{'name': 'ams', 'vlans_count': 16},
 {'name': 'atl', 'vlans_count': 16},
 {'name': 'bkk', 'vlans_count': 16},
 {'name': 'can', 'vlans_count': 16},
 {'name': 'cdg', 'vlans_count': 16},
 {'name': 'del', 'vlans_count': 20},
 {'name': 'den', 'vlans_count': 16},
 {'name': 'dfw', 'vlans_count': 16},
 {'name': 'dxb', 'vlans_count': 16},
 {'name': 'fra', 'vlans_count': 16},
 {'name': 'hkg', 'vlans_count': 16},
 {'name': 'hnd', 'vlans_count': 16},
 {'name': 'icn', 'vlans_count': 8},
 {'name': 'jfk', 'vlans_count': 16},
 {'name': 'lax', 'vlans_count': 20},
 {'name': 'lhr', 'vlans_count': 16},
 {'name': 'ord', 'vlans_count': 16},
 {'name': 'pek', 'vlans_count': 16},
 {'name': 'pvg', 'vlans_count': 8},
 {'name': 'sin', 'vlans_count': 16}]

Using Exists Subquery

We can use Exists8 subquery subclass to get back a boolean True if subquery returns results or False if it doesn’t. For instance, we might want to check whether a given location has devices that are assigned edge role.

from django.db.models import Exists, OuterRef

edge_dev_sq = Subquery(
                Device.objects \
                      .filter(
                          location=OuterRef("pk"),
                          device_role__slug="edge"
                      )
              )

locs_w_edge_dev = Location.objects \
                          .annotate(has_edge_devices=Exists(edge_dev_sq)) \
                          .values("name", "has_edge_devices")
>>> pprint(list(locs_w_edge_dev))
[{'has_edge_devices': False, 'name': 'Jersey City'},
 {'has_edge_devices': False, 'name': 'New York City'},
 {'has_edge_devices': True, 'name': 'ams'},
 {'has_edge_devices': True, 'name': 'atl'},
 {'has_edge_devices': True, 'name': 'bkk'},
 {'has_edge_devices': True, 'name': 'can'},
 {'has_edge_devices': True, 'name': 'cdg'},
 {'has_edge_devices': True, 'name': 'del'},
 {'has_edge_devices': True, 'name': 'den'},
 {'has_edge_devices': True, 'name': 'dfw'},
 {'has_edge_devices': True, 'name': 'dxb'},
 {'has_edge_devices': True, 'name': 'fra'},
 {'has_edge_devices': True, 'name': 'hkg'},
 {'has_edge_devices': True, 'name': 'hnd'},
 {'has_edge_devices': True, 'name': 'icn'},
 {'has_edge_devices': True, 'name': 'jfk'},
 {'has_edge_devices': True, 'name': 'lax'},
 {'has_edge_devices': True, 'name': 'lhr'},
 {'has_edge_devices': True, 'name': 'ord'},
 {'has_edge_devices': True, 'name': 'pek'},
 {'has_edge_devices': True, 'name': 'pvg'},
 {'has_edge_devices': True, 'name': 'sin'}]

Using Python Expressions in Annotations

You can also use Python expressions in your annotations. We’ve seen an example of that in Part 2, where we computed percentage of free interfaces on devices. Here is an example that shows how to annotate Device objects with the time elapsed since the object was created.

import datetime

today = datetime.date.today()

devices = Device.objects \
                .annotate(
                    created_delta=today - F("created")
                )
>>> devices[0].created_delta
datetime.timedelta(days=47)
>>> devices[0].created_delta.days
47

We use datetime.datetime.date.today() to get today’s date. Then, we use this value in the annotation to subtract the date the object was created from today’s date. The result is a datetime.timedelta object, which we can query for the number of days since the device was created.

Count distinct Argument

In our final example we will use Count’s distinct argument to compute number of distinct device roles used by devices in each location.

When we use distinct=True, we’re telling Count to compute number of unique values for the given argument. Here, we’re giving it devices__device_role, which will access device role for the devices in each of the locations.

locations = Location.objects \
                    .annotate(
                         devrole_count=Count(
                            "devices__device_role", distinct=True
                         )
                    ) \
                    .values("name", "devrole_count")
>>> pprint(list(locations))
[{'devrole_count': 3, 'name': 'Jersey City'},
 {'devrole_count': 4, 'name': 'New York City'},
 {'devrole_count': 2, 'name': 'ams'},
 {'devrole_count': 2, 'name': 'atl'},
 {'devrole_count': 2, 'name': 'bkk'},
 {'devrole_count': 2, 'name': 'can'},
 {'devrole_count': 2, 'name': 'cdg'},
 {'devrole_count': 2, 'name': 'del'},
 {'devrole_count': 2, 'name': 'den'},
 {'devrole_count': 2, 'name': 'dfw'},
 {'devrole_count': 2, 'name': 'dxb'},
 {'devrole_count': 2, 'name': 'fra'},
 {'devrole_count': 2, 'name': 'hkg'},
 {'devrole_count': 2, 'name': 'hnd'},
 {'devrole_count': 2, 'name': 'icn'},
 {'devrole_count': 2, 'name': 'jfk'},
 {'devrole_count': 2, 'name': 'lax'},
 {'devrole_count': 2, 'name': 'lhr'},
 {'devrole_count': 2, 'name': 'ord'},
 {'devrole_count': 2, 'name': 'pek'},
 {'devrole_count': 2, 'name': 'pvg'},
 {'devrole_count': 2, 'name': 'sin'}]

References

  1. Django docs – Related objects reference 
  2. Django docs – Query Expressions – SubQuery 
  3. Django docs – Query Expressions – OuterRef 
  4. Django docs – Database Functions – Coalesce 
  5. Django docs – Aggregation – Combining multiple aggregations 
  6. Django docs – Database Functions – Length 
  7. Django docs – Query Expressions – F 
  8. Django docs – Query Expressions – Exists 

Conclusion

This post concludes the series on Nautobot and Django QuerySet annotations. We learned about annotations and subqueries and their use cases. We explored different functions that can be used with annotations and worked through a number of practical examples which showcase their power.

With this acquired knowledge, you should be able to write more powerful and efficient queries.

I hope you enjoyed this series and are looking forward to writing some awesome Nautobot code!

-Przemek Rogala



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!