Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,5 @@ cython_debug/

# PyPI configuration file
.pypirc

.vscode
6 changes: 5 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@


def main():
eda = EDAClient(base_url="https://<your_group_id>.srexperts.net:9443")
eda = EDAClient(
base_url="https://<your_group_id>.srexperts.net:9443",
username="admin",
password="<fill in the password>",
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded placeholder password in production code creates a security risk. Consider using environment variables or a secure configuration mechanism instead of placeholder text.

Copilot uses AI. Check for mistakes.
)
my_virtualnetwork = virtualnetwork(ns="eda", name="my-vnet-using-python")
eda.add_to_transaction_create(my_virtualnetwork)
eda.commit_transaction()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ dependencies = [
dev = ["ruff"]

[tool.uv.sources]
pydantic-eda = { git = "https://github.com/eda-labs/pydantic-eda", tag = "v0.3.2" }
pydantic-eda = { git = "https://github.com/eda-labs/pydantic-eda", tag = "v0.4.0" }
98 changes: 67 additions & 31 deletions src/client.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import logging
from typing import Any, List, Literal, Optional
from typing import Any, List, Literal, Optional, Tuple

import httpx
from pydantic import BaseModel
from pydantic_eda.core.v25_4_1.models import (
from pydantic_eda.core.v25_8_1.models import (
GroupVersionKind,
NsCrGvkName,
Transaction,
TransactionContent,
TransactionCr,
TransactionDetails,
TransactionExecutionResult,
TransactionId,
TransactionSummaryResult,
TransactionType,
TransactionValue,
)
Expand All @@ -24,32 +25,35 @@

logger = logging.getLogger(__name__)

EDA_VERSION = "v25.4.1"

KC_REALM = "master"
KC_CLIENT_ID = "admin-cli"
KC_USERNAME = "admin"
KC_PASSWORD = "admin"
EDA_REALM = "eda"
API_CLIENT_ID = "eda"

KC_API_URL = "/core/httpproxy/v1/keycloak"
TX_API_URL = "/core/transaction/v2"


class EDAClient(httpx.Client):
def __init__(self, base_url: str):
def __init__(self, base_url: str, username: str, password: str):
self.base_url: str = base_url
self.kc_url: str = self.base_url.join("/core/httpproxy/v1/keycloak")
self.kc_url: str = self.base_url.join(KC_API_URL)
self.username: str = username
self.password: str = password

self.headers: dict[str, str] = {}
self.token: str = ""
self.transaction: Optional[Transaction] = None
self.transaction_endpoint: str = self.base_url.join("/core/transaction/v1")
self.transaction_endpoint: str = self.base_url.join(TX_API_URL)

super().__init__(headers=self.headers, verify=False)

# acquire the token during initialization
self.auth()
self._auth()

def auth(self) -> None:
def _auth(self) -> None:
"""Authenticate and get access token"""
logger.info("Authenticating with EDA API server")

Expand Down Expand Up @@ -131,7 +135,7 @@ def add_to_transaction(self, resource: BaseModel, type: TxType) -> None:
self.transaction.crs.append(tx_cr)

def commit_transaction(self) -> Any:
"""Commit transaction"""
"""Commit transaction and get its status"""

# convert the transaction instance to a dict
if self.transaction is None:
Expand All @@ -140,15 +144,15 @@ def commit_transaction(self) -> Any:
self.transaction.retain = True
self.transaction.resultType = "normal"

content = self.transaction.model_dump_json(
tx_content = self.transaction.model_dump_json(
exclude_unset=True, exclude_none=True, exclude_defaults=True
)

# logger.info(f"Committing transaction: {content}")

response = self.post(
url=self.transaction_endpoint,
content=content,
content=tx_content,
)
if response.status_code != 200:
raise ValueError(response.text)
Expand All @@ -159,29 +163,23 @@ def commit_transaction(self) -> Any:

logger.info(f"Transaction {tx_id.id} committed")

tx_details: TransactionDetails = self.get_transaction_details(tx_id.id)
errs = self.tx_must_succeed(tx_details)
tx_summary_result, errs = self.tx_must_succeed(tx_id.id)
if errs:
logger.error(f"Transaction {tx_id.id} errors:\n - " + "\n - ".join(errs))
logger.info(f"Transaction {tx_id.id} state: {tx_details.state}")
logger.info(f"Transaction {tx_id.id} state: {tx_summary_result.state}")

def get_transaction_details(self, tx_id: int) -> TransactionDetails:
"""Get transaction"""
def get_transaction_summary_result(self, tx_id: int) -> TransactionSummaryResult:
"""Get transaction summary result"""

params = {
"waitForComplete": "true",
# "failOnErrors": "true"
}
response = self.get(
url=f"{self.transaction_endpoint}/details/{tx_id}",
params=params,
url=f"{self.transaction_endpoint}/result/summary/{tx_id}",
)
if response.status_code != 200:
raise ValueError(response.text)

# logger.info(f"Transaction {tx_id} details:\n{response.json()}")

return TransactionDetails(**response.json())
return TransactionSummaryResult(**response.json())

def _get_client_secret(self) -> str:
client = self
Expand Down Expand Up @@ -238,8 +236,8 @@ def _get_access_token(self) -> str:
"client_id": API_CLIENT_ID,
"grant_type": "password",
"scope": "openid",
"username": "admin",
"password": "admin",
"username": self.username,
"password": self.password,
"client_secret": client_secret,
}

Expand All @@ -249,7 +247,45 @@ def _get_access_token(self) -> str:
response.raise_for_status()
return response.json()["access_token"]

def tx_must_succeed(self, tx_details: TransactionDetails) -> List[str] | None:
"""Check if transaction succeeded"""
if not tx_details.success:
return tx_details.generalErrors
def tx_must_succeed(
self, tx_id: int
) -> Tuple[TransactionSummaryResult, Optional[List[str]]]:
"""Wait for transaction to complete and fetch its status and execution errors"""

errors: List[str] = []

tx_exec_result: TransactionExecutionResult = self.get_transaction_exec_result(
tx_id
)

tx_summary_result: TransactionSummaryResult = (
self.get_transaction_summary_result(tx_id)
)

if not tx_summary_result.success:
if tx_exec_result.generalErrors:
errors.extend(tx_exec_result.generalErrors)
# find errors in the intents
if tx_exec_result.intentsRun:
for intent_result in tx_exec_result.intentsRun:
if intent_result.errors:
for error in intent_result.errors:
errors.append(
f"Intent '{intent_result.intentName}' error: {error.rawError}"
)

return (tx_summary_result, errors)

def get_transaction_exec_result(self, tx_id: int) -> TransactionExecutionResult:
"""Get transaction execution result and wait for transaction to complete"""
params = {"waitForComplete": "true"}
response = self.get(
url=f"{self.transaction_endpoint}/result/execution/{tx_id}", params=params
)

if response.status_code != 200:
raise ValueError(response.text)

# logger.info(f"Transaction {tx_id} exec results:\n{response.json()}")

return TransactionExecutionResult(**response.json())
44 changes: 33 additions & 11 deletions src/virtualnetwork.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pydantic_eda.apps.services.v1alpha1.models as service
import pydantic_eda.apps.services.v1.models as service

router = service.VirtualNetworkSpecRouter(
name="vnet-router",
Expand Down Expand Up @@ -55,10 +55,14 @@
name="vnet-irb-300",
spec=service.VirtualNetworkSpecIrbInterfaceSpec(
bridgeDomain=bd_300.name,
hostRoutePopulate=service.VirtualNetworkSpecIrbInterfaceSpecHostRoutePopulate(dynamic=True, static=True),
hostRoutePopulate=service.VirtualNetworkSpecIrbInterfaceSpecHostRoutePopulate(
dynamic=True, static=True
),
ipAddresses=[
service.VirtualNetworkSpecIrbInterfaceSpecIpAddress(
ipv4Address=service.VirtualNetworkSpecIrbInterfaceSpecIpAddressIpv4Address(ipPrefix="10.30.0.1/24", primary=True)
ipv4Address=service.VirtualNetworkSpecIrbInterfaceSpecIpAddressIpv4Address(
ipPrefix="10.30.0.1/24", primary=True
)
),
service.VirtualNetworkSpecIrbInterfaceSpecIpAddress(
ipv6Address=service.VirtualNetworkSpecIrbInterfaceSpecIpAddressIpv6Address(
Expand All @@ -74,10 +78,14 @@
name="vnet-irb-312",
spec=service.VirtualNetworkSpecIrbInterfaceSpec(
bridgeDomain=bd_312.name,
hostRoutePopulate=service.VirtualNetworkSpecIrbInterfaceSpecHostRoutePopulate(dynamic=True, static=True),
hostRoutePopulate=service.VirtualNetworkSpecIrbInterfaceSpecHostRoutePopulate(
dynamic=True, static=True
),
ipAddresses=[
service.VirtualNetworkSpecIrbInterfaceSpecIpAddress(
ipv4Address=service.VirtualNetworkSpecIrbInterfaceSpecIpAddressIpv4Address(ipPrefix="10.30.2.1/24", primary=True)
ipv4Address=service.VirtualNetworkSpecIrbInterfaceSpecIpAddressIpv4Address(
ipPrefix="10.30.2.1/24", primary=True
)
),
service.VirtualNetworkSpecIrbInterfaceSpecIpAddress(
ipv6Address=service.VirtualNetworkSpecIrbInterfaceSpecIpAddressIpv6Address(
Expand All @@ -94,9 +102,15 @@
name="vnet-routed-interface-client11",
spec=service.VirtualNetworkSpecRoutedInterfaceSpec(
interface="leaf11-client11",
ipv4Addresses=[service.VirtualNetworkSpecRoutedInterfaceSpecIpv4Address(ipPrefix="10.30.1.1/24", primary=True)],
ipv4Addresses=[
service.VirtualNetworkSpecRoutedInterfaceSpecIpv4Address(
ipPrefix="10.30.1.1/24", primary=True
)
],
ipv6Addresses=[
service.VirtualNetworkSpecRoutedInterfaceSpecIpv6Address(ipPrefix="fd00:fdfd:0:3001::1/64", primary=True)
service.VirtualNetworkSpecRoutedInterfaceSpecIpv6Address(
ipPrefix="fd00:fdfd:0:3001::1/64", primary=True
)
],
router=router.name,
vlanID="311",
Expand All @@ -108,9 +122,15 @@
name="vnet-routed-interface-client13",
spec=service.VirtualNetworkSpecRoutedInterfaceSpec(
interface="leaf13-client13",
ipv4Addresses=[service.VirtualNetworkSpecRoutedInterfaceSpecIpv4Address(ipPrefix="10.30.3.1/24", primary=True)],
ipv4Addresses=[
service.VirtualNetworkSpecRoutedInterfaceSpecIpv4Address(
ipPrefix="10.30.3.1/24", primary=True
)
],
ipv6Addresses=[
service.VirtualNetworkSpecRoutedInterfaceSpecIpv6Address(ipPrefix="fd00:fdfd:0:3003::1/64", primary=True)
service.VirtualNetworkSpecRoutedInterfaceSpecIpv6Address(
ipPrefix="fd00:fdfd:0:3003::1/64", primary=True
)
],
router=router.name,
vlanID="313",
Expand All @@ -121,9 +141,11 @@

def virtualnetwork(ns: str, name: str) -> service.VirtualNetwork:
vnet = service.VirtualNetwork(
apiVersion="services.eda.nokia.com/v1alpha1",
apiVersion="services.eda.nokia.com/v1",
kind="VirtualNetwork",
metadata=service.VirtualNetworkMetadata(name=name, namespace=ns, labels={"role": "exercise"}),
metadata=service.VirtualNetworkMetadata(
name=name, namespace=ns, labels={"role": "exercise"}
),
spec=service.VirtualNetworkSpec(
routers=[
router,
Expand Down
Loading