pglift-cli 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pglift_cli/__init__.py +7 -0
- pglift_cli/__main__.py +10 -0
- pglift_cli/_settings.py +44 -0
- pglift_cli/_site.py +34 -0
- pglift_cli/base.py +53 -0
- pglift_cli/console.py +9 -0
- pglift_cli/database.py +260 -0
- pglift_cli/hookspecs.py +24 -0
- pglift_cli/instance.py +461 -0
- pglift_cli/main.py +277 -0
- pglift_cli/model.py +346 -0
- pglift_cli/patroni.py +37 -0
- pglift_cli/pgbackrest/__init__.py +101 -0
- pglift_cli/pgbackrest/repo_path.py +40 -0
- pglift_cli/pgconf.py +151 -0
- pglift_cli/pm.py +19 -0
- pglift_cli/postgres.py +30 -0
- pglift_cli/prometheus.py +138 -0
- pglift_cli/role.py +197 -0
- pglift_cli/util.py +576 -0
- pglift_cli-1.3.0.dist-info/METADATA +59 -0
- pglift_cli-1.3.0.dist-info/RECORD +24 -0
- pglift_cli-1.3.0.dist-info/WHEEL +4 -0
- pglift_cli-1.3.0.dist-info/entry_points.txt +8 -0
pglift_cli/__init__.py
ADDED
pglift_cli/__main__.py
ADDED
pglift_cli/_settings.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from pglift.settings import Settings as BaseSettings
|
|
11
|
+
from pglift.settings import SiteSettings as BaseSiteSettings
|
|
12
|
+
from pglift.settings.base import BaseModel, LogPath, RunPath
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CLISettings(BaseModel):
|
|
16
|
+
"""Settings for pglift's command-line interface."""
|
|
17
|
+
|
|
18
|
+
logpath: Annotated[
|
|
19
|
+
Annotated[Path, LogPath],
|
|
20
|
+
Field(
|
|
21
|
+
description="Directory where temporary log files from command executions will be stored",
|
|
22
|
+
title="CLI log directory",
|
|
23
|
+
),
|
|
24
|
+
] = Path()
|
|
25
|
+
|
|
26
|
+
log_format: Annotated[
|
|
27
|
+
str, Field(description="Format for log messages when written to a file")
|
|
28
|
+
] = "%(asctime)s %(levelname)-8s %(name)s - %(message)s"
|
|
29
|
+
|
|
30
|
+
date_format: Annotated[
|
|
31
|
+
str, Field(description="Date format in log messages when written to a file")
|
|
32
|
+
] = "%Y-%m-%d %H:%M:%S"
|
|
33
|
+
|
|
34
|
+
lock_file: Annotated[
|
|
35
|
+
Path, RunPath, Field(description="Path to lock file dedicated to pglift")
|
|
36
|
+
] = Path(".pglift.lock")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Settings(BaseSettings):
|
|
40
|
+
cli: Annotated[CLISettings, Field(default_factory=CLISettings)]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SiteSettings(Settings, BaseSiteSettings):
|
|
44
|
+
pass
|
pglift_cli/_site.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import pydantic
|
|
9
|
+
|
|
10
|
+
from pglift import exceptions, plugin_manager
|
|
11
|
+
from pglift.models import interface
|
|
12
|
+
|
|
13
|
+
from . import hookspecs
|
|
14
|
+
from ._settings import Settings, SiteSettings
|
|
15
|
+
from .pm import PluginManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_settings() -> Settings:
|
|
19
|
+
try:
|
|
20
|
+
return SiteSettings()
|
|
21
|
+
except (exceptions.SettingsError, pydantic.ValidationError) as e:
|
|
22
|
+
raise click.ClickException(f"invalid site settings\n{e}") from e
|
|
23
|
+
except exceptions.UnsupportedError as e:
|
|
24
|
+
raise click.ClickException(f"unsupported operation: {e}") from None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SETTINGS: Final = _load_settings()
|
|
28
|
+
DEFAULT_SETTINGS: Final = Settings()
|
|
29
|
+
|
|
30
|
+
PLUGIN_MANAGER = PluginManager.get(SETTINGS, hookspecs)
|
|
31
|
+
|
|
32
|
+
pm = plugin_manager(SETTINGS)
|
|
33
|
+
INSTANCE_MODEL: Final = interface.Instance.composite(pm)
|
|
34
|
+
ROLE_MODEL: Final = interface.Role.composite(pm)
|
pglift_cli/base.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from pglift import hooks
|
|
12
|
+
|
|
13
|
+
from . import _site, database
|
|
14
|
+
from . import hookspecs as h
|
|
15
|
+
from . import instance, pgconf, postgres, role
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CLIGroup(click.Group):
|
|
19
|
+
"""Group gathering main commands (defined here), commands from submodules
|
|
20
|
+
and commands from plugins.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
submodules = OrderedDict(
|
|
24
|
+
[
|
|
25
|
+
("instance", instance.cli),
|
|
26
|
+
("pgconf", pgconf.cli),
|
|
27
|
+
("role", role.cli),
|
|
28
|
+
("database", database.cli),
|
|
29
|
+
("postgres", postgres.cli),
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def list_commands(self, context: click.Context) -> list[str]:
|
|
34
|
+
main_commands = super().list_commands(context)
|
|
35
|
+
plugins_commands: list[str] = sorted(g.name for g in hooks(_site.PLUGIN_MANAGER, h.command)) # type: ignore[misc]
|
|
36
|
+
return main_commands + list(self.submodules) + plugins_commands
|
|
37
|
+
|
|
38
|
+
def get_command(self, context: click.Context, name: str) -> click.Command | None:
|
|
39
|
+
main_command = super().get_command(context, name)
|
|
40
|
+
if main_command is not None:
|
|
41
|
+
return main_command
|
|
42
|
+
try:
|
|
43
|
+
command = self.submodules[name]
|
|
44
|
+
except KeyError:
|
|
45
|
+
pass
|
|
46
|
+
else:
|
|
47
|
+
assert isinstance(command, click.Command), command
|
|
48
|
+
return command
|
|
49
|
+
for group in hooks(_site.PLUGIN_MANAGER, h.command):
|
|
50
|
+
assert isinstance(group, click.Command)
|
|
51
|
+
if group.name == name:
|
|
52
|
+
return group
|
|
53
|
+
return None
|
pglift_cli/console.py
ADDED
pglift_cli/database.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import IO, Any
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import psycopg
|
|
14
|
+
from pydantic.v1.utils import deep_update
|
|
15
|
+
|
|
16
|
+
from pglift import databases, postgresql, privileges, task
|
|
17
|
+
from pglift.models import interface, system
|
|
18
|
+
|
|
19
|
+
from . import model
|
|
20
|
+
from .util import (
|
|
21
|
+
Group,
|
|
22
|
+
Obj,
|
|
23
|
+
OutputFormat,
|
|
24
|
+
async_command,
|
|
25
|
+
dry_run_option,
|
|
26
|
+
instance_identifier_option,
|
|
27
|
+
model_dump,
|
|
28
|
+
output_format_option,
|
|
29
|
+
pass_instance,
|
|
30
|
+
print_argspec,
|
|
31
|
+
print_json_for,
|
|
32
|
+
print_schema,
|
|
33
|
+
print_table_for,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.group("database", cls=Group)
|
|
38
|
+
@instance_identifier_option
|
|
39
|
+
@click.option(
|
|
40
|
+
"--schema",
|
|
41
|
+
is_flag=True,
|
|
42
|
+
callback=functools.partial(print_schema, model=interface.Database),
|
|
43
|
+
expose_value=False,
|
|
44
|
+
is_eager=True,
|
|
45
|
+
help="Print the JSON schema of database model and exit.",
|
|
46
|
+
)
|
|
47
|
+
@click.option(
|
|
48
|
+
"--ansible-argspec",
|
|
49
|
+
is_flag=True,
|
|
50
|
+
callback=functools.partial(print_argspec, model=interface.Database),
|
|
51
|
+
expose_value=False,
|
|
52
|
+
is_eager=True,
|
|
53
|
+
hidden=True,
|
|
54
|
+
help="Print the Ansible argspec of database model and exit.",
|
|
55
|
+
)
|
|
56
|
+
def cli(**kwargs: Any) -> None:
|
|
57
|
+
"""Manage databases."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cli.command("create")
|
|
61
|
+
@model.as_parameters(interface.Database, "create")
|
|
62
|
+
@pass_instance
|
|
63
|
+
@click.pass_obj
|
|
64
|
+
@async_command
|
|
65
|
+
async def create(
|
|
66
|
+
obj: Obj, instance: system.Instance, database: interface.Database
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Create a database in a PostgreSQL instance"""
|
|
69
|
+
with obj.lock:
|
|
70
|
+
async with postgresql.running(instance):
|
|
71
|
+
if await databases.exists(instance, database.name):
|
|
72
|
+
raise click.ClickException("database already exists")
|
|
73
|
+
async with task.async_transaction():
|
|
74
|
+
await databases.apply(instance, database)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@cli.command("alter") # type: ignore[arg-type]
|
|
78
|
+
@model.as_parameters(interface.Database, "update", parse_model=False)
|
|
79
|
+
@click.argument("dbname")
|
|
80
|
+
@pass_instance
|
|
81
|
+
@click.pass_obj
|
|
82
|
+
@async_command
|
|
83
|
+
async def alter(
|
|
84
|
+
obj: Obj, instance: system.Instance, dbname: str, **changes: Any
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Alter a database in a PostgreSQL instance"""
|
|
87
|
+
with obj.lock:
|
|
88
|
+
async with postgresql.running(instance):
|
|
89
|
+
values = (await databases.get(instance, dbname)).model_dump(by_alias=True)
|
|
90
|
+
values = deep_update(values, changes)
|
|
91
|
+
altered = interface.Database.model_validate(values)
|
|
92
|
+
await databases.apply(instance, altered)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@cli.command("apply", hidden=True)
|
|
96
|
+
@click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
|
|
97
|
+
@output_format_option
|
|
98
|
+
@dry_run_option
|
|
99
|
+
@pass_instance
|
|
100
|
+
@click.pass_obj
|
|
101
|
+
@async_command
|
|
102
|
+
async def apply(
|
|
103
|
+
obj: Obj,
|
|
104
|
+
instance: system.Instance,
|
|
105
|
+
file: IO[str],
|
|
106
|
+
output_format: OutputFormat,
|
|
107
|
+
dry_run: bool,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Apply manifest as a database"""
|
|
110
|
+
database = interface.Database.parse_yaml(file)
|
|
111
|
+
if dry_run:
|
|
112
|
+
ret = interface.ApplyResult(change_state=None)
|
|
113
|
+
else:
|
|
114
|
+
with obj.lock:
|
|
115
|
+
async with postgresql.running(instance):
|
|
116
|
+
ret = await databases.apply(instance, database)
|
|
117
|
+
if output_format == OutputFormat.json:
|
|
118
|
+
print_json_for(ret)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@cli.command("get")
|
|
122
|
+
@output_format_option
|
|
123
|
+
@click.argument("name")
|
|
124
|
+
@pass_instance
|
|
125
|
+
@async_command
|
|
126
|
+
async def get(
|
|
127
|
+
instance: system.Instance, name: str, output_format: OutputFormat
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Get the description of a database"""
|
|
130
|
+
async with postgresql.running(instance):
|
|
131
|
+
db = await databases.get(instance, name)
|
|
132
|
+
if output_format == OutputFormat.json:
|
|
133
|
+
print_json_for(model_dump(db))
|
|
134
|
+
else:
|
|
135
|
+
print_table_for(
|
|
136
|
+
[db],
|
|
137
|
+
functools.partial(model_dump, exclude={"extensions", "schemas"}),
|
|
138
|
+
box=None,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@cli.command("list")
|
|
143
|
+
@output_format_option
|
|
144
|
+
@click.argument("dbname", nargs=-1)
|
|
145
|
+
@pass_instance
|
|
146
|
+
@async_command
|
|
147
|
+
async def ls(
|
|
148
|
+
instance: system.Instance, dbname: Sequence[str], output_format: OutputFormat
|
|
149
|
+
) -> None:
|
|
150
|
+
"""List databases (all or specified ones)
|
|
151
|
+
|
|
152
|
+
Only queried databases are shown when DBNAME is specified.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
async with postgresql.running(instance):
|
|
156
|
+
dbs = await databases.ls(instance, dbnames=dbname)
|
|
157
|
+
if output_format == OutputFormat.json:
|
|
158
|
+
print_json_for([model_dump(db) for db in dbs])
|
|
159
|
+
else:
|
|
160
|
+
print_table_for(dbs, model_dump)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@cli.command("drop")
|
|
164
|
+
@model.as_parameters(interface.DatabaseDropped, "create")
|
|
165
|
+
@pass_instance
|
|
166
|
+
@click.pass_obj
|
|
167
|
+
@async_command
|
|
168
|
+
async def drop(
|
|
169
|
+
obj: Obj, instance: system.Instance, databasedropped: interface.DatabaseDropped
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Drop a database"""
|
|
172
|
+
with obj.lock:
|
|
173
|
+
async with postgresql.running(instance):
|
|
174
|
+
await databases.drop(instance, databasedropped)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@cli.command("privileges")
|
|
178
|
+
@click.argument("name")
|
|
179
|
+
@click.option("-r", "--role", "roles", multiple=True, help="Role to inspect")
|
|
180
|
+
@click.option("--default", "defaults", is_flag=True, help="Display default privileges")
|
|
181
|
+
@output_format_option
|
|
182
|
+
@pass_instance
|
|
183
|
+
@async_command
|
|
184
|
+
async def list_privileges(
|
|
185
|
+
instance: system.Instance,
|
|
186
|
+
name: str,
|
|
187
|
+
roles: Sequence[str],
|
|
188
|
+
defaults: bool,
|
|
189
|
+
output_format: OutputFormat,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""List privileges on a database."""
|
|
192
|
+
async with postgresql.running(instance):
|
|
193
|
+
await databases.get(instance, name) # check existence
|
|
194
|
+
try:
|
|
195
|
+
prvlgs = await privileges.get(
|
|
196
|
+
instance, databases=(name,), roles=roles, defaults=defaults
|
|
197
|
+
)
|
|
198
|
+
except ValueError as e:
|
|
199
|
+
raise click.ClickException(str(e)) from None
|
|
200
|
+
if output_format == OutputFormat.json:
|
|
201
|
+
print_json_for([model_dump(p) for p in prvlgs])
|
|
202
|
+
else:
|
|
203
|
+
print_table_for(prvlgs, model_dump)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@cli.command("run")
|
|
207
|
+
@click.argument("sql_command")
|
|
208
|
+
@click.option(
|
|
209
|
+
"-d", "--database", "dbnames", multiple=True, help="Database to run command on"
|
|
210
|
+
)
|
|
211
|
+
@click.option(
|
|
212
|
+
"-x",
|
|
213
|
+
"--exclude-database",
|
|
214
|
+
"exclude_dbnames",
|
|
215
|
+
multiple=True,
|
|
216
|
+
help="Database to not run command on",
|
|
217
|
+
)
|
|
218
|
+
@output_format_option
|
|
219
|
+
@pass_instance
|
|
220
|
+
@async_command
|
|
221
|
+
async def run(
|
|
222
|
+
instance: system.Instance,
|
|
223
|
+
sql_command: str,
|
|
224
|
+
dbnames: Sequence[str],
|
|
225
|
+
exclude_dbnames: Sequence[str],
|
|
226
|
+
output_format: OutputFormat,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Run given command on databases of a PostgreSQL instance"""
|
|
229
|
+
async with postgresql.running(instance):
|
|
230
|
+
try:
|
|
231
|
+
result = await databases.run(
|
|
232
|
+
instance,
|
|
233
|
+
sql_command,
|
|
234
|
+
dbnames=dbnames,
|
|
235
|
+
exclude_dbnames=exclude_dbnames,
|
|
236
|
+
)
|
|
237
|
+
except psycopg.ProgrammingError as e:
|
|
238
|
+
raise click.ClickException(str(e)) from None
|
|
239
|
+
if output_format == OutputFormat.json:
|
|
240
|
+
print_json_for(result)
|
|
241
|
+
else:
|
|
242
|
+
for dbname, rows in result.items():
|
|
243
|
+
print_table_for(rows, lambda m: m, title=f"Database {dbname}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@cli.command("dump")
|
|
247
|
+
@click.option(
|
|
248
|
+
"-o",
|
|
249
|
+
"--output",
|
|
250
|
+
metavar="DIRECTORY",
|
|
251
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
252
|
+
help="Write dump file(s) to DIRECTORY instead of configured 'dumps_directory'.",
|
|
253
|
+
)
|
|
254
|
+
@click.argument("dbname")
|
|
255
|
+
@pass_instance
|
|
256
|
+
@async_command
|
|
257
|
+
async def dump(instance: system.Instance, dbname: str, output: Path | None) -> None:
|
|
258
|
+
"""Dump a database"""
|
|
259
|
+
async with postgresql.running(instance):
|
|
260
|
+
await databases.dump(instance, dbname, output)
|
pglift_cli/hookspecs.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import pluggy
|
|
9
|
+
|
|
10
|
+
from . import __name__ as pkgname
|
|
11
|
+
|
|
12
|
+
hookspec = pluggy.HookspecMarker(pkgname)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@hookspec
|
|
16
|
+
def command() -> click.Command:
|
|
17
|
+
"""Return command-line entry point as click Command (or Group) for the plugin."""
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@hookspec
|
|
22
|
+
def add_instance_commands(group: click.Group) -> None:
|
|
23
|
+
"""Extend instance commands 'group' with extra commands from the plugin."""
|
|
24
|
+
raise NotImplementedError
|