aci adapter

Features

The module allows to configure resources on aci. It can be used in two fashions:

  1. Using the generated resources: clearly typed resources, but only show a subset of aci capabilities –> aci::RelativeResource sub entities.

  2. Using generic resources: untyped resources, the structure of the resource is the responsibility of the module user –> aci::StandaloneResource.

Api configuration

Aci api is protected by authentication, the adapter needs to receive some authentication info in order to function properly. Mainly the adapter needs to know:

  1. Where the api is (what is the url?) –> aci::Api, base_url attribute

  2. Who should be managing resource on the api (username) –> aci::Api, username, or username_env_var attribute

  3. How can the user authenticate to the api (password) –> aci::Api, password, or password_env_var attribute

:bulb: For username and password, either the raw value or the environment variable should provided. If the environment variable is used, the value the attribute should receive is the name of the environment variable that the agent has access to, and in which the expected value (username or password value) can be found.

Usage example

Here are two equivalent models illustrating the usage of the module, using the generated resources and the generic ones.

  1. Generated resources:

import aci
import aci::pol
import aci::fv

api = aci::Api(
    name="api",
    base_url="https://192.168.26.200",
    username_env_var="ACI_API_USERNAME",
    password_env_var="ACI_API_PASSWORD",
    verify=false,
    retry_max_attempts=5,
    retry_interval=3,
)

tenant = aci::fv::Tenant(
    parent=aci::pol::Uni(parent=aci::Root(api=api, managed=false), managed=false),
    name="test",
    managed=false,
)

vrf = aci::fv::Ctx(
    parent=tenant,
    name="test",
    purged=false,
)

app = aci::fv::Ap(
    parent=tenant,
    name="test",
    purged=false,
)

bd = aci::fv::BD(
    parent=tenant,
    name="test",
    purged=false,
)

rs_ctx = aci::fv::RsCtx(
    parent=bd,
    tnFvCtxName=vrf.name,
    requires=[bd, vrf],
)

epg = aci::fv::AEPg(
    parent=app,
    name="test",
    descr="10",
    fvRsBd=aci::fv::RsBd(
        tnFvBDName=bd.name,
        requires=[epg, bd],
    ),
    fvRsPathAtt=aci::fv::RsPathAtt(
        tDn="topology/pod-1/paths-101/pathep-[eth1/5]",
        encap="vlan-11",
        requires=epg,
    ),
    requires=app,
)
  1. Generic resources

import aci

api = aci::Api(
    name="api",
    base_url="https://192.168.26.200",
    username_env_var="ACI_API_USERNAME",
    password_env_var="ACI_API_PASSWORD",
    verify=false,
    retry_max_attempts=5,
    retry_interval=3,
)

tenant = aci::StandaloneResource(
    api=api,
    mo_class_name="fvTenant",
    rn_format="tn-%(name)s",
    parent_dn="uni",
    attributes={
        "name": "test",
    },
    managed=false,
)

vrf = aci::StandaloneResource(
    api=api,
    mo_class_name="fvCtx",
    rn_format="ctx-%(name)s",
    parent_dn=tenant.dn,
    attributes={
        "name": "test",
    },
    requires=tenant,
    purged=false,
)

app = aci::StandaloneResource(
    api=api,
    mo_class_name="fvAp",
    rn_format="ap-%(name)s",
    parent_dn=tenant.dn,
    attributes={
        "name": "test",
    },
    requires=tenant,
    purged=false,
)

bd = aci::StandaloneResource(
    api=api,
    mo_class_name="fvBD",
    rn_format="BD-%(name)s",
    parent_dn=tenant.dn,
    attributes={
        "name": "test",
    },
    requires=tenant,
    purged=false,
)

rs_ctx = aci::StandaloneResource(
    api=api,
    mo_class_name="fvRsCtx",
    rn_format="rsctx",
    parent_dn=bd.dn,
    attributes={
        "tnFvCtxName": vrf.attributes["name"],
    },
    requires=[bd, vrf],
)

epg = aci::StandaloneResource(
    api=api,
    parent_dn=app.dn,
    rn_format="epg-%(name)s",
    mo_class_name="fvAEPg",
    attributes={
        "name": "test",
        "descr": "10",
    },
    requires=app,
)

rs_bd = aci::StandaloneResource(
    api=api,
    parent_dn=epg.dn,
    rn_format="rsbd",
    mo_class_name="fvRsBd",
    attributes={
        "tnFvBDName": bd.attributes["name"],
    },
    requires=[epg, bd],
)

static_port = aci::StandaloneResource(
    api=api,
    parent_dn=epg.dn,
    rn_format="rspathAtt-[%(tDn)s]",
    mo_class_name="fvRsPathAtt",
    attributes={
        "tDn": "topology/pod-1/paths-101/pathep-[eth1/5]",
        "encap": "vlan-11",
    },
    requires=[epg],
)

Referencing objects we don’t manage

A resource is always identified relatively to its parent, so the whole chain up to the root has to be part of the model. Those ancestors usually already exist on the api and we don’t want to configure them, only to point at them. For that, every generated resource comes with a matching reference entity, named after it with a Reference suffix (aci::fv::TenantReference for aci::fv::Tenant). A reference only carries what identifies the object – its naming attributes and the relation to its own parent – and is never exported as an orchestrator resource.

References and resources are interchangeable: every relation of the generated model (to a parent, to a child, …) accepts either of them, so a reference can be used anywhere in the configuration tree.

import aci
import aci::pol
import aci::fv

api = aci::Api(
    name="api",
    base_url="https://192.168.26.200",
    username_env_var="ACI_API_USERNAME",
    password_env_var="ACI_API_PASSWORD",
    verify=false,
)

# The root, the policy universe and the tenant already exist: we only point at them.
tenant = aci::fv::TenantReference(
    parent=aci::pol::UniReference(parent=aci::RootReference(api=api)),
    name="test",
)

# Only the objects below the tenant are managed.
vrf = aci::fv::Ctx(parent=tenant, name="test")
app = aci::fv::Ap(parent=tenant, name="test")

:bulb: Prefer a reference over an unmanaged resource (managed=false) for the objects you only want to point at. An unmanaged resource is still exported, so two parts of a model that share an ancestor also share the resource representing it – which is not possible when they are deployed in different resource sets. A reference exports nothing, so each of them can declare the shared ancestor on its own.

:bulb: A reference can co-exist with the resource managing the same object: the index is defined on the resource and on the reference separately, not on the base entity they share. One part of the model can therefore manage an object while another one only points at it.

Aggregating resources

When a model defines many aci resources, exporting and deploying each of them as an individual orchestrator resource can be expensive. The aci::AggregateResource lets you group many resources into a single orchestrator resource.

Every object whose aggregate_resource relation points to an aci::AggregateResource puts all of its children (and their children, recursively) in that aggregate: they are managed by it instead of being deployed on their own, so attaching a single common ancestor is enough to aggregate a whole configuration tree.

Resources that belong to the same configuration tree are both read and pushed to the api in a single request each: the aggregate handler reads the whole tree with one subtree query (query-target=subtree) and deploys it (creations, modifications and deletions at once) with one POST of the corresponding managed-object subtree. Attaching a common ancestor (e.g. a aci::fv::Tenant, even an unmanaged one used only as a container) is therefore enough to read and deploy its whole subtree in a single request each.

import aci
import aci::pol
import aci::fv

api = aci::Api(
    name="api",
    base_url="https://192.168.26.200",
    username_env_var="ACI_API_USERNAME",
    password_env_var="ACI_API_PASSWORD",
    verify=false,
)

aggregate = aci::AggregateResource(api=api, name="my-tenant")

# Attaching the tenant is enough: vrf, app, bd, epg, ... are pulled in automatically and
# the whole tree is deployed in a single api request.
tenant = aci::fv::Tenant(
    parent=aci::pol::Uni(parent=aci::Root(api=api, managed=false), managed=false),
    name="test",
    managed=false,
    aggregate_resource=aggregate,
)

vrf = aci::fv::Ctx(parent=tenant, name="test")
app = aci::fv::Ap(parent=tenant, name="test")
bd = aci::fv::BD(parent=tenant, name="test")
rs_ctx = aci::fv::RsCtx(parent=bd, tnFvCtxName=vrf.name, requires=[bd, vrf])
epg = aci::fv::AEPg(
    parent=app,
    name="test",
    descr="10",
    fvRsBd=aci::fv::RsBd(tnFvBDName=bd.name, requires=[epg, bd]),
    requires=app,
)

:bulb: Dependencies (requires/provides) between a resource outside of the aggregate and any aggregated resource — including auto-discovered children — are preserved: they are reflected onto the aggregate resource itself. Dependencies between two resources of the same aggregate need no ordering: the whole tree is configured in a single atomic api request.

:bulb: report_only is honored per resource: if a resource that needs a change is in report_only mode, the aggregate reports it as non-compliant instead of applying it.

:bulb: An aggregate only contains resources. A reference can be attached to one – the subtree below it is then pulled in as usual – but the reference itself is not part of the aggregate and can not be the root of a deployed tree. Use an unmanaged resource (managed=false) as the common ancestor when you want the whole subtree in a single request.

Checking for faults

Aci accepts any well-formed configuration and reports what it can not apply asynchronously, as faults on the objects. aci::FaultCheck fails when any of the objects it checks carries an active fault. It runs once the checked resources are deployed and keeps polling for faults until settle_delay seconds have passed since the last change of a checked object.

check = aci::FaultCheck(
    api=api,
    name="my-tenant",
    aggregates=[aggregate],
    min_severity="warning",
    ignored_codes=[],
)

Automatic conversion

To automatically convert an ACI json file into a model

  1. install acicobra and acimodel python modules (can be obtained from an ACI instance on https://[apic_ip]/cobra/_downloads)

  2. run aci_instance_generator some_file.json