Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Add Feature Templates support. (#95)
Browse files Browse the repository at this point in the history
* Add basic aaa templates

* Add basic script

* create radius j2

* radius tacacs scripts for aaa feature template

* Fix User dataclass

* Move configuration jinja files

* created vpn

* created vpn

* Add TenantAPI.

* working template

* Add TenantAPI.

* Rename template_api.

* Rename template_api.

* create dns for vpn

* added mapping

* Add TenantModel.

* Add TenantModel.

* Add AAAModel.

* tojson

* Add tenant payload

* Align new model to pydantic.

* adaptation to the new concept

* changed names with vpn to cisco_vpn

* added generate vpn id

* added vpn ipv4routing next-hop

* Finish Cisco VPN template

* Add path as a abstract property.

* Overload generate_payload.

* Finish Tenant template.

* Fix AAAModel.

* Fix static typing.

* Remove fr_templates.py

* Revert changes.

* Use double-quotes

* Fix unittests.

* fix test

* Fix mocks order.

* Fix mocks order.

* Add TemplateAlreadyExistsError.

* Return template_id.

* Changed numeric value to enum

* Update dependencies.

Co-authored-by: Tomasz Zietkowski -X (tzietkow - CODILIME SP ZOO at Cisco) <[email protected]>
  • Loading branch information
kagrski and tzietkow authored Jan 23, 2023
1 parent 98f71ca commit 0d8dfbd
Show file tree
Hide file tree
Showing 23 changed files with 2,683 additions and 799 deletions.
1,636 changes: 881 additions & 755 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ aiohttp = "^3.8.1"
ciscoconfparse = "^1.6.40"
tenacity = "^8.1.0"
parameterized = "^0.8.1"
cattrs = "^22.2.0"
pydantic = "^1.10.4"
Jinja2 = "^3.1.2"
flake8-quotes = "^3.3.1"
clint = "^0.5.1"
Expand Down
62 changes: 51 additions & 11 deletions vmngclient/api/templates.py → vmngclient/api/template_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import logging
from difflib import Differ
from enum import Enum
from typing import List
from typing import List, Optional

from ciscoconfparse import CiscoConfParse # type: ignore
from requests.exceptions import HTTPError

from vmngclient.api.task_status_api import wait_for_completed
from vmngclient.dataclasses import Device, Template
from vmngclient.api.templates.feature_template import FeatureTemplate
from vmngclient.dataclasses import Device, FeatureTemplateInformation, Template
from vmngclient.exceptions import InvalidOperationError
from vmngclient.session import vManageSession
from vmngclient.utils.creation_tools import create_dataclass
from vmngclient.utils.device_model import DeviceModel
Expand All @@ -22,14 +24,14 @@ class TemplateType(Enum):
FEATURE = "template"


class NotFoundError(Exception):
class TemplateNotFoundError(Exception):
"""Used when a template item is not found."""

def __init__(self, template):
self.message = f"No such template: '{template}'"


class NameAlreadyExistError(Exception):
class TemplateAlreadyExistsError(Exception):
"""Used when a template item exists."""

def __init__(self, name):
Expand All @@ -50,7 +52,7 @@ def __init__(self, name):
self.message = f"Template: {name} - wrong template type."


class TemplateAPI:
class TemplatesAPI:
def __init__(self, session: vManageSession) -> None:
self.session = session

Expand All @@ -72,15 +74,15 @@ def get(self, name: str) -> Template:
name (str): Name of template.
Raises:
NotFoundError: If template does not exist.
TemplateNotFoundError: If template does not exist.
Returns:
Template: Selected template.
"""
for template in self.templates:
if name == template.name:
return template
raise NotFoundError(name)
raise TemplateNotFoundError(name)

def get_id(self, name: str) -> str:
"""
Expand All @@ -106,7 +108,7 @@ def attach(self, name: str, device: Device) -> bool:
try:
template_id = self.get_id(name)
self.template_validation(template_id, device=device)
except NotFoundError:
except TemplateNotFoundError:
logger.error(f"Error, Template with name {name} not found on {device}.")
return False
except HTTPError as error:
Expand Down Expand Up @@ -199,21 +201,59 @@ def create(
config (CiscoConfParse): The config to device.
Raises:
NameAlreadyExistError: If such template name already exists.
TemplateAlreadyExistsError: If such template name already exists.
Returns:
bool: True if create template is successful, otherwise - False.
"""
try:
self.get(name)
logger.error(f"Error, Template with name: {name} exists.")
raise NameAlreadyExistError(name)
except NotFoundError:
raise TemplateAlreadyExistsError(name)
except TemplateNotFoundError:
cli_template = CLITemplate(self.session, device_model, name, description)
cli_template.config = config
logger.info(f"Template with name: {name} - created.")
return cli_template.send_to_device()

def create_feature_template(self, template: FeatureTemplate) -> str:
try:
self.get_single_feature_template(name=template.name)
except TemplateNotFoundError:
payload = template.generate_payload(self.session)
response = self.session.post("/dataservice/template/feature", json=json.loads(payload))
template_id = response.json()["templateId"]
logger.info(f"Template {template.name} was created successfully ({template_id}).")
return template_id
raise TemplateAlreadyExistsError(template.name)

def get_feature_templates(self, name: Optional[str] = None) -> List[FeatureTemplateInformation]:
"""Get feature template list.
Note: In a multitenant vManage system, this API is only available in the Provider view.
"""
payload = {"summary": "true"}
response = self.session.get("/dataservice/template/feature", params=payload)
parsed_response = response.json()["data"]
fr_templates = [
create_dataclass(FeatureTemplateInformation, feature_template) for feature_template in parsed_response
]

if name is None:
return fr_templates
return list(filter(lambda template: template.name == name, fr_templates))

def get_single_feature_template(self, name: str) -> FeatureTemplateInformation:
fr_templates = self.get_feature_templates(name=name)

if not fr_templates:
raise TemplateNotFoundError(name)

if len(fr_templates) > 1:
raise InvalidOperationError("The input sequence contains more than one element.")

return fr_templates[0]

def template_validation(self, id: str, device: Device) -> str:
"""Checking the template of the configuration on the machine.
Expand Down
41 changes: 41 additions & 0 deletions vmngclient/api/templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Contains a list of feature templates.
These feature template models are used to create and modify the templates
on the vManage server.
In addition, they are used to convert CLI config into separate feature
templates in vManage.
"""

# Basic FeatureTemplate class
from vmngclient.api.templates.feature_template import FeatureTemplate

# AAA Templates
from vmngclient.api.templates.payloads.aaa.aaa_model import AAAModel

# Cisco VPN Templates
from vmngclient.api.templates.payloads.cisco_vpn.cisco_vpn_model import (
DNS,
CiscoVPNModel,
GatewayType,
IPv4Route,
IPv6Route,
Mapping,
NextHop,
)

# CEdge Templates
from vmngclient.api.templates.payloads.tenant.tenant_model import TenantModel

__all__ = [
"FeatureTemplate",
"TenantModel",
"AAAModel",
"CiscoVPNModel",
"DNS",
"Mapping",
"IPv4Route",
"IPv6Route",
"GatewayType",
"NextHop",
]
36 changes: 36 additions & 0 deletions vmngclient/api/templates/feature_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from abc import ABC, abstractmethod
from pathlib import Path

from jinja2 import DebugUndefined, Environment, FileSystemLoader, meta # type: ignore
from pydantic import BaseModel # type: ignore

from vmngclient.session import vManageSession


class FeatureTemplate(BaseModel, ABC):
name: str
description: str

def generate_payload(self, session: vManageSession) -> str:
env = Environment(
loader=FileSystemLoader(self.payload_path.parent),
trim_blocks=True,
lstrip_blocks=True,
undefined=DebugUndefined,
)
template = env.get_template(self.payload_path.name)
output = template.render(self.dict())

ast = env.parse(output)
if meta.find_undeclared_variables(ast):
print(meta.find_undeclared_variables(ast))
raise Exception
return output

def generate_cli(self) -> str:
raise NotImplementedError()

@property
@abstractmethod
def payload_path(self) -> Path:
raise NotImplementedError()
89 changes: 89 additions & 0 deletions vmngclient/api/templates/payloads/aaa/aaa_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

from enum import Enum
from pathlib import Path
from typing import ClassVar, List, Optional

from attr import define, field # type: ignore

from vmngclient.api.templates.feature_template import FeatureTemplate
from vmngclient.dataclasses import User


class AuthenticationOrder(Enum):
LOCAL = "local"
RADIUS = "radius"
TACACS = "tacacs"


class TacacsAuthenticationMethod(Enum):
PAP = "pap"


class Action(Enum):
ACCEPT = "accept"
DENY = "deny"


class VpnType(Enum):
VPN_TRANSPORT = 0
VPN_MANAGMENT = 512


# from vmngclient.third_parties
@define
class TacacsServer:
"""Default values from documentations."""

address: str
auth_port: int = field(default=49)
secret_key: Optional[str] = field(default=None)
source_interface: Optional[str] = field(default=None)
vpn: int = field(default=0)
priority: int = field(default=0)


@define
class RadiusServer:
"""Default values from documentations."""

address: str
secret_key: Optional[str] = field(default=None)
source_interface: Optional[str] = field(default=None)
acct_port: int = field(default=1813)
auth_port: int = field(default=1812)
tag: Optional[str] = field(default=None)
timeout: int = field(default=5)
vpn: int = field(default=0)
priority: int = field(default=0)


@define
class AuthTask:
name: str
default_action: Action = field(default=Action.ACCEPT)


class AAAModel(FeatureTemplate):
class Config:
arbitrary_types_allowed = True

payload_path: ClassVar[Path] = Path(__file__).parent / "feature" / "aaa.json.j2"

auth_order: List[AuthenticationOrder]
auth_fallback: bool
auth_disable_audit_logs: bool
auth_admin_order: bool
auth_disable_netconf_logs: bool
auth_radius_servers: List[str] = []

local_users: List[User] = []

accounting: bool = True

tacacs_authentication: TacacsAuthenticationMethod = TacacsAuthenticationMethod.PAP
tacacs_timeout: int = 5
tacacs_servers: List[TacacsServer] = []
radius_retransmit: int = 3
radius_timeout: int = 5
radius_servers: List[RadiusServer] = []
Loading

0 comments on commit 0d8dfbd

Please sign in to comment.