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/instance.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2021 Dalibo
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from functools import partial
|
|
10
|
+
from typing import IO, Any
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from pydantic.v1.utils import deep_update
|
|
14
|
+
|
|
15
|
+
from pglift import (
|
|
16
|
+
async_hooks,
|
|
17
|
+
hooks,
|
|
18
|
+
hookspecs,
|
|
19
|
+
instances,
|
|
20
|
+
postgresql,
|
|
21
|
+
privileges,
|
|
22
|
+
task,
|
|
23
|
+
)
|
|
24
|
+
from pglift.models import interface, system
|
|
25
|
+
from pglift.settings._postgresql import PostgreSQLVersion
|
|
26
|
+
from pglift.types import Status
|
|
27
|
+
|
|
28
|
+
from . import _site
|
|
29
|
+
from . import hookspecs as h
|
|
30
|
+
from . import model
|
|
31
|
+
from .util import (
|
|
32
|
+
Obj,
|
|
33
|
+
OutputFormat,
|
|
34
|
+
PluggableCommandGroup,
|
|
35
|
+
async_command,
|
|
36
|
+
dry_run_option,
|
|
37
|
+
foreground_option,
|
|
38
|
+
instance_identifier,
|
|
39
|
+
model_dump,
|
|
40
|
+
output_format_option,
|
|
41
|
+
print_argspec,
|
|
42
|
+
print_json_for,
|
|
43
|
+
print_schema,
|
|
44
|
+
print_table_for,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class InstanceCommands(PluggableCommandGroup):
|
|
49
|
+
"""Group for 'instance' sub-commands, part of which come from registered
|
|
50
|
+
plugins.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def register_plugin_commands(self, obj: Obj) -> None:
|
|
54
|
+
hooks(_site.PLUGIN_MANAGER, h.add_instance_commands, group=self)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def print_instance_schema(
|
|
58
|
+
context: click.Context, param: click.Parameter, value: bool
|
|
59
|
+
) -> None:
|
|
60
|
+
return print_schema(context, param, value, model=_site.INSTANCE_MODEL)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def print_instance_argspec(
|
|
64
|
+
context: click.Context, param: click.Parameter, value: bool
|
|
65
|
+
) -> None:
|
|
66
|
+
print_argspec(context, param, value, model=_site.INSTANCE_MODEL)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@click.group(cls=InstanceCommands)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--schema",
|
|
72
|
+
is_flag=True,
|
|
73
|
+
callback=print_instance_schema,
|
|
74
|
+
expose_value=False,
|
|
75
|
+
is_eager=True,
|
|
76
|
+
help="Print the JSON schema of instance model and exit.",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--ansible-argspec",
|
|
80
|
+
is_flag=True,
|
|
81
|
+
callback=print_instance_argspec,
|
|
82
|
+
expose_value=False,
|
|
83
|
+
is_eager=True,
|
|
84
|
+
hidden=True,
|
|
85
|
+
help="Print the Ansible argspec of instance model and exit.",
|
|
86
|
+
)
|
|
87
|
+
def cli() -> None:
|
|
88
|
+
"""Manage instances."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@cli.command("create")
|
|
92
|
+
@model.as_parameters(_site.INSTANCE_MODEL, "create")
|
|
93
|
+
@click.option(
|
|
94
|
+
"--drop-on-error/--no-drop-on-error",
|
|
95
|
+
default=True,
|
|
96
|
+
help=(
|
|
97
|
+
"On error, drop partially initialized instance by possibly "
|
|
98
|
+
"rolling back operations (true by default)."
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
@click.pass_obj
|
|
102
|
+
@async_command
|
|
103
|
+
async def create(obj: Obj, instance: interface.Instance, drop_on_error: bool) -> None:
|
|
104
|
+
"""Initialize a PostgreSQL instance"""
|
|
105
|
+
with obj.lock:
|
|
106
|
+
if instances.exists(instance.name, instance.version, _site.SETTINGS):
|
|
107
|
+
raise click.ClickException("instance already exists")
|
|
108
|
+
async with task.async_transaction(drop_on_error):
|
|
109
|
+
await instances.apply(_site.SETTINGS, instance)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@cli.command("alter") # type: ignore[arg-type]
|
|
113
|
+
@instance_identifier(nargs=1)
|
|
114
|
+
@model.as_parameters(_site.INSTANCE_MODEL, "update", parse_model=False)
|
|
115
|
+
@click.pass_obj
|
|
116
|
+
@async_command
|
|
117
|
+
async def alter(obj: Obj, instance: system.Instance, **changes: Any) -> None:
|
|
118
|
+
"""Alter PostgreSQL INSTANCE"""
|
|
119
|
+
with obj.lock:
|
|
120
|
+
status = await postgresql.status(instance)
|
|
121
|
+
manifest = await instances._get(instance, status)
|
|
122
|
+
values = manifest.model_dump(by_alias=True, exclude={"settings"})
|
|
123
|
+
values = deep_update(values, changes)
|
|
124
|
+
altered = _site.INSTANCE_MODEL.model_validate(values)
|
|
125
|
+
await instances.apply(
|
|
126
|
+
_site.SETTINGS, altered, _is_running=status == Status.running
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@cli.command("apply", hidden=True)
|
|
131
|
+
@click.option("-f", "--file", type=click.File("r"), metavar="MANIFEST", required=True)
|
|
132
|
+
@output_format_option
|
|
133
|
+
@dry_run_option
|
|
134
|
+
@click.pass_obj
|
|
135
|
+
@async_command
|
|
136
|
+
async def apply(
|
|
137
|
+
obj: Obj, file: IO[str], output_format: OutputFormat, dry_run: bool
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Apply manifest as a PostgreSQL instance"""
|
|
140
|
+
instance = _site.INSTANCE_MODEL.parse_yaml(file)
|
|
141
|
+
if dry_run:
|
|
142
|
+
ret = interface.InstanceApplyResult(change_state=None)
|
|
143
|
+
else:
|
|
144
|
+
with obj.lock:
|
|
145
|
+
ret = await instances.apply(_site.SETTINGS, instance)
|
|
146
|
+
if output_format == OutputFormat.json:
|
|
147
|
+
print_json_for(ret)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@cli.command("promote")
|
|
151
|
+
@instance_identifier(nargs=1)
|
|
152
|
+
@click.pass_obj
|
|
153
|
+
@async_command
|
|
154
|
+
async def promote(obj: Obj, instance: system.Instance) -> None:
|
|
155
|
+
"""Promote standby PostgreSQL INSTANCE"""
|
|
156
|
+
with obj.lock:
|
|
157
|
+
await instances.promote(instance)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@cli.command("get")
|
|
161
|
+
@output_format_option
|
|
162
|
+
@instance_identifier(nargs=1)
|
|
163
|
+
@async_command
|
|
164
|
+
async def get(instance: system.Instance, output_format: OutputFormat) -> None:
|
|
165
|
+
"""Get the description of PostgreSQL INSTANCE.
|
|
166
|
+
|
|
167
|
+
Unless --output-format is specified, 'settings' and 'state' fields are not
|
|
168
|
+
shown as well as 'standby' information if INSTANCE is not a standby.
|
|
169
|
+
"""
|
|
170
|
+
i = await instances.get(instance)
|
|
171
|
+
if output_format == OutputFormat.json:
|
|
172
|
+
print_json_for(model_dump(i))
|
|
173
|
+
else:
|
|
174
|
+
exclude = {
|
|
175
|
+
"settings",
|
|
176
|
+
"state",
|
|
177
|
+
"data_directory",
|
|
178
|
+
"wal_directory",
|
|
179
|
+
"powa",
|
|
180
|
+
}
|
|
181
|
+
if not instance.standby:
|
|
182
|
+
exclude.add("standby")
|
|
183
|
+
print_table_for([i], partial(model_dump, exclude=exclude), box=None)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@cli.command("list")
|
|
187
|
+
@click.option(
|
|
188
|
+
"--version",
|
|
189
|
+
type=click.Choice(list(PostgreSQLVersion)),
|
|
190
|
+
help="Only list instances of specified version.",
|
|
191
|
+
)
|
|
192
|
+
@output_format_option
|
|
193
|
+
@async_command
|
|
194
|
+
async def ls(version: PostgreSQLVersion | None, output_format: OutputFormat) -> None:
|
|
195
|
+
"""List the available instances"""
|
|
196
|
+
items = [i async for i in instances.ls(_site.SETTINGS, version=version)]
|
|
197
|
+
if output_format == OutputFormat.json:
|
|
198
|
+
print_json_for([model_dump(m) for m in items])
|
|
199
|
+
else:
|
|
200
|
+
print_table_for(items, model_dump)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@cli.command("drop")
|
|
204
|
+
@instance_identifier(nargs=-1)
|
|
205
|
+
@click.pass_obj
|
|
206
|
+
@async_command
|
|
207
|
+
async def drop(obj: Obj, instance: tuple[system.Instance, ...]) -> None:
|
|
208
|
+
"""Drop PostgreSQL INSTANCE"""
|
|
209
|
+
with obj.lock:
|
|
210
|
+
for i in instance:
|
|
211
|
+
await instances.drop(i)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@cli.command("status")
|
|
215
|
+
@instance_identifier(nargs=1)
|
|
216
|
+
@click.pass_context
|
|
217
|
+
@async_command
|
|
218
|
+
async def status(context: click.Context, instance: system.Instance) -> None:
|
|
219
|
+
"""Check the status of instance and all satellite components.
|
|
220
|
+
|
|
221
|
+
Output the status string value ('running', 'not running') for each component.
|
|
222
|
+
If not all services are running, the command exit code will be 3.
|
|
223
|
+
"""
|
|
224
|
+
exit_code = Status.running.value
|
|
225
|
+
results = list(
|
|
226
|
+
filter(
|
|
227
|
+
None,
|
|
228
|
+
await async_hooks(
|
|
229
|
+
instance._settings, hookspecs.instance_status, instance=instance
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
for status, component in reversed(results):
|
|
234
|
+
if status != Status.running:
|
|
235
|
+
exit_code = Status.not_running.value
|
|
236
|
+
click.echo(f"{component}: {status.name.replace('_', ' ')}")
|
|
237
|
+
context.exit(exit_code)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@cli.command("start")
|
|
241
|
+
@instance_identifier(nargs=-1)
|
|
242
|
+
@foreground_option
|
|
243
|
+
@click.option("--all", "all_instances", is_flag=True, help="Start all instances.")
|
|
244
|
+
@click.pass_obj
|
|
245
|
+
@async_command
|
|
246
|
+
async def start(
|
|
247
|
+
obj: Obj,
|
|
248
|
+
instance: tuple[system.Instance, ...],
|
|
249
|
+
foreground: bool,
|
|
250
|
+
all_instances: bool,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Start PostgreSQL INSTANCE"""
|
|
253
|
+
if foreground and len(instance) != 1:
|
|
254
|
+
raise click.UsageError(
|
|
255
|
+
"only one INSTANCE argument may be given with --foreground"
|
|
256
|
+
)
|
|
257
|
+
with obj.lock:
|
|
258
|
+
for i in instance:
|
|
259
|
+
await instances.start(i, foreground=foreground)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@cli.command("stop")
|
|
263
|
+
@instance_identifier(nargs=-1)
|
|
264
|
+
@click.option("--all", "all_instances", is_flag=True, help="Stop all instances.")
|
|
265
|
+
@click.pass_obj
|
|
266
|
+
@async_command
|
|
267
|
+
async def stop(
|
|
268
|
+
obj: Obj, instance: tuple[system.Instance, ...], all_instances: bool
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Stop PostgreSQL INSTANCE"""
|
|
271
|
+
with obj.lock:
|
|
272
|
+
for i in instance:
|
|
273
|
+
await instances.stop(i)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@cli.command("reload")
|
|
277
|
+
@instance_identifier(nargs=-1)
|
|
278
|
+
@click.option("--all", "all_instances", is_flag=True, help="Reload all instances.")
|
|
279
|
+
@click.pass_obj
|
|
280
|
+
@async_command
|
|
281
|
+
async def reload(
|
|
282
|
+
obj: Obj, instance: tuple[system.Instance, ...], all_instances: bool
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Reload PostgreSQL INSTANCE"""
|
|
285
|
+
with obj.lock:
|
|
286
|
+
for i in instance:
|
|
287
|
+
await instances.reload(i)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@cli.command("restart")
|
|
291
|
+
@instance_identifier(nargs=-1)
|
|
292
|
+
@click.option("--all", "all_instances", is_flag=True, help="Restart all instances.")
|
|
293
|
+
@click.pass_obj
|
|
294
|
+
@async_command
|
|
295
|
+
async def restart(
|
|
296
|
+
obj: Obj, instance: tuple[system.Instance, ...], all_instances: bool
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Restart PostgreSQL INSTANCE"""
|
|
299
|
+
with obj.lock:
|
|
300
|
+
for i in instance:
|
|
301
|
+
await instances.restart(i)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@cli.command("exec")
|
|
305
|
+
@instance_identifier(nargs=1, required=True)
|
|
306
|
+
@click.argument("command", required=True, nargs=-1, type=click.UNPROCESSED)
|
|
307
|
+
def exec(instance: system.Instance, command: tuple[str, ...]) -> None:
|
|
308
|
+
"""Execute command in the libpq environment for PostgreSQL INSTANCE.
|
|
309
|
+
|
|
310
|
+
COMMAND parts may need to be prefixed with -- to separate them from
|
|
311
|
+
options when confusion arises.
|
|
312
|
+
"""
|
|
313
|
+
instances.exec(instance, command)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_shell(context: click.Context, param: click.Parameter, value: str | None) -> str:
|
|
317
|
+
if value is None:
|
|
318
|
+
try:
|
|
319
|
+
value = os.environ["SHELL"]
|
|
320
|
+
except KeyError:
|
|
321
|
+
raise click.UsageError(
|
|
322
|
+
f"SHELL environment variable not found; try to use {'/'.join(param.opts)} option"
|
|
323
|
+
) from None
|
|
324
|
+
return value
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@cli.command("shell")
|
|
328
|
+
@instance_identifier(nargs=1)
|
|
329
|
+
@click.option(
|
|
330
|
+
"--shell",
|
|
331
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
332
|
+
callback=get_shell,
|
|
333
|
+
help="Path to shell executable",
|
|
334
|
+
)
|
|
335
|
+
def shell(instance: system.Instance, shell: str) -> None:
|
|
336
|
+
"""Start a shell with instance environment.
|
|
337
|
+
|
|
338
|
+
Unless --shell option is specified, the $SHELL environment variable is
|
|
339
|
+
used to guess which shell executable to use.
|
|
340
|
+
"""
|
|
341
|
+
env = instances.env_for(instance, path=True)
|
|
342
|
+
click.echo(f"Starting {shell!r} with {instance} environment")
|
|
343
|
+
os.execle(shell, shell, os.environ | env) # nosec
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@cli.command("env")
|
|
347
|
+
@instance_identifier(nargs=1)
|
|
348
|
+
@output_format_option
|
|
349
|
+
def env(instance: system.Instance, output_format: OutputFormat) -> None:
|
|
350
|
+
"""Output environment variables suitable to handle to PostgreSQL INSTANCE.
|
|
351
|
+
|
|
352
|
+
This can be injected in shell using:
|
|
353
|
+
|
|
354
|
+
export $(pglift instance env myinstance)
|
|
355
|
+
"""
|
|
356
|
+
instance_env = instances.env_for(instance, path=True)
|
|
357
|
+
if output_format == OutputFormat.json:
|
|
358
|
+
print_json_for(instance_env)
|
|
359
|
+
else:
|
|
360
|
+
for key, value in sorted(instance_env.items()):
|
|
361
|
+
click.echo(f"{key}={value}")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@cli.command("logs")
|
|
365
|
+
@click.option("--follow/--no-follow", "-f/", default=False, help="Follow log output.")
|
|
366
|
+
@instance_identifier(nargs=1)
|
|
367
|
+
def logs(instance: system.Instance, follow: bool) -> None:
|
|
368
|
+
"""Output PostgreSQL logs of INSTANCE.
|
|
369
|
+
|
|
370
|
+
This assumes that the PostgreSQL instance is configured to use file-based
|
|
371
|
+
logging (i.e. log_destination amongst 'stderr' or 'csvlog').
|
|
372
|
+
"""
|
|
373
|
+
if follow:
|
|
374
|
+
logstream = postgresql.logs(instance)
|
|
375
|
+
else:
|
|
376
|
+
logstream = postgresql.logs(instance, timeout=0)
|
|
377
|
+
try:
|
|
378
|
+
for line in logstream:
|
|
379
|
+
click.echo(line, nl=False)
|
|
380
|
+
except TimeoutError:
|
|
381
|
+
pass
|
|
382
|
+
except FileNotFoundError as e:
|
|
383
|
+
raise click.ClickException(str(e)) from None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@cli.command("privileges")
|
|
387
|
+
@instance_identifier(nargs=1)
|
|
388
|
+
@click.option(
|
|
389
|
+
"-d",
|
|
390
|
+
"--database",
|
|
391
|
+
"databases",
|
|
392
|
+
multiple=True,
|
|
393
|
+
help="Database to inspect. When not provided, all databases are inspected.",
|
|
394
|
+
)
|
|
395
|
+
@click.option("-r", "--role", "roles", multiple=True, help="Role to inspect")
|
|
396
|
+
@click.option("--default", "defaults", is_flag=True, help="Display default privileges")
|
|
397
|
+
@output_format_option
|
|
398
|
+
@async_command
|
|
399
|
+
async def list_privileges(
|
|
400
|
+
instance: system.Instance,
|
|
401
|
+
databases: Sequence[str],
|
|
402
|
+
roles: Sequence[str],
|
|
403
|
+
defaults: bool,
|
|
404
|
+
output_format: OutputFormat,
|
|
405
|
+
) -> None:
|
|
406
|
+
"""List privileges on INSTANCE's databases."""
|
|
407
|
+
async with postgresql.running(instance):
|
|
408
|
+
try:
|
|
409
|
+
prvlgs = await privileges.get(
|
|
410
|
+
instance, databases=databases, roles=roles, defaults=defaults
|
|
411
|
+
)
|
|
412
|
+
except ValueError as e:
|
|
413
|
+
raise click.ClickException(str(e)) from None
|
|
414
|
+
if output_format == OutputFormat.json:
|
|
415
|
+
print_json_for([model_dump(p) for p in prvlgs])
|
|
416
|
+
else:
|
|
417
|
+
if defaults:
|
|
418
|
+
title = f"Default privileges on instance {instance}"
|
|
419
|
+
else:
|
|
420
|
+
title = f"Privileges on instance {instance}"
|
|
421
|
+
print_table_for(prvlgs, model_dump, title=title)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@cli.command("upgrade")
|
|
425
|
+
@instance_identifier(nargs=1)
|
|
426
|
+
@click.option(
|
|
427
|
+
"--version",
|
|
428
|
+
"newversion",
|
|
429
|
+
type=click.Choice(list(PostgreSQLVersion)),
|
|
430
|
+
help="PostgreSQL version of the new instance (default to site-configured value).",
|
|
431
|
+
)
|
|
432
|
+
@click.option(
|
|
433
|
+
"--name", "newname", help="Name of the new instance (default to old instance name)."
|
|
434
|
+
)
|
|
435
|
+
@click.option(
|
|
436
|
+
"--port", required=False, type=click.INT, help="Port of the new instance."
|
|
437
|
+
)
|
|
438
|
+
@click.option(
|
|
439
|
+
"--jobs",
|
|
440
|
+
required=False,
|
|
441
|
+
type=click.INT,
|
|
442
|
+
help="Number of simultaneous processes or threads to use (from pg_upgrade).",
|
|
443
|
+
)
|
|
444
|
+
@click.pass_obj
|
|
445
|
+
@async_command
|
|
446
|
+
async def upgrade(
|
|
447
|
+
obj: Obj,
|
|
448
|
+
instance: system.Instance,
|
|
449
|
+
newversion: PostgreSQLVersion | None,
|
|
450
|
+
newname: str | None,
|
|
451
|
+
port: int | None,
|
|
452
|
+
jobs: int | None,
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Upgrade INSTANCE using pg_upgrade"""
|
|
455
|
+
with obj.lock:
|
|
456
|
+
await postgresql.check_status(instance, Status.not_running)
|
|
457
|
+
async with task.async_transaction():
|
|
458
|
+
new_instance = await instances.upgrade(
|
|
459
|
+
instance, version=newversion, name=newname, port=port, jobs=jobs
|
|
460
|
+
)
|
|
461
|
+
await instances.start(new_instance)
|