avi Module¶
Inmanta adapter for the AVI / VMware NSX Advanced Load Balancer controller,
built on top of restbase.
The DSL model - the resource entities and their attributes - is generated from
the controller’s own API schema, so an entity carries every field AVI documents
for that object type. Only a thin layer is hand written: the session client, the
registration token plugin and the CRUD handler. Of the model, only
model/base.cf (the credentials and the base resource) is. Objects are read by name and
created, updated and deleted by uuid; an update overlays the fields the model
sets on the object currently on the controller, so fields nobody manages are left
alone.
Scope¶
Feature |
Entity / plugin |
|---|---|
Tenant |
|
Service engine group |
|
Virtual service VIP |
|
VRF context (route domain), BGP peers, static routes |
|
Service engine registration token |
|
Service engine (wait for / SE group / remove) |
|
Service engine data interface (VRF context) |
|
Service engine image (download) |
|
Configuration¶
The model reads no environment variables. Everything a resource needs comes
from the avi::base::Credentials instance it is given: the controller URL, the
credentials, the API version and the default tenant. The agent a resource is
deployed on is derived from the controller host, so no std::AgentConfig is
required. (AVI_URL is only used when regenerating the model, see below.)
Every resource is scoped to a tenant through tenant_scope (default admin). To
manage an object inside another tenant, set it and add a requires on the
avi::Tenant, which orders the two.
Example¶
import avi
import avi::base
import avi::service_engine
import avi::tenant::definitions
import avi::vrf_context::definitions
import avi::vs_vip::definitions
credentials = avi::base::Credentials(
host="https://172.25.223.252",
username="admin",
password="secret",
)
tenant = avi::Tenant(
credentials=credentials,
name="my-tenant",
local=true,
# Give the tenant its own routing domain, so it can hold VRF contexts.
config_settings=avi::tenant::definitions::TenantConfiguration(tenant_vrf=true),
)
seg = avi::ServiceEngineGroup(
credentials=credentials,
name="my-seg",
tenant_scope="my-tenant", # X-Avi-Tenant scope for this object
max_se=4,
min_scaleout_per_vs=2,
ha_mode="HA_MODE_SHARED_PAIR",
requires=[tenant], # the group is created after the tenant
)
# Nested objects (here the vip list and its address) are generated entities too.
avi::VsVip(
credentials=credentials,
name="my-vip",
tenant_scope="my-tenant",
vip=[avi::vs_vip::definitions::Vip(
vip_id="1",
enabled=true,
ip_address=avi::vs_vip::definitions::IpAddr(addr="42.42.42.42", type="V4"),
)],
requires=[tenant],
)
avi::VrfContext(
credentials=credentials,
name="my-vrf",
tenant_scope="my-tenant",
cloud="Default-Cloud",
bgp_profile=avi::vrf_context::definitions::BgpProfile(
ibgp=false, # eBGP
local_as=64979,
num_as_path_prepend=2,
community=["64979:19200", "64979:20"],
send_community=true,
# One peer per anchor leaf of the ACI floating l3out.
peers=[avi::vrf_context::definitions::BgpPeer(
peer_ip=avi::vrf_context::definitions::IpAddr(
addr="10.1.1.3", type="V4"),
subnet=avi::vrf_context::definitions::IpAddrPrefix(
ip_addr=avi::vrf_context::definitions::IpAddr(
addr="10.1.1.0", type="V4"),
mask=29,
),
remote_as=65001,
md5_secret="s3cr3t",
advertise_vip=true,
label="aci", # the routing options to apply to this peer
)],
# What the peers carrying that label learn and advertise. The four
# booleans form two pairs that can not be mixed (see below).
routing_options=[avi::vrf_context::definitions::BgpRoutingOptions(
label="aci",
learn_routes=true,
advertise_default_route=true,
max_learn_limit=100,
)],
),
requires=[tenant],
)
# The engine that registered itself with the token it booted with, moved into
# the service engine group of its service - the last step of the load balancer
# creation. Registration takes a while, so this waits for the engine to show up.
avi::service_engine::ServiceEngine(
credentials=credentials,
name="my-engine", # identifies the resource
engine_name="10.0.37.27", # the engine as the controller names it
tenant_scope="my-tenant",
cloud="Default-Cloud",
se_group_ref="/api/serviceenginegroup?name=my-seg",
requires=[tenant, seg], # the group it is moved into has to exist
)
# The data interface the cloud attached to a service engine, in that VRF. Only
# the mac address of the port identifies it; the engine and the interface both
# show up on the controller a while after the port was attached, so this waits.
avi::service_engine::Interface(
credentials=credentials,
engine="10.0.37.27", # the engine as the controller names it
tenant_scope="my-tenant",
cloud="Default-Cloud",
name="my-data-port", # identifies the interface on that engine
mac_address="fa:16:3e:cd:ad:e3",
vrf="my-vrf", # null puts it back in "my-tenant-default"
)
Things worth knowing per resource¶
avi::Tenanttakes the service engines registered in it along when it is purged. The controller refuses to delete a tenant anything still refers to (400 Cannot delete Tenant ... is referred by: <engine>), and an engine is not created through the API - it registers itself into the tenant whose token it was given - so nothing guarantees the model holds one for every engine that ended up there. Purge the objects the model does hold before the tenant all the same: only the engines are cleaned up here.avi::VrfContextis scoped to a cloud, not only to a tenant: every cloud has VRF contexts of its own and names only have to be unique within one, socloudis mandatory and part of the resource identity. The tenant needsconfig_settings.tenant_vrfset, otherwise the controller refuses the write. Purge a VRF after everything that references it (a VIP’svrf_context_ref, a service engine’s data vnics) and before its tenant. Itsbgp_profile.peers, itsbgp_profile.routing_optionsand itsstatic_routesare what a connectivity adds to a route domain: a peer is identified bypeer_ip/peer_ip6, a routing options object by itslabeland a route byroute_id, so all three can be added and removed one at a time without rewriting the rest. A peer’smd5_secretis written but never read back (the controller answers"<sensitive>"), so it is not compared: a secret that changes and nothing else is only written along with the next change to the VRF. A peer’slabelpicks the routing options that apply to it. Those four booleans form two pairs that can not be mixed:learn_routesgoes withadvertise_default_route, andlearn_only_default_routewithadvertise_learned_routes. The controller refuses any other combination with400 "<one> cannot be enabled with <other>", and it holdsmax_learn_limitto the 50-250 range.Known limitation: taking the last item of a managed list away does not clear it on the controller, and neither does dropping the
bgp_profileinstance: an empty relation is left out of the desired state altogether, so no change is detected, no request is sent and the resource reports success. Adding, changing and removing items while at least one remains is correct, as is creating a VRF with or without BGP; only the transition to nothing is not.avi::service_engine::ServiceEngineis not created through the API: the engine VM boots with a single use token and registers itself. The resource covers the two ends of that lifecycle, driven bypurged- wait for the engine to show up (polling, which gates everything in itsprovides), or remove it from the controller. Order it after its VM in both directions, and before the service engine group it sits in. In between, the attributes the model sets on the engine are written to it, andse_group_refis the one the load balancer creation needs: an engine registers into the default group of its cloud, and its own group is the last step. Point at that group the way a model points at any AVI object, by name (se_group_ref="/api/serviceenginegroup?name=my-seg"); the controller resolves it in the cloud the engine registered into and refuses a group that is not there, which is the scoping a group name needs - it is only unique within a cloud. Moving an engine between groups makes the controller deactivate and reactivate it, so it refuses to move an unlicensed engine (400 "Cannot perform deactivate/activate operation since SE is not licensed."). The engine is replaced as a whole on a write, so attributes nobody sets are left alone, and itsname, its interfaces (data_vnics/mgmt_vnic, which haveavi::service_engine::Interfaceresources of their own) and itsresourcesare never written.avi::service_engine::Interfaceis one data interface of an engine, found by itsmac_address, and the VRF context it belongs to. It is not created either - the cloud attaches a port to the engine’s VM and the controller discovers the interface - so the resource waits for it to show up and then configures it.vrfnames the VRF context, looked up in the cloud the engine registered into; leaving it null (or purging the resource) puts the interface back in the tenant’s default VRF<tenant_scope>-default, which is where the controller puts a newly discovered one. The interface is a member of its engine, so writing it replaces the whole engine object: interfaces of one engine are written one at a time, and everything else on it is left alone.avi::se_image::ServiceEngineImageis a file the controller serves, not an API object. It downloads the image (1.8 GB) onto the agent for whatever uploads it to the hypervisor, re-downloads it when the controller serves another one, and resumes an interrupted download.avi::create_token_referenceyields a registration token as aReference[str]resolved on the agent, to feed to an SE VM as itsAVICNTRL_AUTHTOKENmetadata. Tokens are single use and expire after an hour, so they are generated at deploy time rather than at compile time.
Regenerating the model from the controller¶
The entities under model/ and the cached schemas under files/schemas/ are
generated. To refresh them against a controller:
pip install -e .[generator]
AVI_URL=https://172.25.223.252 make generate
This fetches the swagger document of each managed object type from
<AVI_URL>/swagger/<ObjectType>.json (one document per type - the controller
exposes no combined schema), caches it under files/schemas/ and rewrites the
generated .cf files in place. Which object types those are is decided by the
@generated_avi_resource classes in inmanta_plugins/avi/resources.py.
The same run emits the entities no swagger describes - how an object is found on
the controller and how long it is waited for, the cloud a VRF context lives in,
and the service engine image, which is a file rather than an API object. Those
are declared with the same module factory in the last section of resources.py,
so model/base.cf is the only model file that is not generated.
Running tests¶
pip install -e . -c requirements.txt -r requirements.dev.txt
pytest -m "not integration" tests # offline
pytest -m integration tests # live, against a controller
There is one test file per resource, each with offline tests and one live
test_integration. The integration tests need the controller connection
parameters, through environment variables or pytest arguments (see
tests/conftest.py); CI provides them through .ci-integration-tests.yml.