├── .docker-compose ├── README.md ├── docker-compose.yml └── script.sh ├── .gitignore ├── .gitlab-ci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── Makefile ├── classes.rst ├── conf.py ├── examples.rst └── index.rst ├── requirements.txt ├── rtapi └── __init__.py ├── setup.py └── tests ├── db_connection └── __init__.py ├── object_for_test.py ├── requirements.txt ├── rtapi_con └── __init__.py ├── test_00_basic_methods.py ├── test_10_attributes.py ├── test_10_chassis.py └── test_10_ipv4.py /.docker-compose/README.md: -------------------------------------------------------------------------------- 1 | ### Local testing environment 2 | 3 | This is simple docker-compose environment for local testing only. 4 | 5 | Simple testing process 6 | 7 | ``` 8 | cd .docker-compose/ 9 | docker-compose up --build 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /.docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | rtdb: 5 | image: registry.gitlab.com/rvojcik/rtapi/test/rtdb:0.21.1 6 | 7 | testing: 8 | build: ../ 9 | depends_on: ['rtdb'] -------------------------------------------------------------------------------- /.docker-compose/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | pip install -r requirements.txt 4 | python setup.py install 5 | pip install -r tests/requirements.txt 6 | coverage run --source rtapi -m py.test 7 | coverage report -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - build 4 | - publish 5 | 6 | Test (Python 3.6): 7 | stage: test 8 | allow_failure: false 9 | only: 10 | - master 11 | - tags 12 | services: 13 | - name: registry.gitlab.com/rvojcik/rtapi/test/rtdb:0.21.1 14 | alias: rtdb 15 | image: python:3.6-buster 16 | script: 17 | - pip install -r requirements.txt 18 | - python setup.py install 19 | - pip install -r tests/requirements.txt 20 | - coverage run --source rtapi -m py.test 21 | - coverage report 22 | 23 | Test (Python 3.7): 24 | stage: test 25 | allow_failure: false 26 | only: 27 | - master 28 | - tags 29 | services: 30 | - name: registry.gitlab.com/rvojcik/rtapi/test/rtdb:0.21.1 31 | alias: rtdb 32 | image: python:3.7-buster 33 | script: 34 | - pip install -r requirements.txt 35 | - python setup.py install 36 | - pip install -r tests/requirements.txt 37 | - coverage run --source rtapi -m py.test 38 | - coverage report 39 | 40 | Test (Python 3.8): 41 | stage: test 42 | allow_failure: false 43 | only: 44 | - master 45 | - tags 46 | services: 47 | - name: registry.gitlab.com/rvojcik/rtapi/test/rtdb:0.21.1 48 | alias: rtdb 49 | image: python:3.8-buster 50 | script: 51 | - pip install -r requirements.txt 52 | - python setup.py install 53 | - pip install -r tests/requirements.txt 54 | - coverage run --source rtapi -m py.test 55 | - coverage report 56 | 57 | build_python3: 58 | stage: build 59 | allow_failure: false 60 | only: 61 | - tags 62 | image: python:3.6 63 | script: 64 | - pip install --upgrade setuptools wheel 65 | - python setup.py sdist bdist_wheel 66 | artifacts: 67 | paths: 68 | - dist/ 69 | 70 | publish_package: 71 | stage: publish 72 | allow_failure: false 73 | only: 74 | - tags 75 | image: python:3.6 76 | script: 77 | - pip install --upgrade setuptools wheel twine 78 | - ls -l dist/* 79 | - twine upload dist/* 80 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7-stretch 2 | 3 | RUN mkdir /work 4 | ADD . /work/ 5 | 6 | ADD .docker-compose/script.sh /work/ 7 | 8 | WORKDIR /work/ 9 | 10 | CMD /work/script.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtapi 2 | 3 | [![pipeline status](https://gitlab.com/rvojcik/rtapi/badges/master/pipeline.svg)](https://gitlab.com/rvojcik/rtapi/commits/master) [![coverage report](https://gitlab.com/rvojcik/rtapi/badges/master/coverage.svg)](https://gitlab.com/rvojcik/rtapi/commits/master) 4 | 5 | Racktables API 6 | 7 | Python module for accessing and manipulating RackTables objects. 8 | 9 | # Installation 10 | 11 | ```bash 12 | pip install mysqlclient 13 | pip install racktables-api 14 | ``` 15 | 16 | # PyPi project 17 | 18 | https://pypi.org/project/racktables-api/ 19 | 20 | # Documentation 21 | 22 | https://rtapi.readthedocs.io 23 | 24 | # Example 25 | 26 | ```python 27 | import MySQLdb 28 | import rtapi 29 | import sys 30 | 31 | # Create connection to database 32 | try: 33 | # Create connection to database 34 | db = MySQLdb.connect(host='hostname',port=3306, passwd='mypass',db='racktables',user='racktables') 35 | except MySQLdb.Error: 36 | e = sys.exc_info()[1] 37 | print("Error %d: %s" % (e.args[0],e.args[1])) 38 | sys.exit(1) 39 | 40 | # Initialize rtapi with database connection 41 | rt = rtapi.RTObject(db) 42 | 43 | # List all objects from database 44 | print (rt.ListObjects(data='list')) 45 | 46 | # List all IPv4 Networks from database 47 | print (rt.GetIpv4Networks()) 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | -------------------------------------------------------------------------------- /docs/classes.rst: -------------------------------------------------------------------------------- 1 | API Classes and Functions 2 | ========================= 3 | 4 | List of Classes available in rtapi module 5 | with functions and methods. 6 | 7 | .. autoclass:: rtapi::RTObject 8 | :members: 9 | 10 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'racktables-api' 23 | copyright = u'2019, Robert Vojcik' 24 | author = u'Robert Vojcik' 25 | 26 | # The short X.Y version 27 | version = u'' 28 | # The full version, including alpha/beta/rc tags 29 | release = u'0.2.6' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.viewcode', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = None 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path. 69 | exclude_patterns = [u'_build', 'Thumbs.db', '.DS_Store'] 70 | 71 | # The name of the Pygments (syntax highlighting) style to use. 72 | pygments_style = None 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'sphinx_rtd_theme' 81 | 82 | # Theme options are theme-specific and customize the look and feel of a theme 83 | # further. For a list of options available for each theme, see the 84 | # documentation. 85 | # 86 | # html_theme_options = {} 87 | 88 | # Add any paths that contain custom static files (such as style sheets) here, 89 | # relative to this directory. They are copied after the builtin static files, 90 | # so a file named "default.css" will overwrite the builtin "default.css". 91 | html_static_path = ['_static'] 92 | 93 | # Custom sidebar templates, must be a dictionary that maps document names 94 | # to template names. 95 | # 96 | # The default sidebars (for documents that don't match any pattern) are 97 | # defined by theme itself. Builtin themes are using these templates by 98 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 99 | # 'searchbox.html']``. 100 | # 101 | # html_sidebars = {} 102 | 103 | 104 | # -- Options for HTMLHelp output --------------------------------------------- 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'racktables-apidoc' 108 | 109 | 110 | # -- Options for LaTeX output ------------------------------------------------ 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'racktables-api.tex', u'racktables-api Documentation', 135 | u'Robert Vojcik', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output ------------------------------------------ 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'racktables-api', u'racktables-api Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ---------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'racktables-api', u'racktables-api Documentation', 156 | author, 'racktables-api', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | 161 | # -- Options for Epub output ------------------------------------------------- 162 | 163 | # Bibliographic Dublin Core info. 164 | epub_title = project 165 | 166 | # The unique identifier of the text. This can be a ISBN number 167 | # or the project homepage. 168 | # 169 | # epub_identifier = '' 170 | 171 | # A unique identification for the text. 172 | # 173 | # epub_uid = '' 174 | 175 | # A list of files that should not be packed into the epub file. 176 | epub_exclude_files = ['search.html'] 177 | 178 | 179 | # -- Extension configuration ------------------------------------------------- 180 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Working Examples 2 | ================ 3 | 4 | .. highlight:: python 5 | 6 | Some of working example to help starting with rtapi. 7 | 8 | .. code-block:: python 9 | 10 | import MySQLdb 11 | import rtapi 12 | 13 | # Create connection to database 14 | try: 15 | # Create connection to database 16 | db = MySQLdb.connect(host='hostname',port=3306, passwd='mypass',db='racktables',user='racktables') 17 | except MySQLdb.Error: 18 | e = sys.exc_info()[1] 19 | print("Error %d: %s" % (e.args[0],e.args[1])) 20 | sys.exit(1) 21 | 22 | # Initialize rtapi with database connection 23 | rt = rtapi.RTObject(db) 24 | 25 | # List all objects from database 26 | print rt.ListObjects() 27 | 28 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. racktables-api documentation master file, created by 2 | sphinx-quickstart on Thu Jan 17 15:02:45 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to racktables-api's documentation! 7 | ========================================== 8 | 9 | Racktables-API is simple interface to Racktables Database 10 | for python applications. 11 | 12 | Simple and straigt use give your racktables ability for 13 | scripting, migrating data, data exports and automation. 14 | 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | :caption: Contents: 19 | 20 | classes 21 | examples 22 | 23 | 24 | Search 25 | ================== 26 | 27 | * :ref:`search` 28 | 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ipaddress 2 | -------------------------------------------------------------------------------- /rtapi/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # RTAPI 4 | # Racktables API is simple python module providing some methods 5 | # for monipulation with racktables objects. 6 | # 7 | # This utility is released under GPL v2 8 | # 9 | # Server Audit utility for Racktables Datacenter management project. 10 | # Copyright (C) 2012 Robert Vojcik (robert@vojcik.net) 11 | # 12 | # This program is free software; you can redistribute it and/or 13 | # modify it under the terms of the GNU General Public License 14 | # as published by the Free Software Foundation; either version 2 15 | # of the License, or (at your option) any later version. 16 | # 17 | # This program is distributed in the hope that it will be useful, 18 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | # GNU General Public License for more details. 21 | # 22 | # You should have received a copy of the GNU General Public License 23 | # along with this program; if not, write to the Free Software 24 | # Foundation, Inc., 51 Franklin Street, 25 | # Fifth Floor, Boston, MA 02110-1301, USA. 26 | 27 | """ 28 | Racktables-API (rtapi) is simple python 29 | module for manipulation with objects 30 | in racktables database. 31 | 32 | It allows you to interact with database for 33 | exporting, migration or automation purposes. 34 | 35 | For proper function, some methods 36 | need ipaddress module (https://pypi.org/project/ipaddress/) 37 | 38 | More information about Racktables project can be 39 | found on https://www.racktables.org/ 40 | """ 41 | __author__ = "Robert Vojcik (robert@vojcik.net)" 42 | __version__ = "0.3.1" 43 | __copyright__ = "OpenSource" 44 | __license__ = "GPLv2" 45 | 46 | __all__ = ["RTObject"] 47 | 48 | 49 | import re 50 | import ipaddress 51 | from datetime import datetime 52 | from datetime import timedelta 53 | 54 | 55 | class RTObject: 56 | """ 57 | Main class which create rtapi object. 58 | You could create multiple rtapi objects each 59 | for different database and interact between them. 60 | 61 | This Class needs only one parameter and it's 62 | database object. 63 | """ 64 | 65 | # Init method 66 | def __init__(self, dbobject): 67 | """Initialize Object""" 68 | # Open configuration file 69 | self.db = dbobject 70 | self.dbresult = self.db.cursor() 71 | 72 | # DATABASE methods 73 | def db_query_one(self, sql): 74 | """ 75 | SQL query function, return one row. 76 | Require sql query as parameter 77 | """ 78 | self.dbresult.execute(sql) 79 | return self.dbresult.fetchone() 80 | 81 | def db_query_all(self, sql): 82 | """ 83 | SQL query function, return all rows. 84 | Require sql query as parameter 85 | """ 86 | self.dbresult.execute(sql) 87 | return self.dbresult.fetchall() 88 | 89 | def db_insert(self, sql): 90 | """SQL insert/update function. Require sql query as parameter""" 91 | self.dbresult.execute(sql) 92 | self.db.commit() 93 | 94 | def db_fetch_lastid(self): 95 | """SQL function which return ID of last inserted row.""" 96 | return self.dbresult.lastrowid 97 | 98 | def ListObjects(self, data='sum'): 99 | """ 100 | List all objects from database 101 | You can specify data sum or list to get summary or list of objects 102 | In list you get array of id,name,asset_no,objtype_id 103 | """ 104 | if data == 'list': 105 | sql = 'SELECT id,name,asset_no,objtype_id FROM Object' 106 | return self.db_query_all(sql) 107 | else: 108 | sql = 'SELECT count(name) FROM Object' 109 | return "Found " + str(self.db_query_one(sql)[0]) + " objects in database" 110 | 111 | def ListObjectsByType(self, object_tid): 112 | """ 113 | Get list of objects based on object type ID 114 | """ 115 | sql = 'SELECT id,name,asset_no,label,comment,has_problems from Object WHERE objtype_id = %i' % (object_tid) 116 | return self.db_query_all(sql) 117 | 118 | # Object methotds 119 | def ObjectExistST(self, service_tag): 120 | """Check if object exist in database based on asset_no""" 121 | sql = 'SELECT name FROM Object WHERE asset_no = \'' + service_tag + '\'' 122 | if self.db_query_one(sql) is None: 123 | return False 124 | else: 125 | return True 126 | 127 | def ObjectExistName(self, name): 128 | """Check if object exist in database based on name""" 129 | sql = 'select id from Object where name = \'' + name + '\'' 130 | if self.db_query_one(sql) is None: 131 | return False 132 | else: 133 | return True 134 | 135 | def ObjectExistSTName(self, name, asset_no): 136 | """Check if object exist in database based on name""" 137 | sql = "SELECT id FROM Object WHERE name = '%s' AND asset_no = '%s'" % (name, asset_no) 138 | if self.db_query_one(sql) is None: 139 | return False 140 | else: 141 | return True 142 | 143 | def AddObject(self, name, server_type_id, asset_no, label): 144 | """Add new object to racktables""" 145 | sql = "INSERT INTO Object (name,objtype_id,asset_no,label) VALUES ('%s',%d,'%s','%s')" % (name, server_type_id, asset_no, label) 146 | self.db_insert(sql) 147 | return self.db_fetch_lastid() 148 | 149 | def UpdateObjectLabel(self, object_id, label): 150 | """Update label on object""" 151 | sql = "UPDATE Object SET label = '%s' where id = %d" % (label, object_id) 152 | self.db_insert(sql) 153 | 154 | def UpdateObjectComment(self, object_id, comment): 155 | """Update comment on object""" 156 | sql = "UPDATE Object SET comment = '%s' where id = %d" % (comment, object_id) 157 | self.db_insert(sql) 158 | 159 | def UpdateObjectName(self, object_id, name): 160 | """Update name on object""" 161 | sql = "UPDATE Object SET name = '%s' where id = %d" % (name, object_id) 162 | self.db_insert(sql) 163 | 164 | def GetObjectName(self, object_id): 165 | """Translate Object ID to Object Name""" 166 | # Get interface id 167 | sql = "SELECT name FROM Object WHERE id = %d" % (object_id) 168 | result = self.db_query_one(sql) 169 | if result is not None: 170 | object_name = result[0] 171 | else: 172 | object_name = None 173 | 174 | return object_name 175 | 176 | def GetObjectNameByAsset(self, service_tag): 177 | """Translate Object AssetTag to Object Name""" 178 | # Get interface id 179 | sql = "SELECT name FROM Object WHERE asset_no = '%s'" % (service_tag) 180 | result = self.db_query_one(sql) 181 | if result is not None: 182 | object_name = result[0] 183 | else: 184 | object_name = None 185 | 186 | return object_name 187 | 188 | def GetObjectIdByAsset(self, service_tag): 189 | """Get Object ID by Asset Tag""" 190 | 191 | sql = "SELECT id FROM Object WHERE asset_no = '%s'" % (service_tag) 192 | result = self.db_query_one(sql) 193 | if result is not None: 194 | object_id = result[0] 195 | else: 196 | object_id = None 197 | 198 | return object_id 199 | 200 | def GetObjectLabel(self, object_id): 201 | """Get object label""" 202 | # Get interface id 203 | sql = "SELECT label FROM Object WHERE id = %d" % (object_id) 204 | result = self.db_query_one(sql) 205 | if result is not None: 206 | object_label = result[0] 207 | else: 208 | object_label = None 209 | 210 | return object_label 211 | 212 | def GetObjectComment(self, object_id): 213 | """Get object comment""" 214 | # Get interface id 215 | sql = "SELECT comment FROM Object WHERE id = %d" % (object_id) 216 | result = self.db_query_one(sql) 217 | if result is not None: 218 | object_comment = result[0] 219 | else: 220 | object_comment = None 221 | 222 | return object_comment 223 | 224 | def GetObjectTags(self, object_id): 225 | """Get object tags""" 226 | sql = "SELECT t1.tag as parent_tag, t2.tag as tag FROM TagTree as t1 RIGHT JOIN TagTree as t2 ON t1.id = t2.parent_id WHERE t2.id IN (SELECT tag_id FROM TagStorage JOIN Object ON TagStorage.entity_id = Object.id WHERE TagStorage.entity_realm='object' and Object.id = '%d')" % (object_id) 227 | result = self.db_query_all(sql) 228 | 229 | return result 230 | 231 | def GetObjectsByTag(self, tag_name): 232 | """Get Array of objects from Racktables database by Tag name""" 233 | 234 | sql = "SELECT t1.name, \ 235 | t1.id \ 236 | FROM Object AS t1 \ 237 | JOIN \ 238 | TagStorage AS t2 \ 239 | ON t1.id = t2.entity_id \ 240 | JOIN \ 241 | TagTree AS t3 \ 242 | ON t2.tag_id = t3.id \ 243 | WHERE t3.tag = '%s'" % (str(tag_name)) 244 | 245 | return self.db_query_all(sql) 246 | 247 | def GetObjectId(self, name): 248 | """Translate Object name to object id""" 249 | # Get interface id 250 | sql = "SELECT id FROM Object WHERE name = '%s'" % (name) 251 | result = self.db_query_one(sql) 252 | if result is not None: 253 | object_id = result[0] 254 | else: 255 | object_id = None 256 | 257 | return object_id 258 | 259 | def ListDockerContainersOfHost(self, docker_host): 260 | """List all Docker containers of specified host""" 261 | sql = 'SELECT name FROM IPv4Address WHERE comment = "Docker host: %s"' % (docker_host) 262 | return self.db_query_all(sql) 263 | 264 | def AddDockerContainer(self, container_ip, container_name, docker_host): 265 | """Add new Docker container to racktables""" 266 | self.InsertIPv4Log(container_ip, "Name set to " + container_name + ", comment set to Docker host: " + docker_host + "") 267 | sql = "INSERT INTO IPv4Address (ip,name,comment,reserved) VALUES (INET_ATON('%s'),'%s','Docker host: %s','yes')" % (container_ip, container_name, docker_host) 268 | self.db_insert(sql) 269 | 270 | def RemoveDockerContainerFromHost(self, container_name, docker_host): 271 | """Remove Docker container from racktables""" 272 | sql = "SELECT INET_NTOA(ip) FROM IPv4Address WHERE comment = 'Docker host: %s' AND name = '%s'" % (docker_host, container_name) 273 | for ip in self.db_query_all(sql): 274 | sql = "SELECT IFNULL(DATEDIFF(NOW(), MAX(date)), 0) FROM IPv4Log WHERE ip = INET_ATON('%s')" % (ip[0]) 275 | if self.db_query_one(sql) >= 1: 276 | self.InsertIPv4Log(ip[0], "Name " + container_name + " removed, comment Docker host: " + docker_host + " removed") 277 | sql = "DELETE FROM IPv4Address WHERE ip = INET_ATON('%s')" % (ip[0]) 278 | self.db_insert(sql) 279 | 280 | def UpdateDockerContainerName(self, ip, name): 281 | """Update Docker container name""" 282 | self.InsertIPv4Log(ip, "Name set to " + name + "") 283 | sql = "UPDATE IPv4Address SET name = '%s' WHERE ip = INET_ATON('%s')" % (name, ip) 284 | self.db_insert(sql) 285 | 286 | def UpdateDockerContainerHost(self, ip, host): 287 | """Update Docker container host""" 288 | self.InsertIPv4Log(ip, "Comment set to Docker host: " + host + "") 289 | sql = "UPDATE IPv4Address SET comment = 'Docker host: %s' WHERE ip = INET_ATON('%s')" % (host, ip) 290 | self.db_insert(sql) 291 | 292 | def GetDockerContainerName(self, ip): 293 | """Get Docker container name""" 294 | # Get interface id 295 | sql = "SELECT name FROM IPv4Address WHERE ip = INET_ATON('%s')" % (ip) 296 | result = self.db_query_one(sql) 297 | if result is not None: 298 | ip_name = result[0] 299 | else: 300 | ip_name = None 301 | return ip_name 302 | 303 | def GetDockerContainerHost(self, ip): 304 | """Get Docker container host""" 305 | # Get interface id 306 | sql = "SELECT comment FROM IPv4Address WHERE ip = INET_ATON('%s')" % (ip) 307 | result = self.db_query_one(sql) 308 | host = None 309 | if result is not None: 310 | m = re.match("^Docker host: (.*)$", result[0]) 311 | if m: 312 | host = m.group(1) 313 | return host 314 | 315 | # Logging 316 | def InsertLog(self, object_id, message): 317 | """Attach log message to specific object""" 318 | sql = "INSERT INTO ObjectLog (object_id,user,date,content) VALUES (%d,'script',now(),'%s')" % (int(object_id), message) 319 | self.db_insert(sql) 320 | 321 | def InsertIPv4Log(self, ip, message): 322 | """Attach log message to IPv4""" 323 | sql = "INSERT INTO IPv4Log (ip,user,date,message) VALUES (INET_ATON('%s'),'script',now(),'%s')" % (ip, message) 324 | self.db_insert(sql) 325 | 326 | # Attrubute methods 327 | def CreateAttribute(self, attr_type, attr_name): 328 | """ Create new attribute in Racktables. Require attr_type (string, dict, uint) and attr_name """ 329 | sql = "SELECT id FROM Attribute WHERE name = '" + attr_name + "'" 330 | 331 | result = self.db_query_one(sql) 332 | if result is not None: 333 | getted_id = result[0] 334 | else: 335 | getted_id = None 336 | 337 | if getted_id == None: 338 | sql = "INSERT INTO Attribute (type, name) VALUES ('%s', '%s')" % (attr_type, attr_name) 339 | self.db_insert(sql) 340 | 341 | def MapAttribute(self, objtype_id, attr_id, chapter_id='NULL', sticky='no'): 342 | """ Map attribute to object type """ 343 | 344 | if chapter_id != 'NULL': 345 | chap_search = "chapter_id = %d AND " % (int(chapter_id)) 346 | else: 347 | chap_search = "" 348 | 349 | sql = "SELECT objtype_id FROM AttributeMap WHERE objtype_id = %d AND attr_id = %d AND %s sticky = '%s'" % ( objtype_id, attr_id, chap_search, sticky ) 350 | 351 | result = self.db_query_one(sql) 352 | if result is not None: 353 | getted_id = result[0] 354 | else: 355 | getted_id = None 356 | 357 | if getted_id == None: 358 | sql = "INSERT INTO AttributeMap (objtype_id, attr_id, chapter_id, sticky) VALUES (%d, %d, %s, '%s')" % (objtype_id, attr_id, str(chapter_id), sticky) 359 | self.db_insert(sql) 360 | 361 | def InsertAttribute(self, object_id, object_tid, attr_id, string_value, uint_value, name=None): 362 | """Add or Update object attribute. 363 | Require 6 arguments: object_id, object_tid, attr_id, string_value, uint_value, name""" 364 | 365 | # Check if attribute exist 366 | sql = "SELECT string_value,uint_value FROM AttributeValue WHERE object_id = %d AND object_tid = %d AND attr_id = %d" % (object_id, object_tid, attr_id) 367 | result = self.db_query_one(sql) 368 | 369 | if result is not None: 370 | # Check if attribute value is same and determine attribute type 371 | old_string_value = result[0] 372 | old_uint_value = result[1] 373 | same_flag = "no" 374 | attribute_type = "None" 375 | 376 | if old_string_value is not None: 377 | attribute_type = "string" 378 | if old_string_value == string_value: 379 | same_flag = "yes" 380 | elif old_uint_value is not None: 381 | attribute_type = "uint" 382 | if old_uint_value == uint_value: 383 | same_flag = "yes" 384 | 385 | # If exist, update value 386 | if same_flag == "no": 387 | if attribute_type == "string": 388 | sql = "UPDATE AttributeValue SET string_value = '%s' WHERE object_id = %d AND attr_id = %d AND object_tid = %d" % (string_value, object_id, attr_id, object_tid) 389 | if attribute_type == "uint": 390 | sql = "UPDATE AttributeValue SET uint_value = %d WHERE object_id = %d AND attr_id = %d AND object_tid = %d" % (uint_value, object_id, attr_id, object_tid) 391 | 392 | self.db_insert(sql) 393 | 394 | else: 395 | # Attribute not exist, insert new 396 | if string_value == "NULL": 397 | sql = "INSERT INTO AttributeValue (object_id,object_tid,attr_id,uint_value) VALUES (%d,%d,%d,%d)" % (object_id, object_tid, attr_id, uint_value) 398 | else: 399 | sql = "INSERT INTO AttributeValue (object_id,object_tid,attr_id,string_value) VALUES (%d,%d,%d,'%s')" % (object_id, object_tid, attr_id, string_value) 400 | self.db_insert(sql) 401 | 402 | def GetAttributeId(self, searchstring): 403 | """Search racktables database and get attribud id based on search string as argument""" 404 | sql = "SELECT id FROM Attribute WHERE name LIKE '%" + searchstring + "%'" 405 | 406 | result = self.db_query_one(sql) 407 | 408 | if result is not None: 409 | getted_id = result[0] 410 | else: 411 | getted_id = None 412 | 413 | return getted_id 414 | 415 | def GetAttributeIdByName(self, attr_name): 416 | """Get the ID of an attribute by its EXACT name""" 417 | sql = "SELECT id FROM Attribute WHERE name = '%s'" % (attr_name) 418 | 419 | result = self.db_query_one(sql) 420 | 421 | if result is not None: 422 | getted_id = result[0] 423 | else: 424 | getted_id = None 425 | 426 | return getted_id 427 | 428 | def GetAttributeValue(self, object_id, attr_id): 429 | """Search racktables database and get attribute values""" 430 | sql = "SELECT string_value,uint_value,float_value FROM AttributeValue WHERE object_id = %d AND attr_id = %d" % (object_id, attr_id) 431 | 432 | result = self.db_query_one(sql) 433 | 434 | if result is not None: 435 | output = [result[0], result[1], result[2]] 436 | else: 437 | output = None 438 | 439 | return output 440 | 441 | # Interfaces methods 442 | def GetInterfaceList(self, object_id): 443 | """ 444 | Get list of object interfaces ids and names 445 | Return array of touples id, name, type 446 | """ 447 | sql = "SELECT id, name, type FROM Port where object_id = %i" % (object_id) 448 | return self.db_query_all(sql) 449 | 450 | def GetInterfaceName(self, object_id, interface_id): 451 | """Find name of specified interface. Required object_id and interface_id argument""" 452 | # Get interface name 453 | sql = "SELECT name FROM Port WHERE object_id = %d AND id = %d" % (object_id, interface_id) 454 | result = self.db_query_one(sql) 455 | if result is not None: 456 | port_name = result[0] 457 | else: 458 | port_name = None 459 | 460 | return port_name 461 | 462 | def GetInterfaceId(self, object_id, interface): 463 | """Find id of specified interface""" 464 | # Get interface id 465 | sql = "SELECT id,name FROM Port WHERE object_id = %d AND name = '%s'" % (object_id, interface) 466 | result = self.db_query_one(sql) 467 | if result is not None: 468 | port_id = result[0] 469 | else: 470 | port_id = None 471 | 472 | return port_id 473 | 474 | def UpdateNetworkInterface(self, object_id, interface): 475 | """Add network interfece to object if not exist""" 476 | 477 | sql = "SELECT id,name FROM Port WHERE object_id = %d AND name = '%s'" % (object_id, interface) 478 | 479 | result = self.db_query_one(sql) 480 | if result is None: 481 | 482 | sql = "INSERT INTO Port (object_id,name,iif_id,type) VALUES (%d,'%s',1,24)" % (object_id, interface) 483 | self.db_insert(sql) 484 | port_id = self.db_fetch_lastid() 485 | 486 | else: 487 | port_id = result[0] 488 | 489 | return port_id 490 | 491 | def GetPortDeviceNameById(self, port_id): 492 | """Get Device name and Port Name by port ID, return dictionary device_name, port_name""" 493 | 494 | sql = "SELECT Port.name as port_name, Object.name as obj_name FROM Port INNER JOIN Object ON Port.object_id = Object.id WHERE Port.id = %d;" % (port_id) 495 | result = self.db_query_one(sql) 496 | 497 | if result is None: 498 | return result 499 | else: 500 | port_name = result[0] 501 | device_name = result[1] 502 | return {'device_name': device_name, 'port_name': port_name} 503 | 504 | def GetDictionaryId(self, searchstring, chapter_id=None): 505 | """ 506 | Search racktables dictionary using searchstring and return id of dictionary element 507 | It is possible to specify chapter_id for more specific search 508 | """ 509 | if not chapter_id: 510 | sql = "SELECT dict_key FROM Dictionary WHERE dict_value LIKE '%%%s%%'" % (searchstring) 511 | else: 512 | sql = "SELECT dict_key FROM Dictionary WHERE chapter_id = %d AND dict_value LIKE '%%%s%%'" % (int(chapter_id), searchstring) 513 | 514 | result = self.db_query_one(sql) 515 | if result is not None: 516 | getted_id = result[0] 517 | else: 518 | getted_id = None 519 | 520 | return getted_id 521 | 522 | def GetDictionaryChapterId(self, value): 523 | """Search racktables dictionary chapter using exact value and return id of dictionary chapter""" 524 | sql = "SELECT id FROM Chapter WHERE name = '" + value + "'" 525 | 526 | result = self.db_query_one(sql) 527 | if result is not None: 528 | getted_id = result[0] 529 | else: 530 | getted_id = None 531 | 532 | return getted_id 533 | 534 | def GetDictionaryIdByValue(self, dict_value, chapter_id=None): 535 | """ 536 | Get the ID of a dictionary entry by its EXACT value 537 | Is it possible to specify chapter_id for more specific search. 538 | """ 539 | if not chapter_id: 540 | sql = "SELECT dict_key FROM Dictionary WHERE dict_value = '%s'" % (dict_value) 541 | else: 542 | sql = "SELECT dict_key FROM Dictionary WHERE dict_value = '%s' AND chapter_id = %d" % (dict_value, int(chapter_id)) 543 | 544 | result = self.db_query_one(sql) 545 | if result is not None: 546 | getted_id = result[0] 547 | else: 548 | getted_id = None 549 | 550 | return getted_id 551 | 552 | def GetDictionaryValueById(self, dict_key): 553 | """Get value from Dictionary by ID reference""" 554 | sql = "SELECT dict_value FROM Dictionary WHERE dict_key = %d " % (dict_key) 555 | 556 | result = self.db_query_one(sql) 557 | if result is not None: 558 | getted_id = result[0] 559 | else: 560 | getted_id = None 561 | 562 | return getted_id 563 | 564 | def InsertDictionaryChapter(self, value, sticky='no'): 565 | """ Insert new dictionary chapter """ 566 | sql = "INSERT INTO Chapter (sticky, name) VALUES ('%s', '%s')" % (sticky, value) 567 | self.db_insert(sql) 568 | 569 | def InsertDictionaryValue(self, dict_id, value): 570 | """Insert value into dictionary identified by dict_id""" 571 | sql = "INSERT INTO Dictionary (chapter_id,dict_value) VALUES (%d, '%s')" % (dict_id, value) 572 | self.db_insert(sql) 573 | 574 | # Attribute methods 575 | def QueryTypedAttributeValue(self, object_id, attr_id, attr_type): 576 | sql = "SELECT %s FROM AttributeValue WHERE object_id = %d AND attr_id = %d" % (attr_type, object_id, attr_id) 577 | res = self.db_query_one(sql) 578 | 579 | if(res is None): 580 | return None 581 | else: 582 | return res[0] 583 | 584 | def InsertOrUpdateStringAttribute(self, object_id, objtype_id, attr_id, new_value): 585 | old_value = self.QueryTypedAttributeValue(object_id, attr_id, 'string_value') 586 | if(old_value is None): 587 | # INSERT 588 | return "INSERT INTO AttributeValue (object_id,object_tid,attr_id,string_value) VALUES (%d,%d,%d,'%s')" % (object_id, objtype_id, attr_id, new_value) 589 | else: 590 | # UPDATE 591 | return "UPDATE AttributeValue SET string_value = '%s' WHERE object_id = %d AND attr_id = %d AND object_tid = %d" % (new_value, object_id, attr_id, objtype_id) 592 | 593 | def InsertOrUpdateUintAttribute(self, object_id, objtype_id, attr_id, new_value): 594 | old_value = self.QueryTypedAttributeValue(object_id, attr_id, 'uint_value') 595 | if(old_value is None): 596 | # INSERT 597 | return "INSERT INTO AttributeValue (object_id,object_tid,attr_id,uint_value) VALUES (%d,%d,%d,%d)" % (object_id, objtype_id, attr_id, new_value) 598 | elif(old_value != new_value): 599 | # UPDATE 600 | return "UPDATE AttributeValue SET uint_value = %d WHERE object_id = %d AND attr_id = %d AND object_tid = %d" % (new_value, object_id, attr_id, objtype_id) 601 | 602 | def InsertOrUpdateFloatAttribute(self, object_id, objtype_id, attr_id, new_value): 603 | old_value = self.QueryTypedAttributeValue(object_id, attr_id, 'float_value') 604 | if(old_value is None): 605 | # INSERT 606 | return "INSERT INTO AttributeValue (object_id,object_tid,attr_id,float_value) VALUES (%d,%d,%d,%f)" % (object_id, objtype_id, attr_id, new_value) 607 | elif(old_value != new_value): 608 | # UPDATE 609 | return "UPDATE AttributeValue SET float_value = %f WHERE object_id = %d AND attr_id = %d AND object_tid = %d" % (new_value, object_id, attr_id, objtype_id) 610 | 611 | def InsertOrUpdateDateAttribute(self, object_id, objtype_id, attr_id, new_value): 612 | dt = datetime.strptime(new_value, "%Y-%m-%d") 613 | return self.InsertOrUpdateUintAttribute(object_id, objtype_id, attr_id, (dt - datetime(1970, 1, 1)) / timedelta(seconds=1)) 614 | 615 | def InsertOrUpdateAttribute_FunctionDispatcher(self, attr_type): 616 | InsertOrUpdateAttribute_TypeFunctions = { 617 | 'uint': self.InsertOrUpdateUintAttribute, 618 | 'dict': self.InsertOrUpdateUintAttribute, 619 | 'float': self.InsertOrUpdateFloatAttribute, 620 | 'string': self.InsertOrUpdateStringAttribute, 621 | 'date': self.InsertOrUpdateDateAttribute 622 | } 623 | return InsertOrUpdateAttribute_TypeFunctions.get(attr_type) 624 | 625 | def InsertOrUpdateAttribute(self, object_id, attr_id, new_value): 626 | # Get the object type 627 | sql = "SELECT objtype_id FROM Object WHERE id = %d" % (object_id) 628 | result = self.db_query_one(sql) 629 | 630 | if(result is not None): 631 | objtype_id = result[0] 632 | else: 633 | # Object not found in database - return None since we can not update an attribute on a non-existing object 634 | return None 635 | 636 | # Get the attribute type 637 | sql = "SELECT type FROM Attribute WHERE id = %d" % (attr_id) 638 | result = self.db_query_one(sql) 639 | 640 | if(result is not None): 641 | attr_type = result[0] 642 | else: 643 | # Attribute with given ID does not exist - return None since the requested attribute does not exist 644 | return None 645 | 646 | # Get the correct function for this attribute type 647 | func = self.InsertOrUpdateAttribute_FunctionDispatcher(attr_type) 648 | 649 | # Get the SQL statement for Insert/Update 650 | sql = func(object_id, objtype_id, attr_id, new_value) 651 | 652 | # If there is nothing to update (eg. old_value == new_value) then the InsertOrUpdateAttribute_TypeFunction returns None and there is no SQL statement to execute 653 | if(sql is not None): 654 | self.db_insert(sql) 655 | 656 | def GetObjectAttributes(self, object_id): 657 | """Get list of Object attributes""" 658 | 659 | sql = "SELECT ob.id, \ 660 | ob.name AS routerName, \ 661 | ob.label, \ 662 | a.name AS attrName, \ 663 | a.type, \ 664 | d.dict_value, \ 665 | av.* FROM Object as ob \ 666 | JOIN AttributeValue AS av ON (ob.id=av.object_id) \ 667 | JOIN Attribute AS a ON (av.attr_id=a.id) \ 668 | LEFT JOIN Dictionary AS d ON (d.dict_key=av.uint_value) \ 669 | WHERE ob.id = %d ORDER BY a.name" % (int(object_id)) 670 | 671 | return self.db_query_all(sql) 672 | 673 | def CleanUnusedInterfaces(self, object_id, interface_list): 674 | """Remove unused old interfaces""" 675 | sql = "SELECT id, name FROM Port WHERE object_id = %d" % (object_id) 676 | result = self.db_query_all(sql) 677 | 678 | # Copy interface list becouse we need to change it 679 | interfaces = interface_list[:] 680 | # Add drac to interface list 681 | interfaces.append("drac") 682 | 683 | if result is not None: 684 | for row in result: 685 | if row[1] not in interfaces: 686 | # Remove IPv4 allocation 687 | sql = "DELETE FROM IPv4Allocation WHERE object_id = %d AND name = '%s'" % (object_id, row[1]) 688 | self.db_insert(sql) 689 | self.InsertLog(object_id, "Removed IPv4 ips for %s" % row[1]) 690 | # Remove IPv6 allocation 691 | sql = "DELETE FROM IPv6Allocation WHERE object_id = %d AND name = '%s'" % (object_id, row[1]) 692 | self.db_insert(sql) 693 | self.InsertLog(object_id, "Removed IPv6 ips for %s" % row[1]) 694 | # Remove port links 695 | sql = "DELETE FROM Link WHERE porta = %d OR portb = %d" % (row[0], row[0]) 696 | self.db_insert(sql) 697 | self.InsertLog(object_id, "Remove port links %s" % row[1]) 698 | # Remove port 699 | sql = "DELETE FROM Port WHERE object_id = %d AND name = '%s'" % (object_id, row[1]) 700 | self.db_insert(sql) 701 | self.InsertLog(object_id, "Removed interface %s" % row[1]) 702 | 703 | def CleanVirtuals(self, object_id, virtual_servers): 704 | """Clean dead virtuals from hypervisor. virtual_servers is list of active virtual servers on hypervisor (object_id)""" 705 | 706 | sql = "SELECT child_entity_id FROM EntityLink WHERE parent_entity_id = %d" % object_id 707 | 708 | result = self.db_query_all(sql) 709 | 710 | if result is not None: 711 | old_virtuals_ids = result 712 | delete_virtual_id = [] 713 | new_virtuals_ids = [] 714 | # Translate names into ids 715 | for new_virt in virtual_servers: 716 | new_id = self.GetObjectId(new_virt) 717 | if new_id is not None: 718 | new_virtuals_ids.append(new_id) 719 | 720 | for old_id in old_virtuals_ids: 721 | try: 722 | new_virtuals_ids.index(old_id[0]) 723 | except ValueError: 724 | delete_virtual_id.append(old_id[0]) 725 | if len(delete_virtual_id) != 0: 726 | for virt_id in delete_virtual_id: 727 | 728 | sql = "DELETE FROM EntityLink WHERE parent_entity_id = %d AND child_entity_id = %d" % (object_id, virt_id) 729 | self.db_insert(sql) 730 | virt_name = self.GetObjectName(virt_id) 731 | logstring = "Removed virtual %s" % virt_name 732 | self.InsertLog(object_id, logstring) 733 | 734 | 735 | def LinkVirtualHypervisor(self, object_id, virtual_id): 736 | """Assign virtual server to correct hypervisor""" 737 | sql = "SELECT child_entity_id FROM EntityLink WHERE parent_entity_id = %d AND child_entity_id = %d" % (object_id, virtual_id) 738 | result = self.db_query_one(sql) 739 | 740 | if result is None: 741 | sql = "INSERT INTO EntityLink (parent_entity_type, parent_entity_id, child_entity_type, child_entity_id) VALUES ('object',%d,'object',%d)" % (object_id, virtual_id) 742 | self.db_insert(sql) 743 | text = "Linked virtual %s with hypervisor" % self.GetObjectName(virtual_id) 744 | self.InsertLog(object_id, text) 745 | 746 | def AssignChassisSlot(self, chassis_name, slot_number, server_name): 747 | """Assign server objects to server chassis""" 748 | chassis_id = self.GetObjectId(chassis_name) 749 | server_id = self.GetObjectId(server_name) 750 | slot_attribute_id = self.GetAttributeId("Slot number") 751 | 752 | sql = "SELECT string_value FROM AttributeValue WHERE object_id = %d AND object_tid = 4 AND attr_id = %d" % (server_id, slot_attribute_id) 753 | result = self.db_query_one(sql) 754 | 755 | if result is not None: 756 | # Try to update Value, if no success Insert new value 757 | sql = "UPDATE AttributeValue SET string_value = '%s' WHERE object_id = %d AND object_tid = 4 AND attr_id = %d" % (slot_number, server_id, slot_attribute_id) 758 | self.db_insert(sql) 759 | else: 760 | # Assign slot number to server 761 | sql = "INSERT INTO AttributeValue (object_id,object_tid,attr_id,string_value) VALUES ( %d, 4, %d, '%s')" % (server_id, slot_attribute_id, slot_number) 762 | self.db_insert(sql) 763 | 764 | # Assign server to chassis 765 | # Check if it's connected 766 | sql = "SELECT parent_entity_id FROM EntityLink WHERE child_entity_type = 'object' AND child_entity_id = %d" % (server_id) 767 | result = self.db_query_one(sql) 768 | 769 | if result is not None: 770 | # Object is connected to someone 771 | if result[0] != chassis_id: 772 | # Connected to differend chassis/chassis 773 | sql = "UPDATE EntityLink SET parent_entity_id = %d WHERE child_entity_id = %d AND child_entity_type = 'object' AND parent_entity_id = %d" % (chassis_id, server_id, result[0]) 774 | self.db_insert(sql) 775 | 776 | old_object_name = self.GetObjectName(result[0]) 777 | self.InsertLog(result[0], "Unlinked server %s" % (server_name)) 778 | self.InsertLog(server_id, "Unlinked from Blade Chassis %s" % (old_object_name)) 779 | self.InsertLog(chassis_id, "Linked with server %s" % (server_name)) 780 | self.InsertLog(server_id, "Linked with Blade Chassis %s" % (chassis_name)) 781 | 782 | else: 783 | # Object is not connected 784 | sql = "INSERT INTO EntityLink (parent_entity_type, parent_entity_id, child_entity_type, child_entity_id) VALUES ('object', %d, 'object', %d)" % (chassis_id, server_id) 785 | self.db_insert(sql) 786 | self.InsertLog(chassis_id, "Linked with server %s" % (server_name)) 787 | self.InsertLog(server_id, "Linked with Blade Chassis %s" % (chassis_name)) 788 | 789 | def GetAllServerChassisId(self): 790 | """Get list of all server chassis IDs""" 791 | sql = "SELECT id FROM Object WHERE objtype_id = 1502" 792 | return self.db_query_all(sql) 793 | 794 | # 795 | # Networks methots 796 | # 797 | def GetIpv4Networks(self): 798 | """Get All IPV4 Networks""" 799 | sql = "SELECT id, INET_NTOA(ip), mask, name FROM IPv4Network" 800 | 801 | return self.db_query_all(sql) 802 | 803 | def GetIpv6Networks(self): 804 | """Get All IPV6 Networks""" 805 | sql = "SELECT id, HEX(ip), mask, name FROM IPv6Network" 806 | 807 | return self.db_query_all(sql) 808 | 809 | def GetIpv4Allocations(self): 810 | """Get IPv4 Allocations for specific network""" 811 | sql = "SELECT INET_NTOA(ip), object_id, name AS int_name, Null AS name, Null AS comment from IPv4Allocation UNION SELECT INET_NTOA(ip), Null AS object_id, Null AS int_name, name, comment FROM IPv4Address" 812 | 813 | return self.db_query_all(sql) 814 | 815 | def GetIpv6Allocations(self): 816 | """Get IPv6 Allocations for specific network""" 817 | sql = "SELECT HEX(ip), object_id, name AS int_name, Null AS name, Null AS comment from IPv6Allocation UNION SELECT HEX(ip), Null AS object_id, Null AS int_name, name, comment FROM IPv6Address" 818 | 819 | return self.db_query_all(sql) 820 | 821 | def SetIPComment(self, comment, ip): 822 | """ Set comment for IP address """ 823 | sql = "SELECT comment FROM IPv4Address WHERE INET_NTOA(ip) = '%s'" % (ip) 824 | result = self.db_query_one(sql) 825 | 826 | if result is not None: 827 | sql = "UPDATE IPv4Address SET comment = '%s' WHERE INET_NTOA(ip) = '%s'" % (comment, ip) 828 | else: 829 | sql = "INSERT INTO IPv4Address (ip, comment) VALUES (INET_ATON('%s'), '%s')" % (ip, comment) 830 | 831 | self.db_insert(sql) 832 | 833 | def SetIPName(self, name, ip): 834 | """ Set name for IP address """ 835 | sql = "SELECT name FROM IPv4Address WHERE INET_NTOA(ip) = '%s'" % (ip) 836 | result = self.db_query_one(sql) 837 | 838 | if result is not None: 839 | sql = "UPDATE IPv4Address SET name = '%s' WHERE INET_NTOA(ip) = '%s'" % (name, ip) 840 | else: 841 | sql = "INSERT INTO IPv4Address (ip, name) VALUES (INET_ATON('%s'), '%s')" % (ip, name) 842 | 843 | self.db_insert(sql) 844 | 845 | def FindIPFromComment(self, comment, network_name): 846 | """Find IP address based on comment""" 847 | # Get Network information 848 | sql = "SELECT ip,mask from IPv4Network WHERE name = '%s'" % (network_name) 849 | result = self.db_query_one(sql) 850 | 851 | if result is not None: 852 | ip_int = result[0] 853 | ip_mask = result[1] 854 | ip_int_max = (2 ** (32 - ip_mask)) + ip_int 855 | 856 | sql = "SELECT INET_NTOA(ip) FROM IPv4Address WHERE ip >= %d AND ip <= %d and comment = '%s'" % (ip_int, ip_int_max, comment) 857 | 858 | result = self.db_query_all(sql) 859 | if result is not None: 860 | return "\n".join(str(x[0]) + "/" + str(ip_mask) for x in result) 861 | else: 862 | return False 863 | 864 | else: 865 | return False 866 | 867 | def SetIP6Comment(self, comment, ip): 868 | """ Set comment for IPv6 address """ 869 | 870 | # Create address object using ipaddr 871 | addr6 = ipaddress.IPv6Address(ip) 872 | # Create IPv6 format for Mysql 873 | db_ip6_format = "".join(str(x) for x in addr6.exploded.split(':')).upper() 874 | 875 | sql = "SELECT comment FROM IPv6Address WHERE HEX(ip) = '%s'" % (db_ip6_format) 876 | result = self.db_query_one(sql) 877 | 878 | if result is not None: 879 | sql = "UPDATE IPv6Address SET comment = '%s' WHERE HEX(ip) = '%s'" % (comment, db_ip6_format) 880 | else: 881 | sql = "INSERT INTO IPv6Address (ip, comment) VALUES (UNHEX('%s'), '%s')" % (db_ip6_format, comment) 882 | 883 | self.db_insert(sql) 884 | 885 | def FindIPv6FromComment(self, comment, network_name): 886 | """Find IP address based on comment""" 887 | sql = "SELECT HEX(ip),mask,hex(last_ip) from IPv6Network WHERE name = '%s'" % (network_name) 888 | result = self.db_query_one(sql) 889 | 890 | if result is not None: 891 | ip = result[0] 892 | ip_mask = result[1] 893 | ip_max = result[2] 894 | 895 | sql = "select HEX(ip) from IPv6Address where ip between UNHEX('%s') AND UNHEX('%s') AND comment = '%s';" % (ip, ip_max, comment) 896 | 897 | result = self.db_query_all(sql) 898 | if result is not None: 899 | return "\n".join(str(re.sub(r'(.{4})(?=.)', r'\1:', x[0]).lower()) + "/" + str(ip_mask) for x in result) 900 | else: 901 | return False 902 | 903 | else: 904 | return False 905 | 906 | def CleanIPAddresses(self, object_id, ip_addresses, device): 907 | """Clean unused ip from object. ip addresses is list of IP addresses configured on device (device) on host (object_id)""" 908 | 909 | sql = "SELECT INET_NTOA(ip) FROM IPv4Allocation WHERE object_id = %d AND name = '%s'" % (object_id, device) 910 | 911 | result = self.db_query_all(sql) 912 | 913 | if result is not None: 914 | old_ips = result 915 | delete_ips = [] 916 | 917 | for old_ip in old_ips: 918 | try: 919 | ip_addresses.index(old_ip[0]) 920 | except ValueError: 921 | delete_ips.append(old_ip[0]) 922 | if len(delete_ips) != 0: 923 | for ip in delete_ips: 924 | sql = "DELETE FROM IPv4Allocation WHERE ip = INET_ATON('%s') AND object_id = %d AND name = '%s'" % (ip, object_id, device) 925 | self.db_insert(sql) 926 | logstring = "Removed IP %s from %s" % (ip, device) 927 | self.InsertLog(object_id, logstring) 928 | 929 | def CleanIPv6Addresses(self, object_id, ip_addresses, device): 930 | """Clean unused ipv6 from object. ip_addresses mus be list of active IP addresses on device (device) on host (object_id)""" 931 | 932 | sql = "SELECT HEX(ip) FROM IPv6Allocation WHERE object_id = %d AND name = '%s'" % (object_id, device) 933 | result = self.db_query_all(sql) 934 | 935 | if result is not None: 936 | old_ips = result 937 | delete_ips = [] 938 | new_ip6_ips = [] 939 | 940 | # We must prepare ipv6 addresses into same format for compare 941 | for new_ip in ip_addresses: 942 | converted = ipaddress.IPv6Address(new_ip).exploded.lower() 943 | new_ip6_ips.append(converted) 944 | 945 | for old_ip_hex in old_ips: 946 | try: 947 | # First we must construct IP from HEX 948 | tmp = re.sub("(.{4})", "\\1:", old_ip_hex[0], re.DOTALL) 949 | # Remove last : and lower string 950 | old_ip = tmp[:len(tmp) - 1].lower() 951 | 952 | new_ip6_ips.index(old_ip) 953 | 954 | except ValueError: 955 | delete_ips.append(old_ip) 956 | 957 | if len(delete_ips) != 0: 958 | for ip in delete_ips: 959 | db_ip6_format = "".join(str(x) for x in ip.split(':')) 960 | sql = "DELETE FROM IPv6Allocation WHERE ip = UNHEX('%s') AND object_id = %d AND name = '%s'" % (db_ip6_format, object_id, device) 961 | self.db_insert(sql) 962 | logstring = "Removed IP %s from %s" % (ip, device) 963 | self.InsertLog(object_id, logstring) 964 | 965 | def CheckIfIp4IPExists(self, ip): 966 | """Check if ipv4 record exist in database""" 967 | sql = "select ip from IPv4Address where ip = INET_ATON('%s')" % (ip) 968 | if self.db_query_one(sql) is None: 969 | sql = "select ip from IPv4Allocation where ip = INET_ATON('%s')" % (ip) 970 | if self.db_query_one(sql) is None: 971 | return False 972 | else: 973 | return True 974 | else: 975 | return True 976 | 977 | def LinkNetworkInterface(self, object_id, interface, switch_name, interface_switch): 978 | """Link two devices togetger""" 979 | # Get interface id 980 | port_id = self.GetInterfaceId(object_id, interface) 981 | if port_id is not None: 982 | # Get switch object ID 983 | switch_object_id = self.GetObjectId(switch_name) 984 | if switch_object_id is not None: 985 | switch_port_id = self.GetInterfaceId(switch_object_id, interface_switch) 986 | if switch_port_id is not None: 987 | if switch_port_id > port_id: 988 | select_object = 'portb' 989 | else: 990 | select_object = 'porta' 991 | 992 | # Check server interface, update or create new link 993 | sql = "SELECT %s FROM Link WHERE porta = %d OR portb = %d" % (select_object, port_id, port_id) 994 | result = self.db_query_one(sql) 995 | if result is None: 996 | # Check if switch port is connected to another server 997 | sql = "SELECT porta,portb FROM Link WHERE porta = %d OR portb = %d" % (switch_port_id, switch_port_id) 998 | result = self.db_query_one(sql) 999 | if result is not None: 1000 | # Get ports id of old link 1001 | old_link_a, old_link_b = result 1002 | old_link_a_dict = self.GetPortDeviceNameById(old_link_a) 1003 | old_link_b_dict = self.GetPortDeviceNameById(old_link_b) 1004 | 1005 | # Clean switchport connection 1006 | sql = "DELETE FROM Link WHERE porta = %d OR portb = %d" % (switch_port_id, switch_port_id) 1007 | self.db_insert(sql) 1008 | 1009 | # Log message to both device 1010 | text = "Disconnected %s,%s from %s,%s" % (old_link_a_dict['device_name'], old_link_a_dict['port_name'], old_link_b_dict['device_name'], old_link_b_dict['port_name']) 1011 | self.InsertLog(self.GetObjectId(old_link_a_dict['device_name']), text) 1012 | self.InsertLog(self.GetObjectId(old_link_b_dict['device_name']), text) 1013 | 1014 | # Insert new connection 1015 | sql = "INSERT INTO Link (porta,portb) VALUES (%d,%d)" % (port_id, switch_port_id) 1016 | self.db_insert(sql) 1017 | resolution = True 1018 | 1019 | # Log it to both devices 1020 | device_dict = self.GetPortDeviceNameById(port_id) 1021 | switch_dict = self.GetPortDeviceNameById(switch_port_id) 1022 | text = "New connection %s,%s with %s,%s" % (device_dict['device_name'], device_dict['port_name'], switch_dict['device_name'], switch_dict['port_name']) 1023 | self.InsertLog(self.GetObjectId(device_dict['device_name']), text) 1024 | self.InsertLog(self.GetObjectId(switch_dict['device_name']), text) 1025 | else: 1026 | # Update old connection 1027 | old_switch_port_id = result[0] 1028 | if old_switch_port_id != switch_port_id: 1029 | # Clean previous link first 1030 | # Check and clean previous link (port_id) 1031 | sql = "SELECT porta,portb FROM Link WHERE porta = %d OR portb = %d" % (port_id, port_id) 1032 | result = self.db_query_one(sql) 1033 | if result is not None: 1034 | # Get ports id of old link 1035 | old_link_a, old_link_b = result 1036 | old_link_a_dict = self.GetPortDeviceNameById(old_link_a) 1037 | old_link_b_dict = self.GetPortDeviceNameById(old_link_b) 1038 | 1039 | # Clean switchport connection 1040 | sql = "DELETE FROM Link WHERE porta = %d OR portb = %d" % (port_id, port_id) 1041 | self.db_insert(sql) 1042 | 1043 | # Log message to both device 1044 | text = "Disconnected %s,%s from %s,%s" % (old_link_a_dict['device_name'], old_link_a_dict['port_name'], old_link_b_dict['device_name'], old_link_b_dict['port_name']) 1045 | self.InsertLog(self.GetObjectId(old_link_a_dict['device_name']), text) 1046 | self.InsertLog(self.GetObjectId(old_link_b_dict['device_name']), text) 1047 | 1048 | # Insert new connection 1049 | sql = "INSERT INTO Link (porta,portb) VALUES (%d,%d)" % (switch_port_id, port_id) 1050 | self.db_insert(sql) 1051 | 1052 | # Log all three devices 1053 | old_switch_dict = self.GetPortDeviceNameById(old_switch_port_id) 1054 | switch_dict = self.GetPortDeviceNameById(switch_port_id) 1055 | device_dict = self.GetPortDeviceNameById(port_id) 1056 | text = "Update connection from %s,%s to %s,%s" % (old_switch_dict['device_name'], old_switch_dict['port_name'], switch_dict['device_name'], switch_dict['port_name']) 1057 | self.InsertLog(self.GetObjectId(device_dict['device_name']), text) 1058 | 1059 | text = "%s,%s changed connection from %s,%s and connected to %s,%s" % (device_dict['device_name'], device_dict['port_name'], old_switch_dict['device_name'], old_switch_dict['port_name'], switch_dict['device_name'], switch_dict['port_name']) 1060 | self.InsertLog(self.GetObjectId(old_switch_dict['device_name']), text) 1061 | self.InsertLog(self.GetObjectId(switch_dict['device_name']), text) 1062 | 1063 | resolution = True 1064 | resolution = None 1065 | 1066 | else: 1067 | resolution = None 1068 | else: 1069 | resolution = None 1070 | 1071 | else: 1072 | resolution = None 1073 | 1074 | return resolution 1075 | 1076 | def ObjectGetIpv4IPList(self,object_id): 1077 | ''' Get list of IPv4 IP from object ''' 1078 | sql = "SELECT INET_NTOA(ip) AS ip from IPv4Allocation where object_id = %i" % (object_id) 1079 | return self.db_query_all(sql) 1080 | 1081 | def ObjectGetIpv6IPList(self,object_id): 1082 | ''' Get list of IPv6 IP from object ''' 1083 | sql = "SELECT HEX(ip) AS ip from IPv6Allocation where object_id = %i" % (object_id) 1084 | return self.db_query_all(sql) 1085 | 1086 | def InterfaceGetIpv4IP(self, object_id, interface): 1087 | """ Get list of IPv4 IP from interface """ 1088 | sql = "SELECT INET_NTOA(ip) AS ip from IPv4Allocation where object_id = %i AND name = '%s'" % (object_id, interface) 1089 | return self.db_query_all(sql) 1090 | 1091 | def InterfaceGetIpv6IP(self, object_id, interface): 1092 | """ Get list of IPv6 IP from interface """ 1093 | sql = "SELECT HEX(ip) AS ip from IPv6Allocation where object_id = %i AND name = '%s'" % (object_id, interface) 1094 | return self.db_query_all(sql) 1095 | 1096 | def InterfaceAddIpv4IP(self, object_id, device, ip): 1097 | """Add/Update IPv4 IP on interface""" 1098 | 1099 | sql = "SELECT INET_NTOA(ip) from IPv4Allocation WHERE object_id = %d AND name = '%s'" % (object_id, device) 1100 | result = self.db_query_all(sql) 1101 | 1102 | if result is not None: 1103 | old_ips = result 1104 | 1105 | is_there = "no" 1106 | 1107 | for old_ip in old_ips: 1108 | if old_ip[0] == ip: 1109 | is_there = "yes" 1110 | 1111 | if is_there == "no": 1112 | sql = "SELECT name FROM IPv4Allocation WHERE object_id = %d AND ip = INET_ATON('%s')" % (object_id, ip) 1113 | result = self.db_query_all(sql) 1114 | 1115 | if result is not None: 1116 | if result != (): 1117 | sql = "DELETE FROM IPv4Allocation WHERE object_id = %d AND ip = INET_ATON('%s')" % (object_id, ip) 1118 | self.db_insert(sql) 1119 | self.InsertLog(object_id, "Removed IP (%s) from interface %s" % (ip, result[0][0])) 1120 | 1121 | sql = "INSERT INTO IPv4Allocation (object_id,ip,name) VALUES (%d,INET_ATON('%s'),'%s')" % (object_id, ip, device) 1122 | self.db_insert(sql) 1123 | text = "Added IP %s on %s" % (ip, device) 1124 | self.InsertLog(object_id, text) 1125 | 1126 | def InterfaceAddIpv6IP(self, object_id, device, ip): 1127 | """Add/Update IPv6 IP on interface""" 1128 | # Create address object using ipaddress 1129 | addr6 = ipaddress.IPv6Address(ip) 1130 | # Create IPv6 format for Mysql 1131 | ip6 = "".join(str(x) for x in addr6.exploded.split(':')).upper() 1132 | 1133 | sql = "SELECT HEX(ip) FROM IPv6Allocation WHERE object_id = %d AND name = '%s'" % (object_id, device) 1134 | result = self.db_query_all(sql) 1135 | 1136 | if result is not None: 1137 | old_ips = result 1138 | 1139 | is_there = "no" 1140 | 1141 | for old_ip in old_ips: 1142 | if old_ip[0] == ip6: 1143 | is_there = "yes" 1144 | 1145 | if is_there == "no": 1146 | sql = "SELECT name FROM IPv6Allocation WHERE object_id = %d AND ip = UNHEX('%s')" % (object_id, ip6) 1147 | result = self.db_query_all(sql) 1148 | 1149 | if result is not None: 1150 | if result != (): 1151 | sql = "DELETE FROM IPv6Allocation WHERE object_id = %d AND ip = UNHEX('%s')" % (object_id, ip6) 1152 | self.db_insert(sql) 1153 | self.InsertLog(object_id, "Removed IP (%s) from interface %s" % (ip, result[0][0])) 1154 | 1155 | sql = "INSERT INTO IPv6Allocation (object_id,ip,name) VALUES (%d,UNHEX('%s'),'%s')" % (object_id, ip6, device) 1156 | self.db_insert(sql) 1157 | text = "Added IPv6 IP %s on %s" % (ip, device) 1158 | self.InsertLog(object_id, text) 1159 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | with open("requirements.txt", "r") as fh: 7 | install_reqs = fh.read().split() 8 | 9 | setup( 10 | name="racktables-api", 11 | version="0.3.1", 12 | packages=["rtapi"], 13 | license="GPLv2", 14 | description="Simple racktables API", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | install_requires=install_reqs, 18 | url="https://github.com/rvojcik/rtapi", 19 | author="Robert Vojcik", 20 | author_email="robert@vojcik.net", 21 | keywords=['rtapi', 'racktables', 'racktables api', 'racktables-api','racktables cli','racktables-cli'], 22 | classifiers=[ 23 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 24 | "Operating System :: POSIX", 25 | "Operating System :: Unix", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.6", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Topic :: Database" 32 | ] 33 | ) 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/db_connection/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import MySQLdb 4 | import sys 5 | 6 | # Create connection to database 7 | try: 8 | # Create connection to database 9 | db = MySQLdb.connect(host='rtdb',port=3306, passwd='toor',db='racktables',user='root') 10 | except MySQLdb.Error: 11 | e = sys.exc_info()[1] 12 | print("Error %d: %s" % (e.args[0],e.args[1])) 13 | sys.exit(1) 14 | 15 | -------------------------------------------------------------------------------- /tests/object_for_test.py: -------------------------------------------------------------------------------- 1 | test_object_basic = { "name":"server1", "typeid":4, "asset":"GHHR1234", "label":"test server" } 2 | test_chassis = { "name":"BLADE1", "typeid":1502, "asset":"BLADE-1234", "label":"Blade chassis" } 3 | test_chassis_child = { "name":"server-module", "typeid":4, "asset":"GYYR134", "label":"blade server" } 4 | test_object_ip = { "name":"network", "typeid":4, "asset":"GYYR140", "label":"server for IP tests" } 5 | test_ip_interfaces = [ "eth0", "enp1s0" ] 6 | test_ip_addresses = { "ipv4" : "192.168.0.1" , "ipv6" : "2001:fe45:34::1" } 7 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | ipaddress 2 | mysqlclient 3 | pytest 4 | coverage 5 | -------------------------------------------------------------------------------- /tests/rtapi_con/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import db_connection as db 4 | import rtapi 5 | 6 | rtapi = rtapi.RTObject(db.db) 7 | -------------------------------------------------------------------------------- /tests/test_00_basic_methods.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rtapi_con as rt 4 | import object_for_test 5 | import sys 6 | test_object = object_for_test.test_object_basic 7 | test_object_id = 0 8 | 9 | if sys.version_info[0] == 2: 10 | test_instance = long 11 | else: 12 | test_instance = int 13 | 14 | def test_noObjectExistST(): 15 | assert rt.rtapi.ObjectExistST('asdf000') == False 16 | 17 | def test_noObjectExistName(): 18 | assert rt.rtapi.ObjectExistName('asdf000') == False 19 | 20 | def test_AddObject(): 21 | global test_object_id 22 | test_object_id = rt.rtapi.AddObject(test_object["name"],test_object["typeid"],test_object["asset"], test_object["label"]) 23 | assert isinstance(test_object_id, test_instance) == True 24 | 25 | def test_ObjectExistST(): 26 | assert rt.rtapi.ObjectExistST(test_object["asset"]) == True 27 | 28 | def test_ObjectExistName(): 29 | assert rt.rtapi.ObjectExistName(test_object["name"]) == True 30 | 31 | def test_GetObjectName(): 32 | assert rt.rtapi.GetObjectName(test_object_id) == test_object["name"] 33 | 34 | def test_GetObjectNameByAsset(): 35 | assert rt.rtapi.GetObjectNameByAsset(test_object["asset"]) == test_object["name"] 36 | 37 | def test_GetObjectIdByAsset(): 38 | assert rt.rtapi.GetObjectIdByAsset(test_object["asset"]) == test_object_id 39 | 40 | def test_GetObjectLabel(): 41 | assert rt.rtapi.GetObjectLabel(test_object_id) == test_object["label"] 42 | 43 | def test_UpdateObjectComment(): 44 | assert rt.rtapi.UpdateObjectComment(test_object_id, 'commentXY') == None 45 | 46 | def test_GetObjectComment(): 47 | assert rt.rtapi.GetObjectComment(test_object_id) == 'commentXY' 48 | 49 | def test_UpdateObjectLabel(): 50 | assert rt.rtapi.UpdateObjectLabel(test_object_id, 'labelXY') == None 51 | 52 | def test_UpdateObjectName(): 53 | assert rt.rtapi.UpdateObjectName(test_object_id, 'TESTNAME') == None 54 | 55 | def test_GetObjectId(): 56 | assert rt.rtapi.GetObjectId('TESTNAME') == test_object_id 57 | 58 | 59 | -------------------------------------------------------------------------------- /tests/test_10_attributes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rtapi_con as rt 4 | import object_for_test 5 | test_dict_value = None 6 | test_chapter = 'kernels' 7 | test_object_id = 1 8 | 9 | 10 | def test_GetAttributeIdByName(): 11 | assert isinstance(rt.rtapi.GetAttributeIdByName('HW type'), int) == True 12 | 13 | def test_GetAttributeId(): 14 | assert isinstance(rt.rtapi.GetAttributeId('HW type'), int) == True 15 | 16 | def test_GetDictionaryChapterId(): 17 | chapter_id = rt.rtapi.GetDictionaryChapterId('Yes/No') 18 | assert isinstance(chapter_id, int) == True 19 | 20 | def test_IntertDictionaryValue(): 21 | chapter_id = rt.rtapi.GetDictionaryChapterId('server models') 22 | assert rt.rtapi.InsertDictionaryValue(chapter_id, 'testmodel') == None 23 | 24 | def test_GetDictionaryIdByValue(): 25 | global test_dict_value 26 | test_dict_value = rt.rtapi.GetDictionaryIdByValue('testmodel') 27 | assert isinstance(test_dict_value, int) == True 28 | 29 | def test_GetDictionaryValueById(): 30 | assert rt.rtapi.GetDictionaryValueById(test_dict_value) == 'testmodel' 31 | 32 | def test_InsertDictionaryChapter(): 33 | assert rt.rtapi.InsertDictionaryChapter(test_chapter) == None 34 | 35 | def test_InsertAttribute(): 36 | att_id = rt.rtapi.GetAttributeId("HW type") 37 | hw_id = rt.rtapi.GetDictionaryId('testmodel') 38 | assert rt.rtapi.InsertAttribute(test_object_id,4,att_id,"NULL",hw_id, 'TESTNAME') == None 39 | -------------------------------------------------------------------------------- /tests/test_10_chassis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rtapi_con as rt 4 | import object_for_test as tobj 5 | 6 | 7 | def test_AssignChassisSlot(): 8 | # Add chassis and server to DB 9 | rt.rtapi.AddObject(tobj.test_chassis['name'], tobj.test_chassis['typeid'], tobj.test_chassis['asset'], tobj.test_chassis['label']) 10 | rt.rtapi.AddObject(tobj.test_chassis_child['name'], tobj.test_chassis_child['typeid'], tobj.test_chassis_child['asset'], tobj.test_chassis_child['label']) 11 | 12 | assert rt.rtapi.AssignChassisSlot(tobj.test_chassis['name'], "A1", tobj.test_chassis_child['name']) == None 13 | 14 | def test_AssignCHassisSlot(): 15 | chassis_id = rt.rtapi.GetObjectId(tobj.test_chassis['name']) 16 | 17 | assert rt.rtapi.GetAllServerChassisId()[0][0] == chassis_id 18 | -------------------------------------------------------------------------------- /tests/test_10_ipv4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import rtapi_con as rt 4 | import object_for_test as tobj 5 | 6 | rt.rtapi.AddObject(tobj.test_object_ip['name'], tobj.test_object_ip['typeid'], tobj.test_object_ip['asset'], tobj.test_object_ip['label']) 7 | obj_id = rt.rtapi.GetObjectId(tobj.test_object_ip['name']) 8 | 9 | def test_UpdateNetworkInterface(): 10 | # Add test interfaces 11 | for i in tobj.test_ip_interfaces: 12 | rt.rtapi.UpdateNetworkInterface(obj_id, i) 13 | 14 | # Check number of interfaces 15 | assert len(rt.rtapi.GetInterfaceList(obj_id)) == len(tobj.test_ip_interfaces) 16 | 17 | def test_GetInterfaceName(): 18 | assert rt.rtapi.GetInterfaceName(obj_id, rt.rtapi.GetInterfaceId(obj_id, tobj.test_ip_interfaces[0])) == tobj.test_ip_interfaces[0] 19 | 20 | def test_InterfaceAddIpv4IP(): 21 | assert rt.rtapi.InterfaceAddIpv4IP(obj_id, tobj.test_ip_interfaces[0], tobj.test_ip_addresses["ipv4"]) == None 22 | 23 | def test_InterfaceGetIpv4IP(): 24 | assert rt.rtapi.InterfaceGetIpv4IP(obj_id, tobj.test_ip_interfaces[0])[0][0] == tobj.test_ip_addresses["ipv4"] 25 | 26 | def test_InterfaceAddIpv6IP(): 27 | assert rt.rtapi.InterfaceAddIpv6IP(obj_id, tobj.test_ip_interfaces[0], tobj.test_ip_addresses["ipv6"]) == None 28 | 29 | --------------------------------------------------------------------------------