SSL management feature
Add instance API actions to show, enable, disable, and roll back SSL/TLS configuration. Certificates are supplied as PKCS#12 secrets stored in Barbican, and support basic, enforced, and mutual-TLS modes. Implement guest-agent support for MySQL, MariaDB, and PostgreSQL, including certificate propagation to replicas and Barbican consumer lifecycle handling. Blueprint: ssl-management-feature Depends-On: https://review.opendev.org/c/openstack/trove-tempest-plugin/+/988053 Signed-off-by: Erkin Mussurmankulov <mangust404@gmail.com> Change-Id: I93b9647c30cfcc7823e7d293652816f9040ee201
This commit is contained in:
1 parent
db36c51d5b
commit
729f614a73
48 files changed
+2670
-40
No files matched your search
@@ -12,6 +12,7 @@
|
||||
.. include:: instances.inc
|
||||
.. include:: instance-actions.inc
|
||||
.. include:: instance-logs.inc
|
||||
.. include:: instance-ssl.inc
|
||||
.. include:: backups.inc
|
||||
.. include:: backup-strategy.inc
|
||||
.. include:: configurations.inc
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
.. -*- rst -*-
|
||||
|
||||
=============
|
||||
Instance SSL
|
||||
=============
|
||||
|
||||
|
||||
|
||||
|
||||
Show instance SSL status
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: GET /v1.0/{project_id}/instances/{instanceId}/ssl
|
||||
|
||||
Show current SSL status for the instance and information about current
|
||||
SSL certificate (if present).
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- project_id: project_id
|
||||
- instanceId: instanceId
|
||||
- include_certificate: ssl_include_cert
|
||||
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- ssl: instance_ssl
|
||||
- ssl.status: ssl_status
|
||||
- ssl.mode: ssl_mode
|
||||
- ssl.certificate: ssl_certificate
|
||||
- ssl.certificate.payload: ssl_certificate_payload
|
||||
- ssl.certificate.cn: ssl_certificate_cn
|
||||
- ssl.certificate.expire_at: ssl_certificate_expire_at
|
||||
- ssl.certificate.san: ssl_certificate_san
|
||||
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/instance-ssl-show-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
|
||||
|
||||
SSL enable action for instance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: POST /v1.0/{project_id}/instances/{instanceId}/ssl
|
||||
|
||||
Enable SSL for a database instance, or renew an existing SSL
|
||||
certificate on the instance.
|
||||
|
||||
Instance should be in `ACTIVE/HEALTHY` state.
|
||||
|
||||
Barbican consumer will be added for `container_ref` secret.
|
||||
|
||||
For replication, the SSL action should be called against the primary node only.
|
||||
The configuration will be applied sequentially to each replica in a transactional manner.
|
||||
If an error occurs on a single node, the entire configuration will be rolled back.
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- project_id: project_id
|
||||
- instanceId: instanceId
|
||||
- ssl: instance_ssl
|
||||
- ssl.enable: ssl_action_enable
|
||||
- ssl.container_ref: ssl_container_ref
|
||||
- ssl.password_ref: ssl_password_ref
|
||||
|
||||
Request Example
|
||||
---------------
|
||||
|
||||
.. literalinclude:: samples/instance-ssl-action-request.json
|
||||
:language: javascript
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- ssl: instance_ssl
|
||||
- ssl.status: ssl_status
|
||||
- ssl.container_ref: ssl_container_ref
|
||||
- ssl.restart_required: ssl_restart_required
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/instance-ssl-action-response.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
|
||||
SSL disable action for instance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: POST /v1.0/{project_id}/instances/{instanceId}/ssl
|
||||
|
||||
Disable SSL for a database instance.
|
||||
|
||||
Note that disabling SSL for some datastores, e.g. for MySQL 8.4 and
|
||||
above, is not supported.
|
||||
|
||||
Barbican consumer will be removed from `container_ref` secret.
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- project_id: project_id
|
||||
- instanceId: instanceId
|
||||
- ssl: instance_ssl
|
||||
- ssl.disable: ssl_action_disable
|
||||
|
||||
Request Example
|
||||
---------------
|
||||
|
||||
.. literalinclude:: samples/instance-ssl-action-request-disable.json
|
||||
:language: javascript
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- ssl: instance_ssl
|
||||
- ssl.status: ssl_status
|
||||
- ssl.restart_required: ssl_restart_required
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/instance-ssl-action-response-disable.json
|
||||
:language: javascript
|
||||
|
||||
|
||||
|
||||
SSL rollback action for instance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. rest_method:: POST /v1.0/{project_id}/instances/{instanceId}/ssl
|
||||
|
||||
Roll back the last SSL configuration on the instance.
|
||||
|
||||
Use only in emergencies. Note that the Barbican consumer state may be
|
||||
inconsistent after this operation because the guest agent applies the stored
|
||||
certificate payload directly, without referring to the Barbican secret.
|
||||
|
||||
Normal response codes: 200
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- project_id: project_id
|
||||
- instanceId: instanceId
|
||||
- ssl: instance_ssl
|
||||
- ssl.rollback: ssl_action_rollback
|
||||
|
||||
Request Example
|
||||
---------------
|
||||
|
||||
.. literalinclude:: samples/instance-ssl-action-request-rollback.json
|
||||
:language: javascript
|
||||
|
||||
Response Parameters
|
||||
-------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- ssl: instance_ssl
|
||||
- ssl.status: ssl_status
|
||||
- ssl.restart_required: ssl_restart_required
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
|
||||
.. literalinclude:: samples/instance-ssl-action-response-rollback.json
|
||||
:language: javascript
|
||||
@@ -79,6 +79,13 @@ version:
|
||||
in: path
|
||||
required: false
|
||||
type: string
|
||||
# variables in query
|
||||
ssl_include_cert:
|
||||
description: |
|
||||
If false or not provided, certificate payload is omitted.
|
||||
in: query
|
||||
required: false
|
||||
type: boolean
|
||||
# variables in body
|
||||
access:
|
||||
description: |
|
||||
@@ -588,6 +595,30 @@ instance_replicas:
|
||||
in: body
|
||||
required: false
|
||||
type: array
|
||||
instance_ssl:
|
||||
description: |
|
||||
An ``ssl`` object.
|
||||
in: body
|
||||
required: true
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
mode:
|
||||
type: string
|
||||
container_ref:
|
||||
type: string
|
||||
certificate:
|
||||
type: object
|
||||
properties:
|
||||
payload:
|
||||
type: string
|
||||
cn:
|
||||
type: string
|
||||
expire_at:
|
||||
type: string
|
||||
san:
|
||||
type: [string, "null"]
|
||||
instance_status:
|
||||
description: |
|
||||
Status of the instance.
|
||||
@@ -861,6 +892,94 @@ slave_of:
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
ssl_action_disable:
|
||||
description: |
|
||||
Disable installed SSL certificate.
|
||||
in: body
|
||||
required: false
|
||||
type: boolean
|
||||
ssl_action_enable:
|
||||
description: |
|
||||
Enable new SSL certificate.
|
||||
in: body
|
||||
required: false
|
||||
type: boolean
|
||||
ssl_action_rollback:
|
||||
description: |
|
||||
Rollback to last SSL configuration.
|
||||
in: body
|
||||
required: false
|
||||
type: boolean
|
||||
ssl_certificate:
|
||||
description: |
|
||||
Certificate object
|
||||
in: body
|
||||
required: false
|
||||
type: object
|
||||
ssl_certificate_cn:
|
||||
description: |
|
||||
CN for the current installed certificate.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
ssl_certificate_expire_at:
|
||||
description: |
|
||||
Expiration timestamp of the current certificate.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
ssl_certificate_payload:
|
||||
description: |
|
||||
Contains public certificate only. Private keys are never exposed.
|
||||
Provided only if `?include_certificate=true` query param is
|
||||
present.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
ssl_certificate_san:
|
||||
description: |
|
||||
Certificate Subject Alternative Names.
|
||||
in: body
|
||||
required: false
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
ssl_container_ref:
|
||||
description: |
|
||||
Reference to a PKCS#12 container stored in Barbican.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
ssl_mode:
|
||||
description: |
|
||||
Currently enabled SSL/TLS mode:
|
||||
- builtin (default certificates installed by DBMS)
|
||||
- basic
|
||||
- enforced
|
||||
- mtls
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
ssl_password_ref:
|
||||
description: |
|
||||
Provide a Barbican reference to the password if the PKCS#12 container is
|
||||
password-protected, with instance project member scope.
|
||||
in: body
|
||||
required: false
|
||||
type: string
|
||||
ssl_restart_required:
|
||||
description: |
|
||||
True if configuration changes require database restart to take effect.
|
||||
in: body
|
||||
required: false
|
||||
type: boolean
|
||||
ssl_status:
|
||||
description: |
|
||||
"on" if SSL is enabled, "off" if SSL is disabled or misconfigured.
|
||||
in: body
|
||||
required: true
|
||||
type: enum
|
||||
enum: [on, off]
|
||||
storage_driver:
|
||||
description: |
|
||||
The backup strategy
|
||||
@@ -1021,4 +1140,4 @@ wal_size:
|
||||
PostgreSQL manager only.
|
||||
in: body
|
||||
required: false
|
||||
type: integer
|
||||
type: integer
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ssl": {
|
||||
"disable": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ssl": {
|
||||
"rollback": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ssl": {
|
||||
"enable": true,
|
||||
"mode": "basic",
|
||||
"container_ref": "https://barbican.example.kz/v1/secrets/2b4edf0-586b-49c4-a183-716a66d86f4e1",
|
||||
"password_ref": "https://barbican.example.kz/v1/secrets/3927c462-60eb-4f1f-b64c-5a6835778371"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ssl": {
|
||||
"status": "off",
|
||||
"restart_required": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ssl": {
|
||||
"status": "on",
|
||||
"mode": "basic",
|
||||
"restart_required": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ssl": {
|
||||
"status": "on",
|
||||
"mode": "basic",
|
||||
"restart_required": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ssl": {
|
||||
"status": "on",
|
||||
"mode": "basic",
|
||||
"certificate": {
|
||||
"cn": "self-signed certificate",
|
||||
"expire_at": "2027-02-10T09:06:25.000000",
|
||||
"san": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,6 +433,13 @@ function configure_tempest_for_trove {
|
||||
echo "Enable trove-tempest-plugin"
|
||||
pip_install $DEST/trove-tempest-plugin
|
||||
|
||||
if [ -d "$DEST/barbican-tempest-plugin" ]; then
|
||||
# This command has a meaning only for developers DevStack installtion.
|
||||
# In Zuul jobs, this plugin installed by tempest_plugins: section in jobs.yaml.
|
||||
echo "Enable barbican-tempest-plugin"
|
||||
pip_install -e $DEST/barbican-tempest-plugin
|
||||
fi
|
||||
|
||||
echo "Adding [database] section to tempest.conf"
|
||||
if [[ -n "$TROVE_DATASTORE_TYPE" ]]; then
|
||||
iniset $TEMPEST_CONFIG database enabled_datastores $TROVE_DATASTORE_TYPE
|
||||
|
||||
@@ -102,6 +102,10 @@ configuration:
|
||||
enable_plugin trove https://opendev.org/openstack/trove
|
||||
enable_plugin trove-tempest-plugin https://opendev.org/openstack/trove-tempest-plugin
|
||||
|
||||
# barbican service & tempest plugin
|
||||
enable_plugin barbican https://opendev.org/openstack/barbican
|
||||
enable_plugin barbican-tempest-plugin https://git.openstack.org/openstack/barbican-tempest-plugin
|
||||
|
||||
# required for not interrupting install process
|
||||
SWIFT_HASH=66a3d6b56c1f479c8b4e70ab5c2000f5
|
||||
|
||||
|
||||
@@ -19,3 +19,4 @@ handling complex administrative tasks.
|
||||
upgrade-datastore.rst
|
||||
set-up-clustering.rst
|
||||
upgrade-cluster-datastore.rst
|
||||
ssl/index
|
||||
@@ -0,0 +1,189 @@
|
||||
.. _ssl_pkcs12:
|
||||
|
||||
Preparing Certificates
|
||||
======================
|
||||
|
||||
This section describes how to generate and prepare certificates for use
|
||||
with Trove.
|
||||
|
||||
Generate CA private key
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl genrsa -out ca.key 2048
|
||||
Generating RSA private key, 2048 bit long modulus
|
||||
........................................+++++
|
||||
................................+++++
|
||||
e is 65537 (0x10001)
|
||||
|
||||
.. warning::
|
||||
|
||||
Keep the CA private key in a secure offline location.
|
||||
|
||||
Create self-signed CA certificate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl req -x509 -new -nodes \
|
||||
-key ca.key \
|
||||
-sha256 \
|
||||
-days 365 \
|
||||
-subj "/CN=Trove Test CA" \
|
||||
-out ca-cert.crt
|
||||
|
||||
Generate server private key
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl genrsa -out server.key 2048
|
||||
Generating RSA private key, 2048 bit long modulus
|
||||
........................................+++++
|
||||
........................+++++
|
||||
e is 65537 (0x10001)
|
||||
|
||||
Generate server CSR
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl req -new \
|
||||
-key server.key \
|
||||
-out server.csr \
|
||||
-subj "/CN=db.example.com" \
|
||||
-addext "subjectAltName=DNS:db.example.com,DNS:db.internal,IP:10.0.0.10"
|
||||
|
||||
.. note::
|
||||
|
||||
Please note that domain name and (or) IP address must match the real
|
||||
IP address that your client will use for connection to DB server.
|
||||
|
||||
Sign server certificate with CA
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl x509 -req \
|
||||
-in server.csr \
|
||||
-CA ca-cert.crt \
|
||||
-CAkey ca.key \
|
||||
-CAcreateserial \
|
||||
-out server.crt \
|
||||
-days 365 \
|
||||
-sha256
|
||||
Signature ok
|
||||
subject=/CN=db.example.com
|
||||
Getting CA Private Key
|
||||
|
||||
Create PKCS#12 container
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl pkcs12 -export \
|
||||
-inkey server.key \
|
||||
-in server.crt \
|
||||
-certfile ca-cert.crt \
|
||||
-passout pass:MySecretPassword01! \
|
||||
-out server.p12
|
||||
|
||||
.. _store_barbican_pass:
|
||||
|
||||
Store PKCS#12 password in Barbican
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack secret store --name my-password-for-pkcs12 \
|
||||
--payload MySecretPassword01!
|
||||
+---------------+----------------------------------------------------------------------------------+
|
||||
| Field | Value |
|
||||
+---------------+----------------------------------------------------------------------------------+
|
||||
| Secret href | https://barbican-api.example.com/v1/secrets/f54bc745-00e7-4e42-bd6a-0e80879b62f6 |
|
||||
| Name | my-password-for-pkcs12 |
|
||||
| Created | None |
|
||||
| Status | None |
|
||||
| Content types | None |
|
||||
| Algorithm | aes |
|
||||
| Bit length | 256 |
|
||||
| Secret type | opaque |
|
||||
| Mode | cbc |
|
||||
| Expiration | None |
|
||||
+---------------+----------------------------------------------------------------------------------+
|
||||
|
||||
Upload PKCS#12 to Barbican
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack secret store --file server.p12 \
|
||||
--name my-pkcs12-container-for-ssl \
|
||||
--secret-type certificate
|
||||
+---------------+----------------------------------------------------------------------------------+
|
||||
| Field | Value |
|
||||
+---------------+----------------------------------------------------------------------------------+
|
||||
| Secret href | https://barbican-api.example.com/v1/secrets/c3fd1238-7d85-4eec-8e36-53c4953bab39 |
|
||||
| Name | my-pkcs12-container-for-ssl |
|
||||
| Created | None |
|
||||
| Status | None |
|
||||
| Content types | None |
|
||||
| Algorithm | aes |
|
||||
| Bit length | 256 |
|
||||
| Secret type | certificate |
|
||||
| Mode | cbc |
|
||||
| Expiration | None |
|
||||
+---------------+----------------------------------------------------------------------------------+
|
||||
|
||||
.. _ssl_client_cert:
|
||||
|
||||
Create client certificate for mTLS authentication
|
||||
-------------------------------------------------
|
||||
|
||||
Generate client private key
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl genrsa -out client.key 2048
|
||||
|
||||
Generate client CSR
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl req -new \
|
||||
-key client.key \
|
||||
-out client.csr \
|
||||
-subj "/CN=dbuser"
|
||||
|
||||
.. note::
|
||||
|
||||
Please note that subject should be equal to a real user name used
|
||||
to connect to the database.
|
||||
|
||||
Sign client certificate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openssl x509 -req \
|
||||
-in client.csr \
|
||||
-CA ca-cert.crt \
|
||||
-CAkey ca.key \
|
||||
-CAcreateserial \
|
||||
-out client.crt \
|
||||
-days 365 \
|
||||
-sha256
|
||||
|
||||
PostgreSQL connection using client certificate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
psql "sslmode=verify-full \
|
||||
sslrootcert=ca-cert.crt \
|
||||
sslcert=client.crt \
|
||||
sslkey=client.key \
|
||||
host=10.0.0.10 user=dbuser dbname=test_db"
|
||||
@@ -0,0 +1,16 @@
|
||||
SSL/TLS Support in Trove
|
||||
========================
|
||||
|
||||
Trove provides support for enabling SSL/TLS encryption for database
|
||||
instances, including optional mutual TLS (mTLS) authentication.
|
||||
|
||||
This section describes the concepts, configuration, and usage of SSL/TLS
|
||||
in Trove.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
overview
|
||||
usage
|
||||
certificates
|
||||
replication
|
||||
@@ -0,0 +1,59 @@
|
||||
Overview
|
||||
========
|
||||
|
||||
Trove supports SSL/TLS to secure connections between clients and database
|
||||
instances.
|
||||
|
||||
Users may enable, disable, and monitor SSL configuration for database
|
||||
instances.
|
||||
|
||||
SSL operating modes
|
||||
-------------------
|
||||
|
||||
By default, most DBMSs generate their own self-signed SSL certificate on
|
||||
startup. This is referred to as the "builtin" mode and is not managed by
|
||||
Trove until SSL/TLS is explicitly enabled using one of the supported
|
||||
operation modes.
|
||||
|
||||
Basic mode
|
||||
~~~~~~~~~~
|
||||
|
||||
* SSL is enabled on the database instance.
|
||||
* Both SSL and non-SSL client connections are allowed.
|
||||
* SSL encryption is optional.
|
||||
* Authentication remains password-based (for example, MD5).
|
||||
* No client certificate validation.
|
||||
|
||||
This mode provides basic protection against passive traffic interception
|
||||
and may be used as intermediate step for implementing enforced or
|
||||
mTLS mode in production infrastructure.
|
||||
|
||||
Enforced mode
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
* SSL is enabled and mandatory.
|
||||
* Only encrypted connections are accepted.
|
||||
* Non-SSL connections are rejected.
|
||||
* Authentication remains password-based.
|
||||
* No client certificate validation.
|
||||
|
||||
This mode improves security and helps prevent insecure client
|
||||
configurations.
|
||||
|
||||
Mutual TLS mode (mTLS)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* SSL is enabled and mandatory.
|
||||
* Clients must present valid certificates. Example of :ref:`creating cert <ssl_client_cert>`
|
||||
* Certificates are validated using trusted CA.
|
||||
* Client identity may be based on certificate attributes.
|
||||
* Password authentication is disabled.
|
||||
|
||||
This mode provides strong authentication and encryption.
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
* SSL configuration requires the instance to be in ACTIVE state
|
||||
* For replication setups, SSL is applied to all replicas
|
||||
@@ -0,0 +1,108 @@
|
||||
.. _ssl_for_replication:
|
||||
|
||||
SSL/TLS for replication
|
||||
-----------------------
|
||||
|
||||
Trove supports SSL/TLS configuration for replicated database clusters.
|
||||
SSL configuration is managed at the primary instance level and is
|
||||
automatically propagated to all replicas.
|
||||
|
||||
General behavior
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The following rules apply when SSL is used in replication:
|
||||
|
||||
* SSL can be enabled only on the primary instance.
|
||||
* When SSL is enabled on the primary, the certificate and key are applied
|
||||
to all attached replicas automatically.
|
||||
* All instances in the replication cluster must remain consistent with
|
||||
the primary SSL configuration.
|
||||
|
||||
These constraints ensure secure communication between replication nodes
|
||||
and prevent configuration drift.
|
||||
|
||||
Prerequisites
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Before enabling SSL in a replication cluster:
|
||||
|
||||
* The replication topology must be fully configured.
|
||||
* All replicas must be attached to the primary instance.
|
||||
* The replication cluster must be in ``ACTIVE`` and ``HEALTHY`` state.
|
||||
|
||||
If any instance is not ready, the operation fails. This prevents partial
|
||||
SSL configuration and ensures cluster integrity.
|
||||
|
||||
Enable SSL in replication
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The SSL enable operation must be performed on the primary instance only.
|
||||
If the target instance is not primary, the operation is rejected.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack database ssl enable <primary_id> <container_ref>
|
||||
|
||||
When the operation starts:
|
||||
|
||||
* Trove validates the state of the entire replication cluster.
|
||||
* Certificates are retrieved from Barbican.
|
||||
* SSL configuration is applied to the primary and all replicas.
|
||||
|
||||
If validation fails, no changes are applied.
|
||||
Partial changes will be rolled back to the original state.
|
||||
|
||||
Restart behavior
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Depending on the datastore manager and database engine, a restart may be
|
||||
required to apply SSL configuration.
|
||||
|
||||
The recommended restart order is:
|
||||
|
||||
1. Restart the primary instance.
|
||||
2. Restart each replica sequentially.
|
||||
|
||||
Sequential restarts help preserve replication consistency and reduce
|
||||
service disruption.
|
||||
|
||||
If Trove indicates that restart is required, users should avoid restarting
|
||||
replicas in parallel unless supported by the specific database engine.
|
||||
|
||||
Replica provisioning
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When a new replica is created in an existing replication cluster:
|
||||
|
||||
* The replica automatically retrieves the required SSL certificate,
|
||||
private key, and trusted CA from the primary instance.
|
||||
* No additional SSL configuration is required from the user.
|
||||
* The replica becomes consistent with the primary SSL configuration.
|
||||
|
||||
This behavior ensures that all replication nodes remain securely configured
|
||||
and simplifies operational workflows.
|
||||
|
||||
Certificate rotation
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Trove supports certificate rotation in replication clusters.
|
||||
|
||||
To refresh or replace a certificate:
|
||||
|
||||
* Call the ``ssl enable`` command on the primary instance.
|
||||
* Provide a new PKCS#12 container reference and the required SSL mode.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack database ssl enable <primary_id> <new_container_ref> \
|
||||
--mode <required_mode>
|
||||
|
||||
During this operation:
|
||||
|
||||
* The new certificate is retrieved from Barbican.
|
||||
* The certificate and key are propagated to all replicas.
|
||||
* The replication cluster remains consistent with the primary.
|
||||
|
||||
Depending on the datastore manager, a rolling restart of the replication
|
||||
cluster may be required to apply the updated certificate.
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
Usage
|
||||
=====
|
||||
|
||||
SSL management actions
|
||||
----------------------
|
||||
|
||||
The following actions are available for SSL configuration.
|
||||
|
||||
Show current SSL status
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Returns the current SSL configuration of a database instance.
|
||||
|
||||
The response may include:
|
||||
|
||||
* SSL status (on/off)
|
||||
* Operating mode (basic/enforced/mtls)
|
||||
* Certificate expiration date
|
||||
* Certificate CN (common name)
|
||||
* Certificate SAN (Subject Alternative Name)
|
||||
* Certificate payload
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack database ssl show $dbid
|
||||
+-----------------------+----------------------------+
|
||||
| Field | Value |
|
||||
+-----------------------+----------------------------+
|
||||
| certificate_cn | trove-test |
|
||||
| certificate_expire_at | 2027-02-10T09:06:25.000000 |
|
||||
| certificate_san | IP:10.0.0.1 |
|
||||
| mode | basic |
|
||||
| status | on |
|
||||
+-----------------------+----------------------------+
|
||||
|
||||
Enable SSL
|
||||
~~~~~~~~~~
|
||||
|
||||
Enables SSL on the instance or rotates the existing certificate.
|
||||
|
||||
Requirements:
|
||||
|
||||
* The user must provide a reference to a PKCS12 container stored in Barbican.
|
||||
* The container must include:
|
||||
* Server certificate
|
||||
* Private key for certificate
|
||||
* Optional CA chain
|
||||
|
||||
Example of creating a PKCS#12 container: :ref:`PKCS#12 example <ssl_pkcs12>`
|
||||
|
||||
Optional:
|
||||
|
||||
* password_ref should be provided if the container is password-protected.
|
||||
|
||||
Example: :ref:`Store Barbican password <store_barbican_pass>`
|
||||
|
||||
Parameters:
|
||||
|
||||
* container_ref
|
||||
* password_ref (optional)
|
||||
|
||||
Depending on database manager, restart may be required for applying SSL
|
||||
configuration.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack database ssl enable $dbid https://barbican-api.example.com/9036aa7c-7c71-45e3-a9c4-a71465c4ccc9 \
|
||||
--password-ref https://barbican-api.example.com/d7211e91-4c74-43e1-8a7f-47937e56c0bd
|
||||
+------------------+-------+
|
||||
| Field | Value |
|
||||
+------------------+-------+
|
||||
| restart_required | True |
|
||||
+------------------+-------+
|
||||
|
||||
Please note that secret consumer will be created for provided certificate
|
||||
container in Barbican, referencing the database instance.
|
||||
|
||||
Disable SSL
|
||||
~~~~~~~~~~~
|
||||
|
||||
Disables SSL on the instance.
|
||||
|
||||
Depending on database manager, restart may be required for applying SSL
|
||||
configuration.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack database ssl disable $dbid
|
||||
+------------------+-------+
|
||||
| Field | Value |
|
||||
+------------------+-------+
|
||||
| restart_required | True |
|
||||
+------------------+-------+
|
||||
|
||||
|
||||
Certificate requirements
|
||||
------------------------
|
||||
|
||||
The provided PKCS12 container must:
|
||||
|
||||
* Contain a valid private key and certificate.
|
||||
* Match the database instance hostname.
|
||||
* Use modern cryptographic parameters.
|
||||
|
||||
Recommended:
|
||||
|
||||
* TLS 1.2 or higher.
|
||||
* Proper Subject Alternative Names.
|
||||
* Certificate signed by a trusted CA.
|
||||
|
||||
|
||||
End-user recommendations
|
||||
------------------------
|
||||
|
||||
The following guidance may help users operating production DBaaS:
|
||||
|
||||
* Use at least enforced encryption in production environments.
|
||||
* Automate certificate rotation.
|
||||
* Monitor certificate expiration.
|
||||
* Use separate certificates per environment.
|
||||
* Consider mTLS for sensitive workloads.
|
||||
* Validate client TLS compatibility before enforcing encryption.
|
||||
* Integrate SSL lifecycle with centralized secret management.
|
||||
|
||||
These recommendations are based on common production practices and may
|
||||
vary depending on organizational security policies.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
SSL/TLS certificate management for securing the client-server channel.
|
||||
The PKCS#12 container and credentials are stored in Barbican.
|
||||
Available modes are basic, enforced, and mTLS. See the documentation for more information.
|
||||
@@ -21,6 +21,7 @@ python-designateclient>=2.7.0 # Apache-2.0
|
||||
python-neutronclient>=6.7.0 # Apache-2.0
|
||||
python-glanceclient>=2.8.0 # Apache-2.0
|
||||
python-troveclient>=2.2.0 # Apache-2.0
|
||||
python-barbicanclient>=7.0.0 # Apache-2.0
|
||||
jsonschema>=3.2.0 # MIT
|
||||
Jinja2>=2.10 # BSD License (3 clause)
|
||||
pexpect!=3.3,>=3.1 # ISC License
|
||||
@@ -46,6 +47,7 @@ docker>=4.2.0 # Apache-2.0
|
||||
psycopg2-binary>=2.6.2 # LGPL/ZPL
|
||||
semantic-version>=2.7.0 # BSD
|
||||
oslo.cache>=1.26.0 # Apache-2.0
|
||||
pyOpenSSL>=24.2.1 # Apache-2.0
|
||||
|
||||
# for trove network driver
|
||||
Flask>=2.2.3 # BSD
|
||||
|
||||
@@ -37,6 +37,8 @@ CONF.register_opts([
|
||||
openstack_cfg.BoolOpt('network_isolation',
|
||||
help='whether to plug user defined '
|
||||
'port to database container'),
|
||||
openstack_cfg.StrOpt('ssl_mode', default=None,
|
||||
help='Current SSL enabled mode.'),
|
||||
openstack_cfg.StrOpt('replication_strategy',
|
||||
default='trove.guestagent.strategies.replication.'
|
||||
'mysql_gtid.MysqlGTIDReplication',
|
||||
|
||||
@@ -129,6 +129,14 @@ class API(wsgi.Router):
|
||||
controller=instance_resource,
|
||||
action="guest_log_action",
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect("/{tenant_id}/instances/{id}/ssl",
|
||||
controller=instance_resource,
|
||||
action="ssl_show",
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect("/{tenant_id}/instances/{id}/ssl",
|
||||
controller=instance_resource,
|
||||
action="ssl_action",
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect("/{tenant_id}/instances/{id}/modules",
|
||||
controller=instance_resource,
|
||||
action="module_list",
|
||||
|
||||
@@ -125,6 +125,11 @@ common_opts = [
|
||||
help='Service type to use when searching catalog.'),
|
||||
cfg.StrOpt('trove_endpoint_type', default='publicURL',
|
||||
help='Service endpoint type to use when searching catalog.'),
|
||||
cfg.URIOpt('barbican_url', help='URL ending in ``AUTH_``.'),
|
||||
cfg.StrOpt('barbican_service_type', default='key-manager',
|
||||
help='Service type to use when fetching keys.'),
|
||||
cfg.StrOpt('barbican_endpoint_type', default='publicURL',
|
||||
help='Service endpoint type to use when fetching keys.'),
|
||||
cfg.IPOpt('host', default='0.0.0.0',
|
||||
help='Host to listen for RPC messages.'),
|
||||
cfg.IntOpt('report_interval', default=30,
|
||||
|
||||
@@ -19,10 +19,12 @@ from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common.strategies.cluster import strategy
|
||||
|
||||
from barbicanclient import client as BarbicanClient
|
||||
from cinderclient.v3 import client as CinderClient
|
||||
import glanceclient
|
||||
from keystoneauth1.identity import v3
|
||||
from keystoneauth1 import session as ka_session
|
||||
from keystoneauth1 import token_endpoint
|
||||
from keystoneclient.service_catalog import ServiceCatalog
|
||||
from neutronclient.v2_0 import client as NeutronClient
|
||||
from novaclient.client import Client
|
||||
@@ -214,6 +216,31 @@ def glance_client(context, region_name=None):
|
||||
)
|
||||
|
||||
|
||||
def barbican_client(context, region_name=None):
|
||||
if CONF.barbican_url:
|
||||
endpoint_url = CONF.barbican_url
|
||||
else:
|
||||
region = region_name or CONF.service_credentials.region_name
|
||||
endpoint_url = get_endpoint(
|
||||
context.service_catalog,
|
||||
service_type=CONF.barbican_service_type,
|
||||
endpoint_region=region,
|
||||
endpoint_type=CONF.barbican_endpoint_type
|
||||
)
|
||||
|
||||
auth = token_endpoint.Token(endpoint_url, context.auth_token)
|
||||
session = ka_session.Session(auth=auth)
|
||||
|
||||
return BarbicanClient.Client(
|
||||
'v1',
|
||||
session=session,
|
||||
project_id=context.project_id,
|
||||
service_type=CONF.barbican_service_type,
|
||||
interface=CONF.barbican_endpoint_type,
|
||||
endpoint=endpoint_url
|
||||
)
|
||||
|
||||
|
||||
def create_dns_client(*arg, **kwargs):
|
||||
return import_class(CONF.remote_dns_client)(*arg, **kwargs)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class RedisConfParser(object):
|
||||
class MySQLConfParser(object):
|
||||
|
||||
SERVER_CONF_SECTION = 'mysqld'
|
||||
CLIENT_CONF_SECTION = 'client'
|
||||
CODEC = stream_codecs.IniCodec(
|
||||
default_value='1', comment_markers=('#', ';', '!'))
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
# Copyright 2026 PS Cloud Services
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from barbicanclient import exceptions as barbican_exceptions
|
||||
from cryptography.hazmat.primitives.serialization \
|
||||
import pkcs12, Encoding, PrivateFormat, NoEncryption
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from datetime import datetime
|
||||
import OpenSSL.crypto as crypto
|
||||
from oslo_log import log as logging
|
||||
from trove.common import cfg
|
||||
from trove.common.clients import barbican_client
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MODE_BUILTIN = 'builtin'
|
||||
MODE_BASIC = 'basic'
|
||||
MODE_ENFORCED = 'enforced'
|
||||
MODE_MTLS = 'mtls'
|
||||
|
||||
|
||||
class TroveSSL(object):
|
||||
def __init__(self, context):
|
||||
self._context = context
|
||||
|
||||
# Fetch certificate, key and ca from p12 container
|
||||
def get_p12_bundle(self, p12_ref, password_ref=None):
|
||||
barbican = barbican_client(self._context)
|
||||
pkcs12_secret = barbican.secrets.get(p12_ref)
|
||||
|
||||
p12_pass = None
|
||||
if password_ref:
|
||||
p12_pass = barbican.secrets.get(password_ref).payload
|
||||
if isinstance(p12_pass, str):
|
||||
p12_pass = p12_pass.encode("utf-8")
|
||||
|
||||
pk, cert, ca_chain = pkcs12.load_key_and_certificates(
|
||||
pkcs12_secret.payload,
|
||||
p12_pass,
|
||||
backend=default_backend()
|
||||
)
|
||||
|
||||
pk = pk.private_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
format=PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=NoEncryption()
|
||||
).decode("utf-8")
|
||||
|
||||
cert = cert.public_bytes(Encoding.PEM).decode("utf-8")
|
||||
|
||||
ca_pems = ""
|
||||
if ca_chain:
|
||||
for ca in ca_chain:
|
||||
ca_pems += ca.public_bytes(
|
||||
Encoding.PEM).decode("utf-8")
|
||||
|
||||
return {
|
||||
'private_key': pk,
|
||||
'certificate': cert,
|
||||
'ca': ca_pems}
|
||||
|
||||
def certificate_details(self, cert_payload):
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_payload)
|
||||
|
||||
subject = cert.get_subject()
|
||||
expiry_bytes = cert.get_notAfter()
|
||||
expiry_str = expiry_bytes.decode("ascii")
|
||||
expiry = datetime.strptime(expiry_str, "%Y%m%d%H%M%SZ")
|
||||
|
||||
# Extract SANs
|
||||
san_list = []
|
||||
# Loop through all extensions in the certificate
|
||||
for i in range(cert.get_extension_count()):
|
||||
ext = cert.get_extension(i)
|
||||
# Check if the extension is 'subjectAltName'
|
||||
if ext.get_short_name() == b"subjectAltName":
|
||||
san_str = str(ext)
|
||||
san_list = [item.strip() for item in san_str.split(",")]
|
||||
|
||||
return {
|
||||
'cn': subject.CN,
|
||||
'expire_at': expiry,
|
||||
'san': san_list if san_list else None
|
||||
}
|
||||
|
||||
def register_consumer(self, uri, resource_type, resource_id):
|
||||
barbican = barbican_client(self._context)
|
||||
barbican.secrets.register_consumer(
|
||||
uri, 'database', resource_type, resource_id)
|
||||
|
||||
def remove_consumer(self, uri, resource_type, resource_id):
|
||||
barbican = barbican_client(self._context)
|
||||
barbican.secrets.remove_consumer(
|
||||
uri, 'database', resource_type, resource_id)
|
||||
|
||||
def disable_instance_ssl(self, instance):
|
||||
rollback_changes = []
|
||||
if instance.db_info.ssl_ref:
|
||||
try:
|
||||
args = [instance.db_info.ssl_ref, 'instance', instance.id]
|
||||
self.remove_consumer(*args)
|
||||
rollback_changes.append({'type': 'register', 'args': args})
|
||||
except barbican_exceptions.HTTPClientError as e:
|
||||
LOG.info('Unable to remove consumer for %s: %s',
|
||||
instance.db_info.ssl_ref, e)
|
||||
instance.db_info.ssl_ref = None
|
||||
instance.db_info.ssl_mode = None
|
||||
instance.db_info.save()
|
||||
return rollback_changes
|
||||
|
||||
def enable_instance_ssl(self, instance, mode, container_ref):
|
||||
rollback_changes = []
|
||||
if instance.db_info.ssl_ref and \
|
||||
instance.db_info.ssl_ref != container_ref:
|
||||
LOG.info("Instance %s already has container ref, tidying up %s",
|
||||
instance.id, instance.db_info.ssl_ref)
|
||||
# Certificate refresh, remove consumer from previous ref
|
||||
rollback_changes.extend(self.disable_instance_ssl(instance))
|
||||
|
||||
instance.db_info.ssl_mode = mode
|
||||
if container_ref:
|
||||
instance.db_info.ssl_ref = container_ref
|
||||
try:
|
||||
args = container_ref, 'instance', instance.id
|
||||
self.register_consumer(*args)
|
||||
rollback_changes.append({'type': 'remove', 'args': args})
|
||||
except barbican_exceptions.HTTPClientError as e:
|
||||
LOG.info('Unable to register consumer for %s: %s',
|
||||
container_ref, e)
|
||||
instance.db_info.save()
|
||||
return rollback_changes
|
||||
|
||||
def apply_ssl_state(self, instance, mode, container_ref, enable, disable):
|
||||
if enable:
|
||||
return self.enable_instance_ssl(instance, mode, container_ref)
|
||||
elif disable:
|
||||
return self.disable_instance_ssl(instance)
|
||||
|
||||
def rollback_ssl_state(self, instance, old_mode, old_ref,
|
||||
consumer_changes):
|
||||
LOG.info('Rolling back ssl state for %s', instance.id)
|
||||
instance.db_info.ssl_mode = old_mode
|
||||
instance.db_info.ssl_ref = old_ref
|
||||
instance.db_info.save()
|
||||
for state in reversed(consumer_changes):
|
||||
if state['type'] == 'remove':
|
||||
self.remove_consumer(*state['args'])
|
||||
elif state['type'] == 'register':
|
||||
self.register_consumer(*state['args'])
|
||||
|
||||
|
||||
def run_ssl_state_transaction(ssl_client, instances, mode, container_ref,
|
||||
enable, disable, execute, rollback):
|
||||
"""SSL state change transaction wrapper."""
|
||||
states = []
|
||||
try:
|
||||
# Apply SSL state to DB and Barbican
|
||||
for instance in instances:
|
||||
state = {
|
||||
'instance': instance,
|
||||
'mode': instance.db_info.ssl_mode,
|
||||
'ref': instance.db_info.ssl_ref}
|
||||
state['consumer_changes'] = ssl_client.apply_ssl_state(
|
||||
instance, mode, container_ref, enable, disable)
|
||||
states.append(state)
|
||||
# Execute the wrapped action (guest agent changes)
|
||||
result = execute()
|
||||
|
||||
except Exception as e:
|
||||
# Rollback all applied states if setup or execution fails
|
||||
LOG.exception(
|
||||
"Failed to execute SSL action: %s, rolling back states.", e)
|
||||
try:
|
||||
rollback()
|
||||
except Exception as exc:
|
||||
LOG.exception(
|
||||
"Guest SSL rollback failed, error: %s.", exc)
|
||||
for state in reversed(states):
|
||||
ssl_client.rollback_ssl_state(
|
||||
state['instance'], state['mode'], state['ref'],
|
||||
state['consumer_changes'])
|
||||
raise
|
||||
|
||||
return result
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""add ssl_mode and ssl_ref to instances
|
||||
|
||||
Revision ID: 95418fe2a1c8
|
||||
Revises: f90016d7baf8
|
||||
Create Date: 2026-02-20 17:23:54.492132
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import String, Text
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '95418fe2a1c8'
|
||||
down_revision: Union[str, None] = 'f90016d7baf8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
'instances',
|
||||
sa.Column(
|
||||
'ssl_mode',
|
||||
String(16),
|
||||
nullable=True
|
||||
)
|
||||
)
|
||||
op.add_column(
|
||||
'instances',
|
||||
sa.Column(
|
||||
'ssl_ref',
|
||||
Text(),
|
||||
nullable=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('instances', 'ssl_mode')
|
||||
op.drop_column('instances', 'ssl_ref')
|
||||
+37
-1
@@ -45,7 +45,7 @@ class API(object):
|
||||
|
||||
# API_LATEST_VERSION should bump the minor number each time
|
||||
# a method signature is added or changed
|
||||
API_LATEST_VERSION = '1.2'
|
||||
API_LATEST_VERSION = '1.3'
|
||||
|
||||
# API_BASE_VERSION should only change on major version upgrade
|
||||
API_BASE_VERSION = '1.0'
|
||||
@@ -60,6 +60,7 @@ class API(object):
|
||||
'ussuri': '1.0',
|
||||
'victoria': '1.1',
|
||||
'epoxy': '1.2',
|
||||
'hibiscus': '1.3',
|
||||
|
||||
'latest': API_LATEST_VERSION
|
||||
}
|
||||
@@ -663,6 +664,41 @@ class API(object):
|
||||
enable=enable, disable=disable,
|
||||
publish=publish, discard=discard)
|
||||
|
||||
def ssl_show(self):
|
||||
LOG.debug("Showing SSL guest status for %s.", self.id)
|
||||
version = '1.3'
|
||||
if not self.client.can_send_version(version):
|
||||
raise exception.GuestError(
|
||||
original_message=("Operation requires guest version %s or "
|
||||
"later" % version))
|
||||
|
||||
result = self._call("ssl_show", self.agent_high_timeout,
|
||||
version=version)
|
||||
return result
|
||||
|
||||
def ssl_action(self, mode, container, enable, disable):
|
||||
LOG.debug("Executing SSL action for %s.", self.id)
|
||||
version = '1.3'
|
||||
if not self.client.can_send_version(version):
|
||||
raise exception.GuestError(
|
||||
original_message=("Operation requires guest version %s or "
|
||||
"later" % version))
|
||||
|
||||
return self._call("ssl_action", self.agent_high_timeout,
|
||||
version=version, mode=mode, container=container,
|
||||
enable=enable, disable=disable)
|
||||
|
||||
def ssl_rollback(self):
|
||||
LOG.debug("Rolling back SSL configuration for %s.", self.id)
|
||||
version = '1.3'
|
||||
if not self.client.can_send_version(version):
|
||||
raise exception.GuestError(
|
||||
original_message=("Operation requires guest version %s or "
|
||||
"later" % version))
|
||||
|
||||
return self._call("ssl_rollback", self.agent_high_timeout,
|
||||
version=version)
|
||||
|
||||
def module_list(self, include_contents):
|
||||
LOG.debug("Querying modules on %s (contents: %s).",
|
||||
self.id, include_contents)
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
#
|
||||
|
||||
import abc
|
||||
import configparser
|
||||
import operator
|
||||
import os
|
||||
|
||||
import docker
|
||||
from oslo_config import cfg as oslo_cfg
|
||||
@@ -29,6 +31,7 @@ from trove.common.notification import EndNotification
|
||||
from trove.guestagent.common import guestagent_utils
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.common.operating_system import FileMode
|
||||
from trove.guestagent.datastore.ssl import SSLManager
|
||||
from trove.guestagent import dbaas
|
||||
from trove.guestagent import guest_log
|
||||
from trove.guestagent.module import driver_manager
|
||||
@@ -41,7 +44,7 @@ LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class Manager(periodic_task.PeriodicTasks):
|
||||
class Manager(periodic_task.PeriodicTasks, SSLManager):
|
||||
"""This is the base class for all datastore managers. Over time, common
|
||||
functionality should be pulled back here from the existing managers.
|
||||
"""
|
||||
@@ -912,6 +915,19 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
'replica_conf': replica_conf
|
||||
}
|
||||
|
||||
ssl_status = self.show_ssl_status()
|
||||
if ssl_status['status'] == 'on' and CONF.ssl_mode and \
|
||||
CONF.ssl_mode != 'None':
|
||||
LOG.info(
|
||||
"SSL is enabled, adding CA and certificates "
|
||||
"to the replication snapshot")
|
||||
replication_snapshot['cert_container'] = {}
|
||||
replication_snapshot['ssl_mode'] = CONF.ssl_mode
|
||||
|
||||
for key, filename in self._get_ssl_files().items():
|
||||
content = operating_system.read_file(filename, as_root=True)
|
||||
replication_snapshot['cert_container'][key] = content
|
||||
|
||||
return replication_snapshot
|
||||
|
||||
def attach_replica(self, context, snapshot, slave_config, restart=False):
|
||||
@@ -983,3 +999,25 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
LOG.error("Run post_create_backup failed, error: %s" % str(e))
|
||||
|
||||
return {}
|
||||
|
||||
def override_guest_info(self, **kwargs):
|
||||
if ('/' in CONF.guest_info):
|
||||
# Set guest_info_file to exactly guest_info from the conf file.
|
||||
# This should be /etc/guest_info for pre-Kilo compatibility.
|
||||
guest_info_file = CONF.guest_info
|
||||
else:
|
||||
guest_info_file = os.path.join(CONF.injected_config_location,
|
||||
CONF.guest_info)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(guest_info_file)
|
||||
|
||||
LOG.debug('Override guest_info.conf: %s', kwargs)
|
||||
|
||||
config.defaults().update(kwargs)
|
||||
|
||||
with open(guest_info_file, 'w') as f:
|
||||
config.write(f)
|
||||
|
||||
for key, value in kwargs.items():
|
||||
CONF.set_override(key, value)
|
||||
@@ -15,6 +15,7 @@
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils.excutils import save_and_reraise_exception
|
||||
|
||||
import semantic_version
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common import utils
|
||||
@@ -143,3 +144,31 @@ class Manager(manager.MySqlManager):
|
||||
str(err))
|
||||
pass
|
||||
LOG.info('Finished to reset password for restore')
|
||||
|
||||
def _get_default_tls_versions(self):
|
||||
mariadb_10_4_6 = semantic_version.Version('10.4.6')
|
||||
cur_ver = semantic_version.Version.coerce(CONF.datastore_version)
|
||||
|
||||
if cur_ver <= mariadb_10_4_6:
|
||||
return 'TLSv1.2'
|
||||
else:
|
||||
return 'TLSv1.2,TLSv1.3'
|
||||
|
||||
def _get_disable_ssl_overrides(self):
|
||||
return {
|
||||
# Change of ssl_cert to empty value leads to error in MariaDB
|
||||
# startup process
|
||||
'ssl': 'false',
|
||||
# The only reliable way to disable SSL is to unset tls_version.
|
||||
'tls_version': ''
|
||||
}
|
||||
|
||||
def _get_enable_ssl_overrides(self):
|
||||
files = self._get_ssl_files()
|
||||
return {
|
||||
'ssl_cert': files['certificate'],
|
||||
'ssl_key': files['private_key'],
|
||||
'ssl_ca': files['ca'],
|
||||
'ssl': 'on',
|
||||
'tls_version': self._get_default_tls_versions()
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
import semantic_version
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.guestagent.common import operating_system
|
||||
@@ -65,3 +66,71 @@ class Manager(manager.MySqlManager):
|
||||
raise exception.BackupCreationError(str(e))
|
||||
|
||||
return status
|
||||
|
||||
def _get_default_tls_versions(self):
|
||||
# NOTE(mangust404): to operate correctly, your datastore version
|
||||
# should contain patch version number. Datastore name like "5.7"
|
||||
# wouldn't be able to use TLSv1.3
|
||||
mysql_5_7_0 = semantic_version.Version('5.7.0')
|
||||
mysql_5_7_27 = semantic_version.Version('5.7.27')
|
||||
|
||||
cur_ver = semantic_version.Version.coerce(CONF.datastore_version)
|
||||
|
||||
if cur_ver < mysql_5_7_0:
|
||||
# For MySQL 5.6 there is no TLS support
|
||||
raise exception.BadRequest(
|
||||
'No support of TLS for MySQL prior to 5.7')
|
||||
elif cur_ver <= mysql_5_7_27:
|
||||
return 'TLSv1,TLSv1.1,TLSv1.2'
|
||||
elif cur_ver > mysql_5_7_27:
|
||||
return 'TLSv1.2,TLSv1.3'
|
||||
|
||||
def _get_enable_client_ssl_overrides(self):
|
||||
mysql_8 = semantic_version.Version('8.0.0')
|
||||
cur_ver = semantic_version.Version.coerce(CONF.datastore_version)
|
||||
if cur_ver >= mysql_8:
|
||||
return {'ssl-mode': 'REQUIRED'}
|
||||
else:
|
||||
return {'ssl': 'on'}
|
||||
|
||||
def _get_disable_client_ssl_overrides(self):
|
||||
mysql_8 = semantic_version.Version('8.0.0')
|
||||
cur_ver = semantic_version.Version.coerce(CONF.datastore_version)
|
||||
if cur_ver >= mysql_8:
|
||||
return {'ssl-mode': 'DISABLED'}
|
||||
else:
|
||||
return {'ssl': 'off'}
|
||||
|
||||
def disable_ssl_certificate(self):
|
||||
cur_ver = semantic_version.Version.coerce(CONF.datastore_version)
|
||||
mysql_8_0 = semantic_version.Version('8.0.0')
|
||||
|
||||
if cur_ver >= mysql_8_0:
|
||||
# Starting from MySQL 8.0 there is no reliable way to disable
|
||||
# SSL/TLS completely because caching_sha2_password depends on it
|
||||
raise exception.TroveError("Not supported for MySQL 8.0 and above")
|
||||
|
||||
return super(Manager, self).disable_ssl_certificate()
|
||||
|
||||
def _get_enable_ssl_overrides(self):
|
||||
files = self._get_ssl_files()
|
||||
overrides = {
|
||||
'ssl_cert': files['certificate'],
|
||||
'ssl_key': files['private_key'],
|
||||
'ssl_ca': files['ca'],
|
||||
'tls_version': self._get_default_tls_versions(),
|
||||
'require_secure_transport': 'ON'
|
||||
}
|
||||
return overrides
|
||||
|
||||
def _get_disable_ssl_overrides(self):
|
||||
overrides = {
|
||||
'ssl_cert': '',
|
||||
'ssl_key': '',
|
||||
'ssl_ca': '',
|
||||
# The only reliable way to disable SSL is to unset tls_version.
|
||||
'tls_version': '',
|
||||
'require_secure_transport': 'OFF'
|
||||
}
|
||||
|
||||
return overrides
|
||||
@@ -24,6 +24,7 @@ from trove.common import configurations
|
||||
from trove.common import constants
|
||||
from trove.common import exception
|
||||
from trove.common.notification import EndNotification
|
||||
from trove.common import ssl
|
||||
from trove.common import utils
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.datastore import manager
|
||||
@@ -272,6 +273,11 @@ class MySqlManager(manager.Manager):
|
||||
if 'replication_strategy' in replica_info:
|
||||
self._validate_slave_for_replication(context, replica_info)
|
||||
|
||||
if 'cert_container' in replica_info and \
|
||||
'ssl_mode' in replica_info:
|
||||
self.enable_ssl_certificate(
|
||||
replica_info['ssl_mode'], replica_info['cert_container'],
|
||||
is_startup=True)
|
||||
self.replication.enable_as_slave(self.app, replica_info,
|
||||
slave_config)
|
||||
except Exception as err:
|
||||
@@ -322,6 +328,12 @@ class MySqlManager(manager.Manager):
|
||||
self.app.configuration_manager.reset_configuration(config_contents)
|
||||
self.app.update_overrides(config_overrides)
|
||||
|
||||
if self.ssl_mode_at_least(CONF.ssl_mode, ssl.MODE_BASIC):
|
||||
cert_container = self._read_ssl_files()
|
||||
self.enable_ssl_certificate(
|
||||
CONF.ssl_mode, cert_container,
|
||||
apply_overrides=False, is_startup=True)
|
||||
|
||||
# Start database service.
|
||||
command = self.get_start_db_params(data_dir)
|
||||
self.app.start_db(ds_version=ds_version, command=command)
|
||||
@@ -344,3 +356,98 @@ class MySqlManager(manager.Manager):
|
||||
LOG.error("Run post_create_backup failed, error: %s" % str(e))
|
||||
|
||||
return {}
|
||||
|
||||
def _get_default_tls_versions(self):
|
||||
raise Exception('not implemented')
|
||||
|
||||
def _get_ssl_files(self):
|
||||
datadir = self.app.get_data_dir()
|
||||
return {
|
||||
"private_key": f"{datadir}/server-key.pem",
|
||||
"certificate": f"{datadir}/server-cert.pem",
|
||||
"ca": f"{datadir}/ca.pem"
|
||||
}
|
||||
|
||||
def _get_enable_ssl_overrides(self):
|
||||
files = self._get_ssl_files()
|
||||
return {
|
||||
'ssl_cert': files['certificate'],
|
||||
'ssl_key': files['private_key'],
|
||||
'ssl_ca': files['ca'],
|
||||
'tls_version': self._get_default_tls_versions()
|
||||
}
|
||||
|
||||
def _get_enable_client_ssl_overrides(self):
|
||||
return {'ssl': 'on'}
|
||||
|
||||
def _enable_ssl_certificate_impl(self, mode, apply_overrides=True):
|
||||
overrides = self._get_enable_ssl_overrides()
|
||||
self.app.update_overrides(overrides)
|
||||
|
||||
client_overrides = self._get_enable_client_ssl_overrides()
|
||||
self.app.update_client_overrides(client_overrides)
|
||||
|
||||
if apply_overrides:
|
||||
self.adm.set_users_access_mode(mode)
|
||||
|
||||
self.status.set_status(
|
||||
service_status.ServiceStatuses.RESTART_REQUIRED)
|
||||
|
||||
# Restart is required
|
||||
return True
|
||||
|
||||
def _get_disable_ssl_overrides(self):
|
||||
return {
|
||||
'ssl_cert': '',
|
||||
'ssl_key': '',
|
||||
'ssl_ca': '',
|
||||
# The only reliable way to disable SSL is to unset tls_version.
|
||||
'tls_version': ''
|
||||
}
|
||||
|
||||
def _get_disable_client_ssl_overrides(self):
|
||||
return {'ssl': 'off'}
|
||||
|
||||
def _disable_ssl_certificate_impl(self, apply_overrides=True):
|
||||
overrides = self._get_disable_ssl_overrides()
|
||||
self.app.update_overrides(overrides)
|
||||
|
||||
client_overrides = self._get_disable_client_ssl_overrides()
|
||||
self.app.update_client_overrides(client_overrides)
|
||||
|
||||
self.status.set_status(
|
||||
service_status.ServiceStatuses.RESTART_REQUIRED)
|
||||
|
||||
# Restart is required
|
||||
return True
|
||||
|
||||
def _get_ssl_cert(self):
|
||||
result = self.app.execute_sql(
|
||||
'SHOW VARIABLES LIKE "ssl_cert"')
|
||||
return result.first()._mapping['Value']
|
||||
|
||||
def _get_tls_version(self):
|
||||
result = self.app.execute_sql(
|
||||
'SHOW VARIABLES LIKE "tls_version"')
|
||||
return result.first()._mapping['Value']
|
||||
|
||||
def show_ssl_status(self):
|
||||
tls_version = self._get_tls_version()
|
||||
ssl_cert = self._get_ssl_cert()
|
||||
|
||||
if tls_version and ssl_cert:
|
||||
if not os.path.isabs(ssl_cert):
|
||||
ssl_cert = os.path.join(
|
||||
self.app.get_data_dir(),
|
||||
ssl_cert)
|
||||
|
||||
certificate = operating_system.read_file(
|
||||
ssl_cert,
|
||||
as_root=True)
|
||||
|
||||
return {
|
||||
'status': 'on',
|
||||
'certificate': certificate
|
||||
}
|
||||
|
||||
return {'status': 'off'}
|
||||
@@ -29,6 +29,7 @@ from trove.common import constants
|
||||
from trove.common.db.mysql import models
|
||||
from trove.common import exception
|
||||
from trove.common.i18n import _
|
||||
from trove.common import ssl
|
||||
from trove.common import utils
|
||||
from trove.guestagent.common.configuration import ConfigurationManager
|
||||
from trove.guestagent.common.configuration import ImportOverrideStrategy
|
||||
@@ -397,6 +398,61 @@ class BaseMySqlAdmin(object, metaclass=abc.ABCMeta):
|
||||
user = self._get_user(username, hostname)
|
||||
return user.databases
|
||||
|
||||
def set_users_access_mode(self, mode):
|
||||
"""Set access mode for all non-system users.
|
||||
|
||||
Modes:
|
||||
- basic: no SSL requirement
|
||||
- enforced: REQUIRE SSL
|
||||
- mtls: REQUIRE X509
|
||||
"""
|
||||
if mode not in (ssl.MODE_BASIC,
|
||||
ssl.MODE_ENFORCED, ssl.MODE_MTLS):
|
||||
raise exception.BadRequest(
|
||||
_("Unknown access mode: %s") % mode
|
||||
)
|
||||
|
||||
LOG.info("Setting users access mode to '%s'", mode)
|
||||
|
||||
with mysql_util.SqlClient(
|
||||
self.mysql_app.get_engine(), use_flush=True) as client:
|
||||
|
||||
# Get all users except system ones
|
||||
q = sql_query.Query()
|
||||
q.columns = ['User', 'Host']
|
||||
q.tables = ['mysql.user']
|
||||
q.where = ["Host != 'localhost'"]
|
||||
t = text(str(q))
|
||||
result = client.execute(t).fetchall()
|
||||
|
||||
for row in result:
|
||||
username = row._mapping['User']
|
||||
host = row._mapping['Host']
|
||||
|
||||
# Skip system users
|
||||
if username in ['root', 'os_admin', 'exporter']:
|
||||
continue
|
||||
if username.startswith('slave_'):
|
||||
continue
|
||||
|
||||
try:
|
||||
user = models.MySQLUser(name=username, host=host)
|
||||
user.check_reserved()
|
||||
except ValueError:
|
||||
# skip invalid/reserved users
|
||||
continue
|
||||
|
||||
# Build ALTER USER statement
|
||||
if mode == ssl.MODE_BASIC:
|
||||
stmt = f"ALTER USER '{username}'@'{host}' REQUIRE NONE"
|
||||
elif mode == ssl.MODE_ENFORCED:
|
||||
stmt = f"ALTER USER '{username}'@'{host}' REQUIRE SSL"
|
||||
elif mode == ssl.MODE_MTLS:
|
||||
stmt = f"ALTER USER '{username}'@'{host}' REQUIRE X509"
|
||||
|
||||
LOG.debug("Altering user access mode: %s", stmt)
|
||||
client.execute(text(stmt))
|
||||
|
||||
|
||||
class BaseMySqlApp(service.BaseDbApp):
|
||||
_configuration_manager = None
|
||||
@@ -528,6 +584,11 @@ class BaseMySqlApp(service.BaseDbApp):
|
||||
self.configuration_manager.apply_user_override(
|
||||
{MySQLConfParser.SERVER_CONF_SECTION: overrides})
|
||||
|
||||
def update_client_overrides(self, overrides):
|
||||
if overrides:
|
||||
self.configuration_manager.apply_user_override(
|
||||
{MySQLConfParser.CLIENT_CONF_SECTION: overrides})
|
||||
|
||||
def apply_overrides(self, overrides):
|
||||
with mysql_util.SqlClient(self.get_engine()) as client:
|
||||
for k, v in overrides.items():
|
||||
|
||||
@@ -22,6 +22,7 @@ from trove.common import cfg
|
||||
from trove.common import constants
|
||||
from trove.common import exception
|
||||
from trove.common.notification import EndNotification
|
||||
from trove.common import ssl
|
||||
from trove.common import utils
|
||||
from trove.guestagent.common import guestagent_utils
|
||||
from trove.guestagent.common import operating_system
|
||||
@@ -48,6 +49,13 @@ class PostgresManager(manager.Manager):
|
||||
def configuration_manager(self):
|
||||
return self.app.configuration_manager
|
||||
|
||||
def _apply_access_rules(self, mode=None):
|
||||
mode = mode or CONF.ssl_mode
|
||||
|
||||
self.app.apply_access_rules(
|
||||
ssl=self.ssl_mode_at_least(mode, ssl.MODE_ENFORCED),
|
||||
mtls=self.ssl_mode_at_least(mode, ssl.MODE_MTLS))
|
||||
|
||||
def _get_max_wal_size_bytes(self):
|
||||
"""Get PostgreSQL max_wal_size as bytes (default 1GB)."""
|
||||
value = self.app.get_config_param('max_wal_size')
|
||||
@@ -160,7 +168,7 @@ class PostgresManager(manager.Manager):
|
||||
self.app.update_overrides(overrides)
|
||||
|
||||
# Prepare pg_hba.conf
|
||||
self.app.apply_access_rules()
|
||||
self._apply_access_rules()
|
||||
self.configuration_manager.apply_system_override(
|
||||
{'hba_file': service.HBA_CONFIG_FILE})
|
||||
|
||||
@@ -255,6 +263,11 @@ class PostgresManager(manager.Manager):
|
||||
"""Set up the standby server."""
|
||||
self.replication.enable_as_slave(self.app, replica_info, None)
|
||||
|
||||
if 'cert_container' in replica_info:
|
||||
self.enable_ssl_certificate(
|
||||
replica_info['ssl_mode'], replica_info['cert_container'],
|
||||
apply_overrides=False, is_startup=True)
|
||||
|
||||
# For the previous primary, don't start db service in order to run
|
||||
# pg_rewind command next.
|
||||
if restart:
|
||||
@@ -323,8 +336,15 @@ class PostgresManager(manager.Manager):
|
||||
self.app.set_data_dir(self.app.datadir)
|
||||
self.app.update_overrides(config_overrides)
|
||||
|
||||
# Enable SSL, if necessary
|
||||
if self.ssl_mode_at_least(CONF.ssl_mode, ssl.MODE_BASIC):
|
||||
cert_container = self._read_ssl_files()
|
||||
self.enable_ssl_certificate(
|
||||
CONF.ssl_mode, cert_container,
|
||||
apply_overrides=False, is_startup=True)
|
||||
|
||||
# force to create pg_hba.conf on rebuild
|
||||
self.app.apply_access_rules()
|
||||
self._apply_access_rules()
|
||||
self.app.configuration_manager.apply_system_override(
|
||||
{'hba_file': service.HBA_CONFIG_FILE})
|
||||
|
||||
@@ -420,3 +440,65 @@ class PostgresManager(manager.Manager):
|
||||
'wal_size': operating_system.get_dir_size(wal_dir, as_root=True),
|
||||
'wal_archive_size': operating_system.get_dir_size(wal_archive_dir)
|
||||
}
|
||||
|
||||
def _get_ssl_files(self):
|
||||
return {
|
||||
'private_key': f"{self.app.datadir}/server.key",
|
||||
'certificate': f"{self.app.datadir}/server.crt",
|
||||
'ca': f"{self.app.datadir}/ca.crt"
|
||||
}
|
||||
|
||||
# SSL modes (configured via pg_hba.conf):
|
||||
# basic - SSL enabled, client cert not required, md5 auth
|
||||
# enforced - SSL required (hostssl), md5 auth still used
|
||||
# mtls - SSL required, client cert required (verify-full),
|
||||
# authentication via certificate (CN must match user)
|
||||
def _enable_ssl_certificate_impl(self, mode, apply_overrides=True):
|
||||
LOG.debug('Enable SSL mode: %s', mode)
|
||||
|
||||
self._apply_access_rules(mode)
|
||||
|
||||
overrides = {
|
||||
'ssl': 'on',
|
||||
'ssl_ca_file': 'ca.crt',
|
||||
'ssl_cert_file': 'server.crt',
|
||||
'ssl_key_file': 'server.key'
|
||||
}
|
||||
|
||||
self.app.update_overrides(overrides)
|
||||
if apply_overrides:
|
||||
self.app.apply_overrides(overrides)
|
||||
|
||||
# PostgreSQL reloads SSL configuration without restart.
|
||||
return False
|
||||
|
||||
def _disable_ssl_certificate_impl(self, apply_overrides=True):
|
||||
LOG.debug('Disable SSL')
|
||||
|
||||
self._apply_access_rules(None)
|
||||
|
||||
overrides = {
|
||||
'ssl': 'off'
|
||||
}
|
||||
|
||||
self.app.update_overrides(overrides)
|
||||
if apply_overrides:
|
||||
self.app.apply_overrides(overrides)
|
||||
|
||||
# PostgreSQL reloads SSL configuration without restart.
|
||||
return False
|
||||
|
||||
def show_ssl_status(self):
|
||||
status = self.app.adm.query('SHOW ssl')[0][0]
|
||||
|
||||
if status != 'on':
|
||||
return {'status': status}
|
||||
|
||||
certificate = operating_system.read_file(
|
||||
self._get_ssl_files()['certificate'],
|
||||
as_root=True)
|
||||
|
||||
return {
|
||||
'status': status,
|
||||
'certificate': certificate
|
||||
}
|
||||
@@ -11,12 +11,12 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from collections import OrderedDict
|
||||
import os
|
||||
|
||||
from oslo_log import log as logging
|
||||
import psycopg2
|
||||
|
||||
from collections import OrderedDict
|
||||
from trove.common import cfg
|
||||
from trove.common import constants
|
||||
from trove.common.db.postgresql import models
|
||||
@@ -35,6 +35,7 @@ LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
SUPER_USER_NAME = "postgres"
|
||||
REPLICATION_USER = 'replicator'
|
||||
CONFIG_FILE = "/etc/postgresql/postgresql.conf"
|
||||
CNF_EXT = 'conf'
|
||||
# The same with include_dir config option
|
||||
@@ -94,7 +95,8 @@ class PgSqlApp(service.BaseDbApp):
|
||||
cmd = f"pg_ctl reload -D {self.datadir}"
|
||||
docker_util.run_command(self.docker_client, cmd)
|
||||
|
||||
def apply_access_rules(self):
|
||||
def apply_access_rules(self, ssl=False, mtls=False,
|
||||
replication_user=None):
|
||||
"""PostgreSQL Client authentication settings
|
||||
|
||||
The order of entries is important. The first failure to authenticate
|
||||
@@ -103,18 +105,34 @@ class PgSqlApp(service.BaseDbApp):
|
||||
"""
|
||||
LOG.debug("Applying client authentication access rules.")
|
||||
|
||||
host_d = 'hostssl' if ssl else 'host'
|
||||
if mtls:
|
||||
auth_method = ['cert', 'clientcert=verify-full']
|
||||
else:
|
||||
auth_method = ['md5']
|
||||
|
||||
host_rules = [
|
||||
['all', SUPER_USER_NAME, '127.0.0.1/32', 'trust'],
|
||||
['all', SUPER_USER_NAME, '::1/128', 'trust'],
|
||||
['all', SUPER_USER_NAME, 'localhost', 'trust'],
|
||||
['all', SUPER_USER_NAME, '0.0.0.0/0', 'reject'],
|
||||
['all', SUPER_USER_NAME, '::/0', 'reject'],
|
||||
['all', 'all', '0.0.0.0/0', *auth_method],
|
||||
['all', 'all', '::/0', *auth_method],
|
||||
]
|
||||
if replication_user:
|
||||
host_rules.extend([
|
||||
['replication', replication_user, '0.0.0.0/0', 'md5'],
|
||||
['replication', replication_user, '::/0', 'md5'],
|
||||
])
|
||||
|
||||
access_rules = OrderedDict(
|
||||
[('local', [['all', SUPER_USER_NAME, None, 'trust'],
|
||||
['replication', SUPER_USER_NAME, None, 'trust'],
|
||||
['all', 'all', None, 'md5']]),
|
||||
('host', [['all', SUPER_USER_NAME, '127.0.0.1/32', 'trust'],
|
||||
['all', SUPER_USER_NAME, '::1/128', 'trust'],
|
||||
['all', SUPER_USER_NAME, 'localhost', 'trust'],
|
||||
['all', SUPER_USER_NAME, '0.0.0.0/0', 'reject'],
|
||||
['all', SUPER_USER_NAME, '::/0', 'reject'],
|
||||
['all', 'all', '0.0.0.0/0', 'md5'],
|
||||
['all', 'all', '::/0', 'md5']])
|
||||
(host_d, host_rules)
|
||||
])
|
||||
|
||||
operating_system.write_file(
|
||||
HBA_CONFIG_FILE, access_rules,
|
||||
stream_codecs.PropertiesCodec(string_mappings={'\t': None}),
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# Copyright 2026 PS Cloud Services
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import abc
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from oslo_log import log as logging
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common import ssl
|
||||
from trove.common.ssl import TroveSSL
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.common.operating_system import FileMode
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class SSLManager(object):
|
||||
def _get_ssl_state_file(self):
|
||||
return os.path.join(
|
||||
CONF.injected_config_location,
|
||||
"ssl_state.json")
|
||||
|
||||
def _write_ssl_state(self, state):
|
||||
state_file = self._get_ssl_state_file()
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
dir=os.path.dirname(state_file),
|
||||
delete=False) as fp:
|
||||
json.dump(state, fp)
|
||||
fp.flush()
|
||||
os.fsync(fp.fileno())
|
||||
tmp_name = fp.name
|
||||
|
||||
os.replace(tmp_name, state_file)
|
||||
|
||||
def _read_ssl_state(self):
|
||||
with open(self._get_ssl_state_file()) as fp:
|
||||
return json.load(fp)
|
||||
|
||||
def _delete_ssl_state(self):
|
||||
state_file = self._get_ssl_state_file()
|
||||
if os.path.exists(state_file):
|
||||
os.unlink(state_file)
|
||||
|
||||
def _write_ssl_files(self, cert_container):
|
||||
for key, filename in self._get_ssl_files().items():
|
||||
payload = cert_container.get(key)
|
||||
if not payload:
|
||||
continue
|
||||
operating_system.write_file(
|
||||
filename,
|
||||
payload,
|
||||
as_root=True)
|
||||
operating_system.chown(
|
||||
filename,
|
||||
user=self.app.database_service_uid,
|
||||
group=self.app.database_service_gid,
|
||||
force=True,
|
||||
as_root=True)
|
||||
operating_system.chmod(
|
||||
filename,
|
||||
FileMode.SET_USR_RW(),
|
||||
as_root=True)
|
||||
operating_system.sync(os.path.dirname(filename))
|
||||
|
||||
def _read_ssl_files(self):
|
||||
files = {}
|
||||
|
||||
for key, filename in self._get_ssl_files().items():
|
||||
files[key] = operating_system.read_file(
|
||||
filename,
|
||||
as_root=True)
|
||||
|
||||
return files
|
||||
|
||||
def _save_ssl_state(self):
|
||||
status = self.show_ssl_status()
|
||||
state = {
|
||||
"status": status["status"],
|
||||
"mode": CONF.ssl_mode,
|
||||
}
|
||||
if status["status"] == "on":
|
||||
state["files"] = self._read_ssl_files()
|
||||
|
||||
self._write_ssl_state(state)
|
||||
|
||||
def ssl_mode_at_least(self, current_mode, required_mode):
|
||||
priorities = [
|
||||
ssl.MODE_BASIC, ssl.MODE_ENFORCED, ssl.MODE_MTLS]
|
||||
try:
|
||||
return (
|
||||
priorities.index(current_mode) >=
|
||||
priorities.index(required_mode)
|
||||
)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def ssl_show(self, context):
|
||||
LOG.info("Getting ssl status for instance")
|
||||
result = self.show_ssl_status()
|
||||
if result["status"] == "on":
|
||||
result["mode"] = CONF.ssl_mode
|
||||
ssl = TroveSSL(context)
|
||||
cert_details = ssl.certificate_details(result["certificate"])
|
||||
cert_details["payload"] = result["certificate"]
|
||||
del result["certificate"]
|
||||
return {**result, "certificate": cert_details}
|
||||
else:
|
||||
return result
|
||||
|
||||
def ssl_action(self, context, mode, container, enable, disable):
|
||||
if enable and disable:
|
||||
raise exception.BadRequest("Cannot enable and disable ssl")
|
||||
LOG.info(
|
||||
"SSL action for instance: mode=%s, enable=%s, disable=%s",
|
||||
mode, enable, disable)
|
||||
result = {}
|
||||
if enable:
|
||||
result["restart_required"] = self.enable_ssl_certificate(
|
||||
mode, container)
|
||||
elif disable:
|
||||
result["restart_required"] = self.disable_ssl_certificate()
|
||||
return result
|
||||
|
||||
def ssl_rollback(self, context):
|
||||
state = self._read_ssl_state()
|
||||
LOG.info("Rolling back ssl configuration for instance")
|
||||
LOG.debug("Previous ssl status: %s, mode: %s",
|
||||
state['status'], state['mode'])
|
||||
result = {
|
||||
"status": state["status"]
|
||||
}
|
||||
if state["status"] == "off":
|
||||
result["restart_required"] = self._disable_ssl_certificate_impl()
|
||||
self.override_guest_info(ssl_mode=None)
|
||||
result["mode"] = None
|
||||
else:
|
||||
self._write_ssl_files(state["files"])
|
||||
result["restart_required"] = self._enable_ssl_certificate_impl(
|
||||
state["mode"])
|
||||
self.override_guest_info(ssl_mode=state["mode"])
|
||||
result["mode"] = state["mode"]
|
||||
|
||||
self._delete_ssl_state()
|
||||
return result
|
||||
|
||||
def enable_ssl_certificate(
|
||||
self, mode, cert_container, apply_overrides=True,
|
||||
is_startup=False):
|
||||
LOG.debug("Called enable_ssl_certificate, mode=%s", mode)
|
||||
if not is_startup:
|
||||
self._save_ssl_state()
|
||||
self._write_ssl_files(cert_container)
|
||||
restart_required = self._enable_ssl_certificate_impl(
|
||||
mode, apply_overrides=apply_overrides)
|
||||
self.override_guest_info(ssl_mode=mode)
|
||||
return restart_required
|
||||
|
||||
def disable_ssl_certificate(self):
|
||||
LOG.debug("Called disable_ssl_certificate")
|
||||
self._save_ssl_state()
|
||||
restart_required = self._disable_ssl_certificate_impl()
|
||||
self.override_guest_info(ssl_mode=None)
|
||||
return restart_required
|
||||
|
||||
def _enable_ssl_certificate_impl(self, mode, apply_overrides=True):
|
||||
raise exception.TroveError("ssl enable not implemented")
|
||||
|
||||
def _disable_ssl_certificate_impl(self, apply_overrides=True):
|
||||
raise exception.TroveError("ssl disable not implemented")
|
||||
|
||||
def show_ssl_status(self):
|
||||
"""Manager should return current ssl certificate details and current
|
||||
ssl status (on/off)
|
||||
"""
|
||||
return {"status": "not implemented"}
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_ssl_files(self):
|
||||
"""Manager should return dict with private_key, certificate and ca
|
||||
as keys and corresponding paths as values.
|
||||
"""
|
||||
return {}
|
||||
@@ -104,6 +104,17 @@ class MariaDBGTIDReplication(mysql_base.MysqlReplicationBase):
|
||||
'user': replica_conf['replication_user']['name'],
|
||||
'password': replica_conf['replication_user']['password']
|
||||
})
|
||||
|
||||
if 'cert_container' in master_info and \
|
||||
'ssl_mode' in master_info:
|
||||
change_master_cmd = change_master_cmd + (
|
||||
", MASTER_SSL_VERIFY_SERVER_CERT=1"
|
||||
", MASTER_SSL=1")
|
||||
else:
|
||||
change_master_cmd = change_master_cmd + (
|
||||
", MASTER_SSL_VERIFY_SERVER_CERT=0"
|
||||
", MASTER_SSL=0")
|
||||
|
||||
service.execute_sql(change_master_cmd)
|
||||
|
||||
service.start_slave()
|
||||
@@ -73,6 +73,11 @@ class MysqlGTIDReplication(mysql_base.MysqlReplicationBase):
|
||||
'user': replica_conf['replication_user']['name'],
|
||||
'password': replica_conf['replication_user']['password']
|
||||
})
|
||||
if 'cert_container' in master_info and \
|
||||
'ssl_mode' in master_info:
|
||||
change_master_cmd = change_master_cmd + (
|
||||
",SOURCE_SSL=1"
|
||||
)
|
||||
else:
|
||||
change_master_cmd = (
|
||||
"CHANGE MASTER TO "
|
||||
@@ -88,6 +93,11 @@ class MysqlGTIDReplication(mysql_base.MysqlReplicationBase):
|
||||
'user': replica_conf['replication_user']['name'],
|
||||
'password': replica_conf['replication_user']['password']
|
||||
})
|
||||
if 'cert_container' in master_info and \
|
||||
'ssl_mode' in master_info:
|
||||
change_master_cmd = change_master_cmd + (
|
||||
",MASTER_SSL=1"
|
||||
)
|
||||
|
||||
service.execute_sql(change_master_cmd)
|
||||
service.start_slave()
|
||||
@@ -21,6 +21,7 @@ from trove.common import cfg
|
||||
from trove.common import constants
|
||||
from trove.common.db.postgresql import models
|
||||
from trove.common import exception
|
||||
from trove.common import ssl
|
||||
from trove.common import utils
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.common.operating_system import FileMode
|
||||
@@ -29,7 +30,7 @@ from trove.guestagent.strategies.replication import base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
REPL_USER = 'replicator'
|
||||
REPL_USER = pg_service.REPLICATION_USER
|
||||
|
||||
|
||||
class PostgresqlReplicationStreaming(base.Replication):
|
||||
@@ -90,32 +91,17 @@ class PostgresqlReplicationStreaming(base.Replication):
|
||||
return repl_user_info
|
||||
|
||||
def enable_as_master(self, service, master_config):
|
||||
"""Primary postgredql settings.
|
||||
"""Primary postgresql settings.
|
||||
|
||||
For a server to be a master in postgres, we need to enable
|
||||
the replication user in pg_hba.conf
|
||||
"""
|
||||
self._get_or_create_replication_user(service)
|
||||
|
||||
hba_entry = f"host replication {REPL_USER} 0.0.0.0/0 md5\n"
|
||||
tmp_hba = '/tmp/pg_hba'
|
||||
operating_system.copy(pg_service.HBA_CONFIG_FILE, tmp_hba,
|
||||
force=True, as_root=True)
|
||||
operating_system.chmod(tmp_hba, FileMode.SET_ALL_RWX(),
|
||||
as_root=True)
|
||||
with open(tmp_hba, 'a+') as hba_file:
|
||||
hba_file.write(hba_entry)
|
||||
|
||||
operating_system.copy(tmp_hba, pg_service.HBA_CONFIG_FILE,
|
||||
force=True, as_root=True)
|
||||
operating_system.chown(pg_service.HBA_CONFIG_FILE,
|
||||
user=service.database_service_uid,
|
||||
group=service.database_service_gid,
|
||||
as_root=True)
|
||||
operating_system.chmod(pg_service.HBA_CONFIG_FILE,
|
||||
FileMode.SET_USR_RWX(),
|
||||
as_root=True)
|
||||
operating_system.remove(tmp_hba, as_root=True)
|
||||
service.apply_access_rules(
|
||||
ssl=CONF.ssl_mode in (ssl.MODE_ENFORCED, ssl.MODE_MTLS),
|
||||
mtls=CONF.ssl_mode == ssl.MODE_MTLS,
|
||||
replication_user=REPL_USER)
|
||||
LOG.debug(f"{pg_service.HBA_CONFIG_FILE} changed")
|
||||
|
||||
service.restart()
|
||||
|
||||
@@ -1067,9 +1067,10 @@ class BaseInstance(SimpleInstance):
|
||||
"tenant_id=%s\n"
|
||||
"datastore_registry_ext=%s\n"
|
||||
"replication_strategy=%s\n"
|
||||
"ssl_mode=%s\n"
|
||||
% (self.id, datastore_manager, datastore_version,
|
||||
self.tenant_id, self.datastore_registry_ext,
|
||||
self.datastore_repl_strategy)
|
||||
self.datastore_repl_strategy, self.db_info.ssl_mode)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1262,7 +1263,8 @@ class Instance(BuiltInstance):
|
||||
availability_zone=None, nics=None,
|
||||
configuration_id=None, slave_of_id=None, cluster_config=None,
|
||||
replica_count=None, volume_type=None, modules=None,
|
||||
locality=None, region_name=None, access=None):
|
||||
locality=None, region_name=None, access=None,
|
||||
ssl_mode=None, ssl_ref=None):
|
||||
nova_client = clients.create_nova_client(context)
|
||||
cinder_client = clients.create_cinder_client(context)
|
||||
datastore_cfg = CONF.get(datastore_version.manager)
|
||||
@@ -1396,6 +1398,8 @@ class Instance(BuiltInstance):
|
||||
context,
|
||||
instance_id=slave_of_id))
|
||||
replica_source_instance.validate_can_perform_action()
|
||||
ssl_mode = replica_source_instance.db_info.ssl_mode
|
||||
ssl_ref = replica_source_instance.db_info.ssl_ref
|
||||
|
||||
multi_replica = slave_of_id and replica_count and replica_count > 1
|
||||
instance_count = replica_count if multi_replica else 1
|
||||
@@ -1445,7 +1449,8 @@ class Instance(BuiltInstance):
|
||||
configuration_id=configuration_id,
|
||||
slave_of_id=slave_of_id, cluster_id=cluster_id,
|
||||
shard_id=shard_id, type=instance_type,
|
||||
region_id=region_name, access=access)
|
||||
region_id=region_name, access=access,
|
||||
ssl_mode=ssl_mode, ssl_ref=ssl_ref)
|
||||
instance_id = db_info.id
|
||||
instance_name = name
|
||||
LOG.debug(f"Creating new instance {instance_id}")
|
||||
@@ -2026,7 +2031,8 @@ class DBInstance(dbmodels.DatabaseModelBase):
|
||||
'volume_size', 'tenant_id', 'server_status',
|
||||
'deleted', 'deleted_at', 'datastore_version_id',
|
||||
'configuration_id', 'slave_of_id', 'cluster_id',
|
||||
'shard_id', 'type', 'region_id', 'encrypted_key', 'access']
|
||||
'shard_id', 'type', 'region_id', 'encrypted_key', 'access',
|
||||
'ssl_mode', 'ssl_ref']
|
||||
_table_name = 'instances'
|
||||
|
||||
def __init__(self, task_status, **kwargs):
|
||||
|
||||
@@ -31,6 +31,7 @@ from trove.common import notification
|
||||
from trove.common.notification import StartNotification
|
||||
from trove.common import pagination
|
||||
from trove.common import policy
|
||||
from trove.common import ssl
|
||||
from trove.common import utils
|
||||
from trove.common import wsgi
|
||||
from trove.datastore import models as ds_models
|
||||
@@ -280,7 +281,24 @@ class InstanceController(wsgi.Controller):
|
||||
instance_module['module_id'])
|
||||
module_models.InstanceModule.delete(
|
||||
context, instance_module)
|
||||
try:
|
||||
if instance.db_info['ssl_mode'] and \
|
||||
instance.db_info['ssl_mode'] != 'None':
|
||||
# (mangust404) The problem here that to correctly remove
|
||||
# consumer, the context should be a project's owner.
|
||||
# Barbican doesn't allow to manage secrets/consumers
|
||||
# even for admins.
|
||||
# If you run instance delettion as admin, secret consumers
|
||||
# cannot be updated.
|
||||
ssl_client = ssl.TroveSSL(context)
|
||||
ssl_client.disable_instance_ssl(instance)
|
||||
except Exception as e:
|
||||
LOG.warning(
|
||||
"Unable to remove consumer for key %s on %s deletion: %s",
|
||||
instance.db_info['ssl_ref'], instance.id, e,
|
||||
exc_info=True)
|
||||
instance.delete()
|
||||
|
||||
return wsgi.Result(None, 202)
|
||||
|
||||
def _check_nic(self, context, nic):
|
||||
@@ -496,6 +514,12 @@ class InstanceController(wsgi.Controller):
|
||||
region_name=region_name,
|
||||
access=access)
|
||||
|
||||
if slave_of_id and replica_source.ssl_mode and \
|
||||
replica_source.ssl_mode != 'None':
|
||||
ssl_client = ssl.TroveSSL(context)
|
||||
ssl_client.enable_instance_ssl(
|
||||
instance, replica_source.ssl_mode, replica_source.ssl_ref)
|
||||
|
||||
view = views.InstanceDetailView(instance, req=req)
|
||||
return wsgi.Result(view.data(), 200)
|
||||
|
||||
@@ -662,6 +686,135 @@ class InstanceController(wsgi.Controller):
|
||||
publish, discard)
|
||||
return wsgi.Result({'log': guest_log}, 200)
|
||||
|
||||
def ssl_show(self, req, tenant_id, id):
|
||||
"""Show information about SSL for an instance."""
|
||||
LOG.debug("Showing SSL status for tenant %s", tenant_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
self.authorize_instance_action(context, 'show', instance)
|
||||
client = clients.create_guest_client(context, id)
|
||||
ssl_info = client.ssl_show()
|
||||
if instance.db_info.ssl_mode and \
|
||||
instance.db_info.ssl_mode != 'None':
|
||||
ssl_info['mode'] = instance.db_info.ssl_mode
|
||||
else:
|
||||
ssl_info['mode'] = ssl.MODE_BUILTIN
|
||||
include_cert = strutils.bool_from_string(
|
||||
req.GET.get('include_certificate'))
|
||||
if not include_cert and 'certificate' in ssl_info and \
|
||||
'payload' in ssl_info['certificate']:
|
||||
del ssl_info['certificate']['payload']
|
||||
return wsgi.Result({'ssl': ssl_info}, 200)
|
||||
|
||||
def ssl_action(self, req, body, tenant_id, id):
|
||||
LOG.info("SSL action for tenant %s", tenant_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
|
||||
instance.validate_can_perform_action()
|
||||
self.authorize_instance_action(context, 'update', instance)
|
||||
|
||||
client = clients.create_guest_client(context, id)
|
||||
|
||||
container_ref = body['ssl'].get('container_ref', None)
|
||||
password_ref = body['ssl'].get('password_ref', None)
|
||||
enable = body['ssl'].get('enable', None)
|
||||
if enable and not container_ref:
|
||||
raise exception.BadRequest(
|
||||
"You should provide container_ref for enabling ssl.")
|
||||
disable = body['ssl'].get('disable', None)
|
||||
rollback = body['ssl'].get('rollback', None)
|
||||
|
||||
actions = [enable, disable, rollback]
|
||||
if sum(1 for op in actions if op) > 1:
|
||||
raise exception.BadRequest(
|
||||
"Cannot do several operations at once.")
|
||||
|
||||
# Rolling back for replica is allowed, to cover edge cases
|
||||
if rollback:
|
||||
LOG.debug(
|
||||
"Rolling back SSL configuration for tenant %s", tenant_id)
|
||||
result = client.ssl_rollback()
|
||||
# Write real guest mode into database
|
||||
if 'mode' in result:
|
||||
instance.db_info.ssl_mode = result['mode']
|
||||
instance.db_info.save()
|
||||
return wsgi.Result({'ssl': result}, 200)
|
||||
|
||||
if instance.slave_of_id:
|
||||
raise exception.BadRequest(
|
||||
"Unable to modify ssl on the replica. "
|
||||
"Please perform the action on the primary instead.")
|
||||
|
||||
mode = body['ssl'].get('mode', ssl.MODE_BASIC)
|
||||
if mode not in (ssl.MODE_BASIC,
|
||||
ssl.MODE_ENFORCED, ssl.MODE_MTLS):
|
||||
raise exception.BadRequest(
|
||||
"Unknown SSL mode. Available options are: "
|
||||
"%s, %s, %s" % (
|
||||
ssl.MODE_BASIC, ssl.MODE_ENFORCED, ssl.MODE_MTLS))
|
||||
|
||||
container = None
|
||||
ssl_client = ssl.TroveSSL(context)
|
||||
|
||||
if container_ref:
|
||||
# Get certificate container from Barbican.
|
||||
container = ssl_client.get_p12_bundle(container_ref, password_ref)
|
||||
|
||||
replicas = []
|
||||
replica_clients = {}
|
||||
|
||||
for dbinfo in instance.slaves:
|
||||
replica = models.Instance.load(
|
||||
context, dbinfo.id, needs_server=True)
|
||||
|
||||
replica.validate_can_perform_action()
|
||||
self.authorize_instance_action(
|
||||
context, 'update', replica)
|
||||
|
||||
replicas.append(replica)
|
||||
replica_clients[replica.id] = (
|
||||
clients.create_guest_client(context, replica.id))
|
||||
|
||||
instances = [instance] + replicas
|
||||
completed = []
|
||||
|
||||
# Define the actual task to be passed into transaction wrapper
|
||||
def _execute_guest_ssl_actions():
|
||||
ssl_info = client.ssl_action(mode, container, enable, disable)
|
||||
completed.append(client)
|
||||
for replica in replicas:
|
||||
replica_clients[replica.id].ssl_action(
|
||||
mode, container, enable, disable)
|
||||
completed.append(replica_clients[replica.id])
|
||||
return ssl_info
|
||||
|
||||
# Define the rollback task to be passed into transaction wrapper
|
||||
def _rollback_guest_ssl_actions():
|
||||
error = None
|
||||
for guest in reversed(completed):
|
||||
try:
|
||||
guest.ssl_rollback()
|
||||
except Exception as exc:
|
||||
LOG.exception(
|
||||
"Failed to rollback guest SSL configuration: %s",
|
||||
exc)
|
||||
if error is None:
|
||||
error = exc
|
||||
if error:
|
||||
raise error
|
||||
|
||||
ssl_info = ssl.run_ssl_state_transaction(
|
||||
ssl_client, instances, mode, container_ref, enable, disable,
|
||||
_execute_guest_ssl_actions, _rollback_guest_ssl_actions)
|
||||
|
||||
return wsgi.Result({'ssl': ssl_info}, 200)
|
||||
|
||||
def module_list(self, req, tenant_id, id):
|
||||
"""Return information about modules on an instance."""
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright 2026 PS Cloud
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from trove.common import ssl
|
||||
from trove.guestagent.datastore import manager as base_manager
|
||||
from trove.tests.unittests import trove_testtools
|
||||
|
||||
|
||||
class TestManager(trove_testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestManager, self).setUp()
|
||||
self.manager = base_manager.Manager('test')
|
||||
|
||||
def test_ssl_mode_at_least(self):
|
||||
self.assertTrue(self.manager.ssl_mode_at_least(
|
||||
ssl.MODE_BASIC, ssl.MODE_BASIC))
|
||||
self.assertTrue(self.manager.ssl_mode_at_least(
|
||||
ssl.MODE_ENFORCED, ssl.MODE_BASIC))
|
||||
self.assertTrue(self.manager.ssl_mode_at_least(
|
||||
ssl.MODE_ENFORCED, ssl.MODE_ENFORCED))
|
||||
self.assertTrue(self.manager.ssl_mode_at_least(
|
||||
ssl.MODE_MTLS, ssl.MODE_ENFORCED))
|
||||
self.assertFalse(self.manager.ssl_mode_at_least(
|
||||
ssl.MODE_BASIC, ssl.MODE_MTLS))
|
||||
self.assertFalse(self.manager.ssl_mode_at_least(
|
||||
'invalid', ssl.MODE_BASIC))
|
||||
self.assertFalse(self.manager.ssl_mode_at_least(
|
||||
ssl.MODE_BASIC, 'invalid'))
|
||||
self.assertFalse(self.manager.ssl_mode_at_least(
|
||||
None, ssl.MODE_BASIC))
|
||||
@@ -502,3 +502,411 @@ class TestInstanceController(trove_testtools.TestCase):
|
||||
|
||||
ins.attach_configuration.assert_called_once_with('fake_config_id')
|
||||
ins.detach_configuration.assert_not_called()
|
||||
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_show_calls_guest_agent(self, load_mock, guest_client_mock):
|
||||
ins_mock = MagicMock()
|
||||
ins_mock.tenant_id = 'tenant123'
|
||||
ins_mock.db_info.ssl_mode = 'basic'
|
||||
load_mock.return_value = ins_mock
|
||||
|
||||
guest_mock = MagicMock()
|
||||
guest_client_mock.return_value = guest_mock
|
||||
guest_mock.ssl_show.return_value = {
|
||||
'status': 'on'}
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
result = self.controller.ssl_show(req, 'fake_tenant', 'fake_id')
|
||||
|
||||
load_mock.assert_called_once_with(self.context, 'fake_id')
|
||||
guest_client_mock.assert_called_once_with(self.context, 'fake_id')
|
||||
guest_mock.ssl_show.assert_called_once_with()
|
||||
self.assertEqual({'ssl': {'status': 'on', 'mode': 'basic'}},
|
||||
result.data('application/json'))
|
||||
|
||||
@mock.patch('trove.common.ssl.TroveSSL.register_consumer')
|
||||
@mock.patch('trove.common.ssl.TroveSSL.get_p12_bundle')
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_action_calls_guest_agent_enable(
|
||||
self, load_mock, guest_client_mock, get_bundle_mock,
|
||||
register_consumer_mock):
|
||||
ins_mock = MagicMock()
|
||||
ins_mock.tenant_id = 'tenant123'
|
||||
ins_mock.db_info = MagicMock()
|
||||
ins_mock.db_info.ssl_ref = None
|
||||
ins_mock.db_info.ssl_mode = None
|
||||
ins_mock.slave_of_id = None
|
||||
ins_mock.slaves = []
|
||||
load_mock.return_value = ins_mock
|
||||
|
||||
guest_mock = MagicMock()
|
||||
guest_client_mock.return_value = guest_mock
|
||||
guest_mock.ssl_action.return_value = {'status': 'disabled'}
|
||||
|
||||
get_bundle_mock.return_value = {
|
||||
'private_key': 'key',
|
||||
'certificate': 'cert',
|
||||
'ca': 'ca'
|
||||
}
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
body = {'ssl': {
|
||||
'enable': True,
|
||||
'container_ref': 'container_ref'}}
|
||||
result = self.controller.ssl_action(req, body,
|
||||
'fake_tenant', 'fake_id')
|
||||
|
||||
load_mock.assert_called_once_with(self.context, 'fake_id')
|
||||
guest_client_mock.assert_called_once_with(self.context, 'fake_id')
|
||||
get_bundle_mock.assert_called_once_with('container_ref', None)
|
||||
|
||||
guest_mock.ssl_action.assert_called_once_with(
|
||||
'basic',
|
||||
get_bundle_mock.return_value,
|
||||
True,
|
||||
None
|
||||
)
|
||||
register_consumer_mock.assert_called_once()
|
||||
self.assertEqual({'ssl': {'status': 'disabled'}},
|
||||
result.data('application/json'))
|
||||
|
||||
@mock.patch.object(InstanceController, 'authorize_instance_action')
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_action_enable_and_disable_raises(
|
||||
self, load_mock, guest_client_mock, authorize_mock):
|
||||
instance = MagicMock()
|
||||
instance.tenant_id = 'tenant123'
|
||||
load_mock.return_value = instance
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
body = {'ssl': {'enable': True, 'disable': True}}
|
||||
|
||||
self.assertRaises(
|
||||
exception.BadRequest,
|
||||
self.controller.ssl_action,
|
||||
req, body, 'fake_tenant', 'fake_id'
|
||||
)
|
||||
|
||||
load_mock.assert_called_once_with(self.context, 'fake_id')
|
||||
guest_client_mock.assert_called_once_with(self.context, 'fake_id')
|
||||
authorize_mock.assert_called_once_with(
|
||||
self.context, 'update', instance)
|
||||
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_action_against_replica_raises(
|
||||
self, load_mock, guest_client_mock):
|
||||
ins_mock = MagicMock()
|
||||
ins_mock.tenant_id = 'tenant123'
|
||||
ins_mock.db_info = MagicMock()
|
||||
ins_mock.db_info.ssl_ref = None
|
||||
ins_mock.db_info.ssl_mode = None
|
||||
ins_mock.slave_of_id = 'tenant234'
|
||||
load_mock.return_value = ins_mock
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
body = {'ssl': {
|
||||
'enable': True,
|
||||
'container_ref': 'container_ref'}}
|
||||
|
||||
self.assertRaises(
|
||||
exception.BadRequest,
|
||||
self.controller.ssl_action,
|
||||
req, body, 'fake_tenant', 'fake_id'
|
||||
)
|
||||
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_show_include_certificate_flag(
|
||||
self, load_mock, guest_client_mock):
|
||||
ins_mock = MagicMock()
|
||||
ins_mock.tenant_id = 'tenant123'
|
||||
ins_mock.db_info.ssl_mode = 'basic'
|
||||
load_mock.return_value = ins_mock
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
'name': 'parameter is not set',
|
||||
'query': {},
|
||||
'expect_payload': False,
|
||||
},
|
||||
{
|
||||
'name': 'include_certificate=1',
|
||||
'query': {'include_certificate': '1'},
|
||||
'expect_payload': True,
|
||||
},
|
||||
{
|
||||
'name': 'include_certificate=false',
|
||||
'query': {'include_certificate': 'false'},
|
||||
'expect_payload': False,
|
||||
},
|
||||
{
|
||||
'name': 'include_certificate=true',
|
||||
'query': {'include_certificate': 'true'},
|
||||
'expect_payload': True,
|
||||
},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
guest_mock = MagicMock()
|
||||
guest_client_mock.return_value = guest_mock
|
||||
|
||||
guest_mock.ssl_show.return_value = {
|
||||
'status': 'on',
|
||||
'certificate': {
|
||||
'name': 'server-cert',
|
||||
'payload': 'secret-cert-data'
|
||||
}
|
||||
}
|
||||
|
||||
req.GET = case['query']
|
||||
|
||||
result = self.controller.ssl_show(
|
||||
req, 'fake_tenant', 'fake_id')
|
||||
|
||||
expected_certificate = {
|
||||
'name': 'server-cert'
|
||||
}
|
||||
|
||||
if case['expect_payload']:
|
||||
expected_certificate['payload'] = 'secret-cert-data'
|
||||
|
||||
self.assertEqual(
|
||||
{'ssl': {
|
||||
'status': 'on',
|
||||
'mode': 'basic',
|
||||
'certificate': expected_certificate
|
||||
}},
|
||||
result.data('application/json'),
|
||||
case['name']
|
||||
)
|
||||
|
||||
@mock.patch('trove.common.ssl.TroveSSL.remove_consumer')
|
||||
@mock.patch('trove.common.ssl.TroveSSL.register_consumer')
|
||||
@mock.patch('trove.common.ssl.TroveSSL.get_p12_bundle')
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_action_rolls_back_metadata_on_guest_failure(
|
||||
self,
|
||||
load_mock,
|
||||
guest_client_mock,
|
||||
get_bundle_mock,
|
||||
register_consumer_mock,
|
||||
remove_consumer_mock):
|
||||
|
||||
instance = MagicMock()
|
||||
instance.id = 'instance-id'
|
||||
instance.slave_of_id = None
|
||||
instance.slaves = []
|
||||
instance.db_info.ssl_mode = None
|
||||
instance.db_info.ssl_ref = None
|
||||
load_mock.return_value = instance
|
||||
|
||||
guest = MagicMock()
|
||||
guest.ssl_action.side_effect = RuntimeError('guest failure')
|
||||
guest_client_mock.return_value = guest
|
||||
|
||||
get_bundle_mock.return_value = 'fake-container'
|
||||
register_consumer_mock.return_value = None
|
||||
remove_consumer_mock.return_value = None
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
body = {
|
||||
'ssl': {
|
||||
'enable': True,
|
||||
'mode': 'basic',
|
||||
'container_ref': 'container-ref'
|
||||
}
|
||||
}
|
||||
|
||||
# The controller should re-raise the RuntimeError after rollback
|
||||
self.assertRaises(
|
||||
RuntimeError,
|
||||
self.controller.ssl_action,
|
||||
req,
|
||||
body,
|
||||
'tenant-id',
|
||||
'instance-id'
|
||||
)
|
||||
|
||||
# Verify register_consumer was called before the failure
|
||||
register_consumer_mock.assert_called_once_with(
|
||||
'container-ref',
|
||||
'instance',
|
||||
'instance-id'
|
||||
)
|
||||
|
||||
# Verify rollback logic: remove_consumer should be called on failure
|
||||
remove_consumer_mock.assert_called_once_with(
|
||||
'container-ref',
|
||||
'instance',
|
||||
'instance-id'
|
||||
)
|
||||
|
||||
# Verify metadata rollback:
|
||||
# The wrapper should have reset these to None after the guest failure
|
||||
self.assertIsNone(instance.db_info.ssl_mode)
|
||||
self.assertIsNone(instance.db_info.ssl_ref)
|
||||
|
||||
# Verify saves were executed (one for the initial update, one for
|
||||
# rollback)
|
||||
self.assertGreaterEqual(instance.db_info.save.call_count, 2)
|
||||
|
||||
@mock.patch('trove.common.ssl.TroveSSL.register_consumer')
|
||||
@mock.patch('trove.common.ssl.TroveSSL.get_p12_bundle')
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_action_success_applies_metadata_and_calls_guest(
|
||||
self,
|
||||
load_mock,
|
||||
guest_client_mock,
|
||||
get_bundle_mock,
|
||||
register_consumer_mock):
|
||||
|
||||
instance = MagicMock()
|
||||
instance.id = 'instance-id'
|
||||
instance.slave_of_id = None
|
||||
instance.slaves = []
|
||||
|
||||
# Initial DB state (No SSL yet)
|
||||
instance.db_info.ssl_mode = None
|
||||
instance.db_info.ssl_ref = None
|
||||
|
||||
load_mock.return_value = instance
|
||||
|
||||
guest = MagicMock()
|
||||
guest.ssl_action.return_value = {'status': 'enabled'}
|
||||
guest_client_mock.return_value = guest
|
||||
|
||||
get_bundle_mock.return_value = 'fake-container'
|
||||
# register_consumer returns None by default, simulating success
|
||||
|
||||
# --- 4. Execute Test ---
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
|
||||
body = {
|
||||
'ssl': {
|
||||
'enable': True,
|
||||
'mode': 'basic',
|
||||
'container_ref': 'container-ref'
|
||||
}
|
||||
}
|
||||
|
||||
result = self.controller.ssl_action(
|
||||
req, body, 'tenant-id', 'instance-id'
|
||||
)
|
||||
|
||||
# Verify the Guest Agent was called with data from our get_bundle mock
|
||||
guest.ssl_action.assert_called_once_with(
|
||||
'basic',
|
||||
'fake-container',
|
||||
True,
|
||||
None
|
||||
)
|
||||
|
||||
# Verify the wrapper updated the Database state correctly
|
||||
self.assertEqual('basic', instance.db_info.ssl_mode)
|
||||
self.assertEqual('container-ref', instance.db_info.ssl_ref)
|
||||
|
||||
# Verify Barbican interaction was triggered by the real wrapper
|
||||
register_consumer_mock.assert_called_once_with(
|
||||
'container-ref',
|
||||
'instance',
|
||||
'instance-id'
|
||||
)
|
||||
|
||||
# Ensure the changes were actually persisted to the DB
|
||||
instance.db_info.save.assert_called()
|
||||
|
||||
# Check response structure
|
||||
self.assertEqual(
|
||||
{'ssl': {'status': 'enabled'}},
|
||||
result.data('application/json')
|
||||
)
|
||||
|
||||
@mock.patch('trove.instance.service.ssl.run_ssl_state_transaction')
|
||||
@mock.patch('trove.common.ssl.TroveSSL.get_p12_bundle')
|
||||
@mock.patch('trove.instance.service.clients.create_guest_client')
|
||||
@mock.patch('trove.instance.models.Instance.load')
|
||||
def test_ssl_action_passes_guest_actions_to_transaction(
|
||||
self, load_mock, guest_client_mock, get_bundle_mock,
|
||||
transaction_mock):
|
||||
instance = MagicMock()
|
||||
instance.id = 'instance-id'
|
||||
instance.slave_of_id = None
|
||||
instance.slaves = [MagicMock(id='replica-id')]
|
||||
|
||||
replica = MagicMock()
|
||||
replica.id = 'replica-id'
|
||||
load_mock.side_effect = [instance, replica]
|
||||
|
||||
primary_guest = MagicMock()
|
||||
replica_guest = MagicMock()
|
||||
guest_client_mock.side_effect = [primary_guest, replica_guest]
|
||||
primary_guest.ssl_action.return_value = {'status': 'enabled'}
|
||||
get_bundle_mock.return_value = 'certificate-bundle'
|
||||
|
||||
guest_calls = MagicMock()
|
||||
guest_calls.attach_mock(primary_guest, 'primary')
|
||||
guest_calls.attach_mock(replica_guest, 'replica')
|
||||
|
||||
def run_transaction(ssl_client, instances, mode, container_ref,
|
||||
enable, disable, execute, rollback):
|
||||
self.assertEqual([instance, replica], instances)
|
||||
self.assertEqual('enforced', mode)
|
||||
self.assertEqual('container-ref', container_ref)
|
||||
self.assertTrue(enable)
|
||||
self.assertIsNone(disable)
|
||||
|
||||
result = execute()
|
||||
rollback()
|
||||
return result
|
||||
|
||||
transaction_mock.side_effect = run_transaction
|
||||
|
||||
req = MagicMock()
|
||||
req.environ = {'trove.context': self.context}
|
||||
body = {
|
||||
'ssl': {
|
||||
'enable': True,
|
||||
'mode': 'enforced',
|
||||
'container_ref': 'container-ref'
|
||||
}
|
||||
}
|
||||
|
||||
result = self.controller.ssl_action(
|
||||
req, body, 'tenant-id', 'instance-id')
|
||||
|
||||
transaction_mock.assert_called_once()
|
||||
self.assertEqual(
|
||||
[
|
||||
mock.call.primary.ssl_action(
|
||||
'enforced', 'certificate-bundle', True, None),
|
||||
mock.call.replica.ssl_action(
|
||||
'enforced', 'certificate-bundle', True, None),
|
||||
mock.call.replica.ssl_rollback(),
|
||||
mock.call.primary.ssl_rollback(),
|
||||
],
|
||||
guest_calls.mock_calls
|
||||
)
|
||||
self.assertEqual(
|
||||
{'ssl': {'status': 'enabled'}},
|
||||
result.data('application/json')
|
||||
)
|
||||
@@ -227,6 +227,93 @@
|
||||
TROVE_BUILD_BACKUP_IMAGES: True
|
||||
tempest_test_regex: ^trove_tempest_plugin\.tests\.scenario\.test_replication
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-base
|
||||
parent: trove-tempest
|
||||
files:
|
||||
- ^trove/.*ssl.*\.py$
|
||||
- ^trove/instance/.*\.py$
|
||||
- ^trove/guestagent/datastore/manager\.py$
|
||||
- ^trove/guestagent/datastore/service\.py$
|
||||
required-projects:
|
||||
- openstack/trove
|
||||
- openstack/trove-tempest-plugin
|
||||
- openstack/barbican
|
||||
- openstack/barbican-tempest-plugin
|
||||
- openstack/tempest
|
||||
vars:
|
||||
devstack_plugins:
|
||||
trove: https://opendev.org/openstack/trove.git
|
||||
barbican: https://opendev.org/openstack/barbican.git
|
||||
devstack_services:
|
||||
etcd3: false
|
||||
cinder: true
|
||||
c-sch: true
|
||||
c-api: true
|
||||
c-vol: true
|
||||
c-bak: false
|
||||
swift: true
|
||||
s-account: true
|
||||
s-container: true
|
||||
s-object: true
|
||||
s-proxy: true
|
||||
tls-proxy: true
|
||||
tempest: true
|
||||
barbican-svc: true
|
||||
barbican-retry: true
|
||||
tempest_plugins:
|
||||
- trove-tempest-plugin
|
||||
- barbican-tempest-plugin
|
||||
devstack_localrc:
|
||||
# Prevent "Secret creation attempt not allowed - please review your user/project privileges"
|
||||
# In Zuul, barbican works correctly only with enforce_new_defaults = True
|
||||
ENFORCE_SCOPE: True
|
||||
tempest_test_regex: ^trove_tempest_plugin\.tests\.scenario\.test_instance_ssl\.(?!.*test_ssl_replication)
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-replication
|
||||
parent: trove-tempest
|
||||
files:
|
||||
- ^trove/.*ssl.*\.py$
|
||||
- ^trove/instance/.*\.py$
|
||||
- ^trove/guestagent/datastore/manager\.py$
|
||||
- ^trove/guestagent/datastore/service\.py$
|
||||
required-projects:
|
||||
- openstack/trove
|
||||
- openstack/trove-tempest-plugin
|
||||
- openstack/barbican
|
||||
- openstack/barbican-tempest-plugin
|
||||
- openstack/tempest
|
||||
vars:
|
||||
devstack_plugins:
|
||||
trove: https://opendev.org/openstack/trove.git
|
||||
barbican: https://opendev.org/openstack/barbican.git
|
||||
devstack_services:
|
||||
etcd3: false
|
||||
cinder: true
|
||||
c-sch: true
|
||||
c-api: true
|
||||
c-vol: true
|
||||
c-bak: false
|
||||
swift: true
|
||||
s-account: true
|
||||
s-container: true
|
||||
s-object: true
|
||||
s-proxy: true
|
||||
tls-proxy: true
|
||||
tempest: true
|
||||
barbican-svc: true
|
||||
barbican-retry: true
|
||||
tempest_plugins:
|
||||
- trove-tempest-plugin
|
||||
- barbican-tempest-plugin
|
||||
devstack_localrc:
|
||||
TROVE_BUILD_BACKUP_IMAGES: True
|
||||
# Prevent "Secret creation attempt not allowed - please review your user/project privileges"
|
||||
# In Zuul, barbican works correctly only with enforce_new_defaults = True
|
||||
ENFORCE_SCOPE: True
|
||||
tempest_test_regex: ^trove_tempest_plugin\.tests\.scenario\.test_instance_ssl\..*test_ssl_replication
|
||||
|
||||
- job:
|
||||
name: publish-trove-guest-image
|
||||
parent: publish-openstack-artifacts
|
||||
|
||||
@@ -114,3 +114,37 @@
|
||||
backup_wait_timeout: 1200
|
||||
enabled_datastores: "mariadb"
|
||||
default_datastore_versions: mariadb:11.4
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-mariadb
|
||||
parent: trove-tempest-ubuntu-ssl-base
|
||||
files: !inherit
|
||||
- ^trove/guestagent/.*mariadb.*\.py$
|
||||
vars:
|
||||
devstack_localrc:
|
||||
TROVE_DATASTORE_TYPE: mariadb
|
||||
TROVE_DATASTORE_VERSION: 11.4
|
||||
devstack_local_conf:
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
database:
|
||||
backup_wait_timeout: 1200
|
||||
enabled_datastores: "mariadb"
|
||||
default_datastore_versions: mariadb:11.4
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-replication-mariadb
|
||||
parent: trove-tempest-ubuntu-ssl-replication
|
||||
files: !inherit
|
||||
- ^trove/guestagent/.*mariadb.*\.py$
|
||||
vars:
|
||||
devstack_localrc:
|
||||
TROVE_DATASTORE_TYPE: mariadb
|
||||
TROVE_DATASTORE_VERSION: 11.4
|
||||
devstack_local_conf:
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
database:
|
||||
backup_wait_timeout: 1200
|
||||
enabled_datastores: "mariadb"
|
||||
default_datastore_versions: mariadb:11.4
|
||||
@@ -115,3 +115,37 @@
|
||||
database:
|
||||
backup_wait_timeout: 1200
|
||||
default_datastore_versions: mysql:8.4
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-mysql
|
||||
parent: trove-tempest-ubuntu-ssl-base
|
||||
files: !inherit
|
||||
- ^trove/guestagent/.*mysql.*\.py$
|
||||
vars:
|
||||
devstack_localrc:
|
||||
TROVE_DATASTORE_TYPE: mysql
|
||||
TROVE_DATASTORE_VERSION: 8.4
|
||||
TROVE_STATE_CHANGE_WAIT_TIME: 900
|
||||
devstack_local_conf:
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
database:
|
||||
backup_wait_timeout: 1200
|
||||
default_datastore_versions: mysql:8.4
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-replication-mysql
|
||||
parent: trove-tempest-ubuntu-ssl-replication
|
||||
files: !inherit
|
||||
- ^trove/guestagent/.*mysql.*\.py$
|
||||
vars:
|
||||
devstack_localrc:
|
||||
TROVE_DATASTORE_TYPE: mysql
|
||||
TROVE_DATASTORE_VERSION: 8.4
|
||||
TROVE_STATE_CHANGE_WAIT_TIME: 900
|
||||
devstack_local_conf:
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
database:
|
||||
backup_wait_timeout: 1200
|
||||
default_datastore_versions: mysql:8.4
|
||||
@@ -115,3 +115,37 @@
|
||||
backup_wait_timeout: 1200
|
||||
enabled_datastores: "postgresql"
|
||||
default_datastore_versions: postgresql:18
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-postgresql
|
||||
parent: trove-tempest-ubuntu-ssl-base
|
||||
files: !inherit
|
||||
- ^trove/guestagent/.*postgresql.*\.py$
|
||||
vars:
|
||||
devstack_localrc:
|
||||
TROVE_DATASTORE_TYPE: postgresql
|
||||
TROVE_DATASTORE_VERSION: 18
|
||||
devstack_local_conf:
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
database:
|
||||
backup_wait_timeout: 1200
|
||||
enabled_datastores: "postgresql"
|
||||
default_datastore_versions: postgresql:18
|
||||
|
||||
- job:
|
||||
name: trove-tempest-ubuntu-ssl-replication-postgresql
|
||||
parent: trove-tempest-ubuntu-ssl-replication
|
||||
files: !inherit
|
||||
- ^trove/guestagent/.*postgresql.*\.py$
|
||||
vars:
|
||||
devstack_localrc:
|
||||
TROVE_DATASTORE_TYPE: postgresql
|
||||
TROVE_DATASTORE_VERSION: 18
|
||||
devstack_local_conf:
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
database:
|
||||
backup_wait_timeout: 1200
|
||||
enabled_datastores: "postgresql"
|
||||
default_datastore_versions: postgresql:18
|
||||
+10
-1
@@ -20,18 +20,27 @@
|
||||
voting: false
|
||||
- trove-tempest-ubuntu-replication-mysql:
|
||||
voting: false
|
||||
- trove-tempest-cinder-storage-driver-mysql:
|
||||
- trove-tempest-ubuntu-ssl-mysql
|
||||
- trove-tempest-ubuntu-ssl-replication-mysql:
|
||||
voting: false
|
||||
- trove-tempest-cinder-storage-driver-mysql:
|
||||
voting: false
|
||||
- trove-tempest-ubuntu-base-mariadb
|
||||
- trove-tempest-ubuntu-backup-mariadb:
|
||||
voting: false
|
||||
- trove-tempest-ubuntu-replication-mariadb:
|
||||
voting: false
|
||||
- trove-tempest-ubuntu-ssl-mariadb
|
||||
- trove-tempest-ubuntu-ssl-replication-mariadb:
|
||||
voting: false
|
||||
- trove-tempest-cinder-storage-driver-mariadb:
|
||||
voting: false
|
||||
- trove-tempest-ubuntu-base-postgresql
|
||||
- trove-tempest-ubuntu-backup-postgresql:
|
||||
voting: false
|
||||
- trove-tempest-ubuntu-ssl-postgresql
|
||||
- trove-tempest-ubuntu-ssl-replication-postgresql:
|
||||
voting: false
|
||||
- trove-tempest-ubuntu-replication-postgresql:
|
||||
voting: false
|
||||
- trove-tempest-cinder-storage-driver-postgresql:
|
||||
|
||||
Reference in new issue
Block a user