├── .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 | 
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 |
--------------------------------------------------------------------------------