opensipscli 0.4.1__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {opensipscli-0.4.1 → opensipscli-0.4.2}/.github/workflows/pypi.yml +1 -1
  2. {opensipscli-0.4.1 → opensipscli-0.4.2}/PKG-INFO +1 -1
  3. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/modules/mi.md +6 -1
  4. opensipscli-0.4.2/docs/modules/tls.md +191 -0
  5. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/cli.py +10 -3
  6. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/db.py +13 -2
  7. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/mi.py +29 -3
  8. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/tls.py +337 -0
  9. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/version.py +1 -1
  10. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/changelog +6 -0
  11. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/redhat_fedora/opensips-cli.spec +3 -1
  12. {opensipscli-0.4.1 → opensipscli-0.4.2}/test/alltests.py +12 -0
  13. opensipscli-0.4.1/docs/modules/tls.md +0 -99
  14. {opensipscli-0.4.1 → opensipscli-0.4.2}/.github/workflows/docker-push-image.yml +0 -0
  15. {opensipscli-0.4.1 → opensipscli-0.4.2}/.github/workflows/docker-readme.yml +0 -0
  16. {opensipscli-0.4.1 → opensipscli-0.4.2}/.gitignore +0 -0
  17. {opensipscli-0.4.1 → opensipscli-0.4.2}/LICENSE +0 -0
  18. {opensipscli-0.4.1 → opensipscli-0.4.2}/README.md +0 -0
  19. {opensipscli-0.4.1 → opensipscli-0.4.2}/SECURITY.md +0 -0
  20. {opensipscli-0.4.1 → opensipscli-0.4.2}/bin/opensips-cli +0 -0
  21. {opensipscli-0.4.1 → opensipscli-0.4.2}/docker/Dockerfile +0 -0
  22. {opensipscli-0.4.1 → opensipscli-0.4.2}/docker/Makefile +0 -0
  23. {opensipscli-0.4.1 → opensipscli-0.4.2}/docker/docker.md +0 -0
  24. {opensipscli-0.4.1 → opensipscli-0.4.2}/docker/run.sh +0 -0
  25. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/INSTALLATION.md +0 -0
  26. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/modules/database.md +0 -0
  27. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/modules/diagnose.md +0 -0
  28. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/modules/instance.md +0 -0
  29. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/modules/trace.md +0 -0
  30. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/modules/trap.md +0 -0
  31. {opensipscli-0.4.1 → opensipscli-0.4.2}/docs/modules/user.md +0 -0
  32. {opensipscli-0.4.1 → opensipscli-0.4.2}/etc/default.cfg +0 -0
  33. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/__init__.py +0 -0
  34. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/args.py +0 -0
  35. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/comm.py +0 -0
  36. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/config.py +0 -0
  37. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/defaults.py +0 -0
  38. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/libs/__init__.py +0 -0
  39. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/libs/sqlalchemy_utils.py +0 -0
  40. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/logger.py +0 -0
  41. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/main.py +0 -0
  42. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/module.py +0 -0
  43. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/__init__.py +0 -0
  44. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/database.py +0 -0
  45. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/diagnose.py +0 -0
  46. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/instance.py +0 -0
  47. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/trace.py +0 -0
  48. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/trap.py +0 -0
  49. {opensipscli-0.4.1 → opensipscli-0.4.2}/opensipscli/modules/user.py +0 -0
  50. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/.gitignore +0 -0
  51. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/compat +0 -0
  52. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/control +0 -0
  53. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/copyright +0 -0
  54. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/rules +0 -0
  55. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/source/format +0 -0
  56. {opensipscli-0.4.1 → opensipscli-0.4.2}/packaging/debian/watch +0 -0
  57. {opensipscli-0.4.1 → opensipscli-0.4.2}/pyproject.toml +0 -0
  58. {opensipscli-0.4.1 → opensipscli-0.4.2}/setup.cfg +0 -0
  59. {opensipscli-0.4.1 → opensipscli-0.4.2}/setup.py +0 -0
  60. {opensipscli-0.4.1 → opensipscli-0.4.2}/test/test-database.sh +0 -0
  61. {opensipscli-0.4.1 → opensipscli-0.4.2}/test/test.sh +0 -0
@@ -69,7 +69,7 @@ jobs:
69
69
  name: python-package-distributions
70
70
  path: dist/
71
71
  - name: Sign the dists with Sigstore
72
- uses: sigstore/gh-action-sigstore-python@v2.1.1
72
+ uses: sigstore/gh-action-sigstore-python@v3.5.0
73
73
  with:
74
74
  inputs: >-
75
75
  ./dist/*.tar.gz
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opensipscli
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: OpenSIPS Command Line Interface
5
5
  Project-URL: Homepage, https://github.com/OpenSIPS/opensips-cli
6
6
  Project-URL: Source, https://github.com/OpenSIPS/opensips-cli
@@ -7,9 +7,14 @@ commands and display the result in the console.
7
7
 
8
8
  This module exports all the commands that are exported by the OpenSIPS
9
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
10
+ running the OpenSIPS MI `which` command. It also loads MI aliases using the
11
+ `which_aliases` command. When running a command, it returns
11
12
  the raw, unprocessed data from OpenSIPS.
12
13
 
14
+ Aliases are accepted when executing commands, but are not included in command
15
+ completion suggestions. Ambiguous aliases are ignored and must be invoked by
16
+ their canonical command name.
17
+
13
18
  Commands using this module can be run in two manners:
14
19
  * using *positional* parameters: this is similar to the old way the
15
20
  `opensipsctl` tool was working: the parameters passed to the function are
@@ -0,0 +1,191 @@
1
+ # OpenSIPS CLI - TLS module
2
+
3
+ Using the `tls` module, you can generate TLS certificates and private keys.
4
+
5
+ The module has the following 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
+ * `add` - adds a new TLS domain to the `tls_mgm` table.
11
+ * `update` - changes the columns of an existing TLS domain.
12
+ * `list` - lists the TLS domains provisioned in the `tls_mgm` table.
13
+ * `show` - prints the columns of a TLS domain.
14
+ * `delete` - removes a TLS domain from the `tls_mgm` table.
15
+
16
+ The `add`, `update`, `list`, `show` and `delete` subcommands provision the
17
+ `tls_mgm` module over the database, where the certificate, private key and CA
18
+ list are stored as BLOB values rather than as paths to files. A TLS domain is
19
+ identified by its name and its type (`server` or `client`), both passed as
20
+ arguments:
21
+ ```
22
+ opensips-cli -x tls delete a.example.org server
23
+ ```
24
+ The domain and its type may also be given by name, in which case they can
25
+ appear anywhere among the other columns:
26
+ ```
27
+ opensips-cli -x tls delete domain=a.example.org type=server
28
+ ```
29
+ Giving one of them both ways at once is an error. The commands addressing a
30
+ single domain ask for whatever is left out; `add` and `show` default the
31
+ type to `server`, while `update` and `delete` have no default and keep
32
+ asking until one is given, so that they cannot change a different domain than
33
+ the intended one.
34
+
35
+ `add` and `update` take the remaining `tls_mgm` columns as `column=value`
36
+ arguments, in any order and after the domain and the type:
37
+ ```
38
+ opensips-cli -x tls add a.example.org server method=TLSv1_2 verify_cert=1
39
+ ```
40
+ The settable columns are `match_ip_address`, `match_sip_domain`, `method`,
41
+ `verify_cert`, `require_cert`, `certificate`, `private_key`, `crl_check_all`,
42
+ `crl_dir`, `ca_list`, `ca_dir`, `cipher_list`, `dh_params` and `ec_curve`. A
43
+ column that is not given is left to its default in the database schema;
44
+ `update` only changes the columns it is given.
45
+
46
+ The `certificate`, `private_key`, `ca_list` and `dh_params` columns hold PEM
47
+ content, so their value is the path of the file holding it, and that file is
48
+ read and stored in the table:
49
+ ```
50
+ opensips-cli -x tls add a.example.org server \
51
+ certificate=/etc/opensips/tls/user/user-cert.pem \
52
+ private_key=/etc/opensips/tls/user/user-privkey.pem
53
+ ```
54
+ Every other column is stored as the value it is given, paths included: for
55
+ example, `ca_list` reads the file it points to, while `ca_dir` and `crl_dir`
56
+ keep the directory as such, which is what `tls_mgm` expects of them.
57
+
58
+ After every change, the `tls_reload` MI command is issued so that a running
59
+ OpenSIPS picks up the new domains. If OpenSIPS cannot be reached, a warning is
60
+ logged and the domains are loaded at the next restart.
61
+
62
+ ## Configuration
63
+
64
+ Certificates and private keys can be customized using the following settings:
65
+
66
+ List of `opensips-cli.cfg` settings for configuring self-signed CA certificates:
67
+
68
+ * tls_ca_dir - output directory where the cert and key will be written to
69
+ * tls_ca_cert_file - output certificate file path, within `tls_ca_dir`
70
+ * tls_ca_key_file - output private key file path, within `tls_ca_dir`
71
+ * tls_ca_overwrite - set this to "y" in order to overwrite existing files
72
+ * tls_ca_common_name - the address of the website (e.g. "opensips.org")
73
+ * tls_ca_country - the initials of the country (e.g. "RO")
74
+ * tls_ca_state - the state (e.g. "Bucharest")
75
+ * tls_ca_locality - the city (e.g. "Bucharest")
76
+ * tls_ca_organisation - the name of the organisation (e.g. "OpenSIPS")
77
+ * tls_ca_organisational_unit - the organisational unit (e.g. "Project")
78
+ * tls_ca_notafter - the validity period, in seconds (e.g. 315360000)
79
+ * tls_ca_key_size - the size of the RSA key, in bits (e.g. 4096)
80
+ * tls_ca_md - the digest algorithm to use for signing (e.g. SHA256)
81
+
82
+ List of `opensips-cli.cfg` settings for configuring user certificates:
83
+
84
+ * tls_user_dir - output directory where the cert and key will be written to
85
+ * tls_user_cert_file - output certificate file path, within `tls_user_dir`
86
+ * tls_user_key_file - output private key file path, within `tls_user_dir`
87
+ * tls_user_calist_file - output CA list file path, within `tls_user_dir`
88
+ * tls_user_overwrite - set this to "y" in order to overwrite existing files
89
+ * tls_user_cacert - path to the input CA certificate
90
+ * tls_user_cakey - path to the input CA private key
91
+ * tls_user_common_name - the address of the website (e.g. "www.opensips.org")
92
+ * tls_user_country - the initials of the country (e.g. "RO")
93
+ * tls_user_state - the state (e.g. "Bucharest")
94
+ * tls_user_locality - the city (e.g. "Bucharest")
95
+ * tls_user_organisation - the name of the organisation (e.g. "OpenSIPS")
96
+ * tls_user_organisational_unit - the organisational unit (e.g. "Project")
97
+ * tls_user_notafter - the validity period, in seconds (e.g. 315360000)
98
+ * tls_user_key_size - the size of the RSA key, in bits (e.g. 4096)
99
+ * tls_user_md - the digest algorithm to use for signing (e.g. SHA256)
100
+
101
+ List of `opensips-cli.cfg` settings for the `tls_mgm` database subcommands:
102
+
103
+ * database_tls_url - URL of the database holding the `tls_mgm` table; falls
104
+ back to `database_url`
105
+ * database_tls_name - name of the database; falls back to `database_name`
106
+
107
+
108
+ ## Examples
109
+
110
+ To create a self-signed certificate and a private key for rootCA, enter this snippet:
111
+ ```
112
+ opensips-cli -x tls rootCA
113
+ ```
114
+ Configuration file example for rootCA:
115
+ ```
116
+ [default]
117
+ tls_ca_dir: /etc/opensips/tls/rootCA
118
+ tls_ca_cert_file: cacert.pem
119
+ tls_ca_key_file: private/cakey.pem
120
+ tls_ca_overwrite: yes
121
+ tls_ca_common_name: opensips.org
122
+ tls_ca_country: RO
123
+ tls_ca_state: Bucharest
124
+ tls_ca_locality: Bucharest
125
+ tls_ca_organisation: OpenSIPS
126
+ tls_ca_organisational_unit: Project
127
+ tls_ca_notafter: 315360000
128
+ tls_ca_key_size: 4096
129
+ tls_ca_md: SHA256
130
+ ```
131
+
132
+ To create a user certificate signed by the above rootCA, along with a private
133
+ key and a CA list (chain of trust) file:
134
+ ```
135
+ opensips-cli -x tls userCERT
136
+ ```
137
+ Configuration file example for userCERT:
138
+ ```
139
+ [default]
140
+ tls_user_dir: /etc/opensips/tls/user
141
+ tls_user_cert_file: user-cert.pem
142
+ tls_user_key_file: user-privkey.pem
143
+ tls_user_calist_file: user-calist.pem
144
+ tls_user_overwrite: yes
145
+ tls_user_cacert: /etc/opensips/tls/rootCA/cacert.pem
146
+ tls_user_cakey: /etc/opensips/tls/rootCA/private/cakey.pem
147
+ tls_user_common_name: www.opensips.org
148
+ tls_user_country: RO
149
+ tls_user_state: Bucharest
150
+ tls_user_locality: Bucharest
151
+ tls_user_organisation: OpenSIPS
152
+ tls_user_organisational_unit: Project
153
+ tls_user_notafter: 315360000
154
+ tls_user_key_size: 4096
155
+ tls_user_md: SHA256
156
+ ```
157
+
158
+ To provision the certificate generated above as a TLS domain in the database:
159
+ ```
160
+ opensips-cli -x tls add a.example.org server \
161
+ certificate=/etc/opensips/tls/user/user-cert.pem \
162
+ private_key=/etc/opensips/tls/user/user-privkey.pem \
163
+ ca_list=/etc/opensips/tls/user/user-calist.pem
164
+ ```
165
+ Certificates issued by a public CA are provisioned the same way:
166
+ ```
167
+ opensips-cli -x tls add a.example.org server \
168
+ certificate=/etc/letsencrypt/live/a.example.org/fullchain.pem \
169
+ private_key=/etc/letsencrypt/live/a.example.org/privkey.pem
170
+ ```
171
+ Configuration file example for the `tls_mgm` database subcommands:
172
+ ```
173
+ [default]
174
+ database_url: mysql://opensips:opensipsrw@localhost
175
+ database_name: opensips
176
+ ```
177
+
178
+ To renew the certificate of a domain, or to change any of its other columns:
179
+ ```
180
+ opensips-cli -x tls update a.example.org server \
181
+ certificate=/etc/letsencrypt/live/a.example.org/fullchain.pem \
182
+ private_key=/etc/letsencrypt/live/a.example.org/privkey.pem
183
+ opensips-cli -x tls update a.example.org server cipher_list=HIGH
184
+ ```
185
+
186
+ To inspect and remove the provisioned domains:
187
+ ```
188
+ opensips-cli -x tls list
189
+ opensips-cli -x tls show a.example.org server
190
+ opensips-cli -x tls delete a.example.org server
191
+ ```
@@ -29,7 +29,6 @@ from opensipscli import comm
29
29
  from opensipscli import defaults
30
30
  from opensipscli.config import cfg
31
31
  from opensipscli.logger import logger
32
- from opensipscli.modules import *
33
32
 
34
33
  class OpenSIPSCLI(cmd.Cmd, object):
35
34
  """
@@ -121,6 +120,10 @@ class OpenSIPSCLI(cmd.Cmd, object):
121
120
  self.current_instance = instance
122
121
  self.update_logger()
123
122
 
123
+ # import the modules only now, so that their import-time messages
124
+ # honor the logging level
125
+ importlib.import_module("opensipscli.modules")
126
+
124
127
  # Update the intro and prompt
125
128
  self.intro = cfg.get('prompt_intro')
126
129
  self.prompt = '(%s): ' % cfg.get('prompt_name')
@@ -175,7 +178,11 @@ class OpenSIPSCLI(cmd.Cmd, object):
175
178
  continue
176
179
  logger.debug("Loaded module '{}'".format(name))
177
180
  imod = mod()
178
- self.modules[name] = (imod, mod.__get_methods__(imod))
181
+ methods = mod.__get_methods__(imod)
182
+ aliases = {}
183
+ if hasattr(imod, '__get_aliases__'):
184
+ aliases = imod.__get_aliases__() or {}
185
+ self.modules[name] = (imod, methods, aliases)
179
186
 
180
187
  def history_write(self):
181
188
  """
@@ -419,7 +426,7 @@ class OpenSIPSCLI(cmd.Cmd, object):
419
426
  logger.error("module '{}' expects the following commands: {}".
420
427
  format(module, ", ".join(mod[1])))
421
428
  return -1
422
- elif cmd and not cmd in mod[1]:
429
+ elif cmd and cmd not in mod[1] and cmd not in mod[2]:
423
430
  logger.error("no command '{}' in module '{}'".
424
431
  format(cmd, module))
425
432
  return -1
@@ -36,17 +36,26 @@ try:
36
36
  #from sqlalchemy.engine.url import make_url
37
37
 
38
38
  sqlalchemy_available = True
39
+ sqlalchemy_import_error = None
39
40
  logger.debug("SQLAlchemy version: %s", sqlalchemy.__version__)
40
41
  # always use the vendored shim — the system sqlalchemy-utils package
41
42
  # varies a lot across distros (0.36.x ships broken database_exists for
42
43
  # PostgreSQL on Bullseye; 0.41.x is required for SA 2.0); the shim is
43
44
  # tested against SA 1.3–2.0 and behaves consistently
44
45
  from opensipscli.libs import sqlalchemy_utils
46
+ except ImportError as e:
47
+ # keep the cause around so callers can report it when the database
48
+ # is actually needed
49
+ logger.info("sqlalchemy not available: %s", e)
50
+ sqlalchemy_available = False
51
+ sqlalchemy_import_error = e
52
+
53
+ try:
45
54
  import pymysql
46
55
  pymysql.install_as_MySQLdb()
47
56
  except ImportError:
48
- logger.info("sqlalchemy not available!")
49
- sqlalchemy_available = False
57
+ # only needed for mysql:// URLs; the other backends work without it
58
+ pass
50
59
 
51
60
  SUPPORTED_BACKENDS = [
52
61
  "mysql",
@@ -767,6 +776,8 @@ class osdb(object):
767
776
  check for support of a given database dialect via SQLAlchemy
768
777
  """
769
778
  # TODO: do this only for SQLAlchemy
779
+ if not sqlalchemy_available:
780
+ return False
770
781
  try:
771
782
  sqlalchemy.create_engine('{}://'.format(dialect))
772
783
  except sqlalchemy.exc.NoSuchModuleError:
@@ -64,6 +64,9 @@ MI_MODIFIERS = [ "-j" ]
64
64
 
65
65
  class mi(Module):
66
66
 
67
+ def __init__(self):
68
+ self.aliases = {}
69
+
67
70
  def print_pretty_print(self, result):
68
71
  print(json.dumps(result, indent=4))
69
72
 
@@ -150,11 +153,34 @@ class mi(Module):
150
153
  new_params.append(params[MI_ARRAY_PARAMS_COMMANDS[cmd][0]:])
151
154
  return new_params
152
155
 
156
+ def __get_aliases__(self):
157
+ result = comm.execute('which_aliases', silent=True)
158
+ if not isinstance(result, list):
159
+ return self.aliases
160
+
161
+ aliases = {}
162
+ for item in result:
163
+ if not isinstance(item, dict):
164
+ continue
165
+ alias = item.get('alias')
166
+ command = item.get('command')
167
+ if not isinstance(alias, str) or not isinstance(command, str):
168
+ continue
169
+ if alias in aliases and aliases[alias] != command:
170
+ aliases[alias] = None
171
+ else:
172
+ aliases[alias] = command
173
+
174
+ self.aliases = {alias: command for alias, command in aliases.items()
175
+ if command is not None}
176
+ return self.aliases
177
+
153
178
  def __invoke__(self, cmd, params=None, modifiers=None):
154
- params = self.parse_params(cmd, params, modifiers)
179
+ command = self.aliases.get(cmd, cmd)
180
+ params = self.parse_params(command, params, modifiers)
155
181
  # Mi Module works with JSON Communication
156
- logger.debug("running command '{}' '{}'".format(cmd, params))
157
- res = comm.execute(cmd, params)
182
+ logger.debug("running command '{}' '{}'".format(command, params))
183
+ res = comm.execute(command, params)
158
184
  if res is None:
159
185
  return -1
160
186
  output_type = cfg.get('output_type')
@@ -25,8 +25,38 @@ from time import gmtime, mktime
25
25
  from os.path import exists, join, dirname
26
26
  from os import makedirs
27
27
  from opensipscli.config import cfg, OpenSIPSCLIConfig
28
+ from opensipscli.db import osdb, osdbError, sqlalchemy_import_error
29
+ from opensipscli import comm
28
30
  from random import randrange
29
31
 
32
+ DEFAULT_DB_NAME = "opensips"
33
+ TLS_MGM_TABLE = "tls_mgm"
34
+ TLS_DOMAIN_COL = "domain"
35
+ TLS_TYPE_COL = "type"
36
+ TLS_CERT_COL = "certificate"
37
+ TLS_PK_COL = "private_key"
38
+ TLS_CALIST_COL = "ca_list"
39
+ TLS_DH_COL = "dh_params"
40
+
41
+ # the tls_mgm columns that can be provisioned
42
+ TLS_MGM_COLUMNS = ["match_ip_address", "match_sip_domain", "method",
43
+ "verify_cert", "require_cert", TLS_CERT_COL, TLS_PK_COL,
44
+ "crl_check_all", "crl_dir", TLS_CALIST_COL, "ca_dir", "cipher_list",
45
+ TLS_DH_COL, "ec_curve"]
46
+
47
+ # columns holding PEM content, which is read from the file they point to
48
+ TLS_PEM_COLUMNS = [TLS_CERT_COL, TLS_PK_COL, TLS_CALIST_COL, TLS_DH_COL]
49
+
50
+ # columns identifying a row, in the order they are accepted as arguments
51
+ TLS_KEY_COLUMNS = [TLS_DOMAIN_COL, TLS_TYPE_COL]
52
+
53
+ # generated by the database, never provisioned
54
+ TLS_ID_COL = "id"
55
+
56
+ # as defined by CLIENT_DOMAIN_TYPE/SERVER_DOMAIN_TYPE in tls_mgm/tls_domain.h
57
+ TLS_DOMAIN_TYPES = {"client": 1, "server": 2}
58
+ TLS_TYPE_NAMES = {v: k for k, v in TLS_DOMAIN_TYPES.items()}
59
+
30
60
  openssl_version = None
31
61
 
32
62
  try:
@@ -207,6 +237,7 @@ class tlsCryptographyKey(tlsKey):
207
237
  password=None)
208
238
 
209
239
  class tls(Module):
240
+
210
241
  def do_rootCA(self, params, modifiers=None):
211
242
  global cfg
212
243
  logger.info("Preparing to generate CA cert + key...")
@@ -349,6 +380,312 @@ class tls(Module):
349
380
  logger.info("user private key created in " + k_f)
350
381
  logger.info("user CA list (chain of trust) created in " + ca_f)
351
382
 
383
+ def tls_db_connect(self):
384
+ """
385
+ connects to the database holding the tls_mgm table
386
+ """
387
+ if not osdb.has_sqlalchemy():
388
+ logger.error("SQLAlchemy not available (%s): cannot access the database",
389
+ sqlalchemy_import_error)
390
+ return None
391
+
392
+ engine = osdb.get_db_engine()
393
+
394
+ db_url = cfg.read_param(["database_tls_url", "database_url"],
395
+ "Please provide us the URL of the database")
396
+ if db_url is None:
397
+ print()
398
+ logger.error("no URL specified: aborting!")
399
+ return None
400
+
401
+ db_url = osdb.set_url_driver(db_url, engine)
402
+ db_name = cfg.read_param(["database_tls_name", "database_name"],
403
+ "Please provide the database storing the TLS domains",
404
+ DEFAULT_DB_NAME)
405
+
406
+ try:
407
+ db = osdb(db_url, db_name)
408
+ except osdbError:
409
+ logger.error("failed to connect to database %s", db_name)
410
+ return None
411
+
412
+ if not db.connect():
413
+ return None
414
+
415
+ return db
416
+
417
+ def tls_db_reload(self):
418
+ """
419
+ makes a running OpenSIPS pick up the tls_mgm changes
420
+ """
421
+ if comm.execute('tls_reload') is None:
422
+ logger.warning("could not reload the TLS domains; "
423
+ "OpenSIPS will load them at the next restart")
424
+
425
+ def tls_db_params(self, params, require_type=False):
426
+ """
427
+ resolves the (domain, type) pair identifying a tls_mgm row, along with
428
+ the columns to provision. Everything is given as 'column=value', with
429
+ the domain and its type also accepted as the first two arguments; what
430
+ is left out is asked for. The value of a PEM column is the path of the
431
+ file holding it
432
+ """
433
+ cols = {}
434
+ for param in [p for p in params if '=' in p]:
435
+ col, val = param.split('=', 1)
436
+ if col == TLS_ID_COL:
437
+ logger.error("column '%s' is generated by the database",
438
+ TLS_ID_COL)
439
+ return None, None, None
440
+
441
+ if col not in TLS_MGM_COLUMNS and col not in TLS_KEY_COLUMNS:
442
+ logger.error("unknown %s column '%s'", TLS_MGM_TABLE, col)
443
+ return None, None, None
444
+
445
+ if col in TLS_PEM_COLUMNS:
446
+ path = val
447
+ try:
448
+ with open(path, "rt") as f:
449
+ val = f.read()
450
+ except Exception as e:
451
+ logger.exception(e)
452
+ logger.error("Failed to read %s", path)
453
+ return None, None, None
454
+
455
+ if "-----BEGIN" not in val:
456
+ logger.error("%s is not in PEM format", path)
457
+ return None, None, None
458
+
459
+ cols[col] = val
460
+
461
+ # the domain and its type identify the row, they are not provisioned
462
+ args = [p for p in params if '=' not in p]
463
+ if len(args) > len(TLS_KEY_COLUMNS):
464
+ logger.error("too many arguments: expected at most a domain and "
465
+ "its type")
466
+ return None, None, None
467
+
468
+ key = {}
469
+ for i, col in enumerate(TLS_KEY_COLUMNS):
470
+ if i < len(args):
471
+ if col in cols:
472
+ logger.error("'%s' given both as an argument and as "
473
+ "'%s='", col, col)
474
+ return None, None, None
475
+ key[col] = args[i]
476
+ else:
477
+ key[col] = cols.pop(col, None)
478
+
479
+ domain = key[TLS_DOMAIN_COL]
480
+ if not domain:
481
+ domain = cfg.read_param(None,
482
+ "Please provide the name of the TLS domain")
483
+ if not domain:
484
+ logger.error("no TLS domain specified!")
485
+ return None, None, None
486
+
487
+ dtype = key[TLS_TYPE_COL]
488
+ if not dtype:
489
+ # commands changing an existing row get no default, so that they
490
+ # cannot pick a different row than the intended one
491
+ dtype = cfg.read_param(None, "TLS domain type (server/client)",
492
+ None if require_type else "server")
493
+ if not dtype:
494
+ logger.error("no TLS domain type specified!")
495
+ return None, None, None
496
+
497
+ if dtype.lower() not in TLS_DOMAIN_TYPES:
498
+ logger.error("invalid TLS domain type '%s': "
499
+ "expected 'server' or 'client'", dtype)
500
+ return None, None, None
501
+
502
+ return domain, TLS_DOMAIN_TYPES[dtype.lower()], cols
503
+
504
+ def do_add(self, params=None, modifiers=None):
505
+ """
506
+ provisions a new TLS domain in the database
507
+ """
508
+ domain, dtype, cols = self.tls_db_params(params or [])
509
+ if not domain:
510
+ return -1
511
+
512
+ db = self.tls_db_connect()
513
+ if not db:
514
+ return -1
515
+
516
+ row = {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype}
517
+ if db.entry_exists(TLS_MGM_TABLE, row):
518
+ logger.error("TLS %s domain '%s' already exists",
519
+ TLS_TYPE_NAMES[dtype], domain)
520
+ db.destroy()
521
+ return -1
522
+
523
+ row.update(cols)
524
+ if db.insert(TLS_MGM_TABLE, row) is False:
525
+ db.destroy()
526
+ return -1
527
+
528
+ db.destroy()
529
+ logger.info("Successfully added TLS %s domain '%s'",
530
+ TLS_TYPE_NAMES[dtype], domain)
531
+ self.tls_db_reload()
532
+ return True
533
+
534
+ def do_update(self, params=None, modifiers=None):
535
+ """
536
+ changes the given columns of an existing TLS domain
537
+ """
538
+ domain, dtype, cols = self.tls_db_params(params or [], True)
539
+ if not domain:
540
+ return -1
541
+
542
+ if not cols:
543
+ logger.error("no column to update: expected 'column=value'")
544
+ return -1
545
+
546
+ db = self.tls_db_connect()
547
+ if not db:
548
+ return -1
549
+
550
+ row = {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype}
551
+ if not db.entry_exists(TLS_MGM_TABLE, row):
552
+ logger.error("TLS %s domain '%s' does not exist",
553
+ TLS_TYPE_NAMES[dtype], domain)
554
+ db.destroy()
555
+ return -1
556
+
557
+ if db.update(TLS_MGM_TABLE, cols, row) is False:
558
+ db.destroy()
559
+ return -1
560
+
561
+ db.destroy()
562
+ logger.info("Successfully updated TLS %s domain '%s'",
563
+ TLS_TYPE_NAMES[dtype], domain)
564
+ self.tls_db_reload()
565
+ return True
566
+
567
+ def do_list(self, params=None, modifiers=None):
568
+ """
569
+ lists the TLS domains provisioned in the database
570
+ """
571
+ db = self.tls_db_connect()
572
+ if not db:
573
+ return -1
574
+
575
+ res = db.find(TLS_MGM_TABLE,
576
+ ["id", TLS_DOMAIN_COL, TLS_TYPE_COL, "method",
577
+ "verify_cert", "require_cert"], None)
578
+ if res is None:
579
+ db.destroy()
580
+ return -1
581
+
582
+ rows = res.fetchall()
583
+ db.destroy()
584
+
585
+ if not rows:
586
+ logger.info("no TLS domain provisioned in %s", TLS_MGM_TABLE)
587
+ return True
588
+
589
+ print("{:<5} {:<32} {:<8} {:<8} {:<8} {:<8}".format(
590
+ "id", "domain", "type", "method", "verify", "require"))
591
+ for r in rows:
592
+ print("{:<5} {:<32} {:<8} {:<8} {:<8} {:<8}".format(
593
+ r[0], r[1], TLS_TYPE_NAMES.get(r[2], r[2]),
594
+ str(r[3]), str(r[4]), str(r[5])))
595
+ return True
596
+
597
+ def do_show(self, params=None, modifiers=None):
598
+ """
599
+ prints the columns of a TLS domain
600
+ """
601
+ domain, dtype, cols = self.tls_db_params(params or [])
602
+ if domain and cols:
603
+ logger.error("show takes no column: '%s'", list(cols)[0])
604
+ return -1
605
+ if not domain:
606
+ return -1
607
+
608
+ db = self.tls_db_connect()
609
+ if not db:
610
+ return -1
611
+
612
+ res = db.find(TLS_MGM_TABLE, TLS_MGM_COLUMNS,
613
+ {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype})
614
+ row = res.first() if res is not None else None
615
+ db.destroy()
616
+
617
+ if not row:
618
+ logger.error("TLS %s domain '%s' does not exist",
619
+ TLS_TYPE_NAMES[dtype], domain)
620
+ return -1
621
+
622
+ def decode(val):
623
+ return val.decode('utf-8') if isinstance(val, bytes) else val
624
+
625
+ values = dict(zip(TLS_MGM_COLUMNS, row))
626
+
627
+ print("{} domain: {}".format(TLS_TYPE_NAMES[dtype], domain))
628
+ for col in TLS_MGM_COLUMNS:
629
+ if col in TLS_PEM_COLUMNS:
630
+ continue
631
+ print("{}: {}".format(col,
632
+ "<empty>" if values[col] is None else values[col]))
633
+
634
+ # the private key is never printed back
635
+ print("{}: {}".format(TLS_PK_COL,
636
+ "<hidden>" if values[TLS_PK_COL] else "<empty>"))
637
+
638
+ for col in TLS_PEM_COLUMNS:
639
+ if col == TLS_PK_COL:
640
+ continue
641
+ print("\n{}:\n{}".format(col,
642
+ decode(values[col]) if values[col] else "<empty>"))
643
+ return True
644
+
645
+ def do_delete(self, params=None, modifiers=None):
646
+ """
647
+ removes a TLS domain from the database
648
+ """
649
+ domain, dtype, cols = self.tls_db_params(params or [], True)
650
+ if domain and cols:
651
+ logger.error("delete takes no column: '%s'", list(cols)[0])
652
+ return -1
653
+ if not domain:
654
+ return -1
655
+
656
+ db = self.tls_db_connect()
657
+ if not db:
658
+ return -1
659
+
660
+ row = {TLS_DOMAIN_COL: domain, TLS_TYPE_COL: dtype}
661
+ if not db.entry_exists(TLS_MGM_TABLE, row):
662
+ logger.error("TLS %s domain '%s' does not exist",
663
+ TLS_TYPE_NAMES[dtype], domain)
664
+ db.destroy()
665
+ return -1
666
+
667
+ if db.delete(TLS_MGM_TABLE, row) is False:
668
+ db.destroy()
669
+ return -1
670
+
671
+ db.destroy()
672
+ logger.info("Successfully deleted TLS %s domain '%s'",
673
+ TLS_TYPE_NAMES[dtype], domain)
674
+ self.tls_db_reload()
675
+ return True
676
+
677
+ def __complete__(self, command, text, line, begidx, endidx):
678
+ """
679
+ helper for autocompletion in interactive mode
680
+ """
681
+ if command not in ('add', 'update'):
682
+ return ['']
683
+
684
+ cols = [c + '=' for c in TLS_MGM_COLUMNS]
685
+ if not text:
686
+ return cols
687
+
688
+ return [c for c in cols if c.startswith(text)] or ['']
352
689
 
353
690
  def __exclude__(self):
354
691
  return (not openssl_version, None)
@@ -17,6 +17,6 @@
17
17
  ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
18
  ##
19
19
 
20
- __version__ = '0.4.1'
20
+ __version__ = '0.4.2'
21
21
 
22
22
  # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
@@ -1,3 +1,9 @@
1
+ opensips-cli (0.4.2-1) stable; urgency=low
2
+
3
+ * New public release.
4
+
5
+ -- Razvan Crainea <razvan@opensips.org> Mon, 09 Nov 2026 00:00:00 +0000
6
+
1
7
  opensips-cli (0.4.1-1) stable; urgency=low
2
8
 
3
9
  * New public release.
@@ -1,6 +1,6 @@
1
1
  Summary: Interactive command-line tool for OpenSIPS 3.0+
2
2
  Name: opensips-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Release: 0%{?dist}
5
5
  License: GPL-3+
6
6
  Group: System Environment/Daemons
@@ -68,6 +68,8 @@ rm -rf $RPM_BUILD_ROOT
68
68
  %license LICENSE
69
69
 
70
70
  %changelog
71
+ * Mon Nov 9 2026 Razvan Crainea <razvan@opensips.org> - 0.4.2-0
72
+ - Bump package version to 0.4.2.
71
73
  * Thu Sep 3 2026 Stefan Darius <darius.stefan@opensips.org> - 0.4.1-0
72
74
  - Bump package version to 0.4.1.
73
75
  * Fri May 8 2026 Razvan Crainea <razvan@opensips.org> - 0.4.0-0
@@ -1,8 +1,20 @@
1
1
  import unittest
2
+ from unittest.mock import patch
2
3
 
3
4
  from opensipscli.db import make_url
5
+ from opensipscli.modules.mi import mi
4
6
 
5
7
  class OpenSIPSCLIUnitTests(unittest.TestCase):
8
+ def testMIAliasUsesCanonicalCommand(self):
9
+ module = mi()
10
+ module.aliases = {'alias': 'canonical'}
11
+
12
+ with patch('opensipscli.modules.mi.comm.execute', return_value=None) as execute:
13
+ result = module.__invoke__('alias', ['key=value'], [])
14
+
15
+ self.assertEqual(result, -1)
16
+ execute.assert_called_once_with('canonical', {'key': 'value'})
17
+
6
18
  def testMakeURL(self):
7
19
  u = make_url('x://')
8
20
  assert repr(u) == 'x://'
@@ -1,99 +0,0 @@
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
- ```
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes