Skip to content

Commit 2ee9dd9

Browse files
committed
MB-70103 Add backup-service repo-get command
Duplicates the functionality of backup-service repository --get which will be deprecated. Change-Id: Ib2044840ad3354c4652c9a5aeee116f1324f239f Reviewed-on: https://review.couchbase.org/c/couchbase-cli/+/238410 Reviewed-by: Safian Ali <safian.ali@couchbase.com> Tested-by: Build Bot <build@couchbase.com>
1 parent 7657b56 commit 2ee9dd9

7 files changed

Lines changed: 318 additions & 5 deletions

File tree

cbmgr.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6996,19 +6996,24 @@ def __init__(self):
69966996
self.settings_cmd = BackupServiceSettings(self.subparser)
69976997
self.repository_cmd = BackupServiceRepository(self.subparser)
69986998
self.repo_list_cmd = BackupServiceRepoList(self.subparser)
6999+
self.repo_get_cmd = BackupServiceRepoGet(self.subparser)
69997000
self.plan_cmd = BackupServicePlan(self.subparser)
70007001
self.nodeThreads_cmd = BackupServiceNodeThreadsMap(self.subparser)
70017002

70027003
def execute(self, opts):
7003-
if opts.sub_cmd is None or opts.sub_cmd not in ['settings', 'repository', 'repo-list', 'plan', 'node-threads']:
7004-
_exit_if_errors(['<subcommand> must be one of [settings, repository, repo-list, plan, node-threads]'])
7004+
subcommands = ['settings', 'repository', 'repo-list', 'repo-get', 'plan', 'node-threads']
7005+
7006+
if opts.sub_cmd is None or opts.sub_cmd not in subcommands:
7007+
_exit_if_errors([f'<subcommand> must be one of {subcommands}'])
70057008

70067009
if opts.sub_cmd == 'settings':
70077010
self.settings_cmd.execute(opts)
70087011
elif opts.sub_cmd == 'repository':
70097012
self.repository_cmd.execute(opts)
70107013
elif opts.sub_cmd == 'repo-list':
70117014
self.repo_list_cmd.execute(opts)
7015+
elif opts.sub_cmd == 'repo-get':
7016+
self.repo_get_cmd.execute(opts)
70127017
elif opts.sub_cmd == 'plan':
70137018
self.plan_cmd.execute(opts)
70147019
elif opts.sub_cmd == 'node-threads':
@@ -7096,7 +7101,7 @@ def human_friendly_print_repository(repository):
70967101
if 'bucket' in repository:
70977102
print(f'Bucket: {repository["bucket"]["name"]}')
70987103
if 'plan_name' in repository and repository['plan_name'] != "":
7099-
print(f'plan: {repository["plan_name"]}')
7104+
print(f'Plan: {repository["plan_name"]}')
71007105
print(f'Creation time: {repository["creation_time"]}')
71017106

71027107
if 'scheduled' in repository and repository['scheduled']:
@@ -7242,6 +7247,42 @@ def get_description():
72427247
return 'List backup service repositories'
72437248

72447249

7250+
class BackupServiceRepoGet:
7251+
"""Retrieves one repository from the backup service
7252+
7253+
If the repository does not exist an error will be returned
7254+
"""
7255+
7256+
def __init__(self, subparser):
7257+
"""setup the parser"""
7258+
self.rest = None
7259+
repository_parser = subparser.add_parser('repo-get', help='Retrieve a backup repository', add_help=False,
7260+
allow_abbrev=False)
7261+
7262+
repository_parser.add_argument('--id', metavar='<id>', help='The repository id', required=True)
7263+
repository_parser.add_argument('--state', metavar='<state>', choices=['active', 'archived', 'imported'],
7264+
help='The repository state.', required=True)
7265+
7266+
@rest_initialiser(version_check=True, enterprise_check=True, cluster_init_check=True)
7267+
def execute(self, opts):
7268+
"""Run the backup-service repo-get subcommand"""
7269+
7270+
repository, errors = self.rest.get_backup_service_repository(opts.id, opts.state)
7271+
_exit_if_errors(errors)
7272+
if opts.output == 'json':
7273+
print(json.dumps(repository, indent=2))
7274+
else:
7275+
human_friendly_print_repository(repository)
7276+
7277+
@staticmethod
7278+
def get_man_page_name():
7279+
return get_doc_page_name("couchbase-cli-backup-service-repo-get")
7280+
7281+
@staticmethod
7282+
def get_description():
7283+
return 'Retrieves a repository from the backup service'
7284+
7285+
72457286
class BackupServiceRepository:
72467287
"""This command manages backup services repositories.
72477288

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
** xref:cli:cbcli/couchbase-cli-backup-service.adoc[backup-service]
55
** xref:cli:cbcli/couchbase-cli-backup-service-nodes-threads-map.adoc[backup-service nodes-threads-map]
66
** xref:cli:cbcli/couchbase-cli-backup-service-plan.adoc[backup-service plan]
7+
** xref:cli:cbcli/couchbase-cli-backup-service-repo-get.adoc[backup-service repo-get]
78
** xref:cli:cbcli/couchbase-cli-backup-service-repo-list.adoc[backup-service repo-list]
89
** xref:cli:cbcli/couchbase-cli-backup-service-repository.adoc[backup-service repository]
910
** xref:cli:cbcli/couchbase-cli-backup-service-settings.adoc[backup-service settings]
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
= couchbase-cli-backup-service-repo-get(1)
2+
:description: Retrieve a backup service repository
3+
ifndef::doctype-manpage[:doctitle: backup-service repo-get]
4+
5+
ifdef::doctype-manpage[]
6+
== NAME
7+
8+
couchbase-cli-backup-service-repo-get -
9+
endif::[]
10+
Retrieve 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-get_ [--id <id>] [--state <state>]
19+
20+
== DESCRIPTION
21+
22+
Retrieves a single backup repository by its ID and state. If the repository
23+
does not exist an error will be returned. This command supports retrieving
24+
repository information in both JSON and human-friendly format.
25+
26+
== OPTIONS
27+
28+
--id <id>::
29+
The repository ID. This argument is required.
30+
31+
--state <state>::
32+
The repository state. Valid states are 'active', 'archived', or 'imported'.
33+
This argument is required.
34+
35+
include::{partialsdir}/cbcli/part-common-options.adoc[]
36+
37+
include::{partialsdir}/cbcli/part-host-formats.adoc[]
38+
39+
include::{partialsdir}/cbcli/part-certificate-authentication.adoc[]
40+
41+
== EXAMPLES
42+
43+
To retrieve the details of an active backup repository with ID 'weekly-all',
44+
run the following command.
45+
46+
----
47+
$ couchbase-cli backup-service -c 127.0.0.1:8091 -u Administrator -p password \
48+
repo-get --id weekly-all --state active
49+
50+
ID: weekly-all
51+
State: active
52+
Healthy: True
53+
Archive: /backup
54+
Repository: a8059549-7fc3-401a-8fb8-008d1e20f1b0
55+
Plan: _weekly
56+
Creation time: 2020-07-10T07:44:18.826195+01:00
57+
58+
Scheduled tasks:
59+
Name | Task type | Next run
60+
----------------------------------------------
61+
backup_monday_full | Backup | 2020-07-13T22:00:00+01:00
62+
backup_wednesday | Backup | 2020-07-15T22:00:00+01:00
63+
merge_week | Merge | 2020-07-12T23:20:00+01:00
64+
----
65+
66+
To retrieve an archived repository, use the `--state archived` flag.
67+
68+
----
69+
$ couchbase-cli backup-service -c 127.0.0.1:8091 -u Administrator -p password \
70+
repo-get --id old-data --state archived
71+
72+
ID: old-data
73+
State: archived
74+
Healthy: True
75+
Archive: /backup/archive
76+
Repository: d6ccec04-6f03-4599-94c5-b95ac10a4f80
77+
Plan: _daily
78+
Creation time: 2020-06-15T10:30:00.000000+01:00
79+
----
80+
81+
To get the output in JSON format, use the `--output json` flag before the
82+
subcommand.
83+
84+
----
85+
$ couchbase-cli backup-service -c 127.0.0.1:8091 -u Administrator -p password \
86+
--output json repo-get --id weekly-all --state active
87+
{
88+
"id": "weekly-all",
89+
"state": "active",
90+
"archive": "/backup",
91+
"repo": "a8059549-7fc3-401a-8fb8-008d1e20f1b0",
92+
"profile_name": "_weekly",
93+
"health": {
94+
"healthy": true
95+
},
96+
"creation_time": "2025-12-19T14:28:34.539238Z",
97+
"update_time": "2025-12-19T14:28:34.539238Z",
98+
"scheduled": {
99+
"backup_hourly": {
100+
"name": "backup_hourly",
101+
"task_type": "BACKUP",
102+
"next_run": "2026-01-09T11:00:47Z"
103+
},
104+
"merge_every_6_hours": {
105+
"name": "merge_every_6_hours",
106+
"task_type": "MERGE",
107+
"next_run": "2026-01-09T15:30:46Z"
108+
},
109+
"merge_week": {
110+
"name": "merge_week",
111+
"task_type": "MERGE",
112+
"next_run": "2026-01-11T23:40:00Z"
113+
}
114+
}
115+
}
116+
----
117+
118+
== ENVIRONMENT AND CONFIGURATION VARIABLES
119+
120+
include::{partialsdir}/cbcli/part-common-env.adoc[]
121+
122+
== SEE ALSO
123+
124+
man:couchbase-cli-backup-service[1],
125+
man:couchbase-cli-backup-service-repo-list[1],
126+
127+
include::{partialsdir}/cbcli/part-footer.adoc[]
128+

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ include::{partialsdir}/cbcli/part-common-env.adoc[]
111111

112112
== SEE ALSO
113113

114-
man:couchbase-cli-backup-service[1]
114+
man:couchbase-cli-backup-service[1],
115+
man:couchbase-cli-backup-service-repo-get[1]
115116

116117
include::{partialsdir}/cbcli/part-footer.adoc[]
117118

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ State: active
131131
Healthy: True
132132
Archive: /backup
133133
Repository: a8059549-7fc3-401a-8fb8-008d1e20f1b0
134-
Profile: _weekly
134+
plan: _weekly
135135
Creation time: 2020-07-10T07:44:18.826195+01:00
136136

137137
Scheduled tasks:

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ man:couchbase-cli-backup-service-nodes-threads-map[1]::
4040
man:couchbase-cli-backup-service-repo-list[1]::
4141
List backup service repositories.
4242

43+
man:couchbase-cli-backup-service-repo-get[1]::
44+
Retrieve a backup service repository.
45+
4346
include::{partialsdir}/cbcli/part-host-formats.adoc[]
4447

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

test/test_cli.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4934,6 +4934,145 @@ def test_imported_repository_shows_na_for_plan(self):
49344934
self.assertIn('N/A', self.str_output)
49354935

49364936

4937+
class TestBackupServiceRepoGet(CommandTest):
4938+
"""Test the backup-service repo-get subcommand"""
4939+
4940+
def setUp(self):
4941+
self.server_args = {'enterprise': True, 'init': True, 'is_admin': True,
4942+
'/pools/default/nodeServices': {'nodesExt': [{
4943+
'hostname': host,
4944+
'services': {
4945+
'backupAPI': port,
4946+
},
4947+
}]}}
4948+
self.command = ['couchbase-cli', 'backup-service'] + cluster_connect_args + ['repo-get']
4949+
super(TestBackupServiceRepoGet, self).setUp()
4950+
4951+
def test_missing_id(self):
4952+
"""Test that the command fails if --id is not provided"""
4953+
self.system_exit_run(self.command + ['--state', 'active'], self.server_args)
4954+
self.assertIn('--id', self.str_error)
4955+
self.assertIn('required', self.str_error)
4956+
4957+
def test_missing_state(self):
4958+
"""Test that the command fails if --state is not provided"""
4959+
self.system_exit_run(self.command + ['--id', 'repo1'], self.server_args)
4960+
self.assertIn('--state', self.str_error)
4961+
self.assertIn('required', self.str_error)
4962+
4963+
def test_invalid_state(self):
4964+
"""Test that if a state not in [active, imported, archived] is provided the command exits with a non zero status
4965+
code
4966+
"""
4967+
self.system_exit_run(self.command + ['--id', 'repo1', '--state', 'invalid'], self.server_args)
4968+
4969+
def test_get_active_repository(self):
4970+
"""Test that the command retrieves an active repository correctly"""
4971+
self.server_args['/api/v1/cluster/self/repository/active/repo1'] = {
4972+
'id': 'repo1',
4973+
'state': 'active',
4974+
'archive': '/backup/archive',
4975+
'repo': '/backup/repo',
4976+
'plan_name': 'daily-plan',
4977+
'health': {'healthy': True},
4978+
'creation_time': '2024-01-01T00:00:00Z',
4979+
}
4980+
4981+
self.no_error_run(self.command + ['--id', 'repo1', '--state', 'active'], self.server_args)
4982+
self.assertIn('GET:/api/v1/cluster/self/repository/active/repo1', self.server.trace)
4983+
self.assertIn('ID: repo1', self.str_output)
4984+
self.assertIn('State: active', self.str_output)
4985+
self.assertIn('Healthy: True', self.str_output)
4986+
self.assertIn('Archive: /backup/archive', self.str_output)
4987+
self.assertIn('Repository: /backup/repo', self.str_output)
4988+
self.assertIn('Plan: daily-plan', self.str_output)
4989+
4990+
def test_get_archived_repository(self):
4991+
"""Test that the command retrieves an archived repository correctly"""
4992+
self.server_args['/api/v1/cluster/self/repository/archived/archivedrepo'] = {
4993+
'id': 'archivedrepo',
4994+
'state': 'archived',
4995+
'archive': '/backup/archive',
4996+
'repo': '/backup/repo',
4997+
'plan_name': 'weekly-plan',
4998+
'creation_time': '2024-01-01T00:00:00Z',
4999+
}
5000+
5001+
self.no_error_run(self.command + ['--id', 'archivedrepo', '--state', 'archived'], self.server_args)
5002+
self.assertIn('GET:/api/v1/cluster/self/repository/archived/archivedrepo', self.server.trace)
5003+
self.assertIn('ID: archivedrepo', self.str_output)
5004+
self.assertIn('State: archived', self.str_output)
5005+
5006+
def test_get_imported_repository(self):
5007+
"""Test that the command retrieves an imported repository correctly"""
5008+
self.server_args['/api/v1/cluster/self/repository/imported/importedrepo'] = {
5009+
'id': 'importedrepo',
5010+
'state': 'imported',
5011+
'archive': '/backup/archive',
5012+
'repo': '/backup/repo',
5013+
'creation_time': '2024-01-01T00:00:00Z',
5014+
}
5015+
5016+
self.no_error_run(self.command + ['--id', 'importedrepo', '--state', 'imported'], self.server_args)
5017+
self.assertIn('GET:/api/v1/cluster/self/repository/imported/importedrepo', self.server.trace)
5018+
self.assertIn('ID: importedrepo', self.str_output)
5019+
self.assertIn('State: imported', self.str_output)
5020+
5021+
def test_get_repository_json_output(self):
5022+
"""Test that the command outputs JSON when --output json is specified"""
5023+
self.server_args['/api/v1/cluster/self/repository/active/repo1'] = {
5024+
'id': 'repo1',
5025+
'state': 'active',
5026+
'archive': '/backup/archive',
5027+
'repo': '/backup/repo',
5028+
'plan_name': 'daily-plan',
5029+
'health': {'healthy': True},
5030+
'creation_time': '2024-01-01T00:00:00Z',
5031+
}
5032+
5033+
# --output must come before repo-get subcommand
5034+
json_command = ['couchbase-cli', 'backup-service'] + cluster_connect_args + ['--output', 'json', 'repo-get']
5035+
self.no_error_run(json_command + ['--id', 'repo1', '--state', 'active'], self.server_args)
5036+
self.assertIn('GET:/api/v1/cluster/self/repository/active/repo1', self.server.trace)
5037+
output = json.loads(self.str_output)
5038+
self.assertEqual(output['id'], 'repo1')
5039+
self.assertEqual(output['state'], 'active')
5040+
self.assertEqual(output['archive'], '/backup/archive')
5041+
self.assertEqual(output['repo'], '/backup/repo')
5042+
self.assertEqual(output['plan_name'], 'daily-plan')
5043+
5044+
def test_get_unhealthy_repository(self):
5045+
"""Test that an unhealthy repository shows Healthy: False"""
5046+
self.server_args['/api/v1/cluster/self/repository/active/unhealthyrepo'] = {
5047+
'id': 'unhealthyrepo',
5048+
'state': 'active',
5049+
'archive': '/backup/archive',
5050+
'repo': '/backup/repo',
5051+
'plan_name': 'daily-plan',
5052+
'health': {'healthy': False},
5053+
'creation_time': '2024-01-01T00:00:00Z',
5054+
}
5055+
5056+
self.no_error_run(self.command + ['--id', 'unhealthyrepo', '--state', 'active'], self.server_args)
5057+
self.assertIn('Healthy: False', self.str_output)
5058+
5059+
def test_get_repository_with_bucket(self):
5060+
"""Test that the bucket name is displayed when present"""
5061+
self.server_args['/api/v1/cluster/self/repository/active/repo1'] = {
5062+
'id': 'repo1',
5063+
'state': 'active',
5064+
'archive': '/backup/archive',
5065+
'repo': '/backup/repo',
5066+
'plan_name': 'daily-plan',
5067+
'health': {'healthy': True},
5068+
'creation_time': '2024-01-01T00:00:00Z',
5069+
'bucket': {'name': 'my-bucket'},
5070+
}
5071+
5072+
self.no_error_run(self.command + ['--id', 'repo1', '--state', 'active'], self.server_args)
5073+
self.assertIn('Bucket: my-bucket', self.str_output)
5074+
5075+
49375076
class TestBackupServiceSettings(CommandTest):
49385077
"""Test the backup-service settings subcommand"""
49395078

0 commit comments

Comments
 (0)