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