Skip to content

Commit 29f459c

Browse files
committed
MB-70102 Add subcommand for configuring WORM for an existing repository
Add the 'repo-worm' command. It accepts a '--period' and '--disable' flags similarly to the 'cbbackupmgr worm' command. Change-Id: I05b4387c64e676c796147f0298cebd5e9a45f3b6 Reviewed-on: https://review.couchbase.org/c/couchbase-cli/+/238415 Tested-by: Build Bot <build@couchbase.com> Reviewed-by: Safian Ali <safian.ali@couchbase.com>
1 parent ce41888 commit 29f459c

7 files changed

Lines changed: 238 additions & 3 deletions

File tree

cbmgr.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7003,12 +7003,13 @@ def __init__(self):
70037003
self.repo_remove_cmd = BackupServiceRepoRemove(self.subparser)
70047004
self.repo_pause_cmd = BackupServiceRepoPause(self.subparser)
70057005
self.repo_resume_cmd = BackupServiceRepoResume(self.subparser)
7006+
self.repo_worm_cmd = BackupServiceRepoWORM(self.subparser)
70067007
self.plan_cmd = BackupServicePlan(self.subparser)
70077008
self.nodeThreads_cmd = BackupServiceNodeThreadsMap(self.subparser)
70087009

70097010
def execute(self, opts):
70107011
subcommands = ['settings', 'repository', 'repo-list', 'repo-get', 'repo-add', 'repo-archive', 'repo-remove',
7011-
'repo-pause', 'repo-resume', 'plan', 'node-threads']
7012+
'repo-pause', 'repo-resume', 'repo-worm', 'plan', 'node-threads']
70127013

70137014
if opts.sub_cmd is None or opts.sub_cmd not in subcommands:
70147015
_exit_if_errors([f'<subcommand> must be one of {subcommands}'])
@@ -7031,6 +7032,8 @@ def execute(self, opts):
70317032
self.repo_pause_cmd.execute(opts)
70327033
elif opts.sub_cmd == 'repo-resume':
70337034
self.repo_resume_cmd.execute(opts)
7035+
elif opts.sub_cmd == 'repo-worm':
7036+
self.repo_worm_cmd.execute(opts)
70347037
elif opts.sub_cmd == 'plan':
70357038
self.plan_cmd.execute(opts)
70367039
elif opts.sub_cmd == 'node-threads':
@@ -7538,6 +7541,44 @@ def get_description():
75387541
return 'Resume a repository'
75397542

75407543

7544+
class BackupServiceRepoWORM:
7545+
"""Set WORM for a repository.
7546+
"""
7547+
7548+
def __init__(self, subparser):
7549+
"""setup the parser"""
7550+
self.rest = None
7551+
repository_parser = subparser.add_parser('repo-worm', help='Set WORM for a repository', add_help=False,
7552+
allow_abbrev=False)
7553+
7554+
repository_parser.add_argument('--id', metavar='<id>', help='The repository id', required=True)
7555+
7556+
action_group = repository_parser.add_mutually_exclusive_group(required=True)
7557+
action_group.add_argument('--period', type=int, metavar='<days>',
7558+
help='The WORM compliance period in days [3, 36525]')
7559+
action_group.add_argument('--disable', action='store_true',
7560+
help='Disable WORM once its validity period has expired')
7561+
7562+
@rest_initialiser(version_check=True, enterprise_check=True, cluster_init_check=True)
7563+
def execute(self, opts):
7564+
"""Run the backup-service repo-worm subcommand"""
7565+
7566+
if opts.period is not None and (opts.period < 3 or opts.period > 36525):
7567+
_exit_if_errors(['the provided period must be in the [3, 36525] range'])
7568+
7569+
_, errors = self.rest.set_worm_for_backup_repo(opts.id, opts.period, opts.disable)
7570+
_exit_if_errors(errors)
7571+
_success('Repository WORM config was set')
7572+
7573+
@staticmethod
7574+
def get_man_page_name():
7575+
return get_doc_page_name("couchbase-cli-backup-service-repo-worm")
7576+
7577+
@staticmethod
7578+
def get_description():
7579+
return 'Set WORM for a repository'
7580+
7581+
75417582
class BackupServiceRepository:
75427583
"""(Deprecated) This command manages backup services repositories.
75437584

cluster_manager.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2695,11 +2695,35 @@ def resume_backup_repository(self, repository_id):
26952695

26962696
return self._post_json(f'{hosts[0]}/api/v1/cluster/self/repository/active/{repository_id}/resume', None)
26972697

2698+
def set_worm_for_backup_repo(self, repository_id: str, period: Optional[int], disable: bool):
2699+
"""Set WORM for a repository
2700+
2701+
Args:
2702+
repository_id (str): The repository id.
2703+
period (int | None): The WORM compliance period.
2704+
disable (bool): Whether to disable WORM for the repository.
2705+
"""
2706+
hosts, errors = self.get_hostnames_for_service(BACKUP_SERVICE)
2707+
if errors:
2708+
return None, errors
2709+
2710+
if not hosts:
2711+
raise ServiceNotAvailableException(BACKUP_SERVICE)
2712+
2713+
body: Dict[str, Any] = {
2714+
"disable": disable
2715+
}
2716+
2717+
if period is not None:
2718+
body["period"] = period
2719+
2720+
return self._post_json(f'{hosts[0]}/api/v1/cluster/self/repository/active/{repository_id}/worm', body)
2721+
26982722
def add_backup_active_repository(self, repository_id: str, body: Dict[str, Any], cluster: str = 'self'):
2699-
"""Archive an active repository
2723+
"""Add an active repository
27002724
27012725
Args:
2702-
repository_id (str): The id to be given to the new repository.
2726+
repository_id (str): The repository id.
27032727
body (dict): The add active repository request.
27042728
cluster (str): Only 'self' is supported.
27052729
"""

docs/modules/cli/pages/_partials/cbcli/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
** xref:cli:cbcli/couchbase-cli-backup-service-repo-pause.adoc[backup-service repo-pause]
1212
** xref:cli:cbcli/couchbase-cli-backup-service-repo-remove.adoc[backup-service repo-remove]
1313
** xref:cli:cbcli/couchbase-cli-backup-service-repo-resume.adoc[backup-service repo-resume]
14+
** xref:cli:cbcli/couchbase-cli-backup-service-repo-worm.adoc[backup-service repo-worm]
1415
** xref:cli:cbcli/couchbase-cli-backup-service-repository.adoc[backup-service repository]
1516
** xref:cli:cbcli/couchbase-cli-backup-service-settings.adoc[backup-service settings]
1617
** xref:cli:cbcli/couchbase-cli-bucket-compact.adoc[bucket-compact]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
= couchbase-cli-backup-service-repo-worm(1)
2+
:description: Configure WORM for a backup service repository
3+
ifndef::doctype-manpage[:doctitle: backup-service repo-worm]
4+
5+
ifdef::doctype-manpage[]
6+
== NAME
7+
8+
couchbase-cli-backup-service-repo-worm -
9+
endif::[]
10+
Configure WORM for a backup service repository
11+
12+
== SYNOPSIS
13+
14+
[verse]
15+
_couchbase-cli backup-service_ [--cluster <url>] [--username <user>]
16+
[--password <password>] [--client-cert <path>]
17+
[--client-cert-password <password>] [--client-key <path>]
18+
[--client-key-password <password>] _repo-worm_ [--id <id>]
19+
(--period <days> | --disable)
20+
21+
== DESCRIPTION
22+
23+
Configures Write Once Read Many (WORM) compliance settings for a backup service
24+
repository. WORM ensures that backup data cannot be modified or deleted for a
25+
specified retention period, providing data immutability for regulatory compliance.
26+
27+
This command allows you to either set a WORM compliance period (in days) or
28+
disable WORM for a repository once its validity period has expired. Only paused
29+
repositories can have their WORM configuration modified.
30+
31+
== OPTIONS
32+
33+
--id <id>::
34+
The repository ID. This argument is required.
35+
36+
--period <days>::
37+
The WORM compliance period in days. Valid values are between 3 and 36525
38+
days (approximately 100 years). This option is mutually exclusive with
39+
`--disable`.
40+
41+
--disable::
42+
Disable WORM for the repository once its validity period has expired. This
43+
option is mutually exclusive with `--period`.
44+
45+
include::{partialsdir}/cbcli/part-common-options.adoc[]
46+
47+
include::{partialsdir}/cbcli/part-host-formats.adoc[]
48+
49+
include::{partialsdir}/cbcli/part-certificate-authentication.adoc[]
50+
51+
== EXAMPLES
52+
53+
To set a WORM compliance period of 30 days for a repository named 'weekly-backup',
54+
run the following command.
55+
56+
----
57+
$ couchbase-cli backup-service -c 127.0.0.1:8091 -u Administrator -p password \
58+
repo-worm --id weekly-backup --period 30
59+
----
60+
61+
If successful, the following output will be displayed:
62+
63+
----
64+
Repository WORM config was set
65+
----
66+
67+
To set a WORM compliance period of 3 days:
68+
69+
----
70+
$ couchbase-cli backup-service -c 127.0.0.1:8091 -u Administrator -p password \
71+
repo-worm --id my-repo --period 3
72+
----
73+
74+
To disable WORM for a repository after its validity period has expired:
75+
76+
----
77+
$ couchbase-cli backup-service -c 127.0.0.1:8091 -u Administrator -p password \
78+
repo-worm --id old-backup --disable
79+
----
80+
81+
== ENVIRONMENT AND CONFIGURATION VARIABLES
82+
83+
include::{partialsdir}/cbcli/part-common-env.adoc[]
84+
85+
== SEE ALSO
86+
87+
man:couchbase-cli-backup-service[1],
88+
man:couchbase-cli-backup-service-repo-list[1],
89+
man:couchbase-cli-backup-service-repo-get[1],
90+
man:couchbase-cli-backup-service-repo-add[1],
91+
man:couchbase-cli-backup-service-repo-archive[1]
92+
93+
include::{partialsdir}/cbcli/part-footer.adoc[]
94+

docs/modules/cli/pages/cbcli/couchbase-cli-backup-service.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ man:couchbase-cli-backup-service-repo-pause[1]::
5555
man:couchbase-cli-backup-service-repo-resume[1]::
5656
Resume a backup service repository.
5757

58+
man:couchbase-cli-backup-service-repo-worm[1]::
59+
Configure WORM for a backup service repository.
60+
5861
include::{partialsdir}/cbcli/part-host-formats.adoc[]
5962

6063
include::{partialsdir}/cbcli/part-certificate-authentication.adoc[]

test/mock_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ def export_eventing_functions(rest_params=None, server_args=None, path="", endpo
632632
(r'/api/v1/cluster/self/repository/active/\w+/archive$', {'POST': do_nothing}),
633633
(r'/api/v1/cluster/self/repository/active/[\w-]+/pause$', {'POST': do_nothing}),
634634
(r'/api/v1/cluster/self/repository/active/[\w-]+/resume$', {'POST': do_nothing}),
635+
(r'/api/v1/cluster/self/repository/active/[\w-]+/worm$', {'POST': do_nothing}),
635636
(r'/api/v1/plan$', {'GET': get_by_path}),
636637
(r'/api/v1/plan/\w+$', {'GET': get_by_path, 'DELETE': do_nothing, 'POST': do_nothing}),
637638
(r'/api/v1/cluster/self/repository/(:?active|archived|imported)/\w+(:?\?remove_repository=\w+)?$',

test/test_cli.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5464,6 +5464,77 @@ def test_resume_repository_success(self):
54645464
self.assertIn('Repository was resumed', self.str_output)
54655465

54665466

5467+
class TestBackupServiceRepoWORM(CommandTest):
5468+
"""Test the backup-service repo-worm subcommand"""
5469+
5470+
def setUp(self):
5471+
self.server_args = {'enterprise': True, 'init': True, 'is_admin': True,
5472+
'/pools/default/nodeServices': {'nodesExt': [{
5473+
'hostname': host,
5474+
'services': {
5475+
'backupAPI': port,
5476+
},
5477+
}]}}
5478+
self.command = ['couchbase-cli', 'backup-service'] + cluster_connect_args + ['repo-worm']
5479+
super(TestBackupServiceRepoWORM, self).setUp()
5480+
5481+
def test_missing_id(self):
5482+
"""Test that the command fails if --id is not provided"""
5483+
self.system_exit_run(self.command + ['--period', '30'], self.server_args)
5484+
self.assertIn('--id', self.str_error)
5485+
self.assertIn('required', self.str_error)
5486+
5487+
def test_missing_period_or_disable(self):
5488+
"""Test that the command fails if neither --period nor --disable is provided"""
5489+
self.system_exit_run(self.command + ['--id', 'myrepo'], self.server_args)
5490+
self.assertIn('--period', self.str_error)
5491+
self.assertIn('--disable', self.str_error)
5492+
self.assertIn('required', self.str_error)
5493+
5494+
def test_period_and_disable_mutually_exclusive(self):
5495+
"""Test that the command fails if both --period and --disable are provided"""
5496+
self.system_exit_run(self.command + ['--id', 'myrepo', '--period', '30', '--disable'], self.server_args)
5497+
self.assertIn('not allowed with argument', self.str_error)
5498+
5499+
def test_period_below_minimum(self):
5500+
"""Test that the command fails if --period is less than 3"""
5501+
self.system_exit_run(self.command + ['--id', 'myrepo', '--period', '2'], self.server_args)
5502+
self.assertIn('the provided period must be in the [3, 36525] range', self.str_output)
5503+
5504+
def test_period_above_maximum(self):
5505+
"""Test that the command fails if --period is greater than 36525"""
5506+
self.system_exit_run(self.command + ['--id', 'myrepo', '--period', '36526'], self.server_args)
5507+
self.assertIn('the provided period must be in the [3, 36525] range', self.str_output)
5508+
5509+
def test_set_worm_period_success(self):
5510+
"""Test that the command successfully sets WORM period for a repository"""
5511+
self.no_error_run(self.command + ['--id', 'myrepo', '--period', '30'], self.server_args)
5512+
self.assertIn('POST:/api/v1/cluster/self/repository/active/myrepo/worm', self.server.trace)
5513+
self.rest_parameter_match([json.dumps({'disable': False, 'period': 30}, sort_keys=True)])
5514+
self.assertIn('Repository WORM config was set', self.str_output)
5515+
5516+
def test_set_worm_period_minimum(self):
5517+
"""Test that the command accepts the minimum period of 3 days"""
5518+
self.no_error_run(self.command + ['--id', 'myrepo', '--period', '3'], self.server_args)
5519+
self.assertIn('POST:/api/v1/cluster/self/repository/active/myrepo/worm', self.server.trace)
5520+
self.rest_parameter_match([json.dumps({'disable': False, 'period': 3}, sort_keys=True)])
5521+
self.assertIn('Repository WORM config was set', self.str_output)
5522+
5523+
def test_set_worm_period_maximum(self):
5524+
"""Test that the command accepts the maximum period of 36525 days"""
5525+
self.no_error_run(self.command + ['--id', 'myrepo', '--period', '36525'], self.server_args)
5526+
self.assertIn('POST:/api/v1/cluster/self/repository/active/myrepo/worm', self.server.trace)
5527+
self.rest_parameter_match([json.dumps({'disable': False, 'period': 36525}, sort_keys=True)])
5528+
self.assertIn('Repository WORM config was set', self.str_output)
5529+
5530+
def test_disable_worm_success(self):
5531+
"""Test that the command successfully disables WORM for a repository"""
5532+
self.no_error_run(self.command + ['--id', 'myrepo', '--disable'], self.server_args)
5533+
self.assertIn('POST:/api/v1/cluster/self/repository/active/myrepo/worm', self.server.trace)
5534+
self.rest_parameter_match([json.dumps({'disable': True}, sort_keys=True)])
5535+
self.assertIn('Repository WORM config was set', self.str_output)
5536+
5537+
54675538
class TestBackupServiceSettings(CommandTest):
54685539
"""Test the backup-service settings subcommand"""
54695540

0 commit comments

Comments
 (0)