├── .github └── workflows │ ├── docker-push-image.yml │ ├── docker-readme.yml │ └── pypi.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── bin └── opensips-cli ├── docker ├── Dockerfile ├── Makefile ├── docker.md └── run.sh ├── docs ├── INSTALLATION.md └── modules │ ├── database.md │ ├── diagnose.md │ ├── instance.md │ ├── mi.md │ ├── tls.md │ ├── trace.md │ ├── trap.md │ └── user.md ├── etc └── default.cfg ├── opensipscli ├── __init__.py ├── args.py ├── cli.py ├── comm.py ├── config.py ├── db.py ├── defaults.py ├── libs │ ├── __init__.py │ └── sqlalchemy_utils.py ├── logger.py ├── main.py ├── module.py ├── modules │ ├── __init__.py │ ├── database.py │ ├── diagnose.py │ ├── instance.py │ ├── mi.py │ ├── tls.py │ ├── trace.py │ ├── trap.py │ └── user.py └── version.py ├── packaging ├── debian │ ├── .gitignore │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── rules │ ├── source │ │ └── format │ └── watch └── redhat_fedora │ └── opensips-cli.spec ├── setup.py └── test ├── alltests.py ├── test-database.sh └── test.sh /.github/workflows/docker-push-image.yml: -------------------------------------------------------------------------------- 1 | name: Push OpenSIPS CLI Images in Docker Hub 2 | 3 | on: 4 | push: 5 | repository_dispatch: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Log in to Docker Hub 16 | uses: docker/login-action@v2.1.0 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_TOKEN }} 20 | 21 | - name: Build and push Docker image 22 | uses: docker/build-push-action@v4 23 | with: 24 | context: ./docker/ 25 | push: true 26 | tags: opensips/opensips-cli:latest 27 | -------------------------------------------------------------------------------- /.github/workflows/docker-readme.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update Docker Hub Description 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ./docker/docker.md 9 | - .github/workflows/docker-readme.yml 10 | 11 | jobs: 12 | dockerHubDescription: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | 18 | - name: Docker Hub Description 19 | uses: peter-evans/dockerhub-description@v4 20 | with: 21 | username: ${{ secrets.DOCKER_USERNAME }} 22 | password: ${{ secrets.DOCKER_TOKEN }} 23 | repository: opensips/opensips-cli 24 | readme-filepath: ./docker/docker.md 25 | short-description: ${{ github.event.repository.description }} 26 | enable-url-completion: true 27 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish OpenSIPS CLI Python package to PyPI 3 | 4 | on: push 5 | 6 | jobs: 7 | build: 8 | name: Build distribution 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.x" 17 | - name: Install pypa/build 18 | run: >- 19 | python3 -m 20 | pip install 21 | build 22 | --user 23 | - name: Build a binary wheel and a source tarball 24 | run: python3 -m build 25 | - name: Store the distribution packages 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: python-package-distributions 29 | path: dist/ 30 | 31 | publish-to-pypi: 32 | name: >- 33 | Publish Python OpenSIPS CLI Python package to PyPI 34 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 35 | needs: 36 | - build 37 | runs-on: ubuntu-latest 38 | environment: 39 | name: release 40 | url: https://pypi.org/p/opensips-cli 41 | permissions: 42 | id-token: write # IMPORTANT: mandatory for trusted publishing 43 | 44 | steps: 45 | - name: Download all the dists 46 | uses: actions/download-artifact@v4 47 | with: 48 | name: python-package-distributions 49 | path: dist/ 50 | - name: Publish distribution OpenSIPS CLI to PyPI 51 | uses: pypa/gh-action-pypi-publish@release/v1 52 | 53 | github-release: 54 | name: >- 55 | Sign the OpenSIPS CLI Python package with Sigstore 56 | and upload them to GitHub Release 57 | needs: 58 | - publish-to-pypi 59 | runs-on: ubuntu-latest 60 | 61 | permissions: 62 | contents: write # IMPORTANT: mandatory for making GitHub Releases 63 | id-token: write # IMPORTANT: mandatory for sigstore 64 | 65 | steps: 66 | - name: Download all the dists 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: python-package-distributions 70 | path: dist/ 71 | - name: Sign the dists with Sigstore 72 | uses: sigstore/gh-action-sigstore-python@v2.1.1 73 | with: 74 | inputs: >- 75 | ./dist/*.tar.gz 76 | ./dist/*.whl 77 | - name: Create GitHub Release 78 | env: 79 | GITHUB_TOKEN: ${{ github.token }} 80 | run: >- 81 | gh release create 82 | '${{ github.ref_name }}' 83 | --repo '${{ github.repository }}' 84 | --notes "" 85 | - name: Upload artifact signatures to GitHub Release 86 | env: 87 | GITHUB_TOKEN: ${{ github.token }} 88 | # Upload to GitHub Release using the `gh` CLI. 89 | # `dist/` contains the built packages, and the 90 | # sigstore-produced signatures and certificates. 91 | run: >- 92 | gh release upload 93 | '${{ github.ref_name }}' dist/** 94 | --repo '${{ github.repository }}' 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.swp 4 | __pycach* 5 | /.pybuild/ 6 | /build/ 7 | /dist/ 8 | /MANIFEST 9 | *egg-info/ 10 | tags 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE MANIFEST MANIFEST.in etc/default.cfg 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI (Command Line Interface) 2 | 3 | OpenSIPS CLI is an interactive command line tool that can be used to control 4 | and monitor **OpenSIPS SIP servers**. It uses the Management Interface 5 | exported by OpenSIPS over JSON-RPC to gather raw information from OpenSIPS and 6 | display it in a nicer, more structured manner to the user. 7 | 8 | The tool is very flexible and has a modular design, consisting of multiple 9 | modules that implement different features. New modules can be easily added by 10 | creating a new module that implements the [OpenSIPS CLI 11 | Module](opensipscli/module.py) Interface. 12 | 13 | OpenSIPS CLI is an interactive console that features auto-completion and 14 | reverse/forward command history search, but can also be used to execute 15 | one-liners for automation purposes. 16 | 17 | OpenSIPS CLI can communicate with an OpenSIPS server using different transport 18 | methods, such as fifo or http. 19 | 20 | # Compatibility 21 | 22 | This tool uses the new JSON-RPC interface added in OpenSIPS 3.0, therefore 23 | it can only be used with OpenSIPS versions higher than or equal to 3.0. For older 24 | versions of OpenSIPS, use the classic `opensipsctl` tool from the `opensips` project. 25 | 26 | ## Usage 27 | 28 | ### Tool 29 | 30 | Simply run `opensips-cli` tool directly in your cli. 31 | By default the tool will start in interactive mode. 32 | 33 | OpenSIPS CLI accepts the following arguments: 34 | * `-h|--help` - used to display information about running `opensips-cli` 35 | * `-v|--version` - displays the version of the running tool 36 | * `-d|--debug` - starts the `opensips-cli` tool with debugging enabled 37 | * `-f|--config` - specifies a configuration file (see [Configuration 38 | Section](#configuration) for more information) 39 | * `-i|--instance INSTANCE` - changes the configuration instance (see [Instance 40 | Module](docs/modules/instance.md) Documentation for more information) 41 | * `-o|--option KEY=VALUE` - sets/overwrites the `KEY` configuration parameter 42 | with the specified `VALUE`. Works for both core and modules parameters. Can be 43 | used multiple times, for different options 44 | * `-x|--execute` - executes the command specified and exits 45 | 46 | In order to run `opensips-cli` without installing it, you have to export the 47 | `PYTHONPATH` variable to the root of the `opensips-cli` and `python-opensips` 48 | packages. If you installed the two packages under `/usr/local/src`, simply do: 49 | 50 | ``` 51 | export PYTHONPATH=/usr/local/src/opensips-cli:/usr/local/src/python-opensips 52 | /usr/local/src/opensips-cli/bin/opensips-cli 53 | ``` 54 | 55 | ### Python Module 56 | 57 | The module can be used as a python module as well. A simple snippet of running 58 | an MI command using the tool is: 59 | 60 | ``` 61 | from opensipscli import cli 62 | 63 | opensipscli = cli.OpenSIPSCLI() 64 | print(opensipscli.mi('ps')) 65 | ``` 66 | 67 | The OpenSIPSCLI object can receive a set of arguments/modifiers through the 68 | `OpenSIPSCLIArgs` class, i.e.: 69 | 70 | ``` 71 | from opensipscli import args 72 | ... 73 | args = OpenSIPSCLIArgs(debug=True) 74 | opensipscli = cli.OpenSIPSCLI(args) 75 | ... 76 | ``` 77 | 78 | Custom settings can be provided thourgh the arguments, i.e.: 79 | ``` 80 | # run commands over http 81 | args = OpenSIPSCLIArgs(communcation_type = "http", 82 | url="http://127.0.0.1:8080/mi") 83 | ... 84 | ``` 85 | 86 | ### Docker Image 87 | 88 | The OpenSIPS CLI tool can be run in a Docker container. The image is available 89 | on Docker Hub at [opensips/opensips-cli](https://hub.docker.com/r/opensips/opensips-cli). 90 | For more information on how to run the tool in a Docker container, please refer to the 91 | [OpenSIPS CLI Docker Image](docker/docker.md) documentation. 92 | 93 | ## Configuration 94 | 95 | OpenSIPS CLI accepts a configuration file, formatted as an `ini` or `cfg` 96 | file, that can store certain parameters that influence the behavior of the 97 | OpenSIPS CLI tool. You can find [here](etc/default.cfg) an example of a 98 | configuration file that behaves exactly as the default parameters. The set of 99 | default values used, when no configuration file is specified, can be found 100 | [here](opensipscli/defaults.py). 101 | 102 | The configuration file can have multiple sections/instances, managed by the 103 | [Instance](docs/modules/instance.md) module. One can choose different 104 | instances from the configuration file by specifying the `-i INSTANCE` argument 105 | when starting the cli tool. 106 | 107 | If no configuration file is specified by the `-f|--config` argument, OpenSIPS 108 | CLI searches for one in the following locations: 109 | 110 | * `~/.opensips-cli.cfg` (highest precedence) 111 | * `/etc/opensips-cli.cfg` 112 | * `/etc/opensips/opensips-cli.cfg` (lowest precedence) 113 | 114 | If no file is found, it starts with the default configuration. 115 | 116 | The OpenSIPS CLI core can use the following parameters: 117 | 118 | * `prompt_name`: The name of the OpenSIPS CLI prompt (Default: `opensips-cli`) 119 | * `prompt_intro`: Introduction message when entering the OpenSIPS CLI 120 | * `prompt_emptyline_repeat_cmd`: Repeat the last command on an emptyline (Default: `False`) 121 | * `history_file`: The path of the history file (Default: `~/.opensips-cli.history`) 122 | * `history_file_size`: The backlog size of the history file (Default: `1000`) 123 | * `log_level`: The level of the console logging (Default: `WARNING`) 124 | * `communication_type`: Communication transport used by OpenSIPS CLI (Default: `fifo`) 125 | * `fifo_file`: The OpenSIPS FIFO file to which the CLI will write commands 126 | (Default: `/var/run/opensips/opensips_fifo`) 127 | * `fifo_file_fallback`: A fallback FIFO file that is being used when the `fifo_file` 128 | is not found - this has been introduces for backwards compatibility when the default 129 | `fifo_file` has been changed from `/tmp/opensips_fifo` (Default: `/tmp/opensips_fifo`) 130 | * `fifo_reply_dir`: The default directory where `opensips-cli` will create the 131 | fifo used for the reply from OpenSIPS (Default: `/tmp`) 132 | * `url`: The default URL used when `http` `communication_type` is used 133 | (Default: `http://127.0.0.1:8888/mi`). 134 | * `datagram_ip`: The default IP used when `datagram` `communication_type` is used (Default: `127.0.0.1`) 135 | * `datagram_port`: The default port used when `datagram` `communication_type` is used (Default: `8080`) 136 | 137 | Each module can use each of the parameters above, but can also declare their 138 | own. You can find in each module's documentation page the parameters that they 139 | are using. 140 | 141 | Configuration parameters can be overwritten using the `-o/--option` arguments, 142 | as described in the [Usage](#tool) section. 143 | 144 | It is also possible to set a parameters dynamically, using the `set` command. 145 | This configuration is only available during the current interactive session, 146 | and also gets cleaned up when an instance is switched. 147 | 148 | ## Modules 149 | 150 | The OpenSIPS CLI tool consists of the following modules: 151 | * [Management Interface](docs/modules/mi.md) - run MI commands 152 | * [Database](docs/modules/database.md) - commands to create, modify, drop, or 153 | migrate an OpenSIPS database 154 | * [Diagnose](docs/modules/diagnose.md) - instantly diagnose OpenSIPS instances 155 | * [Instance](docs/modules/instance.md) - used to switch through different 156 | instances/configuration within the config file 157 | * [User](docs/modules/user.md) - utility used to add and remove OpenSIPS users 158 | * [Trace](docs/modules/trace.md) - trace calls information from users 159 | * [Trap](docs/modules/trap.md) - use `gdb` to take snapshots of OpenSIPS workers 160 | * [TLS](docs/modules/tls.md) - utility to generate certificates for TLS 161 | 162 | ## Communication 163 | 164 | OpenSIPS CLI can communicate with an OpenSIPS instance through MI using 165 | different transports. Supported transports at the moment are: 166 | * `FIFO` - communicate over the `mi_fifo` module 167 | * `HTTP` - use JSONRPC over HTTP through the `mi_http` module 168 | * `DATAGRAM` - communicate over UDP using the `mi_datagram` module 169 | 170 | ## Installation 171 | 172 | Please follow the details provided in the 173 | Installation section, for a complete guide 174 | on how to install `opensips-cli` as a replacement for the deprecated 175 | `opensipsctl` shell script. 176 | 177 | ## Contribute 178 | 179 | Feel free to contribute to this project with any module, or functionality you 180 | find useful by opening a pull request. 181 | 182 | ## History 183 | 184 | This project was started by **Dorin Geman** 185 | ([dorin98](https://github.com/dorin98)) as part of the [ROSEdu 186 | 2018](http://soc.rosedu.org/2018/) program. It has later been adapted to the 187 | new OpenSIPS 3.0 MI interface and became the main external tool for managing 188 | OpenSIPS. 189 | 190 | ## License 191 | 192 | 193 | [License-GPLv3]: https://www.gnu.org/licenses/gpl-3.0.en.html "GNU GPLv3" 194 | [Logo-CC_BY]: https://i.creativecommons.org/l/by/4.0/88x31.png "Creative Common Logo" 195 | [License-CC_BY]: https://creativecommons.org/licenses/by/4.0/legalcode "Creative Common License" 196 | 197 | The `opensips-cli` source code is licensed under the [GNU General Public License v3.0][License-GPLv3] 198 | 199 | All documentation files (i.e. `.md` extension) are licensed under the [Creative Common License 4.0][License-CC_BY] 200 | 201 | ![Creative Common Logo][Logo-CC_BY] 202 | 203 | © 2018 - 2020 OpenSIPS Solutions 204 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | All available versions are eligible for security updates 6 | 7 | ## Reporting a Vulnerability 8 | 9 | For any security/vulnerability issues you may discover, please send us a full report at security@opensips.org. 10 | -------------------------------------------------------------------------------- /bin/opensips-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from opensipscli import main 4 | 5 | def run_console(): 6 | main.main() 7 | 8 | if __name__ == '__main__': 9 | run_console() 10 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster 2 | LABEL maintainer="Razvan Crainea " 3 | 4 | USER root 5 | 6 | # Set Environment Variables 7 | ENV DEBIAN_FRONTEND noninteractive 8 | 9 | #install basic components 10 | RUN apt-get -y update -qq && \ 11 | apt-get -y install git default-libmysqlclient-dev gcc 12 | 13 | #add keyserver, repository 14 | RUN git clone https://github.com/OpenSIPS/opensips-cli.git /usr/src/opensips-cli && \ 15 | cd /usr/src/opensips-cli && \ 16 | python3 setup.py install clean --all && \ 17 | cd / && rm -rf /usr/src/opensips-cli 18 | 19 | RUN apt-get purge -y git gcc && \ 20 | apt-get autoremove -y && \ 21 | apt-get clean 22 | 23 | ADD "run.sh" "/run.sh" 24 | 25 | ENV PYTHONPATH /usr/lib/python3/dist-packages 26 | 27 | ENTRYPOINT ["/run.sh", "-o", "communication_type=http"] 28 | -------------------------------------------------------------------------------- /docker/Makefile: -------------------------------------------------------------------------------- 1 | NAME ?= opensips-cli 2 | OPENSIPS_DOCKER_TAG ?= latest 3 | 4 | all: build start 5 | 6 | .PHONY: build start 7 | build: 8 | docker build \ 9 | --tag="opensips/opensips-cli:$(OPENSIPS_DOCKER_TAG)" \ 10 | . 11 | 12 | start: 13 | docker run -d --name $(NAME) opensips/opensips-cli:$(OPENSIPS_DOCKER_TAG) 14 | -------------------------------------------------------------------------------- /docker/docker.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI Docker Image 2 | 3 | Docker recipe for running [OpenSIPS Command Line 4 | Interface](https://github.com/OpenSIPS/opensips-cli). 5 | 6 | ## Building the image 7 | You can build the docker image by running: 8 | ``` 9 | make build 10 | ``` 11 | 12 | This command will build a docker image with OpenSIPS CLI master version taken from 13 | the git repository 14 | 15 | ## Parameters 16 | 17 | The container receives parameters in the following format: 18 | ``` 19 | [-o KEY=VALUE]* CMD [PARAMS]* 20 | ``` 21 | 22 | Meaning of the parameters is as it follows: 23 | 24 | * `-o KEY=VALUE` - used to tune `opensips-cli` at runtime; these parameters 25 | will end up in opensips-cli config file, in the `default` section, as 26 | `KEY: VALUE` lines 27 | * `CMD` - the command used to run; if the `CMD` ends with `.sh` extension, it 28 | will be run as a bash script, if the `CMD` ends with `.py` extension, it is 29 | run as a python script, otherwise it is run as a `opensips-cli` command 30 | * `PARAMS` - optional additional parameters passed to `CMD` 31 | 32 | ## Run 33 | 34 | To run a bash script, simply pass the connector followed by the bash script: 35 | ``` 36 | docker run -d --name opensips-cli opensips/opensips-cli:latest \ 37 | -o url=http://8.8.8.8:8888/mi script.sh 38 | ``` 39 | 40 | Similarly, run a python script: 41 | ``` 42 | docker run -d --name opensips-cli opensips/opensips-cli:latest \ 43 | -o url=http://8.8.8.8:8888/mi script.py 44 | ``` 45 | 46 | To run a single MI command, use: 47 | ``` 48 | docker run -d --name opensips-cli opensips/opensips-cli:latest \ 49 | -o url=http://8.8.8.8:8888/mi -x mi ps 50 | ``` 51 | 52 | ## DockerHub 53 | 54 | Docker images are available on 55 | [DockerHub](https://hub.docker.com/r/opensips/opensips-cli). 56 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OPTS= 4 | CMD= 5 | PARAMS= 6 | CFG=/etc/opensips-cli.cfg 7 | 8 | echo "[default]" > "$CFG" 9 | 10 | while [[ $# -gt 0 ]]; do 11 | case "$1" in 12 | -o|--option) 13 | shift 14 | P=$(cut -d'=' -f1 <<<"$1") 15 | V=$(cut -d'=' -f2- <<<"$1") 16 | echo "$P: $V" >> "$CFG" 17 | ;; 18 | *) 19 | if [ -z "$CMD" ]; then 20 | CMD="$1" 21 | else 22 | PARAMS="${PARAMS} ${1}" 23 | fi 24 | ;; 25 | esac 26 | shift 27 | done 28 | 29 | if [[ $CMD == *.py ]]; then 30 | TOOL=python3 31 | elif [[ $CMD == *.sh ]]; then 32 | TOOL=bash 33 | else 34 | TOOL=opensips-cli 35 | fi 36 | 37 | exec $TOOL $CMD $PARAMS 38 | -------------------------------------------------------------------------------- /docs/INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | `opensips-cli` is the intended convenient command-line interface for 4 | administrating `opensips 3.x` installations. 5 | 6 | Since `version 3.x`, the former script tool `opensipsctl` has been removed. 7 | Its successor, `opensips-cli`, offers many advantages and will improve handling 8 | for regular tasks in a pleasant console environment. 9 | 10 | ## From Packages 11 | 12 | ### Debian packages (.deb) 13 | 14 | Add the ["cli-releases" repository from apt.opensips.org](https://apt.opensips.org/packages.php?v=cli) 15 | to your system using the instructions provided, then install the 16 | `opensips-cli` package. 17 | 18 | Supported Operating Systems (at the time of writing): 19 | 20 | * Debian 8-10 21 | * Ubuntu 14.04, 16.04, 18.04, 19.04 22 | 23 | ### RPM packages (.rpm) 24 | 25 | The ["opensips-yum-releases" meta-package from yum.opensips.org](https://yum.opensips.org/) 26 | will install a repository that includes both `opensips` and `opensips-cli` 27 | packages. Once installed, install the `opensips-cli` package. 28 | 29 | Supported Operating Systems (at the time of writing): 30 | 31 | * RHEL 6-8, CentOS 6-8, Scientific Linux 6-8, Oracle Linux 6-8 32 | * Fedora 27-31 33 | 34 | ### Arch Linux AUR 35 | 36 | The distribution is managed as a rolling release. Packages are administered 37 | via the `pacman` front-end. Please install the `opensips-cli` package from the 38 | `AUR` using your favorite client: 39 | 40 | ``` 41 | # nightly build (latest `master` branch) 42 | yay opensips-cli-git 43 | 44 | # latest release branch 45 | yay opensips-cli 46 | ``` 47 | 48 | ## From Source Code 49 | 50 | ### Requirements 51 | 52 | Before building the CLI, you need to install some dependencies. The process 53 | will vary on every supported operating system. 54 | 55 | #### Debian / Ubuntu 56 | 57 | ``` 58 | # required OS packages 59 | sudo apt install python3 python3-pip python3-dev gcc default-libmysqlclient-dev \ 60 | python3-mysqldb python3-sqlalchemy python3-sqlalchemy-utils \ 61 | python3-openssl 62 | 63 | # alternatively, you can build the requirements from source 64 | sudo pip3 install mysqlclient sqlalchemy sqlalchemy-utils pyOpenSSL 65 | ``` 66 | 67 | #### Red Hat / CentOS 68 | 69 | ``` 70 | # required CentOS 7 packages 71 | sudo yum install python36 python36-pip python36-devel gcc mysql-devel \ 72 | python36-mysql python36-sqlalchemy python36-pyOpenSSL 73 | 74 | # required CentOS 8 packages 75 | sudo yum install python3 python3-pip python3-devel gcc mysql-devel \ 76 | python3-mysqlclient python3-sqlalchemy python3-pyOpenSSL 77 | 78 | # alternatively, you can build the requirements from source 79 | sudo pip3 install mysqlclient sqlalchemy sqlalchemy-utils pyOpenSSL 80 | ``` 81 | 82 | ### Download, Build & Install 83 | 84 | We can now download and install the latest development state from the GitHub 85 | repository: 86 | 87 | ``` 88 | git clone https://github.com/opensips/opensips-cli ~/src/opensips-cli 89 | cd ~/src/opensips-cli 90 | 91 | # local install (only visible to your user) 92 | python3 setup.py install --user clean 93 | 94 | # system-wide install 95 | sudo python3 setup.py install clean 96 | ``` 97 | 98 | ### Cleaning up the install 99 | 100 | To clean up the manually built and installed `opensips-cli` binary and package 101 | files, run a command similar to: 102 | 103 | ``` 104 | sudo rm -fr /usr/local/bin/opensips-cli /usr/local/lib/python3.6/dist-packages/opensipscli* 105 | ``` 106 | 107 | ## Database Installation 108 | 109 | Follow the [Database](modules/database.md#Examples) module documentation for a 110 | complete guide. 111 | -------------------------------------------------------------------------------- /docs/modules/database.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - Database module 2 | 3 | Module used to manipulate the database used by OpenSIPS. 4 | 5 | ## Commands 6 | 7 | This module exports the following commands: 8 | * `create` - creates a new database. Can receive an optional parameter, 9 | specifying the name of the database to be created. This command also deploys 10 | the standard OpenSIPS table, as well as other standard tables 11 | * `drop` - drops a database. Can receive an optional parameter, specifying 12 | which database to delete. 13 | * `add` - adds a new module's tables in an existing database. Receives as 14 | parameter the name of the module, as specified in the OpenSIPS scripts 15 | hierarchy. 16 | * `migrate` - copy and convert an OpenSIPS database into its next OpenSIPS 17 | release equivalent 18 | 19 | ## Configuration 20 | 21 | ### Database Schema Files 22 | 23 | The database schema files for each supported SQL backend can be installed via 24 | their corresponding OpenSIPS client module package. For example (only install modules useful to you): 25 | 26 | ``` 27 | apt install opensips-mysql-module opensips-postgres-module opensips-sqlite-module opensips-berkeley-module 28 | yum install opensips-mysql-module opensips-postgres-module opensips-sqlite-module opensips-berkeley-module 29 | ``` 30 | 31 | Once installed, the schema files will be auto-detected by `opensips-cli`. 32 | 33 | ### Setting up the `database` module 34 | 35 | The following parameters are allowed in the config file: 36 | 37 | * `database_schema_path` (optional) - absolute path to the OpenSIPS DB schema directory, 38 | usually `/usr/share/opensips` if installed from packages or `/path/to/opensips/scripts` if you 39 | are using the OpenSIPS source tree. Default: `/usr/share/opensips` 40 | * `database_admin_url` (optional) - a connection string to the database with privileged 41 | (administrator) access level which will be used to create/drop databases, as 42 | well as to create or ensure access for the non-privileged DB access user 43 | provided via `database_url`. The URL combines schema, username, password, host 44 | and port. Default: `mysql://root@localhost`. 45 | * `database_url` (optional) - the connection string to the database. A good practice 46 | would be to use a non-administrator access user for this URL. Default: 47 | `mysql://opensips:opensipsrw@localhost`. 48 | * `database_name` (optional) - the name of the database. Modules may be separately added 49 | to this database if you choose not to install all of them. Default: `opensips`. 50 | * `database_modules` (optional) - accepts the `ALL` keyword that indicates all the 51 | available modules should be installed, or a space-separated list of modules 52 | names. If processed with the `create` command, the corresponding tables will 53 | be deployed. Default: `acc alias_db auth_db avpops clusterer dialog 54 | dialplan dispatcher domain drouting group load_balancer msilo permissions 55 | rtpproxy rtpengine speeddial tls_mgm usrloc`. 56 | * `database_force_drop` (optional) - indicates whether the `drop` command will drop the 57 | database without user interaction. Default: `false` 58 | 59 | ## Usage Examples 60 | 61 | ### Database Management 62 | 63 | Consider the following configuration file: 64 | 65 | ``` 66 | [default] 67 | #database_modules: acc clusterer dialog dialplan dispatcher domain rtpproxy usrloc 68 | database_modules: ALL 69 | 70 | #database_admin_url: postgresql://root@localhost 71 | database_admin_url: mysql://root@localhost 72 | ``` 73 | 74 | The following command will create the `opensips` database and all possible 75 | tables within the MySQL instance. Additionally, the `opensips:opensipsrw` user 76 | will be created will `ALL PRIVILEGES` for the `opensips` database. For some 77 | backends, such as PostgreSQL, any additionally required permissions will be 78 | transparently granted to the `opensips` user, for example: table-level or 79 | sequence-level permissions. 80 | 81 | ``` 82 | opensips-cli -x database create 83 | Password for admin DB user (root): _ 84 | ``` 85 | 86 | If we want to add a new module, say `rtpproxy`, we have to run: 87 | 88 | ``` 89 | opensips-cli -x database add rtpproxy 90 | ``` 91 | The command above will create the `rtpproxy_sockets` table. 92 | 93 | A drop command will prompt the user whether they really want to drop the 94 | database or not: 95 | 96 | ``` 97 | $ opensips-cli -x database drop 98 | Do you really want to drop the 'opensips' database [Y/n] (Default is n): n 99 | ``` 100 | 101 | But setting the `database_force_drop` parameter will drop it without asking: 102 | ``` 103 | opensips-cli -o database_force_drop=true -x database drop 104 | ``` 105 | 106 | ### Database Migration (MySQL only) 107 | 108 | The `database migrate` command can be used to _incrementally_ upgrade 109 | your OpenSIPS database. 110 | 111 | #### Migrating from 2.4 to 3.0 112 | 113 | ``` 114 | # fetch the 3.0 OpenSIPS repo & migration scripts 115 | git clone https://github.com/OpenSIPS/opensips -b 3.0 ~/src/opensips-3.0 116 | 117 | # provide the custom path to the migration scripts and perform the migration 118 | opensips-cli -o database_schema_path=~/src/opensips-3.0/scripts \ 119 | -x database migrate 2.4_to_3.0 opensips_2_4 opensips_mig_3_0 120 | ``` 121 | 122 | #### Migrating from 3.0 to 3.1 123 | 124 | ``` 125 | # fetch the 3.1 OpenSIPS repo & migration scripts 126 | git clone https://github.com/OpenSIPS/opensips -b 3.1 ~/src/opensips-3.1 127 | 128 | # provide the custom path to the migration scripts and perform the migration 129 | opensips-cli -o database_schema_path=~/src/opensips-3.1/scripts \ 130 | -x database migrate 3.0_to_3.1 opensips_3_0 opensips_mig_3_1 131 | ``` 132 | 133 | ## Dependencies 134 | 135 | * [sqlalchemy and sqlalchemy_utils](https://www.sqlalchemy.org/) - used to 136 | abstract the SQL database regardless of the backend used 137 | 138 | ## Limitations 139 | 140 | This module can only manipulate database backends that are supported by the 141 | [SQLAlchemy](https://www.sqlalchemy.org/) project, such as SQLite, 142 | Postgresql, MySQL, Oracle, MS-SQL. 143 | -------------------------------------------------------------------------------- /docs/modules/diagnose.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - Diagnose module 2 | 3 | This module can be used in order to troubleshoot a given OpenSIPS instance. By 4 | using the MI interface and acting as an OpenSIPS event consumer via JSON-RPC, 5 | it is able to offer, in a time-critical manner, valuable information regarding 6 | commonly occurring emergencies in production, such as: 7 | 8 | * excessive I/O operations (DNS, SQL, NoSQL) which are hogging OpenSIPS 9 | * intensive CPU workloads (too much traffic / flood attacks) 10 | * poorly sized shared or private memory pools 11 | * slow DNS, SQL and NoSQL queries -- the tool answers the following: 12 | * which exact queries are being slow? 13 | * which are the the slowest queries? 14 | * which are the consistently slow queries? 15 | 16 | ## Configuration 17 | 18 | If OpenSIPS CLI is running not on the same host with OpenSIPS, it can accept 19 | the following parameters in the config file: 20 | * diagnose_listen_ip - ip address for listening JSON-RPC events from OpenSIPS 21 | By default ip is `127.0.0.1` 22 | * diagnose_listen_port - port for listening JSON-RPC events from OpenSIPS 23 | By default port is `8899` 24 | 25 | Subcommand `diagnose load` works best if the `psutil` Python package is present 26 | on the system. 27 | 28 | ## Examples 29 | 30 | Quickly glance at a summarized status of an OpenSIPS instance: 31 | ``` 32 | opensips-cli -x diagnose 33 | OpenSIPS Overview 34 | ----------------- 35 | Worker Capacity: OK 36 | Shared Memory: CRITICAL (run 'diagnose memory' for more info) 37 | Private Memory: OK 38 | SIP Processing: CRITICAL (run 'diagnose sip' for more info) 39 | DNS Queries: CRITICAL (run 'diagnose dns' for more info) 40 | SQL queries: CRITICAL (run 'diagnose sql' for more info) 41 | NoSQL Queries: OK 42 | 43 | (press Ctrl-c to exit) 44 | ``` 45 | 46 | ... ouch! This OpenSIPS box has some issues, let's take them one by one. 47 | First, let's take a look at the OpenSIPS UDP listeners, since these handle 48 | the majority of our traffic: 49 | 50 | ``` 51 | opensips-cli -x diagnose load udp 52 | OpenSIPS Processing Status 53 | 54 | SIP UDP Interface #1 (udp:127.0.0.1:5060) 55 | Receive Queue: 0.0 bytes 56 | Avg. CPU usage: 0% (last 1 sec) 57 | 58 | Process 6 load: 0%, 0%, 0% (SIP receiver udp:127.0.0.1:5060) 59 | Process 7 load: 0%, 0%, 0% (SIP receiver udp:127.0.0.1:5060) 60 | Process 8 load: 0%, 0%, 0% (SIP receiver udp:127.0.0.1:5060) 61 | Process 9 load: 0%, 0%, 0% (SIP receiver udp:127.0.0.1:5060) 62 | Process 10 load: 0%, 0%, 0% (SIP receiver udp:127.0.0.1:5060) 63 | 64 | OK: no issues detected. 65 | ---------------------------------------------------------------------- 66 | SIP UDP Interface #2 (udp:10.0.0.10:5060) 67 | Receive Queue: 0.0 bytes 68 | Avg. CPU usage: 0% (last 1 sec) 69 | 70 | Process 11 load: 0%, 0%, 0% (SIP receiver udp:10.0.0.10:5060) 71 | Process 12 load: 0%, 0%, 0% (SIP receiver udp:10.0.0.10:5060) 72 | Process 13 load: 0%, 0%, 0% (SIP receiver udp:10.0.0.10:5060) 73 | 74 | OK: no issues detected. 75 | ---------------------------------------------------------------------- 76 | 77 | Info: the load percentages represent the amount of time spent by an 78 | OpenSIPS worker processing SIP messages, as opposed to waiting 79 | for new ones. The three numbers represent the 'busy' percentage 80 | over the last 1 sec, last 1 min and last 10 min, respectively. 81 | 82 | (press Ctrl-c to exit) 83 | ``` 84 | 85 | The UDP listeners look fine, no real issues there. Let's see what we can do 86 | about the memory warning: 87 | 88 | ``` 89 | opensips-cli -x diagnose memory 90 | Shared Memory Status 91 | -------------------- 92 | Current Usage: 27.5MB / 64.0MB (43%) 93 | Peak Usage: 64.0MB / 64.0MB (99%) 94 | 95 | CRITICAL: Peak shared memory usage > 90%, increase 96 | the "-m" command line parameter as soon as possible!! 97 | 98 | Private Memory Status 99 | --------------------- 100 | Each process has 16.0MB of private (packaged) memory. 101 | 102 | Process 1: no pkg memory stats found (MI FIFO) 103 | Process 2: no pkg memory stats found (HTTPD INADDR_ANY:8081) 104 | Process 3: no pkg memory stats found (JSON-RPC sender) 105 | Process 4: no pkg memory stats found (time_keeper) 106 | Process 5: no pkg memory stats found (timer) 107 | Process 6: 4% usage, 4% peak usage (SIP receiver udp:127.0.0.1:5060) 108 | Process 7: 4% usage, 4% peak usage (SIP receiver udp:127.0.0.1:5060) 109 | Process 8: 4% usage, 4% peak usage (SIP receiver udp:127.0.0.1:5060) 110 | Process 9: 4% usage, 4% peak usage (SIP receiver udp:127.0.0.1:5060) 111 | Process 10: 4% usage, 4% peak usage (SIP receiver udp:127.0.0.1:5060) 112 | Process 11: 4% usage, 4% peak usage (SIP receiver udp:10.0.0.10:5060) 113 | Process 12: 4% usage, 4% peak usage (SIP receiver udp:10.0.0.10:5060) 114 | Process 13: 4% usage, 4% peak usage (SIP receiver udp:10.0.0.10:5060) 115 | Process 14: 4% usage, 4% peak usage (SIP receiver hep_udp:10.0.0.10:9999) 116 | Process 15: 4% usage, 4% peak usage (SIP receiver hep_udp:10.0.0.10:9999) 117 | Process 16: 4% usage, 4% peak usage (SIP receiver hep_udp:10.0.0.10:9999) 118 | Process 17: 4% usage, 4% peak usage (TCP receiver) 119 | Process 18: 4% usage, 4% peak usage (Timer handler) 120 | Process 19: 4% usage, 4% peak usage (TCP main) 121 | 122 | OK: no issues detected. 123 | 124 | (press Ctrl-c to exit) 125 | ``` 126 | 127 | It seems the shared memory pool is too low, potentially causing problems during 128 | peak traffic hours. We will bump it to 256 MB on the next restart. Next, the 129 | SIP traffic: 130 | 131 | ``` 132 | opensips-cli -x diagnose sip 133 | In the last 2 seconds... 134 | SIP Processing [WARNING] 135 | * Slowest SIP messages: 136 | INVITE sip:sipp@localhost:5060, Call-ID: 59-26705@localhost (2191 us) 137 | INVITE sip:sipp@localhost:5060, Call-ID: 58-26705@localhost (2029 us) 138 | BYE sip:localhost:7050, Call-ID: 48-26705@localhost (1300 us) 139 | * 14 / 14 SIP messages (100%) exceeded threshold 140 | 141 | (press Ctrl-c to exit) 142 | ``` 143 | 144 | SIP message processing is a bit "slow" (below 1ms processing time). Maybe 145 | the current 1ms processing threshold is a bit excessive, we will bump it to 146 | 500 ms on this next restart. Moving on to DNS queries: 147 | 148 | ``` 149 | opensips-cli -x diagnose dns 150 | In the last 16 seconds... 151 | DNS Queries [WARNING] 152 | * Slowest queries: 153 | sipdomain.invalid (669 us) 154 | sipdomain.invalid (555 us) 155 | _sip._udp.sipdomain.invalid (541 us) 156 | * Constantly slow queries 157 | localhost (32 times exceeded threshold) 158 | sipdomain.invalid (2 times exceeded threshold) 159 | _sip._udp.sipdomain.invalid (1 times exceeded threshold) 160 | * 35 / 35 queries (100%) exceeded threshold 161 | 162 | (press Ctrl-c to exit) 163 | ``` 164 | 165 | We now know which are the slowest queries, and which are the ones failing 166 | most often, so we can take action. A similar output is provided for both 167 | SQL and NoSQL queries: 168 | 169 | ``` 170 | opensips-cli -x diagnose sql 171 | opensips-cli -x diagnose nosql 172 | ``` 173 | 174 | We apply the changes, restart OpenSIPS, and all errors are cleaned up! 175 | Thank you, doctor! 176 | 177 | ``` 178 | opensips-cli -x diagnose 179 | OpenSIPS Overview 180 | ----------------- 181 | Worker Capacity: OK 182 | Shared Memory: OK 183 | Private Memory: OK 184 | SIP Processing: OK 185 | DNS Queries: OK 186 | SQL queries: OK 187 | NoSQL Queries: OK 188 | 189 | (press Ctrl-c to exit) 190 | ``` 191 | -------------------------------------------------------------------------------- /docs/modules/instance.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - Instance module 2 | 3 | This module can be used to list and switch different configuration sets 4 | provisioned in the config file. 5 | 6 | ## Commands 7 | 8 | This module exports the following commands: 9 | * `show` - shows the current instance's name 10 | * `list` - lists the instances available in the loaded configuration file 11 | * `switch` - switches to a new instance 12 | 13 | ## Examples 14 | 15 | Consider the following configuration file, which sets different prompts for 16 | different instances: 17 | 18 | ``` 19 | [instance1] 20 | prompt_name: instance-1 21 | 22 | [instance2] 23 | prompt_name: instance-2 24 | ``` 25 | 26 | Starting the OpenSIPS CLI without any parameter will start in the `default` 27 | instance, but we can navigate afterwards through each provisioned instance: 28 | 29 | ``` 30 | $ opensips-cli -f instances.cfg 31 | Welcome to OpenSIPS Command Line Interface! 32 | (opensips-cli): 33 | (opensips-cli): instance list 34 | default 35 | instance1 36 | instance2 37 | (opensips-cli): instance switch instance1 38 | (instance-1): instance switch instance2 39 | (instance-2): instance switch default 40 | (opensips-cli): 41 | ``` 42 | 43 | One can also start OpenSIPS CLI with an instance parameter: 44 | 45 | ``` 46 | $ opensips-cli -f instances.cfg -i instance1 47 | Welcome to OpenSIPS Command Line Interface! 48 | (instance-1): 49 | ``` 50 | 51 | ## Remarks 52 | 53 | * The `default` instance is always available, even if not provisioned in the 54 | configuration file. This is because the default config file is always loaded. 55 | -------------------------------------------------------------------------------- /docs/modules/mi.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - Management Interface module 2 | 3 | This module can be used by the `opensips-cli` tool to execute JSON-RPC 4 | commands and display the result in the console. 5 | 6 | ## Commands 7 | 8 | This module exports all the commands that are exported by the OpenSIPS 9 | instance that `opensips-cli` points to. It fetches the available commands by 10 | running the OpenSIPS MI `which` command. When running a command, it returns 11 | the raw, unprocessed data from OpenSIPS. 12 | 13 | Commands using this module can be run in two manners: 14 | * using *positional* parameters: this is similar to the old way the 15 | `opensipsctl` tool was working: the parameters passed to the function are 16 | specified in the same order to the MI interface, in a JSON-RPC array 17 | * using *named* parameters: parameters should be specified using their name 18 | **Note**: due to the new OpenSIPS MI interface, some functions (such as 19 | `sip_trace`, `dlg_list`) can no longer be used using positional parameters, 20 | and they have to be specified using named parameters. 21 | 22 | ## Configuration 23 | 24 | This module can accept the following parameters in the config file: 25 | * `output_type`: indicates the format of the output printed. Possible values 26 | are: 27 | * `pretty-print` - (default) prints the output in a pretty-prited JSON format 28 | * `dictionary` - prints the output as a JSON dictionary 29 | * `lines` - prints the output on indented lines 30 | * `yaml` - prints the output in a YAML format 31 | * `none` - does not print anything 32 | 33 | ## Modifiers 34 | 35 | The `mi` module can receive a set of modifiers for its commands that influence 36 | the communication with OpenSIPS. Available modifiers are: 37 | * `-j`: the modifier instructs the module to avoid converting the parameters 38 | as strings and treat them as JSON values, if possible. 39 | 40 | ## Examples 41 | 42 | Fetch the OpenSIPS `uptime` in a YAML format: 43 | ``` 44 | opensips-cli -o output_type=yaml -x mi uptime 45 | Now: Wed Feb 20 13:37:25 2019 46 | Up since: Tue Feb 19 14:48:41 2019 47 | ``` 48 | 49 | Display the load and networking statistics one on each line: 50 | **Note**: the `get_statistics` command receives the statistics as an array 51 | parameter 52 | ``` 53 | opensips-cli -o output_type=lines -x mi get_statistics load net: 54 | load:load: 0 55 | net:waiting_udp: 0 56 | net:waiting_tcp: 0 57 | net:waiting_tls: 0 58 | ``` 59 | 60 | The command ran is similar to the following one, but parameters are specified 61 | using their names: 62 | ``` 63 | opensips-cli -o output_type=lines -x mi get_statistics statistics='load net:' 64 | load:load: 0 65 | net:waiting_udp: 0 66 | net:waiting_tcp: 0 67 | net:waiting_tls: 0 68 | ``` 69 | 70 | Use the `-j` modifier for specifying array params as well as json: 71 | ``` 72 | opensips-cli -x -- mi -j raise_event E_TEST '["127.0.0.1", 5060]' 73 | opensips-cli -x -- mi -j raise_event event=E_TEST params='{"ip":"127.0.0.1", "port":5060}}' 74 | ``` 75 | 76 | ## Limitations 77 | 78 | Some commands in OpenSIPS (such as `get_statistics`, or `dlg_push_var`) 79 | require array parameters. Since the current OpenSIPS MI interface does not 80 | allow us to query which parameter is an array, this is currently statically 81 | provisioned in the [mi](opensipscli/modules/mi.py) module, the 82 | `MI_ARRAY_PARAMS_COMMANDS` parameter. **Note:** if a new command that requires 83 | array arguments is defined in OpenSIPS, this array has to be updated!. 84 | -------------------------------------------------------------------------------- /docs/modules/tls.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - TLS module 2 | 3 | Using the `tls` module, you can generate TLS certificates and private keys. 4 | 5 | The module has two subcommands: 6 | * `rootCA` - generates a CA (certification authority) self signed certificate 7 | and private key pair. These are to be used by a TLS server. 8 | * `userCERT` - generates a certificate signed by a given CA, a private key and 9 | a CA list (chain of trust) file. These are to be used by TLS clients (users). 10 | 11 | ## Configuration 12 | 13 | Certificates and private keys can be customized using the following settings: 14 | 15 | List of `opensips-cli.cfg` settings for configuring self-signed CA certificates: 16 | 17 | * tls_ca_dir - output directory where the cert and key will be written to 18 | * tls_ca_cert_file - output certificate file path, within `tls_ca_dir` 19 | * tls_ca_key_file - output private key file path, within `tls_ca_dir` 20 | * tls_ca_overwrite - set this to "y" in order to overwrite existing files 21 | * tls_ca_common_name - the address of the website (e.g. "opensips.org") 22 | * tls_ca_country - the initials of the country (e.g. "RO") 23 | * tls_ca_state - the state (e.g. "Bucharest") 24 | * tls_ca_locality - the city (e.g. "Bucharest") 25 | * tls_ca_organisation - the name of the organisation (e.g. "OpenSIPS") 26 | * tls_ca_organisational_unit - the organisational unit (e.g. "Project") 27 | * tls_ca_notafter - the validity period, in seconds (e.g. 315360000) 28 | * tls_ca_key_size - the size of the RSA key, in bits (e.g. 4096) 29 | * tls_ca_md - the digest algorithm to use for signing (e.g. SHA256) 30 | 31 | List of `opensips-cli.cfg` settings for configuring user certificates: 32 | 33 | * tls_user_dir - output directory where the cert and key will be written to 34 | * tls_user_cert_file - output certificate file path, within `tls_user_dir` 35 | * tls_user_key_file - output private key file path, within `tls_user_dir` 36 | * tls_user_calist_file - output CA list file path, within `tls_user_dir` 37 | * tls_user_overwrite - set this to "y" in order to overwrite existing files 38 | * tls_user_cacert - path to the input CA certificate 39 | * tls_user_cakey - path to the input CA private key 40 | * tls_user_common_name - the address of the website (e.g. "www.opensips.org") 41 | * tls_user_country - the initials of the country (e.g. "RO") 42 | * tls_user_state - the state (e.g. "Bucharest") 43 | * tls_user_locality - the city (e.g. "Bucharest") 44 | * tls_user_organisation - the name of the organisation (e.g. "OpenSIPS") 45 | * tls_user_organisational_unit - the organisational unit (e.g. "Project") 46 | * tls_user_notafter - the validity period, in seconds (e.g. 315360000) 47 | * tls_user_key_size - the size of the RSA key, in bits (e.g. 4096) 48 | * tls_user_md - the digest algorithm to use for signing (e.g. SHA256) 49 | 50 | 51 | ## Examples 52 | 53 | To create a self-signed certificate and a private key for rootCA, enter this snippet: 54 | ``` 55 | opensips-cli -x tls rootCA 56 | ``` 57 | Configuration file example for rootCA: 58 | ``` 59 | [default] 60 | tls_ca_dir: /etc/opensips/tls/rootCA 61 | tls_ca_cert_file: cacert.pem 62 | tls_ca_key_file: private/cakey.pem 63 | tls_ca_overwrite: yes 64 | tls_ca_common_name: opensips.org 65 | tls_ca_country: RO 66 | tls_ca_state: Bucharest 67 | tls_ca_locality: Bucharest 68 | tls_ca_organisation: OpenSIPS 69 | tls_ca_organisational_unit: Project 70 | tls_ca_notafter: 315360000 71 | tls_ca_key_size: 4096 72 | tls_ca_md: SHA256 73 | ``` 74 | 75 | To create a user certificate signed by the above rootCA, along with a private 76 | key and a CA list (chain of trust) file: 77 | ``` 78 | opensips-cli -x tls userCERT 79 | ``` 80 | Configuration file example for userCERT: 81 | ``` 82 | [default] 83 | tls_user_dir: /etc/opensips/tls/user 84 | tls_user_cert_file: user-cert.pem 85 | tls_user_key_file: user-privkey.pem 86 | tls_user_calist_file: user-calist.pem 87 | tls_user_overwrite: yes 88 | tls_user_cacert: /etc/opensips/tls/rootCA/cacert.pem 89 | tls_user_cakey: /etc/opensips/tls/rootCA/private/cakey.pem 90 | tls_user_common_name: www.opensips.org 91 | tls_user_country: RO 92 | tls_user_state: Bucharest 93 | tls_user_locality: Bucharest 94 | tls_user_organisation: OpenSIPS 95 | tls_user_organisational_unit: Project 96 | tls_user_notafter: 315360000 97 | tls_user_key_size: 4096 98 | tls_user_md: SHA256 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/modules/trace.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - Trace module 2 | 3 | This module can be used by the `opensips-cli` tool to trace information about 4 | calls passing through SIP. This module can offer information about SIP traffic 5 | of a call, as well as other information such as script logs (logged using 6 | `xlog`). 7 | 8 | ## Commands 9 | 10 | This module does not export any command, but can receive a set of filters that 11 | are used to filter the calls or traffic received from OpenSIPS. Available 12 | filters for the current version are: 13 | * `caller`: the identity of the caller, specified as `user` or `user@domain`; 14 | this field is compared with the identity in the From header 15 | * `callee`: the identity of the callee, specified as `user` or `user@domain`; 16 | this field is compared with the identity in the Request URI 17 | * `ip`: the IP where the call is initiated from 18 | 19 | If there is no filter specified, when running the `trace` module, you will be 20 | interactive prompted about what filter you want to apply. 21 | 22 | **Note**: if you are not specifying any filters, you will receive the entire 23 | traffic OpenSIPS is handling! Depending on your setup and traffic, this 24 | connection might be overloaded. 25 | 26 | ## Examples 27 | 28 | Trace the calls from *alice*: 29 | ``` 30 | opensips-cli -x trace caller=alice 31 | ``` 32 | 33 | Trace the calls from *alice* to *bob*: 34 | ``` 35 | opensips-cli -x trace caller=alice callee=bob 36 | ``` 37 | 38 | Trace the calls originated from IP 10.0.0.1: 39 | ``` 40 | opensips-cli -x trace ip=10.0.0.1 41 | ``` 42 | 43 | Call the `trace` module interactively without a filter: 44 | ``` 45 | (opensips-cli): trace 46 | Caller filter: 47 | Callee filter: 48 | Source IP filter: 49 | No filter specified! Continue without a filter? [Y/n] (Default is n): y 50 | ``` 51 | 52 | ## Limitations 53 | 54 | Filtering limitations are coming from the filters that OpenSIPS `trace_start` 55 | MI command supports. If one wants to define other filters, they will also need 56 | to be implemented in OpenSIPS the `tracer`module. 57 | -------------------------------------------------------------------------------- /docs/modules/trap.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - Trap module 2 | 3 | Using this module you can create a trap file of the OpenSIPS processes. 4 | When running without any parameters, the `trap` module fetches the OpenSIPS 5 | pids by issuing an `mi ps` command. However, the pids of OpenSIPS can be 6 | specified as additional parameters to the `trap` command. 7 | 8 | ## Configuration 9 | 10 | This module can have the following parameters specified through a config file: 11 | * `trap_file` - name of the file that will contain the trap (Default is 12 | `/tmp/gdb_opensips_$(date +%Y%m%d_%H%M%S)`). 13 | * `process_name` - name of OpenSIPS process (Default is `opensips`). 14 | 15 | ## Examples 16 | 17 | Trapping OpenSIPS with pids specified through MI: 18 | 19 | ``` 20 | opensips-cli -x trap 21 | ``` 22 | 23 | When OpenSIPS is stuck and cannot be trapped, because you cannot run MI 24 | commands, you can specify the OpenSIPS pids you want to trap directly in the 25 | cli: 26 | 27 | ``` 28 | opensips-cli -x trap 5113 5114 5115 5116 29 | ``` 30 | 31 | ## Remarks 32 | 33 | * This module only works when `opensips-cli` is ran on the same machine as 34 | OpenSIPS, since it needs direct access to OpenSIPS processes. 35 | * This module requires to have the `gdb` command in system's `PATH`. At 36 | startup, it checks if `gdb` can be located (using `which`), and if it cannot, 37 | the module becomes unavailable. 38 | * You need administrative priviledges to run the `trap`. 39 | -------------------------------------------------------------------------------- /docs/modules/user.md: -------------------------------------------------------------------------------- 1 | # OpenSIPS CLI - User module 2 | 3 | Module used to add/remove/update user information in OpenSIPS tables. 4 | 5 | ## Commands 6 | 7 | This module exports the following commands: 8 | * `add` - adds a new username in the database; accepts an optional user 9 | (with or without a domain) as parameter, followed by a password. If any of 10 | them are missing, you will be prompted for 11 | * `password` - changes the password of an username; accepts similar parameters 12 | as `add` 13 | * `delete` - removes an username from the database; accepts the user as 14 | parameter 15 | 16 | ## Configuration 17 | 18 | The parameters from this tool can be either provisioned in the configuration 19 | file, either prompted for during runtime, similar to the `database` module. 20 | If a parameter is specified in the configuration file, you will not be 21 | prompted for it! 22 | 23 | These are the parameters that can be specified in the config file: 24 | * `domain` - the domain of the username; this is only read/prompted for when 25 | the user to be added/deleted does not already have a domain part 26 | `scripts/` directory in the OpenSIPS source tree, or `/usr/share/opensips/` 27 | * `plain_text_passwords` - indicates whether passwords should be stored in 28 | plain-text, or just the `ha1` and `ha1b` values. Defaults to `false` 29 | 30 | ## Examples 31 | 32 | Add the `username@domain.com` user with the `S3cureP4s$` password. 33 | 34 | ``` 35 | opensips-cli -x user add username@domain.com S3cureP4s$ 36 | ``` 37 | 38 | If the domain, or password is not specified, it will be prompted: 39 | 40 | ``` 41 | (opensips-cli): user add razvan 42 | Please provide the domain of the user: domain.com 43 | Please enter new password: 44 | Please repeat the password: 45 | ``` 46 | A similar behavior is for the `delete` and `passwords` commands, where you 47 | will be prompted for the missing/necessary information. 48 | 49 | To remove an username, use the `delete` command: 50 | ``` 51 | opensips-cli -x user delete username@domain.com 52 | ``` 53 | 54 | ## Dependencies 55 | 56 | * [sqlalchemy and sqlalchemy_utils](https://www.sqlalchemy.org/) - used to 57 | abstract the database manipulation, regardless of the backend used 58 | 59 | ## Limitations 60 | 61 | This module can only manipulate database backends that are supported by the 62 | [SQLAlchemy](https://www.sqlalchemy.org/) project, such as SQLite, 63 | Postgresql, MySQL, Oracle, MS-SQL. 64 | -------------------------------------------------------------------------------- /etc/default.cfg: -------------------------------------------------------------------------------- 1 | [default] 2 | log_level: WARNING 3 | prompt_name: opensips-cli 4 | prompt_intro: Welcome to OpenSIPS Command Line Interface! 5 | prompt_emptyline_repeat_cmd: False 6 | history_file: ~/.opensips-cli.history 7 | history_file_size: 1000 8 | output_type: pretty-print 9 | communication_type: fifo 10 | fifo_file: /tmp/opensips_fifo 11 | -------------------------------------------------------------------------------- /opensipscli/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from .version import __version__ 21 | -------------------------------------------------------------------------------- /opensipscli/args.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | """ 21 | Class that instruct the default values for arguments 22 | """ 23 | 24 | from opensipscli import defaults 25 | 26 | class OpenSIPSCLIArgs: 27 | 28 | """ 29 | Class that contains the default values of CLI Arguments 30 | """ 31 | debug = False 32 | print = False 33 | execute = True 34 | command = [] 35 | config = None 36 | instance = defaults.DEFAULT_SECTION 37 | extra_options = {} 38 | 39 | __fields__ = ['debug', 40 | 'print', 41 | 'execute', 42 | 'command', 43 | 'config', 44 | 'instance', 45 | 'extra_options'] 46 | 47 | def __init__(self, **kwargs): 48 | for k in kwargs: 49 | if k in self.__fields__: 50 | self.__setattr__(k, kwargs[k]) 51 | else: 52 | self.extra_options[k] = kwargs[k] 53 | 54 | 55 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 56 | 57 | -------------------------------------------------------------------------------- /opensipscli/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | import cmd 21 | import sys 22 | import os 23 | import shlex 24 | import readline 25 | import atexit 26 | import importlib 27 | from opensipscli import args 28 | from opensipscli import comm 29 | from opensipscli import defaults 30 | from opensipscli.config import cfg 31 | from opensipscli.logger import logger 32 | from opensipscli.modules import * 33 | 34 | class OpenSIPSCLI(cmd.Cmd, object): 35 | """ 36 | OpenSIPS-Cli shell 37 | """ 38 | modules = {} 39 | excluded_errs = {} 40 | registered_atexit = False 41 | 42 | def __init__(self, options = None): 43 | """ 44 | contructor for OpenSIPS-Cli 45 | """ 46 | 47 | if not options: 48 | options = args.OpenSIPSCLIArgs() 49 | 50 | self.debug = options.debug 51 | self.print = options.print 52 | self.execute = options.execute 53 | self.command = options.command 54 | self.modules_dir_inserted = None 55 | 56 | if self.debug: 57 | logger.setLevel("DEBUG") 58 | 59 | cfg_file = None 60 | if not options.config: 61 | for f in defaults.CFG_PATHS: 62 | if os.path.isfile(f) and os.access(f, os.R_OK): 63 | # found a valid config file 64 | cfg_file = f 65 | break 66 | else: 67 | cfg_file = options.config 68 | if not cfg_file: 69 | logger.debug("no config file found in any of {}". 70 | format(", ".join(defaults.CFG_PATHS))) 71 | else: 72 | logger.debug("using config file {}".format(cfg_file)) 73 | 74 | # __init__ of the configuration file 75 | cfg.parse(cfg_file) 76 | if not cfg.has_instance(options.instance): 77 | logger.warning("Unknown instance '{}'! Using default instance '{}'!". 78 | format(options.instance, defaults.DEFAULT_SECTION)) 79 | instance = defaults.DEFAULT_SECTION 80 | else: 81 | instance = options.instance 82 | cfg.set_instance(instance) 83 | if options: 84 | cfg.set_custom_options(options.extra_options) 85 | 86 | if not self.execute: 87 | # __init__ of cmd.Cmd module 88 | cmd.Cmd.__init__(self) 89 | 90 | # Opening the current working instance 91 | self.update_instance(cfg.current_instance) 92 | 93 | if self.print: 94 | logger.info(f"Config:\n" + "\n".join([f"{k}: {v}" for k, v in cfg.to_dict().items()])) 95 | 96 | def update_logger(self): 97 | """ 98 | alter logging level 99 | """ 100 | 101 | # first of all, let's handle logging 102 | if self.debug: 103 | level = "DEBUG" 104 | else: 105 | level = cfg.get("log_level") 106 | logger.setLevel(level) 107 | 108 | def clear_instance(self): 109 | """ 110 | update history 111 | """ 112 | # make sure we dump everything before swapping files 113 | self.history_write() 114 | 115 | def update_instance(self, instance): 116 | """ 117 | constructor of an OpenSIPS-Cli instance 118 | """ 119 | 120 | # first of all, let's handle logging 121 | self.current_instance = instance 122 | self.update_logger() 123 | 124 | # Update the intro and prompt 125 | self.intro = cfg.get('prompt_intro') 126 | self.prompt = '(%s): ' % cfg.get('prompt_name') 127 | 128 | # initialize communcation handler 129 | self.handler = comm.initialize() 130 | 131 | # remove all loaded modules 132 | self.modules = {} 133 | 134 | skip_modules = [] 135 | if cfg.exists('skip_modules'): 136 | skip_modules = cfg.get('skip_modules') 137 | sys_modules = {} 138 | if not self.execute: 139 | print(self.intro) 140 | # add the built-in modules and commands list 141 | for mod in ['set', 'clear', 'help', 'history', 'exit', 'quit']: 142 | self.modules[mod] = (self, None) 143 | sys_modules = sys.modules 144 | else: 145 | try: 146 | mod = "opensipscli.modules.{}".format(self.command[0]) 147 | sys_modules = { mod: sys.modules[mod] } 148 | except: 149 | pass 150 | 151 | available_modules = { key[20:]: sys_modules[key] for key in 152 | sys_modules.keys() if 153 | key.startswith("opensipscli.modules.") and 154 | key[20:] not in skip_modules } 155 | for name, module in available_modules.items(): 156 | m = importlib.import_module("opensipscli.modules.{}".format(name)) 157 | if not hasattr(m, "Module"): 158 | logger.debug("Skipping module '{}' - does not extend Module". 159 | format(name)) 160 | continue 161 | if not hasattr(m, name): 162 | logger.debug("Skipping module '{}' - module implementation not found". 163 | format(name)) 164 | continue 165 | mod = getattr(module, name) 166 | if not hasattr(mod, '__exclude__') or not hasattr(mod, '__get_methods__'): 167 | logger.debug("Skipping module '{}' - module does not implement Module". 168 | format(name)) 169 | continue 170 | excl_mod = mod.__exclude__(mod) 171 | if excl_mod[0] is True: 172 | if excl_mod[1]: 173 | self.excluded_errs[name] = excl_mod[1] 174 | logger.debug("Skipping module '{}' - excluded on purpose".format(name)) 175 | continue 176 | logger.debug("Loaded module '{}'".format(name)) 177 | imod = mod() 178 | self.modules[name] = (imod, mod.__get_methods__(imod)) 179 | 180 | def history_write(self): 181 | """ 182 | save history file 183 | """ 184 | history_file = cfg.get('history_file') 185 | logger.debug("saving history in {}".format(history_file)) 186 | os.makedirs(os.path.expanduser(os.path.dirname(history_file)), exist_ok=True) 187 | try: 188 | readline.write_history_file(os.path.expanduser(history_file)) 189 | except PermissionError: 190 | logger.warning("failed to write CLI history to {} " + 191 | "(no permission)".format( 192 | history_file)) 193 | 194 | def preloop(self): 195 | """ 196 | preload a history file 197 | """ 198 | history_file = cfg.get('history_file') 199 | logger.debug("using history file {}".format(history_file)) 200 | try: 201 | readline.read_history_file(os.path.expanduser(history_file)) 202 | except PermissionError: 203 | logger.warning("failed to read CLI history from {} " + 204 | "(no permission)".format( 205 | history_file)) 206 | except FileNotFoundError: 207 | pass 208 | 209 | readline.set_history_length(int(cfg.get('history_file_size'))) 210 | if not self.registered_atexit: 211 | atexit.register(self.history_write) 212 | 213 | def postcmd(self, stop, line): 214 | """ 215 | post command after switching instance 216 | """ 217 | if self.current_instance != cfg.current_instance: 218 | self.clear_instance() 219 | self.update_instance(cfg.current_instance) 220 | # make sure we update all the history information 221 | self.preloop() 222 | 223 | return stop 224 | 225 | def print_topics(self, header, cmds, cmdlen, maxcol): 226 | """ 227 | print topics, omit misc commands 228 | """ 229 | if header is not None: 230 | if cmds: 231 | self.stdout.write('%s\n' % str(header)) 232 | if self.ruler: 233 | self.stdout.write('%s\n' % str(self.ruler*len(header))) 234 | self.columnize(cmds, maxcol-1) 235 | self.stdout.write('\n') 236 | 237 | def cmdloop(self, intro=None): 238 | """ 239 | command loop, catching SIGINT 240 | """ 241 | if self.execute: 242 | if len(self.command) < 1: 243 | logger.error("no modules to run specified!") 244 | return -1 245 | 246 | module, command, modifiers, params = self.parse_command(self.command) 247 | 248 | logger.debug("running in non-interactive mode {} {} {}". 249 | format(module, command, params)) 250 | try: 251 | ret = self.run_command(module, command, modifiers, params) 252 | except KeyboardInterrupt: 253 | print('^C') 254 | return -1 255 | 256 | # assume that by default it exists with success 257 | if ret is None: 258 | ret = 0 259 | return ret 260 | while True: 261 | try: 262 | super(OpenSIPSCLI, self).cmdloop(intro='') 263 | break 264 | except KeyboardInterrupt: 265 | print('^C') 266 | # any other commands exits with negative value 267 | return -1 268 | 269 | def emptyline(self): 270 | if cfg.getBool('prompt_emptyline_repeat_cmd'): 271 | super().emptyline() 272 | 273 | def complete_modules(self, text): 274 | """ 275 | complete modules selection based on given text 276 | """ 277 | l = [a for a in self.modules.keys() if a.startswith(text)] 278 | if len(l) == 1: 279 | l[0] = l[0] + " " 280 | return l 281 | 282 | def complete_functions(self, module, text, line, begidx, endidx): 283 | """ 284 | complete function selection based on given text 285 | """ 286 | 287 | # builtin commands 288 | _, command, modifiers, params = self.parse_command(line.split()) 289 | # get all the available modifiers of the module 290 | all_params = [] 291 | if not command: 292 | # haven't got to a command yet, so we might have some modifiers 293 | try: 294 | modiffunc = getattr(module[0], '__get_modifiers__') 295 | modifiers_params = modiffunc() 296 | except: 297 | pass 298 | all_params = [ x for x in modifiers_params if x not in modifiers ] 299 | # if we are introducing a modifier, auto-complete only them 300 | if begidx > 1 and line[begidx-1] == '-': 301 | stripped_params = [ p.lstrip("-") for p in modifiers_params ] 302 | l = [a for a in stripped_params if a.startswith(text)] 303 | if len(l) == 1: 304 | l[0] = l[0] + " " 305 | else: 306 | l = [a for a in l if a not in [ m.strip("-") for m in modifiers]] 307 | return l 308 | 309 | if module[1]: 310 | all_params = all_params + module[1] 311 | if len(all_params) > 0 and (not command or 312 | (len(params) == 0 and line[-1] != ' ')): 313 | l = [a for a in all_params if a.startswith(text)] 314 | if len(l) == 1: 315 | l[0] += " " 316 | else: 317 | try: 318 | compfunc = getattr(module[0], '__complete__') 319 | l = compfunc(command, text, line, begidx, endidx) 320 | if not l: 321 | return None 322 | except AttributeError: 323 | return [''] 324 | # looking for a different command 325 | return l 326 | 327 | # Overwritten function for our customized auto-complete 328 | def complete(self, text, state): 329 | """ 330 | auto-complete selection based on given text and state parameters 331 | """ 332 | if state == 0: 333 | origline = readline.get_line_buffer() 334 | line = origline.lstrip() 335 | stripped = len(origline) - len(line) 336 | begidx = readline.get_begidx() - stripped 337 | endidx = readline.get_endidx() - stripped 338 | if begidx > 0: 339 | mod, args, foo = self.parseline(line) 340 | if mod == '': 341 | return self.complete_modules(text)[state] 342 | elif not mod in self.modules: 343 | logger.error("BUG: mod '{}' not found!".format(mod)) 344 | else: 345 | module = self.modules[mod] 346 | self.completion_matches = \ 347 | self.complete_functions(module, text, line, begidx, endidx) 348 | else: 349 | self.completion_matches = self.complete_modules(text) 350 | try: 351 | return self.completion_matches[state] 352 | except IndexError: 353 | return [''] 354 | 355 | # Parse parameters 356 | def parse_command(self, line): 357 | 358 | module = line[0] 359 | if len(line) < 2: 360 | return module, None, [], [] 361 | paramIndex = 1 362 | while paramIndex < len(line): 363 | if line[paramIndex][0] != "-": 364 | break 365 | paramIndex = paramIndex + 1 366 | if paramIndex == 1: 367 | modifiers = [] 368 | command = line[1] 369 | params = line[2:] 370 | elif paramIndex == len(line): 371 | modifiers = line[1:paramIndex] 372 | command = None 373 | params = [] 374 | else: 375 | modifiers = line[1:paramIndex] 376 | command = line[paramIndex] 377 | params = line[paramIndex + 1:] 378 | 379 | return module, command, modifiers, params 380 | 381 | # Execute commands from Modules 382 | def run_command(self, module, cmd, modifiers, params): 383 | """ 384 | run a module command with given parameters 385 | """ 386 | try: 387 | mod = self.modules[module] 388 | except (AttributeError, KeyError): 389 | if module in self.excluded_errs: 390 | for err_msg in self.excluded_errs[module]: 391 | logger.error(err_msg) 392 | return -1 393 | else: 394 | logger.error("no module '{}' loaded".format(module)) 395 | return -1 396 | # if the module does not return any methods (returned None) 397 | # we simply call the module's name method 398 | if not mod[1]: 399 | if cmd and params is not None: 400 | params.insert(0, cmd) 401 | cmd = mod[0].__module__ 402 | if cmd.startswith("opensipscli.modules."): 403 | cmd = cmd[20:] 404 | elif not cmd and '' not in mod[1]: 405 | logger.error("module '{}' expects the following commands: {}". 406 | format(module, ", ".join(mod[1]))) 407 | return -1 408 | elif cmd and not cmd in mod[1]: 409 | logger.error("no command '{}' in module '{}'". 410 | format(cmd, module)) 411 | return -1 412 | logger.debug("running command '{}' '{}'".format(cmd, params)) 413 | return mod[0].__invoke__(cmd, params, modifiers) 414 | 415 | def default(self, line): 416 | try: 417 | aux = shlex.split(line) 418 | except ValueError: 419 | """ if the line ends in a backspace, just clean it""" 420 | line = line[:-1] 421 | aux = shlex.split(line) 422 | 423 | module, cmd, modifiers, params = self.parse_command(aux) 424 | self.run_command(module, cmd, modifiers, params) 425 | 426 | def do_history(self, line): 427 | """ 428 | print entries in history file 429 | """ 430 | if not line: 431 | try: 432 | with open(os.path.expanduser(cfg.get('history_file'))) as hf: 433 | for num, line in enumerate(hf, 1): 434 | print(num, line, end='') 435 | except FileNotFoundError: 436 | pass 437 | 438 | def do_set(self, line): 439 | """ 440 | handle dynamic settings (key-value pairs) 441 | """ 442 | parsed = line.split('=', 1) 443 | if len(parsed) < 2: 444 | logger.error("setting value format is 'key=value'!") 445 | return 446 | key = parsed[0] 447 | value = parsed[1] 448 | cfg.set(key, value) 449 | 450 | # Used to get info for a certain command 451 | def do_help(self, line): 452 | # TODO: Add help for commands 453 | print("Usage:: help cmd - returns information about \"cmd\"") 454 | 455 | # Clear the terminal screen 456 | def do_clear(self, line): 457 | os.system('clear') 458 | 459 | # Commands used to exit the shell 460 | def do_EOF(self, line): # It catches Ctrl+D 461 | print('^D') 462 | return True 463 | 464 | def do_quit(self, line): 465 | return True 466 | 467 | def do_exit(self, line): 468 | return True 469 | 470 | def mi(self, cmd, params = [], silent = False): 471 | """helper for running MI commands""" 472 | return comm.execute(cmd, params, silent) 473 | -------------------------------------------------------------------------------- /opensipscli/comm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from opensipscli.logger import logger 21 | from opensipscli.config import cfg 22 | from opensips.mi import OpenSIPSMI, OpenSIPSMIException 23 | 24 | comm_handler = None 25 | comm_handler_valid = None 26 | 27 | def initialize(): 28 | global comm_handler 29 | comm_type = cfg.get('communication_type') 30 | comm_handler = OpenSIPSMI(comm_type, **cfg.to_dict()) 31 | valid() 32 | 33 | def execute(cmd, params=[], silent=False): 34 | global comm_handler 35 | try: 36 | ret = comm_handler.execute(cmd, params) 37 | except OpenSIPSMIException as ex: 38 | if not silent: 39 | logger.error("command '{}' returned: {}".format(cmd, ex)) 40 | return None 41 | return ret 42 | 43 | def valid(): 44 | global comm_handler 45 | global comm_handler_valid 46 | if comm_handler_valid: 47 | return comm_handler_valid 48 | if not comm_handler: 49 | comm_handler_valid = (False, None) 50 | try: 51 | if hasattr(comm_handler, "valid"): 52 | comm_handler_valid = comm_handler.valid() 53 | else: 54 | comm_handler_valid = (True, None) 55 | except: 56 | comm_handler_valid = (False, None) 57 | return comm_handler_valid 58 | -------------------------------------------------------------------------------- /opensipscli/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | import os 21 | import configparser 22 | from opensipscli import defaults 23 | from opensipscli.logger import logger 24 | 25 | class OpenSIPSCLIConfig: 26 | 27 | current_instance = defaults.DEFAULT_SECTION 28 | 29 | def __init__(self): 30 | self.config = configparser.ConfigParser( 31 | defaults=defaults.DEFAULT_VALUES, 32 | default_section=defaults.DEFAULT_SECTION) 33 | self.dynamic_options = {} 34 | self.custom_options = {} 35 | 36 | # Read the file given as parameter in order to parse it 37 | def parse(self, in_file): 38 | if not in_file: 39 | logger.info("no config file used!") 40 | elif os.path.isfile(in_file) and os.access(in_file, os.R_OK): 41 | self.config.read(in_file) 42 | else: 43 | logger.error("Either file is missing or is not readable.") 44 | 45 | def set_option(self, option, value = None): 46 | if value: 47 | self.custom_options[option] = value 48 | else: 49 | del self.custom_options[option] 50 | 51 | def set_custom_options(self, options): 52 | if options is None: 53 | return 54 | if isinstance(options, dict): 55 | for k in options.keys(): 56 | self.set_option(k, options[k]) 57 | else: 58 | for arg in options: 59 | parsed = arg.split('=') 60 | key = parsed[0] 61 | val = '='.join(parsed[1:]) 62 | self.set_option(key, val) 63 | 64 | # Function to get the value from a section.value 65 | def get(self, key): 66 | if self.dynamic_options and key in self.dynamic_options: 67 | return self.dynamic_options[key] 68 | if self.custom_options and key in self.custom_options: 69 | return self.custom_options[key] 70 | elif self.current_instance not in self.config: 71 | return defaults.DEFAULT_VALUES[key] 72 | else: 73 | return self.config[self.current_instance][key] 74 | 75 | # Function to set a dynamic value 76 | def set(self, key, value): 77 | self.dynamic_options[key] = value 78 | logger.debug("set {}={}".format(key, value)) 79 | 80 | def mkBool(self, val): 81 | return val.lower() in ['yes', '1', 'true'] 82 | 83 | def getBool(self, key): 84 | return self.mkBool(self.get(key)) 85 | 86 | # checks if a configuration exists 87 | def exists(self, key): 88 | if self.dynamic_options and key in self.dynamic_options: 89 | return True 90 | if self.custom_options and key in self.custom_options: 91 | return True 92 | elif self.current_instance not in self.config: 93 | return key in defaults.DEFAULT_VALUES 94 | else: 95 | return key in self.config[self.current_instance] 96 | 97 | def set_instance(self, instance): 98 | self.current_instance = instance 99 | self.dynamic_options = {} 100 | 101 | def has_instance(self, instance): 102 | return instance in self.config 103 | 104 | def get_default_instance(self): 105 | return defaults.DEFAULT_SECTION 106 | 107 | # reads a param or returns a default 108 | def read_param(self, param, prompt, default=None, yes_no=False, 109 | isbool=False, allow_empty=False): 110 | if param: 111 | if type(param) != list: 112 | param = [param] 113 | for p in param: 114 | if self.exists(p): 115 | return self.mkBool(self.get(p)) if isbool else self.get(p) 116 | val = "" 117 | if yes_no: 118 | prompt = prompt + " [y/n]" 119 | if default is not None: 120 | prompt = prompt + " (default: '{}')".format("y" if default else "n") 121 | elif default is not None: 122 | prompt = prompt + " (default: '{}')".format(default) 123 | prompt = prompt + ": " 124 | while val == "": 125 | try: 126 | val = input(prompt).strip() 127 | except Exception as e: 128 | return None 129 | if val == "": 130 | if allow_empty: 131 | return "" 132 | 133 | if default is not None: 134 | return default 135 | elif yes_no: 136 | if val.lower() in ['y', 'yes']: 137 | return True 138 | elif val.lower() in ['n', 'no']: 139 | return False 140 | else: 141 | prompt = "Please choose 'y' or 'n': " 142 | else: 143 | return val 144 | 145 | def to_dict(self): 146 | temp = defaults.DEFAULT_VALUES.copy() 147 | temp.update(self.config.defaults()) 148 | 149 | if not self.config.has_section(self.current_instance): 150 | temp.update(self.custom_options) 151 | temp.update(self.dynamic_options) 152 | return temp 153 | 154 | for option in self.config.options(self.current_instance): 155 | temp[option] = self.config.get(self.current_instance, option) 156 | 157 | temp.update(self.custom_options) 158 | temp.update(self.dynamic_options) 159 | return temp 160 | 161 | 162 | cfg = OpenSIPSCLIConfig() 163 | -------------------------------------------------------------------------------- /opensipscli/defaults.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | """ 21 | Default configuration for OpenSIPS CLI 22 | """ 23 | 24 | import os 25 | import time 26 | 27 | DEFAULT_SECTION = 'default' 28 | DEFAULT_NAME = 'opensips-cli' 29 | try: 30 | home_dir = os.environ["HOME"] 31 | except: 32 | # default home dir to root 33 | home_dir = "/" 34 | 35 | """ 36 | Default history file is in ~/.opensips-cli.history 37 | """ 38 | HISTORY_FILE = os.path.join(home_dir, ".{}.history".format(DEFAULT_NAME)) 39 | 40 | """ 41 | Try configuration files in this order: 42 | * ~/.opensips-cli.cfg 43 | * /etc/opensips-cli.cfg 44 | * /etc/opensips/opensips-cli.cfg 45 | """ 46 | CFG_PATHS = [ 47 | os.path.join(home_dir, ".{}.cfg".format(DEFAULT_NAME)), 48 | "/etc/{}.cfg".format(DEFAULT_NAME), 49 | "/etc/opensips/{}.cfg".format(DEFAULT_NAME), 50 | ] 51 | 52 | DEFAULT_VALUES = { 53 | # CLI settings 54 | "prompt_name": "opensips-cli", 55 | "prompt_intro": "Welcome to OpenSIPS Command Line Interface!", 56 | "prompt_emptyline_repeat_cmd": "False", 57 | "history_file": HISTORY_FILE, 58 | "history_file_size": "1000", 59 | "output_type": "pretty-print", 60 | "log_level": "INFO", 61 | 62 | # communication information 63 | "communication_type": "fifo", 64 | "fifo_reply_dir": "/tmp", 65 | "fifo_file": "/var/run/opensips/opensips_fifo", 66 | "fifo_file_fallback": "/tmp/opensips_fifo", 67 | "url": "http://127.0.0.1:8888/mi", 68 | "datagram_ip": "127.0.0.1", 69 | "datagram_port": "8080", 70 | 71 | # database module 72 | "database_url": "mysql://opensips:opensipsrw@localhost", 73 | "database_name": "opensips", 74 | "database_schema_path": "/usr/share/opensips", 75 | 76 | # user module 77 | "plain_text_passwords": "False", 78 | 79 | # diagnose module 80 | "diagnose_listen_ip": "127.0.0.1", 81 | "diagnose_listen_port": "8899", 82 | 83 | # trace module 84 | "trace_listen_ip": "127.0.0.1", 85 | "trace_listen_port": "0", 86 | 87 | # trap module 88 | "trap_file": '/tmp/gdb_opensips_{}'.format(time.strftime('%Y%m%d_%H%M%S')) 89 | } 90 | 91 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 92 | -------------------------------------------------------------------------------- /opensipscli/libs/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from opensipscli.libs import sqlalchemy_utils 21 | -------------------------------------------------------------------------------- /opensipscli/libs/sqlalchemy_utils.py: -------------------------------------------------------------------------------- 1 | ## Copyright (c) 2012, Konsta Vesterinen 2 | ## 3 | ## All rights reserved. 4 | ## 5 | ## Redistribution and use in source and binary forms, with or without 6 | ## modification, are permitted provided that the following conditions are met: 7 | ## 8 | ## * Redistributions of source code must retain the above copyright notice, this 9 | ## list of conditions and the following disclaimer. 10 | ## 11 | ## * Redistributions in binary form must reproduce the above copyright notice, 12 | ## this list of conditions and the following disclaimer in the documentation 13 | ## and/or other materials provided with the distribution. 14 | ## 15 | ## * The names of the contributors may not be used to endorse or promote products 16 | ## derived from this software without specific prior written permission. 17 | ## 18 | ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ## ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | ## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | ## DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, 22 | ## INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 23 | ## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | ## LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | ## OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 27 | ## ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | ## 29 | ## Copied from https://github.com/kvesteri/sqlalchemy-utils/blob/2e8ee0093f4a33a5c7479bc9aaf16d7863a74a16/sqlalchemy_utils/functions/database.py 30 | ## Please check LICENSE 31 | 32 | from copy import copy 33 | 34 | import os 35 | import sqlalchemy as sa 36 | from sqlalchemy.engine.url import make_url 37 | from sqlalchemy.exc import OperationalError, ProgrammingError 38 | from sqlalchemy.engine.interfaces import Dialect 39 | from sqlalchemy.orm.session import object_session 40 | from sqlalchemy.orm.exc import UnmappedInstanceError 41 | 42 | def database_exists(url): 43 | """Check if a database exists. 44 | :param url: A SQLAlchemy engine URL. 45 | Performs backend-specific testing to quickly determine if a database 46 | exists on the server. :: 47 | database_exists('postgresql://postgres@localhost/name') #=> False 48 | create_database('postgresql://postgres@localhost/name') 49 | database_exists('postgresql://postgres@localhost/name') #=> True 50 | Supports checking against a constructed URL as well. :: 51 | engine = create_engine('postgresql://postgres@localhost/name') 52 | database_exists(engine.url) #=> False 53 | create_database(engine.url) 54 | database_exists(engine.url) #=> True 55 | """ 56 | 57 | def get_scalar_result(engine, sql): 58 | result_proxy = engine.execute(sql) 59 | result = result_proxy.scalar() 60 | result_proxy.close() 61 | engine.dispose() 62 | return result 63 | 64 | def sqlite_file_exists(database): 65 | if not os.path.isfile(database) or os.path.getsize(database) < 100: 66 | return False 67 | 68 | with open(database, 'rb') as f: 69 | header = f.read(100) 70 | 71 | return header[:16] == b'SQLite format 3\x00' 72 | 73 | url = copy(make_url(url)) 74 | if hasattr(url, "_replace"): 75 | database = url.database 76 | url = url._replace(database=None) 77 | else: 78 | database, url.database = url.database, None 79 | 80 | engine = sa.create_engine(url) 81 | 82 | if engine.dialect.name == 'postgresql': 83 | text = "SELECT 1 FROM pg_database WHERE datname='%s'" % database 84 | return bool(get_scalar_result(engine, text)) 85 | 86 | elif engine.dialect.name == 'mysql': 87 | text = ("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA " 88 | "WHERE SCHEMA_NAME = '%s'" % database) 89 | return bool(get_scalar_result(engine, text)) 90 | 91 | elif engine.dialect.name == 'sqlite': 92 | if database: 93 | return database == ':memory:' or sqlite_file_exists(database) 94 | else: 95 | # The default SQLAlchemy database is in memory, 96 | # and :memory is not required, thus we should support that use-case 97 | return True 98 | 99 | else: 100 | engine.dispose() 101 | engine = None 102 | text = 'SELECT 1' 103 | try: 104 | if hasattr(url, "_replace"): 105 | url = url._replace(database=database) 106 | else: 107 | url.database = database 108 | 109 | engine = sa.create_engine(url) 110 | result = engine.execute(text) 111 | result.close() 112 | return True 113 | 114 | except (ProgrammingError, OperationalError): 115 | return False 116 | finally: 117 | if engine is not None: 118 | engine.dispose() 119 | 120 | def get_bind(obj): 121 | """ 122 | Return the bind for given SQLAlchemy Engine / Connection / declarative 123 | model object. 124 | :param obj: SQLAlchemy Engine / Connection / declarative model object 125 | :: 126 | from sqlalchemy_utils import get_bind 127 | get_bind(session) # Connection object 128 | get_bind(user) 129 | """ 130 | if hasattr(obj, 'bind'): 131 | conn = obj.bind 132 | else: 133 | try: 134 | conn = object_session(obj).bind 135 | except UnmappedInstanceError: 136 | conn = obj 137 | 138 | if not hasattr(conn, 'execute'): 139 | raise TypeError( 140 | 'This method accepts only Session, Engine, Connection and ' 141 | 'declarative model objects.' 142 | ) 143 | return conn 144 | 145 | def quote(mixed, ident): 146 | """ 147 | Conditionally quote an identifier. 148 | :: 149 | from sqlalchemy_utils import quote 150 | engine = create_engine('sqlite:///:memory:') 151 | quote(engine, 'order') 152 | # '"order"' 153 | quote(engine, 'some_other_identifier') 154 | # 'some_other_identifier' 155 | :param mixed: SQLAlchemy Session / Connection / Engine / Dialect object. 156 | :param ident: identifier to conditionally quote 157 | """ 158 | if isinstance(mixed, Dialect): 159 | dialect = mixed 160 | else: 161 | dialect = get_bind(mixed).dialect 162 | return dialect.preparer(dialect).quote(ident) 163 | 164 | def drop_database(url): 165 | """Issue the appropriate DROP DATABASE statement. 166 | :param url: A SQLAlchemy engine URL. 167 | Works similar to the :ref:`create_database` method in that both url text 168 | and a constructed url are accepted. :: 169 | drop_database('postgresql://postgres@localhost/name') 170 | drop_database(engine.url) 171 | """ 172 | 173 | url = copy(make_url(url)) 174 | 175 | database = url.database 176 | 177 | if url.drivername.startswith('postgres'): 178 | if hasattr(url, "set"): 179 | url = url.set(database='postgres') 180 | else: 181 | url.database = 'postgres' 182 | 183 | elif url.drivername.startswith('mssql'): 184 | if hasattr(url, "set"): 185 | url = url.set(database='master') 186 | else: 187 | url.database = 'master' 188 | 189 | elif not url.drivername.startswith('sqlite'): 190 | if hasattr(url, "_replace"): 191 | url = url._replace(database=None) 192 | else: 193 | url.database = None 194 | 195 | if url.drivername == 'mssql+pyodbc': 196 | engine = sa.create_engine(url, connect_args={'autocommit': True}) 197 | elif url.drivername == 'postgresql+pg8000': 198 | engine = sa.create_engine(url, isolation_level='AUTOCOMMIT') 199 | else: 200 | engine = sa.create_engine(url) 201 | conn_resource = None 202 | 203 | if engine.dialect.name == 'sqlite' and database != ':memory:': 204 | if database: 205 | os.remove(database) 206 | 207 | elif ( 208 | engine.dialect.name == 'postgresql' and 209 | engine.driver in {'psycopg2', 'psycopg2cffi'} 210 | ): 211 | if engine.driver == 'psycopg2': 212 | from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT 213 | connection = engine.connect() 214 | connection.connection.set_isolation_level( 215 | ISOLATION_LEVEL_AUTOCOMMIT 216 | ) 217 | else: 218 | connection = engine.connect() 219 | connection.connection.set_session(autocommit=True) 220 | 221 | # Disconnect all users from the database we are dropping. 222 | version = connection.dialect.server_version_info 223 | pid_column = ( 224 | 'pid' if (version >= (9, 2)) else 'procpid' 225 | ) 226 | text = ''' 227 | SELECT pg_terminate_backend(pg_stat_activity.%(pid_column)s) 228 | FROM pg_stat_activity 229 | WHERE pg_stat_activity.datname = '%(database)s' 230 | AND %(pid_column)s <> pg_backend_pid(); 231 | ''' % {'pid_column': pid_column, 'database': database} 232 | connection.execute(text) 233 | 234 | # Drop the database. 235 | text = 'DROP DATABASE {0}'.format(quote(connection, database)) 236 | connection.execute(text) 237 | conn_resource = connection 238 | else: 239 | text = 'DROP DATABASE {0}'.format(quote(engine, database)) 240 | conn_resource = engine.execute(text) 241 | 242 | if conn_resource is not None: 243 | conn_resource.close() 244 | engine.dispose() 245 | -------------------------------------------------------------------------------- /opensipscli/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | """ 21 | logger.py - implements coloured logging for the opensips-cli project 22 | """ 23 | 24 | import logging 25 | 26 | #These are the sequences need to get colored ouput 27 | RESET_SEQ = "\033[0m" 28 | COLOR_SEQ = "\033[1;%dm" 29 | BOLD_SEQ = "\033[1m" 30 | 31 | def formatter_message(message, use_color = True): 32 | if use_color: 33 | message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) 34 | else: 35 | message = message.replace("$RESET", "").replace("$BOLD", "") 36 | return message 37 | 38 | # Custom logger class with multiple destinations 39 | class ColoredLogger(logging.Logger): 40 | 41 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 42 | 43 | FORMAT = "$BOLD%(levelname)s$RESET: %(message)s" 44 | COLOR_FORMAT = formatter_message(FORMAT, True) 45 | 46 | def __init__(self, name): 47 | logging.Logger.__init__(self, name) 48 | 49 | color_formatter = ColoredFormatter(self.COLOR_FORMAT) 50 | 51 | console = logging.StreamHandler() 52 | console.setFormatter(color_formatter) 53 | 54 | self.addHandler(console) 55 | return 56 | 57 | def color(self, color, message): 58 | return COLOR_SEQ % (30 + color) + message + RESET_SEQ 59 | 60 | class ColoredFormatter(logging.Formatter): 61 | 62 | LEVELS_COLORS = { 63 | 'WARNING': ColoredLogger.YELLOW, 64 | 'INFO': ColoredLogger.MAGENTA, 65 | 'DEBUG': ColoredLogger.BLUE, 66 | 'CRITICAL': ColoredLogger.YELLOW, 67 | 'ERROR': ColoredLogger.RED 68 | } 69 | 70 | def __init__(self, msg, use_color = True): 71 | logging.Formatter.__init__(self, msg) 72 | self.use_color = use_color 73 | 74 | def format(self, record): 75 | levelname = record.levelname 76 | if self.use_color and levelname in self.LEVELS_COLORS: 77 | levelname_color = COLOR_SEQ % (30 + self.LEVELS_COLORS[levelname]) + levelname + RESET_SEQ 78 | record.levelname = levelname_color 79 | return logging.Formatter.format(self, record) 80 | 81 | 82 | logging.setLoggerClass(ColoredLogger) 83 | logger = logging.getLogger(__name__) 84 | 85 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 86 | -------------------------------------------------------------------------------- /opensipscli/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | import sys 21 | import argparse 22 | from opensipscli import cli, defaults, version 23 | 24 | parser = argparse.ArgumentParser(description='OpenSIPS CLI interactive tool', 25 | prog=sys.argv[0], 26 | usage='%(prog)s [OPTIONS]', 27 | epilog='\n') 28 | 29 | # Argument used to print the current version 30 | parser.add_argument('-v', '--version', 31 | action='version', 32 | default=None, 33 | version='OpenSIPS CLI {}'.format(version.__version__)) 34 | # Argument used to enable debugging 35 | parser.add_argument('-d', '--debug', 36 | action='store_true', 37 | default=False, 38 | help='enable debugging') 39 | # Argument used to specify a configuration file 40 | parser.add_argument('-f', '--config', 41 | metavar='[FILE]', 42 | type=str, 43 | default=None, 44 | help='used to specify a configuration file') 45 | # Argument used to switch to a different instance 46 | parser.add_argument('-i', '--instance', 47 | metavar='[INSTANCE]', 48 | type=str, 49 | action='store', 50 | default=defaults.DEFAULT_SECTION, 51 | help='choose an opensips instance') 52 | # Argument used to overwrite certain values in the config 53 | parser.add_argument('-o', '--option', 54 | metavar='[KEY=VALUE]', 55 | action='append', 56 | type=str, 57 | dest="extra_options", 58 | default=None, 59 | help='overwrite certain values in the config') 60 | # Argument used to dump the configuration 61 | parser.add_argument('-p', '--print', 62 | action='store_true', 63 | default=False, 64 | help='dump the configuration') 65 | # Argument used to run the command in non-interactive mode 66 | parser.add_argument('-x', '--execute', 67 | action='store_true', 68 | default=False, 69 | help='run the command in non-interactive mode') 70 | # Argument used to specify the command to run 71 | parser.add_argument('command', 72 | nargs='*', 73 | default=[], 74 | help='the command to run') 75 | 76 | def main(): 77 | 78 | # Parse all arguments 79 | args = parser.parse_args() 80 | 81 | # Open the CLI 82 | shell = cli.OpenSIPSCLI(args) 83 | sys.exit(shell.cmdloop()) 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /opensipscli/module.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | class Module: 21 | """ 22 | An abstract class, that has to be implemented by every Module that should be handled 23 | """ 24 | 25 | def __exclude__(self): 26 | """ 27 | indicates whether the module should be excluded 28 | """ 29 | return (False, None) 30 | 31 | def __invoke__(self, cmd, params=None, modifiers=None): 32 | """ 33 | used to invoke a command from the module (starting with prefix 'do_') 34 | """ 35 | f = getattr(self, 'do_' + cmd) 36 | return f(params, modifiers) 37 | 38 | def __get_methods__(self): 39 | """ 40 | returns all the available methods of the module 41 | if the method returns None, the do_`module_name` 42 | method is called for each command 43 | """ 44 | return ([x[3:] for x in dir(self) 45 | if x.startswith('do_') and callable(getattr(self, x))]) 46 | 47 | def __get_modifiers__(self): 48 | """ 49 | returns all the available modifiers of a specific module 50 | """ 51 | return None 52 | 53 | def __complete__(self, command, text, line, begidx, endidx): 54 | """ 55 | returns a list with all the auto-completion values 56 | """ 57 | if not command: 58 | modifiers = self.__get_modifiers__() 59 | return modifiers if modifiers else [''] 60 | try: 61 | compfunc = getattr(self, 'complete_' + command) 62 | l = compfunc(text, line, begidx, endidx) 63 | if not l: 64 | return [''] 65 | except AttributeError: 66 | return None 67 | if len(l) == 1: 68 | l[0] += " " 69 | return l 70 | -------------------------------------------------------------------------------- /opensipscli/modules/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | import pkgutil 21 | 22 | __path__ = pkgutil.extend_path(__path__, __name__) 23 | for importer, modname, ispkg in pkgutil.walk_packages(path=__path__, prefix=__name__+'.'): 24 | __import__(modname) 25 | -------------------------------------------------------------------------------- /opensipscli/modules/instance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from opensipscli.config import cfg 21 | from opensipscli.logger import logger 22 | from opensipscli.module import Module 23 | 24 | class instance(Module): 25 | 26 | def get_instances(self): 27 | l = cfg.config.sections() 28 | default_section = cfg.get_default_instance() 29 | if default_section not in l: 30 | l.insert(0, default_section) 31 | return l 32 | 33 | def do_show(self, params, modifiers): 34 | print(cfg.current_instance) 35 | 36 | def do_list(self, params, modifiers): 37 | for i in self.get_instances(): 38 | print(i) 39 | 40 | def complete_switch(self, text, line, *ignore): 41 | if len(line.split(' ')) > 3: 42 | return [] 43 | return [ a for a in self.get_instances() if a.startswith(text)] 44 | 45 | def do_switch(self, params, modifiers): 46 | if len(params) == 0: 47 | return 48 | new_instance = params[0] 49 | if cfg.has_instance(new_instance): 50 | cfg.set_instance(new_instance) 51 | else: 52 | logger.error("cannot switch to instance '{}': instance not found!".format(new_instance)) 53 | return -1 54 | -------------------------------------------------------------------------------- /opensipscli/modules/mi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | import re 21 | import json 22 | import shlex 23 | from collections import OrderedDict 24 | from opensipscli.config import cfg 25 | from opensipscli.logger import logger 26 | from opensipscli.module import Module 27 | from opensipscli import comm 28 | 29 | try: 30 | import yaml 31 | yaml_available = True 32 | except ImportError: 33 | yaml_available = False 34 | 35 | # temporary special handling for commands that require array params 36 | # format is: command: (idx, name) 37 | MI_ARRAY_PARAMS_COMMANDS = { 38 | "fs_subscribe": (1, "events"), 39 | "fs_unsubscribe": (1, "events"), 40 | "b2b_trigger_scenario": (3, "scenario_params"), 41 | "dlg_push_var": (2, "DID"), 42 | "get_statistics": (0, "statistics"), 43 | "list_statistics": (0, "statistics"), 44 | "reset_statistics": (0, "statistics"), 45 | "trace_start": (0, "filter"), 46 | "raise_event": (1, "params"), 47 | "dfks_set_feature": (4, "values"), 48 | "cluster_broadcast_mi": (2, "cmd_params"), 49 | } 50 | 51 | 52 | MI_MODIFIERS = [ "-j" ] 53 | 54 | class mi(Module): 55 | 56 | def print_pretty_print(self, result): 57 | print(json.dumps(result, indent=4)) 58 | 59 | def print_dictionary(self, result): 60 | print(str(result)) 61 | 62 | def print_lines(self, result, indent=0): 63 | if type(result) in [OrderedDict, dict]: 64 | for k, v in result.items(): 65 | if type(v) in [OrderedDict, list, dict]: 66 | print(" " * indent + k + ":") 67 | self.print_lines(v, indent + 4) 68 | else: 69 | print(" " * indent + "{}: {}". format(k, v)) 70 | elif type(result) == list: 71 | for v in result: 72 | self.print_lines(v, indent) 73 | else: 74 | print(" " * indent + str(result)) 75 | pass 76 | 77 | def print_yaml(self, result): 78 | if not yaml_available: 79 | logger.warning("yaml not available on your platform! " 80 | "Please install `python-yaml` package or similar!") 81 | else: 82 | print(yaml.dump(result, default_flow_style=False).strip()) 83 | 84 | def get_params_set(self, cmds): 85 | l = set() 86 | for p in cmds: 87 | m = re.match('([a-zA-Z\.\-_]+)=', p) 88 | # if it's not a parameter name, skip 89 | if m: 90 | l.add(m.group(1)) 91 | else: 92 | return None 93 | return l 94 | 95 | def get_params_names(self, line): 96 | cmds = shlex.split(line) 97 | # cmd[0] = module, cmd[1] = command 98 | if len(cmds) < 2: 99 | return None 100 | return self.get_params_set(cmds[2:]) 101 | 102 | def parse_params(self, cmd, params, modifiers): 103 | 104 | # first, we check to see if we have only named parameters 105 | nparams = self.get_params_set(params) 106 | if nparams is not None: 107 | logger.debug("named parameters are used") 108 | new_params = {} 109 | for p in params: 110 | s = p.split("=", 1) 111 | value = "" if len(s) == 1 else s[1] 112 | # check to see if we have to split them in array or not 113 | if cmd in MI_ARRAY_PARAMS_COMMANDS and \ 114 | "-j" not in modifiers and \ 115 | MI_ARRAY_PARAMS_COMMANDS[cmd][1] == s[0]: 116 | value = shlex.split(value) 117 | new_params[s[0]] = value 118 | else: 119 | # old style positional parameters 120 | logger.debug("positional parameters are used") 121 | # if the command is not in MI_ARRAY_PARAMS_COMMANDS, return the 122 | # parameters as they are 123 | logger.debug("0. {}".format(params)) 124 | if "-j" in modifiers: 125 | json_params = [] 126 | for x in params: 127 | try: 128 | x = json.loads(x) 129 | except: 130 | pass 131 | json_params.append(x) 132 | params = json_params 133 | 134 | if not cmd in MI_ARRAY_PARAMS_COMMANDS or "-j" in modifiers: 135 | return params 136 | # build params based on their index 137 | new_params = params[0:MI_ARRAY_PARAMS_COMMANDS[cmd][0]] 138 | if params[MI_ARRAY_PARAMS_COMMANDS[cmd][0]:]: 139 | new_params.append(params[MI_ARRAY_PARAMS_COMMANDS[cmd][0]:]) 140 | return new_params 141 | 142 | def __invoke__(self, cmd, params=None, modifiers=None): 143 | params = self.parse_params(cmd, params, modifiers) 144 | # Mi Module works with JSON Communication 145 | logger.debug("running command '{}' '{}'".format(cmd, params)) 146 | res = comm.execute(cmd, params) 147 | if res is None: 148 | return -1 149 | output_type = cfg.get('output_type') 150 | if output_type == "pretty-print": 151 | self.print_pretty_print(res) 152 | elif output_type == "dictionary": 153 | self.print_dictionary(res) 154 | elif output_type == "lines": 155 | self.print_lines(res) 156 | elif output_type == "yaml": 157 | self.print_yaml(res) 158 | elif output_type == "none": 159 | pass # no one interested in the reply 160 | else: 161 | logger.error("unknown output_type='{}'! Dropping output!" 162 | .format(output_type)) 163 | return 0 164 | 165 | def __complete__(self, command, text, line, begidx, endidx): 166 | # TODO: shall we cache this? 167 | params_arr = comm.execute('which', {'command': command}) 168 | if len(text) == 0: 169 | # if last character is an equal, it's probably a value, or it will 170 | if line[-1] == "=": 171 | return [''] 172 | params = self.get_params_names(line) 173 | if params is None: 174 | flat_list = list([item for sublist in params_arr for item in sublist]) 175 | else: 176 | # check in the line to see the parameters we've used 177 | flat_list = set() 178 | for p in params_arr: 179 | sp = set(p) 180 | if params.issubset(sp): 181 | flat_list = flat_list.union(sp) 182 | flat_list = flat_list - params 183 | else: 184 | flat_list = [] 185 | for l in params_arr: 186 | p = [ x for x in l if x.startswith(text) ] 187 | if len(p) != 0: 188 | flat_list += p 189 | l = [ x + "=" for x in list(dict.fromkeys(flat_list)) ] 190 | return l if len(l) > 0 else [''] 191 | 192 | def __exclude__(self): 193 | vld = comm.valid() 194 | return (not vld[0], vld[1]) 195 | 196 | def __get_methods__(self): 197 | return comm.execute('which') 198 | 199 | def __get_modifiers__(self): 200 | return MI_MODIFIERS 201 | -------------------------------------------------------------------------------- /opensipscli/modules/tls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from opensipscli.module import Module 21 | from opensipscli.logger import logger 22 | from socket import gethostname 23 | from pprint import pprint 24 | from time import gmtime, mktime 25 | from os.path import exists, join, dirname 26 | from os import makedirs 27 | from opensipscli.config import cfg, OpenSIPSCLIConfig 28 | from random import randrange 29 | 30 | openssl_version = None 31 | 32 | try: 33 | from cryptography import x509 34 | from cryptography.hazmat.backends import default_backend 35 | from cryptography.hazmat.primitives import hashes, serialization 36 | from cryptography.hazmat.primitives.asymmetric import rsa 37 | from cryptography.x509.oid import NameOID 38 | import datetime 39 | openssl_version = 'cryptography' 40 | except ImportError: 41 | logger.info("cryptography library not available!") 42 | try: 43 | from OpenSSL import crypto, SSL 44 | openssl_version = 'openssl' 45 | except (TypeError, ImportError): 46 | logger.info("OpenSSL library not available!") 47 | 48 | class tlsCert: 49 | 50 | def __init__(self, prefix, cfg=None): 51 | 52 | if not cfg: 53 | self.load(prefix) 54 | return 55 | self.CN = cfg.read_param("tls_"+prefix+"_common_name", "Website address (CN)", "opensips.org") 56 | self.C = cfg.read_param("tls_"+prefix+"_country", "Country (C)", "RO") 57 | self.ST = cfg.read_param("tls_"+prefix+"_state", "State (ST)", "Bucharest") 58 | self.L = cfg.read_param("tls_"+prefix+"_locality", "Locality (L)", "Bucharest") 59 | self.O = cfg.read_param("tls_"+prefix+"_organisation", "Organization (O)", "OpenSIPS") 60 | self.OU = cfg.read_param("tls_"+prefix+"_organisational_unit", "Organisational Unit (OU)", "Project") 61 | self.notafter = int(cfg.read_param("tls_"+prefix+"_notafter", "Certificate validity (seconds)", 315360000)) 62 | self.md = cfg.read_param("tls_"+prefix+"_md", "Digest Algorithm", "SHA256") 63 | 64 | class tlsKey: 65 | 66 | def __init__(self, prefix, cfg=None): 67 | 68 | if not cfg: 69 | self.load(prefix) 70 | return 71 | self.key_size = int(cfg.read_param("tls_"+prefix+"_key_size", "RSA key size (bits)", 4096)) 72 | 73 | 74 | class tlsOpenSSLCert(tlsCert): 75 | 76 | def __init__(self, prefix, cfg=None): 77 | super().__init__(prefix, cfg) 78 | if not cfg: 79 | return 80 | cert = crypto.X509() 81 | cert.set_version(2) 82 | cert.get_subject().CN = self.CN 83 | cert.get_subject().C = self.C 84 | cert.get_subject().ST = self.ST 85 | cert.get_subject().L = self.L 86 | cert.get_subject().O = self.O 87 | cert.get_subject().OU = self.OU 88 | cert.set_serial_number(randrange(100000)) 89 | cert.gmtime_adj_notBefore(0) 90 | cert.gmtime_adj_notAfter(self.notafter) 91 | 92 | extensions = [ 93 | crypto.X509Extension(b'basicConstraints', False, b'CA:TRUE'), 94 | crypto.X509Extension(b'extendedKeyUsage', False, b'clientAuth,serverAuth') 95 | ] 96 | 97 | cert.add_extensions(extensions) 98 | 99 | self.cert = cert 100 | 101 | def load(self, cacert): 102 | self.cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cacert, 'rt').read()) 103 | 104 | def sign(self, key): 105 | self.cert.set_pubkey(key) 106 | self.cert.sign(key.key, self.md) 107 | 108 | def set_issuer(self, issuer): 109 | self.cert.set_issuer(issuer) 110 | 111 | def get_subject(self): 112 | return self.cert.get_subject() 113 | 114 | def dump(self): 115 | return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert).decode('utf-8') 116 | 117 | class tlsCryptographyCert(tlsCert): 118 | 119 | def __init__(self, prefix, cfg=None): 120 | super().__init__(prefix, cfg) 121 | if not cfg: 122 | return 123 | builder = x509.CertificateBuilder() 124 | builder = builder.subject_name(x509.Name([ 125 | x509.NameAttribute(NameOID.COMMON_NAME, self.CN), 126 | x509.NameAttribute(NameOID.COUNTRY_NAME, self.C), 127 | x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, self.ST), 128 | x509.NameAttribute(NameOID.LOCALITY_NAME, self.L), 129 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, self.O), 130 | x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, self.OU), 131 | ])) 132 | builder = builder.serial_number(x509.random_serial_number()) 133 | builder = builder.not_valid_before(datetime.datetime.today() - 134 | datetime.timedelta(1)) 135 | builder = builder.not_valid_after(datetime.datetime.today() + 136 | datetime.timedelta(0, self.notafter)) 137 | builder = builder.add_extension( 138 | x509.BasicConstraints(ca=False, path_length=None), 139 | critical=False 140 | ) 141 | builder = builder.add_extension( 142 | x509.ExtendedKeyUsage([ 143 | x509.ExtendedKeyUsageOID.CLIENT_AUTH, 144 | x509.ExtendedKeyUsageOID.SERVER_AUTH]), 145 | critical=False 146 | ) 147 | self.builder = builder 148 | self.cert = None 149 | 150 | def load(self, cacert): 151 | self.cert = x509.load_pem_x509_certificate(open(cacert, 'rb').read()) 152 | 153 | def sign(self, key): 154 | self.builder = self.builder.public_key(key.key.public_key()) 155 | self.cert = self.builder.sign(private_key = key.key, 156 | algorithm=getattr(hashes, self.md)(), 157 | backend=default_backend()) 158 | 159 | def set_issuer(self, issuer): 160 | self.builder = self.builder.issuer_name(issuer) 161 | 162 | def get_subject(self): 163 | if self.cert: 164 | return self.cert.subject 165 | return self.builder._subject_name 166 | 167 | def dump(self): 168 | return self.cert.public_bytes(encoding=serialization.Encoding.PEM).decode('utf-8') 169 | 170 | 171 | class tlsOpenSSLKey(tlsKey): 172 | 173 | def __init__(self, prefix, cfg=None): 174 | super().__init__(prefix, cfg) 175 | if not cfg: 176 | return 177 | key = crypto.PKey() 178 | key.generate_key(crypto.TYPE_RSA, self.key_size) 179 | self.key = key 180 | 181 | def dump(self): 182 | return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key).decode('utf-8') 183 | 184 | def load(self, key): 185 | self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, open(key, 'rt').read()) 186 | 187 | class tlsCryptographyKey(tlsKey): 188 | 189 | def __init__(self, prefix, cfg=None): 190 | super().__init__(prefix, cfg) 191 | if not cfg: 192 | return 193 | self.key = rsa.generate_private_key( 194 | key_size=self.key_size, 195 | public_exponent=65537, 196 | backend=default_backend() 197 | ) 198 | 199 | def dump(self): 200 | return self.key.private_bytes(encoding=serialization.Encoding.PEM, 201 | format=serialization.PrivateFormat.TraditionalOpenSSL, 202 | encryption_algorithm=serialization.NoEncryption() 203 | ).decode('utf-8') 204 | 205 | def load(self, key): 206 | self.key = serialization.load_pem_private_key(open(key, 'rb').read(), 207 | password=None) 208 | 209 | class tls(Module): 210 | def do_rootCA(self, params, modifiers=None): 211 | global cfg 212 | logger.info("Preparing to generate CA cert + key...") 213 | 214 | # TODO 215 | # separate cli.cfg files for TLS are fully deprecated, this if block is 216 | # only kept for backwards-compatibility. Remove starting from v3.2! <3 217 | if cfg.exists('tls_ca_config'): 218 | tls_cfg = cfg.get('tls_ca_config') 219 | cfg = OpenSIPSCLIConfig() 220 | cfg.parse(tls_cfg) 221 | 222 | ca_dir = cfg.read_param("tls_ca_dir", "Output directory", "/etc/opensips/tls/rootCA/") 223 | cert_file = cfg.read_param("tls_ca_cert_file", "Output cert file", "cacert.pem") 224 | key_file = cfg.read_param("tls_ca_key_file", "Output key file", "private/cakey.pem") 225 | c_f = join(ca_dir, cert_file) 226 | k_f = join(ca_dir, key_file) 227 | 228 | if (exists(c_f) or exists(k_f)) and not cfg.read_param("tls_ca_overwrite", 229 | "CA certificate or key already exists, overwrite?", "yes", True): 230 | return 231 | 232 | if openssl_version == 'openssl': 233 | cert = tlsOpenSSLCert("ca", cfg) 234 | key = tlsOpenSSLKey("ca", cfg) 235 | else: 236 | cert = tlsCryptographyCert("ca", cfg) 237 | key = tlsCryptographyKey("ca", cfg) 238 | 239 | cert.set_issuer(cert.get_subject()) 240 | cert.sign(key) 241 | 242 | try: 243 | if not exists(dirname(c_f)): 244 | makedirs(dirname(c_f)) 245 | open(c_f, "wt").write(cert.dump()) 246 | except Exception as e: 247 | logger.exception(e) 248 | logger.error("Failed to write to %s", c_f) 249 | return 250 | 251 | try: 252 | if not exists(dirname(k_f)): 253 | makedirs(dirname(k_f)) 254 | open(k_f, "wt").write(key.dump()) 255 | except Exception as e: 256 | logger.exception(e) 257 | logger.error("Failed to write to %s", k_f) 258 | return 259 | 260 | logger.info("CA certificate created in " + c_f) 261 | logger.info("CA private key created in " + k_f) 262 | 263 | def do_userCERT(self, params, modifiers=None): 264 | global cfg 265 | logger.info("Preparing to generate user cert + key + CA list...") 266 | 267 | # TODO 268 | # separate cli.cfg files for TLS are fully deprecated, this if block is 269 | # only kept for backwards-compatibility. Remove starting from v3.2! <3 270 | if cfg.exists('tls_user_config'): 271 | tls_cfg = cfg.get('tls_user_config') 272 | cfg = OpenSIPSCLIConfig() 273 | cfg.parse(tls_cfg) 274 | 275 | user_dir = cfg.read_param("tls_user_dir", "Output directory", "/etc/opensips/tls/user/") 276 | cert_file = cfg.read_param("tls_user_cert_file", "Output cert file", "user-cert.pem") 277 | key_file = cfg.read_param("tls_user_key_file", "Output key file", "user-privkey.pem") 278 | calist_file = cfg.read_param("tls_user_calist_file", "Output CA list file", "user-calist.pem") 279 | 280 | c_f = join(user_dir, cert_file) 281 | k_f = join(user_dir, key_file) 282 | ca_f = join(user_dir, calist_file) 283 | 284 | if (exists(c_f) or exists(k_f) or exists(ca_f)) and not cfg.read_param("tls_user_overwrite", 285 | "User certificate, key or CA list file already exists, overwrite?", "yes", True): 286 | return 287 | 288 | cacert = cfg.read_param("tls_user_cacert", "CA cert file", "/etc/opensips/tls/rootCA/cacert.pem") 289 | cakey = cfg.read_param("tls_user_cakey", "CA key file", "/etc/opensips/tls/rootCA/private/cakey.pem") 290 | 291 | try: 292 | if openssl_version == 'openssl': 293 | ca_cert = tlsOpenSSLCert(cacert) 294 | else: 295 | ca_cert = tlsCryptographyCert(cacert) 296 | except Exception as e: 297 | logger.exception(e) 298 | logger.error("Failed to load %s", cacert) 299 | return 300 | 301 | try: 302 | if openssl_version == 'openssl': 303 | ca_key = tlsOpenSSLLey(cakey) 304 | else: 305 | ca_key = tlsCryptographyKey(cakey) 306 | except Exception as e: 307 | logger.exception(e) 308 | logger.error("Failed to load %s", cakey) 309 | return 310 | 311 | # create a self-signed cert 312 | if openssl_version == 'openssl': 313 | cert = tlsOpenSSLCert("user", cfg) 314 | key = tlsOpenSSLKey("user", cfg) 315 | else: 316 | cert = tlsCryptographyCert("user", cfg) 317 | key = tlsCryptographyKey("user", cfg) 318 | 319 | cert.set_issuer(ca_cert.get_subject()) 320 | cert.sign(ca_key) 321 | try: 322 | if not exists(dirname(c_f)): 323 | makedirs(dirname(c_f)) 324 | open(c_f, "wt").write(cert.dump()) 325 | except Exception as e: 326 | logger.exception(e) 327 | logger.error("Failed to write to %s", c_f) 328 | return 329 | 330 | try: 331 | if not exists(dirname(k_f)): 332 | makedirs(dirname(k_f)) 333 | open(k_f, "wt").write(key.dump()) 334 | except Exception as e: 335 | logger.exception(e) 336 | logger.error("Failed to write to %s", k_f) 337 | return 338 | 339 | try: 340 | if not exists(dirname(ca_f)): 341 | makedirs(dirname(ca_f)) 342 | open(ca_f, "wt").write(ca_cert.dump()) 343 | except Exception as e: 344 | logger.exception(e) 345 | logger.error("Failed to write to %s", ca_f) 346 | return 347 | 348 | logger.info("user certificate created in " + c_f) 349 | logger.info("user private key created in " + k_f) 350 | logger.info("user CA list (chain of trust) created in " + ca_f) 351 | 352 | 353 | def __exclude__(self): 354 | return (not openssl_version, None) 355 | -------------------------------------------------------------------------------- /opensipscli/modules/trace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from datetime import datetime 21 | from time import time 22 | import random 23 | import socket 24 | from opensipscli import comm 25 | from opensipscli.config import cfg 26 | from opensipscli.logger import logger 27 | from opensipscli.module import Module 28 | 29 | TRACE_BUFFER_SIZE = 65535 30 | 31 | ''' 32 | find out more information here: 33 | * https://github.com/sipcapture/HEP/blob/master/docs/HEP3NetworkProtocolSpecification_REV26.pdf 34 | ''' 35 | 36 | protocol_types = { 37 | 0x00: "UNKNOWN", 38 | 0x01: "SIP", 39 | 0x02: "XMPP", 40 | 0x03: "SDP", 41 | 0x04: "RTP", 42 | 0x05: "RTCP JSON", 43 | 0x56: "LOG", 44 | 0x57: "MI", 45 | 0x58: "REST", 46 | 0x59: "NET", 47 | 0x60: "CONTROL", 48 | } 49 | 50 | protocol_ids = { 51 | num:name[8:] for name,num in vars(socket).items() if name.startswith("IPPROTO") 52 | } 53 | 54 | class HEPpacketException(Exception): 55 | pass 56 | 57 | class HEPpacket(object): 58 | 59 | def __init__(self, payloads): 60 | self.payloads = payloads 61 | self.family = socket.AF_INET 62 | self.protocol = "UNKNOWN" 63 | self.src_addr = None 64 | self.dst_addr = None 65 | self.src_port = None 66 | self.dst_port = None 67 | self.data = None 68 | self.correlation = None 69 | self.ts = time() 70 | self.tms = datetime.now().microsecond 71 | 72 | def __str__(self): 73 | time_str = "{}.{}".format( 74 | self.ts, 75 | self.tms) 76 | protocol_str = " {}/{}".format( 77 | self.protocol, 78 | self.type) 79 | 80 | if self.type == "SIP": 81 | ip_str = " {}:{} -> {}:{}".format( 82 | socket.inet_ntop(self.family, self.src_addr), 83 | self.src_port, 84 | socket.inet_ntop(self.family, self.dst_addr), 85 | self.dst_port) 86 | else: 87 | ip_str = "" 88 | if self.data: 89 | data_str = self.data.decode() 90 | else: 91 | data_str = "" 92 | 93 | return logger.color(logger.BLUE, time_str) + \ 94 | logger.color(logger.CYAN, protocol_str + ip_str) + \ 95 | "\n" + data_str 96 | 97 | def parse(self): 98 | length = len(self.payloads) 99 | payloads = self.payloads 100 | while length > 0: 101 | if length < 6: 102 | logger.error("payload too small {}".format(length)) 103 | return None 104 | chunk_vendor_id = int.from_bytes(payloads[0:2], 105 | byteorder="big", signed=False) 106 | chunk_type_id = int.from_bytes(payloads[2:4], 107 | byteorder="big", signed=False) 108 | chunk_len = int.from_bytes(payloads[4:6], 109 | byteorder="big", signed=False) 110 | if chunk_len < 6: 111 | logger.error("chunk too small {}".format(chunk_len)) 112 | return None 113 | payload = payloads[6:chunk_len] 114 | payloads = payloads[chunk_len:] 115 | length = length - chunk_len 116 | self.push_chunk(chunk_vendor_id, chunk_type_id, payload) 117 | 118 | def push_chunk(self, vendor_id, type_id, payload): 119 | 120 | if vendor_id != 0: 121 | logger.warning("Unknown vendor id {}".format(vendor_id)) 122 | raise HEPpacketException 123 | if type_id == 0x0001: 124 | if len(payload) != 1: 125 | raise HEPpacketException 126 | self.family = payload[0] 127 | elif type_id == 0x0002: 128 | if len(payload) != 1: 129 | raise HEPpacketException 130 | if not payload[0] in protocol_ids: 131 | self.protocol = str(payload[0]) 132 | else: 133 | self.protocol = protocol_ids[payload[0]] 134 | elif type_id >= 0x0003 and type_id <= 0x0006: 135 | expected_payload_len = 4 if type_id <= 0x0004 else 16 136 | if len(payload) != expected_payload_len: 137 | raise HEPpacketException 138 | if type_id == 0x0003 or type_id == 0x0005: 139 | self.src_addr = payload 140 | else: 141 | self.dst_addr = payload 142 | elif type_id == 0x0007 or type_id == 0x0008: 143 | if len(payload) != 2: 144 | raise HEPpacketException 145 | port = int.from_bytes(payload, 146 | byteorder="big", signed=False) 147 | if type_id == 7: 148 | self.src_port = port 149 | else: 150 | self.dst_port = port 151 | elif type_id == 0x0009 or type_id == 0x000a: 152 | if len(payload) != 4: 153 | raise HEPpacketException 154 | timespec = int.from_bytes(payload, 155 | byteorder="big", signed=False) 156 | if type_id == 0x0009: 157 | self.ts = timespec 158 | else: 159 | self.tms = timespec 160 | elif type_id == 0x000b: 161 | if len(payload) != 1: 162 | raise HEPpacketException 163 | if not payload[0] in protocol_types: 164 | self.type = str(payload[0]) 165 | else: 166 | self.type = protocol_types[payload[0]] 167 | elif type_id == 0x000c: 168 | pass # capture id not used now 169 | elif type_id == 0x000f: 170 | self.data = payload 171 | elif type_id == 0x0011: 172 | self.correlation = payload 173 | else: 174 | logger.warning("unhandled payload type {}".format(type_id)) 175 | 176 | class trace(Module): 177 | 178 | def __print_hep(self, packet): 179 | # this works as a HEP parser 180 | logger.debug("initial packet size is {}".format(len(packet))) 181 | 182 | while len(packet) > 0: 183 | if len(packet) < 4: 184 | return packet 185 | # currently only HEPv3 is accepted 186 | if packet[0:4] != b'HEP3': 187 | logger.warning("packet not HEPv3: [{}]".format(packet[0:4])) 188 | return None 189 | length = int.from_bytes(packet[4:6], byteorder="big", signed=False) 190 | if length > len(packet): 191 | logger.debug("partial packet: {} out of {}". 192 | format(len(packet), length)) 193 | # wait for entire packet to parse it 194 | return packet 195 | logger.debug("packet size is {}".format(length)) 196 | # skip the header 197 | hep_packet = HEPpacket(packet[6:length]) 198 | try: 199 | hep_packet.parse() 200 | except HEPpacketException: 201 | return None 202 | packet = packet[length:] 203 | print(hep_packet) 204 | 205 | return packet 206 | 207 | def __complete__(self, command, text, line, begidx, endidx): 208 | filters = [ "caller", "callee", "ip" ] 209 | 210 | # remove the filters already used 211 | filters = [f for f in filters if line.find(f + "=") == -1] 212 | if not command: 213 | return filters 214 | 215 | if (not text or text == "") and line[-1] == "=": 216 | return [""] 217 | 218 | ret = [f for f in filters if (f.startswith(text) and line.find(f + "=") == -1)] 219 | if len(ret) == 1 : 220 | ret[0] = ret[0] + "=" 221 | return ret 222 | 223 | def __get_methods__(self): 224 | return None 225 | 226 | def do_trace(self, params, modifiers): 227 | 228 | filters = [] 229 | 230 | if params is None: 231 | caller_f = input("Caller filter: ") 232 | if caller_f != "": 233 | filters.append("caller={}".format(caller_f)) 234 | callee_f = input("Callee filter: ") 235 | if callee_f != "": 236 | filters.append("callee={}".format(callee_f)) 237 | ip_f = input("Source IP filter: ") 238 | if ip_f != "": 239 | filters.append("ip={}".format(ip_f)) 240 | if len(filters) == 0: 241 | ans = cfg.read_param(None, "No filter specified! "\ 242 | "Continue without a filter?", False, True) 243 | if not ans: 244 | return False 245 | filters = None 246 | else: 247 | filters = params 248 | 249 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 250 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 251 | trace_ip = cfg.get("trace_listen_ip") 252 | trace_port = int(cfg.get("trace_listen_port")) 253 | s.bind((trace_ip, trace_port)) 254 | if trace_port == 0: 255 | trace_port = s.getsockname()[1] 256 | s.listen(1) 257 | conn = None 258 | trace_name = "opensips-cli.{}".format(random.randint(0, 65536)) 259 | trace_socket = "hep:{}:{};transport=tcp;version=3".format( 260 | trace_ip, trace_port) 261 | args = { 262 | 'id': trace_name, 263 | 'uri': trace_socket, 264 | } 265 | if filters: 266 | args['filter'] = filters 267 | 268 | logger.debug("filters are {}".format(filters)) 269 | trace_started = comm.execute('trace_start', args) 270 | if not trace_started: 271 | return False 272 | 273 | try: 274 | conn, addr = s.accept() 275 | logger.debug("New TCP connection from {}:{}". 276 | format(addr[0], addr[1])) 277 | remaining = b'' 278 | while True: 279 | data = conn.recv(TRACE_BUFFER_SIZE) 280 | if not data: 281 | break 282 | remaining = self.__print_hep(remaining + data) 283 | if remaining is None: 284 | break 285 | except KeyboardInterrupt: 286 | comm.execute('trace_stop', {'id' : trace_name }, True) 287 | if conn is not None: 288 | conn.close() 289 | 290 | def __exclude__(self): 291 | valid = comm.valid() 292 | return (not valid[0], valid[1]) 293 | -------------------------------------------------------------------------------- /opensipscli/modules/trap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from opensipscli.module import Module 21 | from opensipscli.logger import logger 22 | from opensipscli.config import cfg 23 | from opensipscli import comm 24 | from threading import Thread 25 | import subprocess 26 | import shutil 27 | import os 28 | 29 | DEFAULT_PROCESS_NAME = 'opensips' 30 | 31 | class trap(Module): 32 | 33 | def get_process_name(self): 34 | if cfg.exists("process_name"): 35 | return cfg.get("process_name") 36 | else: 37 | return DEFAULT_PROCESS_NAME 38 | 39 | def get_pids(self): 40 | try: 41 | mi_pids = comm.execute('ps') 42 | self.pids = [str(pid['PID']) for pid in mi_pids['Processes']] 43 | info = ["Process ID={} PID={} Type={}". 44 | format(pid['ID'], pid['PID'], pid['Type']) 45 | for pid in mi_pids['Processes']] 46 | self.process_info = "\n".join(info) 47 | except: 48 | self.pids = [] 49 | 50 | def get_gdb_output(self, pid): 51 | if os.path.islink("/proc/{}/exe".format(pid)): 52 | # get process line of pid 53 | process = os.readlink("/proc/{}/exe".format(pid)) 54 | else: 55 | logger.error("could not find OpenSIPS process {} running on local machine".format(pid)) 56 | return -1 57 | # Check if process is opensips (can be different if CLI is running on another host) 58 | path, filename = os.path.split(process) 59 | process_name = self.get_process_name() 60 | if filename != process_name: 61 | logger.error("process ID {}/{} is not OpenSIPS process".format(pid, filename)) 62 | return -1 63 | logger.debug("Dumping backtrace for {} pid {}".format(process, pid)) 64 | cmd = ["gdb", process, pid, "-batch", "--eval-command", "bt full"] 65 | out = subprocess.check_output(cmd) 66 | if len(out) != 0: 67 | self.gdb_outputs[pid] = out.decode() 68 | 69 | def do_trap(self, params, modifiers): 70 | 71 | self.pids = [] 72 | self.gdb_outputs = {} 73 | self.process_info = "" 74 | 75 | trap_file = cfg.get("trap_file") 76 | process_name = self.get_process_name() 77 | 78 | logger.info("Trapping {} in {}".format(process_name, trap_file)) 79 | if params and len(params) > 0: 80 | self.pids = params 81 | else: 82 | thread = Thread(target=self.get_pids) 83 | thread.start() 84 | thread.join(timeout=1) 85 | if len(self.pids) == 0: 86 | logger.warning("could not get OpenSIPS pids through MI!") 87 | try: 88 | ps_pids = subprocess.check_output(["pidof", process_name]) 89 | self.pids = ps_pids.decode().split() 90 | except: 91 | logger.warning("could not find any OpenSIPS running!") 92 | self.pids = [] 93 | 94 | if len(self.pids) < 1: 95 | logger.error("could not find OpenSIPS' pids") 96 | return -1 97 | 98 | logger.debug("Dumping PIDs: {}".format(", ".join(self.pids))) 99 | 100 | threads = [] 101 | for pid in self.pids: 102 | thread = Thread(target=self.get_gdb_output, args=(pid,)) 103 | thread.start() 104 | threads.append(thread) 105 | 106 | for thread in threads: 107 | thread.join() 108 | 109 | if len(self.gdb_outputs) == 0: 110 | logger.error("could not get output of gdb") 111 | return -1 112 | 113 | with open(trap_file, "w") as tf: 114 | tf.write(self.process_info) 115 | for pid in self.pids: 116 | if pid not in self.gdb_outputs: 117 | logger.warning("No output from pid {}".format(pid)) 118 | continue 119 | try: 120 | procinfo = subprocess.check_output( 121 | ["ps", "--no-headers", "-ww", "-fp", pid]).decode()[:-1] 122 | except: 123 | procinfo = "UNKNOWN" 124 | 125 | tf.write("\n\n---start {} ({})\n{}". 126 | format(pid, procinfo, self.gdb_outputs[pid])) 127 | 128 | print("Trap file: {}".format(trap_file)) 129 | 130 | def __get_methods__(self): 131 | return None 132 | 133 | def __exclude__(self): 134 | valid = comm.valid() 135 | if not valid[0]: 136 | return False, valid[1] 137 | # check to see if we have gdb installed 138 | return (shutil.which("gdb") is None, None) 139 | -------------------------------------------------------------------------------- /opensipscli/modules/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | from opensipscli.module import Module 21 | from opensipscli.logger import logger 22 | from opensipscli.config import cfg 23 | from opensipscli.db import ( 24 | osdb, osdbError 25 | ) 26 | 27 | import os 28 | import getpass 29 | import hashlib 30 | 31 | DEFAULT_DB_NAME = "opensips" 32 | USER_TABLE = "subscriber" 33 | USER_NAME_COL = "username" 34 | USER_DOMAIN_COL = "domain" 35 | USER_PASS_COL = "password" 36 | USER_HA1_COL = "ha1" 37 | USER_HA1B_COL = "ha1b" 38 | USER_HA1_SHA256_COL = "ha1_sha256" 39 | USER_HA1_SHA512T256_COL = "ha1_sha512t256" 40 | USER_RPID_COL = "rpid" 41 | 42 | class user(Module): 43 | 44 | def user_db_connect(self): 45 | engine = osdb.get_db_engine() 46 | 47 | db_url = cfg.read_param(["database_user_url", "database_url"], 48 | "Please provide us the URL of the database") 49 | if db_url is None: 50 | print() 51 | logger.error("no URL specified: aborting!") 52 | return None, None 53 | 54 | db_url = osdb.set_url_driver(db_url, engine) 55 | db_name = cfg.read_param(["database_user_name", "database_name"], 56 | "Please provide the database to add user to", DEFAULT_DB_NAME) 57 | 58 | try: 59 | db = osdb(db_url, db_name) 60 | except osdbError: 61 | logger.error("failed to connect to database %s", db_name) 62 | return None, None 63 | 64 | if not db.connect(): 65 | return None, None 66 | 67 | res = db.find('version', 'table_version', {'table_name': USER_TABLE}) 68 | if not res: 69 | osips_ver = '3.2+' 70 | else: 71 | # RFC 8760 support was introduced in OpenSIPS 3.2 (table ver 8+) 72 | tb_ver = res.first()[0] 73 | if tb_ver >= 8: 74 | osips_ver = '3.2+' 75 | else: 76 | osips_ver = '3.1' 77 | 78 | return db, osips_ver 79 | 80 | def user_get_domain(self, name): 81 | s = name.split('@') 82 | if len(s) > 2: 83 | logger.warning("invalid username {}". 84 | format(name)) 85 | return None 86 | elif len(s) == 1: 87 | domain = cfg.read_param("domain", 88 | "Please provide the domain of the user") 89 | if not domain: 90 | logger.warning("no domain specified for {}". 91 | format(name)) 92 | return None 93 | return name, domain 94 | return s[0], s[1] 95 | 96 | def user_get_password(self): 97 | while True: 98 | pw1 = getpass.getpass("Please enter new password: ") 99 | pw2 = getpass.getpass("Please repeat the password: ") 100 | if pw1 != pw2: 101 | logger.warning("passwords are not the same! Please retry...") 102 | else: 103 | return pw1 104 | 105 | def user_get_ha1(self, user, domain, password): 106 | string = "{}:{}:{}".format(user, domain, password) 107 | return hashlib.md5(string.encode('utf-8')).hexdigest() 108 | 109 | def user_get_ha1b(self, user, domain, password): 110 | string = "{}@{}:{}:{}".format(user, domain, domain, password) 111 | return hashlib.md5(string.encode('utf-8')).hexdigest() 112 | 113 | def user_get_ha1_sha256(self, user, domain, password): 114 | string = "{}:{}:{}".format(user, domain, password) 115 | return hashlib.sha256(string.encode('utf-8')).hexdigest() 116 | 117 | def user_get_ha1_sha512t256(self, user, domain, password): 118 | string = "{}:{}:{}".format(user, domain, password) 119 | try: 120 | o = hashlib.new("sha512-256") 121 | except ValueError: 122 | # SHA-512/256 is only available w/ OpenSSL 1.1.1 (Sep 2018) or 123 | # newer, so let's just leave the field blank if we get an exception 124 | logger.error(("The SHA-512/256 hashing algorithm is " 125 | "apparently not available!?")) 126 | logger.error("Adding user, but with a blank '{}' column!".format( 127 | USER_HA1_SHA512T256_COL)) 128 | logger.error("Tip: installing OpenSSL 1.1.1+ should fix this") 129 | return "" 130 | 131 | o.update(string.encode('utf-8')) 132 | return o.hexdigest() 133 | 134 | def do_add(self, params=None, modifiers=None): 135 | 136 | if len(params) < 1: 137 | name = cfg.read_param(None, 138 | "Please provide the username you want to add") 139 | if not name: 140 | logger.warning("no username to add!") 141 | return -1 142 | else: 143 | name = params[0] 144 | username, domain = self.user_get_domain(name) 145 | 146 | db, osips_ver = self.user_db_connect() 147 | if not db: 148 | return -1 149 | 150 | insert_dict = { 151 | USER_NAME_COL: username, 152 | USER_DOMAIN_COL: domain 153 | } 154 | # check if the user already exists 155 | if db.entry_exists(USER_TABLE, insert_dict): 156 | logger.error("User {}@{} already exists". 157 | format(username, domain)) 158 | return -1 159 | 160 | if len(params) > 1: 161 | password = params[1] 162 | else: 163 | password = self.user_get_password() 164 | if password is None: 165 | logger.error("password not specified: cannot add user {}@{}". 166 | format(user, domain)) 167 | return -1 168 | insert_dict[USER_HA1_COL] = \ 169 | self.user_get_ha1(username, domain, password) 170 | 171 | # only populate the 'ha1b' column on 3.1 or older OpenSIPS DBs 172 | if osips_ver < '3.2': 173 | insert_dict[USER_HA1B_COL] = \ 174 | self.user_get_ha1b(username, domain, password) 175 | else: 176 | insert_dict[USER_HA1_SHA256_COL] = \ 177 | self.user_get_ha1_sha256(username, domain, password) 178 | insert_dict[USER_HA1_SHA512T256_COL] = \ 179 | self.user_get_ha1_sha512t256(username, domain, password) 180 | 181 | insert_dict[USER_PASS_COL] = \ 182 | password if cfg.getBool("plain_text_passwords") else "" 183 | 184 | db.insert(USER_TABLE, insert_dict) 185 | logger.info("Successfully added {}@{}".format(username, domain)) 186 | 187 | db.destroy() 188 | return True 189 | 190 | def do_password(self, params=None, modifiers=None): 191 | 192 | if len(params) < 1: 193 | name = cfg.read_param(None, 194 | "Please provide the username to change the password for") 195 | if not name: 196 | logger.error("empty username") 197 | return -1 198 | else: 199 | name = params[0] 200 | username, domain = self.user_get_domain(name) 201 | 202 | db, osips_ver = self.user_db_connect() 203 | if not db: 204 | return -1 205 | 206 | user_dict = { 207 | USER_NAME_COL: username, 208 | USER_DOMAIN_COL: domain 209 | } 210 | # check if the user already exists 211 | if not db.entry_exists(USER_TABLE, user_dict): 212 | logger.warning("User {}@{} does not exist". 213 | format(username, domain)) 214 | return -1 215 | 216 | if len(params) > 1: 217 | password = params[1] 218 | else: 219 | password = self.user_get_password() 220 | if password is None: 221 | logger.error("Password not specified: " + 222 | "cannot change passowrd for user {}@{}". 223 | format(user, domain)) 224 | return -1 225 | plain_text_pw = cfg.getBool("plain_text_passwords") 226 | update_dict = { 227 | USER_HA1_COL: self.user_get_ha1(username, domain, password), 228 | USER_PASS_COL: password if plain_text_pw else "" 229 | } 230 | 231 | if osips_ver < '3.2': 232 | update_dict[USER_HA1B_COL] = self.user_get_ha1b( 233 | username, domain, password) 234 | 235 | db.update(USER_TABLE, update_dict, user_dict) 236 | logger.info("Successfully changed password for {}@{}". 237 | format(username, domain)) 238 | db.destroy() 239 | return True 240 | 241 | def do_delete(self, params=None, modifiers=None): 242 | 243 | if len(params) < 1: 244 | name = cfg.read_param(None, 245 | "Please provide the username you want to delete") 246 | if not name: 247 | logger.warning("no username to delete!") 248 | return -1 249 | else: 250 | name = params[0] 251 | username, domain = self.user_get_domain(name) 252 | 253 | db, _ = self.user_db_connect() 254 | if not db: 255 | return -1 256 | 257 | delete_dict = { 258 | USER_NAME_COL: username, 259 | USER_DOMAIN_COL: domain 260 | } 261 | # check if the user already exists 262 | if not db.entry_exists(USER_TABLE, delete_dict): 263 | logger.error("User {}@{} does not exist". 264 | format(username, domain)) 265 | return -1 266 | 267 | db.delete(USER_TABLE, delete_dict) 268 | logger.info("Successfully deleted {}@{}".format(username, domain)) 269 | 270 | db.destroy() 271 | return True 272 | 273 | def __exclude__(self): 274 | if cfg.exists("dababase_user_url"): 275 | db_url = cfg.get("database_user_url") 276 | elif cfg.exists("database_url"): 277 | db_url = cfg.get("database_url") 278 | else: 279 | return (not osdb.has_sqlalchemy(), None) 280 | return (not osdb.has_dialect(osdb.get_dialect(db_url)), None) 281 | 282 | -------------------------------------------------------------------------------- /opensipscli/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | __version__ = '0.3.1' 21 | 22 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 23 | -------------------------------------------------------------------------------- /packaging/debian/.gitignore: -------------------------------------------------------------------------------- 1 | /usr/ 2 | /DEBIAN/ 3 | /.pybuild/ 4 | /files 5 | /opensips-cli/ 6 | /debhelper-build-stamp 7 | /opensips-cli.substvars 8 | /opensips-cli\.*debhelper* 9 | -------------------------------------------------------------------------------- /packaging/debian/changelog: -------------------------------------------------------------------------------- 1 | opensips-cli (0.3.1-1) stable; urgency=low 2 | 3 | * Minor Public Release. 4 | 5 | -- Razvan Crainea Fri, 24 Feb 2023 11:15:04 +0300 6 | 7 | -------------------------------------------------------------------------------- /packaging/debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /packaging/debian/control: -------------------------------------------------------------------------------- 1 | Source: opensips-cli 2 | Section: python 3 | Priority: optional 4 | Maintainer: Razvan Crainea 5 | Build-Depends: debhelper (>= 9), dh-python, python3-dev, default-libmysqlclient-dev | libmysqlclient-dev, python3-sqlalchemy, python3-opensips 6 | Standards-Version: 3.9.8 7 | Homepage: https://github.com/OpenSIPS/opensips-cli 8 | 9 | Package: opensips-cli 10 | Architecture: all 11 | Multi-Arch: foreign 12 | Depends: python3, ${misc:Depends}, ${python3:Depends}, python3-sqlalchemy, python3-sqlalchemy-utils, python3-openssl, python3-mysqldb, python3-pymysql, python3-opensips 13 | Description: Interactive command-line tool for OpenSIPS 3.0+ 14 | This package contains the OpenSIPS CLI tool, an interactive command line tool 15 | that can be used to control and monitor OpenSIPS 3.0+ servers. 16 | . 17 | OpenSIPS is a very fast and flexible SIP (RFC3261) 18 | server. Written entirely in C, OpenSIPS can handle thousands calls 19 | per second even on low-budget hardware. 20 | . 21 | C Shell-like scripting language provides full control over the server's 22 | behaviour. Its modular architecture allows only required functionality to be 23 | loaded. 24 | . 25 | Among others, the following modules are available: Digest Authentication, CPL 26 | scripts, Instant Messaging, MySQL/PostgreSQL support, Presence Agent, Radius 27 | Authentication, Record Routing, SMS Gateway, Jabber/XMPP Gateway, Transaction 28 | Module, SIP Registrar and User Location, Load Balancing/Dispatching/LCR, 29 | XMLRPC Interface. 30 | -------------------------------------------------------------------------------- /packaging/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: opensips-cli 3 | Source: https://github.com/OpenSIPS/opensips-cli 4 | 5 | Files: * 6 | Copyright: 2019, OpenSIPS Project 7 | License: GPL-3+ 8 | 9 | License: GPL-3+ 10 | This program is free software; you can redistribute it 11 | and/or modify it under the terms of the GNU General Public 12 | License as published by the Free Software Foundation; either 13 | version 3 of the License, or (at your option) any later 14 | version. 15 | . 16 | This program is distributed in the hope that it will be 17 | useful, but WITHOUT ANY WARRANTY; without even the implied 18 | warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 19 | PURPOSE. See the GNU General Public License for more 20 | details. 21 | . 22 | You should have received a copy of the GNU General Public 23 | License along with this package; if not, write to the Free 24 | Software Foundation, Inc., 51 Franklin St, Fifth Floor, 25 | Boston, MA 02110-1301 USA 26 | . 27 | On Debian systems, the full text of the GNU General Public 28 | License version 2 can be found in the file 29 | `/usr/share/common-licenses/GPL-3'. 30 | -------------------------------------------------------------------------------- /packaging/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | VERSION=$(shell python -Bc 'import sys; sys.path.append("."); from opensipscli.version import __version__; print(__version__)') 4 | NAME=opensips-cli 5 | 6 | %: 7 | dh $@ --with python3 --buildsystem=pybuild 8 | 9 | .PHONY: tar 10 | tar: 11 | tar --transform 's,^\.,$(NAME),' \ 12 | --exclude=.git \ 13 | --exclude=.gitignore \ 14 | --exclude=*.swp \ 15 | --exclude=build \ 16 | -czf ../$(NAME)_$(VERSION).orig.tar.gz . 17 | -------------------------------------------------------------------------------- /packaging/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /packaging/debian/watch: -------------------------------------------------------------------------------- 1 | version=3 2 | 3 | opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/opensips-cli-$1\.tar\.gz/ \ 4 | https://github.com/OpenSIPS/opensips-cli/tags .*/v?(\d\S*)\.tar\.gz 5 | 6 | https://github.com/OpenSIPS/opensips-cli/releases /OpenSIPS/opensips-cli/archive/(.+)\.tar\.gz 7 | -------------------------------------------------------------------------------- /packaging/redhat_fedora/opensips-cli.spec: -------------------------------------------------------------------------------- 1 | Summary: Interactive command-line tool for OpenSIPS 3.0+ 2 | Name: opensips-cli 3 | Version: 0.3.1 4 | Release: 0%{?dist} 5 | License: GPL-3+ 6 | Group: System Environment/Daemons 7 | Source0: Source0: http://download.opensips.org/cli/%{name}-%{version}.tar.gz 8 | URL: http://opensips.org 9 | 10 | BuildArch: noarch 11 | 12 | BuildRequires: python3-devel 13 | BuildRequires: python3-setuptools 14 | BuildRequires: python3-rpm-macros 15 | BuildRequires: mysql-devel 16 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 17 | 18 | AutoReqProv: no 19 | 20 | Requires: python3 21 | %if 0%{?rhel} == 7 22 | Requires: python36-sqlalchemy 23 | Requires: python36-mysql 24 | Requires: python36-pyOpenSSL 25 | %else 26 | Requires: python3-sqlalchemy 27 | Requires: python3-mysqlclient 28 | Requires: python3-pyOpenSSL 29 | %endif 30 | Requires: python3-opensips 31 | 32 | %description 33 | This package contains the OpenSIPS CLI tool, an interactive command line tool 34 | that can be used to control and monitor OpenSIPS 3.0+ servers. 35 | . 36 | OpenSIPS is a very fast and flexible SIP (RFC3261) 37 | server. Written entirely in C, OpenSIPS can handle thousands calls 38 | per second even on low-budget hardware. 39 | . 40 | C Shell-like scripting language provides full control over the server's 41 | behaviour. Its modular architecture allows only required functionality to be 42 | loaded. 43 | . 44 | Among others, the following modules are available: Digest Authentication, CPL 45 | scripts, Instant Messaging, MySQL support, Presence Agent, Radius 46 | Authentication, Record Routing, SMS Gateway, Jabber/XMPP Gateway, Transaction 47 | Module, Registrar and User Location, Load Balaning/Dispatching/LCR, 48 | XMLRPC Interface. 49 | 50 | %prep 51 | %autosetup -n %{name}-%{version} 52 | 53 | %build 54 | %py3_build 55 | 56 | %install 57 | %py3_install 58 | 59 | %clean 60 | rm -rf $RPM_BUILD_ROOT 61 | 62 | %files 63 | %{_bindir}/opensips-cli 64 | %{python3_sitelib}/opensipscli/* 65 | %{python3_sitelib}/opensipscli-*.egg-info 66 | %doc README.md 67 | %doc docs/* 68 | %doc etc/default.cfg 69 | %license LICENSE 70 | 71 | %changelog 72 | * Thu Aug 27 2020 Liviu Chircu - 0.1-2 73 | - Update package summary. 74 | * Fri Jan 3 2020 Nick Altmann - 0.1-1 75 | - Initial spec. 76 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | """ 21 | Installs OpenSIPS Command Line Interface 22 | """ 23 | 24 | import os 25 | 26 | try: 27 | from setuptools import setup, Command 28 | except ImportError: 29 | from distutils.core import setup, Command 30 | 31 | from opensipscli import version 32 | 33 | 34 | here = os.path.abspath(os.path.dirname(__file__)) 35 | 36 | with open(os.path.join(here, 'README.md'), encoding='utf-8') as fh: 37 | long_description = fh.read() 38 | 39 | class CleanCommand(Command): 40 | user_options = [ 41 | ('all', None, '(Compatibility with original clean command)') 42 | ] 43 | def initialize_options(self): 44 | self.all = False 45 | def finalize_options(self): 46 | pass 47 | def run(self): 48 | os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info') 49 | 50 | setup( 51 | name = "opensipscli", 52 | version = version.__version__, 53 | author = "OpenSIPS Project", 54 | author_email = "project@opensips.org", 55 | maintainer = "Razvan Crainea", 56 | maintainer_email = "razvan@opensips.org", 57 | description = "OpenSIPS Command Line Interface", 58 | long_description = long_description, 59 | long_description_content_type='text/markdown', 60 | url = "https://github.com/OpenSIPS/opensips-cli", 61 | download_url = "https://github.com/OpenSIPS/opensips-cli/archive/master.zip", 62 | packages = [ 63 | "opensipscli", 64 | "opensipscli.modules", 65 | "opensipscli.libs" 66 | ], 67 | install_requires=[ 68 | 'opensips', 69 | 'mysqlclient<1.4.0rc1', 70 | 'sqlalchemy>=1.3.3,<2', 71 | 'sqlalchemy-utils' 72 | ], 73 | classifiers = [ 74 | "Programming Language :: Python :: 3", 75 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 76 | "Operating System :: OS Independent", 77 | ], 78 | scripts = [ 79 | "bin/opensips-cli" 80 | ], 81 | project_urls = { 82 | "Source Code": "https://github.com/OpenSIPS/opensips-cli", 83 | "Issues Tracker": "https://github.com/OpenSIPS/opensips-cli/issues", 84 | }, 85 | cmdclass={ 86 | 'clean': CleanCommand, 87 | } 88 | ) 89 | 90 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 91 | 92 | -------------------------------------------------------------------------------- /test/alltests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from opensipscli.db import make_url 4 | 5 | class OpenSIPSCLIUnitTests(unittest.TestCase): 6 | def testMakeURL(self): 7 | u = make_url('x://') 8 | assert repr(u) == 'x://' 9 | assert u.drivername == 'x' 10 | assert all(a is None for a in 11 | (u.username, u.password, u.host, u.port, u.database)) 12 | 13 | u.database = 'db' 14 | assert repr(u) == str(u) == 'x:///db' 15 | 16 | u.port = 12 17 | assert repr(u) == str(u) == 'x://:12/db' 18 | 19 | u.host = 'host' 20 | assert repr(u) == str(u) == 'x://host:12/db' 21 | 22 | u.password = 'pass' 23 | assert repr(u) == str(u) == 'x://host:12/db' 24 | 25 | u.username = 'user' 26 | assert repr(u) == 'x://user:***@host:12/db' 27 | assert str(u) == 'x://user:pass@host:12/db' 28 | 29 | u = make_url('mysql://opensips:opensipsrw@localhost/opensips') 30 | assert repr(u) == 'mysql://opensips:***@localhost/opensips' 31 | assert str(u) == 'mysql://opensips:opensipsrw@localhost/opensips' 32 | 33 | u = make_url('mysql://opensips:opensipsrw@localhost') 34 | assert repr(u) == 'mysql://opensips:***@localhost' 35 | assert str(u) == 'mysql://opensips:opensipsrw@localhost' 36 | 37 | u = make_url('mysql://root@localhost') 38 | assert repr(u) == 'mysql://root@localhost' 39 | assert str(u) == 'mysql://root@localhost' 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /test/test-database.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | CLI_CFG=/tmp/.__cli.cfg 21 | DB_NAME=_opensips_cli_test 22 | 23 | MYSQL_URL=mysql://opensips:opensipsrw@localhost 24 | PGSQL_URL=postgresql://opensips:opensipsrw@localhost 25 | 26 | TESTS=( 27 | test_mysql_drop_1_prompt 28 | test_mysql_drop_0_prompts 29 | test_mysql_create_0_prompts 30 | 31 | test_pgsql_drop_1_prompt 32 | test_pgsql_drop_0_prompts 33 | test_pgsql_create_0_prompts 34 | ) 35 | 36 | 37 | test_mysql_drop_1_prompt() { test_db_drop_1_prompt $MYSQL_URL; } 38 | test_mysql_drop_0_prompts() { test_db_drop_0_prompts $MYSQL_URL; } 39 | test_mysql_create_0_prompts() { test_db_create_0_prompts $MYSQL_URL; } 40 | 41 | test_pgsql_drop_1_prompt() { test_db_drop_1_prompt $PGSQL_URL; } 42 | test_pgsql_drop_0_prompts() { test_db_drop_0_prompts $PGSQL_URL; } 43 | test_pgsql_create_0_prompts() { test_db_create_0_prompts $PGSQL_URL; } 44 | 45 | 46 | test_db_drop_1_prompt() { 47 | create_db $DB_NAME $1 48 | 49 | cat >$CLI_CFG </dev/null 59 | } 60 | 61 | 62 | test_db_drop_0_prompts() { 63 | create_db $DB_NAME $1 64 | 65 | cat >$CLI_CFG <$CLI_CFG <$CLI_CFG </dev/null 95 | 96 | drop_db $DB_NAME $1 97 | } 98 | 99 | 100 | create_db() { 101 | cat >$CLI_CFG </dev/null 109 | } 110 | 111 | drop_db() { 112 | cat >$CLI_CFG </dev/null 121 | set -e 122 | } 123 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## 3 | ## This file is part of OpenSIPS CLI 4 | ## (see https://github.com/OpenSIPS/opensips-cli). 5 | ## 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | 20 | TEST_MODULES=( 21 | test-database.sh 22 | ) 23 | 24 | for module in ${TEST_MODULES[@]}; do 25 | . $module 26 | 27 | for test_func in ${TESTS[@]}; do 28 | echo -n "$test_func ... " 29 | 30 | (set -e; $test_func; set +e) 31 | 32 | if [ $? == 0 ]; then 33 | echo "PASSED" 34 | else 35 | echo "FAILED" 36 | fi 37 | done 38 | done 39 | 40 | --------------------------------------------------------------------------------