ptctools 0.1.1__py3-none-any.whl → 0.2.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.
ptctools/volume.py CHANGED
@@ -31,6 +31,7 @@ logger = logging.getLogger(__name__)
31
31
 
32
32
  class VolumeError(PortainerError):
33
33
  """Exception for volume operations."""
34
+
34
35
  pass
35
36
 
36
37
 
@@ -47,7 +48,7 @@ def backup_volume(
47
48
  passphrase: str,
48
49
  ) -> None:
49
50
  """Backup a single volume to S3 using Duplicati CLI.
50
-
51
+
51
52
  Raises:
52
53
  VolumeError: If backup fails
53
54
  """
@@ -56,26 +57,30 @@ def backup_volume(
56
57
  click.echo(f"Backing up {volume_name} using Duplicati...")
57
58
 
58
59
  # Common options for both repair and backup
59
- common_opts = " ".join([
60
- f"--aws-access-key-id={s3_access_key}",
61
- f"--aws-secret-access-key={s3_secret_key}",
62
- f"--passphrase={passphrase}" if passphrase else "--no-encryption",
63
- "--dbpath=/tmp/duplicati.sqlite",
64
- ])
60
+ common_opts = " ".join(
61
+ [
62
+ f"--aws-access-key-id={s3_access_key}",
63
+ f"--aws-secret-access-key={s3_secret_key}",
64
+ f"--passphrase={passphrase}" if passphrase else "--no-encryption",
65
+ "--dbpath=/tmp/duplicati.sqlite",
66
+ ]
67
+ )
65
68
 
66
69
  # Repair command to rebuild local database from remote
67
70
  repair_cmd = f"duplicati-cli repair '{s3_dest}' {common_opts}"
68
71
 
69
72
  # Backup command
70
- backup_cmd = " ".join([
71
- "duplicati-cli",
72
- "backup",
73
- f"'{s3_dest}'",
74
- "/data",
75
- common_opts,
76
- f"--keep-versions={keep_versions}",
77
- "--retention-policy=",
78
- ])
73
+ backup_cmd = " ".join(
74
+ [
75
+ "duplicati-cli",
76
+ "backup",
77
+ f"'{s3_dest}'",
78
+ "/data",
79
+ common_opts,
80
+ f"--keep-versions={keep_versions}",
81
+ "--retention-policy=",
82
+ ]
83
+ )
79
84
 
80
85
  # Run repair first (rebuilds local DB from remote), then backup
81
86
  # Use ; instead of && because repair may return non-zero for warnings
@@ -83,10 +88,10 @@ def backup_volume(
83
88
 
84
89
  try:
85
90
  client = get_portainer_docker_client(portainer_url, api_key, endpoint_id)
86
-
91
+
87
92
  # Pull image
88
93
  # check if image unused then remove image handled by run_container
89
-
94
+
90
95
  # Run container
91
96
  exit_code, logs = run_container(
92
97
  client=client,
@@ -134,10 +139,10 @@ def restore_volume(
134
139
  restore_time: str | None,
135
140
  ) -> None:
136
141
  """Restore a single volume from S3 using Duplicati CLI.
137
-
142
+
138
143
  Args:
139
144
  s3_path: S3 path to restore from (used directly, no modification).
140
-
145
+
141
146
  Raises:
142
147
  VolumeError: If restore fails
143
148
  """
@@ -167,7 +172,7 @@ def restore_volume(
167
172
 
168
173
  try:
169
174
  client = get_portainer_docker_client(portainer_url, api_key, endpoint_id)
170
-
175
+
171
176
  # Run container
172
177
  exit_code, logs = run_container(
173
178
  client=client,
@@ -181,7 +186,9 @@ def restore_volume(
181
186
  raise VolumeError(f"Restore failed: {e}") from e
182
187
 
183
188
  # Success if exit_code == 0, or exit_code == 2 with no files needing restore (already up-to-date)
184
- already_up_to_date = exit_code == 2 and logs and "0 files need to be restored" in logs
189
+ already_up_to_date = (
190
+ exit_code == 2 and logs and "0 files need to be restored" in logs
191
+ )
185
192
  if exit_code != 0 and not already_up_to_date:
186
193
  click.echo(f" ✗ Restore failed with exit code {exit_code}")
187
194
  if logs:
@@ -204,7 +211,13 @@ def cli():
204
211
 
205
212
 
206
213
  @cli.command()
207
- @click.option("--url", "-u", required=True, help="Portainer base URL")
214
+ @click.option(
215
+ "--url",
216
+ "-u",
217
+ envvar="PORTAINER_URL",
218
+ required=True,
219
+ help="Portainer base URL (or PORTAINER_URL env var)",
220
+ )
208
221
  @click.option(
209
222
  "--volumes", "-v", required=True, help="Comma-separated list of volume names"
210
223
  )
@@ -216,9 +229,7 @@ def cli():
216
229
  )
217
230
  @click.option("--s3-endpoint", help="S3/MinIO endpoint URL (or S3_ENDPOINT env var)")
218
231
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
219
- @click.option(
220
- "--keep-versions", type=int, default=7, help="Keep N versions of backups"
221
- )
232
+ @click.option("--keep-versions", type=int, default=7, help="Keep N versions of backups")
222
233
  @click.option("--passphrase", type=str, default="", help="Encryption passphrase")
223
234
  def backup(
224
235
  url: str,
@@ -279,7 +290,9 @@ def backup(
279
290
  )
280
291
  success_count += 1
281
292
  except VolumeError as e:
282
- logger.error("Volume backup failed for '%s': %s\n%s", vol, e, traceback.format_exc())
293
+ logger.error(
294
+ "Volume backup failed for '%s': %s\n%s", vol, e, traceback.format_exc()
295
+ )
283
296
  click.echo()
284
297
 
285
298
  click.echo("=== Volume backup complete ===")
@@ -297,11 +310,11 @@ def copy_volume(
297
310
  team_id: int | None = None,
298
311
  ) -> None:
299
312
  """Copy data from one volume to another.
300
-
313
+
301
314
  Args:
302
315
  ownership: 'copy' (from source), 'private', 'team', or 'public'
303
316
  team_id: Required if ownership is 'team'
304
-
317
+
305
318
  Raises:
306
319
  VolumeError: If copy fails
307
320
  """
@@ -309,7 +322,7 @@ def copy_volume(
309
322
 
310
323
  try:
311
324
  client = get_portainer_docker_client(portainer_url, api_key, endpoint_id)
312
-
325
+
313
326
  # Get source volume info for driver/labels
314
327
  try:
315
328
  source_vol = client.volumes.get(source_volume)
@@ -317,7 +330,7 @@ def copy_volume(
317
330
  except docker.errors.NotFound:
318
331
  click.echo(f" ✗ Source volume '{source_volume}' not found")
319
332
  raise VolumeError(f"Source volume '{source_volume}' not found")
320
-
333
+
321
334
  # Check if dest volume exists, create if not
322
335
  try:
323
336
  client.volumes.get(dest_volume)
@@ -326,11 +339,13 @@ def copy_volume(
326
339
  driver = source_info.get("Driver", "local")
327
340
  labels = source_info.get("Labels", {})
328
341
  try:
329
- client.volumes.create(name=dest_volume, driver=driver, labels=labels or {})
342
+ client.volumes.create(
343
+ name=dest_volume, driver=driver, labels=labels or {}
344
+ )
330
345
  except docker.errors.APIError as e:
331
346
  click.echo(f" ✗ Failed to create destination volume")
332
347
  raise VolumeError(f"Failed to create destination volume: {e}") from e
333
-
348
+
334
349
  # Copy data using busybox
335
350
  copy_cmd = "cp -a /source/. /dest/"
336
351
  exit_code, logs = run_container(
@@ -366,7 +381,9 @@ def copy_volume(
366
381
  if action == "copied":
367
382
  click.echo(f" ✓ Permissions copied from source")
368
383
  elif action == "skipped":
369
- click.echo(f" ✓ Permissions preserved (destination already has permissions)")
384
+ click.echo(
385
+ f" ✓ Permissions preserved (destination already has permissions)"
386
+ )
370
387
  except ResourceControlError as e:
371
388
  click.echo(f" ⚠ Warning: Failed to copy permissions - {e}")
372
389
  else:
@@ -392,7 +409,7 @@ def copy_s3_to_volume(
392
409
  s3_secret_key: str,
393
410
  ) -> None:
394
411
  """Copy files from S3 to a volume using mc (MinIO Client).
395
-
412
+
396
413
  Raises:
397
414
  VolumeError: If copy fails
398
415
  """
@@ -400,7 +417,7 @@ def copy_s3_to_volume(
400
417
 
401
418
  try:
402
419
  client = get_portainer_docker_client(portainer_url, api_key, endpoint_id)
403
-
420
+
404
421
  # Create destination volume if it doesn't exist
405
422
  try:
406
423
  client.volumes.get(volume_name)
@@ -414,10 +431,12 @@ def copy_s3_to_volume(
414
431
 
415
432
  # Build mc commands: configure alias and copy
416
433
  s3_source = f"s3/{s3_bucket}/{s3_path}" if s3_path else f"s3/{s3_bucket}"
417
- mc_cmd = " && ".join([
418
- f"mc alias set s3 {s3_endpoint} {s3_access_key} {s3_secret_key}",
419
- f"mc cp --recursive {s3_source}/ /data/",
420
- ])
434
+ mc_cmd = " && ".join(
435
+ [
436
+ f"mc alias set s3 {s3_endpoint} {s3_access_key} {s3_secret_key}",
437
+ f"mc cp --recursive {s3_source}/ /data/",
438
+ ]
439
+ )
421
440
 
422
441
  exit_code, logs = run_container(
423
442
  client=client,
@@ -452,7 +471,7 @@ def copy_volume_to_s3(
452
471
  s3_secret_key: str,
453
472
  ) -> None:
454
473
  """Copy files from a volume to S3 using mc (MinIO Client).
455
-
474
+
456
475
  Raises:
457
476
  VolumeError: If copy fails
458
477
  """
@@ -460,13 +479,15 @@ def copy_volume_to_s3(
460
479
 
461
480
  try:
462
481
  client = get_portainer_docker_client(portainer_url, api_key, endpoint_id)
463
-
482
+
464
483
  # Build mc commands: configure alias and copy
465
484
  s3_dest = f"s3/{s3_bucket}/{s3_path}" if s3_path else f"s3/{s3_bucket}"
466
- mc_cmd = " && ".join([
467
- f"mc alias set s3 {s3_endpoint} {s3_access_key} {s3_secret_key}",
468
- f"mc cp --recursive /data/ {s3_dest}/",
469
- ])
485
+ mc_cmd = " && ".join(
486
+ [
487
+ f"mc alias set s3 {s3_endpoint} {s3_access_key} {s3_secret_key}",
488
+ f"mc cp --recursive /data/ {s3_dest}/",
489
+ ]
490
+ )
470
491
 
471
492
  exit_code, logs = run_container(
472
493
  client=client,
@@ -490,7 +511,13 @@ def copy_volume_to_s3(
490
511
 
491
512
 
492
513
  @cli.command()
493
- @click.option("--url", "-u", required=True, help="Portainer base URL")
514
+ @click.option(
515
+ "--url",
516
+ "-u",
517
+ envvar="PORTAINER_URL",
518
+ required=True,
519
+ help="Portainer base URL (or PORTAINER_URL env var)",
520
+ )
494
521
  @click.argument("source")
495
522
  @click.argument("dest")
496
523
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
@@ -516,13 +543,18 @@ def cp(
516
543
  SOURCE and DEST can be volume names or S3 URIs.
517
544
 
518
545
  Examples:
519
- ptctools volume cp source dest
520
- ptctools volume cp source dest --ownership team
521
- ptctools volume cp source dest --ownership private
522
- ptctools volume cp s3://bucket/path volume_a
523
- ptctools volume cp volume_a s3://bucket/path
546
+ ptctools docker volume cp source dest
547
+ ptctools docker volume cp source dest --ownership team
548
+ ptctools docker volume cp source dest --ownership private
549
+ ptctools docker volume cp s3://bucket/path volume_a
550
+ ptctools docker volume cp volume_a s3://bucket/path
524
551
  """
525
- from ptctools._s3 import is_s3_uri, parse_s3_uri, get_s3_endpoint, get_s3_credentials
552
+ from ptctools._s3 import (
553
+ is_s3_uri,
554
+ parse_s3_uri,
555
+ get_s3_endpoint,
556
+ get_s3_credentials,
557
+ )
526
558
 
527
559
  access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
528
560
  if not access_token:
@@ -535,7 +567,10 @@ def cp(
535
567
 
536
568
  # Both are S3 - not supported
537
569
  if source_is_s3 and dest_is_s3:
538
- click.echo("Error: Cannot copy directly between S3 locations. Use volume as intermediate.", err=True)
570
+ click.echo(
571
+ "Error: Cannot copy directly between S3 locations. Use volume as intermediate.",
572
+ err=True,
573
+ )
539
574
  sys.exit(1)
540
575
 
541
576
  # Volume to Volume copy
@@ -654,7 +689,13 @@ def cp(
654
689
 
655
690
 
656
691
  @cli.command()
657
- @click.option("--url", "-u", required=True, help="Portainer base URL")
692
+ @click.option(
693
+ "--url",
694
+ "-u",
695
+ envvar="PORTAINER_URL",
696
+ required=True,
697
+ help="Portainer base URL (or PORTAINER_URL env var)",
698
+ )
658
699
  @click.argument("volume")
659
700
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
660
701
  @click.option("--force", "-f", is_flag=True, help="Force removal even if in use")
@@ -670,7 +711,7 @@ def rm(
670
711
 
671
712
  VOLUME is the volume name to remove.
672
713
 
673
- Example: ptctools volume rm volume_a
714
+ Example: ptctools docker volume rm volume_a
674
715
  """
675
716
  access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
676
717
  if not access_token:
@@ -702,7 +743,13 @@ def rm(
702
743
 
703
744
 
704
745
  @cli.command()
705
- @click.option("--url", "-u", required=True, help="Portainer base URL")
746
+ @click.option(
747
+ "--url",
748
+ "-u",
749
+ envvar="PORTAINER_URL",
750
+ required=True,
751
+ help="Portainer base URL (or PORTAINER_URL env var)",
752
+ )
706
753
  @click.argument("old_name")
707
754
  @click.argument("new_name")
708
755
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
@@ -719,7 +766,7 @@ def rename(
719
766
  OLD_NAME is the current volume name.
720
767
  NEW_NAME is the new volume name.
721
768
 
722
- Example: ptctools volume rename volume_a volume_b
769
+ Example: ptctools docker volume rename volume_a volume_b
723
770
  """
724
771
  access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
725
772
  if not access_token:
@@ -729,7 +776,9 @@ def rename(
729
776
  portainer_url = url.rstrip("/")
730
777
 
731
778
  if not yes:
732
- if not click.confirm(f"Rename '{old_name}' to '{new_name}'? This will copy data and delete the original."):
779
+ if not click.confirm(
780
+ f"Rename '{old_name}' to '{new_name}'? This will copy data and delete the original."
781
+ ):
733
782
  click.echo("Aborted.")
734
783
  sys.exit(0)
735
784
 
@@ -741,7 +790,9 @@ def rename(
741
790
  try:
742
791
  copy_volume(portainer_url, access_token, endpoint_id, old_name, new_name)
743
792
  except VolumeError as e:
744
- logger.error("Volume rename failed during copy: %s\n%s", e, traceback.format_exc())
793
+ logger.error(
794
+ "Volume rename failed during copy: %s\n%s", e, traceback.format_exc()
795
+ )
745
796
  click.echo(" ✗ Failed to copy data. Original volume unchanged.", err=True)
746
797
  sys.exit(1)
747
798
 
@@ -756,9 +807,13 @@ def rename(
756
807
  except docker.errors.NotFound:
757
808
  click.echo(f" ✓ Volume '{old_name}' removed") # Already deleted
758
809
  except docker.errors.APIError as e:
759
- logger.error("Volume rename failed during delete: %s\n%s", e, traceback.format_exc())
810
+ logger.error(
811
+ "Volume rename failed during delete: %s\n%s", e, traceback.format_exc()
812
+ )
760
813
  click.echo(f" ⚠ Warning: Failed to remove old volume '{old_name}'", err=True)
761
- click.echo(f" Data has been copied to '{new_name}'. Please remove '{old_name}' manually.")
814
+ click.echo(
815
+ f" Data has been copied to '{new_name}'. Please remove '{old_name}' manually."
816
+ )
762
817
  sys.exit(1)
763
818
 
764
819
  click.echo()
@@ -767,7 +822,13 @@ def rename(
767
822
 
768
823
 
769
824
  @cli.command()
770
- @click.option("--url", "-u", required=True, help="Portainer base URL")
825
+ @click.option(
826
+ "--url",
827
+ "-u",
828
+ envvar="PORTAINER_URL",
829
+ required=True,
830
+ help="Portainer base URL (or PORTAINER_URL env var)",
831
+ )
771
832
  @click.option(
772
833
  "--volume",
773
834
  "-v",
@@ -830,7 +891,10 @@ def restore(
830
891
  # Use the first path segment as volume name
831
892
  volume_name = s3_path.split("/")[0]
832
893
  else:
833
- click.echo("Error: No volume specified. Use --volume or include path in input URI", err=True)
894
+ click.echo(
895
+ "Error: No volume specified. Use --volume or include path in input URI",
896
+ err=True,
897
+ )
834
898
  sys.exit(1)
835
899
 
836
900
  passphrase = passphrase or os.environ.get("DUPLICATI_PASSPHRASE", "")
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: ptctools
3
+ Version: 0.2.0
4
+ Summary: Portainer Client Tools - CLI for managing Portainer from client
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: docker==7.1.0
9
+ Requires-Dist: pydantic>=2.12.5
10
+ Requires-Dist: python-dateutil>=2.9.0.post0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # ptctools - Portainer Client Tools
16
+
17
+ CLI for managing Portainer stacks, volume backups, and database operations.
18
+
19
+ > **Note:** Only tested on Portainer 2.33.6
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # From Git repository
25
+ uv tool install git+https://github.com/tamntlib/ptctools.git
26
+ # or
27
+ uv tool install ptctools --from git+https://github.com/tamntlib/ptctools.git
28
+
29
+ # From local path
30
+ uv tool install openapi-generator-cli==7.19.0
31
+ openapi-generator-cli generate \
32
+ -i portainer_openapi.yml \
33
+ -g python \
34
+ -o ./src/ptctools/portainer_client \
35
+ --skip-validate-spec \
36
+ --additional-properties=generateSourceCodeOnly=true
37
+ uv tool install . --no-cache --reinstall
38
+ ```
39
+
40
+ ## Environment Variables
41
+
42
+ | Variable | Required | Description |
43
+ |----------|----------|-------------|
44
+ | `PORTAINER_URL` | Yes* | Portainer base URL (can also use `-u` flag) |
45
+ | `PORTAINER_ACCESS_TOKEN` | Yes | Portainer API key |
46
+ | `S3_ACCESS_KEY` | For S3 | S3 access key |
47
+ | `S3_SECRET_KEY` | For S3 | S3 secret key |
48
+ | `S3_ENDPOINT` | For S3 | S3/MinIO endpoint URL |
49
+ | `DUPLICATI_PASSPHRASE` | No | Backup encryption passphrase |
50
+
51
+ \* Required for `docker` commands. Can be provided via `-u` flag instead.
52
+
53
+ ## Usage
54
+
55
+ ```bash
56
+ # Set environment variables
57
+ export PORTAINER_URL=https://portainer.example.com
58
+ export PORTAINER_ACCESS_TOKEN=your-api-key
59
+ export S3_ACCESS_KEY=your-s3-key
60
+ export S3_SECRET_KEY=your-s3-secret
61
+ export S3_ENDPOINT=https://s3.<region>.amazonaws.com
62
+
63
+ # Stack deployment (Swarm-only)
64
+ ptctools docker stack deploy -n mystack -f compose.yaml
65
+
66
+ # Secret management (Swarm-only)
67
+ echo "postgresql://user:pass@db:5432/mydb" | ptctools docker secret create db_dsn
68
+ ptctools docker secret create -f /path/to/secret.txt my_secret
69
+ ptctools docker secret create -v "secret-value" my_secret
70
+
71
+ # Config management (Swarm-only)
72
+ ptctools docker config set -n my-config -d "config content"
73
+ ptctools docker config set -n nginx.conf -f ./nginx.conf
74
+ ptctools docker config set -n my-config -d "new content" --force
75
+ ptctools docker config list
76
+ ptctools docker config get -n my-config
77
+ ptctools docker config delete -n my-config
78
+
79
+ # Volume backup/restore (uses Duplicati)
80
+ ptctools docker volume backup -v vol1,vol2 -o s3://mybucket
81
+ ptctools docker volume restore -i s3://mybucket/vol1 # volume name derived from URI path
82
+ ptctools docker volume restore -v vol1 -i s3://mybucket/vol1 # explicit volume name
83
+
84
+ # Volume copy (raw copy using mc/busybox)
85
+ ptctools docker volume cp source dest # volume to volume
86
+ ptctools docker volume cp s3://mybucket/path dest # S3 to volume
87
+ ptctools docker volume cp source s3://mybucket/path # volume to S3
88
+
89
+ # Volume management
90
+ ptctools docker volume rm myvolume # remove volume (with confirmation)
91
+ ptctools docker volume rm -y myvolume # remove without confirmation
92
+ ptctools docker volume rename old_name new_name # rename volume (copy + delete)
93
+
94
+ # Database backup/restore (uses minio/mc for S3)
95
+ ptctools docker db backup -c container_id -v db_data \
96
+ --db-user postgres --db-name mydb -o backup.sql.gz
97
+ ptctools docker db backup -c container_id -v db_data \
98
+ --db-user postgres --db-name mydb -o s3://mybucket/backups/db.sql.gz
99
+
100
+ ptctools docker db restore -c container_id -v db_data \
101
+ --db-user postgres --db-name mydb -i backup.sql.gz
102
+ ptctools docker db restore -c container_id -v db_data \
103
+ --db-user postgres --db-name mydb -i s3://mybucket/backups/db.sql.gz
104
+
105
+ # Override PORTAINER_URL with -u flag
106
+ ptctools docker secret create -u https://other.portainer.com -f dsn.txt my_secret
107
+
108
+ # Utils - local Duplicati operations (no Portainer needed)
109
+ ptctools utils backup --input ./data --output s3://backups/mydata
110
+ ptctools utils restore --input s3://backups/mydata --output ./restored
111
+
112
+ ```
113
+
114
+ ## CLI Structure
115
+
116
+ ```
117
+ ptctools
118
+ ├── docker # Docker commands (via Portainer Docker proxy)
119
+ │ ├── stack deploy # Deploy/update stack (Swarm-only)
120
+ │ ├── secret create # Create Docker secret (Swarm-only)
121
+ │ ├── config set/get/list/delete # Manage configs (Swarm-only)
122
+ │ ├── volume backup/restore/cp/rm/rename
123
+ │ └── db backup/restore
124
+ ├── utils backup/restore # Local Duplicati operations
125
+ └── k8s ... # Kubernetes commands (future)
126
+ ```
127
+
128
+ ## Commands
129
+
130
+ ### `ptctools docker stack deploy`
131
+ Deploy or update a Docker Swarm stack in Portainer.
132
+
133
+ ### `ptctools docker secret create`
134
+ Create a Docker Swarm secret. Value can be provided via stdin, file, or command-line argument.
135
+
136
+ ### `ptctools docker config set/get/list/delete`
137
+ Manage Docker Swarm configs via Portainer API.
138
+
139
+ ### `ptctools docker volume backup/restore`
140
+
141
+ - **backup**: Backup multiple Docker volumes (comma-separated) to S3 using Duplicati container.
142
+ - **restore**: Restore a single Docker volume from S3. Volume name can be specified via `--volume` or derived from the input URI path.
143
+
144
+ ### `ptctools docker volume cp`
145
+ Copy data between volumes and S3 (raw file copy).
146
+ - **volume to volume**: Uses `busybox` with `cp -a`
147
+ - **S3 to volume**: Uses `minio/mc` to download files
148
+ - **volume to S3**: Uses `minio/mc` to upload files
149
+
150
+ ### `ptctools docker volume rm`
151
+ Remove a Docker volume. Use `-y` to skip confirmation, `-f` to force removal.
152
+
153
+ ### `ptctools docker volume rename`
154
+ Rename a volume by copying data to a new volume and deleting the original.
155
+
156
+ ### `ptctools docker db backup/restore`
157
+ Backup/restore PostgreSQL database. Supports both local files and S3 URIs.
158
+ - Uses `pg_dump`/`psql` for database operations
159
+ - Uses `minio/mc` container for S3 transfers
160
+
161
+ ### `ptctools utils backup/restore`
162
+ Local backup/restore operations using Duplicati CLI (docker or local). Does not require Portainer.
@@ -1,12 +1,14 @@
1
1
  ptctools/__init__.py,sha256=O-s77PdUCagdP3HqaQSXYac1y1AC1EbBLOvTWidI_6k,301
2
2
  ptctools/_portainer.py,sha256=XcGDaguPN09SCtQpc_18oS4MvaobPviT1oBEgS3zS2U,19376
3
3
  ptctools/_s3.py,sha256=fOz53Y3X6O9XiFbWhb_qY-xHXjf9OBuoWJl--0nXklc,4974
4
- ptctools/cli.py,sha256=PHyJPIW9SxZ1ZXhwy3kkYbeVVR-qQ6Hopk7ZyeLQxTc,628
5
- ptctools/config.py,sha256=QyUTfor9frcffCiXhtYMQAsAz5e_81bZmqlqG3MFITc,7491
6
- ptctools/db.py,sha256=YtZMbMohO2RveLIGSb3wAO8f1csmRGAtG0MmNW74viU,19415
7
- ptctools/stack.py,sha256=djqM69tRi5QCWX8AFnmKsYnSgPLBqhTW53G4h11lDfM,11779
4
+ ptctools/cli.py,sha256=DxoCpPwN0a_Bl9WKGEGwvjT-ju95ZI5vkNbV6IOl2nc,522
5
+ ptctools/config.py,sha256=LzNtq9qQOSjisLmVFDdVQCdDLwMBuLQNO1QRW-0v894,7779
6
+ ptctools/db.py,sha256=zW6K32mdAxNQZsiQx7ByjKKUQtOs5yYUbCuJCbD0ECM,19785
7
+ ptctools/docker.py,sha256=njF9Tvwx21FDSjFmzaWHGEo4KcGIt9-4FjHBSPyNiiU,674
8
+ ptctools/secret.py,sha256=vdyFhsdBoVeqViiGlNrC6dTbYLVlt7OHV8yobgqHBbk,3977
9
+ ptctools/stack.py,sha256=YymDsE2eLTt6sed9P1P0VvEyblGvf7-4oBHl8E_RXHU,12301
8
10
  ptctools/utils.py,sha256=G9zC4NYRzkPqgkFbsdb18gDiI3ZMSjxz8KnbQ-g3Rgk,13335
9
- ptctools/volume.py,sha256=G2T1FBC5f9hGPZsEdJCW1iA74YysN4ZfWzRuAIqXerk,28593
11
+ ptctools/volume.py,sha256=W1PQ4m8cjduu5W3oqomoqO6DjtxczM7uc1lqV-b4_wM,29384
10
12
  ptctools/portainer_client/.openapi-generator-ignore,sha256=pu2PTide7pJtJ-DFLzDy0cTYQJRlrB-8RRH3zGLeUds,1040
11
13
  ptctools/portainer_client/openapi_client_README.md,sha256=ZfsO03-jNK-G0jqHYzIZQ92XR3-YLHGjOYw-2MouEoA,76219
12
14
  ptctools/portainer_client/.openapi-generator/FILES,sha256=K6TK04JjWPF8gKdJu5Be31HZ01Ijo6XN2tQPTmLrpSo,40032
@@ -1232,8 +1234,8 @@ ptctools/portainer_client/openapi_client/test/test_webhooks_api.py,sha256=pOP2nQ
1232
1234
  ptctools/portainer_client/openapi_client/test/test_webhooks_webhook_create_payload.py,sha256=wUvfMxCbpXS0EKK7kaoDWbwiniQjtA46ybrnRJTgiZE,4729
1233
1235
  ptctools/portainer_client/openapi_client/test/test_webhooks_webhook_update_payload.py,sha256=JQlrXQNDz8AgHkYuxUSAQq_AUPvBdk0fQ7CfIYjgid8,4627
1234
1236
  ptctools/portainer_client/openapi_client/test/test_websocket_api.py,sha256=uqLWUKRrzVfeHKpo4ER7HjY7hpKRtMNjblE0W-DJplU,4342
1235
- ptctools-0.1.1.dist-info/METADATA,sha256=hurN8OKAJ_lNgVggIlCFj46w6gGdw9bYLeSOHdRCU80,4991
1236
- ptctools-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
1237
- ptctools-0.1.1.dist-info/entry_points.txt,sha256=JkiFaKgNdfAR9eL3E8g72rJg6bxTlNzRUdjRp_N6YpY,47
1238
- ptctools-0.1.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1239
- ptctools-0.1.1.dist-info/RECORD,,
1237
+ ptctools-0.2.0.dist-info/METADATA,sha256=kCBfSd_9-RIHJ2sySCE-SzpqLFuDQWS8eg4K85-OGe8,6163
1238
+ ptctools-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
1239
+ ptctools-0.2.0.dist-info/entry_points.txt,sha256=JkiFaKgNdfAR9eL3E8g72rJg6bxTlNzRUdjRp_N6YpY,47
1240
+ ptctools-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1241
+ ptctools-0.2.0.dist-info/RECORD,,