├── .gitignore ├── LICENSE ├── README.MD ├── backend ├── cmdboss_db │ ├── __init__.py │ └── cmdboss_db.py ├── conf │ ├── __init__.py │ └── confload.py ├── models │ ├── __init__.py │ ├── cmdboss_base_model.py │ └── system.py ├── security │ └── get_api_key.py └── util │ ├── __init__.py │ ├── exceptions.py │ ├── file_mgr.py │ ├── model_loader.py │ ├── oapi.py │ └── webhook_runner.py ├── cboss-1.gif ├── cboss-pman.PNG ├── cmdboss.PNG ├── cmdboss.py ├── config.json ├── docker-compose.yml ├── dockerfile ├── extensibles ├── hooks │ ├── __init__.py │ └── sample_webhook.py └── models │ └── __init__.py ├── gunicorn.conf.py ├── log-config.yml ├── marketecture-1.png ├── marketecture-2.png ├── requirements.txt └── routers ├── __init__.py ├── route_utils.py ├── system.py └── usr_models.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vscode/ 3 | .vscode/settings.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 |
5 | 6 |
7 |

8 | API driven, integrated configuration management 9 |

10 |
11 |

12 | 13 |
14 | 15 | ## Features 16 | 17 | - Model driven REST API leveraging **FastAPI** underpinned by **Python** hooks and **Mongo** DB 18 | - Upload your own **Pydantic** models for data validation + **Python** hooks @ Runtime over the REST interface 19 | - Self generating **swagger** & dynamic CRUD REST endpoints for db operations of all user defined models 20 | - Custom **hook** layer providing **async** hook capability on DB CRUD operations via a threaded queue 21 | 22 | ## Concepts 23 | 24 |

25 | 26 |
27 | 28 |
29 |

30 | 31 | ## Getting Started 32 | 33 | - git clone the project ``` https://github.com/tbotnz/cmdboss.git ``` 34 | - cd into the cmdboss dir ```cd cmdboss ``` 35 | - Optional (Configure ```config.json```) with your settings 36 | - docker comppose up ```docker-compose up --build``` 37 | - access swagger via ```http://127.0.0.1:9000/docs``` 38 | - download the online [postman collection](https://documenter.getpostman.com/view/2391814/TzRPjV5h) 39 | 40 | ## Demo 41 | 42 | ![cmdboss](cboss-1.gif) 43 | 44 | 45 | ## Roadmap 46 | 47 | - GUI for real time model editing 48 | - Add Endpoint Authorisation 49 | - Automate reloading of models 50 | - GraphQL support 51 | 52 | ## Community 53 | 54 | You can also find us in the channel #cmdboss on the networktocode Slack. 55 | -------------------------------------------------------------------------------- /backend/cmdboss_db/__init__.py: -------------------------------------------------------------------------------- 1 | from backend.cmdboss_db.cmdboss_db import CMDBOSS_db 2 | 3 | cmdb_oss = CMDBOSS_db() -------------------------------------------------------------------------------- /backend/cmdboss_db/cmdboss_db.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import re 4 | from typing import Any 5 | from concurrent.futures import ThreadPoolExecutor 6 | 7 | from pymongo import MongoClient, database 8 | 9 | from bson.json_util import dumps, loads 10 | from bson.objectid import ObjectId 11 | 12 | from backend.conf.confload import config 13 | from backend.models.system import ResponseBasic 14 | from backend.util.webhook_runner import exec_hook_func 15 | 16 | import time 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | class CMDBOSS_db: 22 | 23 | def __init__(self): 24 | self.server = config.mongo_server_ip 25 | self.port = config.mongo_server_port 26 | self.username = config.mongo_user 27 | self.password = config.mongo_password 28 | 29 | if self.username: 30 | self.raw_connection = MongoClient( 31 | host=self.server, 32 | port=self.port, 33 | username=self.username, 34 | password=self.password, 35 | ) 36 | else: 37 | self.raw_connection = MongoClient( 38 | host=self.server, 39 | port=self.port, 40 | ) 41 | 42 | self.base_connection = self.raw_connection.cmdboss 43 | 44 | def run_hooks(self, operation: str, model: str, data: Any): 45 | filter = { 46 | "events.model": model, 47 | "events.operation": operation 48 | } 49 | hooks = self.query( 50 | payload=filter, 51 | model="hooks" 52 | ) 53 | 54 | if len(hooks["result"]) >= 1: 55 | with ThreadPoolExecutor(config.num_thread_workers) as worker_pool: 56 | executions = [] 57 | for hook in hooks["result"]: 58 | if len(hook["events"]) >= 1: 59 | for event in hook["events"]: 60 | if event["operation"] == operation: 61 | log.info(f"run_hooks: Webhook Executing on {operation} model {model}") 62 | send_payload = { 63 | "base64_payload": hook["base64_payload"], 64 | "payload": data 65 | } 66 | execution = worker_pool.submit(exec_hook_func, send_payload) 67 | executions.append(execution) 68 | 69 | def get_model_name(self, path): 70 | path_array = path.split("/") 71 | url_parser = { 72 | r"\/table/.*/": -2, 73 | r"\/hooks/.*": -2, 74 | r"\/hooks": -1, 75 | r"\/models/.*": -2, 76 | } 77 | for key in url_parser: 78 | if re.search(key, path): 79 | return path_array[url_parser[key]] 80 | return path_array[-1] 81 | 82 | def ingress_parse_object_id(self, data: dict): 83 | if data.get("object_id", False): 84 | data["_id"] = ObjectId(data["object_id"]) 85 | del data["object_id"] 86 | return data 87 | 88 | def egress_parse_object_id(self, data: list): 89 | if len(data) >= 1: 90 | for idx, obj in enumerate(data, start=0): 91 | obj_id = obj["_id"]["$oid"] 92 | data[idx]["object_id"] = obj_id 93 | del data[idx]["_id"] 94 | return data 95 | 96 | def insert_one(self, model, payload: dict): 97 | ret = self.base_connection[model].insert_one(payload) 98 | result = ResponseBasic(status="success", result=[{"object_id": f"{ret.inserted_id}"}]).dict() 99 | return result 100 | 101 | def insert_many(self, model, payload: list): 102 | ret = self.base_connection[model].insert_many(payload) 103 | resp_arr = [] 104 | for obj in ret.inserted_ids: 105 | resp_arr.append({"object_id": f"{obj}"}) 106 | result = ResponseBasic(status="success", result=resp_arr).dict() 107 | return result 108 | 109 | def insert(self, model_instance_data: Any, path: str): 110 | """ wrapper for both insert_one and insert_many""" 111 | model_name = self.get_model_name(path) 112 | if isinstance(model_instance_data, list): 113 | req_data = [] 114 | for item in model_instance_data: 115 | req_data.append(item.dict()) 116 | result = self.insert_many(model=model_name, payload=req_data) 117 | else: 118 | req_data = model_instance_data.dict() 119 | result = self.insert_one(model=model_name, payload=req_data) 120 | self.run_hooks(operation="create", model=model_name, data=result) 121 | return result 122 | 123 | def query(self, model: str, payload: dict): 124 | """ wrapper for find with filtering """ 125 | cleaned_data = self.ingress_parse_object_id(payload) 126 | ret = self.base_connection[model].find(cleaned_data) 127 | temp_json_result = dumps(ret) 128 | loaded_result = json.loads(temp_json_result) 129 | final_result = self.egress_parse_object_id(loaded_result) 130 | if final_result is None or len(loaded_result) < 1: 131 | final_result = [] 132 | result = ResponseBasic(status="success", result=final_result).dict() 133 | return result 134 | 135 | def find(self, model: str): 136 | ret = self.base_connection[model].find() 137 | temp_json_result = dumps(ret) 138 | loaded_result = json.loads(temp_json_result) 139 | final_result = self.egress_parse_object_id(loaded_result) 140 | if final_result is None: 141 | final_result = [] 142 | result = ResponseBasic(status="success", result=final_result).dict() 143 | return result 144 | 145 | def retrieve(self, query_obj: dict, object_id: str, path: str): 146 | """ wrapper for both find and query""" 147 | model_name = self.get_model_name(path) 148 | if query_obj.get("filter", False): 149 | result = self.query(model=model_name, payload=query_obj["filter"]) 150 | elif object_id: 151 | query_obj["filter"] = {} 152 | query_obj["filter"]["object_id"] = object_id 153 | result = self.query(model=model_name, payload=query_obj["filter"]) 154 | elif object_id is None: 155 | result = self.find(model=model_name) 156 | self.run_hooks(operation="retrieve", model=model_name, data=result) 157 | return result 158 | 159 | def delete(self, query: dict, object_id: str, path: str): 160 | final_result = [] 161 | model_name = self.get_model_name(path) 162 | if query.get("filter", False): 163 | cleaned_data = self.ingress_parse_object_id(query["filter"]) 164 | ret = self.base_connection[model_name].delete_many(cleaned_data) 165 | if ret.deleted_count >= 1: 166 | final_result = [{"deleted_object_count": ret.deleted_count}] 167 | if object_id: 168 | query["filter"] = {} 169 | query["filter"]["object_id"] = object_id 170 | cleaned_data = self.ingress_parse_object_id(query["filter"]) 171 | ret = self.base_connection[model_name].delete_many(cleaned_data) 172 | if ret.deleted_count >= 1: 173 | final_result = [{"deleted_object_count": ret.deleted_count}] 174 | result = ResponseBasic(status="success", result=final_result).dict() 175 | self.run_hooks(operation="delete", model=model_name, data=result) 176 | return result 177 | 178 | def update(self, model_instance_data: dict, object_id: str, path: str): 179 | final_result = [] 180 | model_name = self.get_model_name(path) 181 | set_data = model_instance_data.dict() 182 | new_data = { "$set": set_data } 183 | query = {} 184 | query["filter"] = {} 185 | if object_id: 186 | query["filter"]["object_id"] = object_id 187 | cleaned_data = self.ingress_parse_object_id(query["filter"]) 188 | ret = self.base_connection[model_name].update_many(cleaned_data, new_data) 189 | if ret.modified_count >= 1: 190 | final_result = [{"updated_object_count": ret.modified_count}] 191 | result = ResponseBasic(status="success", result=final_result).dict() 192 | self.run_hooks(operation="update", model=model_name, data=result) 193 | return result -------------------------------------------------------------------------------- /backend/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/backend/conf/__init__.py -------------------------------------------------------------------------------- /backend/conf/confload.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | 4 | import logging 5 | import logging.config 6 | 7 | DEFAULT_CONFIG_FILENAME = "config.json" 8 | 9 | 10 | class Config: 11 | def __init__(self, config_filename=None): 12 | if config_filename is None: 13 | config_filename = DEFAULT_CONFIG_FILENAME 14 | 15 | with open(config_filename) as infil: 16 | data = json.load(infil) 17 | 18 | self.mongo_server_ip = data["mongo_server_ip"] 19 | self.mongo_server_port = data["mongo_server_port"] 20 | self.mongo_user = data["mongo_user"] 21 | self.mongo_password = data["mongo_password"] 22 | self.api_key = data["api_key"] 23 | self.api_key_name = data["api_key_name"] 24 | self.gunicorn_workers = data["gunicorn_workers"] 25 | self.listen_ip = data["cmdboss_listen_ip"] 26 | self.listen_port = data["cmdboss_listen_port"] 27 | self.cmdboss_http_https = data["cmdboss_http_https"] 28 | self.model_dir = data["model_dir"] 29 | self.hook_dir = data["hook_dir"] 30 | self.log_config_filename = data["log_config_filename"] 31 | self.num_thread_workers = data["num_thread_workers"] 32 | 33 | def setup_logging(self, max_debug=False): 34 | with open(self.log_config_filename) as infil: 35 | log_config_dict = yaml.load(infil, Loader=yaml_loader) 36 | 37 | if max_debug: 38 | for handler in log_config_dict["handlers"].values(): 39 | handler["level"] = "DEBUG" 40 | 41 | for logger in log_config_dict["loggers"].values(): 42 | logger["level"] = "DEBUG" 43 | 44 | log_config_dict["root"]["level"] = "DEBUG" 45 | 46 | logging.config.dictConfig(log_config_dict) 47 | log.info(f"confload: Logging setup @ {__name__}") 48 | 49 | 50 | config = Config() 51 | 52 | log = logging.getLogger(__name__) 53 | 54 | try: 55 | yaml_loader = yaml.CSafeLoader 56 | except AttributeError: 57 | yaml_loader = yaml.SafeLoader 58 | -------------------------------------------------------------------------------- /backend/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/backend/models/__init__.py -------------------------------------------------------------------------------- /backend/models/cmdboss_base_model.py: -------------------------------------------------------------------------------- 1 | 2 | from pydantic import Field, BaseModel 3 | from datetime import datetime 4 | 5 | 6 | class cmdboss_base_model(BaseModel): 7 | created: datetime = Field(default_factory=datetime.utcnow) 8 | updated: datetime = Field(default_factory=datetime.utcnow) 9 | -------------------------------------------------------------------------------- /backend/models/system.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | from enum import Enum 3 | from pydantic import BaseModel 4 | 5 | 6 | class StatusEnum(str, Enum): 7 | success = "success" 8 | error = "error" 9 | 10 | 11 | class ResponseBasic(BaseModel): 12 | status: StatusEnum 13 | result: list 14 | 15 | 16 | class SysModelIngest(BaseModel): 17 | base64_payload: str 18 | name: Optional[str] = None 19 | 20 | class Config: 21 | schema_extra = { 22 | "example": { 23 | "base64_payload": "ZnJvbSBweWRhbnRpYyBpbXBvcnQgQmFzZU1vZGVsCgpjbGFzcyBkZXZpY2UoQmFzZU1vZGVsKToKICAgIG5hbWU6IHN0cg==" 24 | } 25 | } 26 | 27 | 28 | class CMDBOSSQuery(BaseModel): 29 | filter: Optional[dict] = None 30 | 31 | class Config: 32 | schema_extra = { 33 | "example": { 34 | "filter": { 35 | "name": "bob" 36 | } 37 | } 38 | } 39 | 40 | 41 | class HookEvent(str, Enum): 42 | create = "create" 43 | retrieve = "retrieve" 44 | update = "update" 45 | delete = "delete" 46 | 47 | 48 | class HookModelArgs(BaseModel): 49 | model: Optional[str] = None 50 | order: int 51 | operation: HookEvent 52 | filter: Optional[dict] = None 53 | 54 | 55 | class Hook(BaseModel): 56 | name: str 57 | base64_payload: str 58 | events: List[HookModelArgs] 59 | -------------------------------------------------------------------------------- /backend/security/get_api_key.py: -------------------------------------------------------------------------------- 1 | from fastapi import Security, HTTPException 2 | from fastapi.security.api_key import APIKeyQuery, APIKeyCookie, APIKeyHeader 3 | from starlette.status import HTTP_403_FORBIDDEN 4 | 5 | from backend.conf.confload import config 6 | 7 | api_key_query = APIKeyQuery(name=config.api_key_name, auto_error=False) 8 | api_key_header = APIKeyHeader(name=config.api_key_name, auto_error=False) 9 | api_key_cookie = APIKeyCookie(name=config.api_key_name, auto_error=False) 10 | 11 | 12 | async def get_api_key( 13 | api_key_query: str = Security(api_key_query), 14 | api_key_header: str = Security(api_key_header), 15 | api_key_cookie: str = Security(api_key_cookie), 16 | ): 17 | """checks for an API key""" 18 | if api_key_query == config.api_key: 19 | return api_key_query 20 | elif api_key_header == config.api_key: 21 | return api_key_header 22 | elif api_key_cookie == config.api_key: 23 | return api_key_cookie 24 | else: 25 | raise HTTPException( 26 | status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials" 27 | ) -------------------------------------------------------------------------------- /backend/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/backend/util/__init__.py -------------------------------------------------------------------------------- /backend/util/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class CMDBOSSException(Exception): 4 | """Base CMDBOSS Exception""" 5 | 6 | 7 | class CMDBOSSFileExists(CMDBOSSException): 8 | """File Mgr File Already Exists""" 9 | 10 | 11 | class CMDBOSSHTTPException(Exception): 12 | """HTTP Exception Handler""" 13 | def __init__(self, status_code: int, result: list): 14 | self.status_code = status_code 15 | self.status = "error" 16 | self.result = result 17 | 18 | 19 | class CMDBOSSCallbackHTTPException(Exception): 20 | """Callback HTTP Exception Handler""" 21 | -------------------------------------------------------------------------------- /backend/util/file_mgr.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import base64 4 | import os 5 | 6 | from typing import Dict 7 | 8 | from backend.conf.confload import config 9 | 10 | from backend.models.system import ResponseBasic 11 | 12 | from backend.util.exceptions import ( 13 | CMDBOSSFileExists 14 | ) 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | class FileMgr: 20 | 21 | def __init__(self): 22 | self.path_lookup = { 23 | "model": {"path": config.model_dir, "extn": ".py"}, 24 | "hook": {"path": config.hook_dir, "extn": ".py"} 25 | } 26 | 27 | def create_file(self, payload: Dict[str, str]): 28 | raw_base = base64.b64decode(payload["base64_payload"]).decode('utf-8') 29 | template_path = self.path_lookup[payload["route_type"]]["path"] + payload["name"] + self.path_lookup[payload["route_type"]]["extn"] 30 | # if os.path.exists(template_path): 31 | # raise CMDBOSSFileExists 32 | with open(template_path, "w") as file: 33 | file.write(raw_base) 34 | resultdata = ResponseBasic(status="success", result=[{"created": payload["name"]}]).dict() 35 | return resultdata 36 | 37 | def delete_file(self, payload: Dict[str, str]): 38 | template_path = self.path_lookup[payload["route_type"]]["path"] + payload["name"] + self.path_lookup[payload["route_type"]]["extn"] 39 | os.remove(template_path) 40 | resultdata = ResponseBasic(status="success", result=[{"deleted": payload["name"]}]).dict() 41 | return resultdata 42 | 43 | def retrieve_file(self, payload: Dict[str, str]): 44 | template_path = self.path_lookup[payload["route_type"]]["path"] + payload["name"] + self.path_lookup[payload["route_type"]]["extn"] 45 | result = None 46 | with open(template_path, "r") as file: 47 | result = file.read() 48 | raw_base = base64.b64encode(result.encode('utf-8')) 49 | resultdata = ResponseBasic(status="success", result=[{"base64_payload": raw_base}]).dict() 50 | return resultdata 51 | 52 | def retrieve_files(self, payload: Dict[str, str]): 53 | path = self.path_lookup[payload["route_type"]]["path"] 54 | strip_exten = self.path_lookup[payload["route_type"]]["extn"] 55 | files = [] 56 | fileresult = [] 57 | for r, d, f in os.walk(path): 58 | for file in f: 59 | file.strip(path) 60 | files.append(os.path.join(r, file)) 61 | if len(files) > 0: 62 | for f in files: 63 | if "__init__" not in f: 64 | if "__pycache__" not in f: 65 | if strip_exten: 66 | if strip_exten in f: 67 | ftmpfile = f.replace(strip_exten, '') 68 | fileresult.append(ftmpfile.replace(path, '')) 69 | resultdata = ResponseBasic(status="success", result=fileresult).dict() 70 | return resultdata 71 | 72 | def func_retrieve_files(payload): 73 | fmgr = FileMgr() 74 | return fmgr.retrieve_files(payload) -------------------------------------------------------------------------------- /backend/util/model_loader.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from backend.cmdboss_db import cmdb_oss 4 | 5 | from backend.util.file_mgr import FileMgr 6 | 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | def reload_models(): 12 | q = {} 13 | fmgr = FileMgr() 14 | result = cmdb_oss.retrieve(query_obj=q, object_id=None, path=f"/models") 15 | # determine if reload required 16 | if len(result["result"]) >= 1: 17 | models_on_disk = fmgr.retrieve_files({"route_type": "model"}) 18 | if len(models_on_disk["result"]) >= 1: 19 | for running_model in models_on_disk["result"]: 20 | del_payload = { 21 | "route_type": "model", 22 | "name": running_model 23 | } 24 | fmgr.delete_file(del_payload) 25 | log.info(f"{models_on_disk}") 26 | for model in result["result"]: 27 | log.info(f'CMDBOSS: Reloading model {model["object_id"]}') 28 | model_code = { 29 | "base64_payload": model["base64_payload"], 30 | "route_type": "model", 31 | "name": model["name"] 32 | } 33 | fmgr.create_file(model_code) 34 | -------------------------------------------------------------------------------- /backend/util/oapi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | 4 | from fastapi.openapi.utils import get_openapi 5 | 6 | from backend.conf.confload import config 7 | from backend.util.exceptions import ( 8 | CMDBOSSCallbackHTTPException 9 | ) 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | def custom_openapi(app): 15 | openapi_schema = get_openapi( 16 | title="cmdboss", 17 | version="1.0.0", 18 | description="", 19 | routes=app.routes, 20 | ) 21 | openapi_schema["info"]["x-logo"] = { 22 | "url": "https://raw.githubusercontent.com/tbotnz/cmdboss/main/cmdboss.PNG" 23 | } 24 | app.openapi_schema = openapi_schema 25 | return app.openapi_schema 26 | -------------------------------------------------------------------------------- /backend/util/webhook_runner.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import json 3 | import logging 4 | 5 | from typing import Optional 6 | 7 | from backend.conf.confload import config 8 | 9 | from backend.util.file_mgr import FileMgr 10 | 11 | import uuid 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class hook_runner(FileMgr): 17 | 18 | def __init__(self, hook_code, hook_payload): 19 | super().__init__() 20 | self.hook_raw_name = str(uuid.uuid4()) 21 | self.hook_code = { 22 | "base64_payload": hook_code, 23 | "route_type": "hook", 24 | "name": self.hook_raw_name 25 | } 26 | self.hook_dir_path = config.hook_dir 27 | self.hook_args = hook_payload 28 | self.hook_name = self.hook_dir_path.replace("/",".") + self.hook_raw_name 29 | 30 | def hook_exec(self): 31 | try: 32 | self.create_file(self.hook_code) 33 | log.info(f"hook_exec: running hook {self.hook_name}") 34 | module = importlib.import_module(self.hook_name) 35 | run_whook = getattr(module, "run_hook") 36 | run_whook(payload=self.hook_args) 37 | self.delete_file(self.hook_code) 38 | except Exception as e: 39 | self.delete_file(self.hook_code) 40 | log.error(f"hook_exec: hook error {self.hook_name} error {e}") 41 | 42 | 43 | def exec_hook_func(payload: dict): 44 | code = payload["base64_payload"] 45 | payload = payload.get("payload", None) 46 | hook = hook_runner(hook_code=code, hook_payload=payload) 47 | hook.hook_exec() 48 | -------------------------------------------------------------------------------- /cboss-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/cboss-1.gif -------------------------------------------------------------------------------- /cboss-pman.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/cboss-pman.PNG -------------------------------------------------------------------------------- /cmdboss.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/cmdboss.PNG -------------------------------------------------------------------------------- /cmdboss.py: -------------------------------------------------------------------------------- 1 | 2 | import importlib 3 | 4 | from fastapi import FastAPI, Depends, Request 5 | from fastapi.responses import JSONResponse 6 | 7 | from backend.conf.confload import config 8 | from backend.security.get_api_key import get_api_key 9 | from backend.util.exceptions import ( 10 | CMDBOSSHTTPException 11 | ) 12 | from backend.util.oapi import custom_openapi 13 | from backend.util.model_loader import reload_models 14 | 15 | from routers import ( 16 | system, 17 | usr_models 18 | ) 19 | 20 | config.setup_logging(max_debug=True) 21 | 22 | 23 | app = FastAPI() 24 | 25 | app.include_router(system.router, dependencies=[Depends(get_api_key)]) 26 | 27 | 28 | @app.exception_handler(CMDBOSSHTTPException) 29 | async def unicorn_exception_handler( 30 | request: Request, 31 | exc: CMDBOSSHTTPException 32 | ): 33 | return JSONResponse( 34 | status_code=500, 35 | content={ 36 | "status": f"{exc.status}", 37 | "result": exc.result 38 | }, 39 | ) 40 | 41 | 42 | reload_models() 43 | importlib.reload(usr_models) 44 | app.include_router(usr_models.router, dependencies=[Depends(get_api_key)]) 45 | custom_openapi(app) 46 | 47 | 48 | @app.post("/reload-models", status_code=201) 49 | async def refresh(): 50 | reload_models() 51 | importlib.reload(usr_models) 52 | app.include_router(usr_models.router, dependencies=[Depends(get_api_key)]) 53 | custom_openapi(app) 54 | return True 55 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongo_server_ip":"10.0.2.15", 3 | "mongo_server_port":27017, 4 | "mongo_user":"root", 5 | "mongo_password":"root", 6 | "api_key_name": "x-api-key", 7 | "api_key": "2a84465a-cf38-46b2-9d86-b84Q7d57f288", 8 | "gunicorn_workers": 1, 9 | "cmdboss_http_https": "http", 10 | "cmdboss_listen_ip": "0.0.0.0", 11 | "cmdboss_listen_port": 9000, 12 | "model_dir": "extensibles/models/", 13 | "hook_dir": "extensibles/hooks/", 14 | "log_config_filename": "log-config.yml", 15 | "num_thread_workers": 20 16 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | 5 | cmdboss: 6 | build: 7 | context: . 8 | dockerfile: dockerfile 9 | ports: 10 | - "9000:9000" 11 | networks: 12 | - "cmdboss-network" 13 | depends_on: 14 | - mongo 15 | restart: always 16 | 17 | mongo: 18 | image: mongo 19 | restart: always 20 | ports: 21 | - "27017:27017" 22 | environment: 23 | MONGO_INITDB_ROOT_USERNAME: root 24 | MONGO_INITDB_ROOT_PASSWORD: root 25 | networks: 26 | - "cmdboss-network" 27 | 28 | networks: 29 | 30 | cmdboss-network: 31 | name: "cmdboss-network" -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | ADD requirements.txt /code/ 4 | RUN pip3 install -r /code/requirements.txt 5 | 6 | ADD . /code 7 | WORKDIR /code 8 | CMD gunicorn -c gunicorn.conf.py cmdboss:app -------------------------------------------------------------------------------- /extensibles/hooks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/extensibles/hooks/__init__.py -------------------------------------------------------------------------------- /extensibles/hooks/sample_webhook.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from typing import Optional 4 | 5 | """ 6 | default hook and boilerplate hook template 7 | IMPORTANT NOTES: 8 | - hook function name must be "run_hook" 9 | - payload is a dict containing the record 10 | """ 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | def run_hook(payload: Optional[dict] = None): 16 | try: 17 | # put your code here 18 | log.info("im a stupid hook") 19 | except Exception: 20 | log.error("im a stupid broken hook") -------------------------------------------------------------------------------- /extensibles/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/extensibles/models/__init__.py -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | CONFIG_FILENAME = "/code/config.json" 4 | 5 | 6 | def load_config_files(config_filename: str = CONFIG_FILENAME): 7 | try: 8 | with open(config_filename) as infil: 9 | return json.load(infil) 10 | except FileNotFoundError: 11 | raise FileNotFoundError 12 | 13 | 14 | data = load_config_files() 15 | 16 | bind = data["cmdboss_listen_ip"] + ":" + str(data["cmdboss_listen_port"]) 17 | workers = data["gunicorn_workers"] 18 | timeout = 3 * 60 19 | keepalive = 24 * 60 * 60 20 | worker_class = "uvicorn.workers.UvicornWorker" 21 | threads = 45 22 | -------------------------------------------------------------------------------- /log-config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | formatters: 4 | simple: 5 | # format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 6 | format: '[%(asctime)s:%(name)s:%(funcName)s:%(levelname)s] %(message)s' 7 | handlers: 8 | console: 9 | class: logging.StreamHandler 10 | level: DEBUG 11 | formatter: simple 12 | stream: ext://sys.stdout 13 | loggers: 14 | netmiko: 15 | level: DEBUG 16 | paramiko: 17 | level: DEBUG 18 | # rq.worker: # setting these levels appears to have no effect :( 19 | # level: WARN 20 | # rq: 21 | # level: WARN 22 | netpalm_worker_common: 23 | level: DEBUG 24 | netpalm_worker_pinned: 25 | level: DEBUG 26 | netpalm_worker_fifo: 27 | level: DEBUG 28 | backend: 29 | level: DEBUG 30 | routers: 31 | level: DEBUG 32 | 33 | 34 | root: 35 | # level: DEBUG 36 | handlers: [console] 37 | 38 | disable_existing_loggers: False -------------------------------------------------------------------------------- /marketecture-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/marketecture-1.png -------------------------------------------------------------------------------- /marketecture-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/marketecture-2.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.63.0 2 | pydantic==1.8.1 3 | uvicorn==0.13.4 4 | uvloop==0.15.2 5 | gunicorn==20.0.4 6 | aiofiles==0.6.0 7 | httptools==0.1.1 8 | pymongo==3.11.4 9 | pyyaml==5.4.1 10 | requests==2.25.1 -------------------------------------------------------------------------------- /routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tbotnz/cmdboss/924eeff20175ef4f4fb237042c26834b3ab6ecab/routers/__init__.py -------------------------------------------------------------------------------- /routers/route_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from fastapi import HTTPException 4 | 5 | from backend.util.exceptions import ( 6 | CMDBOSSHTTPException 7 | ) 8 | 9 | from contextlib import contextmanager 10 | import asyncio 11 | from functools import wraps 12 | 13 | from backend.models.system import ResponseBasic 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | class SyncAsyncDecoratorFactory: 19 | 20 | @contextmanager 21 | def wrapper(self, func, *args, **kwargs): 22 | yield 23 | 24 | def __call__(self, func): 25 | @wraps(func) 26 | def sync_wrapper(*args, **kwargs): 27 | with self.wrapper(func, *args, **kwargs): 28 | return func(*args, **kwargs) 29 | 30 | @wraps(func) 31 | async def async_wrapper(*args, **kwargs): 32 | with self.wrapper(func, *args, **kwargs): 33 | return await func(*args, **kwargs) 34 | 35 | if asyncio.iscoroutinefunction(func): 36 | return async_wrapper 37 | else: 38 | return sync_wrapper 39 | 40 | 41 | class HttpErrorHandler(SyncAsyncDecoratorFactory): 42 | @contextmanager 43 | def wrapper(self, *args, **kwargs): 44 | try: 45 | yield 46 | except asyncio.CancelledError: 47 | raise 48 | except Exception as e: 49 | import traceback 50 | log.exception(f"CMDBOSSHTTPException Log: {e}") 51 | error = traceback.format_exc().splitlines() 52 | raise CMDBOSSHTTPException(status_code=500, result=error) 53 | -------------------------------------------------------------------------------- /routers/system.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from typing import Optional 4 | 5 | from fastapi import APIRouter, Request, Query 6 | from fastapi.encoders import jsonable_encoder 7 | 8 | from backend.cmdboss_db import cmdb_oss 9 | 10 | from backend.models.system import ( 11 | SysModelIngest, 12 | ResponseBasic, 13 | Hook, 14 | CMDBOSSQuery 15 | ) 16 | 17 | from backend.util.exceptions import ( 18 | CMDBOSSHTTPException 19 | ) 20 | 21 | from routers.route_utils import HttpErrorHandler 22 | 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | router = APIRouter() 27 | 28 | 29 | @router.post( 30 | "/models/{model_name}", 31 | response_model=ResponseBasic, 32 | status_code=201 33 | ) 34 | @HttpErrorHandler() 35 | async def create_model( 36 | request: Request, 37 | model_payload: SysModelIngest, 38 | model_name: str 39 | ): 40 | model_payload.name = model_name 41 | model_exists = cmdb_oss.retrieve( 42 | query_obj={}, 43 | object_id=None, 44 | path=f"{request.url}" 45 | ) 46 | if len(model_exists["result"]) >= 1: 47 | raise CMDBOSSHTTPException( 48 | status_code=405, 49 | result="model {model_name} already exists!" 50 | ) 51 | result = cmdb_oss.insert( 52 | model_instance_data=model_payload, 53 | path=f"{request.url}" 54 | ) 55 | return jsonable_encoder(result) 56 | 57 | 58 | # delete routes 59 | @router.delete( 60 | f"/models/"+"{object_id}", 61 | response_model=ResponseBasic, 62 | status_code=200, 63 | summary=f"Delete a single model object" 64 | ) 65 | @HttpErrorHandler() 66 | async def delete_model( 67 | request: Request, 68 | query: Optional[CMDBOSSQuery] = None, 69 | object_id: Optional[str] = None 70 | ): 71 | q = {} 72 | if query: 73 | q = query.dict() 74 | result = cmdb_oss.delete( 75 | query=q, 76 | object_id=object_id, 77 | path=f"{request.url}" 78 | ) 79 | return jsonable_encoder(result) 80 | 81 | 82 | @router.get( 83 | f"/models", 84 | response_model=ResponseBasic, 85 | status_code=200, 86 | summary=f"Retrieve many model objects" 87 | ) 88 | @router.get( 89 | f"/models/"+"{object_id}", 90 | response_model=ResponseBasic, 91 | status_code=200, 92 | summary=f"Retrieve a single model object" 93 | ) 94 | @HttpErrorHandler() 95 | async def retrieve_models( 96 | request: Request, 97 | query: Optional[CMDBOSSQuery] = None, 98 | object_id: Optional[str] = None 99 | ): 100 | q = {} 101 | if query: 102 | q = query.dict() 103 | result = cmdb_oss.retrieve( 104 | query_obj=q, 105 | object_id=object_id, 106 | path=f"{request.url}" 107 | ) 108 | return jsonable_encoder(result) 109 | 110 | 111 | @router.patch( 112 | f"/models/"+"{object_id}", 113 | response_model=ResponseBasic, 114 | status_code=200, 115 | summary=f"Update a single model object" 116 | ) 117 | @HttpErrorHandler() 118 | async def update( 119 | model_payload: SysModelIngest, 120 | request: Request, 121 | object_id: Optional[str] = None 122 | ): 123 | result = cmdb_oss.update( 124 | model_instance_data=model_payload, 125 | object_id=object_id, 126 | path=f"{request.url}" 127 | ) 128 | return jsonable_encoder(result) 129 | 130 | 131 | @router.post( 132 | "/hooks", 133 | response_model=ResponseBasic, 134 | status_code=201 135 | ) 136 | @HttpErrorHandler() 137 | async def create_hook(hook_payload: Hook, request: Request): 138 | result = cmdb_oss.insert( 139 | model_instance_data=hook_payload, 140 | path=f"{request.url}" 141 | ) 142 | return jsonable_encoder(result) 143 | 144 | 145 | # delete routes 146 | @router.delete( 147 | f"/hooks/"+"{object_id}", 148 | response_model=ResponseBasic, 149 | status_code=200, 150 | summary=f"Delete a single hook object" 151 | ) 152 | @HttpErrorHandler() 153 | async def delete( 154 | request: Request, 155 | query: Optional[CMDBOSSQuery] = None, 156 | object_id: Optional[str] = None 157 | ): 158 | q = {} 159 | if query: 160 | q = query.dict() 161 | result = cmdb_oss.delete( 162 | query=q, 163 | object_id=object_id, 164 | path=f"{request.url}" 165 | ) 166 | return jsonable_encoder(result) 167 | 168 | 169 | @router.get( 170 | f"/hooks", 171 | response_model=ResponseBasic, 172 | status_code=200, 173 | summary=f"Retrieve many hook objects" 174 | ) 175 | @router.get( 176 | f"/hooks/"+"{object_id}", 177 | response_model=ResponseBasic, 178 | status_code=200, 179 | summary=f"Retrieve a single hook object" 180 | ) 181 | @HttpErrorHandler() 182 | async def retrieve_hooks( 183 | request: Request, 184 | query: Optional[CMDBOSSQuery] = None, 185 | object_id: Optional[str] = None 186 | ): 187 | q = {} 188 | if query: 189 | q = query.dict() 190 | result = cmdb_oss.retrieve( 191 | query_obj=q, 192 | object_id=object_id, 193 | path=f"{request.url}" 194 | ) 195 | return jsonable_encoder(result) 196 | 197 | 198 | @router.patch( 199 | f"/hooks/"+"{object_id}", 200 | response_model=ResponseBasic, 201 | status_code=200, 202 | summary=f"Update a single hook object" 203 | ) 204 | @HttpErrorHandler() 205 | async def update( 206 | model_payload: Hook, 207 | request: Request, 208 | object_id: Optional[str] = None 209 | ): 210 | result = cmdb_oss.update( 211 | model_instance_data=model_payload, 212 | object_id=object_id, 213 | path=f"{request.url}" 214 | ) 215 | return jsonable_encoder(result) 216 | -------------------------------------------------------------------------------- /routers/usr_models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import importlib 3 | 4 | from typing import List, Union, Optional 5 | 6 | from fastapi import APIRouter, Request, Query 7 | 8 | from fastapi.encoders import jsonable_encoder 9 | 10 | from backend.conf.confload import config 11 | 12 | from backend.cmdboss_db import cmdb_oss 13 | 14 | from backend.models.system import ( 15 | ResponseBasic, 16 | CMDBOSSQuery 17 | ) 18 | 19 | from routers.route_utils import ( 20 | HttpErrorHandler 21 | ) 22 | 23 | from backend.util.file_mgr import func_retrieve_files 24 | 25 | log = logging.getLogger(__name__) 26 | 27 | 28 | payload = {} 29 | payload["route_type"] = "model" 30 | 31 | router = APIRouter() 32 | 33 | routes = func_retrieve_files(payload) 34 | 35 | if len(routes["result"]) >= 1: 36 | for model_name in routes["result"]: 37 | try: 38 | 39 | # load model 40 | model_path_raw = config.model_dir 41 | model_path = model_path_raw.replace('/', '.') + model_name 42 | module = importlib.import_module(model_path) 43 | model_data = getattr(module, model_name) 44 | 45 | # create routes 46 | @router.post( 47 | f"/table/{model_name}", 48 | response_model=ResponseBasic, 49 | status_code=201, 50 | summary=f"Create one or many {model_name} objects" 51 | ) 52 | @HttpErrorHandler() 53 | async def create( 54 | model_payload: Union[model_data, List[model_data]], 55 | request: Request 56 | ): 57 | result = cmdb_oss.insert( 58 | model_instance_data=model_payload, 59 | path=f"{request.url}" 60 | ) 61 | return jsonable_encoder(result) 62 | 63 | # retrieve routes 64 | @router.get( 65 | f"/table/{model_name}", 66 | response_model=ResponseBasic, 67 | status_code=200, 68 | summary=f"Retrieve many {model_name} objects" 69 | ) 70 | @router.get( 71 | f"/table/{model_name}/"+"{object_id}", 72 | response_model=ResponseBasic, 73 | status_code=200, 74 | summary=f"Retrieve a single {model_name} object" 75 | ) 76 | @HttpErrorHandler() 77 | async def retrieve( 78 | request: Request, 79 | query: Optional[CMDBOSSQuery] = {}, 80 | object_id: Optional[str] = None 81 | ): 82 | q = {} 83 | if query: 84 | q = query.dict() 85 | result = cmdb_oss.retrieve( 86 | query_obj=q, 87 | object_id=object_id, 88 | path=f"{request.url}" 89 | ) 90 | return jsonable_encoder(result) 91 | 92 | # update routes 93 | @router.patch( 94 | f"/table/{model_name}/", 95 | response_model=ResponseBasic, 96 | status_code=200, 97 | summary=f"Update many {model_name} objects" 98 | ) 99 | @router.patch( 100 | f"/table/{model_name}/"+"{object_id}", 101 | response_model=ResponseBasic, 102 | status_code=200, 103 | summary=f"Update a single {model_name} object" 104 | ) 105 | @HttpErrorHandler() 106 | async def update( 107 | model_payload: model_data, 108 | request: Request, 109 | object_id: Optional[str] = None 110 | ): 111 | result = cmdb_oss.update( 112 | model_instance_data=model_payload, 113 | object_id=object_id, 114 | path=f"{request.url}" 115 | ) 116 | return jsonable_encoder(result) 117 | 118 | # delete routes 119 | @router.delete( 120 | f"/table/{model_name}", 121 | response_model=ResponseBasic, 122 | status_code=200, 123 | summary=f"Delete many {model_name} objects" 124 | ) 125 | @router.delete( 126 | f"/table/{model_name}/"+"{object_id}", 127 | response_model=ResponseBasic, 128 | status_code=200, 129 | summary=f"Delete a single {model_name} object" 130 | ) 131 | @HttpErrorHandler() 132 | async def delete( 133 | request: Request, 134 | query: Optional[CMDBOSSQuery] = {}, 135 | object_id: Optional[str] = None 136 | ): 137 | q = {} 138 | if query: 139 | q = query.dict() 140 | result = cmdb_oss.delete( 141 | query=q, 142 | object_id=object_id, 143 | path=f"{request.url}" 144 | ) 145 | return jsonable_encoder(result) 146 | 147 | except Exception as e: 148 | log.error( 149 | f"user model error: error {e} loading \ 150 | model {model_name} - \ 151 | please check the class name matches the model name" 152 | ) 153 | --------------------------------------------------------------------------------