summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/radosgw/bucket_logging.rst6
-rw-r--r--doc/radosgw/s3/bucketops.rst40
-rw-r--r--doc/radosgw/uadk-accel.rst50
-rw-r--r--examples/rgw/boto3/bucket_logging.py11
-rw-r--r--examples/rgw/boto3/post_bucket_logging.py23
-rw-r--r--examples/rgw/boto3/service-2.sdk-extras.json39
-rwxr-xr-xqa/standalone/scrub/osd-scrub-repair.sh2
-rw-r--r--qa/suites/nvmeof/basic/workloads/nvmeof_initiator.yaml3
-rw-r--r--qa/suites/nvmeof/basic/workloads/nvmeof_namespaces.yaml2
-rw-r--r--qa/suites/nvmeof/basic/workloads/nvmeof_scalability.yaml5
-rw-r--r--qa/suites/nvmeof/thrash/gateway-initiator-setup/10-subsys-90-namespace-no_huge_pages.yaml37
-rw-r--r--qa/suites/nvmeof/thrash/gateway-initiator-setup/120-subsys-8-namespace.yaml (renamed from qa/suites/nvmeof/thrash/gateway-initiator-setup/3-subsys-60-namespace.yaml)4
-rw-r--r--qa/suites/nvmeof/thrash/thrashers/nvmeof_mon_thrash.yaml1
-rw-r--r--qa/suites/nvmeof/thrash/thrashers/nvmeof_thrash.yaml2
-rw-r--r--qa/suites/nvmeof/thrash/workloads/fio.yaml6
-rw-r--r--qa/tasks/nvmeof.py36
-rwxr-xr-xqa/workunits/nvmeof/basic_tests.sh10
-rwxr-xr-xqa/workunits/nvmeof/fio_test.sh7
-rwxr-xr-xqa/workunits/nvmeof/scalability_test.sh51
-rwxr-xr-xqa/workunits/nvmeof/setup_subsystem.sh17
-rw-r--r--src/mon/NVMeofGwMon.cc18
-rw-r--r--src/mon/NVMeofGwMon.h3
-rw-r--r--src/msg/async/Event.cc2
-rw-r--r--src/nvmeof/NVMeofGwMonitorClient.cc6
-rw-r--r--src/nvmeof/NVMeofGwMonitorClient.h2
-rw-r--r--src/osd/PG.cc1
-rw-r--r--src/osd/scrubber/pg_scrubber.cc20
-rw-r--r--src/osd/scrubber_common.h6
-rw-r--r--src/osdc/CMakeLists.txt5
-rw-r--r--src/pybind/mgr/dashboard/controllers/rgw_iam.py52
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html1
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts47
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts9
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html432
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts20
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts8
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html43
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts16
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html3
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts4
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/styles/themes/_content.scss1
-rw-r--r--src/pybind/mgr/dashboard/openapi.yaml270
-rw-r--r--src/pybind/mgr/dashboard/services/rgw_iam.py81
-rw-r--r--src/pybind/mgr/dashboard/tests/test_rgw_iam.py292
-rw-r--r--src/rgw/CMakeLists.txt1
-rw-r--r--src/rgw/driver/posix/notify.h2
-rw-r--r--src/rgw/driver/rados/rgw_sal_rados.cc2
-rw-r--r--src/rgw/rgw_admin.cc232
-rw-r--r--src/rgw/rgw_bucket_logging.cc17
-rw-r--r--src/rgw/rgw_bucket_logging.h16
-rw-r--r--src/rgw/rgw_iam_policy.cc4
-rw-r--r--src/rgw/rgw_iam_policy.h2
-rw-r--r--src/rgw/rgw_op.cc45
-rw-r--r--src/rgw/rgw_op_type.h1
-rw-r--r--src/rgw/rgw_pubsub.cc208
-rw-r--r--src/rgw/rgw_pubsub.h86
-rw-r--r--src/rgw/rgw_rest.cc6
-rw-r--r--src/rgw/rgw_rest_bucket_logging.cc78
-rw-r--r--src/rgw/rgw_rest_bucket_logging.h1
-rw-r--r--src/rgw/rgw_rest_s3.cc7
-rw-r--r--src/rgw/rgw_s3_filter.cc269
-rw-r--r--src/rgw/rgw_s3_filter.h102
-rw-r--r--src/rgw/rgw_sal_dbstore.cc2
-rw-r--r--src/test/rgw/bucket_notification/api.py4
-rw-r--r--src/test/rgw/bucket_notification/test_bn.py236
66 files changed, 2244 insertions, 774 deletions
diff --git a/doc/radosgw/bucket_logging.rst b/doc/radosgw/bucket_logging.rst
index d96ffbe2758..cb9f8465d20 100644
--- a/doc/radosgw/bucket_logging.rst
+++ b/doc/radosgw/bucket_logging.rst
@@ -27,6 +27,8 @@ This time (in seconds) could be set per source bucket via a Ceph extension to th
or globally via the `rgw_bucket_logging_obj_roll_time` configuration option. If not set, the default time is 5 minutes.
Adding a log object to the log bucket is done "lazily", meaning, that if no more records are written to the object, it may
remain outside of the log bucket even after the configured time has passed.
+To counter that, you can flush all logging objects on a given source bucket to log them,
+regardless if enough time passed or if no more records are written to the object.
Standard
````````
@@ -38,8 +40,8 @@ Journal
If logging type is set to "Journal", the records are written to the log bucket before the bucket operation is completed.
This means that if the logging action fails, the operation will not be executed, and an error will be returned to the client.
An exception to the above are "multi/delete" log records: if writing these log records fail, the operation continues and may still be successful.
-Note that it may happen that the log records were successfully written, but the bucket operation failed, since the logs are written
-before such a failure, there will be no indication for that in the log records.
+Journal mode supports filtering out records based on matches of the prefixes and suffixes of the logged object keys. Regular-expression matching can also be used on these to create filters.
+Note that it may happen that the log records were successfully written, but the bucket operation failed, since the logs are written.
Bucket Logging REST API
diff --git a/doc/radosgw/s3/bucketops.rst b/doc/radosgw/s3/bucketops.rst
index 2e5cc48646d..c33a8c0f410 100644
--- a/doc/radosgw/s3/bucketops.rst
+++ b/doc/radosgw/s3/bucketops.rst
@@ -751,6 +751,14 @@ Parameters are XML encoded in the body of the request, in the following format:
<TargetPrefix>string</TargetPrefix>
<LoggingType>Standard|Journal</LoggingType>
<ObjectRollTime>integer</ObjectRollTime>
+ <Filter>
+ <S3Key>
+ <FilterRule>
+ <Name>suffix/prefix/regex</Name>
+ <Value></Value>
+ </FilterRule>
+ </S3Key>
+ </Filter>
</LoggingEnabled>
</BucketLoggingStatus>
@@ -881,6 +889,14 @@ Response is XML encoded in the body of the request, in the following format:
<TargetPrefix>string</TargetPrefix>
<LoggingType>Standard|Journal</LoggingType>
<ObjectRollTime>integer</ObjectRollTime>
+ <Filter>
+ <S3Key>
+ <FilterRule>
+ <Name>suffix/prefix/regex</Name>
+ <Value></Value>
+ </FilterRule>
+ </S3Key>
+ </Filter>
</LoggingEnabled>
</BucketLoggingStatus>
@@ -894,3 +910,27 @@ HTTP Response
| ``404`` | NoSuchBucket | The bucket does not exist |
+---------------+-----------------------+----------------------------------------------------------+
+Flush Bucket Logging
+--------------------
+
+Flushes all logging objects for a given source bucket (logging bucket are written lazily).
+
+Syntax
+~~~~~~
+
+::
+
+ POST /{bucket}?logging HTTP/1.1
+
+
+HTTP Response
+~~~~~~~~~~~~~
+
++---------------+-----------------------+----------------------------------------------------------+
+| HTTP Status | Status Code | Description |
++===============+=======================+==========================================================+
+| ``201`` | Created | Flushed all logging objects successfully |
++---------------+-----------------------+----------------------------------------------------------+
+| ``404`` | NoSuchBucket | The bucket does not exist |
++---------------+-----------------------+----------------------------------------------------------+
+
diff --git a/doc/radosgw/uadk-accel.rst b/doc/radosgw/uadk-accel.rst
index fdf99f891f0..0ed25148d73 100644
--- a/doc/radosgw/uadk-accel.rst
+++ b/doc/radosgw/uadk-accel.rst
@@ -12,9 +12,9 @@ See `Compressor UADK Support`_.
UADK in the Software Stack
==========================
-UADK is a general-purpose user space accelerator framework that uses shared
-virtual addressing (SVA) to provide a unified programming interface for hardware
-acceleration of cryptographic and compression algorithms.
+UADK is a general-purpose user space accelerator framework that uses Shared
+Virtual Addressing (SVA) to provide a unified programming interface for
+hardware acceleration of cryptographic and compression algorithms.
UADK includes Unified/User-space-access-intended Accelerator Framework (UACCE),
which enables hardware accelerators that support SVA to adapt to UADK.
@@ -33,10 +33,10 @@ See `OpenSSL UADK Engine`_.
UADK Environment Setup
======================
-UADK consists of UACCE, vendors’ drivers, and an algorithm layer. UADK requires the
-hardware accelerator to support SVA, and the operating system to support IOMMU and
-SVA. Hardware accelerators from different vendors are registered as different character
-devices with UACCE by using kernel-mode drivers of the vendors.
+UADK consists of UACCE, vendor drivers, and an algorithm layer. UADK requires
+the hardware accelerator to support SVA, and the operating system to support
+IOMMU and SVA. Hardware accelerators are registered as different character
+devices with UACCE by kernel-mode drivers.
::
@@ -77,11 +77,12 @@ Configuration
#. Kernel Requirement
-User needs to make sure that UACCE is already supported in Linux kernel. The kernel version
-should be at least v5.9 with SVA (Shared Virtual Addressing) enabled.
+Users must ensure that UACCE is supported by the Linux kernel release in use,
+which should be 5.9 or later with SVA (Shared Virtual Addressing) enabled.
-UACCE may be built as a module or built into the kernel. Here's an example to build UACCE
-with hardware accelerators for the HiSilicon Kunpeng platform.
+UACCE may be built as a loadable module or built into the kernel. Here's an
+example to build UACCE with hardware accelerators for the HiSilicon Kunpeng
+platform.
.. prompt:: bash $
@@ -97,13 +98,17 @@ with hardware accelerators for the HiSilicon Kunpeng platform.
Make sure all these above kernel configurations are selected.
#. UADK enablement
-If the architecture is aarch64, it will automatically download the UADK source code to build
-the static library. If it runs on other architecture, user can enable it with build parameters
-`-DWITH_UADK=true`
-
-#. Manual Build UADK
-As the above paragraph shows, the UADK is enabled automatically, no need to build manually.
-For developer who is interested in UADK, you can refer to the below steps for building.
+If the architecture is ``aarch64``, it will automatically download the UADK
+source code to build the static library. When building on other CPU
+architectures, the user may enable UADK by adding ``-DWITH_UADK=true`` to the
+compilation command line options. Note that UADK may not be compatible with all
+architectures.
+
+#. Manually Building UADK
+As implied in the above paragraph, if the architecture is ``aarch64``, the UADK
+is enabled automatically and there is no need to build it manually. However,
+below we provide the procedure for manually building UADK so that developers
+can study how it is built.
.. prompt:: bash $
@@ -115,9 +120,9 @@ For developer who is interested in UADK, you can refer to the below steps for bu
make
make install
- .. note:: Without –prefix, UADK will be installed to /usr/local/lib by
- default. If get error:"cannot find -lnuma", please install
- the `libnuma-dev`.
+ .. note:: Without ``--prefix``, UADK will be installed under
+ ``/usr/local/lib`` by default. If you get the error:
+ ``cannot find -lnuma``, install the ``libnuma-dev`` package.
#. Configure
@@ -126,7 +131,8 @@ For developer who is interested in UADK, you can refer to the below steps for bu
uadk_compressor_enabled=true
- The default value in `global.yaml.in` for `uadk_compressor_enabled` is false.
+ The default value in `global.yaml.in` for `uadk_compressor_enabled` is
+ ``false``.
.. _Compressor UADK Support: https://github.com/ceph/ceph/pull/58336
.. _OpenSSL UADK Engine: https://github.com/Linaro/uadk_engine
diff --git a/examples/rgw/boto3/bucket_logging.py b/examples/rgw/boto3/bucket_logging.py
index fdc219c5765..7a972dac8bc 100644
--- a/examples/rgw/boto3/bucket_logging.py
+++ b/examples/rgw/boto3/bucket_logging.py
@@ -39,6 +39,17 @@ bucket_logging_conf = {'LoggingEnabled': {
},
'ObjectRollTime': 60,
'LoggingType': 'Journal',
+ "Filter": {
+ "Key": {
+ "FilterRules":
+ [
+ {
+ "Name": "prefix",
+ "Value": "myfile"
+ }
+ ]
+ }
+ }
}
}
diff --git a/examples/rgw/boto3/post_bucket_logging.py b/examples/rgw/boto3/post_bucket_logging.py
new file mode 100644
index 00000000000..130fc53b50a
--- /dev/null
+++ b/examples/rgw/boto3/post_bucket_logging.py
@@ -0,0 +1,23 @@
+import boto3
+import sys
+
+
+if len(sys.argv) == 2:
+ # bucket name as first argument
+ bucketname = sys.argv[1]
+else:
+ print('Usage: ' + sys.argv[0] + ' <bucket>')
+ sys.exit(1)
+
+# endpoint and keys from vstart
+endpoint = 'http://127.0.0.1:8000/'+bucketname
+access_key='0555b35654ad1656d804'
+secret_key='h7GhxuBLTrlhVUyxSPUKUV8r/2EI4ngqJxD7iBdBYLhwluN30JaT3Q=='
+
+client = boto3.client('s3',
+ endpoint_url=endpoint,
+ aws_access_key_id=access_key,
+ aws_secret_access_key=secret_key)
+
+# flushing the logs for bucket logging
+print(client.post_bucket_logging(Bucket=bucketname))
diff --git a/examples/rgw/boto3/service-2.sdk-extras.json b/examples/rgw/boto3/service-2.sdk-extras.json
index 5c22ee9f248..b81667ecd09 100644
--- a/examples/rgw/boto3/service-2.sdk-extras.json
+++ b/examples/rgw/boto3/service-2.sdk-extras.json
@@ -13,6 +13,17 @@
"documentationUrl":"https://docs.ceph.com/docs/master/radosgw/s3/bucketops/#delete-notification",
"documentation":"<p>Deletes the notification configuration from the bucket.</p>"
},
+ "PostBucketLogging":{
+ "name":"PostBucketLogging",
+ "http":{
+ "method":"POST",
+ "requestUri":"/{Bucket}?logging",
+ "responseCode":201
+ },
+ "input":{"shape":"PostBucketLoggingRequest"},
+ "documentationUrl":"https://docs.ceph.com/docs/master/radosgw/s3/bucketops/#post-bucket-logging",
+ "documentation":"<p>Flushes the logging objects of the buckets.</p>"
+ },
"GetUsageStats":{
"name":"GetUsageStats",
"http":{
@@ -146,6 +157,18 @@
}
}
},
+ "PostBucketLoggingRequest":{
+ "type":"structure",
+ "required":["Bucket"],
+ "members":{
+ "Bucket":{
+ "shape":"BucketName",
+ "documentation":"<p>Name of the bucket to flush its logging objects.</p>",
+ "location":"uri",
+ "locationName":"Bucket"
+ }
+ }
+ },
"FilterRule":{
"type":"structure",
"members":{
@@ -287,6 +310,10 @@
"RecordsBatchSize":{
"shape":"RecordsBatchSize",
"documentation":"indicates how many records to batch in memory before writing to the object. if set to zero, records are written syncronously to the object. if <code>ObjectRollTime</code>e is reached, the batch of records will be written to the object regardless of the number of records. </p>"
+ },
+ "Filter":{
+ "shape":"LoggingConfigurationFilter",
+ "documentation":"<p>A filter for all log object. Filter for the object by its key (prefix, suffix and regex).</p>"
}
},
"documentation":"<p>Describes where logs are stored the prefix assigned to all log object keys for a bucket, and their format. also, the level the delivery guarantee of the records.</p>"
@@ -340,6 +367,18 @@
"Standard",
"Journal"
]
+ },
+ "LoggingConfigurationFilter":{
+ "type":"structure",
+ "members":{
+ "Key":{
+ "shape":"S3KeyFilter",
+ "documentation":"<p/>",
+ "locationName":"S3Key"
+ }
+ },
+ "documentation":"<p>A filter for all log object. Filter for the object by its key (prefix, suffix and regex).</p>",
+ "locationName":"Filter"
}
},
"documentation":"<p/>"
diff --git a/qa/standalone/scrub/osd-scrub-repair.sh b/qa/standalone/scrub/osd-scrub-repair.sh
index 491e46603f7..6dd5b10ae8f 100755
--- a/qa/standalone/scrub/osd-scrub-repair.sh
+++ b/qa/standalone/scrub/osd-scrub-repair.sh
@@ -5833,7 +5833,7 @@ function TEST_periodic_scrub_replicated() {
flush_pg_stats
# Request a regular scrub and it will be done
- pg_schedule_scrub $pg
+ pg_scrub $pg
grep -q "Regular scrub request, deep-scrub details will be lost" $dir/osd.${primary}.log || return 1
# deep-scrub error is no longer present
diff --git a/qa/suites/nvmeof/basic/workloads/nvmeof_initiator.yaml b/qa/suites/nvmeof/basic/workloads/nvmeof_initiator.yaml
index 7c97edae552..0416ae2ea4e 100644
--- a/qa/suites/nvmeof/basic/workloads/nvmeof_initiator.yaml
+++ b/qa/suites/nvmeof/basic/workloads/nvmeof_initiator.yaml
@@ -1,7 +1,8 @@
+# runs on default nvmeof image (i.e. DEFAULT_NVMEOF_IMAGE)
tasks:
- nvmeof:
installer: host.a
- gw_image: quay.io/ceph/nvmeof:latest # "default" is the image cephadm defaults to; change to test specific nvmeof images, example "latest"
+ gw_image: default # "default" is the image cephadm defaults to; change to test specific nvmeof images, example "latest"
rbd:
pool_name: mypool
image_name_prefix: myimage
diff --git a/qa/suites/nvmeof/basic/workloads/nvmeof_namespaces.yaml b/qa/suites/nvmeof/basic/workloads/nvmeof_namespaces.yaml
index 9ef37004427..dfe31380bb6 100644
--- a/qa/suites/nvmeof/basic/workloads/nvmeof_namespaces.yaml
+++ b/qa/suites/nvmeof/basic/workloads/nvmeof_namespaces.yaml
@@ -18,6 +18,7 @@ tasks:
clients:
client.0:
- nvmeof/setup_subsystem.sh
+ - nvmeof/basic_tests.sh
env:
RBD_POOL: mypool
RBD_IMAGE_PREFIX: myimage
@@ -27,7 +28,6 @@ tasks:
timeout: 30m
clients:
client.0:
- - nvmeof/basic_tests.sh
- nvmeof/fio_test.sh --rbd_iostat
client.1:
- nvmeof/basic_tests.sh
diff --git a/qa/suites/nvmeof/basic/workloads/nvmeof_scalability.yaml b/qa/suites/nvmeof/basic/workloads/nvmeof_scalability.yaml
index 12cb50b408d..d66b6fc8093 100644
--- a/qa/suites/nvmeof/basic/workloads/nvmeof_scalability.yaml
+++ b/qa/suites/nvmeof/basic/workloads/nvmeof_scalability.yaml
@@ -31,8 +31,11 @@ tasks:
no_coverage_and_limits: true
timeout: 30m
clients:
- client.0:
+ client.3:
- nvmeof/scalability_test.sh nvmeof.a,nvmeof.b
- nvmeof/scalability_test.sh nvmeof.b,nvmeof.c,nvmeof.d
+ - nvmeof/scalability_test.sh nvmeof.b,nvmeof.c
env:
SCALING_DELAYS: '50'
+ RBD_POOL: mypool
+ NVMEOF_GROUP: mygroup0
diff --git a/qa/suites/nvmeof/thrash/gateway-initiator-setup/10-subsys-90-namespace-no_huge_pages.yaml b/qa/suites/nvmeof/thrash/gateway-initiator-setup/10-subsys-90-namespace-no_huge_pages.yaml
new file mode 100644
index 00000000000..83d54cdf5c3
--- /dev/null
+++ b/qa/suites/nvmeof/thrash/gateway-initiator-setup/10-subsys-90-namespace-no_huge_pages.yaml
@@ -0,0 +1,37 @@
+tasks:
+- nvmeof:
+ installer: host.a
+ gw_image: quay.io/ceph/nvmeof:latest # "default" is the image cephadm defaults to; change to test specific nvmeof images, example "latest"
+ rbd:
+ pool_name: mypool
+ image_name_prefix: myimage
+ gateway_config:
+ subsystems_count: 10
+ namespaces_count: 90 # each subsystem
+ cli_image: quay.io/ceph/nvmeof-cli:latest
+
+- cephadm.wait_for_service:
+ service: nvmeof.mypool.mygroup0
+
+- cephadm.exec:
+ host.a:
+ - ceph orch ls nvmeof --export > /tmp/nvmeof-orig.yaml
+ - cp /tmp/nvmeof-orig.yaml /tmp/nvmeof-no-huge-page.yaml
+ - "sed -i '/ pool: mypool/a\\ spdk_mem_size: 4096' /tmp/nvmeof-no-huge-page.yaml"
+ - cat /tmp/nvmeof-no-huge-page.yaml
+ - ceph orch ls --refresh
+ - ceph orch apply -i /tmp/nvmeof-no-huge-page.yaml
+ - ceph orch redeploy nvmeof.mypool.mygroup0
+
+- cephadm.wait_for_service:
+ service: nvmeof.mypool.mygroup0
+
+- workunit:
+ no_coverage_and_limits: true
+ clients:
+ client.0:
+ - nvmeof/setup_subsystem.sh
+ - nvmeof/basic_tests.sh
+ env:
+ RBD_POOL: mypool
+ RBD_IMAGE_PREFIX: myimage
diff --git a/qa/suites/nvmeof/thrash/gateway-initiator-setup/3-subsys-60-namespace.yaml b/qa/suites/nvmeof/thrash/gateway-initiator-setup/120-subsys-8-namespace.yaml
index b4755a6433b..0f7ac011a60 100644
--- a/qa/suites/nvmeof/thrash/gateway-initiator-setup/3-subsys-60-namespace.yaml
+++ b/qa/suites/nvmeof/thrash/gateway-initiator-setup/120-subsys-8-namespace.yaml
@@ -6,8 +6,8 @@ tasks:
pool_name: mypool
image_name_prefix: myimage
gateway_config:
- subsystems_count: 3
- namespaces_count: 20 # each subsystem
+ subsystems_count: 120
+ namespaces_count: 8 # each subsystem
cli_image: quay.io/ceph/nvmeof-cli:latest
- cephadm.wait_for_service:
diff --git a/qa/suites/nvmeof/thrash/thrashers/nvmeof_mon_thrash.yaml b/qa/suites/nvmeof/thrash/thrashers/nvmeof_mon_thrash.yaml
index 19fa2ec605d..46037784d31 100644
--- a/qa/suites/nvmeof/thrash/thrashers/nvmeof_mon_thrash.yaml
+++ b/qa/suites/nvmeof/thrash/thrashers/nvmeof_mon_thrash.yaml
@@ -11,6 +11,7 @@ overrides:
- NVMEOF_SINGLE_GATEWAY
- NVMEOF_GATEWAY_DOWN
- are in unavailable state
+ - is unavailable
- is in error state
- failed cephadm daemon
diff --git a/qa/suites/nvmeof/thrash/thrashers/nvmeof_thrash.yaml b/qa/suites/nvmeof/thrash/thrashers/nvmeof_thrash.yaml
index 80bf0527715..b58dc14d87b 100644
--- a/qa/suites/nvmeof/thrash/thrashers/nvmeof_thrash.yaml
+++ b/qa/suites/nvmeof/thrash/thrashers/nvmeof_thrash.yaml
@@ -6,9 +6,11 @@ overrides:
- NVMEOF_SINGLE_GATEWAY
- NVMEOF_GATEWAY_DOWN
- are in unavailable state
+ - is unavailable
- is in error state
- failed cephadm daemon
tasks:
- nvmeof.thrash:
checker_host: 'client.0'
+ randomize: False
diff --git a/qa/suites/nvmeof/thrash/workloads/fio.yaml b/qa/suites/nvmeof/thrash/workloads/fio.yaml
index b042b92d6ae..f9a0d0ebde5 100644
--- a/qa/suites/nvmeof/thrash/workloads/fio.yaml
+++ b/qa/suites/nvmeof/thrash/workloads/fio.yaml
@@ -1,11 +1,11 @@
tasks:
- workunit:
no_coverage_and_limits: true
- timeout: 30m
+ timeout: 60m
clients:
client.0:
- - nvmeof/fio_test.sh --rbd_iostat
+ - nvmeof/fio_test.sh --random_devices 200
env:
RBD_POOL: mypool
IOSTAT_INTERVAL: '10'
- RUNTIME: '600'
+ RUNTIME: '1800'
diff --git a/qa/tasks/nvmeof.py b/qa/tasks/nvmeof.py
index 42e357294d9..c58a7267b4e 100644
--- a/qa/tasks/nvmeof.py
+++ b/qa/tasks/nvmeof.py
@@ -128,12 +128,11 @@ class Nvmeof(Task):
total_images = int(self.namespaces_count) * int(self.subsystems_count)
log.info(f'[nvmeof]: creating {total_images} images')
+ rbd_create_cmd = []
for i in range(1, total_images + 1):
imagename = self.image_name_prefix + str(i)
- log.info(f'[nvmeof]: rbd create {poolname}/{imagename} --size {self.rbd_size}')
- _shell(self.ctx, self.cluster_name, self.remote, [
- 'rbd', 'create', f'{poolname}/{imagename}', '--size', f'{self.rbd_size}'
- ])
+ rbd_create_cmd += ['rbd', 'create', f'{poolname}/{imagename}', '--size', f'{self.rbd_size}', run.Raw(';')]
+ _shell(self.ctx, self.cluster_name, self.remote, rbd_create_cmd)
for role, i in daemons.items():
remote, id_ = i
@@ -251,9 +250,9 @@ class NvmeofThrasher(Thrasher, Greenlet):
daemon_max_thrash_times:
For now, NVMeoF daemons have limitation that each daemon can
- be thrashed only 3 times in span of 30 mins. This option
+ be thrashed only 5 times in span of 30 mins. This option
allows to set the amount of times it could be thrashed in a period
- of time. (default: 3)
+ of time. (default: 5)
daemon_max_thrash_period:
This option goes with the above option. It sets the period of time
over which each daemons can be thrashed for daemon_max_thrash_times
@@ -306,12 +305,12 @@ class NvmeofThrasher(Thrasher, Greenlet):
self.max_thrash_daemons = int(self.config.get('max_thrash', len(self.daemons) - 1))
# Limits on thrashing each daemon
- self.daemon_max_thrash_times = int(self.config.get('daemon_max_thrash_times', 3))
+ self.daemon_max_thrash_times = int(self.config.get('daemon_max_thrash_times', 5))
self.daemon_max_thrash_period = int(self.config.get('daemon_max_thrash_period', 30 * 60)) # seconds
self.min_thrash_delay = int(self.config.get('min_thrash_delay', 60))
self.max_thrash_delay = int(self.config.get('max_thrash_delay', self.min_thrash_delay + 30))
- self.min_revive_delay = int(self.config.get('min_revive_delay', 100))
+ self.min_revive_delay = int(self.config.get('min_revive_delay', 60))
self.max_revive_delay = int(self.config.get('max_revive_delay', self.min_revive_delay + 30))
def _get_devices(self, remote):
@@ -347,6 +346,7 @@ class NvmeofThrasher(Thrasher, Greenlet):
run.Raw('&&'), 'ceph', 'orch', 'ps', '--daemon-type', 'nvmeof',
run.Raw('&&'), 'ceph', 'health', 'detail',
run.Raw('&&'), 'ceph', '-s',
+ run.Raw('&&'), 'sudo', 'nvme', 'list',
]
for dev in self.devices:
check_cmd += [
@@ -421,13 +421,11 @@ class NvmeofThrasher(Thrasher, Greenlet):
while not self.stopping.is_set():
killed_daemons = defaultdict(list)
- weight = 1.0 / len(self.daemons)
- count = 0
+ thrash_daemon_num = self.rng.randint(1, self.max_thrash_daemons)
+ selected_daemons = self.rng.sample(self.daemons, thrash_daemon_num)
for daemon in self.daemons:
- skip = self.rng.uniform(0.0, 1.0)
- if weight <= skip:
- self.log('skipping daemon {label} with skip ({skip}) > weight ({weight})'.format(
- label=daemon.id_, skip=skip, weight=weight))
+ if daemon not in selected_daemons:
+ self.log(f'skipping daemon {daemon.id_} ...')
continue
# For now, nvmeof daemons can only be thrashed 3 times in last 30mins.
@@ -445,17 +443,11 @@ class NvmeofThrasher(Thrasher, Greenlet):
continue
self.log('kill {label}'.format(label=daemon.id_))
- # daemon.stop()
kill_method = self.kill_daemon(daemon)
killed_daemons[kill_method].append(daemon)
daemons_thrash_history[daemon.id_] += [datetime.now()]
- # only thrash max_thrash_daemons amount of daemons
- count += 1
- if count >= self.max_thrash_daemons:
- break
-
if killed_daemons:
iteration_summary = "thrashed- "
for kill_method in killed_daemons:
@@ -468,7 +460,7 @@ class NvmeofThrasher(Thrasher, Greenlet):
self.log(f'waiting for {revive_delay} secs before reviving')
time.sleep(revive_delay) # blocking wait
- self.log('done waiting before reviving')
+ self.log(f'done waiting before reviving - iteration #{len(summary)}: {iteration_summary}')
self.do_checks()
self.switch_task()
@@ -487,7 +479,7 @@ class NvmeofThrasher(Thrasher, Greenlet):
if thrash_delay > 0.0:
self.log(f'waiting for {thrash_delay} secs before thrashing')
time.sleep(thrash_delay) # blocking
- self.log('done waiting before thrashing')
+ self.log('done waiting before thrashing - everything should be up now')
self.do_checks()
self.switch_task()
diff --git a/qa/workunits/nvmeof/basic_tests.sh b/qa/workunits/nvmeof/basic_tests.sh
index dc6fd1669da..794353348b4 100755
--- a/qa/workunits/nvmeof/basic_tests.sh
+++ b/qa/workunits/nvmeof/basic_tests.sh
@@ -38,8 +38,10 @@ disconnect_all() {
connect_all() {
sudo nvme connect-all --traddr=$NVMEOF_DEFAULT_GATEWAY_IP_ADDRESS --transport=tcp -l 3600
sleep 5
- output=$(sudo nvme list --output-format=json)
- if ! echo "$output" | grep -q "$SPDK_CONTROLLER"; then
+ expected_devices_count=$1
+ actual_devices=$(sudo nvme list --output-format=json | grep -o "$SPDK_CONTROLLER" | wc -l)
+ if [ "$actual_devices" -ne "$expected_devices_count" ]; then
+ sudo nvme list --output-format=json
return 1
fi
}
@@ -72,11 +74,13 @@ test_run connect
test_run list_subsys 1
test_run disconnect_all
test_run list_subsys 0
-test_run connect_all
+devices_count=$(( $NVMEOF_NAMESPACES_COUNT * $NVMEOF_SUBSYSTEMS_COUNT))
+test_run connect_all $devices_count
gateways_count=$(( $(echo "$NVMEOF_GATEWAY_IP_ADDRESSES" | tr -cd ',' | wc -c) + 1 ))
multipath_count=$(( $gateways_count * $NVMEOF_SUBSYSTEMS_COUNT))
test_run list_subsys $multipath_count
+
echo "-------------Test Summary-------------"
echo "[nvmeof] All nvmeof basic tests passed!"
diff --git a/qa/workunits/nvmeof/fio_test.sh b/qa/workunits/nvmeof/fio_test.sh
index 57d355a6318..03fb58693bd 100755
--- a/qa/workunits/nvmeof/fio_test.sh
+++ b/qa/workunits/nvmeof/fio_test.sh
@@ -5,6 +5,7 @@ sudo yum -y install sysstat
namespace_range_start=
namespace_range_end=
+random_devices_count=
rbd_iostat=false
while [[ $# -gt 0 ]]; do
@@ -17,6 +18,10 @@ while [[ $# -gt 0 ]]; do
namespace_range_end=$2
shift 2
;;
+ --random_devices)
+ random_devices_count=$2
+ shift 2
+ ;;
--rbd_iostat)
rbd_iostat=true
shift
@@ -37,6 +42,8 @@ all_drives_list=$(sudo nvme list --output-format=json |
# run on first 3 namespaces here.
if [ "$namespace_range_start" ] || [ "$namespace_range_end" ]; then
selected_drives=$(echo "${all_drives_list[@]}" | sed -n "${namespace_range_start},${namespace_range_end}p")
+elif [ "$random_devices_count" ]; then
+ selected_drives=$(echo "${all_drives_list[@]}" | shuf -n $random_devices_count)
else
selected_drives="${all_drives_list[@]}"
fi
diff --git a/qa/workunits/nvmeof/scalability_test.sh b/qa/workunits/nvmeof/scalability_test.sh
index 5a26b6284f7..8ede4b7eda2 100755
--- a/qa/workunits/nvmeof/scalability_test.sh
+++ b/qa/workunits/nvmeof/scalability_test.sh
@@ -3,37 +3,64 @@
GATEWAYS=$1 # exmaple "nvmeof.a,nvmeof.b"
DELAY="${SCALING_DELAYS:-50}"
+POOL="${RBD_POOL:-mypool}"
+GROUP="${NVMEOF_GROUP:-mygroup0}"
+source /etc/ceph/nvmeof.env
if [ -z "$GATEWAYS" ]; then
echo "At least one gateway needs to be defined for scalability test"
exit 1
fi
-pip3 install yq
-
status_checks() {
- ceph nvme-gw show mypool ''
- ceph orch ls
- ceph orch ps
- ceph -s
+ expected_count=$1
+
+ output=$(ceph nvme-gw show $POOL $GROUP)
+ nvme_show=$(echo $output | grep -o '"AVAILABLE"' | wc -l)
+ if [ "$nvme_show" -ne "$expected_count" ]; then
+ return 1
+ fi
+
+ orch_ls=$(ceph orch ls)
+ if ! echo "$orch_ls" | grep -q "$expected_count/$expected_count"; then
+ return 1
+ fi
+
+ output=$(ceph orch ps --service-name nvmeof.$POOL.$GROUP)
+ orch_ps=$(echo $output | grep -o 'running' | wc -l)
+ if [ "$orch_ps" -ne "$expected_count" ]; then
+ return 1
+ fi
+
+ ceph_status=$(ceph -s)
+ if ! echo "$ceph_status" | grep -q "HEALTH_OK"; then
+ return 1
+ fi
}
+total_gateways_count=$(( $(echo "$NVMEOF_GATEWAY_IP_ADDRESSES" | tr -cd ',' | wc -c) + 1 ))
+scaled_down_gateways_count=$(( total_gateways_count - $(echo "$GATEWAYS" | tr -cd ',' | wc -c) - 1 ))
+
echo "[nvmeof.scale] Setting up config to remove gateways ${GATEWAYS}"
+ceph orch ls --service-name nvmeof.$POOL.$GROUP --export > /tmp/nvmeof-gw.yaml
ceph orch ls nvmeof --export > /tmp/nvmeof-gw.yaml
cat /tmp/nvmeof-gw.yaml
-yq "del(.placement.hosts[] | select(. | test(\".*($(echo $GATEWAYS | sed 's/,/|/g'))\")))" /tmp/nvmeof-gw.yaml > /tmp/nvmeof-gw-new.yaml
+
+pattern=$(echo $GATEWAYS | sed 's/,/\\|/g')
+sed "/$pattern/d" /tmp/nvmeof-gw.yaml > /tmp/nvmeof-gw-new.yaml
cat /tmp/nvmeof-gw-new.yaml
echo "[nvmeof.scale] Starting scale testing by removing ${GATEWAYS}"
-status_checks
-ceph orch rm nvmeof.mypool && sleep 20 # temp workaround
+status_checks $total_gateways_count
ceph orch apply -i /tmp/nvmeof-gw-new.yaml # downscale
+ceph orch redeploy nvmeof.$POOL.$GROUP
sleep $DELAY
-status_checks
-ceph orch rm nvmeof.mypool && sleep 20 # temp workaround
+status_checks $scaled_down_gateways_count
+echo "[nvmeof.scale] Downscale complete - removed gateways (${GATEWAYS}); now scaling back up"
ceph orch apply -i /tmp/nvmeof-gw.yaml #upscale
+ceph orch redeploy nvmeof.$POOL.$GROUP
sleep $DELAY
-status_checks
+status_checks $total_gateways_count
echo "[nvmeof.scale] Scale testing passed for ${GATEWAYS}"
diff --git a/qa/workunits/nvmeof/setup_subsystem.sh b/qa/workunits/nvmeof/setup_subsystem.sh
index cc4024323eb..b573647b1e3 100755
--- a/qa/workunits/nvmeof/setup_subsystem.sh
+++ b/qa/workunits/nvmeof/setup_subsystem.sh
@@ -26,14 +26,21 @@ list_subsystems () {
done
}
+list_namespaces () {
+ for i in $(seq 1 $NVMEOF_SUBSYSTEMS_COUNT); do
+ subsystem_nqn="${NVMEOF_SUBSYSTEMS_PREFIX}${i}"
+ sudo podman run -it $NVMEOF_CLI_IMAGE --server-address $NVMEOF_DEFAULT_GATEWAY_IP_ADDRESS --server-port $NVMEOF_SRPORT --format plain namespace list --subsystem $subsystem_nqn
+ done
+}
+
+echo "[nvmeof] Starting subsystem setup..."
+
# add all subsystems
for i in $(seq 1 $NVMEOF_SUBSYSTEMS_COUNT); do
subsystem_nqn="${NVMEOF_SUBSYSTEMS_PREFIX}${i}"
sudo podman run -it $NVMEOF_CLI_IMAGE --server-address $NVMEOF_DEFAULT_GATEWAY_IP_ADDRESS --server-port $NVMEOF_SRPORT subsystem add --subsystem $subsystem_nqn --no-group-append
done
-list_subsystems
-
# add all gateway listeners
for i in "${!gateway_ips[@]}"
do
@@ -65,11 +72,5 @@ done
list_subsystems
-# list namespaces
-for i in $(seq 1 $NVMEOF_SUBSYSTEMS_COUNT); do
- subsystem_nqn="${NVMEOF_SUBSYSTEMS_PREFIX}${i}"
- sudo podman run -it $NVMEOF_CLI_IMAGE --server-address $NVMEOF_DEFAULT_GATEWAY_IP_ADDRESS --server-port $NVMEOF_SRPORT --format plain namespace list --subsystem $subsystem_nqn
-done
-
echo "[nvmeof] Subsystem setup done"
diff --git a/src/mon/NVMeofGwMon.cc b/src/mon/NVMeofGwMon.cc
index 0fe5c3e655f..c9a6b789b89 100644
--- a/src/mon/NVMeofGwMon.cc
+++ b/src/mon/NVMeofGwMon.cc
@@ -66,11 +66,6 @@ void NVMeofGwMon::on_shutdown()
void NVMeofGwMon::tick()
{
- if (++tick_ratio == 10) {
- global_rebalance_index++;
- dout(20) << "rebalance index " << global_rebalance_index << dendl;
- tick_ratio = 0;
- }
if (!is_active() || !mon.is_leader()) {
dout(10) << "NVMeofGwMon leader : " << mon.is_leader()
<< "active : " << is_active() << dendl;
@@ -329,8 +324,9 @@ bool NVMeofGwMon::preprocess_command(MonOpRequestRef op)
if (HAVE_FEATURE(mon.get_quorum_con_features(), NVMEOFHA)) {
f->dump_string("features", "LB");
if (map.created_gws[group_key].size()) {
- uint32_t index = (global_rebalance_index %
- map.created_gws[group_key].size()) + 1;
+ time_t seconds_since_1970 = time(NULL);
+ uint32_t index = ((seconds_since_1970/60) %
+ map.created_gws[group_key].size()) + 1;
f->dump_unsigned("rebalance_ana_group", index);
}
}
@@ -625,15 +621,15 @@ bool NVMeofGwMon::prepare_beacon(MonOpRequestRef op)
avail = gw_availability_t::GW_CREATED;
dout(20) << "No-subsystems condition detected for GW " << gw_id <<dendl;
} else {
- bool listener_found = true;
+ bool listener_found = false;
for (auto &subs: sub) {
- if (subs.listeners.size() == 0) {
- listener_found = false;
- dout(10) << "No-listeners condition detected for GW " << gw_id << " for nqn " << subs.nqn << dendl;
+ if (subs.listeners.size()) {
+ listener_found = true;
break;
}
}
if (!listener_found) {
+ dout(10) << "No-listeners condition detected for GW " << gw_id << dendl;
avail = gw_availability_t::GW_CREATED;
}
}// for HA no-subsystems and no-listeners are same usecases
diff --git a/src/mon/NVMeofGwMon.h b/src/mon/NVMeofGwMon.h
index 2d13e153bd2..7fae8b766a5 100644
--- a/src/mon/NVMeofGwMon.h
+++ b/src/mon/NVMeofGwMon.h
@@ -83,9 +83,6 @@ public:
void check_sub(Subscription *sub);
private:
- // used for calculate pool & group GW responsible for rebalance
- uint32_t global_rebalance_index = 1;
- uint8_t tick_ratio = 0;
void synchronize_last_beacon();
void process_gw_down(const NvmeGwId &gw_id,
const NvmeGroupKey& group_key, bool &propose_pending,
diff --git a/src/msg/async/Event.cc b/src/msg/async/Event.cc
index 08e117ea54a..16abb1368b0 100644
--- a/src/msg/async/Event.cc
+++ b/src/msg/async/Event.cc
@@ -347,7 +347,7 @@ void EventCenter::wakeup()
return ;
ldout(cct, 20) << __func__ << dendl;
- char buf = 'c';
+ static constexpr char buf = 'c';
// wake up "event_wait"
#ifdef _WIN32
int n = send(notify_send_fd, &buf, sizeof(buf), 0);
diff --git a/src/nvmeof/NVMeofGwMonitorClient.cc b/src/nvmeof/NVMeofGwMonitorClient.cc
index ce3328aec51..1b128055e08 100644
--- a/src/nvmeof/NVMeofGwMonitorClient.cc
+++ b/src/nvmeof/NVMeofGwMonitorClient.cc
@@ -42,7 +42,6 @@ NVMeofGwMonitorClient::NVMeofGwMonitorClient(int argc, const char **argv) :
monc{g_ceph_context, poolctx},
client_messenger(Messenger::create(g_ceph_context, "async", entity_name_t::CLIENT(-1), "client", getpid())),
objecter{g_ceph_context, client_messenger.get(), &monc, poolctx},
- client{client_messenger.get(), &monc, &objecter},
timer(g_ceph_context, beacon_lock),
orig_argc(argc),
orig_argv(argv)
@@ -134,7 +133,6 @@ int NVMeofGwMonitorClient::init()
// Initialize Messenger
client_messenger->add_dispatcher_tail(this);
client_messenger->add_dispatcher_head(&objecter);
- client_messenger->add_dispatcher_tail(&client);
client_messenger->start();
poolctx.start(2);
@@ -190,7 +188,6 @@ int NVMeofGwMonitorClient::init()
objecter.init();
objecter.enable_blocklist_events();
objecter.start();
- client.init();
timer.init();
{
@@ -302,8 +299,7 @@ void NVMeofGwMonitorClient::shutdown()
std::lock_guard bl(beacon_lock);
timer.shutdown();
}
- // client uses monc and objecter
- client.shutdown();
+
// Stop asio threads, so leftover events won't call into shut down
// monclient/objecter.
poolctx.finish();
diff --git a/src/nvmeof/NVMeofGwMonitorClient.h b/src/nvmeof/NVMeofGwMonitorClient.h
index 6dd167e4e58..e01c823afb5 100644
--- a/src/nvmeof/NVMeofGwMonitorClient.h
+++ b/src/nvmeof/NVMeofGwMonitorClient.h
@@ -21,7 +21,6 @@
#include "common/Timer.h"
#include "common/LogClient.h"
-#include "client/Client.h"
#include "mon/MonClient.h"
#include "osdc/Objecter.h"
#include "messages/MNVMeofGwMap.h"
@@ -58,7 +57,6 @@ protected:
MonClient monc;
std::unique_ptr<Messenger> client_messenger;
Objecter objecter;
- Client client;
std::map<NvmeGroupKey, NvmeGwMonClientStates> map;
ceph::mutex lock = ceph::make_mutex("NVMeofGw::lock");
// allow beacons to be sent independently of handle_nvmeof_gw_map
diff --git a/src/osd/PG.cc b/src/osd/PG.cc
index 307651fd627..cd7cad777bc 100644
--- a/src/osd/PG.cc
+++ b/src/osd/PG.cc
@@ -1278,7 +1278,6 @@ Scrub::schedule_result_t PG::start_scrubbing(
pg_cond.allow_deep =
!(get_osdmap()->test_flag(CEPH_OSDMAP_NODEEP_SCRUB) ||
pool.info.has_flag(pg_pool_t::FLAG_NODEEP_SCRUB));
- pg_cond.has_deep_errors = (info.stats.stats.sum.num_deep_scrub_errors > 0);
pg_cond.can_autorepair =
(cct->_conf->osd_scrub_auto_repair &&
get_pgbackend()->auto_repair_supported());
diff --git a/src/osd/scrubber/pg_scrubber.cc b/src/osd/scrubber/pg_scrubber.cc
index aa53df5ae8a..fea8c757040 100644
--- a/src/osd/scrubber/pg_scrubber.cc
+++ b/src/osd/scrubber/pg_scrubber.cc
@@ -2299,26 +2299,6 @@ Scrub::schedule_result_t PgScrubber::start_scrub_session(
}
}
- // restricting shallow scrubs of PGs that have deep errors:
- if (pg_cond.has_deep_errors && trgt.is_shallow()) {
- if (trgt.urgency() < urgency_t::operator_requested) {
- // if there are deep errors, we should have scheduled a deep scrub first.
- // If we are here trying to perform a shallow scrub, it means that for some
- // reason that deep scrub failed to be initiated. We will not try a shallow
- // scrub until this is solved.
- dout(10) << __func__ << ": Regular scrub skipped due to deep-scrub errors"
- << dendl;
- requeue_penalized(
- s_or_d, delay_both_targets_t::no, delay_cause_t::pg_state, clock_now);
- return schedule_result_t::target_specific_failure;
- } else {
- // we will honor the request anyway, but will report the issue
- m_osds->clog->error() << fmt::format(
- "osd.{} pg {} Regular scrub request, deep-scrub details will be lost",
- m_osds->whoami, m_pg_id);
- }
- }
-
// if only explicitly requested repairing is allowed - skip other types
// of scrubbing
if (osd_restrictions.allow_requested_repair_only &&
diff --git a/src/osd/scrubber_common.h b/src/osd/scrubber_common.h
index 809107e593b..2ab5570a715 100644
--- a/src/osd/scrubber_common.h
+++ b/src/osd/scrubber_common.h
@@ -109,7 +109,6 @@ static_assert(sizeof(Scrub::OSDRestrictions) <= sizeof(uint32_t));
struct ScrubPGPreconds {
bool allow_shallow{true};
bool allow_deep{true};
- bool has_deep_errors{false};
bool can_autorepair{false};
};
static_assert(sizeof(Scrub::ScrubPGPreconds) <= sizeof(uint32_t));
@@ -181,9 +180,8 @@ struct formatter<Scrub::ScrubPGPreconds> {
auto format(const Scrub::ScrubPGPreconds& conds, FormatContext& ctx) const
{
return fmt::format_to(
- ctx.out(), "allowed(shallow/deep):{:1}/{:1},deep-err:{:1},can-autorepair:{:1}",
- conds.allow_shallow, conds.allow_deep, conds.has_deep_errors,
- conds.can_autorepair);
+ ctx.out(), "allowed(shallow/deep):{:1}/{:1},can-autorepair:{:1}",
+ conds.allow_shallow, conds.allow_deep, conds.can_autorepair);
}
};
diff --git a/src/osdc/CMakeLists.txt b/src/osdc/CMakeLists.txt
index 205ad3d4f42..637ce327555 100644
--- a/src/osdc/CMakeLists.txt
+++ b/src/osdc/CMakeLists.txt
@@ -1,9 +1,8 @@
set(osdc_files
Filer.cc
ObjectCacher.cc
- Objecter.cc
- error_code.cc
- Striper.cc)
+ error_code.cc)
+# Objecter.cc and Striper.cc are part of libcommon
add_library(osdc STATIC ${osdc_files})
target_link_libraries(osdc ceph-common)
if(WITH_EVENTTRACE)
diff --git a/src/pybind/mgr/dashboard/controllers/rgw_iam.py b/src/pybind/mgr/dashboard/controllers/rgw_iam.py
new file mode 100644
index 00000000000..458bbbb7321
--- /dev/null
+++ b/src/pybind/mgr/dashboard/controllers/rgw_iam.py
@@ -0,0 +1,52 @@
+from typing import Optional
+
+from ..security import Scope
+from ..services.rgw_iam import RgwAccounts
+from ..tools import str_to_bool
+from . import APIDoc, APIRouter, EndpointDoc, RESTController, allow_empty_body
+
+
+@APIRouter('rgw/accounts', Scope.RGW)
+@APIDoc("RGW User Accounts API", "RgwUserAccounts")
+class RgwUserAccountsController(RESTController):
+
+ @allow_empty_body
+ def create(self, account_name: Optional[str] = None,
+ account_id: Optional[str] = None, email: Optional[str] = None):
+ return RgwAccounts.create_account(account_name, account_id, email)
+
+ def list(self, detailed: bool = False):
+ detailed = str_to_bool(detailed)
+ return RgwAccounts.get_accounts(detailed)
+
+ @EndpointDoc("Get RGW Account by id",
+ parameters={'account_id': (str, 'Account id')})
+ def get(self, account_id: str):
+ return RgwAccounts.get_account(account_id)
+
+ @EndpointDoc("Delete RGW Account",
+ parameters={'account_id': (str, 'Account id')})
+ def delete(self, account_id):
+ return RgwAccounts.delete_account(account_id)
+
+ @EndpointDoc("Update RGW account info",
+ parameters={'account_id': (str, 'Account id')})
+ @allow_empty_body
+ def set(self, account_id: str, account_name: Optional[str] = None,
+ email: Optional[str] = None):
+ return RgwAccounts.modify_account(account_id, account_name, email)
+
+ @EndpointDoc("Set RGW Account/Bucket quota",
+ parameters={'account_id': (str, 'Account id'),
+ 'max_size': (str, 'Max size')})
+ @RESTController.Resource(method='PUT', path='/quota')
+ @allow_empty_body
+ def set_quota(self, quota_type: str, account_id: str, max_size: str, max_objects: str):
+ return RgwAccounts.set_quota(quota_type, account_id, max_size, max_objects)
+
+ @EndpointDoc("Enable/Disable RGW Account/Bucket quota",
+ parameters={'account_id': (str, 'Account id')})
+ @RESTController.Resource(method='PUT', path='/quota/status')
+ @allow_empty_body
+ def set_quota_status(self, quota_type: str, account_id: str, quota_status: str):
+ return RgwAccounts.set_quota_status(quota_type, account_id, quota_status)
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
index 3a9ce12df9d..16963b06920 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
@@ -60,6 +60,7 @@
</cd-dashboard-area-chart>
<cd-dashboard-area-chart chartTitle="Latency"
dataUnits="ms"
+ decimals="2"
[labelsArray]="['GET', 'PUT']"
[dataArray]="[queriesResults.AVG_GET_LATENCY, queriesResults.AVG_PUT_LATENCY]">
</cd-dashboard-area-chart>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
index c0e0517896c..f1f04f7c2f0 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
@@ -17,13 +17,48 @@ import { UserFormComponent } from './user-form/user-form.component';
import { UserListComponent } from './user-list/user-list.component';
import { UserPasswordFormComponent } from './user-password-form/user-password-form.component';
import { UserTabsComponent } from './user-tabs/user-tabs.component';
-import { ButtonModule, GridModule, IconModule, InputModule } from 'carbon-components-angular';
+
+import {
+ ButtonModule,
+ CheckboxModule,
+ DatePickerModule,
+ GridModule,
+ IconModule,
+ IconService,
+ InputModule,
+ ModalModule,
+ NumberModule,
+ RadioModule,
+ SelectModule,
+ UIShellModule,
+ TimePickerModule,
+ ComboBoxModule
+} from 'carbon-components-angular';
+// Icons
+import ChevronDown from '@carbon/icons/es/chevron--down/16';
+import Close from '@carbon/icons/es/close/32';
+import AddFilled from '@carbon/icons/es/add--filled/32';
+import SubtractFilled from '@carbon/icons/es/subtract--filled/32';
+import Reset from '@carbon/icons/es/reset/32';
+import EyeIcon from '@carbon/icons/es/view/16';
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
SharedModule,
+ UIShellModule,
+ InputModule,
+ GridModule,
+ ButtonModule,
+ IconModule,
+ CheckboxModule,
+ RadioModule,
+ SelectModule,
+ NumberModule,
+ ModalModule,
+ DatePickerModule,
+ TimePickerModule,
NgbNavModule,
NgbPopoverModule,
NgxPipeFunctionModule,
@@ -31,8 +66,8 @@ import { ButtonModule, GridModule, IconModule, InputModule } from 'carbon-compon
NgbModule,
IconModule,
GridModule,
- ButtonModule,
- InputModule
+ InputModule,
+ ComboBoxModule
],
declarations: [
LoginComponent,
@@ -46,7 +81,11 @@ import { ButtonModule, GridModule, IconModule, InputModule } from 'carbon-compon
UserPasswordFormComponent
]
})
-export class AuthModule {}
+export class AuthModule {
+ constructor(private iconService: IconService) {
+ this.iconService.registerAll([ChevronDown, Close, AddFilled, SubtractFilled, Reset, EyeIcon]);
+ }
+}
const routes: Routes = [
{ path: '', redirectTo: 'users', pathMatch: 'full' },
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts
index 2d323b04ea5..abf529196f6 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form-role.model.ts
@@ -4,11 +4,12 @@ export class UserFormRoleModel implements SelectOption {
name: string;
description: string;
selected = false;
- scopes_permissions: object;
- enabled = true;
-
- constructor(name: string, description: string) {
+ scopes_permissions?: object;
+ enabled: boolean;
+ content: string;
+ constructor(name: string, description: string, content: string) {
this.name = name;
this.description = description;
+ this.content = content;
}
}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
index 4169d54c39f..d2e52158473 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
@@ -1,249 +1,205 @@
-<div class="cd-col-form"
- *cdFormLoading="loading">
- <form name="userForm"
- #formDir="ngForm"
- [formGroup]="userForm"
- novalidate>
- <div class="card">
+<div cdsCol
+ [columnNumbers]="{md: 4}">
+ <ng-container *cdFormLoading="loading">
+ <form #frm="ngForm"
+ #formDir="ngForm"
+ [formGroup]="userForm"
+ novalidate>
<div i18n="form title"
- class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
- <div class="card-body">
-
- <!-- Username -->
- <div class="form-group row">
- <label class="cd-col-form-label"
- [ngClass]="{'required': mode !== userFormMode.editing}"
- for="username"
- i18n>Username</label>
- <div class="cd-col-form-input">
- <input class="form-control"
- type="text"
- placeholder="Username..."
- id="username"
- name="username"
- formControlName="username"
- autocomplete="off"
- autofocus
- ngbTooltip="White spaces at the beginning and end will be trimmed"
- i18n-ngbTooltip
- cdTrim>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('username', formDir, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('username', formDir, 'notUnique')"
- i18n>The username already exists.</span>
- </div>
- </div>
-
- <!-- Password -->
- <div class="form-group row"
- *ngIf="!authStorageService.isSSO()">
- <label class="cd-col-form-label"
- for="password">
- <ng-container i18n>Password</ng-container>
- <cd-helper *ngIf="passwordPolicyHelpText.length > 0"
- class="text-pre-wrap"
- html="{{ passwordPolicyHelpText }}">
- </cd-helper>
- </label>
- <div class="cd-col-form-input">
- <div class="input-group">
- <input class="form-control"
- type="password"
- placeholder="Password..."
- id="password"
- name="password"
- autocomplete="new-password"
- formControlName="password">
- <button type="button"
- class="btn btn-light"
- cdPasswordButton="password">
- </button>
- </div>
- <div class="password-strength-level">
- <div class="{{ passwordStrengthLevelClass }}"
- data-toggle="tooltip"
- title="{{ passwordValuation }}">
- </div>
- </div>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('password', formDir, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('password', formDir, 'passwordPolicy')">
- {{ passwordValuation }}
- </span>
- </div>
- </div>
-
- <!-- Confirm password -->
- <div class="form-group row"
- *ngIf="!authStorageService.isSSO()">
- <label i18n
- class="cd-col-form-label"
- for="confirmpassword">Confirm password</label>
- <div class="cd-col-form-input">
- <div class="input-group">
- <input class="form-control"
- type="password"
- placeholder="Confirm password..."
- id="confirmpassword"
- name="confirmpassword"
- autocomplete="new-password"
- formControlName="confirmpassword">
- <button type="button"
- class="btn btn-light"
- cdPasswordButton="confirmpassword">
- </button>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('confirmpassword', formDir, 'match')"
- i18n>Password confirmation doesn't match the password.</span>
- </div>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('confirmpassword', formDir, 'required')"
- i18n>This field is required.</span>
- </div>
- </div>
+ class="form-header">{{ action | titlecase }} {{ resource | upperFirst }}
+ </div>
+ <!-- UserName -->
+ <div class="form-item">
+ <cds-text-label labelInputID="username"
+ cdRequiredField="Username"
+ [invalid]="!userForm.controls.username.valid && userForm.controls.username.dirty"
+ [invalidText]="usernameError"
+ i18n>Username
+ <input cdsText
+ placeholder="Username..."
+ i18n-placeholder
+ id="username"
+ formControlName="username"
+ [invalid]="!userForm.controls.username.valid && userForm.controls.username.dirty"
+ autofocus
+ ngbTooltip="White spaces at the beginning and end will be trimmed"
+ i18n-ngbTooltip
+ cdTrim>
+ </cds-text-label>
+ <ng-template #usernameError>
+ <span *ngIf="userForm.showError('username', formDir, 'required')">
+ <ng-container i18n>
+ This field is required.
+ </ng-container>
+ </span>
+ <span *ngIf="userForm.showError('username', formDir, 'notUnique')">
+ <ng-container i18n>
+ The username already exists.
+ </ng-container>
+ </span>
+ </ng-template>
+ </div>
+ <!-- Password -->
+ <div class="form-item">
+ <cds-password-label labelInputID="password"
+ label="Password..."
+ [invalid]="!userForm.controls.password.valid && userForm.controls.password.dirty"
+ [invalidText]="passwordError"
+ i18n>Password
+ <cd-helper *ngIf="passwordPolicyHelpText.length > 0"
+ class="text-pre-wrap"
+ html="{{ passwordPolicyHelpText }}">
+ </cd-helper>
+ <input cdsPassword
+ type="password"
+ placeholder="Password..."
+ id="password"
+ autocomplete="new-password"
+ formControlName="password"
+ >
+ </cds-password-label>
+ <ng-template #passwordError>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('password', formDir, 'match')"
+ i18n>Password confirmation doesn't match the password.
+ </span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('password', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('password', formDir, 'passwordPolicy')">
+ {{ passwordValuation }}
+ </span>
+ </ng-template>
+ </div>
- <!-- Password expiration date -->
- <div class="form-group row"
- *ngIf="!authStorageService.isSSO()">
- <label class="cd-col-form-label"
- [ngClass]="{'required': pwdExpirationSettings.pwdExpirationSpan > 0}"
- for="pwdExpirationDate">
- <ng-container i18n>Password expiration date</ng-container>
- <cd-helper class="text-pre-wrap"
- *ngIf="pwdExpirationSettings.pwdExpirationSpan == 0">
- <p>
+ <!-- Confirm password -->
+ <div class="form-item">
+ <cds-password-label labelInputID="confirmpassword"
+ label="Confirm password..."
+ [invalid]="!userForm.controls.confirmpassword.valid && userForm.controls.confirmpassword.dirty"
+ [invalidText]="confirmpasswordError"
+ i18n> Confirm password
+ <input cdsPassword
+ type="password"
+ placeholder="Confirm password..."
+ id="confirmpassword"
+ formControlName="confirmpassword">
+ </cds-password-label>
+ <ng-template #confirmpasswordError>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmpassword', formDir, 'match')"
+ i18n>Password confirmation doesn't match the password.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('confirmpassword', formDir, 'required')"
+ i18n>This field is required.</span>
+ </ng-template>
+ </div>
+ <!-- Password expiration date -->
+ <div class="form-item"
+ *ngIf="!authStorageService.isSSO()">
+ <cds-text-label [ngClass]="{'required': pwdExpirationSettings.pwdExpirationSpan > 0}">{{'Password Expiration Date'}}
+ <cd-helper class="text-pre-wrap"
+ *ngIf="pwdExpirationSettings.pwdExpirationSpan == 0">
+ <span>
The Dashboard setting defining the expiration interval of
passwords is currently set to <strong>0</strong>. This means
if a date is set, the user password will only expire once.
- </p>
- <p>
- Consider configuring the Dashboard setting
- <a routerLink="/mgr-modules/edit/dashboard"
- class="alert-link">USER_PWD_EXPIRATION_SPAN</a>
- in order to let passwords expire periodically.
- </p>
- </cd-helper>
- </label>
- <div class="cd-col-form-input">
- <div class="input-group">
- <input class="form-control"
- i18n-placeholder
- placeholder="Password expiration date..."
- id="pwdExpirationDate"
- name="pwdExpirationDate"
- formControlName="pwdExpirationDate"
- [ngbPopover]="popContent"
- triggers="manual"
- #p="ngbPopover"
- (click)="p.open()"
- (keypress)="p.close()">
- <button type="button"
- class="btn btn-light"
- (click)="clearExpirationDate()">
- <i class="icon-prepend {{ icons.destroy }}"></i>
- </button>
- <span class="invalid-feedback"
- *ngIf="userForm.showError('pwdExpirationDate', formDir, 'required')"
- i18n>This field is required.</span>
- </div>
- </div>
- </div>
-
- <!-- Name -->
- <div class="form-group row">
- <label i18n
- class="cd-col-form-label"
- for="name">Full name</label>
- <div class="cd-col-form-input">
- <input class="form-control"
- type="text"
- placeholder="Full name..."
- id="name"
- name="name"
- formControlName="name">
- </div>
- </div>
-
- <!-- Email -->
- <div class="form-group row">
- <label i18n
- class="cd-col-form-label"
- for="email">Email</label>
- <div class="cd-col-form-input">
- <input class="form-control"
- type="email"
- placeholder="Email..."
- id="email"
- name="email"
- formControlName="email">
-
- <span class="invalid-feedback"
- *ngIf="userForm.showError('email', formDir, 'email')"
- i18n>Invalid email.</span>
- </div>
- </div>
-
- <!-- Roles -->
- <div class="form-group row">
- <label class="cd-col-form-label"
- i18n>Roles</label>
- <div class="cd-col-form-input">
- <span class="no-border full-height"
- *ngIf="allRoles">
- <cd-select-badges [data]="userForm.controls.roles.value"
- [options]="allRoles"
- [messages]="messages"></cd-select-badges>
</span>
- </div>
- </div>
-
- <!-- Enabled -->
- <div class="form-group row"
- *ngIf="!isCurrentUser()">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-checkbox">
- <input type="checkbox"
- class="custom-control-input"
- id="enabled"
- name="enabled"
- formControlName="enabled">
- <label class="custom-control-label"
- for="enabled"
- i18n>Enabled</label>
- </div>
- </div>
- </div>
-
- <!-- Force change password -->
- <div class="form-group row"
- *ngIf="!isCurrentUser() && !authStorageService.isSSO()">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-checkbox">
- <input type="checkbox"
- class="custom-control-input"
- id="pwdUpdateRequired"
- name="pwdUpdateRequired"
- formControlName="pwdUpdateRequired">
- <label class="custom-control-label"
- for="pwdUpdateRequired"
- i18n>User must change password at next logon</label>
- </div>
- </div>
- </div>
-
+ <span>Consider configuring the Dashboard setting
+ <a routerLink="/mgr-modules/edit/dashboard"
+ class="alert-link">USER_PWD_EXPIRATION_SPAN</a>
+ in order to let passwords expire periodically.
+ </span>
+ </cd-helper>
+ <cd-date-time-picker [control]="userForm.get('pwdExpirationDate')"
+ placeHolder="Password expiration date"
+ [hasTime]="false"
+ [defaultDate]="passwordexp"
+ i18n-name
+ >
+ </cd-date-time-picker>
+ </cds-text-label>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('pwdExpirationDate', formDir, 'required')"
+ i18n>This field is required.
+ </span>
+ </div>
+ <!--Full Name-->
+ <div class="form-item">
+ <cds-text-label for="name"
+ i18n> Full Name
+ <input cdsText
+ type="text"
+ placeholder="Full Name..."
+ id="name"
+ formControlName="name">
+ </cds-text-label>
+ </div>
+ <!-- Email -->
+ <div class="form-item">
+ <cds-text-label for="email"
+ [invalid]="!userForm.controls.email.valid && userForm.controls.email.dirty"
+ [invalidText]="emailError"
+ i18n>
+ Email
+ <input cdsText
+ type="email"
+ placeholder="Email..."
+ id="email"
+ formControlName="email">
+ </cds-text-label>
+ <ng-template #emailError>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('email', formDir, 'email')"
+ i18n>Invalid email.
+ </span>
+ </ng-template>
+ </div>
+ <!-- Roles -->
+ <div class="form-item"
+ *ngIf="allRoles">
+ <cds-combo-box label="Roles"
+ type="multi"
+ selectionFeedback="top-after-reopen"
+ for="roles"
+ formControlName="roles"
+ id="roles"
+ placeholder="Select Roles..."
+ i18n-placeholder
+ [appendInline]="true"
+ [items]="allRoles"
+ itemValueKey="name"
+ i18n>
+ <cds-dropdown-list></cds-dropdown-list>
+ </cds-combo-box>
+ </div>
+ <!-- Enabled -->
+ <div class="form-item"
+ *ngIf="!isCurrentUser()">
+ <cds-checkbox id="enabled"
+ formControlName="enabled"
+ name="enabled"
+ i18n>Enabled
+ </cds-checkbox>
</div>
- <div class="card-footer">
- <cd-form-button-panel (submitActionEvent)="submit()"
- [form]="userForm"
- [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
- wrappingClass="text-right"></cd-form-button-panel>
+ <!-- Force change password -->
+ <div class="form-item"
+ *ngIf="!isCurrentUser() && !authStorageService.isSSO()">
+ <cds-checkbox id="pwdUpdateRequired"
+ formControlName="pwdUpdateRequired"
+ name="pwdUpdateRequired"
+ i18n>User must change password at next logon
+ </cds-checkbox>
</div>
- </div>
- </form>
+ <!--Submit Button-->
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="userForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right">
+ </cd-form-button-panel>
+ </form>
+ </ng-container>
</div>
<ng-template #removeSelfUserReadUpdatePermissionTpl>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
index 7c02b86eae0..009d4c193e4 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.ts
@@ -55,7 +55,8 @@ export class UserFormComponent extends CdForm implements OnInit {
icons = Icons;
pwdExpirationSettings: CdPwdExpirationSettings;
pwdExpirationFormat = 'YYYY-MM-DD';
-
+ selectedRole: string[];
+ passwordexp: boolean = false;
constructor(
private authService: AuthService,
private authStorageService: AuthStorageService,
@@ -91,6 +92,7 @@ export class UserFormComponent extends CdForm implements OnInit {
password: [
'',
[],
+
[
CdValidators.passwordPolicy(
this.userService,
@@ -105,7 +107,7 @@ export class UserFormComponent extends CdForm implements OnInit {
]
],
confirmpassword: [''],
- pwdExpirationDate: [undefined],
+ pwdExpirationDate: [''],
email: ['', [CdValidators.email]],
roles: [[]],
enabled: [true, [Validators.required]],
@@ -121,8 +123,10 @@ export class UserFormComponent extends CdForm implements OnInit {
if (this.router.url.startsWith('/user-management/users/edit')) {
this.mode = this.userFormMode.editing;
this.action = this.actionLabels.EDIT;
+ this.passwordexp = false;
} else {
this.action = this.actionLabels.CREATE;
+ this.passwordexp = true;
}
const observables = [this.roleService.list(), this.settingsService.getStandardSettings()];
@@ -130,6 +134,7 @@ export class UserFormComponent extends CdForm implements OnInit {
(result: [UserFormRoleModel[], CdPwdExpirationSettings]) => {
this.allRoles = _.map(result[0], (role) => {
role.enabled = true;
+ role.content = `${role.name}, ${role.description}`;
return role;
});
this.pwdExpirationSettings = new CdPwdExpirationSettings(result[1]);
@@ -158,7 +163,6 @@ export class UserFormComponent extends CdForm implements OnInit {
this.userService.get(username).subscribe((userFormModel: UserFormModel) => {
this.response = _.cloneDeep(userFormModel);
this.setResponse(userFormModel);
-
this.loadingReady();
});
});
@@ -173,20 +177,28 @@ export class UserFormComponent extends CdForm implements OnInit {
this.userForm.get(key).setValue(response[key])
);
const expirationDate = response['pwdExpirationDate'];
+
if (expirationDate) {
+ this.passwordexp = false;
this.userForm
.get('pwdExpirationDate')
.setValue(moment(expirationDate * 1000).format(this.pwdExpirationFormat));
+ } else {
+ this.passwordexp = true;
}
}
getRequest(): UserFormModel {
const userFormModel = new UserFormModel();
+
['username', 'password', 'name', 'email', 'roles', 'enabled', 'pwdUpdateRequired'].forEach(
- (key) => (userFormModel[key] = this.userForm.get(key).value)
+ (key) => {
+ userFormModel[key] = this.userForm.get(key).value;
+ }
);
const expirationDate = this.userForm.get('pwdExpirationDate').value;
if (expirationDate) {
+ this.passwordexp = false;
const mom = moment(expirationDate, this.pwdExpirationFormat);
if (
this.mode !== this.userFormMode.editing ||
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
index 317293be07c..fedc7b8de0f 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
@@ -163,11 +163,9 @@ export class PrometheusService {
checkNan
) {
queriesResults[queryName].forEach((valueArray: any[]) => {
- valueArray.forEach((val, index) => {
- if (isNaN(parseFloat(val[1]))) {
- valueArray[index][1] = '0';
- }
- });
+ if (isNaN(parseFloat(valueArray[1]))) {
+ valueArray[1] = '0';
+ }
});
}
});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html
index 328e72cc595..ccdb70e39e4 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.html
@@ -1,22 +1,23 @@
-<div cdsCol
- class="form-item">
- <div cdsRow>
-<cds-date-picker [label]="name"
- i18n-label
- placeholder="NOT PROTECTED"
- formControlname="expiresAt"
- dateFormat="Y/m/d"
- [value]="date"
- (valueChange)="onModelChange($event)"
- [helperText]="helperText"
- [disabled]="disabled"
- cdsTheme="theme"></cds-date-picker>
-<cds-timepicker (valueChange)="onModelChange($event)"
- [(ngModel)]="time"
- label="Select a time"
- [disabled]="disabled"
- pattern="(1[012]|[0-9]):[0-5][0-9]"
- *ngIf="hasTime">
+<div cdsCol>
+ <div cdsRow
+ class="form-item-append">
+ <cds-text-label>{{name}}
+ <cds-date-picker i18n-label
+ [placeholder]="placeHolder"
+ formControlname="expiresAt"
+ dateFormat="Y/m/d"
+ [value]="date"
+ (valueChange)="onModelChange($event)"
+ [helperText]="helperText"
+ [disabled]="disabled"
+ cdsTheme="theme"></cds-date-picker>
+ </cds-text-label>
+ <cds-text-label *ngIf="hasTime">Select a time
+ <cds-timepicker (valueChange)="onModelChange($event)"
+ [(ngModel)]="time"
+ [disabled]="disabled"
+ pattern="(1[012]|[0-9]):[0-5][0-9]"
+ *ngIf="hasTime">
<cds-timepicker-select [(ngModel)]="ampm"
[disabled]="disabled"
(valueChange)="onModelChange($event)">
@@ -24,5 +25,7 @@
value="AM">AM</option>
<option value="PM">PM</option>
</cds-timepicker-select>
-</cds-timepicker></div>
+</cds-timepicker>
+</cds-text-label>
+</div>
</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss
index e69de29bb2d..39f2a7115a1 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.scss
@@ -0,0 +1,3 @@
+.form-item-append {
+ margin-top: 1rem;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts
index 4841d2ed92d..3458d9171a7 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/date-time-picker/date-time-picker.component.ts
@@ -25,7 +25,8 @@ export class DateTimePickerComponent implements OnInit {
@Input()
helperText = '';
-
+ @Input()
+ placeHolder = '';
@Input()
disabled = false;
@@ -39,9 +40,8 @@ export class DateTimePickerComponent implements OnInit {
date: { [key: number]: string }[] = [];
time: string;
ampm: string;
-
sub: Subscription;
-
+ @Input() defaultDate: boolean = false;
constructor(private calendar: NgbCalendar) {}
ngOnInit() {
@@ -59,8 +59,12 @@ export class DateTimePickerComponent implements OnInit {
if (!mom.isValid() || mom.isBefore(moment())) {
mom = moment();
}
+ if (this.defaultDate) {
+ this.date.push([]);
+ } else {
+ this.date.push(mom.format('YYYY-MM-DD'));
+ }
- this.date.push(mom.format('YYYY-MM-DD'));
const time = mom.format('HH:mm:ss');
this.time = mom.format('hh:mm');
this.ampm = mom.hour() >= 12 ? 'PM' : 'AM';
@@ -76,7 +80,9 @@ export class DateTimePickerComponent implements OnInit {
onModelChange(event?: any) {
if (event) {
- if (Array.isArray(event)) {
+ if (event.length === 0) {
+ this.datetime.date = { date: null, time: null, ampm: null };
+ } else if (Array.isArray(event)) {
this.datetime.date = moment(event[0]).format('YYYY-MM-DD');
} else if (event && ['AM', 'PM'].includes(event)) {
const initialMoment = moment(this.datetime.time, 'hh:mm:ss A');
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html
index da1a4800f7f..81ad90914b6 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/helper/helper.component.html
@@ -5,7 +5,8 @@
<ng-content></ng-content>
</ng-template>
-<cds-tooltip [description]="popoverTpl">
+<cds-tooltip [description]="popoverTpl"
+ [autoAlign]="true">
<svg cdsIcon="information"
size="16"
title="info"></svg>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts
index 361a404a11b..f1bbebed51d 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts
@@ -11,8 +11,8 @@ export enum Promqls {
export enum RgwPromqls {
RGW_REQUEST_PER_SECOND = 'sum(rate(ceph_rgw_req[1m]))',
- AVG_GET_LATENCY = 'sum(rate(ceph_rgw_op_get_obj_lat_sum[1m])) / sum(rate(ceph_rgw_op_get_obj_lat_count[1m]))',
- AVG_PUT_LATENCY = 'sum(rate(ceph_rgw_op_put_obj_lat_sum[1m])) / sum(rate(ceph_rgw_op_put_obj_lat_count[1m]))',
+ AVG_GET_LATENCY = '(sum(rate(ceph_rgw_op_get_obj_lat_sum[1m])) / sum(rate(ceph_rgw_op_get_obj_lat_count[1m]))) * 1000',
+ AVG_PUT_LATENCY = '(sum(rate(ceph_rgw_op_put_obj_lat_sum[1m])) / sum(rate(ceph_rgw_op_put_obj_lat_count[1m]))) * 1000',
GET_BANDWIDTH = 'sum(rate(ceph_rgw_op_get_obj_bytes[1m]))',
PUT_BANDWIDTH = 'sum(rate(ceph_rgw_op_put_obj_bytes[1m]))'
}
diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/themes/_content.scss b/src/pybind/mgr/dashboard/frontend/src/styles/themes/_content.scss
index 37f89aba17f..0725b63dbfd 100644
--- a/src/pybind/mgr/dashboard/frontend/src/styles/themes/_content.scss
+++ b/src/pybind/mgr/dashboard/frontend/src/styles/themes/_content.scss
@@ -33,6 +33,7 @@ $content-theme: map-merge(
text-primary: vv.$dark,
text-secondary: vv.$dark,
text-disabled: vv.$gray-500,
+ icon-secondary: vv.$gray-800,
field-01: colors.$gray-10,
interactive: vv.$primary
)
diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml
index c9cd5430ec0..de1b3e8b60e 100644
--- a/src/pybind/mgr/dashboard/openapi.yaml
+++ b/src/pybind/mgr/dashboard/openapi.yaml
@@ -10808,6 +10808,274 @@ paths:
- jwt: []
tags:
- Prometheus
+ /api/rgw/accounts:
+ get:
+ parameters:
+ - default: false
+ in: query
+ name: detailed
+ schema:
+ type: boolean
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUserAccounts
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ account_id:
+ type: integer
+ account_name:
+ type: integer
+ email:
+ type: string
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - RgwUserAccounts
+ /api/rgw/accounts/{account_id}:
+ delete:
+ parameters:
+ - description: Account id
+ in: path
+ name: account_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Delete RGW Account
+ tags:
+ - RgwUserAccounts
+ get:
+ parameters:
+ - description: Account id
+ in: path
+ name: account_id
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get RGW Account by id
+ tags:
+ - RgwUserAccounts
+ put:
+ parameters:
+ - description: Account id
+ in: path
+ name: account_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ account_name:
+ type: integer
+ email:
+ type: string
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Update RGW account info
+ tags:
+ - RgwUserAccounts
+ /api/rgw/accounts/{account_id}/quota:
+ put:
+ parameters:
+ - description: Account id
+ in: path
+ name: account_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ max_objects:
+ type: string
+ max_size:
+ description: Max size
+ type: string
+ quota_type:
+ type: string
+ required:
+ - quota_type
+ - max_size
+ - max_objects
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Set RGW Account/Bucket quota
+ tags:
+ - RgwUserAccounts
+ /api/rgw/accounts/{account_id}/quota/status:
+ put:
+ parameters:
+ - description: Account id
+ in: path
+ name: account_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ quota_status:
+ type: string
+ quota_type:
+ type: string
+ required:
+ - quota_type
+ - quota_status
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Enable/Disable RGW Account/Bucket quota
+ tags:
+ - RgwUserAccounts
/api/rgw/bucket:
get:
parameters:
@@ -15984,6 +16252,8 @@ tags:
name: RgwSite
- description: RGW User Management API
name: RgwUser
+- description: RGW User Accounts API
+ name: RgwUserAccounts
- description: '*No description available*'
name: RgwZone
- description: '*No description available*'
diff --git a/src/pybind/mgr/dashboard/services/rgw_iam.py b/src/pybind/mgr/dashboard/services/rgw_iam.py
index dbf00df25e0..5f490323441 100644
--- a/src/pybind/mgr/dashboard/services/rgw_iam.py
+++ b/src/pybind/mgr/dashboard/services/rgw_iam.py
@@ -1,12 +1,13 @@
from subprocess import SubprocessError
-from typing import List
+from typing import List, Optional
from .. import mgr
from ..exceptions import DashboardException
class RgwAccounts:
- def send_rgw_cmd(self, command: List[str]):
+ @classmethod
+ def send_rgw_cmd(cls, command: List[str]):
try:
exit_code, out, err = mgr.send_rgwadmin_command(command)
@@ -19,6 +20,78 @@ class RgwAccounts:
except SubprocessError as e:
raise DashboardException(e, component='rgw')
- def get_accounts(self):
+ @classmethod
+ def get_accounts(cls, detailed: bool = False):
+ """
+ Query account Id's, optionally returning full details.
+
+ :param detailed: Boolean to indicate if full account details are required.
+ """
get_accounts_cmd = ['account', 'list']
- return self.send_rgw_cmd(get_accounts_cmd)
+ account_list = cls.send_rgw_cmd(get_accounts_cmd)
+ detailed_account_list = []
+ if detailed:
+ for account in account_list:
+ detailed_account_list.append(cls.get_account(account))
+ return detailed_account_list
+ return account_list
+
+ @classmethod
+ def get_account(cls, account_id: str):
+ get_account_cmd = ['account', 'get', '--account-id', account_id]
+ return cls.send_rgw_cmd(get_account_cmd)
+
+ @classmethod
+ def create_account(cls, account_name: Optional[str] = None,
+ account_id: Optional[str] = None, email: Optional[str] = None):
+ create_accounts_cmd = ['account', 'create']
+
+ if account_name:
+ create_accounts_cmd += ['--account-name', account_name]
+
+ if account_id:
+ create_accounts_cmd += ['--account_id', account_id]
+
+ if email:
+ create_accounts_cmd += ['--email', email]
+
+ return cls.send_rgw_cmd(create_accounts_cmd)
+
+ @classmethod
+ def modify_account(cls, account_id: str, account_name: Optional[str] = None,
+ email: Optional[str] = None):
+ modify_accounts_cmd = ['account', 'modify', '--account-id', account_id]
+
+ if account_name:
+ modify_accounts_cmd += ['--account-name', account_name]
+
+ if email:
+ modify_accounts_cmd += ['--email', email]
+
+ return cls.send_rgw_cmd(modify_accounts_cmd)
+
+ @classmethod
+ def delete_account(cls, account_id: str):
+ modify_accounts_cmd = ['account', 'rm', '--account-id', account_id]
+
+ return cls.send_rgw_cmd(modify_accounts_cmd)
+
+ @classmethod
+ def get_account_stats(cls, account_id: str):
+ account_stats_cmd = ['account', 'stats', '--account-id', account_id]
+
+ return cls.send_rgw_cmd(account_stats_cmd)
+
+ @classmethod
+ def set_quota(cls, quota_type: str, account_id: str, max_size: str, max_objects: str):
+ set_quota_cmd = ['quota', 'set', '--quota-scope', quota_type, '--account-id', account_id,
+ '--max-size', max_size, '--max-objects', max_objects]
+
+ return cls.send_rgw_cmd(set_quota_cmd)
+
+ @classmethod
+ def set_quota_status(cls, quota_type: str, account_id: str, quota_status: str):
+ set_quota_status_cmd = ['quota', quota_status, '--quota-scope', quota_type,
+ '--account-id', account_id]
+
+ return cls.send_rgw_cmd(set_quota_status_cmd)
diff --git a/src/pybind/mgr/dashboard/tests/test_rgw_iam.py b/src/pybind/mgr/dashboard/tests/test_rgw_iam.py
new file mode 100644
index 00000000000..133b5a0d390
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_rgw_iam.py
@@ -0,0 +1,292 @@
+from unittest import TestCase
+from unittest.mock import patch
+
+from ..controllers.rgw_iam import RgwUserAccountsController
+from ..services.rgw_iam import RgwAccounts
+
+
+class TestRgwUserAccountsController(TestCase):
+
+ @patch.object(RgwAccounts, 'create_account')
+ def test_create_account(self, mock_create_account):
+ mockReturnVal = {
+ "id": "RGW18661471562806836",
+ "tenant": "",
+ "name": "",
+ "email": "",
+ "quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "max_users": 1000,
+ "max_roles": 1000,
+ "max_groups": 1000,
+ "max_buckets": 1000,
+ "max_access_keys": 4
+ }
+
+ # Mock the return value of the create_account method
+ mock_create_account.return_value = mockReturnVal
+
+ controller = RgwUserAccountsController()
+ result = controller.create(account_name='test_account', account_id='RGW18661471562806836',
+ email='test@example.com')
+
+ # Check if the account creation method was called with the correct parameters
+ mock_create_account.assert_called_with('test_account', 'RGW18661471562806836',
+ 'test@example.com')
+ # Check the returned result
+ self.assertEqual(result, mockReturnVal)
+
+ @patch.object(RgwAccounts, 'get_accounts')
+ def test_list_accounts(self, mock_get_accounts):
+ mock_return_value = [
+ "RGW22222222222222222",
+ "RGW59378973811515857",
+ "RGW11111111111111111"
+ ]
+
+ mock_get_accounts.return_value = mock_return_value
+
+ controller = RgwUserAccountsController()
+ result = controller.list(detailed=False)
+
+ mock_get_accounts.assert_called_with(False)
+
+ self.assertEqual(result, mock_return_value)
+
+ @patch.object(RgwAccounts, 'get_accounts')
+ def test_list_accounts_with_details(self, mock_get_accounts):
+ mock_return_value = [
+ {
+ "id": "RGW22222222222222222",
+ "tenant": "",
+ "name": "Account2",
+ "email": "account2@ceph.com",
+ "quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "max_users": 1000,
+ "max_roles": 1000,
+ "max_groups": 1000,
+ "max_buckets": 1000,
+ "max_access_keys": 4
+ },
+ {
+ "id": "RGW11111111111111111",
+ "tenant": "",
+ "name": "Account1",
+ "email": "account1@ceph.com",
+ "quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "max_users": 1000,
+ "max_roles": 1000,
+ "max_groups": 1000,
+ "max_buckets": 1000,
+ "max_access_keys": 4
+ }
+ ]
+
+ mock_get_accounts.return_value = mock_return_value
+
+ controller = RgwUserAccountsController()
+ result = controller.list(detailed=True)
+
+ mock_get_accounts.assert_called_with(True)
+
+ self.assertEqual(result, mock_return_value)
+
+ @patch.object(RgwAccounts, 'get_account')
+ def test_get_account(self, mock_get_account):
+ mock_return_value = {
+ "id": "RGW22222222222222222",
+ "tenant": "",
+ "name": "Account2",
+ "email": "account2@ceph.com",
+ "quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "max_users": 1000,
+ "max_roles": 1000,
+ "max_groups": 1000,
+ "max_buckets": 1000,
+ "max_access_keys": 4
+ }
+ mock_get_account.return_value = mock_return_value
+
+ controller = RgwUserAccountsController()
+ result = controller.get(account_id='RGW22222222222222222')
+
+ mock_get_account.assert_called_with('RGW22222222222222222')
+
+ self.assertEqual(result, mock_return_value)
+
+ @patch.object(RgwAccounts, 'delete_account')
+ def test_delete_account(self, mock_delete_account):
+ mock_delete_account.return_value = None
+
+ controller = RgwUserAccountsController()
+ result = controller.delete(account_id='RGW59378973811515857')
+
+ mock_delete_account.assert_called_with('RGW59378973811515857')
+
+ self.assertEqual(result, None)
+
+ @patch.object(RgwAccounts, 'modify_account')
+ def test_set_account_name(self, mock_modify_account):
+ mock_return_value = mock_return_value = {
+ "id": "RGW59378973811515857",
+ "tenant": "",
+ "name": "new_account_name",
+ "email": "new_email@example.com",
+ "quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": -1
+ },
+ "max_users": 1000,
+ "max_roles": 1000,
+ "max_groups": 1000,
+ "max_buckets": 1000,
+ "max_access_keys": 4
+ }
+ mock_modify_account.return_value = mock_return_value
+
+ controller = RgwUserAccountsController()
+ result = controller.set(account_id='RGW59378973811515857', account_name='new_account_name',
+ email='new_email@example.com')
+
+ mock_modify_account.assert_called_with('RGW59378973811515857', 'new_account_name',
+ 'new_email@example.com')
+
+ self.assertEqual(result, mock_return_value)
+
+ @patch.object(RgwAccounts, 'set_quota')
+ def test_set_quota(self, mock_set_quota):
+ mock_return_value = {
+ "id": "RGW11111111111111111",
+ "tenant": "",
+ "name": "Account1",
+ "email": "account1@ceph.com",
+ "quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": 10737418240,
+ "max_size_kb": 10485760,
+ "max_objects": 1000000
+ },
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": 1000000
+ },
+ "max_users": 1000,
+ "max_roles": 1000,
+ "max_groups": 1000,
+ "max_buckets": 1000,
+ "max_access_keys": 4
+ }
+
+ mock_set_quota.return_value = mock_return_value
+
+ controller = RgwUserAccountsController()
+ result = controller.set_quota(quota_type='account', account_id='RGW11111111111111111',
+ max_size='10GB', max_objects='1000')
+
+ mock_set_quota.assert_called_with('account', 'RGW11111111111111111', '10GB', '1000')
+
+ self.assertEqual(result, mock_return_value)
+
+ @patch.object(RgwAccounts, 'set_quota_status')
+ def test_set_quota_status(self, mock_set_quota_status):
+ mock_return_value = {
+ "id": "RGW11111111111111111",
+ "tenant": "",
+ "name": "Account1",
+ "email": "account1@ceph.com",
+ "quota": {
+ "enabled": True,
+ "check_on_raw": False,
+ "max_size": 10737418240,
+ "max_size_kb": 10485760,
+ "max_objects": 1000000
+ },
+ "bucket_quota": {
+ "enabled": False,
+ "check_on_raw": False,
+ "max_size": -1,
+ "max_size_kb": 0,
+ "max_objects": 1000000
+ },
+ "max_users": 1000,
+ "max_roles": 1000,
+ "max_groups": 1000,
+ "max_buckets": 1000,
+ "max_access_keys": 4
+ }
+
+ mock_set_quota_status.return_value = mock_return_value
+
+ controller = RgwUserAccountsController()
+ result = controller.set_quota_status(quota_type='account',
+ account_id='RGW11111111111111111',
+ quota_status='enabled')
+
+ mock_set_quota_status.assert_called_with('account', 'RGW11111111111111111', 'enabled')
+
+ self.assertEqual(result, mock_return_value)
diff --git a/src/rgw/CMakeLists.txt b/src/rgw/CMakeLists.txt
index 96f3237b896..3727c525ce7 100644
--- a/src/rgw/CMakeLists.txt
+++ b/src/rgw/CMakeLists.txt
@@ -90,6 +90,7 @@ set(librgw_common_srcs
rgw_notify_event_type.cc
rgw_period_history.cc
rgw_period_puller.cc
+ rgw_s3_filter.cc
rgw_pubsub.cc
rgw_coroutine.cc
rgw_cr_rest.cc
diff --git a/src/rgw/driver/posix/notify.h b/src/rgw/driver/posix/notify.h
index 9f6088a893a..4463abc57c2 100644
--- a/src/rgw/driver/posix/notify.h
+++ b/src/rgw/driver/posix/notify.h
@@ -212,7 +212,7 @@ namespace file::listing {
void signal_shutdown() {
uint64_t msg{sig_shutdown};
- (void) write(efd, &msg, sizeof(uint64_t));
+ std::ignore = write(efd, &msg, sizeof(uint64_t));
}
friend class Notify;
diff --git a/src/rgw/driver/rados/rgw_sal_rados.cc b/src/rgw/driver/rados/rgw_sal_rados.cc
index 1eb4eecceea..88da446c3de 100644
--- a/src/rgw/driver/rados/rgw_sal_rados.cc
+++ b/src/rgw/driver/rados/rgw_sal_rados.cc
@@ -723,7 +723,7 @@ int RadosBucket::merge_and_store_attrs(const DoutPrefixProvider* dpp, Attrs& new
attrs[it.first] = it.second;
}
return store->ctl()->bucket->set_bucket_instance_attrs(get_info(),
- new_attrs, &get_info().objv_tracker, y, dpp);
+ attrs, &get_info().objv_tracker, y, dpp);
}
int RadosBucket::try_refresh_info(const DoutPrefixProvider* dpp, ceph::real_time* pmtime, optional_yield y)
diff --git a/src/rgw/rgw_admin.cc b/src/rgw/rgw_admin.cc
index a73db542014..95a7af6a0fa 100644
--- a/src/rgw/rgw_admin.cc
+++ b/src/rgw/rgw_admin.cc
@@ -2553,35 +2553,104 @@ std::ostream& operator<<(std::ostream& out, const indented& h) {
return out << std::setw(h.w) << h.header << std::setw(1) << ' ';
}
-static int bucket_source_sync_status(const DoutPrefixProvider *dpp, rgw::sal::RadosStore* driver, const RGWZone& zone,
+struct bucket_source_sync_info {
+ const RGWZone& _source;
+ std::string_view error;
+ std::map<int,std::string> shards_behind;
+ int total_shards;
+ std::string_view status;
+ rgw_bucket bucket_source;
+
+ bucket_source_sync_info(const RGWZone& source): _source(source) {}
+
+ void _print_plaintext(std::ostream& out, int width) const {
+ out << indented{width, "source zone"} << _source.id << " (" << _source.name << ")" << std::endl;
+ if (!error.empty()) {
+ out << indented{width} << error << std::endl;
+ return;
+ }
+ out << indented{width, "source bucket"} << bucket_source << std::endl;
+ if (!status.empty()) {
+ out << indented{width} << status << std::endl;
+ return;
+ }
+ out << indented{width} << "incremental sync on " << total_shards << " shards\n";
+ if (!shards_behind.empty()) {
+ out << indented{width} << "bucket is behind on " << shards_behind.size() << " shards\n";
+ set<int> shard_ids;
+ for (auto const& [shard_id, _] : shards_behind) {
+ shard_ids.insert(shard_id);
+ }
+ out << indented{width} << "behind shards: [" << shard_ids << "]\n";
+ } else {
+ out << indented{width} << "bucket is caught up with source\n";
+ }
+ }
+
+ void _print_formatter(std::ostream& out, Formatter* formatter) const {
+ formatter->open_object_section("source");
+ formatter->dump_string("source_zone", _source.id);
+ formatter->dump_string("source_name", _source.name);
+
+ if (!error.empty()) {
+ formatter->dump_string("error", error);
+ formatter->close_section();
+ formatter->flush(out);
+ return;
+ }
+
+ formatter->dump_string("source_bucket", bucket_source.name);
+ formatter->dump_string("source_bucket_id", bucket_source.bucket_id);
+
+ if (!status.empty()) {
+ formatter->dump_string("status", status);
+ formatter->close_section();
+ formatter->flush(out);
+ return;
+ }
+
+ formatter->dump_int("total_shards", total_shards);
+ formatter->open_array_section("behind_shards");
+ for (auto const& [id, marker] : shards_behind) {
+ formatter->open_object_section("shard");
+ formatter->dump_int("shard_id", id);
+ formatter->dump_string("shard_marker", marker);
+ formatter->close_section();
+ }
+ formatter->close_section();
+ formatter->close_section();
+ formatter->flush(out);
+ }
+};
+
+static int bucket_source_sync_status(const DoutPrefixProvider *dpp, rgw::sal::RadosStore* driver,
+ const RGWZone& zone,
const RGWZone& source, RGWRESTConn *conn,
const RGWBucketInfo& bucket_info,
rgw_sync_bucket_pipe pipe,
- int width, std::ostream& out)
+ bucket_source_sync_info& source_sync_info)
{
- out << indented{width, "source zone"} << source.id << " (" << source.name << ")" << std::endl;
-
// syncing from this zone?
if (!driver->svc()->zone->zone_syncs_from(zone, source)) {
- out << indented{width} << "does not sync from zone\n";
+ source_sync_info.error = "does not sync from zone";
return 0;
}
if (!pipe.source.bucket) {
- ldpp_dout(dpp, -1) << __func__ << "(): missing source bucket" << dendl;
+ source_sync_info.error = fmt::format("{} (): missing source bucket", __func__);
return -EINVAL;
}
std::unique_ptr<rgw::sal::Bucket> source_bucket;
int r = init_bucket(*pipe.source.bucket, &source_bucket);
if (r < 0) {
- ldpp_dout(dpp, -1) << "failed to read source bucket info: " << cpp_strerror(r) << dendl;
+ source_sync_info.error = fmt::format("failed to read source bucket info: {}", cpp_strerror(r));
return r;
}
- out << indented{width, "source bucket"} << source_bucket->get_key() << std::endl;
- pipe.source.bucket = source_bucket->get_key();
+ source_sync_info.bucket_source = source_bucket->get_key();
+ pipe.source.bucket = source_bucket->get_key();
pipe.dest.bucket = bucket_info.bucket;
uint64_t gen = 0;
@@ -2592,15 +2661,15 @@ static int bucket_source_sync_status(const DoutPrefixProvider *dpp, rgw::sal::Ra
r = rgw_read_bucket_full_sync_status(dpp, driver, pipe, &full_status, null_yield);
if (r >= 0) {
if (full_status.state == BucketSyncState::Init) {
- out << indented{width} << "init: bucket sync has not started\n";
+ source_sync_info.status = "init: bucket sync has not started";
return 0;
}
if (full_status.state == BucketSyncState::Stopped) {
- out << indented{width} << "stopped: bucket sync is disabled\n";
+ source_sync_info.status = "stopped: bucket sync is disabled";
return 0;
}
if (full_status.state == BucketSyncState::Full) {
- out << indented{width} << "full sync: " << full_status.full.count << " objects completed\n";
+ source_sync_info.status = fmt::format("full sync: {} objects completed", full_status.full.count);
return 0;
}
gen = full_status.incremental_gen;
@@ -2609,46 +2678,45 @@ static int bucket_source_sync_status(const DoutPrefixProvider *dpp, rgw::sal::Ra
// no full status, but there may be per-shard status from before upgrade
const auto& logs = source_bucket->get_info().layout.logs;
if (logs.empty()) {
- out << indented{width} << "init: bucket sync has not started\n";
+ source_sync_info.status = "init: bucket sync has not started";
return 0;
}
const auto& log = logs.front();
if (log.gen > 0) {
// this isn't the backward-compatible case, so we just haven't started yet
- out << indented{width} << "init: bucket sync has not started\n";
+ source_sync_info.status = "init: bucket sync has not started";
return 0;
}
if (log.layout.type != rgw::BucketLogType::InIndex) {
- ldpp_dout(dpp, -1) << "unrecognized log layout type " << log.layout.type << dendl;
+ source_sync_info.error = fmt::format("unrecognized log layout type {}", to_string(log.layout.type));
return -EINVAL;
}
// use shard count from our log gen=0
shard_status.resize(rgw::num_shards(log.layout.in_index));
} else {
- lderr(driver->ctx()) << "failed to read bucket full sync status: " << cpp_strerror(r) << dendl;
+ source_sync_info.error = fmt::format("failed to read bucket full sync status: {}", cpp_strerror(r));
return r;
}
r = rgw_read_bucket_inc_sync_status(dpp, driver, pipe, gen, &shard_status);
if (r < 0) {
- lderr(driver->ctx()) << "failed to read bucket incremental sync status: " << cpp_strerror(r) << dendl;
+ source_sync_info.error = fmt::format("failed to read bucket incremental sync status: {}", cpp_strerror(r));
return r;
}
const int total_shards = shard_status.size();
-
- out << indented{width} << "incremental sync on " << total_shards << " shards\n";
+ source_sync_info.total_shards = total_shards;
rgw_bucket_index_marker_info remote_info;
BucketIndexShardsManager remote_markers;
r = rgw_read_remote_bilog_info(dpp, conn, source_bucket->get_key(),
remote_info, remote_markers, null_yield);
if (r < 0) {
- ldpp_dout(dpp, -1) << "failed to read remote log: " << cpp_strerror(r) << dendl;
+ source_sync_info.error = fmt::format("failed to read remote log: {}", cpp_strerror(r));
return r;
}
- std::set<int> shards_behind;
+ std::map<int, std::string> shards_behind;
for (const auto& r : remote_markers.get()) {
auto shard_id = r.first;
if (r.second.empty()) {
@@ -2656,21 +2724,17 @@ static int bucket_source_sync_status(const DoutPrefixProvider *dpp, rgw::sal::Ra
}
if (shard_id >= total_shards) {
// unexpected shard id. we don't have status for it, so we're behind
- shards_behind.insert(shard_id);
+ shards_behind[shard_id] = r.second;
continue;
}
auto& m = shard_status[shard_id];
const auto pos = BucketIndexShardsManager::get_shard_marker(m.inc_marker.position);
if (pos < r.second) {
- shards_behind.insert(shard_id);
+ shards_behind[shard_id] = r.second;
}
}
- if (!shards_behind.empty()) {
- out << indented{width} << "bucket is behind on " << shards_behind.size() << " shards\n";
- out << indented{width} << "behind shards: [" << shards_behind << "]\n";
- } else {
- out << indented{width} << "bucket is caught up with source\n";
- }
+
+ source_sync_info.shards_behind = std::move(shards_behind);
return 0;
}
@@ -2881,25 +2945,82 @@ static int bucket_sync_info(rgw::sal::Driver* driver, const RGWBucketInfo& info,
return 0;
}
+struct bucket_sync_status_info {
+ std::vector<bucket_source_sync_info> source_status_info;
+ rgw::sal::Zone* _zone;
+ const rgw::sal::ZoneGroup* _zonegroup;
+ const RGWBucketInfo& _bucket_info;
+ const int width = 15;
+ std::string error;
+
+ bucket_sync_status_info(const RGWBucketInfo& bucket_info): _bucket_info(bucket_info) {}
+
+ void print(std::ostream& out, bool use_formatter, Formatter* formatter) {
+ if (use_formatter) {
+ _print_formatter(out, formatter);
+ } else {
+ _print_plaintext(out);
+ }
+ }
+
+ void _print_plaintext(std::ostream& out) {
+ out << indented{width, "realm"} << _zone->get_realm_id() << " (" << _zone->get_realm_name() << ")" << std::endl;
+ out << indented{width, "zonegroup"} << _zonegroup->get_id() << " (" << _zonegroup->get_name() << ")" << std::endl;
+ out << indented{width, "zone"} << _zone->get_id() << " (" << _zone->get_name() << ")" << std::endl;
+ out << indented{width, "bucket"} << _bucket_info.bucket << std::endl;
+ out << indented{width, "current time"}
+ << to_iso_8601(ceph::real_clock::now(), iso_8601_format::YMDhms) << "\n\n";
+
+ if (!error.empty()){
+ out << error << std::endl;
+ }
+
+ for (const auto &info : source_status_info) {
+ info._print_plaintext(out, width);
+ }
+ }
+
+ void _print_formatter(std::ostream& out, Formatter* formatter) {
+ formatter->open_object_section("test");
+ formatter->dump_string("realm", _zone->get_realm_id());
+ formatter->dump_string("realm_name", _zone->get_realm_name());
+ formatter->dump_string("zonegroup", _zonegroup->get_id());
+ formatter->dump_string("zonegroup_name", _zonegroup->get_name());
+ formatter->dump_string("zone", _zone->get_id());
+ formatter->dump_string("zone_name", _zone->get_name());
+ formatter->dump_string("bucket", _bucket_info.bucket.name);
+ formatter->dump_string("bucket_instance_id", _bucket_info.bucket.bucket_id);
+ formatter->dump_string("current_time", to_iso_8601(ceph::real_clock::now(), iso_8601_format::YMDhms));
+
+ if (!error.empty()) {
+ formatter->dump_string("error", error);
+ }
+
+ formatter->open_array_section("sources");
+ for (const auto &info : source_status_info) {
+ info._print_formatter(out, formatter);
+ }
+ formatter->close_section();
+
+ formatter->close_section();
+ formatter->flush(out);
+ }
+
+};
+
static int bucket_sync_status(rgw::sal::Driver* driver, const RGWBucketInfo& info,
const rgw_zone_id& source_zone_id,
std::optional<rgw_bucket>& opt_source_bucket,
- std::ostream& out)
+ bucket_sync_status_info& bucket_sync_info)
{
const rgw::sal::ZoneGroup& zonegroup = driver->get_zone()->get_zonegroup();
rgw::sal::Zone* zone = driver->get_zone();
- constexpr int width = 15;
-
- out << indented{width, "realm"} << zone->get_realm_id() << " (" << zone->get_realm_name() << ")\n";
- out << indented{width, "zonegroup"} << zonegroup.get_id() << " (" << zonegroup.get_name() << ")\n";
- out << indented{width, "zone"} << zone->get_id() << " (" << zone->get_name() << ")\n";
- out << indented{width, "bucket"} << info.bucket << "\n";
- out << indented{width, "current time"}
- << to_iso_8601(ceph::real_clock::now(), iso_8601_format::YMDhms) << "\n\n";
+ bucket_sync_info._zone = zone;
+ bucket_sync_info._zonegroup = &zonegroup;
if (!static_cast<rgw::sal::RadosStore*>(driver)->ctl()->bucket->bucket_imports_data(info.bucket, null_yield, dpp())) {
- out << "Sync is disabled for bucket " << info.bucket.name << " or bucket has no sync sources" << std::endl;
+ bucket_sync_info.error = fmt::format("Sync is disabled for bucket {} or bucket has no sync sources", info.bucket.name);
return 0;
}
@@ -2907,7 +3028,7 @@ static int bucket_sync_status(rgw::sal::Driver* driver, const RGWBucketInfo& inf
int r = driver->get_sync_policy_handler(dpp(), std::nullopt, info.bucket, &handler, null_yield);
if (r < 0) {
- ldpp_dout(dpp(), -1) << "ERROR: failed to get policy handler for bucket (" << info.bucket << "): r=" << r << ": " << cpp_strerror(-r) << dendl;
+ bucket_sync_info.error = fmt::format("ERROR: failed to get policy handler for bucket ({}): r={}: {}", info.bucket.name, r, cpp_strerror(-r));
return r;
}
@@ -2920,13 +3041,12 @@ static int bucket_sync_status(rgw::sal::Driver* driver, const RGWBucketInfo& inf
std::unique_ptr<rgw::sal::Zone> zone;
int ret = driver->get_zone()->get_zonegroup().get_zone_by_id(source_zone_id.id, &zone);
if (ret < 0) {
- ldpp_dout(dpp(), -1) << "Source zone not found in zonegroup "
- << zonegroup.get_name() << dendl;
+ bucket_sync_info.error = fmt::format("Source zone not found in zonegroup {}", zonegroup.get_name());
return -EINVAL;
}
auto c = zone_conn_map.find(source_zone_id);
if (c == zone_conn_map.end()) {
- ldpp_dout(dpp(), -1) << "No connection to zone " << zone->get_name() << dendl;
+ bucket_sync_info.error = fmt::format("No connection to zone {}", zone->get_name());
return -EINVAL;
}
zone_ids.insert(source_zone_id);
@@ -2957,10 +3077,15 @@ static int bucket_sync_status(rgw::sal::Driver* driver, const RGWBucketInfo& inf
continue;
}
if (pipe.source.zone.value_or(rgw_zone_id()) == z->second.id) {
- bucket_source_sync_status(dpp(), static_cast<rgw::sal::RadosStore*>(driver), static_cast<rgw::sal::RadosStore*>(driver)->svc()->zone->get_zone(), z->second,
+ bucket_source_sync_info source_sync_info(z->second);
+ auto ret = bucket_source_sync_status(dpp(), static_cast<rgw::sal::RadosStore*>(driver), static_cast<rgw::sal::RadosStore*>(driver)->svc()->zone->get_zone(), z->second,
c->second,
info, pipe,
- width, out);
+ source_sync_info);
+
+ if (ret == 0) {
+ bucket_sync_info.source_status_info.emplace_back(std::move(source_sync_info));
+ }
}
}
}
@@ -3488,6 +3613,7 @@ int main(int argc, const char **argv)
list<string> tags_rm;
int placement_inline_data = true;
bool placement_inline_data_specified = false;
+ bool format_arg_passed = false;
int64_t max_objects = -1;
int64_t max_size = -1;
@@ -3867,6 +3993,7 @@ int main(int argc, const char **argv)
new_bucket_name = val;
} else if (ceph_argparse_witharg(args, i, &val, "--format", (char*)NULL)) {
format = val;
+ format_arg_passed = true;
} else if (ceph_argparse_witharg(args, i, &val, "--categories", (char*)NULL)) {
string cat_str = val;
list<string> cat_list;
@@ -7599,7 +7726,7 @@ int main(int argc, const char **argv)
<< "' to target bucket '" << configuration.target_bucket << "'" << std::endl;
return -ret;
}
- cerr << "flushed pending logging object '" << obj_name
+ cout << "flushed pending logging object '" << obj_name
<< "' to target bucket '" << configuration.target_bucket << "'" << std::endl;
return 0;
}
@@ -9901,7 +10028,18 @@ next:
if (ret < 0) {
return -ret;
}
- bucket_sync_status(driver, bucket->get_info(), source_zone, opt_source_bucket, std::cout);
+
+ auto bucket_info = bucket->get_info();
+ bucket_sync_status_info bucket_sync_info(bucket_info);
+
+ ret = bucket_sync_status(driver, bucket_info, source_zone,
+ opt_source_bucket, bucket_sync_info);
+
+ if (ret == 0) {
+ bucket_sync_info.print(std::cout, format_arg_passed, formatter.get());
+ } else {
+ cerr << "failed to get bucket sync status. see logs for more info" << std::endl;
+ }
}
if (opt_cmd == OPT::BUCKET_SYNC_MARKERS) {
diff --git a/src/rgw/rgw_bucket_logging.cc b/src/rgw/rgw_bucket_logging.cc
index 87a242d9952..d24a53024f1 100644
--- a/src/rgw/rgw_bucket_logging.cc
+++ b/src/rgw/rgw_bucket_logging.cc
@@ -31,6 +31,9 @@ bool configuration::decode_xml(XMLObj* obj) {
logging_type = LoggingType::Standard;
} else if (type == "Journal") {
logging_type = LoggingType::Journal;
+ if (iter = o->find("Filter"); XMLObj* const filter_o = iter.get_next()) {
+ RGWXMLDecoder::decode_xml("S3Key", key_filter, filter_o);
+ }
} else {
// we don't allow for type "Any" in the configuration
throw RGWXMLDecoder::err("invalid bucket logging record type: '" + type + "'");
@@ -73,6 +76,11 @@ void configuration::dump_xml(Formatter *f) const {
break;
case LoggingType::Journal:
::encode_xml("LoggingType", "Journal", f);
+ if (key_filter.has_content()) {
+ f->open_object_section("Filter");
+ ::encode_xml("S3Key", key_filter, f);
+ f->close_section(); // Filter
+ }
break;
case LoggingType::Any:
::encode_xml("LoggingType", "", f);
@@ -118,6 +126,10 @@ void configuration::dump(Formatter *f) const {
break;
case LoggingType::Journal:
encode_json("loggingType", "Journal", f);
+ if (key_filter.has_content()) {
+ Formatter::ObjectSection s(*f, "Filter");
+ encode_json("S3Key", key_filter, f);
+ }
break;
case LoggingType::Any:
encode_json("loggingType", "", f);
@@ -526,6 +538,11 @@ int log_record(rgw::sal::Driver* driver,
if (type != LoggingType::Any && configuration.logging_type != type) {
return 0;
}
+ if (configuration.key_filter.has_content()) {
+ if (!match(configuration.key_filter, obj->get_name())) {
+ return 0;
+ }
+ }
ldpp_dout(dpp, 20) << "INFO: found matching logging configuration of bucket '" << s->bucket->get_name() <<
"' configuration: " << configuration.to_json_str() << dendl;
if (auto ret = log_record(driver, obj, s, op_name, etag, size, configuration, dpp, y, async_completion, log_source_bucket); ret < 0) {
diff --git a/src/rgw/rgw_bucket_logging.h b/src/rgw/rgw_bucket_logging.h
index d380cf0ef09..d4877bafb0f 100644
--- a/src/rgw/rgw_bucket_logging.h
+++ b/src/rgw/rgw_bucket_logging.h
@@ -10,6 +10,7 @@
#include "include/buffer.h"
#include "include/encoding.h"
#include "common/async/yield_context.h"
+#include "rgw_s3_filter.h"
class XMLObj;
namespace ceph { class Formatter; }
@@ -48,6 +49,14 @@ namespace rgw::bucketlogging {
<LoggingType>Standard|Journal</LoggingType> <!-- Ceph extension -->
<ObjectRollTime>integer</ObjectRollTime> <!-- Ceph extension -->
<RecordsBatchSize>integer</RecordsBatchSize> <!-- Ceph extension -->
+ <Filter>
+ <S3Key>
+ <FilterRule>
+ <Name>suffix/prefix/regex</Name>
+ <Value></Value>
+ </FilterRule>
+ </S3Key>
+ </Filter>
</LoggingEnabled>
</BucketLoggingStatus>
*/
@@ -78,6 +87,7 @@ struct configuration {
PartitionDateSource date_source = PartitionDateSource::DeliveryTime;
// EventTime: use only year, month, and day. The hour, minutes and seconds are set to 00 in the key
// DeliveryTime: the time the log object was created
+ rgw_s3_key_filter key_filter;
bool decode_xml(XMLObj *obj);
void dump_xml(Formatter *f) const;
void dump(Formatter *f) const; // json
@@ -92,6 +102,9 @@ struct configuration {
encode(static_cast<int>(logging_type), bl);
encode(records_batch_size, bl);
encode(static_cast<int>(date_source), bl);
+ if (logging_type == LoggingType::Journal) {
+ encode(key_filter, bl);
+ }
ENCODE_FINISH(bl);
}
@@ -108,6 +121,9 @@ struct configuration {
decode(records_batch_size, bl);
decode(type, bl);
date_source = static_cast<PartitionDateSource>(type);
+ if (logging_type == LoggingType::Journal) {
+ decode(key_filter, bl);
+ }
DECODE_FINISH(bl);
}
};
diff --git a/src/rgw/rgw_iam_policy.cc b/src/rgw/rgw_iam_policy.cc
index ce76ed4c3c3..2a5c9cd313e 100644
--- a/src/rgw/rgw_iam_policy.cc
+++ b/src/rgw/rgw_iam_policy.cc
@@ -113,6 +113,7 @@ static const actpair actpairs[] =
{ "s3:PutBucketCORS", s3PutBucketCORS },
{ "s3:PutBucketEncryption", s3PutBucketEncryption },
{ "s3:PutBucketLogging", s3PutBucketLogging },
+ { "s3:PostBucketLogging", s3PostBucketLogging },
{ "s3:PutBucketNotification", s3PutBucketNotification },
{ "s3:PutBucketOwnershipControls", s3PutBucketOwnershipControls },
{ "s3:PutBucketPolicy", s3PutBucketPolicy },
@@ -1406,6 +1407,9 @@ const char* action_bit_string(uint64_t action) {
case s3PutBucketLogging:
return "s3:PutBucketLogging";
+ case s3PostBucketLogging:
+ return "s3:PostBucketLogging";
+
case s3GetBucketTagging:
return "s3:GetBucketTagging";
diff --git a/src/rgw/rgw_iam_policy.h b/src/rgw/rgw_iam_policy.h
index 1494cbf0b81..0476926143f 100644
--- a/src/rgw/rgw_iam_policy.h
+++ b/src/rgw/rgw_iam_policy.h
@@ -81,6 +81,7 @@ enum {
s3PutBucketNotification,
s3GetBucketLogging,
s3PutBucketLogging,
+ s3PostBucketLogging,
s3GetBucketTagging,
s3PutBucketTagging,
s3GetBucketWebsite,
@@ -298,6 +299,7 @@ inline int op_to_perm(std::uint64_t op) {
case s3PutBucketCORS:
case s3PutBucketEncryption:
case s3PutBucketLogging:
+ case s3PostBucketLogging:
case s3PutBucketNotification:
case s3PutBucketPolicy:
case s3PutBucketRequestPayment:
diff --git a/src/rgw/rgw_op.cc b/src/rgw/rgw_op.cc
index 17140436eeb..ee42ab647a1 100644
--- a/src/rgw/rgw_op.cc
+++ b/src/rgw/rgw_op.cc
@@ -1350,9 +1350,9 @@ void RGWDeleteBucketTags::execute(optional_yield y)
}
op_ret = retry_raced_bucket_write(this, s->bucket.get(), [this, y] {
- rgw::sal::Attrs attrs = s->bucket->get_attrs();
+ rgw::sal::Attrs& attrs = s->bucket->get_attrs();
attrs.erase(RGW_ATTR_TAGS);
- op_ret = s->bucket->merge_and_store_attrs(this, attrs, y);
+ op_ret = s->bucket->put_info(this, false, real_time(), y);
if (op_ret < 0) {
ldpp_dout(this, 0) << "RGWDeleteBucketTags() failed to remove RGW_ATTR_TAGS on bucket="
<< s->bucket->get_name()
@@ -6386,9 +6386,9 @@ void RGWDeleteCORS::execute(optional_yield y)
return op_ret;
}
- rgw::sal::Attrs attrs(s->bucket_attrs);
+ rgw::sal::Attrs& attrs = s->bucket->get_attrs();
attrs.erase(RGW_ATTR_CORS);
- op_ret = s->bucket->merge_and_store_attrs(this, attrs, s->yield);
+ op_ret = s->bucket->put_info(this, false, real_time(), s->yield);
if (op_ret < 0) {
ldpp_dout(this, 0) << "RGWLC::RGWDeleteCORS() failed to set attrs on bucket=" << s->bucket->get_name()
<< " returned err=" << op_ret << dendl;
@@ -7042,17 +7042,30 @@ void RGWAbortMultipart::execute(optional_yield y)
return;
upload = s->bucket->get_multipart_upload(s->object->get_name(), upload_id);
+ meta_obj = upload->get_meta_obj();
+ meta_obj->set_in_extra_data(true);
+ meta_obj->get_obj_attrs(s->yield, this);
+
jspan_context trace_ctx(false, false);
if (tracing::rgw::tracer.is_enabled()) {
// read meta object attributes for trace info
- meta_obj = upload->get_meta_obj();
- meta_obj->set_in_extra_data(true);
- meta_obj->get_obj_attrs(s->yield, this);
extract_span_context(meta_obj->get_attrs(), trace_ctx);
}
multipart_trace = tracing::rgw::tracer.add_span(name(), trace_ctx);
+ int max_lock_secs_mp =
+ s->cct->_conf.get_val<int64_t>("rgw_mp_lock_max_time");
+ utime_t dur(max_lock_secs_mp, 0);
+ auto serializer = meta_obj->get_serializer(this, "RGWCompleteMultipart");
+ op_ret = serializer->try_lock(this, dur, y);
+ if (op_ret < 0) {
+ if (op_ret == -ENOENT) {
+ op_ret = -ERR_NO_SUCH_UPLOAD;
+ }
+ return;
+ }
op_ret = upload->abort(this, s->cct, y);
+ serializer->unlock();
}
int RGWListMultipart::verify_permission(optional_yield y)
@@ -7356,6 +7369,12 @@ void RGWDeleteMultiObj::execute(optional_yield y)
return;
}
+ if (multi_delete->objects.empty()) {
+ s->err.message = "Missing required element Object";
+ op_ret = -ERR_MALFORMED_XML;
+ return;
+ }
+
constexpr int DEFAULT_MAX_NUM = 1000;
int max_num = s->cct->_conf->rgw_delete_multi_obj_max_num;
if (max_num < 0) {
@@ -8511,9 +8530,9 @@ void RGWDeleteBucketPolicy::execute(optional_yield y)
}
op_ret = retry_raced_bucket_write(this, s->bucket.get(), [this] {
- rgw::sal::Attrs attrs(s->bucket_attrs);
+ rgw::sal::Attrs& attrs = s->bucket->get_attrs();
attrs.erase(RGW_ATTR_IAM_POLICY);
- op_ret = s->bucket->merge_and_store_attrs(this, attrs, s->yield);
+ op_ret = s->bucket->put_info(this, false, real_time(), s->yield);
return op_ret;
}, y);
}
@@ -9031,9 +9050,9 @@ void RGWDeleteBucketPublicAccessBlock::execute(optional_yield y)
}
op_ret = retry_raced_bucket_write(this, s->bucket.get(), [this] {
- rgw::sal::Attrs attrs(s->bucket_attrs);
+ rgw::sal::Attrs& attrs = s->bucket->get_attrs();
attrs.erase(RGW_ATTR_PUBLIC_ACCESS);
- op_ret = s->bucket->merge_and_store_attrs(this, attrs, s->yield);
+ op_ret = s->bucket->put_info(this, false, real_time(), s->yield);
return op_ret;
}, y);
}
@@ -9142,10 +9161,10 @@ void RGWDeleteBucketEncryption::execute(optional_yield y)
}
op_ret = retry_raced_bucket_write(this, s->bucket.get(), [this, y] {
- rgw::sal::Attrs attrs = s->bucket->get_attrs();
+ rgw::sal::Attrs& attrs = s->bucket->get_attrs();
attrs.erase(RGW_ATTR_BUCKET_ENCRYPTION_POLICY);
attrs.erase(RGW_ATTR_BUCKET_ENCRYPTION_KEY_ID);
- op_ret = s->bucket->merge_and_store_attrs(this, attrs, y);
+ op_ret = s->bucket->put_info(this, false, real_time(), y);
return op_ret;
}, y);
}
diff --git a/src/rgw/rgw_op_type.h b/src/rgw/rgw_op_type.h
index a36274add40..49faea6403d 100644
--- a/src/rgw/rgw_op_type.h
+++ b/src/rgw/rgw_op_type.h
@@ -117,6 +117,7 @@ enum RGWOpType {
RGW_OP_DETACH_GROUP_POLICY,
RGW_OP_LIST_ATTACHED_GROUP_POLICIES,
RGW_OP_PUT_BUCKET_LOGGING,
+ RGW_OP_POST_BUCKET_LOGGING,
/* rgw specific */
RGW_OP_ADMIN_SET_METADATA,
RGW_OP_GET_OBJ_LAYOUT,
diff --git a/src/rgw/rgw_pubsub.cc b/src/rgw/rgw_pubsub.cc
index cb68d72d7da..87a46bd61a6 100644
--- a/src/rgw/rgw_pubsub.cc
+++ b/src/rgw/rgw_pubsub.cc
@@ -62,214 +62,6 @@ void set_event_id(std::string& id, const std::string& hash, const utime_t& ts) {
}
}
-void rgw_s3_key_filter::dump(Formatter *f) const {
- if (!has_content()) {
- return;
- }
- f->open_array_section("FilterRules");
- if (!prefix_rule.empty()) {
- f->open_object_section("");
- ::encode_json("Name", "prefix", f);
- ::encode_json("Value", prefix_rule, f);
- f->close_section();
- }
- if (!suffix_rule.empty()) {
- f->open_object_section("");
- ::encode_json("Name", "suffix", f);
- ::encode_json("Value", suffix_rule, f);
- f->close_section();
- }
- if (!regex_rule.empty()) {
- f->open_object_section("");
- ::encode_json("Name", "regex", f);
- ::encode_json("Value", regex_rule, f);
- f->close_section();
- }
- f->close_section();
-}
-
-bool rgw_s3_key_filter::decode_xml(XMLObj* obj) {
- XMLObjIter iter = obj->find("FilterRule");
- XMLObj *o;
-
- const auto throw_if_missing = true;
- auto prefix_not_set = true;
- auto suffix_not_set = true;
- auto regex_not_set = true;
- std::string name;
-
- while ((o = iter.get_next())) {
- RGWXMLDecoder::decode_xml("Name", name, o, throw_if_missing);
- if (name == "prefix" && prefix_not_set) {
- prefix_not_set = false;
- RGWXMLDecoder::decode_xml("Value", prefix_rule, o, throw_if_missing);
- } else if (name == "suffix" && suffix_not_set) {
- suffix_not_set = false;
- RGWXMLDecoder::decode_xml("Value", suffix_rule, o, throw_if_missing);
- } else if (name == "regex" && regex_not_set) {
- regex_not_set = false;
- RGWXMLDecoder::decode_xml("Value", regex_rule, o, throw_if_missing);
- } else {
- throw RGWXMLDecoder::err("invalid/duplicate S3Key filter rule name: '" + name + "'");
- }
- }
- return true;
-}
-
-void rgw_s3_key_filter::dump_xml(Formatter *f) const {
- if (!prefix_rule.empty()) {
- f->open_object_section("FilterRule");
- ::encode_xml("Name", "prefix", f);
- ::encode_xml("Value", prefix_rule, f);
- f->close_section();
- }
- if (!suffix_rule.empty()) {
- f->open_object_section("FilterRule");
- ::encode_xml("Name", "suffix", f);
- ::encode_xml("Value", suffix_rule, f);
- f->close_section();
- }
- if (!regex_rule.empty()) {
- f->open_object_section("FilterRule");
- ::encode_xml("Name", "regex", f);
- ::encode_xml("Value", regex_rule, f);
- f->close_section();
- }
-}
-
-bool rgw_s3_key_filter::has_content() const {
- return !(prefix_rule.empty() && suffix_rule.empty() && regex_rule.empty());
-}
-
-void rgw_s3_key_value_filter::dump(Formatter *f) const {
- if (!has_content()) {
- return;
- }
- f->open_array_section("FilterRules");
- for (const auto& key_value : kv) {
- f->open_object_section("");
- ::encode_json("Name", key_value.first, f);
- ::encode_json("Value", key_value.second, f);
- f->close_section();
- }
- f->close_section();
-}
-
-bool rgw_s3_key_value_filter::decode_xml(XMLObj* obj) {
- kv.clear();
- XMLObjIter iter = obj->find("FilterRule");
- XMLObj *o;
-
- const auto throw_if_missing = true;
-
- std::string key;
- std::string value;
-
- while ((o = iter.get_next())) {
- RGWXMLDecoder::decode_xml("Name", key, o, throw_if_missing);
- RGWXMLDecoder::decode_xml("Value", value, o, throw_if_missing);
- kv.emplace(key, value);
- }
- return true;
-}
-
-void rgw_s3_key_value_filter::dump_xml(Formatter *f) const {
- for (const auto& key_value : kv) {
- f->open_object_section("FilterRule");
- ::encode_xml("Name", key_value.first, f);
- ::encode_xml("Value", key_value.second, f);
- f->close_section();
- }
-}
-
-bool rgw_s3_key_value_filter::has_content() const {
- return !kv.empty();
-}
-
-void rgw_s3_filter::dump(Formatter *f) const {
- encode_json("S3Key", key_filter, f);
- encode_json("S3Metadata", metadata_filter, f);
- encode_json("S3Tags", tag_filter, f);
-}
-
-bool rgw_s3_filter::decode_xml(XMLObj* obj) {
- RGWXMLDecoder::decode_xml("S3Key", key_filter, obj);
- RGWXMLDecoder::decode_xml("S3Metadata", metadata_filter, obj);
- RGWXMLDecoder::decode_xml("S3Tags", tag_filter, obj);
- return true;
-}
-
-void rgw_s3_filter::dump_xml(Formatter *f) const {
- if (key_filter.has_content()) {
- ::encode_xml("S3Key", key_filter, f);
- }
- if (metadata_filter.has_content()) {
- ::encode_xml("S3Metadata", metadata_filter, f);
- }
- if (tag_filter.has_content()) {
- ::encode_xml("S3Tags", tag_filter, f);
- }
-}
-
-bool rgw_s3_filter::has_content() const {
- return key_filter.has_content() ||
- metadata_filter.has_content() ||
- tag_filter.has_content();
-}
-
-bool match(const rgw_s3_key_filter& filter, const std::string& key) {
- const auto key_size = key.size();
- const auto prefix_size = filter.prefix_rule.size();
- if (prefix_size != 0) {
- // prefix rule exists
- if (prefix_size > key_size) {
- // if prefix is longer than key, we fail
- return false;
- }
- if (!std::equal(filter.prefix_rule.begin(), filter.prefix_rule.end(), key.begin())) {
- return false;
- }
- }
- const auto suffix_size = filter.suffix_rule.size();
- if (suffix_size != 0) {
- // suffix rule exists
- if (suffix_size > key_size) {
- // if suffix is longer than key, we fail
- return false;
- }
- if (!std::equal(filter.suffix_rule.begin(), filter.suffix_rule.end(), (key.end() - suffix_size))) {
- return false;
- }
- }
- if (!filter.regex_rule.empty()) {
- // TODO add regex chaching in the filter
- const std::regex base_regex(filter.regex_rule);
- if (!std::regex_match(key, base_regex)) {
- return false;
- }
- }
- return true;
-}
-
-bool match(const rgw_s3_key_value_filter& filter, const KeyValueMap& kv) {
- // all filter pairs must exist with the same value in the object's metadata/tags
- // object metadata/tags may include items not in the filter
- return std::includes(kv.begin(), kv.end(), filter.kv.begin(), filter.kv.end());
-}
-
-bool match(const rgw_s3_key_value_filter& filter, const KeyMultiValueMap& kv) {
- // all filter pairs must exist with the same value in the object's metadata/tags
- // object metadata/tags may include items not in the filter
- for (auto& filter : filter.kv) {
- auto result = kv.equal_range(filter.first);
- if (std::any_of(result.first, result.second, [&filter](const std::pair<std::string, std::string>& p) { return p.second == filter.second;}))
- continue;
- else
- return false;
- }
- return true;
-}
-
bool match(const rgw::notify::EventTypeList& events, rgw::notify::EventType event) {
// if event list exists, and none of the events in the list matches the event type, filter the message
if (!events.empty() && std::find(events.begin(), events.end(), event) == events.end()) {
diff --git a/src/rgw/rgw_pubsub.h b/src/rgw/rgw_pubsub.h
index 8a6b290cb85..176ada95204 100644
--- a/src/rgw/rgw_pubsub.h
+++ b/src/rgw/rgw_pubsub.h
@@ -9,94 +9,10 @@
#include "rgw_zone.h"
#include "rgw_notify_event_type.h"
#include <boost/container/flat_map.hpp>
+#include "rgw_s3_filter.h"
class XMLObj;
-struct rgw_s3_key_filter {
- std::string prefix_rule;
- std::string suffix_rule;
- std::string regex_rule;
-
- bool has_content() const;
-
- void dump(Formatter *f) const;
- bool decode_xml(XMLObj *obj);
- void dump_xml(Formatter *f) const;
-
- void encode(bufferlist& bl) const {
- ENCODE_START(1, 1, bl);
- encode(prefix_rule, bl);
- encode(suffix_rule, bl);
- encode(regex_rule, bl);
- ENCODE_FINISH(bl);
- }
-
- void decode(bufferlist::const_iterator& bl) {
- DECODE_START(1, bl);
- decode(prefix_rule, bl);
- decode(suffix_rule, bl);
- decode(regex_rule, bl);
- DECODE_FINISH(bl);
- }
-};
-WRITE_CLASS_ENCODER(rgw_s3_key_filter)
-
-using KeyValueMap = boost::container::flat_map<std::string, std::string>;
-using KeyMultiValueMap = std::multimap<std::string, std::string>;
-
-struct rgw_s3_key_value_filter {
- KeyValueMap kv;
-
- bool has_content() const;
-
- void dump(Formatter *f) const;
- bool decode_xml(XMLObj *obj);
- void dump_xml(Formatter *f) const;
-
- void encode(bufferlist& bl) const {
- ENCODE_START(1, 1, bl);
- encode(kv, bl);
- ENCODE_FINISH(bl);
- }
- void decode(bufferlist::const_iterator& bl) {
- DECODE_START(1, bl);
- decode(kv, bl);
- DECODE_FINISH(bl);
- }
-};
-WRITE_CLASS_ENCODER(rgw_s3_key_value_filter)
-
-struct rgw_s3_filter {
- rgw_s3_key_filter key_filter;
- rgw_s3_key_value_filter metadata_filter;
- rgw_s3_key_value_filter tag_filter;
-
- bool has_content() const;
-
- void dump(Formatter *f) const;
- bool decode_xml(XMLObj *obj);
- void dump_xml(Formatter *f) const;
-
- void encode(bufferlist& bl) const {
- ENCODE_START(2, 1, bl);
- encode(key_filter, bl);
- encode(metadata_filter, bl);
- encode(tag_filter, bl);
- ENCODE_FINISH(bl);
- }
-
- void decode(bufferlist::const_iterator& bl) {
- DECODE_START(2, bl);
- decode(key_filter, bl);
- decode(metadata_filter, bl);
- if (struct_v >= 2) {
- decode(tag_filter, bl);
- }
- DECODE_FINISH(bl);
- }
-};
-WRITE_CLASS_ENCODER(rgw_s3_filter)
-
using OptionalFilter = std::optional<rgw_s3_filter>;
struct rgw_pubsub_topic_filter;
diff --git a/src/rgw/rgw_rest.cc b/src/rgw/rgw_rest.cc
index bf3d4ad9967..ac5e65c0dd6 100644
--- a/src/rgw/rgw_rest.cc
+++ b/src/rgw/rgw_rest.cc
@@ -666,8 +666,10 @@ static void build_redirect_url(req_state *s, const string& redirect_base, string
dest_uri = dest_uri.substr(0, dest_uri.size() - 1);
}
dest_uri += s->info.request_uri;
- dest_uri += "?";
- dest_uri += s->info.request_params;
+ if (!s->info.request_params.empty()) {
+ dest_uri += "?";
+ dest_uri += s->info.request_params;
+ }
}
void abort_early(req_state *s, RGWOp* op, int err_no,
diff --git a/src/rgw/rgw_rest_bucket_logging.cc b/src/rgw/rgw_rest_bucket_logging.cc
index 8d17114d804..ed12ce855a9 100644
--- a/src/rgw/rgw_rest_bucket_logging.cc
+++ b/src/rgw/rgw_rest_bucket_logging.cc
@@ -209,6 +209,84 @@ class RGWPutBucketLoggingOp : public RGWDefaultResponseOp {
}
};
+// Post /<bucket name>/?logging
+// actual configuration is XML encoded in the body of the message
+class RGWPostBucketLoggingOp : public RGWDefaultResponseOp {
+ int verify_permission(optional_yield y) override {
+ auto [has_s3_existing_tag, has_s3_resource_tag] = rgw_check_policy_condition(this, s, false);
+ if (has_s3_resource_tag)
+ rgw_iam_add_buckettags(this, s);
+
+ if (!verify_bucket_permission(this, s, rgw::IAM::s3PostBucketLogging)) {
+ return -EACCES;
+ }
+
+ return 0;
+ }
+
+ const char* name() const override { return "post_bucket_logging"; }
+ RGWOpType get_type() override { return RGW_OP_POST_BUCKET_LOGGING; }
+ uint32_t op_mask() override { return RGW_OP_TYPE_WRITE; }
+
+ void execute(optional_yield y) override {
+ op_ret = verify_bucket_logging_params(this, s);
+ if (op_ret < 0) {
+ return;
+ }
+
+ std::unique_ptr<rgw::sal::Bucket> bucket;
+ op_ret = driver->load_bucket(this, rgw_bucket(s->bucket_tenant, s->bucket_name),
+ &bucket, y);
+ if (op_ret < 0) {
+ ldpp_dout(this, 1) << "ERROR: failed to get bucket '" << s->bucket_name << "', ret = " << op_ret << dendl;
+ return;
+ }
+ const auto& bucket_attrs = bucket->get_attrs();
+ auto iter = bucket_attrs.find(RGW_ATTR_BUCKET_LOGGING);
+ if (iter == bucket_attrs.end()) {
+ ldpp_dout(this, 1) << "WARNING: no logging configured on bucket" << dendl;
+ return;
+ }
+ rgw::bucketlogging::configuration configuration;
+ try {
+ configuration.enabled = true;
+ decode(configuration, iter->second);
+ } catch (buffer::error& err) {
+ ldpp_dout(this, 1) << "ERROR: failed to decode logging attribute '" << RGW_ATTR_BUCKET_LOGGING
+ << "'. error: " << err.what() << dendl;
+ op_ret = -EINVAL;
+ return;
+ }
+
+ std::unique_ptr<rgw::sal::Bucket> target_bucket;
+ op_ret = driver->load_bucket(this, rgw_bucket(s->bucket_tenant, configuration.target_bucket),
+ &target_bucket, y);
+ if (op_ret < 0) {
+ ldpp_dout(this, 1) << "ERROR: failed to get target bucket '" << configuration.target_bucket << "', ret = " << op_ret << dendl;
+ return;
+ }
+ std::string obj_name;
+ RGWObjVersionTracker objv_tracker;
+ op_ret = target_bucket->get_logging_object_name(obj_name, configuration.target_prefix, null_yield, this, &objv_tracker);
+ if (op_ret < 0) {
+ ldpp_dout(this, 1) << "ERROR: failed to get pending logging object name from target bucket '" << configuration.target_bucket << "'" << dendl;
+ return;
+ }
+ op_ret = rgw::bucketlogging::rollover_logging_object(configuration, target_bucket, obj_name, this, null_yield, true, &objv_tracker);
+ if (op_ret < 0) {
+ ldpp_dout(this, 1) << "ERROR: failed to flush pending logging object '" << obj_name
+ << "' to target bucket '" << configuration.target_bucket << "'" << dendl;
+ return;
+ }
+ ldpp_dout(this, 20) << "flushed pending logging object '" << obj_name
+ << "' to target bucket '" << configuration.target_bucket << "'" << dendl;
+ }
+};
+
+RGWOp* RGWHandler_REST_BucketLogging_S3::create_post_op() {
+ return new RGWPostBucketLoggingOp();
+}
+
RGWOp* RGWHandler_REST_BucketLogging_S3::create_put_op() {
return new RGWPutBucketLoggingOp();
}
diff --git a/src/rgw/rgw_rest_bucket_logging.h b/src/rgw/rgw_rest_bucket_logging.h
index ca2220084bb..0b31d88dad8 100644
--- a/src/rgw/rgw_rest_bucket_logging.h
+++ b/src/rgw/rgw_rest_bucket_logging.h
@@ -14,5 +14,6 @@ public:
virtual ~RGWHandler_REST_BucketLogging_S3() = default;
static RGWOp* create_get_op();
static RGWOp* create_put_op();
+ static RGWOp* create_post_op();
};
diff --git a/src/rgw/rgw_rest_s3.cc b/src/rgw/rgw_rest_s3.cc
index 4b337f317ce..30ebe8e8965 100644
--- a/src/rgw/rgw_rest_s3.cc
+++ b/src/rgw/rgw_rest_s3.cc
@@ -4954,6 +4954,10 @@ RGWOp *RGWHandler_REST_Bucket_S3::op_post()
return new RGWDeleteMultiObj_ObjStore_S3;
}
+ if (s->info.args.exists("logging")) {
+ return RGWHandler_REST_BucketLogging_S3::create_post_op();
+ }
+
if (s->info.args.exists("mdsearch")) {
if (!s->cct->_conf->rgw_enable_mdsearch) {
return NULL;
@@ -6111,7 +6115,8 @@ AWSGeneralAbstractor::get_auth_data_v4(const req_state* const s,
case RGW_OP_GET_BUCKET_PUBLIC_ACCESS_BLOCK:
case RGW_OP_DELETE_BUCKET_PUBLIC_ACCESS_BLOCK:
case RGW_OP_GET_OBJ://s3select its post-method(payload contain the query) , the request is get-object
- case RGW_OP_PUT_BUCKET_LOGGING:
+ case RGW_OP_PUT_BUCKET_LOGGING:
+ case RGW_OP_POST_BUCKET_LOGGING:
case RGW_OP_GET_BUCKET_LOGGING:
break;
default:
diff --git a/src/rgw/rgw_s3_filter.cc b/src/rgw/rgw_s3_filter.cc
new file mode 100644
index 00000000000..05a7c4a7293
--- /dev/null
+++ b/src/rgw/rgw_s3_filter.cc
@@ -0,0 +1,269 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+#include "rgw_pubsub.h"
+#include "rgw_tools.h"
+#include "rgw_xml.h"
+#include "rgw_s3_filter.h"
+#include "common/errno.h"
+#include "rgw_sal.h"
+#include <regex>
+#include <algorithm>
+
+void rgw_s3_key_filter::dump(Formatter *f) const {
+ if (!has_content()) {
+ return;
+ }
+ f->open_array_section("FilterRules");
+ if (!prefix_rule.empty()) {
+ f->open_object_section("");
+ ::encode_json("Name", "prefix", f);
+ ::encode_json("Value", prefix_rule, f);
+ f->close_section();
+ }
+ if (!suffix_rule.empty()) {
+ f->open_object_section("");
+ ::encode_json("Name", "suffix", f);
+ ::encode_json("Value", suffix_rule, f);
+ f->close_section();
+ }
+ if (!regex_rule.empty()) {
+ f->open_object_section("");
+ ::encode_json("Name", "regex", f);
+ ::encode_json("Value", regex_rule, f);
+ f->close_section();
+ }
+ f->close_section();
+}
+
+bool rgw_s3_key_filter::decode_xml(XMLObj* obj) {
+ XMLObjIter iter = obj->find("FilterRule");
+ XMLObj *o;
+
+ const auto throw_if_missing = true;
+ auto prefix_not_set = true;
+ auto suffix_not_set = true;
+ auto regex_not_set = true;
+ std::string name;
+
+ while ((o = iter.get_next())) {
+ RGWXMLDecoder::decode_xml("Name", name, o, throw_if_missing);
+ if (name == "prefix" && prefix_not_set) {
+ prefix_not_set = false;
+ RGWXMLDecoder::decode_xml("Value", prefix_rule, o, throw_if_missing);
+ } else if (name == "suffix" && suffix_not_set) {
+ suffix_not_set = false;
+ RGWXMLDecoder::decode_xml("Value", suffix_rule, o, throw_if_missing);
+ } else if (name == "regex" && regex_not_set) {
+ regex_not_set = false;
+ RGWXMLDecoder::decode_xml("Value", regex_rule, o, throw_if_missing);
+ } else {
+ throw RGWXMLDecoder::err("invalid/duplicate S3Key filter rule name: '" + name + "'");
+ }
+ }
+ return true;
+}
+
+void rgw_s3_key_filter::dump_xml(Formatter *f) const {
+ if (!prefix_rule.empty()) {
+ f->open_object_section("FilterRule");
+ ::encode_xml("Name", "prefix", f);
+ ::encode_xml("Value", prefix_rule, f);
+ f->close_section();
+ }
+ if (!suffix_rule.empty()) {
+ f->open_object_section("FilterRule");
+ ::encode_xml("Name", "suffix", f);
+ ::encode_xml("Value", suffix_rule, f);
+ f->close_section();
+ }
+ if (!regex_rule.empty()) {
+ f->open_object_section("FilterRule");
+ ::encode_xml("Name", "regex", f);
+ ::encode_xml("Value", regex_rule, f);
+ f->close_section();
+ }
+}
+
+bool rgw_s3_key_filter::has_content() const {
+ return !(prefix_rule.empty() && suffix_rule.empty() && regex_rule.empty());
+}
+
+void rgw_s3_key_value_filter::dump(Formatter *f) const {
+ if (!has_content()) {
+ return;
+ }
+ f->open_array_section("FilterRules");
+ for (const auto& key_value : kv) {
+ f->open_object_section("");
+ ::encode_json("Name", key_value.first, f);
+ ::encode_json("Value", key_value.second, f);
+ f->close_section();
+ }
+ f->close_section();
+}
+
+bool rgw_s3_key_value_filter::decode_xml(XMLObj* obj) {
+ kv.clear();
+ XMLObjIter iter = obj->find("FilterRule");
+ XMLObj *o;
+
+ const auto throw_if_missing = true;
+
+ std::string key;
+ std::string value;
+
+ while ((o = iter.get_next())) {
+ RGWXMLDecoder::decode_xml("Name", key, o, throw_if_missing);
+ RGWXMLDecoder::decode_xml("Value", value, o, throw_if_missing);
+ kv.emplace(key, value);
+ }
+ return true;
+}
+
+void rgw_s3_key_value_filter::dump_xml(Formatter *f) const {
+ for (const auto& key_value : kv) {
+ f->open_object_section("FilterRule");
+ ::encode_xml("Name", key_value.first, f);
+ ::encode_xml("Value", key_value.second, f);
+ f->close_section();
+ }
+}
+
+bool rgw_s3_key_value_filter::has_content() const {
+ return !kv.empty();
+}
+
+void rgw_s3_filter::dump(Formatter *f) const {
+ encode_json("S3Key", key_filter, f);
+ encode_json("S3Metadata", metadata_filter, f);
+ encode_json("S3Tags", tag_filter, f);
+}
+
+bool rgw_s3_filter::decode_xml(XMLObj* obj) {
+ RGWXMLDecoder::decode_xml("S3Key", key_filter, obj);
+ RGWXMLDecoder::decode_xml("S3Metadata", metadata_filter, obj);
+ RGWXMLDecoder::decode_xml("S3Tags", tag_filter, obj);
+ return true;
+}
+
+void rgw_s3_filter::dump_xml(Formatter *f) const {
+ if (key_filter.has_content()) {
+ ::encode_xml("S3Key", key_filter, f);
+ }
+ if (metadata_filter.has_content()) {
+ ::encode_xml("S3Metadata", metadata_filter, f);
+ }
+ if (tag_filter.has_content()) {
+ ::encode_xml("S3Tags", tag_filter, f);
+ }
+}
+
+bool rgw_s3_filter::has_content() const {
+ return key_filter.has_content() ||
+ metadata_filter.has_content() ||
+ tag_filter.has_content();
+}
+
+bool match(const rgw_s3_key_filter& filter, const std::string& key) {
+ const auto key_size = key.size();
+ const auto prefix_size = filter.prefix_rule.size();
+ if (prefix_size != 0) {
+ // prefix rule exists
+ if (prefix_size > key_size) {
+ // if prefix is longer than key, we fail
+ return false;
+ }
+ if (!std::equal(filter.prefix_rule.begin(), filter.prefix_rule.end(), key.begin())) {
+ return false;
+ }
+ }
+ const auto suffix_size = filter.suffix_rule.size();
+ if (suffix_size != 0) {
+ // suffix rule exists
+ if (suffix_size > key_size) {
+ // if suffix is longer than key, we fail
+ return false;
+ }
+ if (!std::equal(filter.suffix_rule.begin(), filter.suffix_rule.end(), (key.end() - suffix_size))) {
+ return false;
+ }
+ }
+ if (!filter.regex_rule.empty()) {
+ // TODO add regex caching in the filter
+ const std::regex base_regex(filter.regex_rule);
+ if (!std::regex_match(key, base_regex)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool match(const rgw_s3_key_value_filter& filter, const KeyValueMap& kv) {
+ // all filter pairs must exist with the same value in the object's metadata/tags
+ // object metadata/tags may include items not in the filter
+ return std::includes(kv.begin(), kv.end(), filter.kv.begin(), filter.kv.end());
+}
+
+bool match(const rgw_s3_key_value_filter& filter, const KeyMultiValueMap& kv) {
+ // all filter pairs must exist with the same value in the object's metadata/tags
+ // object metadata/tags may include items not in the filter
+ for (auto& filter : filter.kv) {
+ auto result = kv.equal_range(filter.first);
+ if (std::any_of(result.first, result.second, [&filter](const std::pair<std::string, std::string>& p) { return p.second == filter.second;}))
+ continue;
+ else
+ return false;
+ }
+ return true;
+}
+
+bool match(const rgw_s3_filter& s3_filter, const rgw::sal::Object* obj) {
+ if (obj == nullptr) {
+ return false;
+ }
+
+ if (match(s3_filter.key_filter, obj->get_name())) {
+ return true;
+ }
+
+ const auto &attrs = obj->get_attrs();
+ if (!s3_filter.metadata_filter.kv.empty()) {
+ KeyValueMap attrs_map;
+ for (auto& attr : attrs) {
+ if (boost::algorithm::starts_with(attr.first, RGW_ATTR_META_PREFIX)) {
+ std::string_view key(attr.first);
+ key.remove_prefix(sizeof(RGW_ATTR_PREFIX)-1);
+ // we want to pass a null terminated version
+ // of the bufferlist, hence "to_str().c_str()"
+ attrs_map.emplace(key, attr.second.to_str().c_str());
+ }
+ }
+ if (match(s3_filter.metadata_filter, attrs_map)) {
+ return true;
+ }
+ }
+
+ if (!s3_filter.tag_filter.kv.empty()) {
+ // tag filter exists
+ // try to fetch tags from the attributes
+ KeyMultiValueMap tags;
+ const auto attr_iter = attrs.find(RGW_ATTR_TAGS);
+ if (attr_iter != attrs.end()) {
+ auto bliter = attr_iter->second.cbegin();
+ RGWObjTags obj_tags;
+ try {
+ ::decode(obj_tags, bliter);
+ } catch (buffer::error &) {
+ // not able to decode tags
+ return false;
+ }
+ tags = std::move(obj_tags.get_tags());
+ }
+ if (match(s3_filter.tag_filter, tags)) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/rgw/rgw_s3_filter.h b/src/rgw/rgw_s3_filter.h
new file mode 100644
index 00000000000..9bbc4ef0088
--- /dev/null
+++ b/src/rgw/rgw_s3_filter.h
@@ -0,0 +1,102 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+#pragma once
+
+#include "rgw_tools.h"
+#include <boost/container/flat_map.hpp>
+
+class XMLObj;
+
+struct rgw_s3_key_filter {
+ std::string prefix_rule;
+ std::string suffix_rule;
+ std::string regex_rule;
+
+ bool has_content() const;
+
+ void dump(Formatter *f) const;
+ bool decode_xml(XMLObj *obj);
+ void dump_xml(Formatter *f) const;
+
+ void encode(bufferlist& bl) const {
+ ENCODE_START(1, 1, bl);
+ encode(prefix_rule, bl);
+ encode(suffix_rule, bl);
+ encode(regex_rule, bl);
+ ENCODE_FINISH(bl);
+ }
+
+ void decode(bufferlist::const_iterator& bl) {
+ DECODE_START(1, bl);
+ decode(prefix_rule, bl);
+ decode(suffix_rule, bl);
+ decode(regex_rule, bl);
+ DECODE_FINISH(bl);
+ }
+};
+WRITE_CLASS_ENCODER(rgw_s3_key_filter)
+
+using KeyValueMap = boost::container::flat_map<std::string, std::string>;
+using KeyMultiValueMap = std::multimap<std::string, std::string>;
+
+struct rgw_s3_key_value_filter {
+ KeyValueMap kv;
+
+ bool has_content() const;
+
+ void dump(Formatter *f) const;
+ bool decode_xml(XMLObj *obj);
+ void dump_xml(Formatter *f) const;
+
+ void encode(bufferlist& bl) const {
+ ENCODE_START(1, 1, bl);
+ encode(kv, bl);
+ ENCODE_FINISH(bl);
+ }
+ void decode(bufferlist::const_iterator& bl) {
+ DECODE_START(1, bl);
+ decode(kv, bl);
+ DECODE_FINISH(bl);
+ }
+};
+WRITE_CLASS_ENCODER(rgw_s3_key_value_filter)
+
+struct rgw_s3_filter {
+ rgw_s3_key_filter key_filter;
+ rgw_s3_key_value_filter metadata_filter;
+ rgw_s3_key_value_filter tag_filter;
+
+ bool has_content() const;
+
+ void dump(Formatter *f) const;
+ bool decode_xml(XMLObj *obj);
+ void dump_xml(Formatter *f) const;
+
+ void encode(bufferlist& bl) const {
+ ENCODE_START(2, 1, bl);
+ encode(key_filter, bl);
+ encode(metadata_filter, bl);
+ encode(tag_filter, bl);
+ ENCODE_FINISH(bl);
+ }
+
+ void decode(bufferlist::const_iterator& bl) {
+ DECODE_START(2, bl);
+ decode(key_filter, bl);
+ decode(metadata_filter, bl);
+ if (struct_v >= 2) {
+ decode(tag_filter, bl);
+ }
+ DECODE_FINISH(bl);
+ }
+};
+WRITE_CLASS_ENCODER(rgw_s3_filter)
+
+bool match(const rgw_s3_key_filter& filter, const std::string& key);
+
+bool match(const rgw_s3_key_value_filter& filter, const KeyValueMap& kv);
+
+bool match(const rgw_s3_key_value_filter& filter, const KeyMultiValueMap& kv);
+
+bool match(const rgw_s3_filter& filter, const rgw::sal::Object* obj);
diff --git a/src/rgw/rgw_sal_dbstore.cc b/src/rgw/rgw_sal_dbstore.cc
index d3af42cf2ec..0e4f95846d1 100644
--- a/src/rgw/rgw_sal_dbstore.cc
+++ b/src/rgw/rgw_sal_dbstore.cc
@@ -271,7 +271,7 @@ namespace rgw::sal {
/* XXX: handle has_instance_obj like in set_bucket_instance_attrs() */
- ret = store->getDB()->update_bucket(dpp, "attrs", info, false, nullptr, &new_attrs, nullptr, &get_info().objv_tracker);
+ ret = store->getDB()->update_bucket(dpp, "attrs", info, false, nullptr, &attrs, nullptr, &get_info().objv_tracker);
return ret;
}
diff --git a/src/test/rgw/bucket_notification/api.py b/src/test/rgw/bucket_notification/api.py
index e7ec31f1711..e84aa16edc7 100644
--- a/src/test/rgw/bucket_notification/api.py
+++ b/src/test/rgw/bucket_notification/api.py
@@ -247,12 +247,16 @@ def delete_all_topics(conn, tenant, cluster):
if tenant == '':
topics_result = admin(['topic', 'list'], cluster)
topics_json = json.loads(topics_result[0])
+ if 'topics' not in topics_json:
+ topics_json = topics_json.get('result',{})
for topic in topics_json['topics']:
rm_result = admin(['topic', 'rm', '--topic', topic['name']], cluster)
print(rm_result)
else:
topics_result = admin(['topic', 'list', '--tenant', tenant], cluster)
topics_json = json.loads(topics_result[0])
+ if 'topics' not in topics_json:
+ topics_json = topics_json.get('result',{})
for topic in topics_json['topics']:
rm_result = admin(['topic', 'rm', '--tenant', tenant, '--topic', topic['name']], cluster)
print(rm_result)
diff --git a/src/test/rgw/bucket_notification/test_bn.py b/src/test/rgw/bucket_notification/test_bn.py
index 359990b3531..90ee33617fe 100644
--- a/src/test/rgw/bucket_notification/test_bn.py
+++ b/src/test/rgw/bucket_notification/test_bn.py
@@ -4359,6 +4359,242 @@ def test_ps_s3_multiple_topics_notification():
http_server.close()
+@attr('data_path_v2_test')
+def test_ps_s3_list_topics_migration():
+ """ test list topics on migration"""
+ if get_config_cluster() == 'noname':
+ return SkipTest('realm is needed for migration test')
+
+ # Initialize connections and configurations
+ conn1 = connection()
+ tenant = 'kaboom1'
+ conn2 = connect_random_user(tenant)
+ bucket_name = gen_bucket_name()
+ topics = [f"{bucket_name}{TOPIC_SUFFIX}{i}" for i in range(1, 7)]
+ tenant_topics = [f"{tenant}_{topic}" for topic in topics]
+
+ # Define topic names with version
+ topic_versions = {
+ "topic1_v2": f"{topics[0]}_v2",
+ "topic2_v2": f"{topics[1]}_v2",
+ "topic3_v1": f"{topics[2]}_v1",
+ "topic4_v1": f"{topics[3]}_v1",
+ "topic5_v1": f"{topics[4]}_v1",
+ "topic6_v1": f"{topics[5]}_v1",
+ "tenant_topic1_v2": f"{tenant_topics[0]}_v2",
+ "tenant_topic2_v1": f"{tenant_topics[1]}_v1",
+ "tenant_topic3_v1": f"{tenant_topics[2]}_v1"
+ }
+
+ # Get necessary configurations
+ host = get_ip()
+ http_port = random.randint(10000, 20000)
+ endpoint_address = 'http://' + host + ':' + str(http_port)
+ endpoint_args = 'push-endpoint=' + endpoint_address + '&persistent=true'
+ zonegroup = get_config_zonegroup()
+ conf_cluster = get_config_cluster()
+
+ # Make sure there are no leftover topics on v2
+ zonegroup_modify_feature(enable=True, feature_name=zonegroup_feature_notification_v2)
+ delete_all_topics(conn1, '', conf_cluster)
+ delete_all_topics(conn2, tenant, conf_cluster)
+
+ # Start v1 notification
+ # Make sure there are no leftover topics on v1
+ zonegroup_modify_feature(enable=False, feature_name=zonegroup_feature_notification_v2)
+ delete_all_topics(conn1, '', conf_cluster)
+ delete_all_topics(conn2, tenant, conf_cluster)
+
+ # Create s3 - v1 topics
+ topic_conf = PSTopicS3(conn1, topic_versions['topic3_v1'], zonegroup, endpoint_args=endpoint_args)
+ topic_arn3 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_versions['topic4_v1'], zonegroup, endpoint_args=endpoint_args)
+ topic_arn4 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_versions['topic5_v1'], zonegroup, endpoint_args=endpoint_args)
+ topic_arn5 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_versions['topic6_v1'], zonegroup, endpoint_args=endpoint_args)
+ topic_arn6 = topic_conf.set_config()
+ tenant_topic_conf = PSTopicS3(conn2, topic_versions['tenant_topic2_v1'], zonegroup, endpoint_args=endpoint_args)
+ tenant_topic_arn2 = tenant_topic_conf.set_config()
+ tenant_topic_conf = PSTopicS3(conn2, topic_versions['tenant_topic3_v1'], zonegroup, endpoint_args=endpoint_args)
+ tenant_topic_arn3 = tenant_topic_conf.set_config()
+
+ # Start v2 notification
+ zonegroup_modify_feature(enable=True, feature_name=zonegroup_feature_notification_v2)
+
+ # Create s3 - v2 topics
+ topic_conf = PSTopicS3(conn1, topic_versions['topic1_v2'], zonegroup, endpoint_args=endpoint_args)
+ topic_arn1 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_versions['topic2_v2'], zonegroup, endpoint_args=endpoint_args)
+ topic_arn2 = topic_conf.set_config()
+ tenant_topic_conf = PSTopicS3(conn2, topic_versions['tenant_topic1_v2'], zonegroup, endpoint_args=endpoint_args)
+ tenant_topic_arn1 = tenant_topic_conf.set_config()
+
+ # Verify topics list
+ try:
+ # Verify no tenant topics
+ res, status = topic_conf.get_list()
+ assert_equal(status // 100, 2)
+ listTopicsResponse = res.get('ListTopicsResponse', {})
+ listTopicsResult = listTopicsResponse.get('ListTopicsResult', {})
+ topics = listTopicsResult.get('Topics', {})
+ member = topics['member'] if topics else []
+ assert_equal(len(member), 6)
+
+ # Verify tenant topics
+ res, status = tenant_topic_conf.get_list()
+ assert_equal(status // 100, 2)
+ listTopicsResponse = res.get('ListTopicsResponse', {})
+ listTopicsResult = listTopicsResponse.get('ListTopicsResult', {})
+ topics = listTopicsResult.get('Topics', {})
+ member = topics['member'] if topics else []
+ assert_equal(len(member), 3)
+ finally:
+ # Cleanup created topics
+ topic_conf.del_config(topic_arn1)
+ topic_conf.del_config(topic_arn2)
+ topic_conf.del_config(topic_arn3)
+ topic_conf.del_config(topic_arn4)
+ topic_conf.del_config(topic_arn5)
+ topic_conf.del_config(topic_arn6)
+ tenant_topic_conf.del_config(tenant_topic_arn1)
+ tenant_topic_conf.del_config(tenant_topic_arn2)
+ tenant_topic_conf.del_config(tenant_topic_arn3)
+
+
+@attr('basic_test')
+def test_ps_s3_list_topics():
+ """ test list topics"""
+
+ # Initialize connections, topic names and configurations
+ conn1 = connection()
+ tenant = 'kaboom1'
+ conn2 = connect_random_user(tenant)
+ bucket_name = gen_bucket_name()
+ topic_name1 = bucket_name + TOPIC_SUFFIX + '1'
+ topic_name2 = bucket_name + TOPIC_SUFFIX + '2'
+ topic_name3 = bucket_name + TOPIC_SUFFIX + '3'
+ tenant_topic_name1 = tenant + "_" + topic_name1
+ tenant_topic_name2 = tenant + "_" + topic_name2
+ host = get_ip()
+ http_port = random.randint(10000, 20000)
+ endpoint_address = 'http://' + host + ':' + str(http_port)
+ endpoint_args = 'push-endpoint=' + endpoint_address + '&persistent=true'
+ zonegroup = get_config_zonegroup()
+
+ # Make sure there are no leftover topics
+ delete_all_topics(conn1, '', get_config_cluster())
+ delete_all_topics(conn2, tenant, get_config_cluster())
+
+ # Create s3 - v2 topics
+ topic_conf = PSTopicS3(conn1, topic_name1, zonegroup, endpoint_args=endpoint_args)
+ topic_arn1 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_name2, zonegroup, endpoint_args=endpoint_args)
+ topic_arn2 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_name3, zonegroup, endpoint_args=endpoint_args)
+ topic_arn3 = topic_conf.set_config()
+ tenant_topic_conf = PSTopicS3(conn2, tenant_topic_name1, zonegroup, endpoint_args=endpoint_args)
+ tenant_topic_arn1 = tenant_topic_conf.set_config()
+ tenant_topic_conf = PSTopicS3(conn2, tenant_topic_name2, zonegroup, endpoint_args=endpoint_args)
+ tenant_topic_arn2 = tenant_topic_conf.set_config()
+
+ # Verify topics list
+ try:
+ # Verify no tenant topics
+ res, status = topic_conf.get_list()
+ assert_equal(status // 100, 2)
+ listTopicsResponse = res.get('ListTopicsResponse', {})
+ listTopicsResult = listTopicsResponse.get('ListTopicsResult', {})
+ topics = listTopicsResult.get('Topics', {})
+ member = topics['member'] if topics else [] # version 2
+ assert_equal(len(member), 3)
+
+ # Verify topics for tenant
+ res, status = tenant_topic_conf.get_list()
+ assert_equal(status // 100, 2)
+ listTopicsResponse = res.get('ListTopicsResponse', {})
+ listTopicsResult = listTopicsResponse.get('ListTopicsResult', {})
+ topics = listTopicsResult.get('Topics', {})
+ member = topics['member'] if topics else []
+ assert_equal(len(member), 2)
+ finally:
+ # Cleanup created topics
+ topic_conf.del_config(topic_arn1)
+ topic_conf.del_config(topic_arn2)
+ topic_conf.del_config(topic_arn3)
+ tenant_topic_conf.del_config(tenant_topic_arn1)
+ tenant_topic_conf.del_config(tenant_topic_arn2)
+
+@attr('data_path_v2_test')
+def test_ps_s3_list_topics_v1():
+ """ test list topics on v1"""
+ if get_config_cluster() == 'noname':
+ return SkipTest('realm is needed')
+
+ # Initialize connections and configurations
+ conn1 = connection()
+ tenant = 'kaboom1'
+ conn2 = connect_random_user(tenant)
+ bucket_name = gen_bucket_name()
+ topic_name1 = bucket_name + TOPIC_SUFFIX + '1'
+ topic_name2 = bucket_name + TOPIC_SUFFIX + '2'
+ topic_name3 = bucket_name + TOPIC_SUFFIX + '3'
+ tenant_topic_name1 = tenant + "_" + topic_name1
+ tenant_topic_name2 = tenant + "_" + topic_name2
+ host = get_ip()
+ http_port = random.randint(10000, 20000)
+ endpoint_address = 'http://' + host + ':' + str(http_port)
+ endpoint_args = 'push-endpoint=' + endpoint_address + '&persistent=true'
+ zonegroup = get_config_zonegroup()
+ conf_cluster = get_config_cluster()
+
+ # Make sure there are no leftover topics
+ delete_all_topics(conn1, '', conf_cluster)
+ delete_all_topics(conn2, tenant, conf_cluster)
+
+ # Make sure that we disable v2
+ zonegroup_modify_feature(enable=False, feature_name=zonegroup_feature_notification_v2)
+
+ # Create s3 - v1 topics
+ topic_conf = PSTopicS3(conn1, topic_name1, zonegroup, endpoint_args=endpoint_args)
+ topic_arn1 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_name2, zonegroup, endpoint_args=endpoint_args)
+ topic_arn2 = topic_conf.set_config()
+ topic_conf = PSTopicS3(conn1, topic_name3, zonegroup, endpoint_args=endpoint_args)
+ topic_arn3 = topic_conf.set_config()
+ tenant_topic_conf = PSTopicS3(conn2, tenant_topic_name1, zonegroup, endpoint_args=endpoint_args)
+ tenant_topic_arn1 = tenant_topic_conf.set_config()
+ tenant_topic_conf = PSTopicS3(conn2, tenant_topic_name2, zonegroup, endpoint_args=endpoint_args)
+ tenant_topic_arn2 = tenant_topic_conf.set_config()
+
+ # Verify topics list
+ try:
+ # Verify no tenant topics
+ res, status = topic_conf.get_list()
+ assert_equal(status // 100, 2)
+ listTopicsResponse = res.get('ListTopicsResponse', {})
+ listTopicsResult = listTopicsResponse.get('ListTopicsResult', {})
+ topics = listTopicsResult.get('Topics', {})
+ member = topics['member'] if topics else []
+ assert_equal(len(member), 3)
+
+ # Verify tenant topics
+ res, status = tenant_topic_conf.get_list()
+ assert_equal(status // 100, 2)
+ listTopicsResponse = res.get('ListTopicsResponse', {})
+ listTopicsResult = listTopicsResponse.get('ListTopicsResult', {})
+ topics = listTopicsResult.get('Topics', {})
+ member = topics['member'] if topics else []
+ assert_equal(len(member), 2)
+ finally:
+ # Cleanup created topics
+ topic_conf.del_config(topic_arn1)
+ topic_conf.del_config(topic_arn2)
+ topic_conf.del_config(topic_arn3)
+ tenant_topic_conf.del_config(tenant_topic_arn1)
+ tenant_topic_conf.del_config(tenant_topic_arn2)
+
+
@attr('basic_test')
def test_ps_s3_topic_permissions():
""" test s3 topic set/get/delete permissions """