From e764634e8f76a718349306dd5a84f57481f931a4 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 16 Nov 2023 13:30:42 +0000 Subject: [PATCH 01/21] Add new sub-routes for /netbox and change NetboxSession class to embbed Proxbox tag --- .../routes/{netbox.py => netbox/__init__.py} | 30 +++++----- .../backend/routes/netbox/dcim/__init__.py | 22 ++++++++ .../backend/routes/netbox/extras/__init__.py | 4 ++ netbox_proxbox/backend/session/netbox.py | 55 +++++++++++++++++-- netbox_proxbox/main.py | 10 ++++ standalone/package.json | 3 +- 6 files changed, 101 insertions(+), 23 deletions(-) rename netbox_proxbox/backend/routes/{netbox.py => netbox/__init__.py} (53%) create mode 100644 netbox_proxbox/backend/routes/netbox/dcim/__init__.py create mode 100644 netbox_proxbox/backend/routes/netbox/extras/__init__.py diff --git a/netbox_proxbox/backend/routes/netbox.py b/netbox_proxbox/backend/routes/netbox/__init__.py similarity index 53% rename from netbox_proxbox/backend/routes/netbox.py rename to netbox_proxbox/backend/routes/netbox/__init__.py index 1bfaf43..1fc7fc3 100644 --- a/netbox_proxbox/backend/routes/netbox.py +++ b/netbox_proxbox/backend/routes/netbox/__init__.py @@ -2,27 +2,20 @@ from typing import Annotated, Any -from netbox_proxbox.backend.schemas import PluginConfig -from netbox_proxbox.backend.schemas.netbox import NetboxSessionSchema -from netbox_proxbox.backend.session.netbox import NetboxSession from netbox_proxbox.backend.routes.proxbox import netbox_settings +from netbox_proxbox.backend.session.netbox import NetboxSessionDep +# FastAPI Router router = APIRouter() -async def netbox_session( - netbox_settings: Annotated[NetboxSessionSchema, Depends(netbox_settings)], -): - """Instantiate 'NetboxSession' class with user parameters and return Netbox HTTP connection to make API calls""" - return await NetboxSession(netbox_settings).pynetbox() - -# Make Session reusable -NetboxSessionDep = Annotated[Any, Depends(netbox_session)] - +# +# Endpoints: /netbox/ +# @router.get("/status") async def netbox_status( nb: NetboxSessionDep ): - return nb.status() + return nb.session.status() @router.get("/devices") async def netbox_devices(nb: NetboxSessionDep): @@ -42,9 +35,14 @@ async def netbox_devices(nb: NetboxSessionDep): @router.get("/") async def netbox( status: Annotated[Any, Depends(netbox_status)], - config: Annotated[Any, Depends(netbox_settings)] + config: Annotated[Any, Depends(netbox_settings)], + nb: NetboxSessionDep, ): return { "config": config, - "status": status - } \ No newline at end of file + "status": status, + "proxbox_tag": nb.tag + } + + + diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py new file mode 100644 index 0000000..34d5844 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter + +from netbox_proxbox.backend.session.netbox import NetboxSessionDep + +# FastAPI Router +router = APIRouter() + +@router.get("/sites") +async def get_sites( + nb: NetboxSessionDep +): + response = nb.dcim.sites.all() + print(response) + return response + +@router.post("/sites") +async def create_sites( + nb: NetboxSessionDep +): + response = nb.dcim.sites.create(name="Teste") + print(response) + return response \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/extras/__init__.py b/netbox_proxbox/backend/routes/netbox/extras/__init__.py new file mode 100644 index 0000000..dc07bf5 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/extras/__init__.py @@ -0,0 +1,4 @@ +from fastapi import APIRouter + +from netbox_proxbox.backend.session.netbox import NetboxSessionDep + diff --git a/netbox_proxbox/backend/session/netbox.py b/netbox_proxbox/backend/session/netbox.py index 7f6771b..3c3fc8f 100644 --- a/netbox_proxbox/backend/session/netbox.py +++ b/netbox_proxbox/backend/session/netbox.py @@ -1,6 +1,12 @@ -import aiohttp import requests +from typing import Annotated, Any +from fastapi import Depends + +from netbox_proxbox.backend.routes.proxbox import netbox_settings +from netbox_proxbox.backend.schemas.netbox import NetboxSessionSchema +from netbox_proxbox.backend.exception import ProxboxException + # Netbox import pynetbox @@ -21,8 +27,11 @@ def __init__(self, netbox_settings): self.token = netbox_settings.token self.ssl = netbox_settings.ssl self.settings = netbox_settings.settings + self.session = self.pynetbox_session() + self.tag = self.proxbox_tag() + - async def pynetbox(self): + def pynetbox_session(self): print("Establish Netbox connection...") try: # CHANGE SSL VERIFICATION TO FALSE @@ -42,7 +51,41 @@ async def pynetbox(self): except Exception as error: raise RuntimeError(f"Error trying to initialize Netbox Session using TOKEN {self.token} provided.\nPython Error: {error}") - async def aiohttp(self): - # Future Development. We're currently use pynetbox. - pass - \ No newline at end of file + def proxbox_tag(self): + proxbox_tag_name = 'Proxbox' + proxbox_tag_slug = 'proxbox' + + # Check if Proxbox tag already exists. + proxbox_tag = self.session.extras.tags.get( + name = proxbox_tag_name, + slug = proxbox_tag_slug + ) + + if proxbox_tag == None: + try: + # If Proxbox tag does not exist, create one. + tag = self.session.extras.tags.create( + name = proxbox_tag_name, + slug = proxbox_tag_slug, + color = 'ff5722', + description = "Proxbox Identifier (used to identify the items the plugin created)" + ) + except Exception as error: + raise ProxboxException( + message = f"Error creating the '{proxbox_tag_name}' tag. Possible errors: the name '{proxbox_tag_name}' or slug '{proxbox_tag_slug}' is already used.", + python_exception=f"{error}" + ) + else: + tag = proxbox_tag + + return tag + + +async def netbox_session( + netbox_settings: Annotated[NetboxSessionSchema, Depends(netbox_settings)], +): + """Instantiate 'NetboxSession' class with user parameters and return Netbox HTTP connection to make API calls""" + return NetboxSession(netbox_settings) + +# Make Session reusable +NetboxSessionDep = Annotated[Any, Depends(netbox_session)] \ No newline at end of file diff --git a/netbox_proxbox/main.py b/netbox_proxbox/main.py index 2129622..528947d 100644 --- a/netbox_proxbox/main.py +++ b/netbox_proxbox/main.py @@ -5,8 +5,14 @@ from netbox_proxbox.backend.exception import ProxboxException +# Netbox Routes from .backend.routes.netbox import router as netbox_router +from .backend.routes.netbox.dcim import router as nb_dcim_router + +# Proxbox Routes from .backend.routes.proxbox import router as proxbox_router + +# Proxmox Routes from .backend.routes.proxmox import router as proxmox_router from .backend.routes.proxmox.cluster import router as px_cluster_router from .backend.routes.proxmox.nodes import router as px_nodes_router @@ -31,8 +37,12 @@ async def proxmoxer_exception_handler(request: Request, exc: ProxboxException): ) +# # Routes (Endpoints) +# app.include_router(netbox_router, prefix="/netbox", tags=["netbox"]) +app.include_router(nb_dcim_router, prefix="/netbox/dcim", tags=["netbox / dcim"]) + app.include_router(proxbox_router, prefix="/proxbox", tags=["proxbox"]) app.include_router(px_nodes_router, prefix="/proxmox/nodes", tags=["proxmox / nodes"]) app.include_router(px_cluster_router, prefix="/proxmox/cluster", tags=["proxmox / cluster"]) diff --git a/standalone/package.json b/standalone/package.json index 7d0d40d..f6ce722 100644 --- a/standalone/package.json +++ b/standalone/package.json @@ -7,7 +7,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "fastapi-dev": "/opt/netbox/venv/bin/uvicorn netbox-proxbox.netbox_proxbox.main:app --host 0.0.0.0 --port 8800 --app-dir /opt/netbox/netbox --reload" + "fastapi-dev": "/opt/netbox/venv/bin/uvicorn netbox-proxbox.netbox_proxbox.main:app --host 0.0.0.0 --port 8800 --app-dir /opt/netbox/netbox --reload", + "netbox-dev": "/opt/netbox/venv/bin/python3 /opt/netbox/netbox/manage.py runserver 0.0.0.0:8000" }, "dependencies": { "@headlessui/react": "^1.7.17", From 1ff54c484a38a91f84898e2f4212dae9a4044438 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 16 Nov 2023 15:40:00 +0000 Subject: [PATCH 02/21] Add Site class with get() and post() methods --- .../backend/routes/netbox/dcim/__init__.py | 19 +-- .../backend/routes/netbox/dcim/sites.py | 111 ++++++++++++++++++ 2 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 netbox_proxbox/backend/routes/netbox/dcim/sites.py diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index 34d5844..839cc69 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -1,22 +1,13 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends -from netbox_proxbox.backend.session.netbox import NetboxSessionDep +from .sites import Sites # FastAPI Router router = APIRouter() @router.get("/sites") -async def get_sites( - nb: NetboxSessionDep -): - response = nb.dcim.sites.all() - print(response) - return response +async def get_sites( site: Sites = Depends() ): return await site.get() + @router.post("/sites") -async def create_sites( - nb: NetboxSessionDep -): - response = nb.dcim.sites.create(name="Teste") - print(response) - return response \ No newline at end of file +async def create_sites( site: Sites = Depends() ): return await site.post() diff --git a/netbox_proxbox/backend/routes/netbox/dcim/sites.py b/netbox_proxbox/backend/routes/netbox/dcim/sites.py new file mode 100644 index 0000000..203cd75 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/dcim/sites.py @@ -0,0 +1,111 @@ +from fastapi import Query + +from typing import Annotated + +from netbox_proxbox.backend.session.netbox import NetboxSessionDep +from netbox_proxbox.backend.exception import ProxboxException + +class Sites: + """ + Class to handle Netbox Sites. + + Logic: + 1. it will use 'site_id' to get the Site from Netbox if provided. + 1.1. if object is returned, it will return it. + 1.2. if object is not returned, it will raise an ProxboxException. + 2. if 'site_id' is not provided, it will check if there's any Site registered on Netbox. + 2.1. if there's no Site registered on Netbox, it will create a default one. + 2.2. if there's any Site registered on Netbox, it will check if is Proxbox one by checking tag and name. + 2.2.1. if it's Proxbox one, it will return it. + 2.2.2. if it's not Proxbox one, it will create a default one. + """ + + def __init__( + self, + nb: NetboxSessionDep, + site_id: Annotated[ + int, + Query( + title="Site ID", + description="Netbox Site ID of Nodes and/or Clusters.") + ] = None + ): + self.nb = nb + self.site_id = site_id + self.default_name = "Proxbox Basic Site" + self.default_slug = "proxbox-basic-site" + self.default_description = "Proxbox Basic Site (used to identify the items the plugin created)" + + + async def get(self): + # 1. If 'site_id' provided, use it to get the Site from Netbox. + if self.site_id: + response = None + try: + response = self.nb.session.dcim.sites.get(self.site_id) + + except Exception as error: + raise ProxboxException( + message=f"Error trying to get Site from Netbox using the specified ID '{self.site_id}''.", + python_exception=f"{error}" + ) + + # 1.1. Return found object. + if response != None: + return response + + # 1.2. Raise ProxboxException if object is not found. + else: + raise ProxboxException( + message=f"Site with ID '{self.site_id}' not found on Netbox.", + ) + + # 2. Check if there's any Site registered on Netbox. + else: + try: + response = self.nb.session.dcim.sites.all() + + # 2.1. If there's no Site registered on Netbox, create a default one. + if len(response) == 0: + default_site_obj = await self.post(default=True) + + return default_site_obj + + else: + # 2.2. If there's any Site registered on Netbox, check if is Proxbox one by checking tag and name. + for site in response: + # 2.2.1. If it's Proxbox one, return it. + if site.tags == [self.nb.tag.id] and site.name == self.default_name and site.slug == self.default_slug: + return site + + # 2.2.2. If it's not Proxbox one, create a default one. + default_site_obj = await self.post(default=True) + + return default_site_obj + + + except Exception as error: + raise ProxboxException( + message="Error trying to get Sites from Netbox.", + python_exception=f"{error}" + ) + + async def post(self, default: bool = False): + if default: + try: + response = self.nb.session.dcim.sites.create( + name = self.default_name, + slug = self.default_slug, + description = self.default_description, + status = 'active', + tags = [self.nb.tag.id] + ) + return response + except Exception as error: + raise ProxboxException( + message=f"Error trying to create the default Proxbox Site on Netbox.", + python_exception=f"{error}" + ) + + async def put(self): + pass \ No newline at end of file From 16e750f6e826cb4993d05c1e1d3f32280734fbcc Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 16 Nov 2023 19:59:57 +0000 Subject: [PATCH 03/21] Finish Sites class with fully functionable get() and post() methods --- .../backend/enum/netbox/__init__.py | 0 .../backend/enum/netbox/dcim/__init__.py | 8 +++ .../backend/routes/netbox/dcim/__init__.py | 21 +++++++- .../backend/routes/netbox/dcim/sites.py | 54 +++++++++++++++---- .../schemas/{netbox.py => netbox/__init__.py} | 6 ++- .../backend/schemas/netbox/dcim/__init__.py | 24 +++++++++ .../backend/schemas/netbox/extras/__init__.py | 11 ++++ 7 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 netbox_proxbox/backend/enum/netbox/__init__.py create mode 100644 netbox_proxbox/backend/enum/netbox/dcim/__init__.py rename netbox_proxbox/backend/schemas/{netbox.py => netbox/__init__.py} (60%) create mode 100644 netbox_proxbox/backend/schemas/netbox/dcim/__init__.py create mode 100644 netbox_proxbox/backend/schemas/netbox/extras/__init__.py diff --git a/netbox_proxbox/backend/enum/netbox/__init__.py b/netbox_proxbox/backend/enum/netbox/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_proxbox/backend/enum/netbox/dcim/__init__.py b/netbox_proxbox/backend/enum/netbox/dcim/__init__.py new file mode 100644 index 0000000..a58f5e4 --- /dev/null +++ b/netbox_proxbox/backend/enum/netbox/dcim/__init__.py @@ -0,0 +1,8 @@ +from enum import Enum + +class StatusOptions(str, Enum): + planned = "planned" + staging = "staging" + active = "active" + decommissioning = "decommissioning" + retired = "retired" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index 839cc69..e006d60 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -1,13 +1,30 @@ from fastapi import APIRouter, Depends +from typing import Annotated + from .sites import Sites +from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool +from netbox_proxbox.backend.schemas.netbox.dcim import SitesSchema + + # FastAPI Router router = APIRouter() +# +# /sites routes +# @router.get("/sites") async def get_sites( site: Sites = Depends() ): return await site.get() - @router.post("/sites") -async def create_sites( site: Sites = Depends() ): return await site.post() +async def create_sites( + site: Sites = Depends(), + default: CreateDefaultBool = False, + data: SitesSchema = None +): + """ + **default:** Boolean to define if Proxbox should create a default Site if there's no Site registered on Netbox.\n + **data:** JSON to create the Site on Netbox. You can create any Site you want, like a proxy to Netbox API. + """ + return await site.post(default, data) diff --git a/netbox_proxbox/backend/routes/netbox/dcim/sites.py b/netbox_proxbox/backend/routes/netbox/dcim/sites.py index 203cd75..b61d049 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/sites.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/sites.py @@ -5,6 +5,9 @@ from netbox_proxbox.backend.session.netbox import NetboxSessionDep from netbox_proxbox.backend.exception import ProxboxException +from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool +from netbox_proxbox.backend.schemas.netbox.dcim import SitesSchema + class Sites: """ Class to handle Netbox Sites. @@ -26,16 +29,20 @@ def __init__( site_id: Annotated[ int, Query( - title="Site ID", - description="Netbox Site ID of Nodes and/or Clusters.") - ] = None + title="Site ID", description="Netbox Site ID of Nodes and/or Clusters.") + ] = None, + all: Annotated[ + bool, + Query(title="List All Sites", description="List all Sites registered on Netbox.") + ] = False + ): self.nb = nb self.site_id = site_id self.default_name = "Proxbox Basic Site" self.default_slug = "proxbox-basic-site" self.default_description = "Proxbox Basic Site (used to identify the items the plugin created)" - + self.all = all async def get(self): # 1. If 'site_id' provided, use it to get the Site from Netbox. @@ -72,25 +79,37 @@ async def get(self): return default_site_obj else: + # If Query param 'all' is True, return all Sites registered on Netbox. + if self.all: + response_list = [] + + for site in response: + response_list.append(site) + + return response_list + # 2.2. If there's any Site registered on Netbox, check if is Proxbox one by checking tag and name. - for site in response: - # 2.2.1. If it's Proxbox one, return it. - if site.tags == [self.nb.tag.id] and site.name == self.default_name and site.slug == self.default_slug: - return site + get_proxbox_site = self.nb.session.dcim.sites.get( + name=self.default_name, + slug=self.default_slug, + tags=[self.nb.tag.id] + ) + if get_proxbox_site != None: + return get_proxbox_site + # 2.2.2. If it's not Proxbox one, create a default one. default_site_obj = await self.post(default=True) return default_site_obj - except Exception as error: raise ProxboxException( message="Error trying to get Sites from Netbox.", python_exception=f"{error}" ) - async def post(self, default: bool = False): + async def post(self, default: bool = False, data: SitesSchema = None): if default: try: response = self.nb.session.dcim.sites.create( @@ -107,5 +126,20 @@ async def post(self, default: bool = False): python_exception=f"{error}" ) + if data: + try: + data_dict = data.model_dump(exclude_unset=True) + + print(data_dict) + response = self.nb.session.dcim.sites.create(data_dict) + return response + + except Exception as error: + raise ProxboxException( + message=f"Error trying to create the Proxbox Site on Netbox.", + detail=f"Payload provided: {data_dict}", + python_exception=f"{error}" + ) + async def put(self): pass \ No newline at end of file diff --git a/netbox_proxbox/backend/schemas/netbox.py b/netbox_proxbox/backend/schemas/netbox/__init__.py similarity index 60% rename from netbox_proxbox/backend/schemas/netbox.py rename to netbox_proxbox/backend/schemas/netbox/__init__.py index 468b0f3..5af9da0 100644 --- a/netbox_proxbox/backend/schemas/netbox.py +++ b/netbox_proxbox/backend/schemas/netbox/__init__.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, RootModel class NetboxSessionSettingsSchema(BaseModel): virtualmachine_role_id: int @@ -10,4 +10,6 @@ class NetboxSessionSchema(BaseModel): http_port: int token: str ssl: bool - settings: NetboxSessionSettingsSchema | None = None \ No newline at end of file + settings: NetboxSessionSettingsSchema | None = None + +CreateDefaultBool = RootModel[ bool | None ] \ No newline at end of file diff --git a/netbox_proxbox/backend/schemas/netbox/dcim/__init__.py b/netbox_proxbox/backend/schemas/netbox/dcim/__init__.py new file mode 100644 index 0000000..17b59ce --- /dev/null +++ b/netbox_proxbox/backend/schemas/netbox/dcim/__init__.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel + + +from netbox_proxbox.backend.schemas.netbox.extras import TagSchema +from netbox_proxbox.backend.enum.netbox.dcim import StatusOptions + +class SitesSchema(BaseModel): + name: str + slug: str + status: StatusOptions + region: int | None = None + group: int | None = None + facility: str | None = None + asns: list[int] | None = None + time_zone: str | None = None + description: str | None = None + tags: list[TagSchema] | None = None + custom_fields: dict | None = None + physical_address: str | None = None + shipping_address: str | None = None + latitude: float | None = None + longitude: float | None = None + tenant: int | None = None + \ No newline at end of file diff --git a/netbox_proxbox/backend/schemas/netbox/extras/__init__.py b/netbox_proxbox/backend/schemas/netbox/extras/__init__.py new file mode 100644 index 0000000..9fc2e42 --- /dev/null +++ b/netbox_proxbox/backend/schemas/netbox/extras/__init__.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + +class TagSchema(BaseModel): + name: str + slug: str + color: str + description: str | None = None + object_types: list[str] | None = None + + + \ No newline at end of file From 61b973896a1813a4d56ad0d096cb1150f782bc18 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 16 Nov 2023 21:14:24 +0000 Subject: [PATCH 04/21] 'ClusterTypes' functional on '/netbox/virtualization/cluster-types' with 'GET' and 'POST' HTTP methods --- .../backend/routes/netbox/dcim/sites.py | 35 +++-- .../routes/netbox/virtualization/__init__.py | 24 +++ .../netbox/virtualization/cluster_type.py | 145 ++++++++++++++++++ .../schemas/netbox/virtualization/__init__.py | 12 ++ netbox_proxbox/main.py | 2 + 5 files changed, 201 insertions(+), 17 deletions(-) create mode 100644 netbox_proxbox/backend/routes/netbox/virtualization/__init__.py create mode 100644 netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py create mode 100644 netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py diff --git a/netbox_proxbox/backend/routes/netbox/dcim/sites.py b/netbox_proxbox/backend/routes/netbox/dcim/sites.py index b61d049..eabbf1b 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/sites.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/sites.py @@ -5,7 +5,6 @@ from netbox_proxbox.backend.session.netbox import NetboxSessionDep from netbox_proxbox.backend.exception import ProxboxException -from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool from netbox_proxbox.backend.schemas.netbox.dcim import SitesSchema class Sites: @@ -13,20 +12,21 @@ class Sites: Class to handle Netbox Sites. Logic: - 1. it will use 'site_id' to get the Site from Netbox if provided. + 1. it will use 'id' to get the Site from Netbox if provided. 1.1. if object is returned, it will return it. - 1.2. if object is not returned, it will raise an ProxboxException. - 2. if 'site_id' is not provided, it will check if there's any Site registered on Netbox. - 2.1. if there's no Site registered on Netbox, it will create a default one. - 2.2. if there's any Site registered on Netbox, it will check if is Proxbox one by checking tag and name. + 1.2. if object is not returned, it will raise an `ProxboxException`. + 2. if 'id' is not provided, it will check if there's any Site registered on Netbox. + 2.1. if there's no `Site` registered on Netbox, it will create a default one. + 2.2. if there's any `Site` registered on Netbox, it will check if is Proxbox one by checking tag and name. 2.2.1. if it's Proxbox one, it will return it. 2.2.2. if it's not Proxbox one, it will create a default one. + 3. if 'all' is True, it will return all `Sites` registered on Netbox. """ def __init__( self, nb: NetboxSessionDep, - site_id: Annotated[ + id: Annotated[ int, Query( title="Site ID", description="Netbox Site ID of Nodes and/or Clusters.") @@ -38,22 +38,22 @@ def __init__( ): self.nb = nb - self.site_id = site_id + self.id = id self.default_name = "Proxbox Basic Site" self.default_slug = "proxbox-basic-site" self.default_description = "Proxbox Basic Site (used to identify the items the plugin created)" self.all = all async def get(self): - # 1. If 'site_id' provided, use it to get the Site from Netbox. - if self.site_id: + # 1. If 'id' provided, use it to get the Site from Netbox using it. + if self.id: response = None try: - response = self.nb.session.dcim.sites.get(self.site_id) + response = self.nb.session.dcim.sites.get(self.id) except Exception as error: raise ProxboxException( - message=f"Error trying to get Site from Netbox using the specified ID '{self.site_id}''.", + message=f"Error trying to get Site from Netbox using the specified ID '{self.id}'.", python_exception=f"{error}" ) @@ -64,7 +64,7 @@ async def get(self): # 1.2. Raise ProxboxException if object is not found. else: raise ProxboxException( - message=f"Site with ID '{self.site_id}' not found on Netbox.", + message=f"Site with ID '{self.id}' not found on Netbox.", ) # 2. Check if there's any Site registered on Netbox. @@ -79,7 +79,7 @@ async def get(self): return default_site_obj else: - # If Query param 'all' is True, return all Sites registered on Netbox. + # 3. If Query param 'all' is True, return all Sites registered on Netbox. if self.all: response_list = [] @@ -88,7 +88,8 @@ async def get(self): return response_list - # 2.2. If there's any Site registered on Netbox, check if is Proxbox one by checking tag and name. + # 2.2 + # 2.2.1. If there's any 'Site' registered on Netbox, check if is Proxbox one by checking tag and name. get_proxbox_site = self.nb.session.dcim.sites.get( name=self.default_name, slug=self.default_slug, @@ -100,12 +101,11 @@ async def get(self): # 2.2.2. If it's not Proxbox one, create a default one. default_site_obj = await self.post(default=True) - return default_site_obj except Exception as error: raise ProxboxException( - message="Error trying to get Sites from Netbox.", + message="Error trying to get 'Sites' from Netbox.", python_exception=f"{error}" ) @@ -120,6 +120,7 @@ async def post(self, default: bool = False, data: SitesSchema = None): tags = [self.nb.tag.id] ) return response + except Exception as error: raise ProxboxException( message=f"Error trying to create the default Proxbox Site on Netbox.", diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py new file mode 100644 index 0000000..8e8567f --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Depends + +from .cluster_type import ClusterType + +from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool +from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema + +router = APIRouter() + +@router.get("/cluster-types") +async def get_cluster_types( cluster_type: ClusterType = Depends() ): + return await cluster_type.get() + +@router.post("/cluster-types") +async def create_cluster_types( + cluster_type: ClusterType = Depends(), + default: CreateDefaultBool = False, + data: ClusterTypeSchema = None +): + """ + **default:** Boolean to define if Proxbox should create a default Cluster Type if there's no Cluster Type registered on Netbox.\n + **data:** JSON to create the Cluster Type on Netbox. You can create any Cluster Type you want, like a proxy to Netbox API. + """ + return await cluster_type.post(default, data) \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py new file mode 100644 index 0000000..281b948 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py @@ -0,0 +1,145 @@ +from fastapi import Query + +from typing import Annotated + +from netbox_proxbox.backend.session.netbox import NetboxSessionDep +from netbox_proxbox.backend.exception import ProxboxException + +from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema + +class ClusterType: + """ + Class to handle Netbox Clyster Types. + + Logic: + 1. it will use 'id' to get the 'Cluster Type' from Netbox if provided. + 1.1. if object is returned, it will return it. + 1.2. if object is not returned, it will raise an `ProxboxException`. + 2. if 'site_id' is not provided, it will check if there's any Site registered on Netbox. + 2.1. if there's no `Cluster Type` registered on Netbox, it will create a default one. + 2.2. if there's any `Cluster Type` registered on Netbox, it will check if is Proxbox one by checking tag and name. + 2.2.1. if it's Proxbox one, it will return it. + 2.2.2. if it's not Proxbox one, it will create a default one. + 3. if 'all' is True, it will return all `Cluster Type` registered on Netbox. + """ + + def __init__( + self, + nb: NetboxSessionDep, + id: Annotated[ + int, + Query( + title="Cluster Type ID", description="Netbox Cluster Type ID of Nodes and/or Clusters." + ) + ] = None, + all: Annotated[ + bool, + Query(title="List All Cluster Types", description="List all Cluster Types registered on Netbox.") + ] = False, + ): + self.nb = nb + self.id = id + self.all = all + + # Default Cluster Type Params + self.default_name = "Proxbox Basic Cluster Type" + self.default_slug = "proxbox-basic-cluster-type" + self.default_description = "Proxbox Basic Cluster Type (used to identify the items the plugin created)" + + async def get(self): + # 1. If 'id' provided, use it to get the Cluster Type from Netbox using it. + if self.id: + response = None + + try: + response = self.nb.session.virtualization.cluster_types.get(self.id) + + except Exception as error: + raise ProxboxException( + message="Error trying to get Cluster Type from Netbox using the specified ID '{self.id}'.", + error=f"{error}" + ) + + # 1.1. Return found object. + if response != None: + return response + + # 1.2. Raise ProxboxException if object is not found. + else: + raise ProxboxException( + message=f"Cluster Type with ID '{self.id}' not found on Netbox." + ) + + # 2. Check if there's any Cluster Type registered on Netbox. + else: + try: + response = self.nb.session.virtualization.cluster_types.all() + + # 2.1. If there's no Cluster Type registered on Netbox, create a default one. + if len(response) == 0: + default_cluster_type_obj = await self.post(default=True) + + return default_cluster_type_obj + + else: + # 3. If Query param 'all' is True, return all Cluster Types registered. + if self.all: + response_list = [] + + for cluster_type in response: + response_list.append(cluster_type) + + return response_list + + # 2.2 + # 2.2.1 If there's any Cluster Type registered on Netbox, check if is Proxbox one by checking tag and name. + get_proxbox_cluster_type = self.nb.session.virtualization.cluster_types.get( + name=self.default_name, + slug=self.default_slug, + tags=[self.nb.tag.id] + ) + + if get_proxbox_cluster_type != None: + return get_proxbox_cluster_type + + # 2.2.2. If it's not Proxbox one, create a default one. + default_cluster_type_obj = await self.post(default=True) + return default_cluster_type_obj + + except Exception as error: + raise ProxboxException( + message="Error trying to get 'Cluster Types' from Netbox.", + python_exception=f"{error}" + ) + + async def post(self, default: bool = False, data: ClusterTypeSchema = None): + if default: + try: + response = self.nb.session.virtualization.cluster_types.create( + name = self.default_name, + slug = self.default_slug, + description = self.default_description, + tags = [self.nb.tag.id] + ) + + return response + + except Exception as error: + raise ProxboxException( + message="Error trying to create default Cluster Type on Netbox.", + python_exception=f"{error}" + ) + + if data: + try: + data_dict = data.model_dump(exclude_unset=True) + + response = self.nb.session.virtualization.cluster_types.create(data_dict) + return response + + except Exception as error: + raise ProxboxException( + message="Error trying to create Cluster Type on Netbox.", + detail=f"Payload provided: {data_dict}", + python_exception=f"{error}" + ) diff --git a/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py new file mode 100644 index 0000000..8edb2fe --- /dev/null +++ b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + +from netbox_proxbox.backend.schemas.netbox.extras import TagSchema +from netbox_proxbox.backend.enum.netbox.dcim import StatusOptions + +class ClusterTypeSchema(BaseModel): + name: str + slug: str + description: str | None = None + tags: list[TagSchema] | None = None + custom_fields: dict | None = None + \ No newline at end of file diff --git a/netbox_proxbox/main.py b/netbox_proxbox/main.py index 528947d..cf9f0f0 100644 --- a/netbox_proxbox/main.py +++ b/netbox_proxbox/main.py @@ -8,6 +8,7 @@ # Netbox Routes from .backend.routes.netbox import router as netbox_router from .backend.routes.netbox.dcim import router as nb_dcim_router +from .backend.routes.netbox.virtualization import router as nb_virtualization_router # Proxbox Routes from .backend.routes.proxbox import router as proxbox_router @@ -42,6 +43,7 @@ async def proxmoxer_exception_handler(request: Request, exc: ProxboxException): # app.include_router(netbox_router, prefix="/netbox", tags=["netbox"]) app.include_router(nb_dcim_router, prefix="/netbox/dcim", tags=["netbox / dcim"]) +app.include_router(nb_virtualization_router, prefix="/netbox/virtualization", tags=["netbox / virtualization"]) app.include_router(proxbox_router, prefix="/proxbox", tags=["proxbox"]) app.include_router(px_nodes_router, prefix="/proxmox/nodes", tags=["proxmox / nodes"]) From b9fe50aed808ce0a68a81bed9af05a8f13acc4b1 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 16 Nov 2023 21:14:57 +0000 Subject: [PATCH 05/21] Minor fixes to 'Sites' class --- netbox_proxbox/backend/routes/netbox/dcim/sites.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netbox_proxbox/backend/routes/netbox/dcim/sites.py b/netbox_proxbox/backend/routes/netbox/dcim/sites.py index eabbf1b..72cf7ca 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/sites.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/sites.py @@ -131,7 +131,6 @@ async def post(self, default: bool = False, data: SitesSchema = None): try: data_dict = data.model_dump(exclude_unset=True) - print(data_dict) response = self.nb.session.dcim.sites.create(data_dict) return response From 0a3144285155b9381f52822412bb4f6690237c3f Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Fri, 17 Nov 2023 21:10:32 +0000 Subject: [PATCH 06/21] Parent 'NetboxBase' class working with both 'ClusterType' and 'Sites' classes. Reduced a lot of code repetition. --- .../backend/routes/netbox/dcim/sites.py | 150 +---------------- .../backend/routes/netbox/generic.py | 155 ++++++++++++++++++ .../routes/netbox/virtualization/__init__.py | 18 +- .../routes/netbox/virtualization/cluster.py | 0 .../netbox/virtualization/cluster_type.py | 151 +---------------- 5 files changed, 187 insertions(+), 287 deletions(-) create mode 100644 netbox_proxbox/backend/routes/netbox/generic.py create mode 100644 netbox_proxbox/backend/routes/netbox/virtualization/cluster.py diff --git a/netbox_proxbox/backend/routes/netbox/dcim/sites.py b/netbox_proxbox/backend/routes/netbox/dcim/sites.py index 72cf7ca..bce07dd 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/sites.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/sites.py @@ -1,145 +1,9 @@ -from fastapi import Query +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase -from typing import Annotated - -from netbox_proxbox.backend.session.netbox import NetboxSessionDep -from netbox_proxbox.backend.exception import ProxboxException - -from netbox_proxbox.backend.schemas.netbox.dcim import SitesSchema - -class Sites: - """ - Class to handle Netbox Sites. - - Logic: - 1. it will use 'id' to get the Site from Netbox if provided. - 1.1. if object is returned, it will return it. - 1.2. if object is not returned, it will raise an `ProxboxException`. - 2. if 'id' is not provided, it will check if there's any Site registered on Netbox. - 2.1. if there's no `Site` registered on Netbox, it will create a default one. - 2.2. if there's any `Site` registered on Netbox, it will check if is Proxbox one by checking tag and name. - 2.2.1. if it's Proxbox one, it will return it. - 2.2.2. if it's not Proxbox one, it will create a default one. - 3. if 'all' is True, it will return all `Sites` registered on Netbox. - """ - - def __init__( - self, - nb: NetboxSessionDep, - id: Annotated[ - int, - Query( - title="Site ID", description="Netbox Site ID of Nodes and/or Clusters.") - ] = None, - all: Annotated[ - bool, - Query(title="List All Sites", description="List all Sites registered on Netbox.") - ] = False - - ): - self.nb = nb - self.id = id - self.default_name = "Proxbox Basic Site" - self.default_slug = "proxbox-basic-site" - self.default_description = "Proxbox Basic Site (used to identify the items the plugin created)" - self.all = all - - async def get(self): - # 1. If 'id' provided, use it to get the Site from Netbox using it. - if self.id: - response = None - try: - response = self.nb.session.dcim.sites.get(self.id) - - except Exception as error: - raise ProxboxException( - message=f"Error trying to get Site from Netbox using the specified ID '{self.id}'.", - python_exception=f"{error}" - ) - - # 1.1. Return found object. - if response != None: - return response - - # 1.2. Raise ProxboxException if object is not found. - else: - raise ProxboxException( - message=f"Site with ID '{self.id}' not found on Netbox.", - ) - - # 2. Check if there's any Site registered on Netbox. - else: - try: - response = self.nb.session.dcim.sites.all() - - # 2.1. If there's no Site registered on Netbox, create a default one. - if len(response) == 0: - default_site_obj = await self.post(default=True) - - return default_site_obj - - else: - # 3. If Query param 'all' is True, return all Sites registered on Netbox. - if self.all: - response_list = [] - - for site in response: - response_list.append(site) - - return response_list - - # 2.2 - # 2.2.1. If there's any 'Site' registered on Netbox, check if is Proxbox one by checking tag and name. - get_proxbox_site = self.nb.session.dcim.sites.get( - name=self.default_name, - slug=self.default_slug, - tags=[self.nb.tag.id] - ) - - if get_proxbox_site != None: - return get_proxbox_site - - # 2.2.2. If it's not Proxbox one, create a default one. - default_site_obj = await self.post(default=True) - return default_site_obj - - except Exception as error: - raise ProxboxException( - message="Error trying to get 'Sites' from Netbox.", - python_exception=f"{error}" - ) - - async def post(self, default: bool = False, data: SitesSchema = None): - if default: - try: - response = self.nb.session.dcim.sites.create( - name = self.default_name, - slug = self.default_slug, - description = self.default_description, - status = 'active', - tags = [self.nb.tag.id] - ) - return response - - except Exception as error: - raise ProxboxException( - message=f"Error trying to create the default Proxbox Site on Netbox.", - python_exception=f"{error}" - ) - - if data: - try: - data_dict = data.model_dump(exclude_unset=True) - - response = self.nb.session.dcim.sites.create(data_dict) - return response - - except Exception as error: - raise ProxboxException( - message=f"Error trying to create the Proxbox Site on Netbox.", - detail=f"Payload provided: {data_dict}", - python_exception=f"{error}" - ) +class Sites(NetboxBase): + default_name = "Proxbox Basic Site" + default_slug = "proxbox-basic-site" + default_description = "Proxbox Basic Site (used to identify the items the plugin created)" - async def put(self): - pass \ No newline at end of file + app = "dcim" + endpoint = "sites" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py new file mode 100644 index 0000000..428eba3 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -0,0 +1,155 @@ +from fastapi import Query + +from typing import Annotated + +from netbox_proxbox.backend.session.netbox import NetboxSessionDep +from netbox_proxbox.backend.exception import ProxboxException + +from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema + + +class NetboxBase: + """ + Class to handle Netbox Clyster Types. + + Logic: + 1. it will use 'id' to get the 'Cluster Type' from Netbox if provided. + 1.1. if object is returned, it will return it. + 1.2. if object is not returned, it will raise an `ProxboxException`. + 2. if 'site_id' is not provided, it will check if there's any Site registered on Netbox. + 2.1. if there's no `Cluster Type` registered on Netbox, it will create a default one. + 2.2. if there's any `Cluster Type` registered on Netbox, it will check if is Proxbox one by checking tag and name. + 2.2.1. if it's Proxbox one, it will return it. + 2.2.2. if it's not Proxbox one, it will create a default one. + 3. if 'all' is True, it will return all `Cluster Type` registered on Netbox. + """ + + def __init__( + self, + nb: NetboxSessionDep, + id: Annotated[ + int, + Query( + title="Cluster Type ID", description="Netbox Cluster Type ID of Nodes and/or Clusters." + ) + ] = None, + all: Annotated[ + bool, + Query(title="List All Cluster Types", description="List all Cluster Types registered on Netbox.") + ] = False, + + ): + self.nb = nb + self.id = id + self.all = all + self.pynetbox_path = getattr(getattr(self.nb.session, self.app), self.endpoint) + + # Default Cluster Type Params + default_name = None + default_slug = None + default_description = None + + # Parameters to be used as Pynetbox class attributes + app = None + endpoint = None + + + + + async def get(self): + # 1. If 'id' provided, use it to get the Cluster Type from Netbox using it. + if self.id: + response = None + + try: + response = self.pynetbox_path.get(self.id) + + except Exception as error: + raise ProxboxException( + message="Error trying to get Cluster Type from Netbox using the specified ID '{self.id}'.", + error=f"{error}" + ) + + # 1.1. Return found object. + if response != None: + return response + + # 1.2. Raise ProxboxException if object is not found. + else: + raise ProxboxException( + message=f"Cluster Type with ID '{self.id}' not found on Netbox." + ) + + # 2. Check if there's any Cluster Type registered on Netbox. + else: + try: + response = self.pynetbox_path.all() + + # 2.1. If there's no Cluster Type registered on Netbox, create a default one. + if len(response) == 0: + default_cluster_type_obj = await self.post(default=True) + + return default_cluster_type_obj + + else: + # 3. If Query param 'all' is True, return all Cluster Types registered. + if self.all: + response_list = [] + + for cluster_type in response: + response_list.append(cluster_type) + + return response_list + + # 2.2 + # 2.2.1 If there's any Cluster Type registered on Netbox, check if is Proxbox one by checking tag and name. + get_proxbox_cluster_type = self.pynetbox_path.get( + name=self.default_name, + slug=self.default_slug, + tags=[self.nb.tag.id] + ) + + if get_proxbox_cluster_type != None: + return get_proxbox_cluster_type + + # 2.2.2. If it's not Proxbox one, create a default one. + default_cluster_type_obj = await self.post(default=True) + return default_cluster_type_obj + + except Exception as error: + raise ProxboxException( + message="Error trying to get 'Cluster Types' from Netbox.", + python_exception=f"{error}" + ) + + async def post(self, default: bool = False, data: ClusterTypeSchema = None): + if default: + try: + response = self.pynetbox_path.create( + name = self.default_name, + slug = self.default_slug, + description = self.default_description, + tags = [self.nb.tag.id] + ) + + return response + + except Exception as error: + raise ProxboxException( + message="Error trying to create default Cluster Type on Netbox.", + python_exception=f"{error}" + ) + + if data: + try: + data_dict = data.model_dump(exclude_unset=True) + + response = self.pynetbox_path.create(data_dict) + return response + + except Exception as error: + raise ProxboxException( + message="Error trying to create Cluster Type on Netbox.", + detail=f"Payload provided: {data_dict}", + python_exception=f"{error}" + ) diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py index 8e8567f..55a7f8a 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py @@ -21,4 +21,20 @@ async def create_cluster_types( **default:** Boolean to define if Proxbox should create a default Cluster Type if there's no Cluster Type registered on Netbox.\n **data:** JSON to create the Cluster Type on Netbox. You can create any Cluster Type you want, like a proxy to Netbox API. """ - return await cluster_type.post(default, data) \ No newline at end of file + return await cluster_type.post(default, data) + + +from netbox_proxbox.backend.session.netbox import NetboxSessionDep +@router.get("/testing") +async def get_testing( + nb: NetboxSessionDep, + app: str | None = None, + endpoint: str | None = None, +): + response_list = [] + + teste = getattr(getattr(nb.session, app), endpoint) + for item in teste.all(): + response_list.append(item) + + return response_list \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py index 281b948..2141a17 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py @@ -1,145 +1,10 @@ -from fastapi import Query +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase -from typing import Annotated - -from netbox_proxbox.backend.session.netbox import NetboxSessionDep -from netbox_proxbox.backend.exception import ProxboxException - -from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema - -class ClusterType: - """ - Class to handle Netbox Clyster Types. +class ClusterType(NetboxBase): + # Default Cluster Type Params + default_name = "Proxbox Basic Cluster Type" + default_slug = "proxbox-basic-cluster-type" + default_description = "Proxbox Basic Cluster Type (used to identify the items the plugin created)" - Logic: - 1. it will use 'id' to get the 'Cluster Type' from Netbox if provided. - 1.1. if object is returned, it will return it. - 1.2. if object is not returned, it will raise an `ProxboxException`. - 2. if 'site_id' is not provided, it will check if there's any Site registered on Netbox. - 2.1. if there's no `Cluster Type` registered on Netbox, it will create a default one. - 2.2. if there's any `Cluster Type` registered on Netbox, it will check if is Proxbox one by checking tag and name. - 2.2.1. if it's Proxbox one, it will return it. - 2.2.2. if it's not Proxbox one, it will create a default one. - 3. if 'all' is True, it will return all `Cluster Type` registered on Netbox. - """ - - def __init__( - self, - nb: NetboxSessionDep, - id: Annotated[ - int, - Query( - title="Cluster Type ID", description="Netbox Cluster Type ID of Nodes and/or Clusters." - ) - ] = None, - all: Annotated[ - bool, - Query(title="List All Cluster Types", description="List all Cluster Types registered on Netbox.") - ] = False, - ): - self.nb = nb - self.id = id - self.all = all - - # Default Cluster Type Params - self.default_name = "Proxbox Basic Cluster Type" - self.default_slug = "proxbox-basic-cluster-type" - self.default_description = "Proxbox Basic Cluster Type (used to identify the items the plugin created)" - - async def get(self): - # 1. If 'id' provided, use it to get the Cluster Type from Netbox using it. - if self.id: - response = None - - try: - response = self.nb.session.virtualization.cluster_types.get(self.id) - - except Exception as error: - raise ProxboxException( - message="Error trying to get Cluster Type from Netbox using the specified ID '{self.id}'.", - error=f"{error}" - ) - - # 1.1. Return found object. - if response != None: - return response - - # 1.2. Raise ProxboxException if object is not found. - else: - raise ProxboxException( - message=f"Cluster Type with ID '{self.id}' not found on Netbox." - ) - - # 2. Check if there's any Cluster Type registered on Netbox. - else: - try: - response = self.nb.session.virtualization.cluster_types.all() - - # 2.1. If there's no Cluster Type registered on Netbox, create a default one. - if len(response) == 0: - default_cluster_type_obj = await self.post(default=True) - - return default_cluster_type_obj - - else: - # 3. If Query param 'all' is True, return all Cluster Types registered. - if self.all: - response_list = [] - - for cluster_type in response: - response_list.append(cluster_type) - - return response_list - - # 2.2 - # 2.2.1 If there's any Cluster Type registered on Netbox, check if is Proxbox one by checking tag and name. - get_proxbox_cluster_type = self.nb.session.virtualization.cluster_types.get( - name=self.default_name, - slug=self.default_slug, - tags=[self.nb.tag.id] - ) - - if get_proxbox_cluster_type != None: - return get_proxbox_cluster_type - - # 2.2.2. If it's not Proxbox one, create a default one. - default_cluster_type_obj = await self.post(default=True) - return default_cluster_type_obj - - except Exception as error: - raise ProxboxException( - message="Error trying to get 'Cluster Types' from Netbox.", - python_exception=f"{error}" - ) - - async def post(self, default: bool = False, data: ClusterTypeSchema = None): - if default: - try: - response = self.nb.session.virtualization.cluster_types.create( - name = self.default_name, - slug = self.default_slug, - description = self.default_description, - tags = [self.nb.tag.id] - ) - - return response - - except Exception as error: - raise ProxboxException( - message="Error trying to create default Cluster Type on Netbox.", - python_exception=f"{error}" - ) - - if data: - try: - data_dict = data.model_dump(exclude_unset=True) - - response = self.nb.session.virtualization.cluster_types.create(data_dict) - return response - - except Exception as error: - raise ProxboxException( - message="Error trying to create Cluster Type on Netbox.", - detail=f"Payload provided: {data_dict}", - python_exception=f"{error}" - ) + app = "virtualization" + endpoint = "cluster_types" \ No newline at end of file From 367175586cc58185b03832925373e14f057209ca Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Fri, 17 Nov 2023 21:13:33 +0000 Subject: [PATCH 07/21] Change NetboxBase docstring --- netbox_proxbox/backend/routes/netbox/generic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index 428eba3..286e377 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -10,18 +10,18 @@ class NetboxBase: """ - Class to handle Netbox Clyster Types. + Class to handle Netbox 'Objects'. Logic: - 1. it will use 'id' to get the 'Cluster Type' from Netbox if provided. + 1. it will use 'id' to get the 'Objects' from Netbox if provided. 1.1. if object is returned, it will return it. 1.2. if object is not returned, it will raise an `ProxboxException`. 2. if 'site_id' is not provided, it will check if there's any Site registered on Netbox. - 2.1. if there's no `Cluster Type` registered on Netbox, it will create a default one. - 2.2. if there's any `Cluster Type` registered on Netbox, it will check if is Proxbox one by checking tag and name. + 2.1. if there's no 'Objects' registered on Netbox, it will create a default one. + 2.2. if there's any 'Objects' registered on Netbox, it will check if is Proxbox one by checking tag and name. 2.2.1. if it's Proxbox one, it will return it. 2.2.2. if it's not Proxbox one, it will create a default one. - 3. if 'all' is True, it will return all `Cluster Type` registered on Netbox. + 3. if 'all' is True, it will return all 'Objects' registered on Netbox. """ def __init__( From 884effc6bb27ec16ed1f86d1006e897fdd7d1849 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Sat, 18 Nov 2023 19:46:10 +0000 Subject: [PATCH 08/21] Add 'default_extra_fields' to 'NetboxBase' class to enable Clusters GET and POST methods. --- .../enum/netbox/virtualization/__init__.py | 8 +++ .../backend/routes/netbox/dcim/sites.py | 3 +- .../backend/routes/netbox/generic.py | 53 ++++++++++------- .../routes/netbox/virtualization/__init__.py | 57 +++++++++++++++---- .../routes/netbox/virtualization/cluster.py | 16 ++++++ .../netbox/virtualization/cluster_type.py | 3 +- .../schemas/netbox/virtualization/__init__.py | 15 ++++- 7 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 netbox_proxbox/backend/enum/netbox/virtualization/__init__.py diff --git a/netbox_proxbox/backend/enum/netbox/virtualization/__init__.py b/netbox_proxbox/backend/enum/netbox/virtualization/__init__.py new file mode 100644 index 0000000..5fb3a59 --- /dev/null +++ b/netbox_proxbox/backend/enum/netbox/virtualization/__init__.py @@ -0,0 +1,8 @@ +from enum import Enum + +class ClusterStatusOptions(str, Enum): + planned = "planned" + staging = "staging" + active = "active" + decommissioning = "decommissioning" + offline = "offline" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/sites.py b/netbox_proxbox/backend/routes/netbox/dcim/sites.py index bce07dd..1ae853b 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/sites.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/sites.py @@ -6,4 +6,5 @@ class Sites(NetboxBase): default_description = "Proxbox Basic Site (used to identify the items the plugin created)" app = "dcim" - endpoint = "sites" \ No newline at end of file + endpoint = "sites" + object_name = "Site" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index 286e377..63f54b4 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -5,9 +5,6 @@ from netbox_proxbox.backend.session.netbox import NetboxSessionDep from netbox_proxbox.backend.exception import ProxboxException -from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema - - class NetboxBase: """ Class to handle Netbox 'Objects'. @@ -30,12 +27,12 @@ def __init__( id: Annotated[ int, Query( - title="Cluster Type ID", description="Netbox Cluster Type ID of Nodes and/or Clusters." + title="Object ID" ) ] = None, all: Annotated[ bool, - Query(title="List All Cluster Types", description="List all Cluster Types registered on Netbox.") + Query(title="List All Objects", description="List all Objects registered on Netbox.") ] = False, ): @@ -43,6 +40,7 @@ def __init__( self.id = id self.all = all self.pynetbox_path = getattr(getattr(self.nb.session, self.app), self.endpoint) + self.extra_dict_fields = None # Default Cluster Type Params default_name = None @@ -52,11 +50,13 @@ def __init__( # Parameters to be used as Pynetbox class attributes app = None endpoint = None - - + object_name = None - async def get(self): + async def get( + self, + default_extra_fields: dict = None + ): # 1. If 'id' provided, use it to get the Cluster Type from Netbox using it. if self.id: response = None @@ -66,7 +66,7 @@ async def get(self): except Exception as error: raise ProxboxException( - message="Error trying to get Cluster Type from Netbox using the specified ID '{self.id}'.", + message=f"Error trying to get {self.object_name} from Netbox using the specified ID '{self.id}'.", error=f"{error}" ) @@ -77,17 +77,19 @@ async def get(self): # 1.2. Raise ProxboxException if object is not found. else: raise ProxboxException( - message=f"Cluster Type with ID '{self.id}' not found on Netbox." + message=f"{self.object_name} with ID '{self.id}' not found on Netbox." ) # 2. Check if there's any Cluster Type registered on Netbox. else: try: response = self.pynetbox_path.all() + print(response, len) # 2.1. If there's no Cluster Type registered on Netbox, create a default one. if len(response) == 0: - default_cluster_type_obj = await self.post(default=True) + print("2.1. If there's no Cluster Type registered on Netbox, create a default one.") + default_cluster_type_obj = await self.post(default=True, default_extra_fields=default_extra_fields) return default_cluster_type_obj @@ -113,30 +115,43 @@ async def get(self): return get_proxbox_cluster_type # 2.2.2. If it's not Proxbox one, create a default one. + print("2.2.2. If it's not Proxbox one, create a default one.") default_cluster_type_obj = await self.post(default=True) return default_cluster_type_obj except Exception as error: raise ProxboxException( - message="Error trying to get 'Cluster Types' from Netbox.", + message=f"Error trying to get '{self.object_name}' from Netbox.", python_exception=f"{error}" ) - async def post(self, default: bool = False, data: ClusterTypeSchema = None): + async def post( + self, + default: bool = False, + default_extra_fields: dict = None, + data: dict = None, + ): + base_dict_fields = { + "name": self.default_name, + "slug": self.default_slug, + "description": self.default_description, + "tags": [self.nb.tag.id] + } + if default: try: + if default_extra_fields: + base_dict_fields.update(default_extra_fields) + response = self.pynetbox_path.create( - name = self.default_name, - slug = self.default_slug, - description = self.default_description, - tags = [self.nb.tag.id] + base_dict_fields ) return response except Exception as error: raise ProxboxException( - message="Error trying to create default Cluster Type on Netbox.", + message=f"Error trying to create default {self.object_name} on Netbox.", python_exception=f"{error}" ) @@ -149,7 +164,7 @@ async def post(self, default: bool = False, data: ClusterTypeSchema = None): except Exception as error: raise ProxboxException( - message="Error trying to create Cluster Type on Netbox.", + message=f"Error trying to create {self.object_name} on Netbox.", detail=f"Payload provided: {data_dict}", python_exception=f"{error}" ) diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py index 55a7f8a..55032d4 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py @@ -1,9 +1,12 @@ from fastapi import APIRouter, Depends from .cluster_type import ClusterType +from .cluster import Cluster from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool -from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema +from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema, ClusterSchema + +from netbox_proxbox.backend.session.netbox import NetboxSessionDep router = APIRouter() @@ -24,17 +27,47 @@ async def create_cluster_types( return await cluster_type.post(default, data) -from netbox_proxbox.backend.session.netbox import NetboxSessionDep -@router.get("/testing") -async def get_testing( +Depends +@router.get("/clusters") +async def get_clusters( nb: NetboxSessionDep, - app: str | None = None, - endpoint: str | None = None, -): - response_list = [] + cluster: Cluster = Depends(), +): + # Instantiate ClusterType class to get Cluster Type ID + cluster_type_obj = ClusterType(nb=nb) + type = await cluster_type_obj.get() - teste = getattr(getattr(nb.session, app), endpoint) - for item in teste.all(): - response_list.append(item) + default_extra_fields = { + "status": "active", + "type": type.id + } + + return await cluster.get(default_extra_fields) + - return response_list \ No newline at end of file +@router.post("/clusters") +async def create_cluster( + nb: NetboxSessionDep, + cluster: Cluster = Depends(), + default: CreateDefaultBool = False, + data: ClusterSchema = None, +): + """ + **default:** Boolean to define if Proxbox should create a default Cluster Type if there's no Cluster Type registered on Netbox.\n + **data:** JSON to create the Cluster Type on Netbox. You can create any Cluster Type you want, like a proxy to Netbox API. + """ + # Instantiate ClusterType class to get Cluster Type ID + cluster_type_obj = ClusterType(nb=nb) + type = await cluster_type_obj.get() + + if default: + default_extra_fields = { + "status": "active", + "type": type.id + } + + return await cluster.post(default, default_extra_fields) + + if data: + print(data) + return await cluster.post(data = data) \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py index e69de29..1f32596 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py @@ -0,0 +1,16 @@ +from fastapi import Depends, Query +from typing import Annotated + +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase + +class Cluster(NetboxBase): + # Default Cluster Type Params + default_name = "Proxbox Basic Cluster" + default_slug = "proxbox-basic-cluster-type" + default_description = "Proxbox Basic Cluster (used to identify the items the plugin created)" + + app = "virtualization" + endpoint = "clusters" + object_name = "Cluster" + + \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py index 2141a17..1c73c30 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py @@ -7,4 +7,5 @@ class ClusterType(NetboxBase): default_description = "Proxbox Basic Cluster Type (used to identify the items the plugin created)" app = "virtualization" - endpoint = "cluster_types" \ No newline at end of file + endpoint = "cluster_types" + object_name = "Cluster Type" \ No newline at end of file diff --git a/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py index 8edb2fe..1e86d97 100644 --- a/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py @@ -1,7 +1,8 @@ from pydantic import BaseModel from netbox_proxbox.backend.schemas.netbox.extras import TagSchema -from netbox_proxbox.backend.enum.netbox.dcim import StatusOptions +from netbox_proxbox.backend.enum.netbox.virtualization import ClusterStatusOptions + class ClusterTypeSchema(BaseModel): name: str @@ -9,4 +10,16 @@ class ClusterTypeSchema(BaseModel): description: str | None = None tags: list[TagSchema] | None = None custom_fields: dict | None = None + +class ClusterSchema(BaseModel): + name: str + type: int + group: int | None = None + site: int | None = None + status: ClusterStatusOptions + tenant: int | None = None + description: str | None = None + comments: str | None = None + tags: list[TagSchema] | None = None + custom_fields: dict | None = None \ No newline at end of file From 78487f6c79d0278791a0fa6d98feae831b29f3fc Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Wed, 22 Nov 2023 20:43:28 +0000 Subject: [PATCH 09/21] Started '_check_duplicate()' to be used by 'get()' and 'post()' methods of 'NetboxBase' class. --- .../backend/routes/netbox/generic.py | 166 ++++++++++++++---- .../routes/netbox/virtualization/__init__.py | 65 +++---- .../routes/netbox/virtualization/cluster.py | 31 +++- .../{proxbox.py => proxbox/__init__.py} | 0 .../routes/proxbox/clusters/__init__.py | 24 +++ .../schemas/netbox/virtualization/__init__.py | 2 +- netbox_proxbox/main.py | 10 +- 7 files changed, 231 insertions(+), 67 deletions(-) rename netbox_proxbox/backend/routes/{proxbox.py => proxbox/__init__.py} (100%) create mode 100644 netbox_proxbox/backend/routes/proxbox/clusters/__init__.py diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index 63f54b4..6cf0042 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -1,4 +1,4 @@ -from fastapi import Query +from fastapi import Query, Body from typing import Annotated @@ -34,14 +34,29 @@ def __init__( bool, Query(title="List All Objects", description="List all Objects registered on Netbox.") ] = False, - + default: Annotated[ + bool, + Query(title="Create Default Object", description="Create a default Object if there's no Object registered on Netbox.") + ] = False, + default_extra_fields: Annotated[ + dict, + Body(title="Extra Fields", description="Extra fields to be added to the default Object.") + ] = None, ): self.nb = nb self.id = id self.all = all - self.pynetbox_path = getattr(getattr(self.nb.session, self.app), self.endpoint) - self.extra_dict_fields = None + self.default = default + self.default_extra_fields = default_extra_fields + self.pynetbox_path = getattr(getattr(self.nb.session, self.app), self.endpoint) + self.default_dict = { + "name": self.default_name, + "slug": self.default_slug, + "description": self.default_description, + "tags": [self.nb.tag.id] + } + # Default Cluster Type Params default_name = None default_slug = None @@ -55,7 +70,6 @@ def __init__( async def get( self, - default_extra_fields: dict = None ): # 1. If 'id' provided, use it to get the Cluster Type from Netbox using it. if self.id: @@ -83,13 +97,11 @@ async def get( # 2. Check if there's any Cluster Type registered on Netbox. else: try: - response = self.pynetbox_path.all() - print(response, len) # 2.1. If there's no Cluster Type registered on Netbox, create a default one. - if len(response) == 0: + if self.pynetbox_path.count() == 0: print("2.1. If there's no Cluster Type registered on Netbox, create a default one.") - default_cluster_type_obj = await self.post(default=True, default_extra_fields=default_extra_fields) + default_cluster_type_obj = await self.post() return default_cluster_type_obj @@ -116,7 +128,7 @@ async def get( # 2.2.2. If it's not Proxbox one, create a default one. print("2.2.2. If it's not Proxbox one, create a default one.") - default_cluster_type_obj = await self.post(default=True) + default_cluster_type_obj = await self.post() return default_cluster_type_obj except Exception as error: @@ -127,28 +139,62 @@ async def get( async def post( self, - default: bool = False, - default_extra_fields: dict = None, - data: dict = None, + data = None, ): - base_dict_fields = { - "name": self.default_name, - "slug": self.default_slug, - "description": self.default_description, - "tags": [self.nb.tag.id] - } - - if default: + if self.default: try: - if default_extra_fields: - base_dict_fields.update(default_extra_fields) + # try: + # # Check if default object exists. + # check_duplicate = self.pynetbox_path.get( + # name = self.default_dict.get("name"), + # slug = self.default_dict.get("slug"), + # tags = [self.nb.tag.id] + # ) + + # if check_duplicate: + # return check_duplicate - response = self.pynetbox_path.create( - base_dict_fields - ) + # except ValueError as error: + # print(f"Mutiple objects returned.\n > {error}") + # try: + # check_duplicate = self.pynetbox_path.filter( + # name = self.default_dict.get("name"), + # slug = self.default_dict.get("slug"), + # tags = [self.nb.tag.id] + # ) + + # # Create list of all objects returned by filter. + # delete_list = [item for item in check_duplicate] + + # # Removes the first object from the list to return it. + # single_default = delete_list.pop(0) + + # # Delete all other objects from the list. + # self.pynetbox_path.delete(delete_list) + + # return single_default + + # except Exception as error: + # raise ProxboxException( + # message=f"Error trying to create default {self.object_name} on Netbox.", + # python_exception=f"{error}", + # detail=f"Multiple objects returned by filter. Please delete all objects with name '{self.default_dict.get('name')}' and slug '{self.default_dict.get('slug')}' and try again." + # ) + + # If default object doesn't exist, create it. + check_duplicate_result = await self._check_duplicate(object = self.default_dict) + if check_duplicate_result == None: + + # Create default object + response = self.pynetbox_path.create(self.default_dict) + return response + + # If duplicate object found, return it. + else: + return check_duplicate_result + + - return response - except Exception as error: raise ProxboxException( message=f"Error trying to create default {self.object_name} on Netbox.", @@ -156,11 +202,18 @@ async def post( ) if data: + print(data) try: + # Parse Pydantic model to Dict data_dict = data.model_dump(exclude_unset=True) - response = self.pynetbox_path.create(data_dict) - return response + check_duplicate_result = await self._check_duplicate(object = data_dict) + print(f"\n\ncheck_duplicate_result: {check_duplicate_result}\n\n") + if check_duplicate_result == None: + response = self.pynetbox_path.create(data_dict) + return response + else: + return check_duplicate_result except Exception as error: raise ProxboxException( @@ -168,3 +221,56 @@ async def post( detail=f"Payload provided: {data_dict}", python_exception=f"{error}" ) + + async def _check_duplicate(self, object: dict): + """ + Check if object exists on Netbox based on the dict provided. + The fields used to distinguish duplicates are: + - name + - slug + - tags + """ + + self.search_params = { + "name": object.get("name"), + "slug": object.get("slug"), + #"tags": [self.nb.tag.id] + } + + try: + print(f"search_params: {self.search_params}") + # Check if default object exists. + search_result = self.pynetbox_path.get(self.search_params) + + print(f"[get] search_result: {search_result}") + + if search_result: + return search_result + + except ValueError as error: + print(f"Mutiple objects returned.\n > {error}") + try: + + search_result = self.pynetbox_path.filter(self.search_params) + + # Create list of all objects returned by filter. + delete_list = [item for item in search_result] + + # Removes the first object from the list to return it. + single_default = delete_list.pop(0) + + # Delete all other objects from the list. + self.pynetbox_path.delete(delete_list) + + # Returns first element of the list. + print(f"[get] search_result: {search_result}") + return single_default + + except Exception as error: + raise ProxboxException( + message=f"Error trying to create default {self.object_name} on Netbox.", + python_exception=f"{error}", + detail=f"Multiple objects returned by filter. Please delete all objects with name '{self.default_dict.get('name')}' and slug '{self.default_dict.get('slug')}' and try again." + ) + + return None \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py index 55032d4..1dd64c7 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Body, Query +from typing import Annotated from .cluster_type import ClusterType from .cluster import Cluster @@ -11,63 +12,63 @@ router = APIRouter() @router.get("/cluster-types") -async def get_cluster_types( cluster_type: ClusterType = Depends() ): +async def get_cluster_types( all = None, cluster_type: ClusterType = Depends() ): + print(f"1 - all: {all}") return await cluster_type.get() @router.post("/cluster-types") async def create_cluster_types( cluster_type: ClusterType = Depends(), - default: CreateDefaultBool = False, - data: ClusterTypeSchema = None + default: Annotated[ + bool, + Body( + title="Create default object.", + description="Create a default object if there's no object registered on Netbox." + ) + ] = False, + data: Annotated[ + ClusterTypeSchema, + Body( + title="Netbox Cluster Type", + description="JSON to create the Cluster Type on Netbox. You can create any Cluster Type you want, like a proxy to Netbox API." + ) + ] = None ): """ **default:** Boolean to define if Proxbox should create a default Cluster Type if there's no Cluster Type registered on Netbox.\n **data:** JSON to create the Cluster Type on Netbox. You can create any Cluster Type you want, like a proxy to Netbox API. """ - return await cluster_type.post(default, data) + return await cluster_type.post(data) -Depends @router.get("/clusters") async def get_clusters( - nb: NetboxSessionDep, cluster: Cluster = Depends(), ): - # Instantiate ClusterType class to get Cluster Type ID - cluster_type_obj = ClusterType(nb=nb) - type = await cluster_type_obj.get() - - default_extra_fields = { - "status": "active", - "type": type.id - } - - return await cluster.get(default_extra_fields) + return await cluster.get() @router.post("/clusters") async def create_cluster( - nb: NetboxSessionDep, cluster: Cluster = Depends(), - default: CreateDefaultBool = False, + default: Annotated[ + bool, + Body( + title="Create default object.", + description="Create a default object if there's no object registered on Netbox." + ), + ] = False, data: ClusterSchema = None, ): """ **default:** Boolean to define if Proxbox should create a default Cluster Type if there's no Cluster Type registered on Netbox.\n **data:** JSON to create the Cluster Type on Netbox. You can create any Cluster Type you want, like a proxy to Netbox API. """ - # Instantiate ClusterType class to get Cluster Type ID - cluster_type_obj = ClusterType(nb=nb) - type = await cluster_type_obj.get() - + if default: - default_extra_fields = { - "status": "active", - "type": type.id - } - - return await cluster.post(default, default_extra_fields) - + return await cluster.post() + if data: - print(data) - return await cluster.post(data = data) \ No newline at end of file + print(f"create_cluster: {data}") + return await cluster.post(data = data) + \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py index 1f32596..b463227 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py @@ -1,9 +1,33 @@ -from fastapi import Depends, Query -from typing import Annotated - from netbox_proxbox.backend.routes.netbox.generic import NetboxBase +from .cluster_type import ClusterType + +from typing import Any class Cluster(NetboxBase): + # Extends NetboxBase.get() + + async def extra_fields(self): + type = await ClusterType(nb = self.nb).get() + + self.default_dict.update( + { + "status": "active", + "type": type.id + } + ) + + async def get(self): + if self.default: + await self.extra_fields() + + return await super().get() + + async def post(self, data: Any = None): + if self.default: + await self.extra_fields() + + return await super().post(data = data) + # Default Cluster Type Params default_name = "Proxbox Basic Cluster" default_slug = "proxbox-basic-cluster-type" @@ -12,5 +36,6 @@ class Cluster(NetboxBase): app = "virtualization" endpoint = "clusters" object_name = "Cluster" + \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/proxbox.py b/netbox_proxbox/backend/routes/proxbox/__init__.py similarity index 100% rename from netbox_proxbox/backend/routes/proxbox.py rename to netbox_proxbox/backend/routes/proxbox/__init__.py diff --git a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py new file mode 100644 index 0000000..cda7ebc --- /dev/null +++ b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Depends + +from typing import Annotated, Any + +# Import Both Proxmox Sessions and Netbox Session Dependencies +from netbox_proxbox.backend.session.proxmox import ProxmoxSessionsDep +from netbox_proxbox.backend.session.netbox import NetboxSessionDep + +router = APIRouter() + +@router.get("/") +async def proxbox_get_clusters( + pxs: ProxmoxSessionsDep, + nb: NetboxSessionDep +): + """Automatically sync Proxmox Clusters with Netbox Clusters""" + pass + # for px in pxs: + # nb. + # json_response.append( + # { + # px.name: px.session.version.get() + # } + # ) \ No newline at end of file diff --git a/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py index 1e86d97..4189c08 100644 --- a/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py @@ -20,6 +20,6 @@ class ClusterSchema(BaseModel): tenant: int | None = None description: str | None = None comments: str | None = None - tags: list[TagSchema] | None = None + tags: list[int] | None = None custom_fields: dict | None = None \ No newline at end of file diff --git a/netbox_proxbox/main.py b/netbox_proxbox/main.py index cf9f0f0..29194f7 100644 --- a/netbox_proxbox/main.py +++ b/netbox_proxbox/main.py @@ -12,6 +12,7 @@ # Proxbox Routes from .backend.routes.proxbox import router as proxbox_router +from .backend.routes.proxbox.clusters import router as pb_cluster_router # Proxmox Routes from .backend.routes.proxmox import router as proxmox_router @@ -41,15 +42,22 @@ async def proxmoxer_exception_handler(request: Request, exc: ProxboxException): # # Routes (Endpoints) # +# Netbox Routes app.include_router(netbox_router, prefix="/netbox", tags=["netbox"]) app.include_router(nb_dcim_router, prefix="/netbox/dcim", tags=["netbox / dcim"]) app.include_router(nb_virtualization_router, prefix="/netbox/virtualization", tags=["netbox / virtualization"]) -app.include_router(proxbox_router, prefix="/proxbox", tags=["proxbox"]) +# Proxmox Routes app.include_router(px_nodes_router, prefix="/proxmox/nodes", tags=["proxmox / nodes"]) app.include_router(px_cluster_router, prefix="/proxmox/cluster", tags=["proxmox / cluster"]) app.include_router(proxmox_router, prefix="/proxmox", tags=["proxmox"]) +# Proxbox Routes +app.include_router(proxbox_router, prefix="/proxbox", tags=["proxbox"]) +app.include_router(pb_cluster_router, prefix="/proxbox/clusters", tags=["proxbox / clusters"]) + + + From be877bb51970153073161544fa77e5e4c2448cad Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 23 Nov 2023 20:08:45 +0000 Subject: [PATCH 10/21] Added logging with colored messages --- netbox_proxbox/__init__.py | 2 + .../backend/routes/netbox/generic.py | 9 ++- netbox_proxbox/logging.py | 68 +++++++++++++++++++ netbox_proxbox/main.py | 6 +- 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 netbox_proxbox/logging.py diff --git a/netbox_proxbox/__init__.py b/netbox_proxbox/__init__.py index 4c885b2..c284400 100755 --- a/netbox_proxbox/__init__.py +++ b/netbox_proxbox/__init__.py @@ -40,3 +40,5 @@ class ProxboxConfig(PluginConfig): config = ProxboxConfig from . import proxbox_api + +from .logging import logger \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index 6cf0042..61f468f 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -5,6 +5,8 @@ from netbox_proxbox.backend.session.netbox import NetboxSessionDep from netbox_proxbox.backend.exception import ProxboxException +from netbox_proxbox import logger + class NetboxBase: """ Class to handle Netbox 'Objects'. @@ -56,7 +58,12 @@ def __init__( "description": self.default_description, "tags": [self.nb.tag.id] } - + logger.info(f"Teste: {self.__class__.__name__}") + logger.warning(f"Teste: {self.__class__.__name__}") + logger.critical(f"Teste: {self.__class__.__name__}") + logger.error(f"Teste: {self.__class__.__name__}") + logger.debug(f"Teste: {self.__class__.__name__}") + # Default Cluster Type Params default_name = None default_slug = None diff --git a/netbox_proxbox/logging.py b/netbox_proxbox/logging.py new file mode 100644 index 0000000..b164c40 --- /dev/null +++ b/netbox_proxbox/logging.py @@ -0,0 +1,68 @@ +import logging +from logging.handlers import TimedRotatingFileHandler + +import logging + +# ANSI escape sequences for colors +class AnsiColorCodes: + BLACK = '\033[30m' + RED = '\033[31m' + GREEN = '\033[32m' + YELLOW = '\033[33m' + BLUE = '\033[34m' + MAGENTA = '\033[35m' + CYAN = '\033[36m' + WHITE = '\033[37m' + RESET = '\033[0m' + DARK_GRAY = '\033[90m' + +class ColorizedFormatter(logging.Formatter): + LEVEL_COLORS = { + logging.DEBUG: AnsiColorCodes.BLUE, + logging.INFO: AnsiColorCodes.GREEN, + logging.WARNING: AnsiColorCodes.YELLOW, + logging.ERROR: AnsiColorCodes.RED, + logging.CRITICAL: AnsiColorCodes.MAGENTA + } + + def format(self, record): + color = self.LEVEL_COLORS.get(record.levelno, AnsiColorCodes.WHITE) + + #record.asctime = f"{AnsiColorCodes.CYAN}{record.asctime}{AnsiColorCodes.RESET}" + record.module = f"{AnsiColorCodes.DARK_GRAY}{record.module}{AnsiColorCodes.RESET}" + + record.levelname = f"{color}{record.levelname}{AnsiColorCodes.RESET}" + return super().format(record) + + +# Path to log file +log_path = '/var/log/proxbox.log' + +# Create a logger +logger = logging.getLogger('proxbox') +logger.setLevel(logging.DEBUG) + +# Create a console handler +console_handler = logging.StreamHandler() + +# Log all messages in the console +console_handler.setLevel(logging.DEBUG) + +# Create a formatter with colors +formatter = ColorizedFormatter('[%(asctime)s] [%(levelname)s] %(module)s: %(message)s') + +# Set the formatter for the console handler and file handler +console_handler.setFormatter(formatter) + +# Create a file handler +file_handler = TimedRotatingFileHandler(log_path, when='midnight', interval=1, backupCount=7) + +# Log only WARNINGS and above in the file +file_handler.setLevel(logging.WARNING) + +# Set the formatter for the file handler +file_handler.setFormatter(formatter) + +# Add the handlers to the logger +logger.addHandler(console_handler) +logger.addHandler(file_handler) \ No newline at end of file diff --git a/netbox_proxbox/main.py b/netbox_proxbox/main.py index 29194f7..445d84e 100644 --- a/netbox_proxbox/main.py +++ b/netbox_proxbox/main.py @@ -25,7 +25,11 @@ PROXBOX_PLUGIN_NAME = "netbox_proxbox" # Init FastAPI -app = FastAPI() +app = FastAPI( + title="Proxbox Backend", + description="## Proxbox Backend made in FastAPI framework", + version="0.0.1" +) @app.exception_handler(ProxboxException) async def proxmoxer_exception_handler(request: Request, exc: ProxboxException): From cc57e8ce71fba411ea902ea72c373db20051f5fb Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Fri, 8 Dec 2023 12:17:01 +0000 Subject: [PATCH 11/21] Saves work --- docs/{introduction.md => index.md} | 49 +--- docs/src/netboxbasic.md | 21 ++ mkdocs.yml | 90 +++--- netbox_proxbox/backend/exception.py | 8 +- .../backend/routes/netbox/dcim/__init__.py | 8 +- .../backend/routes/netbox/generic.py | 265 +++++++++++------- .../routes/netbox/virtualization/__init__.py | 1 + netbox_proxbox/logging.py | 57 ++-- requirements-docs.txt | 8 + 9 files changed, 290 insertions(+), 217 deletions(-) rename docs/{introduction.md => index.md} (88%) create mode 100644 docs/src/netboxbasic.md create mode 100644 requirements-docs.txt diff --git a/docs/introduction.md b/docs/index.md similarity index 88% rename from docs/introduction.md rename to docs/index.md index 4214055..b99ffa6 100644 --- a/docs/introduction.md +++ b/docs/index.md @@ -5,12 +5,6 @@ Proxbox logo
- -
- -### [New Documentation available!](https://proxbox.netbox.dev.br/) -
-
@@ -39,9 +33,8 @@ Proxbox is currently able to get the following information from Proxmox: --- -
- -### Versions + +### Version The following table shows the Netbox and Proxmox versions compatible (tested) with Proxbox plugin. @@ -53,44 +46,6 @@ The following table shows the Netbox and Proxmox versions compatible (tested) wi | >= v3.0.0 < v3.2 | >= v6.2.0 | =v0.0.3 | -
- ---- - -### Summary -[1. Installation](#1-installation) -- [1.1. Install package](#11-install-package) - - [1.1.1. Using pip (production use)](#111-using-pip-production-use---not-working-yet) - - [1.1.2. Using git (development use)](#112-using-git-development-use) -- [1.2. Enable the Plugin](#12-enable-the-plugin) -- [1.3. Configure Plugin](#13-configure-plugin) - - [1.3.1. Change Netbox 'configuration.py' to add PLUGIN parameters](#131-change-netbox-configurationpy-to-add-plugin-parameters) - - [1.3.2. Change Netbox 'settings.py' to include Proxbox Template directory](#132-change-netbox-settingspy-to-include-proxbox-template-directory) -- [1.4. Run Database Migrations](#14-run-database-migrations) -- [1.5 Restart WSGI Service](#15-restart-wsgi-service) - -[2. Configuration Parameters](#2-configuration-parameters) - -[3. Custom Fields](#3-custom-fields) -- [3.1. Custom Field Configuration](#31-custom-field-configuration) - - [3.1.1. Proxmox ID](#311-proxmox-id) - - [3.1.2. Proxmox Node](#312-proxmox-node) - - [3.1.3. Proxmox Type](#313-proxmox-type-qemu-or-lxc) - - [3.1.4. Proxmox Keep Interface](#314-proxmox-keep-interface) -- [3.2. Custom Field Example](#32-custom-field-example) - -[4. Usage](#4-usage) - -[5. Enable Logs](#5-enable-logs) - -[6. Contributing](#6-contributing) - -[7. Roadmap](#7-roadmap) - -[8. Get Help from Community!](#8-get-help-from-community) - ---- - ## 1. Installation The instructions below detail the process for installing and enabling Proxbox plugin. diff --git a/docs/src/netboxbasic.md b/docs/src/netboxbasic.md new file mode 100644 index 0000000..9eac917 --- /dev/null +++ b/docs/src/netboxbasic.md @@ -0,0 +1,21 @@ +# `NetboxBase` class + +::: netbox_proxbox.backend.routes.netbox.generic.NetboxBase + options: + docstring_options: + ignore_init_summary: true + merge_init_into_class: true + show_source: false + + +nb is a dependency injection of [NetboxSessionDep][netbox_proxbox.backend.session.netbox.NetboxSessionDep] + +!!! example "Teste" + + === "1" + ``` + teste 01 + ``` + + === "2" + teste 02 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 70f972d..b81b785 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: ProxBox Documentation (NetBox Plugin) +site_name: Proxbox Docs site_dir: ./site site_url: https://proxbox.netbox.dev.br/ docs_dir: ./docs @@ -13,11 +13,14 @@ theme: palette: - media: "(prefers-color-scheme: light)" scheme: default + primary: black + accent: deep-orange toggle: icon: material/lightbulb-outline name: Switch to Dark Mode - media: "(prefers-color-scheme: dark)" scheme: slate + primary: black toggle: icon: material/lightbulb name: Switch to Light Mode @@ -30,10 +33,60 @@ theme: - navigation.path - navigation.top - toc.follow - - toc.integrate + plugins: - search - social + - mkdocstrings: + handlers: + python: + options: + extensions: + - griffe_typingdoc + show_bases: true + #show_root_heading: true + show_if_no_docstring: true + inherited_members: true + #members_order: source + #separate_signature: true + #unwrap_annotated: true + #merge_init_into_class: true + #docstring_section_style: spacy + #signature_crossrefs: true + +nav: + - Proxbox: index.md + - Source Code: + - 'src/netboxbasic.md' + - Installing & Upgrade: + - Installing Proxbox: 'installation/index.md' + - Upgrading Proxbox: 'installation/upgrading.md' + - Features: + - Virtual Machine (VM): 'features/virtual-machine.md' + - Containers (LXC): 'features/containers.md' + - Network (IPAM): 'features/network.md' + - VLAN Management: 'features/vlan-management.md' + - Storage: 'features/storage.md' + - Backup: 'features/backup.md' + - Monitoring: 'features/monitoring.md' + - Synchronized Data: 'features/synchronized-data.md' + - Background Jobs: 'features/background-jobs.md' + - API & Integration: 'features/api-integration.md' + - Configuration: + - Configuring ProxBox: 'configuration/index.md' + - Required Parameters: 'configuration/required-parameters.md' + - Data Model: + - Virtual Machine (VM): 'models/virtual-machine.md' + - Containers (LXC): 'models/containers.md' + - Others: 'models/others.md' + - Release Notes: + - Summary: 'release-notes/index.md' + - Version 0.0.6: 'release-notes/version-0.0.1.md' + - Version 0.0.5: 'release-notes/version-0.0.1.md' + - Version 0.0.4: 'release-notes/version-0.0.1.md' + - Version 0.0.3: 'release-notes/version-0.0.1.md' + - Version 0.0.2: 'release-notes/version-0.0.1.md' + - Version 0.0.1: 'release-notes/version-0.0.1.md' extra: social: - icon: fontawesome/brands/github @@ -65,7 +118,7 @@ markdown_extensions: - tables - footnotes - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji + emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:materialx.emoji.to_svg - pymdownx.superfences: custom_fences: @@ -76,34 +129,3 @@ markdown_extensions: alternate_style: true extra_javascript: - 'https://cdn.jsdelivr.net/npm/@material-icons/iconfont/material-icons.min.js' -nav: - - Introduction: 'introduction.md' - - Installing & Upgrade: - - Installing Proxbox: 'installation/index.md' - - Upgrading Proxbox: 'installation/upgrading.md' - - Features: - - Virtual Machine (VM): 'features/virtual-machine.md' - - Containers (LXC): 'features/containers.md' - - Network (IPAM): 'features/network.md' - - VLAN Management: 'features/vlan-management.md' - - Storage: 'features/storage.md' - - Backup: 'features/backup.md' - - Monitoring: 'features/monitoring.md' - - Synchronized Data: 'features/synchronized-data.md' - - Background Jobs: 'features/background-jobs.md' - - API & Integration: 'features/api-integration.md' - - Configuration: - - Configuring ProxBox: 'configuration/index.md' - - Required Parameters: 'configuration/required-parameters.md' - - Data Model: - - Virtual Machine (VM): 'models/virtual-machine.md' - - Containers (LXC): 'models/containers.md' - - Others: 'models/others.md' - - Release Notes: - - Summary: 'release-notes/index.md' - - Version 0.0.6: 'release-notes/version-0.0.1.md' - - Version 0.0.5: 'release-notes/version-0.0.1.md' - - Version 0.0.4: 'release-notes/version-0.0.1.md' - - Version 0.0.3: 'release-notes/version-0.0.1.md' - - Version 0.0.2: 'release-notes/version-0.0.1.md' - - Version 0.0.1: 'release-notes/version-0.0.1.md' \ No newline at end of file diff --git a/netbox_proxbox/backend/exception.py b/netbox_proxbox/backend/exception.py index 1b2613a..8443aba 100644 --- a/netbox_proxbox/backend/exception.py +++ b/netbox_proxbox/backend/exception.py @@ -1,3 +1,5 @@ +from netbox_proxbox import logger + class ProxboxException(Exception): def __init__( self, @@ -7,4 +9,8 @@ def __init__( ): self.message = message self.detail = detail - self.python_exception = python_exception \ No newline at end of file + self.python_exception = python_exception + + logger.error(f"ProxboxException: {self.message} | Detail: {self.detail}") + + \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index e006d60..e1cead2 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -7,6 +7,7 @@ from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool from netbox_proxbox.backend.schemas.netbox.dcim import SitesSchema +from netbox_proxbox import logger # FastAPI Router router = APIRouter() @@ -15,7 +16,12 @@ # /sites routes # @router.get("/sites") -async def get_sites( site: Sites = Depends() ): return await site.get() +async def get_sites( + site: Sites = Depends() +): + print("sites") + logger.info("sites") + #return await site.get() @router.post("/sites") async def create_sites( diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index 61f468f..2f100d6 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -1,6 +1,7 @@ from fastapi import Query, Body from typing import Annotated +from typing_extensions import Doc from netbox_proxbox.backend.session.netbox import NetboxSessionDep from netbox_proxbox.backend.exception import ProxboxException @@ -9,18 +10,25 @@ class NetboxBase: """ - Class to handle Netbox 'Objects'. + ## Class to handle Netbox Objects. + + Warning: Deprecated + Stop using this class + + !!! Logic + - it will use `id` to get the `Objects` from Netbox if provided.\n + - if object is returned, it will return it.\n + - if object is not returned, it will raise an `ProxboxException`. + + - if 'site_id' is not provided, it will check if there's any Site registered on Netbox.\n + - if there's no 'Objects' registered on Netbox, it will create a default one.\n + - if there's any 'Objects' registered on Netbox, it will check if is Proxbox one by checking tag and name.\n + + - if it's Proxbox one, it will return it.\n + - if it's not Proxbox one, it will create a default one.\n + + - if 'all' is True, it will return all 'Objects' registered on Netbox. - Logic: - 1. it will use 'id' to get the 'Objects' from Netbox if provided. - 1.1. if object is returned, it will return it. - 1.2. if object is not returned, it will raise an `ProxboxException`. - 2. if 'site_id' is not provided, it will check if there's any Site registered on Netbox. - 2.1. if there's no 'Objects' registered on Netbox, it will create a default one. - 2.2. if there's any 'Objects' registered on Netbox, it will check if is Proxbox one by checking tag and name. - 2.2.1. if it's Proxbox one, it will return it. - 2.2.2. if it's not Proxbox one, it will create a default one. - 3. if 'all' is True, it will return all 'Objects' registered on Netbox. """ def __init__( @@ -44,11 +52,25 @@ def __init__( dict, Body(title="Extra Fields", description="Extra fields to be added to the default Object.") ] = None, + ignore_tag: Annotated[ + bool, + Query( + title="Ignore Proxbox Tag", + description="Ignore Proxbox tag filter when searching for objects in Netbox. This will return all Netbox objects, not only Proxbox related ones." + ), + Doc( + "Ignore Proxbox tag filter when searching for objects in Netbox. This will return all Netbox objects, not only Proxbox related ones." + ) + + ] = False, ): + print("Inside NetboxBase.__init__") + print(f"hasHandlers: {logger.hasHandlers()}") self.nb = nb self.id = id self.all = all self.default = default + self.ignore_tag = ignore_tag self.default_extra_fields = default_extra_fields self.pynetbox_path = getattr(getattr(self.nb.session, self.app), self.endpoint) @@ -58,28 +80,101 @@ def __init__( "description": self.default_description, "tags": [self.nb.tag.id] } - logger.info(f"Teste: {self.__class__.__name__}") - logger.warning(f"Teste: {self.__class__.__name__}") - logger.critical(f"Teste: {self.__class__.__name__}") - logger.error(f"Teste: {self.__class__.__name__}") - logger.debug(f"Teste: {self.__class__.__name__}") - - # Default Cluster Type Params + + + + # Default Object Parameters. + # It should be overwritten by the child class. default_name = None default_slug = None default_description = None - # Parameters to be used as Pynetbox class attributes + # Parameters to be used as Pynetbox class attributes. + # It should be overwritten by the child class. app = None endpoint = None object_name = None - + async def get( self, ): - # 1. If 'id' provided, use it to get the Cluster Type from Netbox using it. + if self.id: return await self._get_by_id() + if self.all: return await self._get_all() + + + + + # 2.1. If there's no Object registered on Netbox, create a default one. + if self.pynetbox_path.count() == 0: + print(f"2.1. If there's no {self.object_name} registered on Netbox, create a default one.") + + create_default_object = await self.post() + + if create_default_object != None: + logger.info(f"No objects found. Default '{self.object_name}' created successfully. {self.object_name} ID: {create_default_object.id}") + return create_default_object + + else: + print('teste') + logger.info("teste") + raise ProxboxException( + message=f"Error trying to create default {self.object_name} on Netbox.", + detail=f"No objects found. Default '{self.object_name}' could not be created." + ) + + + + + + + + + + # 2. Check if there's any 'Object' registered on Netbox. + try: + + # 2.2 + # 2.2.1 If there's any Cluster Type registered on Netbox, check if is Proxbox one by checking tag and name. + try: + get_object = self.pynetbox_path.get( + name=self.default_name, + slug=self.default_slug, + tags=[self.nb.tag.id] + ) + except ValueError as error: + get_object = await self._check_duplicate( + search_params = { + "name": self.default_name, + "slug": self.default_slug, + "tags": [self.nb.tag.id], + } + ) + + if get_object != None: + return get_object + + # 2.2.2. If it's not Proxbox one, create a default one. + print("2.2.2. If it's not Proxbox one, create a default one.") + default_object = await self.post() + return default_object + + except Exception as error: + raise ProxboxException( + message=f"Error trying to get '{self.object_name}' from Netbox.", + python_exception=f"{error}" + ) + + + + async def _get_by_id(self): + """ + If Query Parameter 'id' provided, use it to get the object from Netbox. + """ + logger.info(f"Searching {self.object_name} by ID {self.id}.") + if self.id: + logger.info(f"Searching object by ID {self.id}.") response = None try: @@ -87,12 +182,13 @@ async def get( except Exception as error: raise ProxboxException( - message=f"Error trying to get {self.object_name} from Netbox using the specified ID '{self.id}'.", + message=f"Error trying to get '{self.object_name}' from Netbox using the specified ID '{self.id}'.", error=f"{error}" ) # 1.1. Return found object. if response != None: + logger.info(f"{self.object_name} with ID '{self.id}' found on Netbox.") return response # 1.2. Raise ProxboxException if object is not found. @@ -101,92 +197,46 @@ async def get( message=f"{self.object_name} with ID '{self.id}' not found on Netbox." ) - # 2. Check if there's any Cluster Type registered on Netbox. - else: + + + + + async def _get_all(self): + """ + # If Query Parameter 'all' is True, return all Objects registered from Netbox. + """ + + if self.ignore_tag: try: - - # 2.1. If there's no Cluster Type registered on Netbox, create a default one. - if self.pynetbox_path.count() == 0: - print("2.1. If there's no Cluster Type registered on Netbox, create a default one.") - default_cluster_type_obj = await self.post() - - return default_cluster_type_obj - - else: - # 3. If Query param 'all' is True, return all Cluster Types registered. - if self.all: - response_list = [] - - for cluster_type in response: - response_list.append(cluster_type) - - return response_list - - # 2.2 - # 2.2.1 If there's any Cluster Type registered on Netbox, check if is Proxbox one by checking tag and name. - get_proxbox_cluster_type = self.pynetbox_path.get( - name=self.default_name, - slug=self.default_slug, - tags=[self.nb.tag.id] - ) - - if get_proxbox_cluster_type != None: - return get_proxbox_cluster_type - - # 2.2.2. If it's not Proxbox one, create a default one. - print("2.2.2. If it's not Proxbox one, create a default one.") - default_cluster_type_obj = await self.post() - return default_cluster_type_obj - + # If ignore_tag is True, return all objects from Netbox. + return [item for item in self.pynetbox_path.all()] except Exception as error: raise ProxboxException( - message=f"Error trying to get '{self.object_name}' from Netbox.", + message=f"Error trying to get all '{self.object_name}' from Netbox.", python_exception=f"{error}" ) - + + try: + # If ignore_tag is False, return only objects with Proxbox tag. + return [item for item in self.pynetbox_path.filter(tag = [self.nb.tag.slug])] + + except Exception as error: + raise ProxboxException( + message=f"Error trying to get all Proxbox '{self.object_name}' from Netbox.", + python_exception=f"{error}" + ) + + + + + + async def post( self, data = None, ): if self.default: try: - # try: - # # Check if default object exists. - # check_duplicate = self.pynetbox_path.get( - # name = self.default_dict.get("name"), - # slug = self.default_dict.get("slug"), - # tags = [self.nb.tag.id] - # ) - - # if check_duplicate: - # return check_duplicate - - # except ValueError as error: - # print(f"Mutiple objects returned.\n > {error}") - # try: - # check_duplicate = self.pynetbox_path.filter( - # name = self.default_dict.get("name"), - # slug = self.default_dict.get("slug"), - # tags = [self.nb.tag.id] - # ) - - # # Create list of all objects returned by filter. - # delete_list = [item for item in check_duplicate] - - # # Removes the first object from the list to return it. - # single_default = delete_list.pop(0) - - # # Delete all other objects from the list. - # self.pynetbox_path.delete(delete_list) - - # return single_default - - # except Exception as error: - # raise ProxboxException( - # message=f"Error trying to create default {self.object_name} on Netbox.", - # python_exception=f"{error}", - # detail=f"Multiple objects returned by filter. Please delete all objects with name '{self.default_dict.get('name')}' and slug '{self.default_dict.get('slug')}' and try again." - # ) # If default object doesn't exist, create it. check_duplicate_result = await self._check_duplicate(object = self.default_dict) @@ -229,7 +279,11 @@ async def post( python_exception=f"{error}" ) - async def _check_duplicate(self, object: dict): + async def _check_duplicate(self, search_params: dict = None): + name = search_params.get("name") + slug = search_params.get("slug") + + logger.info(f"Checking if {name} exists on Netbox.") """ Check if object exists on Netbox based on the dict provided. The fields used to distinguish duplicates are: @@ -238,16 +292,10 @@ async def _check_duplicate(self, object: dict): - tags """ - self.search_params = { - "name": object.get("name"), - "slug": object.get("slug"), - #"tags": [self.nb.tag.id] - } - try: - print(f"search_params: {self.search_params}") + print(f"search_params: {search_params}") # Check if default object exists. - search_result = self.pynetbox_path.get(self.search_params) + search_result = self.pynetbox_path.get(name = name, slug = slug) print(f"[get] search_result: {search_result}") @@ -255,10 +303,10 @@ async def _check_duplicate(self, object: dict): return search_result except ValueError as error: - print(f"Mutiple objects returned.\n > {error}") + logger.warning(f"Mutiple objects by get() returned. Proxbox will use filter(), delete duplicate objects and return only the first one.\n > {error}") try: - search_result = self.pynetbox_path.filter(self.search_params) + search_result = self.pynetbox_path.filter(name = name, slug = slug) # Create list of all objects returned by filter. delete_list = [item for item in search_result] @@ -275,6 +323,7 @@ async def _check_duplicate(self, object: dict): except Exception as error: raise ProxboxException( + message=f"Error trying to create default {self.object_name} on Netbox.", python_exception=f"{error}", detail=f"Multiple objects returned by filter. Please delete all objects with name '{self.default_dict.get('name')}' and slug '{self.default_dict.get('slug')}' and try again." diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py index 1dd64c7..f70ddd0 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py @@ -14,6 +14,7 @@ @router.get("/cluster-types") async def get_cluster_types( all = None, cluster_type: ClusterType = Depends() ): print(f"1 - all: {all}") + return await cluster_type.get() @router.post("/cluster-types") diff --git a/netbox_proxbox/logging.py b/netbox_proxbox/logging.py index b164c40..ca559d4 100644 --- a/netbox_proxbox/logging.py +++ b/netbox_proxbox/logging.py @@ -1,6 +1,3 @@ -import logging -from logging.handlers import TimedRotatingFileHandler - import logging # ANSI escape sequences for colors @@ -28,41 +25,49 @@ class ColorizedFormatter(logging.Formatter): def format(self, record): color = self.LEVEL_COLORS.get(record.levelno, AnsiColorCodes.WHITE) - #record.asctime = f"{AnsiColorCodes.CYAN}{record.asctime}{AnsiColorCodes.RESET}" record.module = f"{AnsiColorCodes.DARK_GRAY}{record.module}{AnsiColorCodes.RESET}" record.levelname = f"{color}{record.levelname}{AnsiColorCodes.RESET}" return super().format(record) +def setup_logger(): + # Path to log file + log_path = '/var/log/proxbox.log' + + # Create a logger + logger = logging.getLogger('proxbox') + + logger.propagate = False -# Path to log file -log_path = '/var/log/proxbox.log' + logger.setLevel(logging.DEBUG) -# Create a logger -logger = logging.getLogger('proxbox') -logger.setLevel(logging.DEBUG) + # # Create a console handler + console_handler = logging.StreamHandler() -# Create a console handler -console_handler = logging.StreamHandler() + # # Log all messages in the console + # console_handler.setLevel(logging.DEBUG) -# Log all messages in the console -console_handler.setLevel(logging.DEBUG) + # # Create a formatter with colors + # formatter = ColorizedFormatter('%(name)s [%(asctime)s] [%(levelname)-8s] %(module)s: %(message)s') + # #formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(module)s: %(message)s') + # # Set the formatter for the console handler and file handler + # console_handler.setFormatter(formatter) -# Create a formatter with colors -formatter = ColorizedFormatter('[%(asctime)s] [%(levelname)s] %(module)s: %(message)s') + # # Create a file handler + # file_handler = TimedRotatingFileHandler(log_path, when='midnight', interval=1, backupCount=7) -# Set the formatter for the console handler and file handler -console_handler.setFormatter(formatter) + # # Log only WARNINGS and above in the file + # file_handler.setLevel(logging.WARNING) -# Create a file handler -file_handler = TimedRotatingFileHandler(log_path, when='midnight', interval=1, backupCount=7) + # # Set the formatter for the file handler + # file_handler.setFormatter(formatter) -# Log only WARNINGS and above in the file -file_handler.setLevel(logging.WARNING) + # # Add the handlers to the logger + logger.addHandler(console_handler) + # logger.addHandler(file_handler) + + + return logger -# Set the formatter for the file handler -file_handler.setFormatter(formatter) -# Add the handlers to the logger -logger.addHandler(console_handler) -logger.addHandler(file_handler) \ No newline at end of file +logger = setup_logger() \ No newline at end of file diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..7bd3497 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,8 @@ +griffe==0.38.0 +griffe-typingdoc==0.2.4 +mkdocs==1.5.3 +mkdocs-autorefs==0.5.0 +mkdocs-material==9.4.8 +mkdocs-material-extensions==1.3 +mkdocstrings==0.23.0 +mkdocstrings-python-legacy==0.2.3 \ No newline at end of file From d7a5e63db073741f8a60c8d05a9c8b6a9c429dc6 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Wed, 13 Dec 2023 15:30:59 +0000 Subject: [PATCH 12/21] Adjust default behavior when creating objects and adds detailed log messages --- netbox_proxbox/__init__.py | 4 +- netbox_proxbox/backend/exception.py | 13 +- netbox_proxbox/{ => backend}/logging.py | 32 ++--- .../backend/routes/netbox/dcim/__init__.py | 6 +- .../backend/routes/netbox/generic.py | 124 +++++++++++++----- netbox_proxbox/urls.py | 10 +- netbox_proxbox/views.py | 45 ++++--- 7 files changed, 154 insertions(+), 80 deletions(-) rename netbox_proxbox/{ => backend}/logging.py (62%) diff --git a/netbox_proxbox/__init__.py b/netbox_proxbox/__init__.py index c284400..2e8f129 100755 --- a/netbox_proxbox/__init__.py +++ b/netbox_proxbox/__init__.py @@ -39,6 +39,4 @@ class ProxboxConfig(PluginConfig): config = ProxboxConfig -from . import proxbox_api - -from .logging import logger \ No newline at end of file +from . import proxbox_api \ No newline at end of file diff --git a/netbox_proxbox/backend/exception.py b/netbox_proxbox/backend/exception.py index 8443aba..136278b 100644 --- a/netbox_proxbox/backend/exception.py +++ b/netbox_proxbox/backend/exception.py @@ -1,4 +1,4 @@ -from netbox_proxbox import logger +from netbox_proxbox.backend.logging import logger class ProxboxException(Exception): def __init__( @@ -11,6 +11,15 @@ def __init__( self.detail = detail self.python_exception = python_exception - logger.error(f"ProxboxException: {self.message} | Detail: {self.detail}") + log_message=f"ProxboxException: {self.message}" + + if self.detail: + log_message+=f"\n > Detail: {self.detail}" + + if self.python_exception: + log_message+=f"\n > Python Exception: {self.python_exception}" + + + logger.error(log_message) \ No newline at end of file diff --git a/netbox_proxbox/logging.py b/netbox_proxbox/backend/logging.py similarity index 62% rename from netbox_proxbox/logging.py rename to netbox_proxbox/backend/logging.py index ca559d4..1a779f6 100644 --- a/netbox_proxbox/logging.py +++ b/netbox_proxbox/backend/logging.py @@ -1,4 +1,5 @@ import logging +from logging.handlers import TimedRotatingFileHandler # ANSI escape sequences for colors class AnsiColorCodes: @@ -37,35 +38,34 @@ def setup_logger(): # Create a logger logger = logging.getLogger('proxbox') - logger.propagate = False - logger.setLevel(logging.DEBUG) # # Create a console handler console_handler = logging.StreamHandler() - # # Log all messages in the console - # console_handler.setLevel(logging.DEBUG) + # Log all messages in the console + console_handler.setLevel(logging.DEBUG) - # # Create a formatter with colors - # formatter = ColorizedFormatter('%(name)s [%(asctime)s] [%(levelname)-8s] %(module)s: %(message)s') - # #formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(module)s: %(message)s') - # # Set the formatter for the console handler and file handler - # console_handler.setFormatter(formatter) + # Create a formatter with colors + formatter = ColorizedFormatter('%(name)s [%(asctime)s] [%(levelname)-8s] %(module)s: %(message)s') + #formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(module)s: %(message)s') + # Set the formatter for the console handler and file handler + console_handler.setFormatter(formatter) - # # Create a file handler - # file_handler = TimedRotatingFileHandler(log_path, when='midnight', interval=1, backupCount=7) + # Create a file handler + file_handler = TimedRotatingFileHandler(log_path, when='midnight', interval=1, backupCount=7) - # # Log only WARNINGS and above in the file - # file_handler.setLevel(logging.WARNING) + # Log only WARNINGS and above in the file + file_handler.setLevel(logging.WARNING) - # # Set the formatter for the file handler - # file_handler.setFormatter(formatter) + # Set the formatter for the file handler + file_handler.setFormatter(formatter) - # # Add the handlers to the logger + # Add the handlers to the logger logger.addHandler(console_handler) # logger.addHandler(file_handler) + logger.propagate = False return logger diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index e1cead2..26a4799 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -7,7 +7,7 @@ from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool from netbox_proxbox.backend.schemas.netbox.dcim import SitesSchema -from netbox_proxbox import logger +from netbox_proxbox.backend.logging import logger # FastAPI Router router = APIRouter() @@ -19,9 +19,7 @@ async def get_sites( site: Sites = Depends() ): - print("sites") - logger.info("sites") - #return await site.get() + return await site.get() @router.post("/sites") async def create_sites( diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index 2f100d6..fdd3bd6 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -6,15 +6,12 @@ from netbox_proxbox.backend.session.netbox import NetboxSessionDep from netbox_proxbox.backend.exception import ProxboxException -from netbox_proxbox import logger +from netbox_proxbox.backend.logging import logger class NetboxBase: """ ## Class to handle Netbox Objects. - Warning: Deprecated - Stop using this class - !!! Logic - it will use `id` to get the `Objects` from Netbox if provided.\n - if object is returned, it will return it.\n @@ -64,8 +61,6 @@ def __init__( ] = False, ): - print("Inside NetboxBase.__init__") - print(f"hasHandlers: {logger.hasHandlers()}") self.nb = nb self.id = id self.all = all @@ -99,65 +94,65 @@ def __init__( async def get( self, ): + logger.info(f"[GET] Getting '{self.object_name}' from Netbox.") + if self.id: return await self._get_by_id() if self.all: return await self._get_all() - - - - # 2.1. If there's no Object registered on Netbox, create a default one. + if self.pynetbox_path.count() == 0: - print(f"2.1. If there's no {self.object_name} registered on Netbox, create a default one.") - + + logger.info(f"[GET] There's no '{self.object_name}' registered on Netbox. Creating a DEFAULT ONE.") + + self.default = True create_default_object = await self.post() - + if create_default_object != None: - logger.info(f"No objects found. Default '{self.object_name}' created successfully. {self.object_name} ID: {create_default_object.id}") + logger.info(f"[GET] Default '{self.object_name}' created successfully. {self.object_name} ID: {create_default_object.id}") return create_default_object else: - print('teste') - logger.info("teste") raise ProxboxException( - message=f"Error trying to create default {self.object_name} on Netbox.", + message=f"[GET] Error trying to create default '{self.object_name}' on Netbox.", detail=f"No objects found. Default '{self.object_name}' could not be created." ) - - - - - - # 2. Check if there's any 'Object' registered on Netbox. try: # 2.2 - # 2.2.1 If there's any Cluster Type registered on Netbox, check if is Proxbox one by checking tag and name. + # 2.2.1 If there's any 'Object' registered on Netbox, check if is Proxbox one by checking tag and name. try: + logger.info(f"[GET] '{self.object_name}' found on Netbox. Checking if it's 'Proxbox' one...") get_object = self.pynetbox_path.get( name=self.default_name, slug=self.default_slug, - tags=[self.nb.tag.id] + tag=[self.nb.tag.slug] ) + except ValueError as error: + logger.warning(f"Mutiple objects by get() returned. Proxbox will use filter(), delete duplicate objects and return only the first one.\n > {error}") get_object = await self._check_duplicate( search_params = { "name": self.default_name, "slug": self.default_slug, - "tags": [self.nb.tag.id], + "tag": [self.nb.tag.slug], } ) if get_object != None: + logger.info(f"[GET] The '{self.object_name}' found is from 'Proxbox' (because it has the tag). Returning it.") return get_object # 2.2.2. If it's not Proxbox one, create a default one. - print("2.2.2. If it's not Proxbox one, create a default one.") + logger.info(f"[GET] The '{self.object_name}' object found IS NOT from 'Proxbox'. Creating a default one.") + self.default = True default_object = await self.post() return default_object + + except ProxboxException as error: raise error except Exception as error: raise ProxboxException( @@ -235,11 +230,14 @@ async def post( self, data = None, ): + #logger.info(f"Creating '{self.object_name}' object on Netbox.") if self.default: + + logger.info(f"[POST] Creating DEFAULT '{self.object_name}' object on Netbox.") try: # If default object doesn't exist, create it. - check_duplicate_result = await self._check_duplicate(object = self.default_dict) + check_duplicate_result = await self._check_duplicate() if check_duplicate_result == None: # Create default object @@ -251,10 +249,11 @@ async def post( return check_duplicate_result - + except ProxboxException as error: raise error + except Exception as error: raise ProxboxException( - message=f"Error trying to create default {self.object_name} on Netbox.", + message=f"[POST] Error trying to create DEFAULT '{self.object_name}' on Netbox.", python_exception=f"{error}" ) @@ -272,6 +271,8 @@ async def post( else: return check_duplicate_result + except ProxboxException as error: raise error + except Exception as error: raise ProxboxException( message=f"Error trying to create {self.object_name} on Netbox.", @@ -279,7 +280,66 @@ async def post( python_exception=f"{error}" ) - async def _check_duplicate(self, search_params: dict = None): + async def _check_duplicate(self, search_params: dict = None, object: dict = None): + + logger.info(f"[CHECK DUPLICATE] Checking if '{self.object_name}' exists on Netbox before creating it.") + + + if self.default: + logger.info("[CHECK DUPLICATE] Checking default object.") + try: + result = self.pynetbox_path.get( + name=self.default_name, + slug=self.default_slug, + tag=[self.nb.tag.slug] + ) + + + if result: + return result + + else: + # If no object found searching using tag, try to find without it, using just name and slug. + result = self.pynetbox_path.get( + name=self.default_name, + slug=self.default_slug, + ) + + + if result: + raise ProxboxException( + message=f"Default '{self.object_name}' with ID '{result.id}' found on Netbox, but without Proxbox tag. Please delete it (or add the tag) and try again.", + detail="Netbox does not allow duplicated names and/or slugs." + ) + + + create = self.pynetbox_path.create(self.default_dict) + return create + + except ProxboxException as error: raise error + + except Exception as error: + raise ProxboxException( + message=f"[POST] Error trying to create default {self.object_name} on Netbox.", + python_exception=f"{error}" + ) + + if object: + try: + result = self.pynetbox_path.get(object) + if result: + return result + else: + create = self.pynetbox_path.create(object) + return create + except: + raise ProxboxException( + message=f"Error trying to create {self.object_name} on Netbox.", + detail=f"Payload provided: {object}", + python_exception=f"{error}" + ) + + name = search_params.get("name") slug = search_params.get("slug") @@ -321,6 +381,8 @@ async def _check_duplicate(self, search_params: dict = None): print(f"[get] search_result: {search_result}") return single_default + except ProxboxException as error: raise error + except Exception as error: raise ProxboxException( diff --git a/netbox_proxbox/urls.py b/netbox_proxbox/urls.py index e0cf37e..e4be23a 100755 --- a/netbox_proxbox/urls.py +++ b/netbox_proxbox/urls.py @@ -19,14 +19,14 @@ path('telegram/', views.TelegramView, name='telegram'), # Base Views - path("list/", views.ProxmoxVMListView.as_view(), name="proxmoxvm_list"), + # path("list/", views.ProxmoxVMListView.as_view(), name="proxmoxvm_list"), # = plugins/netbox_proxmoxvm/ | example: plugins/netbox_proxmoxvm/1/ # ProxmoxVMView.as_view() - as.view() is need so that our view class can process requests. # as_view() takes request and returns well-formed response, that is a class based view. - path("/", views.ProxmoxVMView.as_view(), name="proxmoxvm"), - path("add/", views.ProxmoxVMCreateView.as_view(), name="proxmoxvm_add"), - path("/delete/", views.ProxmoxVMDeleteView.as_view(), name="proxmoxvm_delete"), - path("/edit/", views.ProxmoxVMEditView.as_view(), name="proxmoxvm_edit"), + # path("/", views.ProxmoxVMView.as_view(), name="proxmoxvm"), + # path("add/", views.ProxmoxVMCreateView.as_view(), name="proxmoxvm_add"), + # path("/delete/", views.ProxmoxVMDeleteView.as_view(), name="proxmoxvm_delete"), + # path("/edit/", views.ProxmoxVMEditView.as_view(), name="proxmoxvm_edit"), # Proxbox API full update path("full_update/", views.ProxmoxFullUpdate.as_view(), name="proxmoxvm_full_update"), diff --git a/netbox_proxbox/views.py b/netbox_proxbox/views.py index 0c959a6..a947a94 100755 --- a/netbox_proxbox/views.py +++ b/netbox_proxbox/views.py @@ -39,8 +39,8 @@ def get(self, request): plugin_configuration = configuration.PLUGINS_CONFIG default_config = ProxboxConfig.default_settings - print("plugin_configuration: ", plugin_configuration, "\n\n") - print("default_config: ", default_config) + # print("plugin_configuration: ", plugin_configuration, "\n\n") + # print("default_config: ", default_config) return render( request, @@ -52,7 +52,8 @@ def get(self, request): "default_config_json": json.dumps(default_config, indent=4) } ) - + + class ContributingView(View): """Contributing""" template_name = 'netbox_proxbox/contributing.html' @@ -288,20 +289,20 @@ class ProxmoxVMEditView(PermissionRequiredMixin, UpdateView): -import django_tables2 as tables +# import django_tables2 as tables -class ClusterStatus(tables.Table): - id = tables.Column(verbose_name="ID") - name = tables.Column(verbose_name="Name") - type = tables.Column(verbose_name="Type") - ip = tables.Column(verbose_name="IP") - level = tables.Column(verbose_name="Level") - local = tables.Column(verbose_name="Local") - nodeid = tables.Column(verbose_name="Node ID") - nodes = tables.Column(verbose_name="Nodes") - online = tables.Column(verbose_name="Online") - quorate = tables.Column(verbose_name="Quorate") - version = tables.Column(verbose_name="Version") +# class ClusterStatus(tables.Table): +# id = tables.Column(verbose_name="ID") +# name = tables.Column(verbose_name="Name") +# type = tables.Column(verbose_name="Type") +# ip = tables.Column(verbose_name="IP") +# level = tables.Column(verbose_name="Level") +# local = tables.Column(verbose_name="Local") +# nodeid = tables.Column(verbose_name="Node ID") +# nodes = tables.Column(verbose_name="Nodes") +# online = tables.Column(verbose_name="Online") +# quorate = tables.Column(verbose_name="Quorate") +# version = tables.Column(verbose_name="Version") class ProxmoxCluster(View): @@ -311,10 +312,15 @@ class ProxmoxCluster(View): domain = "http://localhost:8800" path = "/proxmox/cluster/status" + response = None + response_error = None + try: + response = requests.get(f"{domain}{path}").json() + except Exception as error: + response_error = error - response = requests.get(f"{domain}{path}").json() - table_cluster_01 = ClusterStatus(response[0].get("PVE-CLUSTER-02")) + #table_cluster_01 = ClusterStatus(response[0].get("PVE-CLUSTER-02")) """ try: response = requests.get(f"{domain}{path}").json() @@ -331,6 +337,7 @@ def get(self, request): self.template_name, { "response": self.response, - "table": self.table_cluster_01 + #"table": self.table_cluster_01 + "response_error": self.response_error } ) \ No newline at end of file From cdfb64258e01d791e27bc4fee820f0e112108358 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Wed, 13 Dec 2023 19:31:52 +0000 Subject: [PATCH 13/21] Increase corner cases handling when posting (creating) new objects using NetboxBase.post() method --- .../backend/routes/netbox/dcim/__init__.py | 17 +- .../backend/routes/netbox/generic.py | 191 ++++++++++++------ .../backend/schemas/netbox/dcim/__init__.py | 3 +- 3 files changed, 140 insertions(+), 71 deletions(-) diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index 26a4799..9641ab8 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Body from typing import Annotated @@ -24,11 +24,20 @@ async def get_sites( @router.post("/sites") async def create_sites( site: Sites = Depends(), - default: CreateDefaultBool = False, - data: SitesSchema = None + data: Annotated[SitesSchema, Body()] = None ): """ **default:** Boolean to define if Proxbox should create a default Site if there's no Site registered on Netbox.\n **data:** JSON to create the Site on Netbox. You can create any Site you want, like a proxy to Netbox API. """ - return await site.post(default, data) + + + # if default: + # print(type(default)) + # print(default) + # return await site(default=True).post() + + if data: + return await site.post(data) + + return await site.post() diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index fdd3bd6..abbc5e0 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -43,7 +43,7 @@ def __init__( ] = False, default: Annotated[ bool, - Query(title="Create Default Object", description="Create a default Object if there's no Object registered on Netbox.") + Query(title="Create Default Object", description="Create a default Object if there's no Object registered on Netbox."), ] = False, default_extra_fields: Annotated[ dict, @@ -166,31 +166,43 @@ async def _get_by_id(self): """ If Query Parameter 'id' provided, use it to get the object from Netbox. """ - logger.info(f"Searching {self.object_name} by ID {self.id}.") - if self.id: - logger.info(f"Searching object by ID {self.id}.") - response = None - - try: + logger.info(f"[GET] Searching '{self.object_name}' by ID {self.id}.") + + response = None + + try: + if self.ignore_tag: response = self.pynetbox_path.get(self.id) - except Exception as error: - raise ProxboxException( - message=f"Error trying to get '{self.object_name}' from Netbox using the specified ID '{self.id}'.", - error=f"{error}" + else: + response = self.pynetbox_path.get( + id=self.id, + tag=[self.nb.tag.slug] ) - + # 1.1. Return found object. if response != None: - logger.info(f"{self.object_name} with ID '{self.id}' found on Netbox.") + logger.info(f"[GET] '{self.object_name}' with ID '{self.id}' found on Netbox. Returning it.") return response # 1.2. Raise ProxboxException if object is not found. else: raise ProxboxException( - message=f"{self.object_name} with ID '{self.id}' not found on Netbox." + message=f"[GET]' {self.object_name}' with ID '{self.id}' not found on Netbox.", + detail=f"Please check if the ID provided is correct. If it is, please check if the object has the Proxbox tag. (You can use the 'ignore_tag' query parameter to ignore this check and return object without Proxbox tag)" ) + + + except ProxboxException as error: raise error + + except Exception as error: + raise ProxboxException( + message=f"[GET] Error trying to get '{self.object_name}' from Netbox using the specified ID '{self.id}'.", + python_exception=f"{error}" + ) + + @@ -258,17 +270,32 @@ async def post( ) if data: - print(data) try: - # Parse Pydantic model to Dict + logger.info(f"[POST] Creating '{self.object_name}' object on Netbox.") + + # Convert Pydantic model to Dict through 'model_dump' Pydantic method. data_dict = data.model_dump(exclude_unset=True) check_duplicate_result = await self._check_duplicate(object = data_dict) - print(f"\n\ncheck_duplicate_result: {check_duplicate_result}\n\n") + if check_duplicate_result == None: + + # Check if tags field exists on the payload and if true, append the Proxbox tag. If not, create it. + if data_dict.get("tags") == None: + data_dict["tags"] = [self.nb.tag.id] + else: + data_dict["tags"].append(self.nb.tag.id) + response = self.pynetbox_path.create(data_dict) - return response + + if response: + logger.info(f"[POST] '{self.object_name}' object created successfully. {self.object_name} ID: {response.id}") + return response + + else: + logger.error(f"[POST] '{self.object_name}' object could not be created.") else: + logger.info(f"[POST] '{self.object_name}' object already exists on Netbox. Returning it.") return check_duplicate_result except ProxboxException as error: raise error @@ -279,12 +306,16 @@ async def post( detail=f"Payload provided: {data_dict}", python_exception=f"{error}" ) + + raise ProxboxException( + message=f"[POST] No data provided to create '{self.object_name}' on Netbox.", + detail=f"Please provide a JSON payload to create the '{self.object_name}' on Netbox or set 'default' to 'Trsue' on Query Parameter to create a default one." + ) async def _check_duplicate(self, search_params: dict = None, object: dict = None): logger.info(f"[CHECK DUPLICATE] Checking if '{self.object_name}' exists on Netbox before creating it.") - if self.default: logger.info("[CHECK DUPLICATE] Checking default object.") try: @@ -308,13 +339,14 @@ async def _check_duplicate(self, search_params: dict = None, object: dict = None if result: raise ProxboxException( - message=f"Default '{self.object_name}' with ID '{result.id}' found on Netbox, but without Proxbox tag. Please delete it (or add the tag) and try again.", + message=f"[CHECK DUPLICATE] Default '{self.object_name}' with ID '{result.id}' found on Netbox, but without Proxbox tag. Please delete it (or add the tag) and try again.", detail="Netbox does not allow duplicated names and/or slugs." ) + return None - create = self.pynetbox_path.create(self.default_dict) - return create + # create = self.pynetbox_path.create(self.default_dict) + # return create except ProxboxException as error: raise error @@ -326,69 +358,98 @@ async def _check_duplicate(self, search_params: dict = None, object: dict = None if object: try: + logger.info("[CHECK DUPLICATE] (1) First attempt: Checking object making EXACT MATCH with the Payload provided...") result = self.pynetbox_path.get(object) + if result: + logger.info(f"[CHECK DUPLICATE] Object found on Netbox. Returning it.") return result + else: - create = self.pynetbox_path.create(object) - return create - except: + logger.info("[CHECK DUPLICATE] (2) Checking object using only NAME and SLUG provided by the Payload and also the PROXBOX TAG). If found, return it.") + result_by_tag = self.pynetbox_path.get( + name=object.get("name"), + slug=object.get("slug"), + tag=[self.nb.tag.slug] + ) + + if result_by_tag: + logger.info(f"[CHECK DUPLICATE] Object found on Netbox. Returning it.") + return result_by_tag + + else: + result_by_name_and_slug = self.pynetbox_path.get( + name=object.get("name"), + slug=object.get("slug"), + ) + + if result_by_name_and_slug: + raise ProxboxException( + message=f"[CHECK DUPLICATE] '{self.object_name}' with ID '{result_by_name_and_slug.id}' found on Netbox, but without Proxbox tag. Please delete it (or add the tag) and try again.", + detail="Netbox does not allow duplicated names and/or slugs." + ) + + return None + + except ProxboxException as error: raise error + + except Exception as error: raise ProxboxException( - message=f"Error trying to create {self.object_name} on Netbox.", + message=f"[CHECK DUPLICATE] Error trying to create {self.object_name} on Netbox.", detail=f"Payload provided: {object}", python_exception=f"{error}" ) - - name = search_params.get("name") - slug = search_params.get("slug") + return None + # name = search_params.get("name") + # slug = search_params.get("slug") - logger.info(f"Checking if {name} exists on Netbox.") - """ - Check if object exists on Netbox based on the dict provided. - The fields used to distinguish duplicates are: - - name - - slug - - tags - """ + # logger.info(f"Checking if {name} exists on Netbox.") + # """ + # Check if object exists on Netbox based on the dict provided. + # The fields used to distinguish duplicates are: + # - name + # - slug + # - tags + # """ - try: - print(f"search_params: {search_params}") - # Check if default object exists. - search_result = self.pynetbox_path.get(name = name, slug = slug) + # try: + # print(f"search_params: {search_params}") + # # Check if default object exists. + # search_result = self.pynetbox_path.get(name = name, slug = slug) - print(f"[get] search_result: {search_result}") + # print(f"[get] search_result: {search_result}") - if search_result: - return search_result + # if search_result: + # return search_result - except ValueError as error: - logger.warning(f"Mutiple objects by get() returned. Proxbox will use filter(), delete duplicate objects and return only the first one.\n > {error}") - try: + # except ValueError as error: + # logger.warning(f"Mutiple objects by get() returned. Proxbox will use filter(), delete duplicate objects and return only the first one.\n > {error}") + # try: - search_result = self.pynetbox_path.filter(name = name, slug = slug) + # search_result = self.pynetbox_path.filter(name = name, slug = slug) - # Create list of all objects returned by filter. - delete_list = [item for item in search_result] + # # Create list of all objects returned by filter. + # delete_list = [item for item in search_result] - # Removes the first object from the list to return it. - single_default = delete_list.pop(0) + # # Removes the first object from the list to return it. + # single_default = delete_list.pop(0) - # Delete all other objects from the list. - self.pynetbox_path.delete(delete_list) + # # Delete all other objects from the list. + # self.pynetbox_path.delete(delete_list) - # Returns first element of the list. - print(f"[get] search_result: {search_result}") - return single_default + # # Returns first element of the list. + # print(f"[get] search_result: {search_result}") + # return single_default - except ProxboxException as error: raise error + # except ProxboxException as error: raise error - except Exception as error: - raise ProxboxException( + # except Exception as error: + # raise ProxboxException( - message=f"Error trying to create default {self.object_name} on Netbox.", - python_exception=f"{error}", - detail=f"Multiple objects returned by filter. Please delete all objects with name '{self.default_dict.get('name')}' and slug '{self.default_dict.get('slug')}' and try again." - ) + # message=f"Error trying to create default {self.object_name} on Netbox.", + # python_exception=f"{error}", + # detail=f"Multiple objects returned by filter. Please delete all objects with name '{self.default_dict.get('name')}' and slug '{self.default_dict.get('slug')}' and try again." + # ) - return None \ No newline at end of file + # return None \ No newline at end of file diff --git a/netbox_proxbox/backend/schemas/netbox/dcim/__init__.py b/netbox_proxbox/backend/schemas/netbox/dcim/__init__.py index 17b59ce..3616a3a 100644 --- a/netbox_proxbox/backend/schemas/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/schemas/netbox/dcim/__init__.py @@ -1,6 +1,5 @@ from pydantic import BaseModel - from netbox_proxbox.backend.schemas.netbox.extras import TagSchema from netbox_proxbox.backend.enum.netbox.dcim import StatusOptions @@ -14,7 +13,7 @@ class SitesSchema(BaseModel): asns: list[int] | None = None time_zone: str | None = None description: str | None = None - tags: list[TagSchema] | None = None + tags: list[TagSchema | int] | None = None custom_fields: dict | None = None physical_address: str | None = None shipping_address: str | None = None From 90542b023b143ae46c57a974b0e03104ac7031a1 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Wed, 13 Dec 2023 20:18:36 +0000 Subject: [PATCH 14/21] '/proxbox/clusters' path successfully creating Cluster with its Cluster Type --- .../backend/routes/netbox/generic.py | 23 ++++++---- .../routes/proxbox/clusters/__init__.py | 43 +++++++++++++++---- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index abbc5e0..2943faa 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -242,7 +242,6 @@ async def post( self, data = None, ): - #logger.info(f"Creating '{self.object_name}' object on Netbox.") if self.default: logger.info(f"[POST] Creating DEFAULT '{self.object_name}' object on Netbox.") @@ -273,20 +272,21 @@ async def post( try: logger.info(f"[POST] Creating '{self.object_name}' object on Netbox.") - # Convert Pydantic model to Dict through 'model_dump' Pydantic method. - data_dict = data.model_dump(exclude_unset=True) + if isinstance(data, dict) == False: + # Convert Pydantic model to Dict through 'model_dump' Pydantic method. + data = data.model_dump(exclude_unset=True) - check_duplicate_result = await self._check_duplicate(object = data_dict) + check_duplicate_result = await self._check_duplicate(object = data) if check_duplicate_result == None: # Check if tags field exists on the payload and if true, append the Proxbox tag. If not, create it. - if data_dict.get("tags") == None: - data_dict["tags"] = [self.nb.tag.id] + if data.get("tags") == None: + data["tags"] = [self.nb.tag.id] else: - data_dict["tags"].append(self.nb.tag.id) + data["tags"].append(self.nb.tag.id) - response = self.pynetbox_path.create(data_dict) + response = self.pynetbox_path.create(data) if response: logger.info(f"[POST] '{self.object_name}' object created successfully. {self.object_name} ID: {response.id}") @@ -303,7 +303,7 @@ async def post( except Exception as error: raise ProxboxException( message=f"Error trying to create {self.object_name} on Netbox.", - detail=f"Payload provided: {data_dict}", + detail=f"Payload provided: {data}", python_exception=f"{error}" ) @@ -312,6 +312,11 @@ async def post( detail=f"Please provide a JSON payload to create the '{self.object_name}' on Netbox or set 'default' to 'Trsue' on Query Parameter to create a default one." ) + + + + + async def _check_duplicate(self, search_params: dict = None, object: dict = None): logger.info(f"[CHECK DUPLICATE] Checking if '{self.object_name}' exists on Netbox before creating it.") diff --git a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py index cda7ebc..2a00f4d 100644 --- a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py +++ b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py @@ -6,6 +6,10 @@ from netbox_proxbox.backend.session.proxmox import ProxmoxSessionsDep from netbox_proxbox.backend.session.netbox import NetboxSessionDep + +from netbox_proxbox.backend.routes.netbox.virtualization.cluster_type import ClusterType +from netbox_proxbox.backend.routes.netbox.virtualization.cluster import Cluster + router = APIRouter() @router.get("/") @@ -13,12 +17,35 @@ async def proxbox_get_clusters( pxs: ProxmoxSessionsDep, nb: NetboxSessionDep ): + + """Automatically sync Proxmox Clusters with Netbox Clusters""" - pass - # for px in pxs: - # nb. - # json_response.append( - # { - # px.name: px.session.version.get() - # } - # ) \ No newline at end of file + + result = [] + + for px in pxs: + + cluster_type_name = f"Proxmox {px.mode.capitalize()}" + cluster_type_slug = f"proxmox-{px.mode}" + + cluster_type_obj = await ClusterType(nb = nb).post( + data = { + "name": cluster_type_name, + "slug": cluster_type_slug, + "description": f"Proxmox Cluster '{px.name}'" + } + ) + + cluster_obj = await Cluster(nb = nb).post( + data = { + "name": px.name, + "slug": px.name, + "type": cluster_type_obj["id"], + "status": "active", + } + ) + + result.append(cluster_obj) + + return result + \ No newline at end of file From fc61a60dcaec7b45cd4807ddc5794eb5d2fce4aa Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 14 Dec 2023 19:10:54 +0000 Subject: [PATCH 15/21] Add 'Device Types', 'Device Roles', 'Manufacturers' objects types to /netbox/dcim --- .../backend/routes/netbox/dcim/__init__.py | 37 +++++++++++++++-- .../routes/netbox/dcim/device_roles.py | 35 ++++++++++++++++ .../routes/netbox/dcim/device_types.py | 41 +++++++++++++++++++ .../backend/routes/netbox/dcim/devices.py | 10 +++++ .../routes/netbox/dcim/manufacturers.py | 10 +++++ .../backend/routes/netbox/dcim/sites.py | 2 +- .../routes/netbox/virtualization/cluster.py | 3 +- .../routes/proxbox/clusters/__init__.py | 2 + setup.py | 2 +- 9 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 netbox_proxbox/backend/routes/netbox/dcim/device_roles.py create mode 100644 netbox_proxbox/backend/routes/netbox/dcim/device_types.py create mode 100644 netbox_proxbox/backend/routes/netbox/dcim/devices.py create mode 100644 netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index 9641ab8..4ddbd55 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -2,7 +2,11 @@ from typing import Annotated -from .sites import Sites +from .sites import Site +from .devices import Device +from .device_types import DeviceType +from .device_roles import DeviceRole +from .manufacturers import Manufacturer from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool from netbox_proxbox.backend.schemas.netbox.dcim import SitesSchema @@ -17,13 +21,13 @@ # @router.get("/sites") async def get_sites( - site: Sites = Depends() + site: Site = Depends() ): return await site.get() @router.post("/sites") async def create_sites( - site: Sites = Depends(), + site: Site = Depends(), data: Annotated[SitesSchema, Body()] = None ): """ @@ -41,3 +45,30 @@ async def create_sites( return await site.post(data) return await site.post() + +# +# /devices routes +# +@router.get("/devices") +async def get_devices( + device: Device = Depends() +): + return await device.get() + +@router.get("/manufacturers") +async def get_manufacturers( + manufacturer: Manufacturer = Depends() +): + return await manufacturer.get() + +@router.get("/device-types") +async def get_device_types( + device_type: DeviceType = Depends() +): + return await device_type.get() + +@router.get("/device-roles") +async def get_device_roles( + device_role: DeviceRole = Depends() +): + return await device_role.get() \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py b/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py new file mode 100644 index 0000000..e7c8991 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py @@ -0,0 +1,35 @@ +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase +from typing import Any + +class DeviceRole(NetboxBase): + + async def extra_fields(self): + + self.default_dict = { + "name": self.default_name, + "slug": self.default_slug, + "color": "ff5722", + "vm_role": False, + "description": self.default_description, + "tags": [self.nb.tag.id] + } + + async def get(self): + if self.default: + await self.extra_fields() + + return await super().get() + + async def post(self, data: Any = None): + if self.default: + await self.extra_fields() + + return await super().post(data = data) + + default_name = "Proxmox Node (Server)" + default_slug = "proxbox-node" + default_description = "Proxbox Basic Manufacturer" + + app = "dcim" + endpoint = "device_roles" + object_name = "Device Types" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/device_types.py b/netbox_proxbox/backend/routes/netbox/dcim/device_types.py new file mode 100644 index 0000000..15e6211 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/dcim/device_types.py @@ -0,0 +1,41 @@ +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase +from .manufacturers import Manufacturer + +from typing import Any + +class DeviceType(NetboxBase): + async def extra_fields(self): + manufacturer = await Manufacturer(nb = self.nb).get() + + + # Replaces the default_dict variable + self.default_dict = { + "model": "Proxbox Basic Device Type", + "slug": self.default_slug, + "manufacturer": manufacturer.id, + "description": self.default_description, + "u_height": 1, + "tags": [self.nb.tag.id] + } + + + async def get(self): + if self.default: + await self.extra_fields() + + return await super().get() + + async def post(self, data: Any = None): + if self.default: + await self.extra_fields() + + return await super().post(data = data) + + # Default Device Type Params + default_name = "Proxbox Basic Device Type" + default_slug = "proxbox-basic-device-type" + default_description = "Proxbox Basic Device Type" + + app = "dcim" + endpoint = "device_types" + object_name = "Device Types" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/devices.py b/netbox_proxbox/backend/routes/netbox/dcim/devices.py new file mode 100644 index 0000000..96d9502 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/dcim/devices.py @@ -0,0 +1,10 @@ +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase + +class Device(NetboxBase): + default_name = "Proxbox Basic Device" + default_slug = "proxbox-basic-device" + default_description = "Proxbox Basic Device" + + app = "dcim" + endpoint = "devices" + object_name = "Device" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py b/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py new file mode 100644 index 0000000..470d7a0 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py @@ -0,0 +1,10 @@ +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase + +class Manufacturer(NetboxBase): + default_name = "Proxbox Basic Manufacturer" + default_slug = "proxbox-basic-manufacturer" + default_description = "Proxbox Basic Manufacturer" + + app = "dcim" + endpoint = "manufacturers" + object_name = "Manufacturer" \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/sites.py b/netbox_proxbox/backend/routes/netbox/dcim/sites.py index 1ae853b..422edca 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/sites.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/sites.py @@ -1,6 +1,6 @@ from netbox_proxbox.backend.routes.netbox.generic import NetboxBase -class Sites(NetboxBase): +class Site(NetboxBase): default_name = "Proxbox Basic Site" default_slug = "proxbox-basic-site" default_description = "Proxbox Basic Site (used to identify the items the plugin created)" diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py index b463227..c7bac67 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py @@ -4,8 +4,7 @@ from typing import Any class Cluster(NetboxBase): - # Extends NetboxBase.get() - + async def extra_fields(self): type = await ClusterType(nb = self.nb).get() diff --git a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py index 2a00f4d..a9e71f8 100644 --- a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py +++ b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py @@ -45,6 +45,8 @@ async def proxbox_get_clusters( } ) + print(px.cluster_status) + result.append(cluster_obj) return result diff --git a/setup.py b/setup.py index ffe9a4a..96905bb 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ 'invoke', 'requests>=2', 'pynetbox>=5', - 'paramiko>=2', 'proxmoxer>=1', 'fastapi[all]', 'starlette', @@ -34,6 +33,7 @@ 'ujson>=5.7.0', 'orjson>=3.8.9', 'httpcore', + 'pydantic', ] dev_requires = [ From b154ea8820e750fa838f86b9c98a8fc956a4ca8a Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Thu, 14 Dec 2023 19:31:59 +0000 Subject: [PATCH 16/21] /netbox/dcim/devices correctly adding default Device object --- .../backend/routes/netbox/dcim/devices.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/netbox_proxbox/backend/routes/netbox/dcim/devices.py b/netbox_proxbox/backend/routes/netbox/dcim/devices.py index 96d9502..6eafd19 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/devices.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/devices.py @@ -1,6 +1,38 @@ from netbox_proxbox.backend.routes.netbox.generic import NetboxBase +from typing import Any + +from .sites import Site +from .device_types import DeviceType +from .device_roles import DeviceRole class Device(NetboxBase): + + async def extra_fields(self): + site = await Site(nb = self.nb).get() + role = await DeviceRole(nb = self.nb).get() + device_type = await DeviceType(nb = self.nb).get() + + self.default_dict.update( + { + "status": "active", + "site": site.id, + "role": role.id, + "device_type": device_type.id, + } + ) + + async def get(self): + if self.default: + await self.extra_fields() + + return await super().get() + + async def post(self, data: Any = None): + if self.default: + await self.extra_fields() + + return await super().post(data = data) + default_name = "Proxbox Basic Device" default_slug = "proxbox-basic-device" default_description = "Proxbox Basic Device" From da763af65f4aed0ba8f74528519fc25b54b73c05 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Fri, 15 Dec 2023 20:27:30 +0000 Subject: [PATCH 17/21] Add base_dict var and minor changes to use | and |= dict merge syntax --- netbox_proxbox/backend/__init__.py | 7 ++ .../backend/routes/netbox/dcim/__init__.py | 15 ++++ .../routes/netbox/dcim/device_roles.py | 11 ++- .../routes/netbox/dcim/manufacturers.py | 9 ++- .../backend/routes/netbox/generic.py | 32 ++++++++- .../routes/netbox/virtualization/cluster.py | 4 +- .../routes/proxbox/clusters/__init__.py | 71 ++++++++++++++++++- 7 files changed, 140 insertions(+), 9 deletions(-) diff --git a/netbox_proxbox/backend/__init__.py b/netbox_proxbox/backend/__init__.py index e69de29..09e9d98 100644 --- a/netbox_proxbox/backend/__init__.py +++ b/netbox_proxbox/backend/__init__.py @@ -0,0 +1,7 @@ +from netbox_proxbox.backend.routes.netbox.virtualization.cluster_type import ClusterType +from netbox_proxbox.backend.routes.netbox.virtualization.cluster import Cluster + +from netbox_proxbox.backend.routes.netbox.dcim.sites import Site +from netbox_proxbox.backend.routes.netbox.dcim.device_roles import DeviceRole +from netbox_proxbox.backend.routes.netbox.dcim.device_types import DeviceType +from netbox_proxbox.backend.routes.netbox.dcim.devices import Device \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index 4ddbd55..adba257 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -61,6 +61,21 @@ async def get_manufacturers( ): return await manufacturer.get() +@router.post("/manufacturers") +async def create_manufacturers( + manufacturer: Manufacturer = Depends(), + data: Annotated[dict, Body()] = None +): + """ + **default:** Boolean to define if Proxbox should create a default Manufacturer if there's no Manufacturer registered on Netbox.\n + **data:** JSON to create the Manufacturer on Netbox. You can create any Manufacturer you want, like a proxy to Netbox API. + """ + + if data: + return await manufacturer.post(data) + + return await manufacturer.post() + @router.get("/device-types") async def get_device_types( device_type: DeviceType = Depends() diff --git a/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py b/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py index e7c8991..906bb5c 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py @@ -32,4 +32,13 @@ async def post(self, data: Any = None): app = "dcim" endpoint = "device_roles" - object_name = "Device Types" \ No newline at end of file + object_name = "Device Types" + + base_dict = { + "name": default_name, + "slug": default_slug, + "color": "ff5722", + "vm_role": False, + "description": default_description, + "tags": [self.nb.tag.id] + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py b/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py index 470d7a0..44d5069 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py @@ -1,10 +1,17 @@ from netbox_proxbox.backend.routes.netbox.generic import NetboxBase class Manufacturer(NetboxBase): + default_name = "Proxbox Basic Manufacturer" default_slug = "proxbox-basic-manufacturer" default_description = "Proxbox Basic Manufacturer" app = "dcim" endpoint = "manufacturers" - object_name = "Manufacturer" \ No newline at end of file + object_name = "Manufacturer" + + base_dict = { + "name": default_name, + "slug": default_slug, + "description": default_description, + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index 2943faa..dfec6fb 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -69,6 +69,7 @@ def __init__( self.default_extra_fields = default_extra_fields self.pynetbox_path = getattr(getattr(self.nb.session, self.app), self.endpoint) + self.default_dict = { "name": self.default_name, "slug": self.default_slug, @@ -77,6 +78,8 @@ def __init__( } + # New Implementantion of "default_dict" amd "default_extra_fields". + base_dict = None # Default Object Parameters. # It should be overwritten by the child class. @@ -93,12 +96,16 @@ def __init__( async def get( self, + **kwargs ): + print(kwargs) logger.info(f"[GET] Getting '{self.object_name}' from Netbox.") if self.id: return await self._get_by_id() if self.all: return await self._get_all() + if kwargs: return await self._get_by_kwargs(**kwargs) + if self.pynetbox_path.count() == 0: @@ -160,7 +167,23 @@ async def get( python_exception=f"{error}" ) - + async def _get_by_kwargs(self, **kwargs): + + logger.info(f"[GET] Searching '{self.object_name}' by kwargs {kwargs}.") + try: + response = self.pynetbox_path.get(**kwargs) + return response + + except ProxboxException as error: raise error + + except Exception as error: + raise ProxboxException( + message=f"[GET] Error trying to get '{self.object_name}' from Netbox using the specified kwargs '{kwargs}'.", + python_exception=f"{error}" + ) + + + async def _get_by_id(self): """ @@ -241,7 +264,7 @@ async def _get_all(self): async def post( self, data = None, - ): + ): if self.default: logger.info(f"[POST] Creating DEFAULT '{self.object_name}' object on Netbox.") @@ -276,6 +299,11 @@ async def post( # Convert Pydantic model to Dict through 'model_dump' Pydantic method. data = data.model_dump(exclude_unset=True) + if self.base_dict: + + # Merge base_dict and data dict. + data = self.base_dict | data + check_duplicate_result = await self._check_duplicate(object = data) if check_duplicate_result == None: diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py index c7bac67..01ceab9 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py @@ -15,11 +15,11 @@ async def extra_fields(self): } ) - async def get(self): + async def get(self, **kwargs): if self.default: await self.extra_fields() - return await super().get() + return await super().get(**kwargs) async def post(self, data: Any = None): if self.default: diff --git a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py index a9e71f8..010de33 100644 --- a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py +++ b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py @@ -6,9 +6,25 @@ from netbox_proxbox.backend.session.proxmox import ProxmoxSessionsDep from netbox_proxbox.backend.session.netbox import NetboxSessionDep +from netbox_proxbox.backend.logging import logger + +from netbox_proxbox.backend import ( + ClusterType, + Cluster, + Site, + DeviceRole, + DeviceType, + Device +) + +# from netbox_proxbox.backend.routes.netbox.virtualization.cluster_type import ClusterType +# from netbox_proxbox.backend.routes.netbox.virtualization.cluster import Cluster + +# from netbox_proxbox.backend.routes.netbox.dcim.sites import Site +# from netbox_proxbox.backend.routes.netbox.dcim.device_roles import DeviceRole +# from netbox_proxbox.backend.routes.netbox.dcim.device_types import DeviceType +# from netbox_proxbox.backend.routes.netbox.dcim.devices import Device -from netbox_proxbox.backend.routes.netbox.virtualization.cluster_type import ClusterType -from netbox_proxbox.backend.routes.netbox.virtualization.cluster import Cluster router = APIRouter() @@ -28,6 +44,7 @@ async def proxbox_get_clusters( cluster_type_name = f"Proxmox {px.mode.capitalize()}" cluster_type_slug = f"proxmox-{px.mode}" + # Create Cluster Type object before the Cluster itself cluster_type_obj = await ClusterType(nb = nb).post( data = { "name": cluster_type_name, @@ -36,6 +53,7 @@ async def proxbox_get_clusters( } ) + # Create the Cluster cluster_obj = await Cluster(nb = nb).post( data = { "name": px.name, @@ -50,4 +68,51 @@ async def proxbox_get_clusters( result.append(cluster_obj) return result - \ No newline at end of file + +@router.get("/nodes") +async def get_nodes( + pxs: ProxmoxSessionsDep, + nb: NetboxSessionDep, +): + + """Get Proxmox Nodes from a Cluster""" + + result = [] + + for px in pxs: + + get_cluster_from_netbox = await Cluster(nb = nb).get(name = px.name) + + # Get Proxmox Nodes from the current Proxmox Cluster + proxmox_nodes = px.session.nodes.get() + + device_role = await DeviceRole(nb = nb).get() + device_type = await DeviceType(nb = nb).get() + site = await Site(nb = nb).get() + + + nodes = [ + await Device(nb=nb).post(data = { + "name": node.get("node"), + "cluster": get_cluster_from_netbox["id"], + "role": device_role.id, + "site": site.id, + "status": "active", + "device_type": device_type.id + + }) for node in proxmox_nodes + ] + + + + logger.debug(f"Nodes: {nodes}") + + + result.append({ + "cluster_netbox_object": get_cluster_from_netbox, + "nodes_netbox_object": nodes, + "cluster_proxmox_object": px.session.nodes.get(), + + }) + + return result \ No newline at end of file From f02c39af592539c0af1c05f9829c60228abc0b95 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Tue, 19 Dec 2023 18:03:57 +0000 Subject: [PATCH 18/21] All current backend paths working with both 'GET' and 'POST' HTTPmethods implementing the 'NetboxBase.get_base_dict()' class method --- docs/backend/index.md | 7 + docs/backend/proxbox-architecture.png | Bin 0 -> 56938 bytes docs/src/netboxbasic.md | 4 +- mkdocs.yml | 1 + .../backend/routes/netbox/dcim/__init__.py | 51 ++++-- .../routes/netbox/dcim/device_roles.py | 39 +--- .../routes/netbox/dcim/device_types.py | 39 ++-- .../backend/routes/netbox/dcim/devices.py | 47 ++--- .../routes/netbox/dcim/manufacturers.py | 12 +- .../backend/routes/netbox/generic.py | 171 +++++++++--------- .../routes/netbox/virtualization/__init__.py | 42 +++-- .../routes/netbox/virtualization/cluster.py | 46 ++--- .../netbox/virtualization/cluster_type.py | 13 +- .../routes/proxbox/clusters/__init__.py | 49 +++-- .../schemas/netbox/virtualization/__init__.py | 2 +- 15 files changed, 271 insertions(+), 252 deletions(-) create mode 100644 docs/backend/index.md create mode 100644 docs/backend/proxbox-architecture.png diff --git a/docs/backend/index.md b/docs/backend/index.md new file mode 100644 index 0000000..958a5cb --- /dev/null +++ b/docs/backend/index.md @@ -0,0 +1,7 @@ +## How it works + +The backend made using the FastAPI framework connects with both Netbox and Proxmox (it can be many different clusters) and exposes the API REST routes that will be consumed by the Netbox Plugin (the Frontend) that is simply a Django App attached to the Netbox Django Project. + +### Proxbox Architecture + +![Proxbox Architecure Image](./proxbox-architecture.png) diff --git a/docs/backend/proxbox-architecture.png b/docs/backend/proxbox-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..ef7043331e3ea3b38c5b29a33c9a4b208db126de GIT binary patch literal 56938 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 literal 0 HcmV?d00001 diff --git a/docs/src/netboxbasic.md b/docs/src/netboxbasic.md index 9eac917..7c3178c 100644 --- a/docs/src/netboxbasic.md +++ b/docs/src/netboxbasic.md @@ -1,11 +1,11 @@ # `NetboxBase` class -::: netbox_proxbox.backend.routes.netbox.generic.NetboxBase + nb is a dependency injection of [NetboxSessionDep][netbox_proxbox.backend.session.netbox.NetboxSessionDep] diff --git a/mkdocs.yml b/mkdocs.yml index b81b785..b4f2313 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ plugins: nav: - Proxbox: index.md + - Backend (FastAPI): 'backend/index.md' - Source Code: - 'src/netboxbasic.md' - Installing & Upgrade: diff --git a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py index adba257..6bbaa71 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/__init__.py @@ -35,12 +35,6 @@ async def create_sites( **data:** JSON to create the Site on Netbox. You can create any Site you want, like a proxy to Netbox API. """ - - # if default: - # print(type(default)) - # print(default) - # return await site(default=True).post() - if data: return await site.post(data) @@ -55,6 +49,18 @@ async def get_devices( ): return await device.get() +@router.post("/devices") +async def create_devices( + device: Device = Depends(), + data: Annotated[dict, Body()] = None +): + """ + **default:** Boolean to define if Proxbox should create a default Device if there's no Device registered on Netbox.\n + **data:** JSON to create the Device on Netbox. You can create any Device you want, like a proxy to Netbox API. + """ + + return await device.post(data) + @router.get("/manufacturers") async def get_manufacturers( manufacturer: Manufacturer = Depends() @@ -71,10 +77,13 @@ async def create_manufacturers( **data:** JSON to create the Manufacturer on Netbox. You can create any Manufacturer you want, like a proxy to Netbox API. """ - if data: - return await manufacturer.post(data) - - return await manufacturer.post() + return await manufacturer.post(data) + + + +""" +DEVICE TYPES +""" @router.get("/device-types") async def get_device_types( @@ -82,8 +91,28 @@ async def get_device_types( ): return await device_type.get() +@router.post("/device-types") +async def create_device_types( + device_type: DeviceType = Depends(), + data: Annotated[dict, Body()] = None +): + return await device_type.post(data) + + +""" +DEVICE ROLES +""" + @router.get("/device-roles") async def get_device_roles( device_role: DeviceRole = Depends() ): - return await device_role.get() \ No newline at end of file + return await device_role.get() + + +@router.post("/device-roles") +async def create_device_roles( + device_role: DeviceRole = Depends(), + data: Annotated[dict, Body()] = None +): + return await device_role.post(data) \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py b/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py index 906bb5c..d44066c 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/device_roles.py @@ -3,29 +3,6 @@ class DeviceRole(NetboxBase): - async def extra_fields(self): - - self.default_dict = { - "name": self.default_name, - "slug": self.default_slug, - "color": "ff5722", - "vm_role": False, - "description": self.default_description, - "tags": [self.nb.tag.id] - } - - async def get(self): - if self.default: - await self.extra_fields() - - return await super().get() - - async def post(self, data: Any = None): - if self.default: - await self.extra_fields() - - return await super().post(data = data) - default_name = "Proxmox Node (Server)" default_slug = "proxbox-node" default_description = "Proxbox Basic Manufacturer" @@ -34,11 +11,11 @@ async def post(self, data: Any = None): endpoint = "device_roles" object_name = "Device Types" - base_dict = { - "name": default_name, - "slug": default_slug, - "color": "ff5722", - "vm_role": False, - "description": default_description, - "tags": [self.nb.tag.id] - } \ No newline at end of file + async def get_base_dict(self): + return { + "name": self.default_name, + "slug": self.default_slug, + "color": "ff5722", + "vm_role": False, + "description": self.default_description, + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/device_types.py b/netbox_proxbox/backend/routes/netbox/dcim/device_types.py index 15e6211..3ccb3c2 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/device_types.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/device_types.py @@ -4,33 +4,7 @@ from typing import Any class DeviceType(NetboxBase): - async def extra_fields(self): - manufacturer = await Manufacturer(nb = self.nb).get() - - - # Replaces the default_dict variable - self.default_dict = { - "model": "Proxbox Basic Device Type", - "slug": self.default_slug, - "manufacturer": manufacturer.id, - "description": self.default_description, - "u_height": 1, - "tags": [self.nb.tag.id] - } - - - async def get(self): - if self.default: - await self.extra_fields() - - return await super().get() - - async def post(self, data: Any = None): - if self.default: - await self.extra_fields() - return await super().post(data = data) - # Default Device Type Params default_name = "Proxbox Basic Device Type" default_slug = "proxbox-basic-device-type" @@ -38,4 +12,15 @@ async def post(self, data: Any = None): app = "dcim" endpoint = "device_types" - object_name = "Device Types" \ No newline at end of file + object_name = "Device Types" + + async def get_base_dict(self): + manufacturer = await Manufacturer(nb = self.nb).get() + + return { + "model": self.default_name, + "slug": self.default_slug, + "manufacturer": manufacturer.id, + "description": self.default_description, + "u_height": 1, + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/devices.py b/netbox_proxbox/backend/routes/netbox/dcim/devices.py index 6eafd19..87cdebe 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/devices.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/devices.py @@ -5,38 +5,31 @@ from .device_types import DeviceType from .device_roles import DeviceRole +from netbox_proxbox.backend.routes.netbox.virtualization.cluster import Cluster + class Device(NetboxBase): - async def extra_fields(self): - site = await Site(nb = self.nb).get() - role = await DeviceRole(nb = self.nb).get() - device_type = await DeviceType(nb = self.nb).get() - - self.default_dict.update( - { - "status": "active", - "site": site.id, - "role": role.id, - "device_type": device_type.id, - } - ) - - async def get(self): - if self.default: - await self.extra_fields() - - return await super().get() - - async def post(self, data: Any = None): - if self.default: - await self.extra_fields() - - return await super().post(data = data) - default_name = "Proxbox Basic Device" default_slug = "proxbox-basic-device" default_description = "Proxbox Basic Device" app = "dcim" endpoint = "devices" - object_name = "Device" \ No newline at end of file + object_name = "Device" + + async def get_base_dict(self): + site = await Site(nb = self.nb).get() + role = await DeviceRole(nb = self.nb).get() + device_type = await DeviceType(nb = self.nb).get() + cluster = await Cluster(nb = self.nb).get() + + return { + "name": self.default_name, + "slug": self.default_slug, + "description": self.default_description, + "site": site.id, + "role": role.id, + "device_type": device_type.id, + "status": "active", + "cluster": cluster.id + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py b/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py index 44d5069..906a666 100644 --- a/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py +++ b/netbox_proxbox/backend/routes/netbox/dcim/manufacturers.py @@ -10,8 +10,10 @@ class Manufacturer(NetboxBase): endpoint = "manufacturers" object_name = "Manufacturer" - base_dict = { - "name": default_name, - "slug": default_slug, - "description": default_description, - } \ No newline at end of file + + async def get_base_dict(self): + return { + "name": self.default_name, + "slug": self.default_slug, + "description": self.default_description, + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index dfec6fb..c09eea9 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -45,10 +45,10 @@ def __init__( bool, Query(title="Create Default Object", description="Create a default Object if there's no Object registered on Netbox."), ] = False, - default_extra_fields: Annotated[ - dict, - Body(title="Extra Fields", description="Extra fields to be added to the default Object.") - ] = None, + # default_extra_fields: Annotated[ + # dict, + # Body(title="Extra Fields", description="Extra fields to be added to the default Object.") + # ] = None, ignore_tag: Annotated[ bool, Query( @@ -66,26 +66,29 @@ def __init__( self.all = all self.default = default self.ignore_tag = ignore_tag - self.default_extra_fields = default_extra_fields + #self.default_extra_fields = default_extra_fields self.pynetbox_path = getattr(getattr(self.nb.session, self.app), self.endpoint) - self.default_dict = { - "name": self.default_name, - "slug": self.default_slug, - "description": self.default_description, - "tags": [self.nb.tag.id] - } + # self.default_dict = { + # "name": self.default_name, + # "slug": self.default_slug, + # "description": self.default_description, + # "tags": [self.nb.tag.id] + # } - # New Implementantion of "default_dict" amd "default_extra_fields". + # New Implementantion of "default_dict" and "default_extra_fields". base_dict = None + async def get_base_dict(self): + "This method MUST be overwritten by the child class." + pass # Default Object Parameters. # It should be overwritten by the child class. - default_name = None - default_slug = None - default_description = None + # default_name = None + # default_slug = None + # default_description = None # Parameters to be used as Pynetbox class attributes. # It should be overwritten by the child class. @@ -98,6 +101,8 @@ async def get( self, **kwargs ): + self.base_dict = await self.get_base_dict() + print(kwargs) logger.info(f"[GET] Getting '{self.object_name}' from Netbox.") @@ -263,82 +268,86 @@ async def _get_all(self): async def post( self, - data = None, + data: dict = None, ): - if self.default: - - logger.info(f"[POST] Creating DEFAULT '{self.object_name}' object on Netbox.") - try: - - # If default object doesn't exist, create it. - check_duplicate_result = await self._check_duplicate() - if check_duplicate_result == None: - - # Create default object - response = self.pynetbox_path.create(self.default_dict) - return response - - # If duplicate object found, return it. - else: - return check_duplicate_result - - - except ProxboxException as error: raise error - - except Exception as error: - raise ProxboxException( - message=f"[POST] Error trying to create DEFAULT '{self.object_name}' on Netbox.", - python_exception=f"{error}" - ) - + self.base_dict = await self.get_base_dict() + print(self.base_dict) + print(data) + if data: - try: - logger.info(f"[POST] Creating '{self.object_name}' object on Netbox.") - - if isinstance(data, dict) == False: + logger.info(f"[POST] Creating '{self.object_name}' object on Netbox.") + + if isinstance(data, dict) == False: + try: # Convert Pydantic model to Dict through 'model_dump' Pydantic method. data = data.model_dump(exclude_unset=True) + except Exception as error: + raise ProxboxException( + message=f"[POST] Error parsing Pydantic model to Dict.", + python_exception=f"{error}", + ) - if self.base_dict: - - # Merge base_dict and data dict. - data = self.base_dict | data + # If no explicit slug was provided by the payload, create one based on the name. + if data.get("slug") == None: + logger.info("[POST] SLUG field not provided on the payload. Creating one based on the NAME or MODEL field.") + try: + data["slug"] = data.get("name").replace(" ", "-").lower() + except AttributeError: + + try: + data["slug"] = data.get("model").replace(" ", "-").lower() + except AttributeError: + raise ProxboxException( + message=f"[POST] No 'name' or 'model' field provided on the payload. Please provide one of them.", + ) - check_duplicate_result = await self._check_duplicate(object = data) + if self.default or data == None: + logger.info(f"[POST] Creating DEFAULT '{self.object_name}' object on Netbox.") + data = self.base_dict + + try: + + """ + Merge base_dict and data dict. + The fields not specificied on data dict will be filled with the base_dict values. + """ + data = self.base_dict | data + + check_duplicate_result = await self._check_duplicate(object = data) + + if check_duplicate_result == None: - if check_duplicate_result == None: - - # Check if tags field exists on the payload and if true, append the Proxbox tag. If not, create it. - if data.get("tags") == None: - data["tags"] = [self.nb.tag.id] - else: - data["tags"].append(self.nb.tag.id) - - response = self.pynetbox_path.create(data) - - if response: - logger.info(f"[POST] '{self.object_name}' object created successfully. {self.object_name} ID: {response.id}") - return response + # Check if tags field exists on the payload and if true, append the Proxbox tag. If not, create it. + if data.get("tags") == None: + data["tags"] = [self.nb.tag.id] + else: + data["tags"].append(self.nb.tag.id) - else: - logger.error(f"[POST] '{self.object_name}' object could not be created.") + response = self.pynetbox_path.create(data) + + if response: + logger.info(f"[POST] '{self.object_name}' object created successfully. {self.object_name} ID: {response.id}") + return response + else: - logger.info(f"[POST] '{self.object_name}' object already exists on Netbox. Returning it.") - return check_duplicate_result - - except ProxboxException as error: raise error - - except Exception as error: - raise ProxboxException( - message=f"Error trying to create {self.object_name} on Netbox.", - detail=f"Payload provided: {data}", - python_exception=f"{error}" - ) + logger.error(f"[POST] '{self.object_name}' object could not be created.") + else: + logger.info(f"[POST] '{self.object_name}' object already exists on Netbox. Returning it.") + return check_duplicate_result - raise ProxboxException( - message=f"[POST] No data provided to create '{self.object_name}' on Netbox.", - detail=f"Please provide a JSON payload to create the '{self.object_name}' on Netbox or set 'default' to 'Trsue' on Query Parameter to create a default one." - ) + except ProxboxException as error: raise error + + except Exception as error: + raise ProxboxException( + message=f"Error trying to create {self.object_name} on Netbox.", + detail=f"Payload provided: {data}", + python_exception=f"{error}" + ) + + # raise ProxboxException( + # message=f"[POST] No data provided to create '{self.object_name}' on Netbox.", + # detail=f"Please provide a JSON payload to create the '{self.object_name}' on Netbox or set 'default' to 'True' on Query Parameter to create a default one." + # ) diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py index f70ddd0..191bb22 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py @@ -20,13 +20,13 @@ async def get_cluster_types( all = None, cluster_type: ClusterType = Depends() ) @router.post("/cluster-types") async def create_cluster_types( cluster_type: ClusterType = Depends(), - default: Annotated[ - bool, - Body( - title="Create default object.", - description="Create a default object if there's no object registered on Netbox." - ) - ] = False, + # default: Annotated[ + # bool, + # Body( + # title="Create default object.", + # description="Create a default object if there's no object registered on Netbox." + # ) + # ] = False, data: Annotated[ ClusterTypeSchema, Body( @@ -52,24 +52,26 @@ async def get_clusters( @router.post("/clusters") async def create_cluster( cluster: Cluster = Depends(), - default: Annotated[ - bool, - Body( - title="Create default object.", - description="Create a default object if there's no object registered on Netbox." - ), - ] = False, - data: ClusterSchema = None, + # default: Annotated[ + # bool, + # Body( + # title="Create default object.", + # description="Create a default object if there's no object registered on Netbox." + # ), + # ] = False, + data: dict = None, ): + print(data, type(data)) """ **default:** Boolean to define if Proxbox should create a default Cluster Type if there's no Cluster Type registered on Netbox.\n **data:** JSON to create the Cluster Type on Netbox. You can create any Cluster Type you want, like a proxy to Netbox API. """ - if default: - return await cluster.post() + # if default: + # return await cluster.post() - if data: - print(f"create_cluster: {data}") - return await cluster.post(data = data) + # if data: + # print(f"create_cluster: {data}") + # return await cluster.post(data = data) + return await cluster.post(data) \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py index 01ceab9..c1ff920 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py @@ -4,37 +4,23 @@ from typing import Any class Cluster(NetboxBase): - - async def extra_fields(self): - type = await ClusterType(nb = self.nb).get() - - self.default_dict.update( - { - "status": "active", - "type": type.id - } - ) - - async def get(self, **kwargs): - if self.default: - await self.extra_fields() - - return await super().get(**kwargs) - - async def post(self, data: Any = None): - if self.default: - await self.extra_fields() - - return await super().post(data = data) - # Default Cluster Type Params - default_name = "Proxbox Basic Cluster" - default_slug = "proxbox-basic-cluster-type" - default_description = "Proxbox Basic Cluster (used to identify the items the plugin created)" + default_name: str = "Proxbox Basic Cluster" + default_slug: str = "proxbox-basic-cluster-type" + default_description: str = "Proxbox Basic Cluster (used to identify the items the plugin created)" - app = "virtualization" - endpoint = "clusters" - object_name = "Cluster" + app: str = "virtualization" + endpoint: str = "clusters" + object_name: str = "Cluster" - \ No newline at end of file + async def get_base_dict(self): + type = await ClusterType(nb = self.nb).get() + + return { + "name": self.default_name, + "slug": self.default_slug, + "description": self.default_description, + "status": "active", + "type": type.id + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py index 1c73c30..77b85ed 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster_type.py @@ -1,11 +1,18 @@ from netbox_proxbox.backend.routes.netbox.generic import NetboxBase class ClusterType(NetboxBase): + app = "virtualization" + endpoint = "cluster_types" + object_name = "Cluster Type" + # Default Cluster Type Params default_name = "Proxbox Basic Cluster Type" default_slug = "proxbox-basic-cluster-type" default_description = "Proxbox Basic Cluster Type (used to identify the items the plugin created)" - app = "virtualization" - endpoint = "cluster_types" - object_name = "Cluster Type" \ No newline at end of file + async def get_base_dict(self) -> dict: + return { + "name": self.default_name, + "slug": self.default_slug, + "description": self.default_description, + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py index 010de33..7a70079 100644 --- a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py +++ b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py @@ -17,16 +17,15 @@ Device ) -# from netbox_proxbox.backend.routes.netbox.virtualization.cluster_type import ClusterType -# from netbox_proxbox.backend.routes.netbox.virtualization.cluster import Cluster +router = APIRouter() -# from netbox_proxbox.backend.routes.netbox.dcim.sites import Site -# from netbox_proxbox.backend.routes.netbox.dcim.device_roles import DeviceRole -# from netbox_proxbox.backend.routes.netbox.dcim.device_types import DeviceType -# from netbox_proxbox.backend.routes.netbox.dcim.devices import Device +async def proxmox_session_with_cluster( + pxs: ProxmoxSessionsDep, + nb: NetboxSessionDep +): + """Get Proxmox Session with Cluster""" -router = APIRouter() @router.get("/") async def proxbox_get_clusters( @@ -41,15 +40,29 @@ async def proxbox_get_clusters( for px in pxs: + """ + Before creating the Cluster, we need to create the Cluster Type. + """ cluster_type_name = f"Proxmox {px.mode.capitalize()}" cluster_type_slug = f"proxmox-{px.mode}" + + standalone_description = "Proxmox Standalone. This Proxmox has only one node and thus is not part of a Cluster." + cluster_description = "Proxmox Cluster. This Proxmox has more than one node and thus is part of a Cluster." + + + description = "" + if px.mode == "standalone": + description = standalone_description + elif px.mode == "cluster": + description = cluster_description + # Create Cluster Type object before the Cluster itself cluster_type_obj = await ClusterType(nb = nb).post( data = { "name": cluster_type_name, "slug": cluster_type_slug, - "description": f"Proxmox Cluster '{px.name}'" + "description": description } ) @@ -62,25 +75,33 @@ async def proxbox_get_clusters( "status": "active", } ) - - print(px.cluster_status) - - result.append(cluster_obj) + + result.append( + { + "name": px.name, + "netbox": { + "cluster": cluster_obj, + } + } + ) return result @router.get("/nodes") async def get_nodes( - pxs: ProxmoxSessionsDep, + #pxs: ProxmoxSessionsDep, nb: NetboxSessionDep, + pxs: ProxmoxSessionsDep, ): """Get Proxmox Nodes from a Cluster""" + result = [] for px in pxs: + get_cluster_from_netbox = await Cluster(nb = nb).get(name = px.name) # Get Proxmox Nodes from the current Proxmox Cluster @@ -94,7 +115,7 @@ async def get_nodes( nodes = [ await Device(nb=nb).post(data = { "name": node.get("node"), - "cluster": get_cluster_from_netbox["id"], + "cluster": px.get("netbox").get("cluster").get("id"), "role": device_role.id, "site": site.id, "status": "active", diff --git a/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py index 4189c08..bad83f8 100644 --- a/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/schemas/netbox/virtualization/__init__.py @@ -6,7 +6,7 @@ class ClusterTypeSchema(BaseModel): name: str - slug: str + slug: str | None = None description: str | None = None tags: list[TagSchema] | None = None custom_fields: dict | None = None From 2839ea9754d08b89e7ccb96b6692cb41e47c8551 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Tue, 19 Dec 2023 19:49:41 +0000 Subject: [PATCH 19/21] Proxbox successfully creating Virtual Machines based on Proxbox using the new backend. This is currently slow and remaining fields like CPU and Memory. I will make it better. --- netbox_proxbox/backend/__init__.py | 2 + .../routes/netbox/virtualization/__init__.py | 17 ++- .../netbox/virtualization/virtual_machines.py | 23 ++++ .../routes/proxbox/clusters/__init__.py | 106 +++++++++++++++--- 4 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 netbox_proxbox/backend/routes/netbox/virtualization/virtual_machines.py diff --git a/netbox_proxbox/backend/__init__.py b/netbox_proxbox/backend/__init__.py index 09e9d98..5e91060 100644 --- a/netbox_proxbox/backend/__init__.py +++ b/netbox_proxbox/backend/__init__.py @@ -1,5 +1,7 @@ from netbox_proxbox.backend.routes.netbox.virtualization.cluster_type import ClusterType from netbox_proxbox.backend.routes.netbox.virtualization.cluster import Cluster +from netbox_proxbox.backend.routes.netbox.virtualization.virtual_machines import VirtualMachine + from netbox_proxbox.backend.routes.netbox.dcim.sites import Site from netbox_proxbox.backend.routes.netbox.dcim.device_roles import DeviceRole diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py index 191bb22..c2ebeaa 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/__init__.py @@ -3,6 +3,7 @@ from .cluster_type import ClusterType from .cluster import Cluster +from .virtual_machines import VirtualMachine from netbox_proxbox.backend.schemas.netbox import CreateDefaultBool from netbox_proxbox.backend.schemas.netbox.virtualization import ClusterTypeSchema, ClusterSchema @@ -74,4 +75,18 @@ async def create_cluster( # print(f"create_cluster: {data}") # return await cluster.post(data = data) return await cluster.post(data) - \ No newline at end of file + + +@router.get("/virtual-machines") +async def get_virtual_machines( + virtual_machine: VirtualMachine = Depends(), +): + return await virtual_machine.get() + + +@router.post("/virtual-machines") +async def create_virtual_machines( + virtual_machine: VirtualMachine = Depends(), + data: Annotated[dict, Body()] = None +): + return await virtual_machine.post(data) \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/virtual_machines.py b/netbox_proxbox/backend/routes/netbox/virtualization/virtual_machines.py new file mode 100644 index 0000000..970c893 --- /dev/null +++ b/netbox_proxbox/backend/routes/netbox/virtualization/virtual_machines.py @@ -0,0 +1,23 @@ +from netbox_proxbox.backend.routes.netbox.generic import NetboxBase +from .cluster import Cluster + +class VirtualMachine(NetboxBase): + # Default Cluster Type Params + default_name: str = "Proxbox Basic Virtual Machine" + default_slug: str = "proxbox-basic-virtual-machine" + default_description: str = "Proxmox Virtual Machine (this is a fallback VM when syncing from Proxmox)" + + app: str = "virtualization" + endpoint: str = "virtual_machines" + object_name: str = "Virtual Machine" + + + async def get_base_dict(self): + cluster = await Cluster(nb = self.nb).get() + return { + "name": self.default_name, + "slug": self.default_slug, + "description": self.default_description, + "status": "active", + "cluster": cluster.id + } \ No newline at end of file diff --git a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py index 7a70079..4f9c960 100644 --- a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py +++ b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py @@ -14,7 +14,8 @@ Site, DeviceRole, DeviceType, - Device + Device, + VirtualMachine ) router = APIRouter() @@ -89,7 +90,6 @@ async def proxbox_get_clusters( @router.get("/nodes") async def get_nodes( - #pxs: ProxmoxSessionsDep, nb: NetboxSessionDep, pxs: ProxmoxSessionsDep, ): @@ -101,39 +101,109 @@ async def get_nodes( for px in pxs: - + # Get Cluster from Netbox based on Proxmox Cluster Name get_cluster_from_netbox = await Cluster(nb = nb).get(name = px.name) # Get Proxmox Nodes from the current Proxmox Cluster proxmox_nodes = px.session.nodes.get() - device_role = await DeviceRole(nb = nb).get() - device_type = await DeviceType(nb = nb).get() - site = await Site(nb = nb).get() - - + # List Comprehension to create the Nodes in Netbox nodes = [ await Device(nb=nb).post(data = { "name": node.get("node"), - "cluster": px.get("netbox").get("cluster").get("id"), - "role": device_role.id, - "site": site.id, + "cluster": get_cluster_from_netbox.id, "status": "active", - "device_type": device_type.id - }) for node in proxmox_nodes ] - - logger.debug(f"Nodes: {nodes}") result.append({ - "cluster_netbox_object": get_cluster_from_netbox, - "nodes_netbox_object": nodes, - "cluster_proxmox_object": px.session.nodes.get(), + "name": px.name, + "netbox": { + "nodes": nodes + } + }) + + return result + +@router.get("/nodes/interfaces") +async def get_nodes_interfaces(): + pass +@router.get("/virtual-machines") +async def get_virtual_machines( + nb: NetboxSessionDep, + pxs: ProxmoxSessionsDep, +): + from enum import Enum + class VirtualMachineStatus(Enum): + """ + Key are Netbox Status. + Values are Proxmox Status. + """ + active = "running" + offline = "stopped" + + result = [] + + + containers = [] + + + + for px in pxs: + virtual_machines = px.session.cluster.resources.get(type="vm") + + created_virtual_machines = [] + + + devices = {} + clusters = {} + for vm in virtual_machines: + vm_node = vm.get("node") + + """ + Get Device from Netbox based on Proxmox Node Name only if it's not already in the devices dict + This way we are able to minimize the number of requests to Netbox API + """ + if devices.get(vm_node) == None: + devices[vm_node] = await Device(nb = nb).get(name = vm.get("node")) + + device = devices[vm_node] + + """ + Get Cluster from Netbox based on Cluster Name only if it's not already in the devices dict + This way we are able to minimize the number of requests to Netbox API + """ + if clusters.get(px.name) == None: + clusters[px.name] = await Cluster(nb = nb).get(name = px.name) + + cluster = clusters[px.name] + + created_virtual_machines.append( + await VirtualMachine(nb = nb).post(data = { + "name": vm.get("name"), + "cluster": cluster.id, + "device": device.id, + "status": VirtualMachineStatus(vm.get("status")).name, + }) + ) + + result.append({ + "name": px.name, + "netbox": { + "virtual_machines": created_virtual_machines + } }) + + return result + + + + + + return result \ No newline at end of file From b367b363a58c5c975338bb73b2b5c7427d2d941b Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Wed, 20 Dec 2023 19:40:46 +0000 Subject: [PATCH 20/21] Add 'asyncio.to_thread' to all pynetbox calls on 'generic.NetboxBase' --- .../backend/routes/netbox/generic.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index c09eea9..e753cd5 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -8,6 +8,8 @@ from netbox_proxbox.backend.logging import logger +import asyncio + class NetboxBase: """ ## Class to handle Netbox Objects. @@ -138,7 +140,7 @@ async def get( # 2.2.1 If there's any 'Object' registered on Netbox, check if is Proxbox one by checking tag and name. try: logger.info(f"[GET] '{self.object_name}' found on Netbox. Checking if it's 'Proxbox' one...") - get_object = self.pynetbox_path.get( + get_object = await asyncio.to_thread(self.pynetbox_path.get, name=self.default_name, slug=self.default_slug, tag=[self.nb.tag.slug] @@ -176,7 +178,7 @@ async def _get_by_kwargs(self, **kwargs): logger.info(f"[GET] Searching '{self.object_name}' by kwargs {kwargs}.") try: - response = self.pynetbox_path.get(**kwargs) + response = await asyncio.to_thread(self.pynetbox_path.get, **kwargs) return response except ProxboxException as error: raise error @@ -201,10 +203,10 @@ async def _get_by_id(self): try: if self.ignore_tag: - response = self.pynetbox_path.get(self.id) + response = await asyncio.to_thread(self.pynetbox_path.get, self.id) else: - response = self.pynetbox_path.get( + response = await asyncio.to_thread(self.pynetbox_path.get, id=self.id, tag=[self.nb.tag.slug] ) @@ -244,7 +246,7 @@ async def _get_all(self): if self.ignore_tag: try: # If ignore_tag is True, return all objects from Netbox. - return [item for item in self.pynetbox_path.all()] + return [item for item in await asyncio.to_thread(self.pynetbox_path.all())] except Exception as error: raise ProxboxException( message=f"Error trying to get all '{self.object_name}' from Netbox.", @@ -253,7 +255,7 @@ async def _get_all(self): try: # If ignore_tag is False, return only objects with Proxbox tag. - return [item for item in self.pynetbox_path.filter(tag = [self.nb.tag.slug])] + return [item for item in asyncio.to_thread(self.pynetbox_path.filter, tag = [self.nb.tag.slug])] except Exception as error: raise ProxboxException( @@ -323,7 +325,7 @@ async def post( else: data["tags"].append(self.nb.tag.id) - response = self.pynetbox_path.create(data) + response = await asyncio.to_thread(self.pynetbox_path.create, data) if response: logger.info(f"[POST] '{self.object_name}' object created successfully. {self.object_name} ID: {response.id}") @@ -361,7 +363,7 @@ async def _check_duplicate(self, search_params: dict = None, object: dict = None if self.default: logger.info("[CHECK DUPLICATE] Checking default object.") try: - result = self.pynetbox_path.get( + result = await asyncio.to_thread(self.pynetbox_path.get, name=self.default_name, slug=self.default_slug, tag=[self.nb.tag.slug] @@ -373,7 +375,7 @@ async def _check_duplicate(self, search_params: dict = None, object: dict = None else: # If no object found searching using tag, try to find without it, using just name and slug. - result = self.pynetbox_path.get( + result = await asyncio.to_thread(self.pynetbox_path.get, name=self.default_name, slug=self.default_slug, ) @@ -401,7 +403,7 @@ async def _check_duplicate(self, search_params: dict = None, object: dict = None if object: try: logger.info("[CHECK DUPLICATE] (1) First attempt: Checking object making EXACT MATCH with the Payload provided...") - result = self.pynetbox_path.get(object) + result = await asyncio.to_thread(self.pynetbox_path.get, object) if result: logger.info(f"[CHECK DUPLICATE] Object found on Netbox. Returning it.") @@ -409,7 +411,7 @@ async def _check_duplicate(self, search_params: dict = None, object: dict = None else: logger.info("[CHECK DUPLICATE] (2) Checking object using only NAME and SLUG provided by the Payload and also the PROXBOX TAG). If found, return it.") - result_by_tag = self.pynetbox_path.get( + result_by_tag = await asyncio.to_thread(self.pynetbox_path.get, name=object.get("name"), slug=object.get("slug"), tag=[self.nb.tag.slug] @@ -420,7 +422,7 @@ async def _check_duplicate(self, search_params: dict = None, object: dict = None return result_by_tag else: - result_by_name_and_slug = self.pynetbox_path.get( + result_by_name_and_slug = await asyncio.to_thread(self.pynetbox_path.get, name=object.get("name"), slug=object.get("slug"), ) From 379437b1aa3a1d8ae726dc541cf3b7db02f6dbb9 Mon Sep 17 00:00:00 2001 From: emersonfelipesp Date: Wed, 27 Dec 2023 19:39:55 +0000 Subject: [PATCH 21/21] Implement generic caching system to avoid duplicate requests --- netbox_proxbox/backend/cache.py | 26 +++++++++++++++++++ netbox_proxbox/backend/logging.py | 2 +- .../backend/routes/netbox/generic.py | 21 +++++++++++---- .../routes/netbox/virtualization/cluster.py | 2 +- .../routes/proxbox/clusters/__init__.py | 11 +++----- 5 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 netbox_proxbox/backend/cache.py diff --git a/netbox_proxbox/backend/cache.py b/netbox_proxbox/backend/cache.py new file mode 100644 index 0000000..d00ddc5 --- /dev/null +++ b/netbox_proxbox/backend/cache.py @@ -0,0 +1,26 @@ +from typing import Any + +class Cache: + def __init__(self): + self.cache: dict = {} + + def get(self, key: str): + result = self.cache.get(key) + if result is not None: + return result + + def set(self, key: str, value: Any): + self.cache[key] = value + + def delete(self, key: str): + try: + self.cache.pop(key) + except KeyError: + pass + +cache = Cache() + + + + + \ No newline at end of file diff --git a/netbox_proxbox/backend/logging.py b/netbox_proxbox/backend/logging.py index 1a779f6..385667c 100644 --- a/netbox_proxbox/backend/logging.py +++ b/netbox_proxbox/backend/logging.py @@ -44,7 +44,7 @@ def setup_logger(): console_handler = logging.StreamHandler() # Log all messages in the console - console_handler.setLevel(logging.DEBUG) + console_handler.setLevel(logging.ERROR) # Create a formatter with colors formatter = ColorizedFormatter('%(name)s [%(asctime)s] [%(levelname)-8s] %(module)s: %(message)s') diff --git a/netbox_proxbox/backend/routes/netbox/generic.py b/netbox_proxbox/backend/routes/netbox/generic.py index e753cd5..c74c0fe 100644 --- a/netbox_proxbox/backend/routes/netbox/generic.py +++ b/netbox_proxbox/backend/routes/netbox/generic.py @@ -8,6 +8,8 @@ from netbox_proxbox.backend.logging import logger +from netbox_proxbox.backend.cache import cache + import asyncio class NetboxBase: @@ -81,7 +83,6 @@ def __init__( # New Implementantion of "default_dict" and "default_extra_fields". - base_dict = None async def get_base_dict(self): "This method MUST be overwritten by the child class." pass @@ -103,7 +104,16 @@ async def get( self, **kwargs ): - self.base_dict = await self.get_base_dict() + self.base_dict = cache.get(self.endpoint) + if self.base_dict is None: + self.base_dict = await self.get_base_dict() + cache.set(self.endpoint, self.base_dict) + + + # if self.base_dict is None: + # await self.get_base_dict() + + #base_dict = await self.get_base_dict() print(kwargs) logger.info(f"[GET] Getting '{self.object_name}' from Netbox.") @@ -272,9 +282,10 @@ async def post( self, data: dict = None, ): - self.base_dict = await self.get_base_dict() - print(self.base_dict) - print(data) + self.base_dict = cache.get(self.endpoint) + if self.base_dict is None: + self.base_dict = await self.get_base_dict() + cache.set(self.endpoint, self.base_dict) if data: logger.info(f"[POST] Creating '{self.object_name}' object on Netbox.") diff --git a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py index c1ff920..dba161a 100644 --- a/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py +++ b/netbox_proxbox/backend/routes/netbox/virtualization/cluster.py @@ -12,7 +12,7 @@ class Cluster(NetboxBase): app: str = "virtualization" endpoint: str = "clusters" object_name: str = "Cluster" - + async def get_base_dict(self): type = await ClusterType(nb = self.nb).get() diff --git a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py index 4f9c960..710c673 100644 --- a/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py +++ b/netbox_proxbox/backend/routes/proxbox/clusters/__init__.py @@ -151,7 +151,9 @@ class VirtualMachineStatus(Enum): containers = [] + from netbox_proxbox.backend.cache import cache + print("CACHE start:", cache.cache) for px in pxs: virtual_machines = px.session.cluster.resources.get(type="vm") @@ -198,12 +200,5 @@ class VirtualMachineStatus(Enum): } }) - return result - - - - - - - + print("CACHE end:", cache.cache) return result \ No newline at end of file