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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from pglift import pgbackrest, postgresql, types
|
|
14
|
+
from pglift.models import system
|
|
15
|
+
from pglift.pgbackrest import base, models
|
|
16
|
+
from pglift.pgbackrest import register_if as register_if # noqa: F401
|
|
17
|
+
|
|
18
|
+
from .. import _site, hookimpl
|
|
19
|
+
from ..util import (
|
|
20
|
+
Command,
|
|
21
|
+
Obj,
|
|
22
|
+
OutputFormat,
|
|
23
|
+
async_command,
|
|
24
|
+
instance_identifier,
|
|
25
|
+
instance_identifier_option,
|
|
26
|
+
model_dump,
|
|
27
|
+
output_format_option,
|
|
28
|
+
print_json_for,
|
|
29
|
+
print_table_for,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.command(
|
|
34
|
+
"pgbackrest",
|
|
35
|
+
hidden=True,
|
|
36
|
+
cls=Command,
|
|
37
|
+
context_settings={"ignore_unknown_options": True},
|
|
38
|
+
)
|
|
39
|
+
@instance_identifier_option
|
|
40
|
+
@click.argument("command", nargs=-1, type=click.UNPROCESSED)
|
|
41
|
+
@click.pass_context
|
|
42
|
+
def pgbackrest_proxy(
|
|
43
|
+
context: click.Context, /, command: tuple[str, ...], **kwargs: Any
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Proxy to pgbackrest operations on an instance"""
|
|
46
|
+
s = context.obj.instance.service(models.Service)
|
|
47
|
+
settings = pgbackrest.get_settings(_site.SETTINGS)
|
|
48
|
+
cmd_args = base.make_cmd(s.stanza, settings, *command)
|
|
49
|
+
try:
|
|
50
|
+
subprocess.run(cmd_args, capture_output=False, check=True) # nosec
|
|
51
|
+
except subprocess.CalledProcessError as e:
|
|
52
|
+
raise click.ClickException(str(e)) from e
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command("restore", cls=Command)
|
|
56
|
+
@instance_identifier(nargs=1)
|
|
57
|
+
@click.option("--label", help="Label of backup to restore")
|
|
58
|
+
@click.option("--date", type=click.DateTime(), help="Date of backup to restore")
|
|
59
|
+
@click.pass_obj
|
|
60
|
+
@async_command
|
|
61
|
+
async def instance_restore(
|
|
62
|
+
obj: Obj, instance: system.Instance, label: str | None, date: datetime | None
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Restore PostgreSQL INSTANCE"""
|
|
65
|
+
await postgresql.check_status(instance, types.Status.not_running)
|
|
66
|
+
if label is not None and date is not None:
|
|
67
|
+
raise click.BadArgumentUsage(
|
|
68
|
+
"--label and --date arguments are mutually exclusive"
|
|
69
|
+
) from None
|
|
70
|
+
settings = pgbackrest.get_settings(_site.SETTINGS)
|
|
71
|
+
with obj.lock:
|
|
72
|
+
await base.restore(instance, settings, label=label, date=date)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@click.command("backups", cls=Command)
|
|
76
|
+
@output_format_option
|
|
77
|
+
@instance_identifier(nargs=1)
|
|
78
|
+
@async_command
|
|
79
|
+
async def instance_backups(
|
|
80
|
+
instance: system.Instance, output_format: OutputFormat
|
|
81
|
+
) -> None:
|
|
82
|
+
"""List available backups for INSTANCE"""
|
|
83
|
+
settings = pgbackrest.get_settings(_site.SETTINGS)
|
|
84
|
+
backups = [b async for b in base.iter_backups(instance, settings)]
|
|
85
|
+
if output_format == OutputFormat.json:
|
|
86
|
+
print_json_for([model_dump(b) for b in backups])
|
|
87
|
+
else:
|
|
88
|
+
print_table_for(
|
|
89
|
+
backups, model_dump, title=f"Available backups for instance {instance}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@hookimpl
|
|
94
|
+
def command() -> click.Command:
|
|
95
|
+
return pgbackrest_proxy
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@hookimpl
|
|
99
|
+
def add_instance_commands(group: click.Group) -> None:
|
|
100
|
+
group.add_command(instance_backups)
|
|
101
|
+
group.add_command(instance_restore)
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
|
|
9
|
+
from pglift import pgbackrest, types
|
|
10
|
+
from pglift.models import system
|
|
11
|
+
from pglift.pgbackrest import repo_path
|
|
12
|
+
from pglift.pgbackrest.repo_path import register_if as register_if # noqa: F401
|
|
13
|
+
|
|
14
|
+
from .. import _site, hookimpl
|
|
15
|
+
from ..util import Command, Obj, async_command, instance_identifier
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command("backup", cls=Command)
|
|
19
|
+
@instance_identifier(nargs=1)
|
|
20
|
+
@click.option(
|
|
21
|
+
"--type",
|
|
22
|
+
"backup_type",
|
|
23
|
+
type=click.Choice(types.BACKUP_TYPES),
|
|
24
|
+
default=types.DEFAULT_BACKUP_TYPE,
|
|
25
|
+
help="Backup type",
|
|
26
|
+
)
|
|
27
|
+
@click.pass_obj
|
|
28
|
+
@async_command
|
|
29
|
+
async def instance_backup(
|
|
30
|
+
obj: Obj, instance: system.Instance, backup_type: types.BackupType
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Back up PostgreSQL INSTANCE"""
|
|
33
|
+
settings = pgbackrest.get_settings(_site.SETTINGS)
|
|
34
|
+
with obj.lock:
|
|
35
|
+
await repo_path.backup(instance, settings, type=backup_type)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@hookimpl
|
|
39
|
+
def add_instance_commands(group: click.Group) -> None:
|
|
40
|
+
group.add_command(instance_backup)
|
pglift_cli/pgconf.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import pgtoolkit.conf
|
|
12
|
+
|
|
13
|
+
from pglift import h, hook, instances, postgresql
|
|
14
|
+
from pglift.models import system
|
|
15
|
+
from pglift.types import ConfigChanges, Status
|
|
16
|
+
|
|
17
|
+
from .util import Group, Obj, async_command, instance_identifier_option, pass_instance
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group(cls=Group)
|
|
21
|
+
@instance_identifier_option
|
|
22
|
+
def cli(**kwargs: Any) -> None:
|
|
23
|
+
"""Manage configuration of a PostgreSQL instance."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def show_configuration_changes(
|
|
27
|
+
changes: ConfigChanges, parameters: Iterable[str] | None = None
|
|
28
|
+
) -> None:
|
|
29
|
+
for param, (old, new) in sorted(changes.items()):
|
|
30
|
+
click.secho(f"{param}: {old} -> {new}", err=True, fg="green")
|
|
31
|
+
if parameters is None:
|
|
32
|
+
return
|
|
33
|
+
if unchanged := set(parameters) - set(changes):
|
|
34
|
+
click.secho(
|
|
35
|
+
f"changes in {', '.join(map(repr, sorted(unchanged)))} not applied",
|
|
36
|
+
err=True,
|
|
37
|
+
fg="red",
|
|
38
|
+
)
|
|
39
|
+
click.secho(
|
|
40
|
+
" hint: either these changes have no effect (values already set) "
|
|
41
|
+
"or specified parameters are already defined in an un-managed file "
|
|
42
|
+
"(e.g. 'postgresql.conf')",
|
|
43
|
+
err=True,
|
|
44
|
+
fg="blue",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cli.command("show")
|
|
49
|
+
@click.argument("parameter", nargs=-1)
|
|
50
|
+
@pass_instance
|
|
51
|
+
def show(instance: system.Instance, parameter: tuple[str]) -> None:
|
|
52
|
+
"""Show configuration (all parameters or specified ones).
|
|
53
|
+
|
|
54
|
+
Only uncommented parameters are shown when no PARAMETER is specified. When
|
|
55
|
+
specific PARAMETERs are queried, commented values are also shown.
|
|
56
|
+
"""
|
|
57
|
+
config = instance.config()
|
|
58
|
+
for entry in config.entries.values():
|
|
59
|
+
if parameter:
|
|
60
|
+
if entry.name in parameter:
|
|
61
|
+
if entry.commented:
|
|
62
|
+
click.echo(f"# {entry.name} = {entry.serialize()}")
|
|
63
|
+
else:
|
|
64
|
+
click.echo(f"{entry.name} = {entry.serialize()}")
|
|
65
|
+
elif not entry.commented:
|
|
66
|
+
click.echo(f"{entry.name} = {entry.serialize()}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_configuration_parameters(
|
|
70
|
+
context: click.Context, param: click.Parameter, value: tuple[str]
|
|
71
|
+
) -> dict[str, str]:
|
|
72
|
+
items = {}
|
|
73
|
+
for v in value:
|
|
74
|
+
try:
|
|
75
|
+
key, val = v.split("=", 1)
|
|
76
|
+
except ValueError:
|
|
77
|
+
raise click.BadParameter(v) from None
|
|
78
|
+
items[key] = val
|
|
79
|
+
return items
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@cli.command("set")
|
|
83
|
+
@click.argument(
|
|
84
|
+
"parameters",
|
|
85
|
+
metavar="<PARAMETER>=<VALUE>...",
|
|
86
|
+
nargs=-1,
|
|
87
|
+
callback=validate_configuration_parameters,
|
|
88
|
+
required=True,
|
|
89
|
+
)
|
|
90
|
+
@pass_instance
|
|
91
|
+
@click.pass_obj
|
|
92
|
+
@async_command
|
|
93
|
+
async def set_(obj: Obj, instance: system.Instance, parameters: dict[str, Any]) -> None:
|
|
94
|
+
"""Set configuration items."""
|
|
95
|
+
with obj.lock:
|
|
96
|
+
status = await postgresql.status(instance)
|
|
97
|
+
manifest = await instances._get(instance, status)
|
|
98
|
+
manifest.settings.update(parameters)
|
|
99
|
+
changes = await instances.configure(
|
|
100
|
+
instance, manifest, _is_running=status == Status.running
|
|
101
|
+
)
|
|
102
|
+
show_configuration_changes(changes, parameters.keys())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cli.command("remove")
|
|
106
|
+
@click.argument("parameters", nargs=-1, required=True)
|
|
107
|
+
@pass_instance
|
|
108
|
+
@click.pass_obj
|
|
109
|
+
@async_command
|
|
110
|
+
async def remove(obj: Obj, instance: system.Instance, parameters: tuple[str]) -> None:
|
|
111
|
+
"""Remove configuration items."""
|
|
112
|
+
with obj.lock:
|
|
113
|
+
status = await postgresql.status(instance)
|
|
114
|
+
manifest = await instances._get(instance, status)
|
|
115
|
+
for p in parameters:
|
|
116
|
+
try:
|
|
117
|
+
del manifest.settings[p]
|
|
118
|
+
except KeyError:
|
|
119
|
+
raise click.ClickException(
|
|
120
|
+
f"{p!r} not found in managed configuration"
|
|
121
|
+
) from None
|
|
122
|
+
changes = await instances.configure(
|
|
123
|
+
instance, manifest, _is_running=status == Status.running
|
|
124
|
+
)
|
|
125
|
+
show_configuration_changes(changes, parameters)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@cli.command("edit")
|
|
129
|
+
@pass_instance
|
|
130
|
+
@click.pass_obj
|
|
131
|
+
@async_command
|
|
132
|
+
async def edit(obj: Obj, instance: system.Instance) -> None:
|
|
133
|
+
"""Edit managed configuration."""
|
|
134
|
+
with obj.lock:
|
|
135
|
+
actual_config = hook(
|
|
136
|
+
instance._settings, h.postgresql_editable_conf, instance=instance
|
|
137
|
+
)
|
|
138
|
+
edited = click.edit(text=actual_config)
|
|
139
|
+
if edited is None:
|
|
140
|
+
click.echo("no change", err=True)
|
|
141
|
+
return
|
|
142
|
+
config = pgtoolkit.conf.parse_string(edited)
|
|
143
|
+
values = config.as_dict()
|
|
144
|
+
status = await postgresql.status(instance)
|
|
145
|
+
manifest = await instances._get(instance, status)
|
|
146
|
+
manifest.settings.clear()
|
|
147
|
+
manifest.settings.update(values)
|
|
148
|
+
changes = await instances.configure(
|
|
149
|
+
instance, manifest, _is_running=status == Status.running
|
|
150
|
+
)
|
|
151
|
+
show_configuration_changes(changes)
|
pglift_cli/pm.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pglift.pm import PluginManager as BasePluginManager
|
|
8
|
+
|
|
9
|
+
from . import __name__ as pkgname
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PluginManager(BasePluginManager):
|
|
13
|
+
ns = pkgname
|
|
14
|
+
modules = (
|
|
15
|
+
"patroni",
|
|
16
|
+
"pgbackrest",
|
|
17
|
+
"pgbackrest.repo_path",
|
|
18
|
+
"prometheus",
|
|
19
|
+
)
|
pglift_cli/postgres.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from pglift.cmd import execute_program
|
|
10
|
+
from pglift.exceptions import InstanceNotFound
|
|
11
|
+
from pglift.models import system
|
|
12
|
+
|
|
13
|
+
from . import _site
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def instance_from_qualname(
|
|
17
|
+
context: click.Context, param: click.Parameter, value: str
|
|
18
|
+
) -> system.PostgreSQLInstance:
|
|
19
|
+
try:
|
|
20
|
+
return system.PostgreSQLInstance.from_qualname(value, _site.SETTINGS)
|
|
21
|
+
except (ValueError, InstanceNotFound) as e:
|
|
22
|
+
raise click.BadParameter(str(e), context) from None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.command("postgres", hidden=True)
|
|
26
|
+
@click.argument("instance", callback=instance_from_qualname)
|
|
27
|
+
def cli(instance: system.Instance) -> None:
|
|
28
|
+
"""Start postgres for specified INSTANCE, identified as <version>-<name>."""
|
|
29
|
+
cmd = [str(instance.bindir / "postgres"), "-D", str(instance.datadir)]
|
|
30
|
+
execute_program(cmd)
|
pglift_cli/prometheus.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from functools import partial
|
|
8
|
+
from typing import IO
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from pglift import exceptions, prometheus, task
|
|
13
|
+
from pglift.models import interface
|
|
14
|
+
from pglift.prometheus import impl, models
|
|
15
|
+
from pglift.prometheus import register_if as register_if # noqa: F401
|
|
16
|
+
|
|
17
|
+
from . import _site, hookimpl, model
|
|
18
|
+
from .util import (
|
|
19
|
+
Group,
|
|
20
|
+
Obj,
|
|
21
|
+
OutputFormat,
|
|
22
|
+
async_command,
|
|
23
|
+
dry_run_option,
|
|
24
|
+
foreground_option,
|
|
25
|
+
output_format_option,
|
|
26
|
+
print_argspec,
|
|
27
|
+
print_json_for,
|
|
28
|
+
print_schema,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group("postgres_exporter", cls=Group)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--schema",
|
|
35
|
+
is_flag=True,
|
|
36
|
+
callback=partial(print_schema, model=models.PostgresExporter),
|
|
37
|
+
expose_value=False,
|
|
38
|
+
is_eager=True,
|
|
39
|
+
help="Print the JSON schema of postgres_exporter model and exit.",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--ansible-argspec",
|
|
43
|
+
is_flag=True,
|
|
44
|
+
callback=partial(print_argspec, model=models.PostgresExporter),
|
|
45
|
+
expose_value=False,
|
|
46
|
+
is_eager=True,
|
|
47
|
+
hidden=True,
|
|
48
|
+
help="Print the Ansible argspec of postgres_exporter model and exit.",
|
|
49
|
+
)
|
|
50
|
+
def cli() -> None:
|
|
51
|
+
"""Handle Prometheus postgres_exporter"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cli.command("apply")
|
|
55
|
+
@click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
|
|
56
|
+
@output_format_option
|
|
57
|
+
@dry_run_option
|
|
58
|
+
@click.pass_obj
|
|
59
|
+
@async_command
|
|
60
|
+
async def apply(
|
|
61
|
+
obj: Obj, file: IO[str], output_format: OutputFormat, dry_run: bool
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Apply manifest as a Prometheus postgres_exporter."""
|
|
64
|
+
exporter = models.PostgresExporter.parse_yaml(file)
|
|
65
|
+
if dry_run:
|
|
66
|
+
ret = interface.ApplyResult(change_state=None)
|
|
67
|
+
else:
|
|
68
|
+
settings = prometheus.get_settings(_site.SETTINGS)
|
|
69
|
+
with obj.lock:
|
|
70
|
+
ret = await impl.apply(exporter, _site.SETTINGS, settings)
|
|
71
|
+
if output_format == OutputFormat.json:
|
|
72
|
+
print_json_for(ret)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@cli.command("install")
|
|
76
|
+
@model.as_parameters(models.PostgresExporter, "create")
|
|
77
|
+
@click.pass_obj
|
|
78
|
+
@async_command
|
|
79
|
+
async def install(obj: Obj, postgresexporter: models.PostgresExporter) -> None:
|
|
80
|
+
"""Install the service for a (non-local) instance."""
|
|
81
|
+
settings = prometheus.get_settings(_site.SETTINGS)
|
|
82
|
+
with obj.lock:
|
|
83
|
+
async with task.async_transaction():
|
|
84
|
+
await impl.apply(postgresexporter, _site.SETTINGS, settings)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@cli.command("uninstall")
|
|
88
|
+
@click.argument("name")
|
|
89
|
+
@click.pass_obj
|
|
90
|
+
@async_command
|
|
91
|
+
async def uninstall(obj: Obj, name: str) -> None:
|
|
92
|
+
"""Uninstall the service."""
|
|
93
|
+
with obj.lock:
|
|
94
|
+
await impl.drop(_site.SETTINGS, name)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@cli.command("start")
|
|
98
|
+
@click.argument("name")
|
|
99
|
+
@foreground_option
|
|
100
|
+
@click.pass_obj
|
|
101
|
+
@async_command
|
|
102
|
+
async def start(obj: Obj, name: str, foreground: bool) -> None:
|
|
103
|
+
"""Start postgres_exporter service NAME.
|
|
104
|
+
|
|
105
|
+
The NAME argument is a local identifier for the postgres_exporter
|
|
106
|
+
service. If the service is bound to a local instance, it should be
|
|
107
|
+
<version>-<name>.
|
|
108
|
+
"""
|
|
109
|
+
settings = prometheus.get_settings(_site.SETTINGS)
|
|
110
|
+
with obj.lock:
|
|
111
|
+
service = impl.system_lookup(name, settings)
|
|
112
|
+
if service is None:
|
|
113
|
+
raise exceptions.InstanceNotFound(name)
|
|
114
|
+
await impl.start(_site.SETTINGS, service, foreground=foreground)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@cli.command("stop")
|
|
118
|
+
@click.argument("name")
|
|
119
|
+
@click.pass_obj
|
|
120
|
+
@async_command
|
|
121
|
+
async def stop(obj: Obj, name: str) -> None:
|
|
122
|
+
"""Stop postgres_exporter service NAME.
|
|
123
|
+
|
|
124
|
+
The NAME argument is a local identifier for the postgres_exporter
|
|
125
|
+
service. If the service is bound to a local instance, it should be
|
|
126
|
+
<version>-<name>.
|
|
127
|
+
"""
|
|
128
|
+
settings = prometheus.get_settings(_site.SETTINGS)
|
|
129
|
+
with obj.lock:
|
|
130
|
+
service = impl.system_lookup(name, settings)
|
|
131
|
+
if service is None:
|
|
132
|
+
raise exceptions.InstanceNotFound(name)
|
|
133
|
+
await impl.stop(_site.SETTINGS, service)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@hookimpl
|
|
137
|
+
def command() -> click.Group:
|
|
138
|
+
return cli
|
pglift_cli/role.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from functools import partial
|
|
9
|
+
from typing import IO, Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from pydantic.v1.utils import deep_update
|
|
13
|
+
|
|
14
|
+
from pglift import postgresql, privileges, roles
|
|
15
|
+
from pglift.models import interface, system
|
|
16
|
+
|
|
17
|
+
from . import _site, model
|
|
18
|
+
from .util import (
|
|
19
|
+
Group,
|
|
20
|
+
Obj,
|
|
21
|
+
OutputFormat,
|
|
22
|
+
async_command,
|
|
23
|
+
dry_run_option,
|
|
24
|
+
instance_identifier_option,
|
|
25
|
+
model_dump,
|
|
26
|
+
output_format_option,
|
|
27
|
+
pass_instance,
|
|
28
|
+
print_argspec,
|
|
29
|
+
print_json_for,
|
|
30
|
+
print_schema,
|
|
31
|
+
print_table_for,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def print_role_schema(
|
|
36
|
+
context: click.Context, param: click.Parameter, value: bool
|
|
37
|
+
) -> None:
|
|
38
|
+
return print_schema(context, param, value, model=_site.ROLE_MODEL)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_role_argspec(
|
|
42
|
+
context: click.Context, param: click.Parameter, value: bool
|
|
43
|
+
) -> None:
|
|
44
|
+
print_argspec(context, param, value, model=_site.ROLE_MODEL)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.group("role", cls=Group)
|
|
48
|
+
@instance_identifier_option
|
|
49
|
+
@click.option(
|
|
50
|
+
"--schema",
|
|
51
|
+
is_flag=True,
|
|
52
|
+
callback=print_role_schema,
|
|
53
|
+
expose_value=False,
|
|
54
|
+
is_eager=True,
|
|
55
|
+
help="Print the JSON schema of role model and exit.",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--ansible-argspec",
|
|
59
|
+
is_flag=True,
|
|
60
|
+
callback=print_role_argspec,
|
|
61
|
+
expose_value=False,
|
|
62
|
+
is_eager=True,
|
|
63
|
+
hidden=True,
|
|
64
|
+
help="Print the Ansible argspec of role model and exit.",
|
|
65
|
+
)
|
|
66
|
+
def cli(**kwargs: Any) -> None:
|
|
67
|
+
"""Manage roles."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@cli.command("create")
|
|
71
|
+
@model.as_parameters(_site.ROLE_MODEL, "create")
|
|
72
|
+
@pass_instance
|
|
73
|
+
@click.pass_obj
|
|
74
|
+
@async_command
|
|
75
|
+
async def create(obj: Obj, instance: system.Instance, role: interface.Role) -> None:
|
|
76
|
+
"""Create a role in a PostgreSQL instance"""
|
|
77
|
+
with obj.lock:
|
|
78
|
+
async with postgresql.running(instance):
|
|
79
|
+
if await roles.exists(instance, role.name):
|
|
80
|
+
raise click.ClickException("role already exists")
|
|
81
|
+
await roles.apply(instance, role)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@cli.command("alter") # type: ignore[arg-type]
|
|
85
|
+
@model.as_parameters(_site.ROLE_MODEL, "update", parse_model=False)
|
|
86
|
+
@click.argument("rolname")
|
|
87
|
+
@pass_instance
|
|
88
|
+
@click.pass_obj
|
|
89
|
+
@async_command
|
|
90
|
+
async def alter(
|
|
91
|
+
obj: Obj, instance: system.Instance, rolname: str, **changes: Any
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Alter a role in a PostgreSQL instance"""
|
|
94
|
+
with obj.lock:
|
|
95
|
+
async with postgresql.running(instance):
|
|
96
|
+
values = (await roles.get(instance, rolname)).model_dump(by_alias=True)
|
|
97
|
+
values = deep_update(values, changes)
|
|
98
|
+
altered = _site.ROLE_MODEL.model_validate(values)
|
|
99
|
+
await roles.apply(instance, altered)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@cli.command("apply", hidden=True)
|
|
103
|
+
@click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
|
|
104
|
+
@output_format_option
|
|
105
|
+
@dry_run_option
|
|
106
|
+
@pass_instance
|
|
107
|
+
@click.pass_obj
|
|
108
|
+
@async_command
|
|
109
|
+
async def apply(
|
|
110
|
+
obj: Obj,
|
|
111
|
+
instance: system.Instance,
|
|
112
|
+
file: IO[str],
|
|
113
|
+
output_format: OutputFormat,
|
|
114
|
+
dry_run: bool,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Apply manifest as a role"""
|
|
117
|
+
role = _site.ROLE_MODEL.parse_yaml(file)
|
|
118
|
+
if dry_run:
|
|
119
|
+
ret = interface.ApplyResult(change_state=None)
|
|
120
|
+
else:
|
|
121
|
+
with obj.lock:
|
|
122
|
+
async with postgresql.running(instance):
|
|
123
|
+
ret = await roles.apply(instance, role)
|
|
124
|
+
if output_format == OutputFormat.json:
|
|
125
|
+
print_json_for(ret)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@cli.command("list")
|
|
129
|
+
@output_format_option
|
|
130
|
+
@pass_instance
|
|
131
|
+
@async_command
|
|
132
|
+
async def ls(instance: system.Instance, output_format: OutputFormat) -> None:
|
|
133
|
+
"""List roles in instance"""
|
|
134
|
+
async with postgresql.running(instance):
|
|
135
|
+
rls = await roles.ls(instance)
|
|
136
|
+
if output_format == OutputFormat.json:
|
|
137
|
+
print_json_for([model_dump(r) for r in rls])
|
|
138
|
+
else:
|
|
139
|
+
print_table_for(rls, partial(model_dump, exclude={"pgpass"}))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@cli.command("get")
|
|
143
|
+
@output_format_option
|
|
144
|
+
@click.argument("name")
|
|
145
|
+
@pass_instance
|
|
146
|
+
@async_command
|
|
147
|
+
async def get(
|
|
148
|
+
instance: system.Instance, name: str, output_format: OutputFormat
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Get the description of a role"""
|
|
151
|
+
async with postgresql.running(instance):
|
|
152
|
+
r = await roles.get(instance, name)
|
|
153
|
+
if output_format == OutputFormat.json:
|
|
154
|
+
print_json_for(model_dump(r))
|
|
155
|
+
else:
|
|
156
|
+
print_table_for([r], model_dump, box=None)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@cli.command("drop")
|
|
160
|
+
@model.as_parameters(interface.RoleDropped, "create")
|
|
161
|
+
@pass_instance
|
|
162
|
+
@async_command
|
|
163
|
+
async def drop(instance: system.Instance, roledropped: interface.RoleDropped) -> None:
|
|
164
|
+
"""Drop a role"""
|
|
165
|
+
async with postgresql.running(instance):
|
|
166
|
+
await roles.drop(instance, roledropped)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@cli.command("privileges")
|
|
170
|
+
@click.argument("name")
|
|
171
|
+
@click.option(
|
|
172
|
+
"-d", "--database", "databases", multiple=True, help="Database to inspect"
|
|
173
|
+
)
|
|
174
|
+
@click.option("--default", "defaults", is_flag=True, help="Display default privileges")
|
|
175
|
+
@output_format_option
|
|
176
|
+
@pass_instance
|
|
177
|
+
@async_command
|
|
178
|
+
async def list_privileges(
|
|
179
|
+
instance: system.Instance,
|
|
180
|
+
name: str,
|
|
181
|
+
databases: Sequence[str],
|
|
182
|
+
defaults: bool,
|
|
183
|
+
output_format: OutputFormat,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""List privileges of a role."""
|
|
186
|
+
async with postgresql.running(instance):
|
|
187
|
+
await roles.get(instance, name) # check existence
|
|
188
|
+
try:
|
|
189
|
+
prvlgs = await privileges.get(
|
|
190
|
+
instance, databases=databases, roles=(name,), defaults=defaults
|
|
191
|
+
)
|
|
192
|
+
except ValueError as e:
|
|
193
|
+
raise click.ClickException(str(e)) from None
|
|
194
|
+
if output_format == OutputFormat.json:
|
|
195
|
+
print_json_for([model_dump(p) for p in prvlgs])
|
|
196
|
+
else:
|
|
197
|
+
print_table_for(prvlgs, model_dump)
|