├── .gitignore ├── LICENSE ├── README.md ├── README_cn.md ├── exmaples ├── ci.py └── cr.py ├── pyproject.toml ├── src └── cmdb │ ├── __init__.py │ ├── client.py │ └── core │ ├── __init__.py │ ├── auth.py │ ├── ci.py │ ├── ci_relations.py │ ├── exc.py │ ├── models.py │ └── policy.py └── tests ├── test_ci.py └── test_cr.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .pytest_cache/ 3 | __pycache__/ 4 | /dist 5 | /build 6 | *.egg-info/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CMDB sdk for Python 2 | 3 | [中文](README_cn.md) / [English](READMEn.md) 4 | 5 | ## build 6 | 7 | build and install 8 | 9 | ```shell 10 | > git clone https://github.com/veops/cmdb-sdk-python.git 11 | > cd cmdb-sdk-python 12 | 13 | > pip install build wheel setuptools # requirements 14 | > python -m build -n 15 | ``` 16 | 17 | **veops_cmdb-0.0.1-py3-none-any.whl** will be created at *cmdb-sdk-python/dist/* 18 | 19 | ## tests 20 | 21 | before running tests, please make sure you have created the ci model book and rank, 22 | they may looks like: 23 | 24 | ```plain 25 | book 26 | id: int, unique 27 | book_id: int, unique, not null 28 | book_name: string, not null, 29 | author: string 30 | 31 | rank: 32 | id: int, unique 33 | rank_id: int, unique, not null 34 | rank: int, not null, 35 | ``` 36 | 37 | and you alse need to form relationship between book and rank, and then you can run tests. 38 | 39 | ```shell 40 | > pytest -s 41 | ``` 42 | 43 | ## usage 44 | 45 | If you can ensure the security of your account, you can set the following parameters to environment variables, 46 | this way, you won't need to manually pass the initial parameters every time to set up the client. 47 | 48 | ```plain 49 | CMDB_HOST=[your host] 50 | CMDB_KEY=[your_key] 51 | CMDB_SECRET=[your secret] 52 | ``` 53 | 54 | ### 1.CI 55 | 56 | ```python3 57 | """ 58 | suppose a ci model is Book(id, book_id, book_name, author) 59 | """ 60 | 61 | import os 62 | 63 | from cmdb.core.ci import CIClient, Option 64 | 65 | 66 | class TestCI: 67 | 68 | def setup_method(self) -> None: 69 | opt = Option( 70 | os.environ["CMDB_HOST"], 71 | os.environ["CMDB_KEY"], 72 | os.environ["CMDB_SECRET"], 73 | ) 74 | self.client = CIClient(opt=opt) 75 | 76 | def test_get(self): 77 | resp = self.client.get_ci(q="_type:book").result 78 | print("get") 79 | print(resp) 80 | 81 | def find_by_name(self, book_name: str): 82 | q = f"_type:book,book_name:{book_name}" 83 | resp = self.client.get_ci(q=q).result 84 | if not resp: 85 | return 86 | return resp[0] 87 | 88 | def test_add(self): 89 | ci = { 90 | "id": 1, 91 | "book_id": 1, 92 | "book_name": "平凡的世界", 93 | "author": "路遥", 94 | } 95 | if self.find_by_name("平凡的世界"): 96 | return 97 | resp = self.client.add_ci("book", ci) 98 | print("add") 99 | print(resp) 100 | 101 | def test_update(self): 102 | ci = self.find_by_name("平凡的世界") 103 | resp = self.client.update_ci("book", ci_id=ci["_id"], attrs={"author": "yao.lu"}) 104 | print("update") 105 | print(resp) 106 | 107 | def test_delete(self): 108 | ci = self.find_by_name("平凡的世界") 109 | resp = self.client.delete_ci(ci["_id"]) 110 | print("delete") 111 | print(resp) 112 | 113 | ``` 114 | 115 | ### 2.CIRelation 116 | 117 | ```python3 118 | """ 119 | suppose a CI model book with field id, name and author, 120 | suppose a CI model rank with field rank_id, book_id, rank 121 | """ 122 | 123 | 124 | import os 125 | 126 | from cmdb.core.ci import CIClient, Option 127 | from cmdb.core.ci_relations import CIRelationClient 128 | 129 | 130 | class TestCI: 131 | 132 | def setup_method(self) -> None: 133 | opt = Option( 134 | os.environ["CMDB_HOST"], 135 | os.environ["CMDB_KEY"], 136 | os.environ["CMDB_SECRET"], 137 | ) 138 | self.ci_client = CIClient(opt=opt) 139 | self.client = CIRelationClient(opt=opt) 140 | self.add_ci() 141 | 142 | def add_ci(self): 143 | book = { 144 | "id": 1, 145 | "book_id": 1, 146 | "book_name": "平凡的世界", 147 | "author": "路遥", 148 | } 149 | rank = { 150 | "id": 1, 151 | "rank_id": 1, 152 | "rank": 5, 153 | } 154 | if not self.find_ci(q="_type:book,book_id:1"): 155 | self.ci_client.add_ci("book", book) 156 | if not self.find_ci(q="_type:rank,rank_id:1"): 157 | self.ci_client.add_ci("rank", rank) 158 | 159 | def find_ci(self, q: str): 160 | resp = self.ci_client.get_ci(q=q).result 161 | if not resp: 162 | return 163 | return resp[0] 164 | 165 | def test_add_ci_relation(self): 166 | book = self.find_ci(q="_type:book,book_id:1") 167 | rank = self.find_ci(q="_type:rank,rank_id:1") 168 | resp = self.client.add_ci_relation(book["_id"], rank["_id"]) 169 | print("add result", resp) 170 | 171 | def test_get_cli_relation(self): 172 | book = self.find_ci(q="_type:book,book_id:1") 173 | resp = self.client.get_ci_relation(root_id=book["_id"]).result 174 | print("get result", resp) 175 | 176 | def test_delete_relation(self): 177 | book = self.find_ci(q="_type:book,book_id:1") 178 | rank = self.find_ci(q="_type:rank,rank_id:1") 179 | resp = self.client.delete_ci_relation(src_ci_id=book["_id"], dst_ci_id=rank["_id"]) 180 | print("delete result", resp) 181 | 182 | ``` 183 | 184 | ## examples 185 | 186 | for full usage examples, please visit [exmaples](./exmaples/) . 187 | -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # CMDB sdk for Python 2 | 3 | [English](READMEn.md) / [中文](README_cn.md) 4 | 5 | CMDB python客户端工具 6 | 7 | ```shell 8 | > from cmdb import get_client 9 | > cli = get_client() 10 | > books = cli.get_ci(q="_type:book") 11 | ``` 12 | 13 | ## 编译 14 | 15 | 以下为编译和安装脚本,编译前需要安装依赖*build*、*wheel*和*setuptools* 16 | 17 | ```shell 18 | # 克隆源码 19 | > git clone https://github.com/veops/cmdb-sdk-python.git 20 | > cd cmdb-sdk-python 21 | 22 | # 安装依赖 23 | > pip install build wheel setuptools # requirements 24 | 25 | # 打包wheel -n指不在独立的虚拟环境中编译 26 | > python -m build -n 27 | ``` 28 | 29 | **veops_cmdb-0.0.1-py3-none-any.whl** 将会在 *cmdb-sdk-python/dist/*目录创建,使用pip安装即可 30 | 31 | ## 测试 32 | 33 | 在测试代码之前,需要正在CMDB中创建如下两个CI模型: 34 | 35 | ```plain 36 | book 37 | id: int, unique 38 | book_id: int, unique, not null 39 | book_name: string, not null, 40 | author: string 41 | 42 | rank: 43 | id: int, unique 44 | rank_id: int, unique, not null 45 | rank: int, not null, 46 | ``` 47 | 48 | 然后你需要为book和rank创建关联关系,一切准备就绪后,使用如下代码来执行测试: 49 | 50 | ```shell 51 | > pytest -s 52 | ``` 53 | 54 | ## 使用 55 | 56 | Client初始化需要传入配置参数,配置参数封装在`Option`类中,主要包含 __url__、__key__ 和 __secret__ 参数,如果你能保证当前环境为个人使用的安全环境,也可以直接将上述信息加入环境变量,这样可以省去每次初始化的参数传入 57 | 58 | ```plain 59 | CMDB_HOST=[your host] 60 | CMDB_KEY=[your_key] 61 | CMDB_SECRET=[your secret] 62 | ``` 63 | 64 | ### 1.CI 65 | 66 | ```python3 67 | """ 68 | suppose a ci model is Book(id, book_id, book_name, author) 69 | """ 70 | 71 | import os 72 | 73 | from cmdb.core.ci import CIClient, Option 74 | 75 | 76 | class TestCI: 77 | 78 | def setup_method(self) -> None: 79 | opt = Option( 80 | os.environ["CMDB_HOST"], 81 | os.environ["CMDB_KEY"], 82 | os.environ["CMDB_SECRET"], 83 | ) 84 | self.client = CIClient(opt=opt) 85 | 86 | def test_get(self): 87 | resp = self.client.get_ci(q="_type:book").result 88 | print("get") 89 | print(resp) 90 | 91 | def find_by_name(self, book_name: str): 92 | q = f"_type:book,book_name:{book_name}" 93 | resp = self.client.get_ci(q=q).result 94 | if not resp: 95 | return 96 | return resp[0] 97 | 98 | def test_add(self): 99 | ci = { 100 | "id": 1, 101 | "book_id": 1, 102 | "book_name": "平凡的世界", 103 | "author": "路遥", 104 | } 105 | if self.find_by_name("平凡的世界"): 106 | return 107 | resp = self.client.add_ci("book", ci) 108 | print("add") 109 | print(resp) 110 | 111 | def test_update(self): 112 | ci = self.find_by_name("平凡的世界") 113 | resp = self.client.update_ci("book", ci_id=ci["_id"], attrs={"author": "yao.lu"}) 114 | print("update") 115 | print(resp) 116 | 117 | def test_delete(self): 118 | ci = self.find_by_name("平凡的世界") 119 | resp = self.client.delete_ci(ci["_id"]) 120 | print("delete") 121 | print(resp) 122 | 123 | ``` 124 | 125 | ### 2.CIRelation 126 | 127 | ```python3 128 | """ 129 | suppose a CI model book with field id, name and author, 130 | suppose a CI model rank with field rank_id, book_id, rank 131 | """ 132 | 133 | 134 | import os 135 | 136 | from cmdb.core.ci import CIClient, Option 137 | from cmdb.core.ci_relations import CIRelationClient 138 | 139 | 140 | CR_ID = None 141 | 142 | 143 | class TestCI: 144 | 145 | def setup_method(self) -> None: 146 | opt = Option( 147 | os.environ["CMDB_HOST"], 148 | os.environ["XDATA_KEY"], 149 | os.environ["XDATA_SECRET"], 150 | ) 151 | self.ci_client = CIClient(opt=opt) 152 | self.client = CIRelationClient(opt=opt) 153 | # self.add_ci() 154 | 155 | def add_ci(self): 156 | book = { 157 | "id": 1, 158 | "book_id": 1, 159 | "book_name": "平凡的世界", 160 | "author": "路遥", 161 | } 162 | rank = { 163 | "id": 1, 164 | "rank_id": 1, 165 | "rank": 5, 166 | } 167 | self.ci_client.add_ci("book", book) 168 | self.ci_client.add_ci("rank", rank) 169 | 170 | def find_ci(self, q: str): 171 | resp = self.ci_client.get_ci(q=q).result 172 | if not resp: 173 | return 174 | return resp[0] 175 | 176 | def test_add_ci_relation(self): 177 | book = self.find_ci(q="_type:book,book_id:1") 178 | rank = self.find_ci(q="_type:rank,rank_id:1") 179 | resp = self.client.add_ci_relation(book["_id"], rank["_id"]) 180 | global CR_ID 181 | CR_ID = resp.cr_id 182 | print("add result", resp) 183 | 184 | def test_get_cli_relation(self): 185 | book = self.find_ci(q="_type:book,book_id:1") 186 | resp = self.client.get_ci_relation(root_id=book["_id"]).result 187 | print("get result", resp) 188 | 189 | def test_delete(self): 190 | global CR_ID 191 | print(CR_ID) 192 | resp = self.client.delete_ci_relation(cr_id=CR_ID) 193 | print("delete result", resp) 194 | 195 | ``` 196 | 197 | ## examples 198 | 199 | 完整示例代码可以访问[exmaples](./exmaples/)查看. 200 | -------------------------------------------------------------------------------- /exmaples/ci.py: -------------------------------------------------------------------------------- 1 | """ 2 | @date: 2023-08-21 3 | @desc: 4 | suppose a CI model book with field book_id, name and author, 5 | this file show how to add, get, update and delete books. 6 | """ 7 | 8 | from cmdb import Option, get_client 9 | 10 | 11 | URL = "" 12 | KEY ="" 13 | SECRET = "" 14 | 15 | 16 | def add_book(): 17 | opt = Option(url=URL, key=KEY, secret=SECRET) 18 | cli = get_client(opt) 19 | book = {"id": 1, "book_id": 1, "book_name": "平凡的世界", "author": "路遥"} 20 | resp = cli.add_ci("book", book) 21 | print(resp.ci_id) # suppose the return ci_id is 2, will be used in update and delete 22 | 23 | 24 | def update_book(): 25 | """update with ci_id""" 26 | opt = Option(url=URL, key=KEY, secret=SECRET) 27 | cli = get_client(opt) 28 | book = {"book_name": "《平凡的世界》"} 29 | cli.update_ci("book", ci_id=2, attrs=book) 30 | 31 | 32 | def update_book2(): 33 | """update with unique key in model""" 34 | opt = Option(url=URL, key=KEY, secret=SECRET) 35 | cli = get_client(opt) 36 | # suppose book_id is the unique key of book 37 | book = {"book_name": "《平凡的世界》"} 38 | cli.update_ci("book", attrs=book, book_id=1) 39 | 40 | 41 | def get_book(): 42 | opt = Option(url=URL, key=KEY, secret=SECRET) 43 | cli = get_client(opt) 44 | resp = cli.get_ci(q="_type:book") 45 | books = resp.result 46 | print(books) 47 | 48 | 49 | def delete_book(): 50 | opt = Option(url=URL, key=KEY, secret=SECRET) 51 | cli = get_client(opt) 52 | resp = cli.delete_ci(ci_id=2) 53 | print(resp.message) 54 | -------------------------------------------------------------------------------- /exmaples/cr.py: -------------------------------------------------------------------------------- 1 | """ 2 | @date: 2023-08-21 3 | @desc: 4 | suppose a CI model book with field id, book_id, book_name and author, 5 | suppose a CI model rank with field id, rank_id, rank 6 | this file show how to form a relationship between book and rank. 7 | """ 8 | 9 | from cmdb import Option, get_client 10 | 11 | 12 | URL = "" 13 | KEY ="" 14 | SECRET = "" 15 | 16 | 17 | def add_relation(): 18 | opt = Option(url=URL, key=KEY, secret=SECRET) 19 | cli = get_client(opt) 20 | book = {"id": 1,"book_id": 1, "book_name": "平凡的世界", "author": "路遥"} 21 | rank = {"id": 1, "rank_id": 5, rank: 11} 22 | book_add_resp = cli.add_ci("book", book) 23 | rank_add_resp = cli.add_ci("rank", rank) 24 | resp = cli.add_ci_relation(book_add_resp.ci_id, rank_add_resp.ci_id) 25 | print(resp.cr_id) # suppose cr_id is 7, will be used in delete 26 | 27 | 28 | def get_relation(): 29 | opt = Option(url=URL, key=KEY, secret=SECRET) 30 | cli = get_client(opt) 31 | book = cli.get_ci(q="_type:book,book_id:1").result[0] 32 | resp = cli.get_ci_relation(book["_id"]) 33 | print(resp.result) 34 | 35 | 36 | def delete_relation(): 37 | opt = Option(url=URL, key=KEY, secret=SECRET) 38 | cli = get_client(opt) 39 | book = cli.get_ci(q="_type:book,book_id:1").result[0] 40 | rank = cli.get_ci(q="_type:book,rank_id:1").result[0] 41 | resp = cli.delete_ci_relation(src_ci_id=book["_id"], dst_ci_id=rank["_id"]) 42 | print(resp.message) 43 | 44 | 45 | def delete_relation2(): 46 | opt = Option(url=URL, key=KEY, secret=SECRET) 47 | cli = get_client(opt) 48 | resp = cli.delete_ci_relation(cr_id=7) # assuming 7 is the cr_id 49 | print(resp.message) 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "veops_cmdb" 7 | description = "sdk for veops cmdb" 8 | classifiers = [ 9 | "Programming Language :: Python :: 3", 10 | "Programming Language :: Python :: 3.7", 11 | "Programming Language :: Python :: 3.8", 12 | "Programming Language :: Python :: 3.9", 13 | "Programming Language :: Python :: 3.10", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | ] 17 | requires-python = ">=3.7" 18 | dependencies = ["requests"] 19 | dynamic = ["version"] 20 | 21 | 22 | [tool.setuptools.dynamic] 23 | version = { attr = "cmdb.__version__" } 24 | 25 | 26 | [tool.pytest.ini_options] 27 | addopts = ["--import-mode=importlib"] 28 | pythonpath = "src" 29 | -------------------------------------------------------------------------------- /src/cmdb/__init__.py: -------------------------------------------------------------------------------- 1 | from cmdb.core.models import * 2 | from cmdb.core.ci import CIClient 3 | from cmdb.core.ci_relations import CIRelationClient 4 | from cmdb.client import Client, get_client 5 | 6 | 7 | __version__ = "0.0.1" 8 | -------------------------------------------------------------------------------- /src/cmdb/client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from cmdb.core.ci import CIClient 4 | from cmdb.core.ci_relations import CIRelationClient 5 | from cmdb.core.models import * 6 | 7 | 8 | class Client: 9 | """ 10 | CMDB handle client 11 | 12 | Attributes: 13 | opt: initialize arugument, if None input, will initiallize with enviroment arguments 14 | 15 | Example: 16 | 17 | 1. initialize with arguments 18 | 19 | > opt = Option(url="https://yourhost.com/api/v0.1", key=your_key, secret=your_secret) 20 | 21 | > client = Client(opt) 22 | 23 | 2. initialize with enviorment arguments 24 | 25 | > client = Client() 26 | 27 | """ 28 | 29 | def __init__(self, opt: Optional[Option] = None): 30 | self.ci = CIClient(opt) 31 | self.cr = CIRelationClient(opt) 32 | 33 | def add_ci( 34 | self, 35 | ci_type: str, 36 | attrs: dict, 37 | no_attribute_policy: NoAttributePolicy = NoAttributePolicy.default(), 38 | exist_policy: ExistPolicy = ExistPolicy.default(), 39 | ) -> CICreateRsp: 40 | """ 41 | create new ci instance 42 | 43 | eg: suppose a ci model with fields [id, name, age], and its ci_type is "Human" 44 | > client.add_ci("Human", {"id": 1, "name": "a", "age": 10}) 45 | 46 | Args: 47 | ci_type: ci model type 48 | attrs: fields of ci to add 49 | no_attribute_policy: default to ignore not existed attributes update operation, optional value include IGNORE|REJECT 50 | exist_policy: default to reject add new ci if exists, optional value include NEED|REJECT|REPLACE 51 | 52 | Retrurns: 53 | CMDB create operation result 54 | """ 55 | return self.ci.add_ci(ci_type, attrs, no_attribute_policy, exist_policy) 56 | 57 | def get_ci( 58 | self, 59 | q: str, 60 | fl: Optional[str] = None, 61 | facet: Optional[str] = None, 62 | count: int = 25, 63 | page: int = 1, 64 | sort: Optional[str] = None, 65 | ret_key: RetKey = RetKey.default(), 66 | ) -> CIRetrieveRsp: 67 | """ 68 | get ci instance 69 | 70 | get target results by search expression 71 | for more information, please reference to veops cmdb guidance [here](https://github.com/veops/cmdb/blob/master/docs/cmdb_api.md). 72 | 73 | Args: 74 | q: search expression, may looks like "_type:Human,name:a" 75 | fl: ret attrubute, split by comma 76 | facet: staticstics 77 | count: ci count per page 78 | page: target page num 79 | sort: sort by target attribute, use `-attr` for descending 80 | ret_key: ret field name, optional values include ID|NAME|ALIAS 81 | 82 | Returns: 83 | target ci results 84 | """ 85 | return self.ci.get_ci(q, fl, facet, count, page, sort, ret_key) 86 | 87 | def update_ci( 88 | self, 89 | ci_type: str, 90 | *, 91 | ci_id: Optional[int] = None, 92 | attrs: Optional[dict] = None, 93 | no_attribute_policy: NoAttributePolicy = NoAttributePolicy.default(), 94 | **kwargs, 95 | ) -> CIUpdateRsp: 96 | """ 97 | update ci attrs 98 | 99 | eg: suppose a ci model with fields [id, name, age], and its ci_type is "Human" 100 | a ci is ci(id=1, name="a", age=10) 101 | update operation may like: 102 | > client.update_ci("Human", ci_id=1, attrs={"age": 11}) 103 | or 104 | > client.update_ci("Human", attrs={"age": 11}, name="a") # in this case, name must be unique in ci model 105 | 106 | Args: 107 | ci_type: ci model type 108 | ci_id: keyword agument only, the id of ci 109 | attrs: keyword agument only, fields to update 110 | no_attribute_policy: default to ignore not existed attributes update operation, optional value include IGNORE|REJECT 111 | 112 | Retrurns: 113 | CMDB update operation result 114 | """ 115 | return self.ci.update_ci(ci_type, ci_id=ci_id, attrs=attrs, no_attribute_policy=no_attribute_policy, **kwargs) 116 | 117 | def delete_ci(self, ci_id: int) -> CIDeleteRsp: 118 | """ 119 | delete a ci by its ci_id 120 | 121 | eg: suppose a ci model with fields [id, name, age], and its ci_type is "Human" 122 | a ci is ci(id=1, name="a", age=10) 123 | delete operation may like: 124 | > client.delete(1) 125 | 126 | Args: 127 | ci_id: ci id for the ci to delete 128 | 129 | Retrurns: 130 | CMDB delete operation result 131 | """ 132 | return self.ci.delete_ci(ci_id) 133 | 134 | def add_ci_relation( 135 | self, 136 | src_ci_id: int, 137 | dst_ci_id: int, 138 | ) -> CIRelationCreateRsp: 139 | """ 140 | create new ci_relation instance 141 | 142 | Args: 143 | src_ci_id: id of source ci 144 | src_ci_id: id of destination ci 145 | 146 | Retrurns: 147 | ci_relation create operation result 148 | """ 149 | return self.cr.add_ci_relation(src_ci_id, dst_ci_id) 150 | 151 | def get_ci_relation( 152 | self, 153 | root_id: int, 154 | level: Optional[str] = None, 155 | reverse: int = 0, 156 | q: Optional[str] = None, 157 | fl: Optional[str] = None, 158 | facet: Optional[str] = None, 159 | count: int = 25, 160 | page: int = 1, 161 | sort: Optional[str] = None, 162 | ret_key: RetKey = RetKey.default(), 163 | ) -> CIRelationRetrieveRsp: 164 | """ 165 | get ci_relation instance 166 | 167 | get target relation by root id 168 | for more information, please refrence to veops cmdb guidance [here](https://github.com/veops/cmdb/blob/master/docs/cmdb_api.md). 169 | 170 | Args: 171 | root_id: ci id of root node 172 | level: levels of relationship, split by comma 173 | reverse: Reverse search or not, 0 for no and 1 for yes, default is 0 174 | q: search expression, may looks like "_type:Human,name:a" 175 | fl: ret attrubute, split by comma 176 | facet: staticstics 177 | count: ci count per page 178 | page: target page num 179 | sort: sort by target attribute, use `-attr` for descending 180 | ret_key: ret field name, optional values include ID|NAME|ALIAS 181 | 182 | Returns: 183 | target ci_relation results 184 | """ 185 | return self.cr.get_ci_relation(root_id, level, reverse, q, fl, facet, count, page, sort, ret_key) 186 | 187 | def delete_ci_relation( 188 | self, 189 | *, 190 | cr_id: Optional[int] = None, 191 | src_ci_id: Optional[int] = None, 192 | dst_ci_id: Optional[int] = None, 193 | ) -> CIRelationDeleteRsp: 194 | """ 195 | to delete the cr, either the cr_id or a combination of dst_ci_id and src_ci_id can be used 196 | 197 | example: 198 | 199 | 1. delete by cr_id 200 | 201 | > client.delete_ci_relation(cr_id=1) 202 | 203 | 2. delete by src_ci_id and dst_ci_id 204 | 205 | > client.delete_ci_relation(src_ci_id=1, dst_ci_id=2) 206 | 207 | Args: 208 | cr_id: cr id for the ci_relation want to delete 209 | src_ci_id: id of source ci 210 | src_ci_id: id of destination ci 211 | 212 | Retrurns: 213 | CMDB delete operation result 214 | """ 215 | return self.cr.delete_ci_relation(cr_id=cr_id, src_ci_id=src_ci_id, dst_ci_id=dst_ci_id) 216 | 217 | 218 | def get_client(opt: Optional[Option] = None) -> Client: 219 | """ 220 | get CMDB handle client for ci and ci_relation 221 | 222 | Args: 223 | opt: option for client, if not support, get required info from enviroment 224 | """ 225 | return Client(opt) 226 | -------------------------------------------------------------------------------- /src/cmdb/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veops/cmdb-sdk-python/84a26be3ab25d756cd8260bf5157c5dd3015795a/src/cmdb/core/__init__.py -------------------------------------------------------------------------------- /src/cmdb/core/auth.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def build_api_key(key: str, secret: str, path: str, params: dict) -> dict: 5 | values = [ 6 | str(params[k]) for k in sorted(params.keys()) 7 | if (k not in ("_key", "_secret") and not isinstance(params[k], (dict, list)) and params[k] is not None) 8 | ] 9 | values = "".join(values) or "" 10 | _secret = "".join([path, secret, values]).encode("utf-8") 11 | params["_secret"] = hashlib.sha1(_secret).hexdigest() 12 | params["_key"] = key 13 | return params 14 | -------------------------------------------------------------------------------- /src/cmdb/core/ci.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | from typing import Optional 3 | 4 | import requests 5 | 6 | from cmdb.core.auth import build_api_key 7 | from cmdb.core.models import * 8 | from cmdb.core.policy import RetKey 9 | from cmdb.core.exc import CMDBError 10 | 11 | 12 | class CIClient: 13 | """ 14 | CMDB CI object handle client 15 | 16 | Attributes: 17 | opt: initialize arugument, if None input, will initiallize with enviroment arguments 18 | 19 | Example: 20 | 21 | 1. initialize with arguments 22 | 23 | > opt = Option(url="https://yourhost.com/api/v0.1", key=your_key, secret=your_secret) 24 | 25 | > client = CIClient(opt) 26 | 27 | 2. initialize with enviorment arguments 28 | 29 | > client = CIClient() 30 | 31 | """ 32 | 33 | def __init__(self, opt: Optional[Option] = None): 34 | self.opt = opt if opt else Option() 35 | self.session = requests.Session() 36 | self.url = f"{self.opt.url}/ci" 37 | 38 | def _build_api_key(self, url: str, payload: dict) -> dict: 39 | return build_api_key(self.opt.key, self.opt.secret, urlparse(url).path, payload) 40 | 41 | def _check_err(self, resp: dict): 42 | msg = resp.get("message") 43 | if msg: 44 | raise CMDBError(msg) 45 | 46 | def _add_ci(self, params: CICreateReq) -> CICreateRsp: 47 | url = self.url 48 | payload = self._build_api_key(url, params.to_params()) 49 | resp = self.session.post(url, json=payload).json() 50 | self._check_err(resp) 51 | return CICreateRsp(**resp) 52 | 53 | def _get_ci(self, params: CIRetrieveReq) -> CIRetrieveRsp: 54 | url = f"{self.url}/s" 55 | payload = params.to_params() 56 | payload = self._build_api_key(url, payload) 57 | resp = self.session.get(url, params=payload).json() 58 | self._check_err(resp) 59 | return CIRetrieveRsp(**resp) 60 | 61 | def _update_ci(self, ci_id: Optional[int], params: CIUpdateReq) -> CIUpdateRsp: 62 | if ci_id: 63 | url = f"{self.url}/{ci_id}" 64 | else: 65 | if not params.unique_key.keys(): 66 | raise CMDBError("if not use ci_id, unique key must in request params") 67 | url = self.url 68 | payload = self._build_api_key(url, params.to_params()) 69 | resp = self.session.put(url, json=payload).json() 70 | self._check_err(resp) 71 | return CIUpdateRsp(**resp) 72 | 73 | def _delete_ci(self, params: CIDeleteReq) -> CIDeleteRsp: 74 | url = f"{self.url}/{params.ci_id}" 75 | payload = self._build_api_key(url, {}) 76 | resp = self.session.delete(url, json=payload).json() 77 | return CIDeleteRsp(**resp) 78 | 79 | def add_ci( 80 | self, 81 | ci_type: str, 82 | attrs: dict, 83 | no_attribute_policy: NoAttributePolicy = NoAttributePolicy.default(), 84 | exist_policy: ExistPolicy = ExistPolicy.default(), 85 | ) -> CICreateRsp: 86 | """ 87 | create new ci instance 88 | 89 | eg: suppose a ci model with fields [id, name, age], and its ci_type is "Human" 90 | 91 | > client.add_ci("Human", {"id": 1, "name": "a", "age": 10}) 92 | 93 | Args: 94 | ci_type: ci model type 95 | attrs: fields of ci to add 96 | no_attribute_policy: default to ignore not existed attributes update operation, optional value include IGNORE|REJECT 97 | exist_policy: default to reject add new ci if exists, optional value include NEED|REJECT|REPLACE 98 | 99 | Retrurns: 100 | CMDB create operation result 101 | """ 102 | param = CICreateReq(ci_type, no_attribute_policy, exist_policy, attrs) 103 | return self._add_ci(param) 104 | 105 | def get_ci( 106 | self, 107 | q: str, 108 | fl: Optional[str] = None, 109 | facet: Optional[str] = None, 110 | count: int = 25, 111 | page: int = 1, 112 | sort: Optional[str] = None, 113 | ret_key: RetKey = RetKey.default(), 114 | ) -> CIRetrieveRsp: 115 | """ 116 | get ci instance 117 | 118 | get target results by search expression 119 | for more information, please refrence to veops cmdb guidance [here](https://github.com/veops/cmdb/blob/master/docs/cmdb_api.md). 120 | 121 | Args: 122 | q: search expression, may looks like "_type:Human,name:a" 123 | fl: ret attrubute, split by comma 124 | facet: staticstics 125 | count: ci count per page 126 | page: target page num 127 | sort: sort by target attribute, use `-attr` for descending 128 | ret_key: ret field name, optional values include ID|NAME|ALIAS 129 | 130 | Returns: 131 | target ci results 132 | """ 133 | params = CIRetrieveReq(q, fl, facet, count, page, sort, ret_key) 134 | return self._get_ci(params) 135 | 136 | def update_ci( 137 | self, 138 | ci_type: str, 139 | *, 140 | ci_id: Optional[int] = None, 141 | attrs: Optional[dict] = None, 142 | no_attribute_policy: NoAttributePolicy = NoAttributePolicy.default(), 143 | **kwargs, 144 | ) -> CIUpdateRsp: 145 | """ 146 | update ci attrs 147 | 148 | eg: suppose a ci model with fields [id, name, age], and its ci_type is "Human" 149 | a ci is ci(id=1, name="a", age=10) 150 | update operation may like: 151 | 152 | > client.update_ci("Human", ci_id=1, attrs={"age": 11}) 153 | 154 | or 155 | 156 | > client.update_ci("Human", attrs={"age": 11}, name="a") # in this case, name must be unique in ci model 157 | 158 | Args: 159 | ci_type: ci model type 160 | ci_id: keyword agument only, the id of ci 161 | attrs: keyword agument only, fields to update 162 | no_attribute_policy: default to ignore not existed attributes update operation, optional value include IGNORE|REJECT 163 | 164 | Retrurns: 165 | CMDB update operation result 166 | """ 167 | param = CIUpdateReq(ci_type, no_attribute_policy, attrs or {}) 168 | if not ci_id: 169 | param.unique_key = kwargs 170 | return self._update_ci(ci_id, param) 171 | 172 | def delete_ci(self, ci_id: int) -> CIDeleteRsp: 173 | """ 174 | delete a ci by its ci_id 175 | 176 | eg: suppose a ci model with fields [id, name, age], and its ci_type is "Human" 177 | a ci is ci(id=1, name="a", age=10) 178 | delete operation may like: 179 | 180 | > client.delete(1) 181 | 182 | Args: 183 | ci_id: ci id for the ci want to delete 184 | 185 | Retrurns: 186 | CMDB delete operation result 187 | """ 188 | param = CIDeleteReq(ci_id) 189 | return self._delete_ci(param) 190 | -------------------------------------------------------------------------------- /src/cmdb/core/ci_relations.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | from typing import Optional 3 | 4 | import requests 5 | 6 | from cmdb.core.auth import build_api_key 7 | from cmdb.core.models import * 8 | from cmdb.core.policy import RetKey 9 | from cmdb.core.exc import CMDBError 10 | 11 | 12 | class CIRelationClient: 13 | """ 14 | CMDB CIRelation Relation object handle client 15 | 16 | Attributes: 17 | opt: initialize arugument, if None input, will initiallize with enviroment arguments 18 | 19 | Example: 20 | 21 | 1. initialize with arguments 22 | 23 | > opt = Option(url="https://yourhost.com/api/v0.1", key=your_key, secret=your_secret) 24 | 25 | > client = CIRelationRelationClient(opt) 26 | 27 | 2. initialize with enviorment arguments 28 | 29 | > client = CIRelationRelationClient() 30 | 31 | """ 32 | 33 | def __init__(self, opt: Optional[Option] = None): 34 | self.opt = opt if opt else Option() 35 | self.session = requests.Session() 36 | self.url = f"{self.opt.url}/ci_relations" 37 | 38 | def _build_api_key(self, url: str, payload: dict) -> dict: 39 | return build_api_key(self.opt.key, self.opt.secret, urlparse(url).path, payload) 40 | 41 | def _check_err(self, resp: dict): 42 | msg = resp.get("message") 43 | if msg: 44 | raise CMDBError(msg) 45 | 46 | def _add_ci_relation(self, params: CIRelationCreateReq) -> CIRelationCreateRsp: 47 | url = f"{self.url}/{params.src_ci_id}/{params.dst_ci_id}" 48 | payload = self._build_api_key(url, params.to_params()) 49 | resp = self.session.post(url, json=payload).json() 50 | self._check_err(resp) 51 | return CIRelationCreateRsp(**resp) 52 | 53 | def _get_ci_relation(self, params: CIRelationRetrieveReq) -> CIRelationRetrieveRsp: 54 | url = f"{self.url}/s" 55 | payload = params.to_params() 56 | payload = self._build_api_key(url, payload) 57 | resp = self.session.get(url, params=payload).json() 58 | self._check_err(resp) 59 | return CIRelationRetrieveRsp(**resp) 60 | 61 | def _delete_ci_relation_by_cr_id(self, params: CIRelationDeleteReq) -> CIRelationDeleteRsp: 62 | url = f"{self.url}/{params.cr_id}" 63 | payload = self._build_api_key(url, params.to_params()) 64 | resp = self.session.delete(url, json=payload).json() 65 | return CIRelationDeleteRsp(**resp) 66 | 67 | def _delete_ci_relation(self, params: CIRelationDeleteReq) -> CIRelationDeleteRsp: 68 | url = f"{self.url}/{params.src_ci_id}/{params.dst_ci_id}" 69 | payload = self._build_api_key(url, params.to_params()) 70 | resp = self.session.delete(url, json=payload).json() 71 | return CIRelationDeleteRsp(**resp) 72 | 73 | def add_ci_relation( 74 | self, 75 | src_ci_id: int, 76 | dst_ci_id: int, 77 | ) -> CIRelationCreateRsp: 78 | """ 79 | create new ci_relation instance 80 | 81 | Args: 82 | src_ci_id: id of source ci 83 | src_ci_id: id of destination ci 84 | 85 | Retrurns: 86 | ci_relation create operation result 87 | """ 88 | param = CIRelationCreateReq(src_ci_id, dst_ci_id) 89 | return self._add_ci_relation(param) 90 | 91 | def get_ci_relation( 92 | self, 93 | root_id: int, 94 | level: Optional[str] = None, 95 | reverse: int = 0, 96 | q: Optional[str] = None, 97 | fl: Optional[str] = None, 98 | facet: Optional[str] = None, 99 | count: int = 25, 100 | page: int = 1, 101 | sort: Optional[str] = None, 102 | ret_key: RetKey = RetKey.default(), 103 | ) -> CIRelationRetrieveRsp: 104 | """ 105 | get ci_relation instance 106 | 107 | get target relation by root id 108 | for more information, please reference to veops cmdb guidance [here](https://github.com/veops/cmdb/blob/master/docs/cmdb_api.md). 109 | 110 | Args: 111 | root_id: ci id of root node 112 | level: levels of relationship, split by comma 113 | reverse: Reverse search or not, 0 for no and 1 for yes, default is 0 114 | q: search expression, may looks like "_type:Human,name:a" 115 | fl: ret attrubute, split by comma 116 | facet: staticstics 117 | count: ci count per page 118 | page: target page num 119 | sort: sort by target attribute, use `-attr` for descending 120 | ret_key: ret field name, optional values include ID|NAME|ALIAS 121 | 122 | Returns: 123 | target ci_relation results 124 | """ 125 | params = CIRelationRetrieveReq(root_id, level, reverse, q, fl, facet, count, page, sort, ret_key) 126 | return self._get_ci_relation(params) 127 | 128 | def delete_ci_relation( 129 | self, 130 | *, 131 | cr_id: Optional[int] = None, 132 | src_ci_id: Optional[int] = None, 133 | dst_ci_id: Optional[int] = None, 134 | ) -> CIRelationDeleteRsp: 135 | """ 136 | to delete the cr, either the cr_id or a combination of dst_ci_id and src_ci_id can be used 137 | 138 | example: 139 | 140 | 1. delete by cr_id 141 | 142 | > client.delete_ci_relation(cr_id=1) 143 | 144 | 2. delete by src_ci_id and dst_ci_id 145 | 146 | > client.delete_ci_relation(src_ci_id=1, dst_ci_id=2) 147 | 148 | Args: 149 | cr_id: cr id for the ci_relation want to delete 150 | src_ci_id: id of source ci 151 | src_ci_id: id of destination ci 152 | 153 | Retrurns: 154 | CMDB delete operation result 155 | """ 156 | if cr_id is not None: 157 | param = CIRelationDeleteReq(cr_id) 158 | return self._delete_ci_relation_by_cr_id(param) 159 | elif all({src_ci_id is not None, dst_ci_id is not None}): 160 | param = CIRelationDeleteReq(src_ci_id=src_ci_id, dst_ci_id=dst_ci_id) 161 | return self._delete_ci_relation(param) 162 | raise CMDBError("cr_id should be provided, or both src_ci_id and dst_ci_id should be provided.") 163 | -------------------------------------------------------------------------------- /src/cmdb/core/exc.py: -------------------------------------------------------------------------------- 1 | 2 | class CMDBError(Exception): 3 | pass -------------------------------------------------------------------------------- /src/cmdb/core/models.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import dataclasses 3 | import os 4 | from typing import Optional 5 | 6 | from cmdb.core.policy import ExistPolicy, NoAttributePolicy, RetKey 7 | 8 | 9 | class Request(abc.ABC): 10 | """cmdb request""" 11 | pass 12 | 13 | @abc.abstractmethod 14 | def to_params(self) -> dict: 15 | raise NotImplemented("") 16 | 17 | 18 | class Response(abc.ABC): 19 | """response of cmdb request""" 20 | pass 21 | 22 | 23 | @dataclasses.dataclass 24 | class Option: 25 | """ 26 | cmdb configure option 27 | 28 | all attributes default initialize with empty str if no argument input, 29 | and then will check the enviorment arguments 30 | """ 31 | url: str = "" 32 | key: str = "" 33 | secret: str = "" 34 | 35 | def __post_init__(self) -> None: 36 | if not self.url: 37 | self.url = os.environ["CMDB_HOST"] 38 | if not self.key: 39 | self.key = os.environ["CMDB_KEY"] 40 | if not self.secret: 41 | self.secret = os.environ["CMDB_SECRET"] 42 | 43 | 44 | @dataclasses.dataclass 45 | class CICreateReq(Request): 46 | """ci create request""" 47 | ci_type: str 48 | no_attribute_policy: NoAttributePolicy = NoAttributePolicy.default() 49 | exist_policy: ExistPolicy = ExistPolicy.default() 50 | attrs: dict = dataclasses.field(default_factory=dict) 51 | 52 | def to_params(self) -> dict: 53 | p = { 54 | "ci_type": self.ci_type, 55 | "no_attribute_policy": self.no_attribute_policy.value, 56 | "exist_policy": self.exist_policy.value 57 | } 58 | p.update(self.attrs) 59 | return p 60 | 61 | 62 | @dataclasses.dataclass 63 | class CICreateRsp(Response): 64 | """response of ci create requet""" 65 | ci_id: int 66 | 67 | 68 | @dataclasses.dataclass 69 | class CIRetrieveReq(Request): 70 | """ci retrieve requet""" 71 | q: str 72 | fl: Optional[str] = None 73 | facet: Optional[str] = None 74 | count: int = 25 75 | page: int = 1 76 | sort: Optional[str] = None 77 | ret_key: RetKey = RetKey.default() 78 | 79 | def to_params(self) -> dict: 80 | p = dataclasses.asdict(self) 81 | p.update({"ret_key": self.ret_key.value}) 82 | return p 83 | 84 | 85 | @dataclasses.dataclass 86 | class CIRetrieveRsp(Response): 87 | """response of ci retrieve requet""" 88 | numfound: int 89 | total: int 90 | page: int 91 | result: list 92 | facet: dict 93 | counter: dict 94 | 95 | 96 | @dataclasses.dataclass 97 | class CIUpdateReq(Request): 98 | """ci update requet""" 99 | ci_type: str 100 | no_attribute_policy: NoAttributePolicy = NoAttributePolicy.default() 101 | attrs: dict = dataclasses.field(default_factory=dict) 102 | unique_key: dict = dataclasses.field(default_factory=dict) 103 | 104 | def to_params(self) -> dict: 105 | p = { 106 | "ci_type": self.ci_type, 107 | "no_attribute_policy": self.no_attribute_policy.value, 108 | "exist_policy": ExistPolicy.REPLACE.value, 109 | **self.unique_key, 110 | } 111 | p.update(self.attrs) 112 | return p 113 | 114 | 115 | @dataclasses.dataclass 116 | class CIUpdateRsp(Response): 117 | """response of ci update requet""" 118 | ci_id: int 119 | 120 | 121 | @dataclasses.dataclass 122 | class CIDeleteReq(Request): 123 | """ci delete requet""" 124 | ci_id: int 125 | 126 | def to_params(self) -> dict: 127 | return dataclasses.asdict(self) 128 | 129 | 130 | @dataclasses.dataclass 131 | class CIDeleteRsp(Response): 132 | """response of ci delete requet""" 133 | message: str 134 | 135 | 136 | @dataclasses.dataclass 137 | class CIRelationCreateReq(Request): 138 | """ci_relation create request""" 139 | src_ci_id: int 140 | dst_ci_id: int 141 | 142 | def to_params(self) -> dict: 143 | return {} 144 | 145 | 146 | @dataclasses.dataclass 147 | class CIRelationCreateRsp(Response): 148 | """response of ci_relation create request""" 149 | cr_id: int 150 | 151 | 152 | @dataclasses.dataclass 153 | class CIRelationRetrieveReq(Request): 154 | """ci_relation retrieve request""" 155 | root_id: int 156 | level: Optional[str] = None 157 | reverse: int = 0 158 | q: Optional[str] = None 159 | fl: Optional[str] = None 160 | facet: Optional[str] = None 161 | count: int = 25 162 | page: int = 1 163 | sort: Optional[str] = None 164 | ret_key: RetKey = RetKey.default() 165 | 166 | def to_params(self) -> dict: 167 | p = dataclasses.asdict(self) 168 | p.update({"ret_key": self.ret_key.value}) 169 | return p 170 | 171 | 172 | @dataclasses.dataclass 173 | class CIRelationRetrieveRsp(Response): 174 | """response of ci_relation retrieve requet""" 175 | numfound: int 176 | total: int 177 | page: int 178 | result: list 179 | facet: dict 180 | counter: dict 181 | 182 | 183 | @dataclasses.dataclass 184 | class CIRelationDeleteReq(Request): 185 | """ci_relation delete requet""" 186 | cr_id: Optional[int] = None 187 | src_ci_id: Optional[int] = None 188 | dst_ci_id: Optional[int] = None 189 | 190 | def to_params(self) -> dict: 191 | return {} 192 | 193 | 194 | @dataclasses.dataclass 195 | class CIRelationDeleteRsp(Response): 196 | """response of ci_relation delete requet""" 197 | message: str -------------------------------------------------------------------------------- /src/cmdb/core/policy.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class RetKey(Enum): 5 | ID = "id" 6 | NAME = "name" 7 | ALIAS = "alias" 8 | 9 | @staticmethod 10 | def default() -> "RetKey": 11 | """ 12 | defalut ret key 13 | """ 14 | return RetKey.NAME 15 | 16 | 17 | class NoAttributePolicy(Enum): 18 | REJECT = "reject" 19 | IGNORE = "ignore" 20 | 21 | @staticmethod 22 | def default() -> "NoAttributePolicy": 23 | """ 24 | defalut no_attribute_policy 25 | """ 26 | return NoAttributePolicy.IGNORE 27 | 28 | 29 | class ExistPolicy(Enum): 30 | NEED = "need" 31 | REJECT = "reject" 32 | REPLACE = "replace" 33 | 34 | @staticmethod 35 | def default() -> "ExistPolicy": 36 | """ 37 | defalut exist_policy 38 | """ 39 | return ExistPolicy.REJECT 40 | -------------------------------------------------------------------------------- /tests/test_ci.py: -------------------------------------------------------------------------------- 1 | """ 2 | suppose a ci model is Book(id, book_id, book_name, author) 3 | """ 4 | 5 | import os 6 | 7 | from cmdb.core.ci import CIClient, Option 8 | 9 | 10 | class TestCI: 11 | 12 | def setup_method(self) -> None: 13 | opt = Option( 14 | os.environ["CMDB_HOST"], 15 | os.environ["CMDB_KEY"], 16 | os.environ["CMDB_SECRET"], 17 | ) 18 | self.client = CIClient(opt=opt) 19 | 20 | def test_get(self): 21 | resp = self.client.get_ci(q="_type:book").result 22 | print("get") 23 | print(resp) 24 | 25 | def find_by_name(self, book_name: str): 26 | q = f"_type:book,book_name:{book_name}" 27 | resp = self.client.get_ci(q=q).result 28 | if not resp: 29 | return 30 | return resp[0] 31 | 32 | def test_add(self): 33 | ci = { 34 | "id": 1, 35 | "book_id": 1, 36 | "book_name": "平凡的世界", 37 | "author": "路遥", 38 | } 39 | if self.find_by_name("平凡的世界"): 40 | return 41 | resp = self.client.add_ci("book", ci) 42 | print("add") 43 | print(resp) 44 | 45 | def test_update(self): 46 | ci = self.find_by_name("平凡的世界") 47 | resp = self.client.update_ci("book", ci_id=ci["_id"], attrs={"author": "yao.lu"}) 48 | print("update") 49 | print(resp) 50 | 51 | def test_delete(self): 52 | ci = self.find_by_name("平凡的世界") 53 | resp = self.client.delete_ci(ci["_id"]) 54 | print("delete") 55 | print(resp) -------------------------------------------------------------------------------- /tests/test_cr.py: -------------------------------------------------------------------------------- 1 | """ 2 | suppose a CI model book with field id, name and author, 3 | suppose a CI model rank with field rank_id, book_id, rank 4 | """ 5 | 6 | 7 | import os 8 | 9 | from cmdb.core.ci import CIClient, Option 10 | from cmdb.core.ci_relations import CIRelationClient 11 | 12 | 13 | class TestCI: 14 | 15 | def setup_method(self) -> None: 16 | opt = Option( 17 | os.environ["CMDB_HOST"], 18 | os.environ["CMDB_KEY"], 19 | os.environ["CMDB_SECRET"], 20 | ) 21 | self.ci_client = CIClient(opt=opt) 22 | self.client = CIRelationClient(opt=opt) 23 | self.add_ci() 24 | 25 | def add_ci(self): 26 | book = { 27 | "id": 1, 28 | "book_id": 1, 29 | "book_name": "平凡的世界", 30 | "author": "路遥", 31 | } 32 | rank = { 33 | "id": 1, 34 | "rank_id": 1, 35 | "rank": 5, 36 | } 37 | if not self.find_ci(q="_type:book,book_id:1"): 38 | self.ci_client.add_ci("book", book) 39 | if not self.find_ci(q="_type:rank,rank_id:1"): 40 | self.ci_client.add_ci("rank", rank) 41 | 42 | def find_ci(self, q: str): 43 | resp = self.ci_client.get_ci(q=q).result 44 | if not resp: 45 | return 46 | return resp[0] 47 | 48 | def test_add_ci_relation(self): 49 | book = self.find_ci(q="_type:book,book_id:1") 50 | rank = self.find_ci(q="_type:rank,rank_id:1") 51 | resp = self.client.add_ci_relation(book["_id"], rank["_id"]) 52 | print("add result", resp) 53 | 54 | def test_get_cli_relation(self): 55 | book = self.find_ci(q="_type:book,book_id:1") 56 | resp = self.client.get_ci_relation(root_id=book["_id"]).result 57 | print("get result", resp) 58 | 59 | def test_delete_relation(self): 60 | book = self.find_ci(q="_type:book,book_id:1") 61 | rank = self.find_ci(q="_type:rank,rank_id:1") 62 | resp = self.client.delete_ci_relation(src_ci_id=book["_id"], dst_ci_id=rank["_id"]) 63 | print("delete result", resp) 64 | --------------------------------------------------------------------------------