Skip to content

Commit

Permalink
Cleaned up
Browse files Browse the repository at this point in the history
  • Loading branch information
prydin committed Sep 19, 2023
1 parent 3bb5a16 commit fe5aa00
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 221 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# aria-auto-nwselect
# IPAM-Aware Network Selection for Aria Automation

## Motivation
This plugin allows for more optimized network selection when using the Infoblox IPAM. It uses the
_Network Configure_ event hook modify the list of selected networks, such that the network with
the most available IP addresses is always picked.

## Building from source

### Prerequisites
* Python 3.9+
* Pip 23.2+
* Maven 3.8+

### Steps
```commandline
cd <top of project directory>
mvn clean package
```

The ZIP package can be found in the `target` directory.

## Installation
1. In Aria Automation Assembler, go to Extensibility->Actions and click "Import action"
2. Add a subscription to "Network Configure" and attach the action "Network Select".
Make sure the "Block Execution" checkbox is checked.
3. Add the following inputs to the action:
1. ipamHost - IP address or hostname of the Infoblox host
2. ipamUser - Name of Infoblox service account user
3. ipamPassword - Encrypted action constant with Infoblox password.
16 changes: 2 additions & 14 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<version>1.1.1</version>
<executions>
<execution>
<id>collect dependencies</id>
<id>collect-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>exec</goal>
Expand All @@ -65,6 +65,7 @@
<executable>pip3</executable>
<arguments>
<argument>install</argument>
<argument>--upgrade</argument>
<argument>-r</argument>
<argument>requirements.txt</argument>
<argument>--target=target/python/nwselect</argument>
Expand All @@ -89,19 +90,6 @@
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
<execution>
<phase>prepare-package</phase>
<id>cleanup</id>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptor>src/main/python/cleanup/assembly.xml</descriptor>
<finalName>cleanup</finalName>
<outputDirectory>${project.build.directory}/actions</outputDirectory>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
<execution>
<phase>package</phase>
<id>main-package</id>
Expand Down
4 changes: 1 addition & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
requests~=2.26.0
PyYAML~=6.0
certifi~=2021.10.8
idna~=3.3
PyYAML~=5.0
urllib3~=1.26.7
10 changes: 0 additions & 10 deletions src/main/abx/cleanup.abx

This file was deleted.

2 changes: 1 addition & 1 deletion src/main/abx/nwselect.abx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
exportVersion: "1"
exportId: "1232c505-5468-4952-a4fe-e375ac46c871"
name: "Select Network (All)"
name: "Select Network"
runtime: "python"
entrypoint: "main.handler"
timeoutSeconds: 600
Expand Down
15 changes: 0 additions & 15 deletions src/main/python/cleanup/assembly.xml

This file was deleted.

34 changes: 0 additions & 34 deletions src/main/python/cleanup/main.py

This file was deleted.

73 changes: 34 additions & 39 deletions src/main/python/nwselect/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,48 @@
import requests
import requests.auth
import re

import yaml

MAX_USAGE = 100
DEBUG = False


def dump(data):
print(json.dumps(data, indent=4))


def get(context, url):
print("GET", url, end="")
if DEBUG:
print("GET", url, end="")
t = time.time()
r = context.request(url, "GET", "")
if r["status"] < 200 or r["status"] > 299:
raise Exception('HTTP error %d: %s' % (r["status"], r["content"]))
print(" (%fs)" % (time.time() - t))
if DEBUG:
print(" (%fs)" % (time.time() - t))
return json.loads(r["content"])


def patch(context, url, data):
print("PATCH", url, end="")
if DEBUG:
print("PATCH", url, end="")
t = time.time()
r = context.request(url, "PATCH", json.dumps(data))
if r["status"] < 200 or r["status"] > 299:
raise Exception('HTTP error %d: %s' % (r["status"], r["content"]))
print(" (%fs)" % (time.time() - t))
if DEBUG:
print(" (%fs)" % (time.time() - t))
return json.loads(r["content"])


def post(context, url, data):
print("POST", url, end="")
if DEBUG:
print("POST", url, end="")
t = time.time()
r = context.request(url, "POST", json.dumps(data))
if r["status"] < 200 or r["status"] > 299:
raise Exception('HTTP error %d: %s' % (r["status"], r["content"]))
print(" (%fs)" % (time.time() - t))
if DEBUG:
print(" (%fs)" % (time.time() - t))
return json.loads(r["content"])


Expand All @@ -56,9 +61,11 @@ def mkfilter(query):
return "$filter=(" + quote(query, safe="*") + ")"


# Cache of IP counts. Used only during a single invocation
cache = {}


# Returns the number of free IP addresses based on IPAM data
def get_available_ips(context, inputs, subnet):
n = cache.get(subnet, None)
if n:
Expand All @@ -77,22 +84,29 @@ def get_available_ips(context, inputs, subnet):
total = end_ip - start_ip
range_link = ip_range['documentSelfLink']
free_ips = -1

# Use external IP if configured
if "ipamUser" and "ipamHost" and "ipamPassword" in inputs:
ipam_url = "https://" + inputs["ipamHost"] + "/wapi/v2.10.5/" + quote(ip_range["id"]) + \
"?_return_fields%2b=utilization"
ipam_auth = requests.auth.HTTPBasicAuth(username=inputs["ipamUser"],
password=context.getSecret(inputs["ipamPassword"]))
print("GET", ipam_url, end="")
t = time.time()
if DEBUG:
print("GET", ipam_url, end="")
t = time.time()
result = requests.get(ipam_url, auth=ipam_auth, verify=False)
print(" (%fs)" % (time.time() - t))
if DEBUG:
print(" (%fs)" % (time.time() - t))
if result.status_code != 200:
# Handle this gracefully. We'll use cached data instead
print("HTTP error %d: %s" % (result.status_code, result.content))
else:
ipam_record = result.json()
print("Determined free IPs using live external IPAM data")
free_ips = (1.0 - float(ipam_record["utilization"]) / 1000) * total

# If we still don't have a value for free_ips, we either didn't have an external IPAM configured,
# or the call to the IPAM failed. Fall back to the internal number in vRA. This could be unreliable.
if free_ips == -1:
if "customProperties" in ip_range and "freeIps" in ip_range["customProperties"]:
# If we got a snapshot free IP count back from an external IPAM we use it,
Expand All @@ -111,7 +125,7 @@ def get_available_ips(context, inputs, subnet):
total_free_ips += free_ips
print("Range %s (subnet %s) has %d total and %d free" % (ip_range['name'], subnet, total, free_ips))
cache[subnet] = total_free_ips
print("Total free IPs for subnet %s is %d" % (ip_range["name"], total_free_ips))
print("*** Total free IPs for subnet %s is %d" % (ip_range["name"], total_free_ips))
return total_free_ips


Expand Down Expand Up @@ -183,33 +197,14 @@ def handler(context, inputs):
vm = selections[vm_idx]
for nic_idx in range(len(vm)):
nic = vm[nic_idx]
# Have we already picked a network for this?
nw_key = dep_id + "|" + component_id + "|" + str(nic_idx)
selected_nw = get(context, "/iaas/api/fabric-networks?" +
mkfilter("expandedTags.item.tag eq '__fitsResource*%s'" % nw_key))

# We've already assigned a best network for this resource. Use it!
if len(selected_nw["content"]) != 0:
best = selected_nw["content"][0]["id"]
print("Selected network based on previous selection")
else:
print("Selecting network based on IP address range")
print("Possible networks:" + json.dumps(nic))
best = reduce(lambda a, b:
a if get_available_ips(context, inputs, a) > get_available_ips(context, inputs, b) else b,
nic)

# Tag the network as suitable for this VM, but only if network was explicitly identified
if has_networks(bp, component_id):
payload = {
"resourceLink": "/resources/sub-networks/" + best,
"tagsToAssign": [{"key": "__fitsResource", "value": nw_key}],
"tagsToUnassign": []
}
post(context, "/provisioning/uerp/provisioning/mgmt/tag-assignment", payload)

# We need to maintain the same number of networks in the selection, so we reshuffle
# 'subnets' to have the best network as its first element.
print("Possible networks:" + json.dumps(nic))

# Determine the network with the most available addresses
best = reduce(lambda a, b:
a if get_available_ips(context, inputs, a) > get_available_ips(context, inputs, b) else b,
nic)

# Create a new list with a single network, i.e. the one with the most IP addresses
nic[:] = [best]
free = get_available_ips(context, inputs, nic[0])
print("Best subnet is %s with %d free IPs" % (nic[0], free))
Expand Down
84 changes: 0 additions & 84 deletions src/main/python/nwselect/original.py

This file was deleted.

Loading

0 comments on commit fe5aa00

Please sign in to comment.