vaultctl 0.1.0__tar.gz
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.
- vaultctl-0.1.0/.devcontainer/devcontainer-lock.json +14 -0
- vaultctl-0.1.0/.devcontainer/devcontainer.json +27 -0
- vaultctl-0.1.0/.github/workflows/release.yaml +121 -0
- vaultctl-0.1.0/.gitignore +6 -0
- vaultctl-0.1.0/.renovaterc.json5 +23 -0
- vaultctl-0.1.0/Dockerfile +17 -0
- vaultctl-0.1.0/LICENSE +21 -0
- vaultctl-0.1.0/PKG-INFO +18 -0
- vaultctl-0.1.0/README.md +1 -0
- vaultctl-0.1.0/pyproject.toml +59 -0
- vaultctl-0.1.0/src/vaultctl/__init__.py +0 -0
- vaultctl-0.1.0/src/vaultctl/commands/__init__.py +15 -0
- vaultctl-0.1.0/src/vaultctl/commands/backup_raft_snapshot.py +236 -0
- vaultctl-0.1.0/src/vaultctl/commands/bootstrap.py +125 -0
- vaultctl-0.1.0/src/vaultctl/commands/login.py +122 -0
- vaultctl-0.1.0/src/vaultctl/commands/pki/__init__.py +0 -0
- vaultctl-0.1.0/src/vaultctl/commands/pki/rotate_issuing.py +155 -0
- vaultctl-0.1.0/src/vaultctl/commands/restore_raft_snapshot.py +210 -0
- vaultctl-0.1.0/src/vaultctl/main.py +21 -0
- vaultctl-0.1.0/src/vaultctl/options.py +200 -0
- vaultctl-0.1.0/src/vaultctl/utils.py +169 -0
- vaultctl-0.1.0/src/vaultctl/version.py +8 -0
- vaultctl-0.1.0/uv.lock +447 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"features": {
|
|
3
|
+
"ghcr.io/devcontainers-extra/features/ruff:2.0.0": {
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"resolved": "ghcr.io/devcontainers-extra/features/ruff@sha256:80f1d8ff0fbd84fffbc9383ff11cc54524898f8bbc820cae642dff4902575117",
|
|
6
|
+
"integrity": "sha256:80f1d8ff0fbd84fffbc9383ff11cc54524898f8bbc820cae642dff4902575117"
|
|
7
|
+
},
|
|
8
|
+
"ghcr.io/devcontainers-extra/features/uv:1.0.2": {
|
|
9
|
+
"version": "1.0.2",
|
|
10
|
+
"resolved": "ghcr.io/devcontainers-extra/features/uv@sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8",
|
|
11
|
+
"integrity": "sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vaultctl devcontainer",
|
|
3
|
+
"image": "mcr.microsoft.com/devcontainers/python:3-3.14",
|
|
4
|
+
"postCreateCommand": "uv sync",
|
|
5
|
+
"customizations": {
|
|
6
|
+
"vscode": {
|
|
7
|
+
"extensions": [
|
|
8
|
+
"ms-python.python",
|
|
9
|
+
"ms-python.vscode-pylance",
|
|
10
|
+
"charliermarsh.ruff",
|
|
11
|
+
"the0807.uv-toolkit"
|
|
12
|
+
],
|
|
13
|
+
"settings": {
|
|
14
|
+
"python.defaultInterpreterPath": "/workspaces/vaultctl/.venv/bin/python",
|
|
15
|
+
"[python]": {
|
|
16
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
17
|
+
"editor.formatOnSave": true
|
|
18
|
+
},
|
|
19
|
+
"ruff.path": ["/usr/local/bin/ruff"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"features": {
|
|
24
|
+
"ghcr.io/devcontainers-extra/features/uv:1.0.2": {},
|
|
25
|
+
"ghcr.io/devcontainers-extra/features/ruff:2.0.0": { "version": "0.15.16"}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write
|
|
8
|
+
packages: write
|
|
9
|
+
id-token: write
|
|
10
|
+
attestations: write
|
|
11
|
+
artifact-metadata: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
docker:
|
|
15
|
+
runs-on: ubuntu-24.04
|
|
16
|
+
env:
|
|
17
|
+
REGISTRY: ghcr.io
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout repository
|
|
21
|
+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.14"
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
30
|
+
|
|
31
|
+
- name: Get project name and version from pyproject.toml
|
|
32
|
+
id: meta
|
|
33
|
+
run: |
|
|
34
|
+
python3 <<'PY'
|
|
35
|
+
import tomllib, pathlib
|
|
36
|
+
p = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]
|
|
37
|
+
print(f"version={p['version']}")
|
|
38
|
+
print(f"name={p['name']}")
|
|
39
|
+
PY
|
|
40
|
+
shell: bash
|
|
41
|
+
|
|
42
|
+
- name: Set up QEMU
|
|
43
|
+
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
|
44
|
+
|
|
45
|
+
- name: Set up Docker Buildx
|
|
46
|
+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
|
47
|
+
|
|
48
|
+
- name: Set up Cosign
|
|
49
|
+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
|
50
|
+
|
|
51
|
+
- name: Log in to GitHub Container Registry
|
|
52
|
+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
|
53
|
+
with:
|
|
54
|
+
registry: ghcr.io
|
|
55
|
+
username: ${{ github.actor }}
|
|
56
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
57
|
+
|
|
58
|
+
- name: Build CLI package
|
|
59
|
+
run: |
|
|
60
|
+
rm -rf dist/ build/ *.egg-info
|
|
61
|
+
uv build
|
|
62
|
+
|
|
63
|
+
- name: Publish CLI to PyPI
|
|
64
|
+
run: |
|
|
65
|
+
uv publish
|
|
66
|
+
|
|
67
|
+
- name: Create GitHub Release
|
|
68
|
+
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
69
|
+
with:
|
|
70
|
+
tag_name: v${{ steps.meta.outputs.version }}
|
|
71
|
+
name: v${{ steps.meta.outputs.version }}
|
|
72
|
+
generate_release_notes: true
|
|
73
|
+
files: |
|
|
74
|
+
dist/*
|
|
75
|
+
|
|
76
|
+
- name: Build and Push Docker Image
|
|
77
|
+
id: build-and-push
|
|
78
|
+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
|
79
|
+
with:
|
|
80
|
+
context: .
|
|
81
|
+
file: ./Dockerfile
|
|
82
|
+
push: true
|
|
83
|
+
platforms: linux/amd64,linux/arm64
|
|
84
|
+
provenance: false
|
|
85
|
+
build-args: |
|
|
86
|
+
CLI_VERSION=${{ steps.meta.outputs.version }}
|
|
87
|
+
VENDOR=${{ github.repository_owner }}
|
|
88
|
+
labels: |
|
|
89
|
+
org.opencontainers.image.revision=${{ github.sha }}
|
|
90
|
+
org.opencontainers.image.vendor=${{ github.repository_owner }}
|
|
91
|
+
org.opencontainers.image.url=https://ghcr.io/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}
|
|
92
|
+
tags: |
|
|
93
|
+
${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}:latest
|
|
94
|
+
${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}:${{ steps.meta.outputs.version }}
|
|
95
|
+
|
|
96
|
+
- name: Sign Image with Cosign
|
|
97
|
+
run: |
|
|
98
|
+
cosign sign --yes \
|
|
99
|
+
${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}@${{ steps.build-and-push.outputs.digest }}
|
|
100
|
+
|
|
101
|
+
- name: Attest build provenance
|
|
102
|
+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
103
|
+
with:
|
|
104
|
+
subject-name: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}
|
|
105
|
+
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
|
106
|
+
push-to-registry: true
|
|
107
|
+
|
|
108
|
+
- name: Generate SBOM
|
|
109
|
+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
|
110
|
+
with:
|
|
111
|
+
image: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}@${{ steps.build-and-push.outputs.digest }}
|
|
112
|
+
format: spdx-json
|
|
113
|
+
output-file: sbom.spdx.json
|
|
114
|
+
|
|
115
|
+
- name: Attest SBOM
|
|
116
|
+
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
|
117
|
+
with:
|
|
118
|
+
subject-name: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}
|
|
119
|
+
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
|
120
|
+
sbom-path: sbom.spdx.json
|
|
121
|
+
push-to-registry: true
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
|
|
3
|
+
extends: [
|
|
4
|
+
'config:recommended',
|
|
5
|
+
'docker:enableMajor',
|
|
6
|
+
'customManagers:dockerfileVersions',
|
|
7
|
+
'helpers:pinGitHubActionDigests',
|
|
8
|
+
':disableRateLimiting',
|
|
9
|
+
':dependencyDashboard',
|
|
10
|
+
':semanticCommits',
|
|
11
|
+
':timezone(Europe/Sofia)',
|
|
12
|
+
],
|
|
13
|
+
dependencyDashboardTitle: 'Renovate Dashboard 🤖',
|
|
14
|
+
suppressNotifications: [
|
|
15
|
+
'prEditedNotification',
|
|
16
|
+
'prIgnoreNotification',
|
|
17
|
+
],
|
|
18
|
+
pip_requirements: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
managerFilePatterns: ["(^|/)pyproject([-.]\\w+)?\\.toml$"],
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
FROM python:3.14-slim@sha256:9b81fe9acff79e61affb44aaf3b6ff234392e8ca477cb86c9f7fd11732ce9b6a
|
|
2
|
+
|
|
3
|
+
RUN \
|
|
4
|
+
groupadd -g 65532 nonroot \
|
|
5
|
+
&& \
|
|
6
|
+
useradd -r -u 65532 -g 65532 -m nonroot
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
COPY dist/*.whl /tmp/
|
|
10
|
+
RUN \
|
|
11
|
+
pip install --no-cache-dir /tmp/*.whl \
|
|
12
|
+
&& \
|
|
13
|
+
rm -f /tmp/*.whl
|
|
14
|
+
|
|
15
|
+
USER nonroot
|
|
16
|
+
|
|
17
|
+
ENTRYPOINT ["vaultctl"]
|
vaultctl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Georgi Panov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vaultctl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vaultctl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault.
|
|
5
|
+
Author-email: d4rkfella <georgi.panov@darkfellanetwork.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: <4.0,>=3.14
|
|
9
|
+
Requires-Dist: boto3==1.43.27
|
|
10
|
+
Requires-Dist: botocore[crt]
|
|
11
|
+
Requires-Dist: cryptography==48.0.1
|
|
12
|
+
Requires-Dist: hvac==2.4
|
|
13
|
+
Requires-Dist: python-dateutil==2.9.0
|
|
14
|
+
Requires-Dist: typer==0.26.7
|
|
15
|
+
Requires-Dist: validators==0.35.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault.
|
vaultctl-0.1.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vaultctl"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "d4rkfella", email = "georgi.panov@darkfellanetwork.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.14,<4.0"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer==0.26.7",
|
|
13
|
+
"validators==0.35.0",
|
|
14
|
+
"hvac==2.4",
|
|
15
|
+
"cryptography==48.0.1",
|
|
16
|
+
"python-dateutil==2.9.0",
|
|
17
|
+
"boto3==1.43.27",
|
|
18
|
+
"botocore[crt]",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
vaultctl = "vaultctl.main:app"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/vaultctl"]
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"types-hvac==2.4.0.20260610",
|
|
34
|
+
"mypy-boto3-s3==1.43.14",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
target-version = "py314"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["ALL"]
|
|
42
|
+
ignore = [
|
|
43
|
+
"E501",
|
|
44
|
+
"D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107",
|
|
45
|
+
"D211",
|
|
46
|
+
"D213",
|
|
47
|
+
"D203",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
51
|
+
runtime-evaluated-base-classes = [
|
|
52
|
+
"typing.Annotated",
|
|
53
|
+
"typing_extensions.Annotated"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
runtime-evaluated-decorators = [
|
|
57
|
+
"typer.Typer.command",
|
|
58
|
+
"typer.Typer.callback"
|
|
59
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from vaultctl.commands.backup_raft_snapshot import app as backup_raft_snapshot
|
|
4
|
+
from vaultctl.commands.bootstrap import app as bootstrap
|
|
5
|
+
from vaultctl.commands.login import app as login
|
|
6
|
+
from vaultctl.commands.pki.rotate_issuing import app as pki
|
|
7
|
+
from vaultctl.commands.restore_raft_snapshot import app as restore_raft_snapshot
|
|
8
|
+
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
|
|
11
|
+
app.add_typer(bootstrap)
|
|
12
|
+
app.add_typer(restore_raft_snapshot)
|
|
13
|
+
app.add_typer(backup_raft_snapshot)
|
|
14
|
+
app.add_typer(login)
|
|
15
|
+
app.add_typer(pki)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import hashlib
|
|
5
|
+
import io
|
|
6
|
+
import tarfile
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated, cast
|
|
10
|
+
|
|
11
|
+
import hvac
|
|
12
|
+
import typer
|
|
13
|
+
from botocore.exceptions import ClientError
|
|
14
|
+
from requests import Response
|
|
15
|
+
|
|
16
|
+
from vaultctl.options import (
|
|
17
|
+
AddressOption,
|
|
18
|
+
AwsAccessKeyIdOption,
|
|
19
|
+
AwsEndpointUrlOption,
|
|
20
|
+
AwsProfileOption,
|
|
21
|
+
AwsRegionOption,
|
|
22
|
+
AwsSecretAccessKeyOption,
|
|
23
|
+
CACertOption,
|
|
24
|
+
CAPathOption,
|
|
25
|
+
K8sMountPointOption,
|
|
26
|
+
K8sRoleOption,
|
|
27
|
+
OutputFileOption,
|
|
28
|
+
S3BucketNameOption,
|
|
29
|
+
S3KeyPrefixOption,
|
|
30
|
+
SkipVerifyOption,
|
|
31
|
+
TimeoutOption,
|
|
32
|
+
TokenOption,
|
|
33
|
+
)
|
|
34
|
+
from vaultctl.utils import handle_s3_authentication, handle_vault_authentication, logger
|
|
35
|
+
|
|
36
|
+
app = typer.Typer()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RaftUploadError(Exception):
|
|
40
|
+
"""Raised when uploading a Raft snapshot to S3 fails."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class S3ChecksumAlgorithm(Enum):
|
|
44
|
+
CRC32 = "CRC32"
|
|
45
|
+
CRC32C = "CRC32C"
|
|
46
|
+
SHA1 = "SHA1"
|
|
47
|
+
SHA256 = "SHA256"
|
|
48
|
+
CRC64NVME = "CRC64NVME"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_sha256sums(content: bytes) -> dict[str, str]:
|
|
52
|
+
sums = {}
|
|
53
|
+
lines = content.strip().split(b"\n")
|
|
54
|
+
for line in lines:
|
|
55
|
+
trimmed_line = line.strip()
|
|
56
|
+
if not trimmed_line:
|
|
57
|
+
continue
|
|
58
|
+
parts = trimmed_line.split()
|
|
59
|
+
if len(parts) == 2: # noqa: PLR2004
|
|
60
|
+
checksum = parts[0].decode("utf-8")
|
|
61
|
+
filename = parts[1].decode("utf-8")
|
|
62
|
+
sums[filename] = checksum
|
|
63
|
+
return sums
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def verify_internal_checksums(snapshot_data: bytes) -> None:
|
|
67
|
+
logger.info("Verifying snapshot integrity...")
|
|
68
|
+
snapshot_stream = io.BytesIO(snapshot_data)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
with tarfile.open(fileobj=snapshot_stream, mode="r:gz") as tar:
|
|
72
|
+
sha_sums_content = None
|
|
73
|
+
files_in_tar: dict[str, bytes] = {}
|
|
74
|
+
|
|
75
|
+
for member in tar.getmembers():
|
|
76
|
+
if not member.isfile():
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if not (f := tar.extractfile(member)):
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
content = f.read()
|
|
83
|
+
|
|
84
|
+
if member.name == "SHA256SUMS":
|
|
85
|
+
sha_sums_content = content
|
|
86
|
+
|
|
87
|
+
files_in_tar[member.name] = content
|
|
88
|
+
|
|
89
|
+
if sha_sums_content is None:
|
|
90
|
+
msg = "SHA256SUMS file not found in the Raft snapshot archive."
|
|
91
|
+
raise ValueError(
|
|
92
|
+
msg,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expected_sums = parse_sha256sums(sha_sums_content)
|
|
96
|
+
|
|
97
|
+
for name, expected_sum in expected_sums.items():
|
|
98
|
+
content = files_in_tar.get(name)
|
|
99
|
+
|
|
100
|
+
if content is None:
|
|
101
|
+
msg = f"file '{name}' listed in SHA256SUMS not found in archive."
|
|
102
|
+
raise ValueError(
|
|
103
|
+
msg,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
computed_sum = hashlib.sha256(content).hexdigest()
|
|
107
|
+
|
|
108
|
+
if computed_sum != expected_sum:
|
|
109
|
+
msg = f"checksum mismatch for file '{name}'. Expected: {expected_sum}, Got: {computed_sum}"
|
|
110
|
+
raise ValueError(
|
|
111
|
+
msg,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
logger.info("Snapshot integrity verified.")
|
|
115
|
+
|
|
116
|
+
except tarfile.TarError as e:
|
|
117
|
+
msg = f"reading archive: {e}"
|
|
118
|
+
raise tarfile.TarError(msg) from e
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@app.command(
|
|
122
|
+
help="Executes a complete workflow for obtaining a HashiCorp Vault Raft snapshot from a cluster, verifying its integrity, and uploading it securely to S3 storage. Provides flexible authentication options for both HashiCorp Vault and S3 APIs.",
|
|
123
|
+
)
|
|
124
|
+
def backup_raft_snapshot( # noqa: PLR0913
|
|
125
|
+
address: AddressOption,
|
|
126
|
+
s3_bucket_name: S3BucketNameOption = None,
|
|
127
|
+
output_file: OutputFileOption = None,
|
|
128
|
+
k8s_role: K8sRoleOption = None,
|
|
129
|
+
k8s_mount_point: K8sMountPointOption = "kubernetes",
|
|
130
|
+
token: TokenOption = None,
|
|
131
|
+
ca_cert: CACertOption = None,
|
|
132
|
+
ca_path: CAPathOption = None,
|
|
133
|
+
aws_profile: AwsProfileOption = None,
|
|
134
|
+
aws_access_key_id: AwsAccessKeyIdOption = None,
|
|
135
|
+
aws_secret_access_key: AwsSecretAccessKeyOption = None,
|
|
136
|
+
aws_endpoint_url: AwsEndpointUrlOption = None,
|
|
137
|
+
aws_region: AwsRegionOption = "us-east-1",
|
|
138
|
+
s3_key_prefix: S3KeyPrefixOption = "",
|
|
139
|
+
s3_checksum_algorithm: Annotated[
|
|
140
|
+
S3ChecksumAlgorithm,
|
|
141
|
+
typer.Option(help="The algorithm to use for s3 transport checksum."),
|
|
142
|
+
] = S3ChecksumAlgorithm.CRC64NVME,
|
|
143
|
+
timeout: TimeoutOption = 30,
|
|
144
|
+
*,
|
|
145
|
+
skip_verify: SkipVerifyOption = False,
|
|
146
|
+
) -> None:
|
|
147
|
+
if not s3_bucket_name and not output_file:
|
|
148
|
+
msg = "Either --s3-bucket-name or --output-file is required"
|
|
149
|
+
raise typer.BadParameter(msg)
|
|
150
|
+
|
|
151
|
+
vault_client = handle_vault_authentication(
|
|
152
|
+
hvac.Client(
|
|
153
|
+
url=address,
|
|
154
|
+
timeout=timeout,
|
|
155
|
+
verify=(
|
|
156
|
+
str(ca_cert)
|
|
157
|
+
if ca_cert
|
|
158
|
+
else str(ca_path)
|
|
159
|
+
if ca_path
|
|
160
|
+
else (not skip_verify)
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
token=token,
|
|
164
|
+
k8s_role=k8s_role,
|
|
165
|
+
k8s_mount_point=k8s_mount_point,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if vault_client.sys.is_sealed():
|
|
169
|
+
logger.error("Vault is sealed. Cannot proceed with backup.")
|
|
170
|
+
raise typer.Exit(code=1)
|
|
171
|
+
|
|
172
|
+
logger.info("Starting backup...")
|
|
173
|
+
|
|
174
|
+
logger.info("Requesting raft snapshot...")
|
|
175
|
+
response: Response = vault_client.sys.take_raft_snapshot()
|
|
176
|
+
|
|
177
|
+
if response.status_code != 200: # noqa: PLR2004
|
|
178
|
+
logger.error(
|
|
179
|
+
"Raft snapshot request failed with status code %s.",
|
|
180
|
+
response.status_code,
|
|
181
|
+
)
|
|
182
|
+
logger.debug("Response body: %s", response.text)
|
|
183
|
+
raise typer.Exit(1)
|
|
184
|
+
|
|
185
|
+
snapshot_data: bytes = response.content
|
|
186
|
+
|
|
187
|
+
logger.info("Raft snapshot retrieved.")
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
verify_internal_checksums(snapshot_data)
|
|
191
|
+
except (tarfile.TarError, ValueError):
|
|
192
|
+
logger.exception("Verifying raft snapshot integrity")
|
|
193
|
+
raise typer.Exit(1) from None
|
|
194
|
+
|
|
195
|
+
if output_file:
|
|
196
|
+
output_path = Path(output_file)
|
|
197
|
+
if output_path.is_dir():
|
|
198
|
+
timestamp = datetime.datetime.now(tz=datetime.UTC).strftime("%Y%m%d_%H%M%S")
|
|
199
|
+
output_path = output_path / f"vault-snapshot-{timestamp}.snap"
|
|
200
|
+
output_path.write_bytes(snapshot_data)
|
|
201
|
+
logger.info("Snapshot written to %s", output_path)
|
|
202
|
+
logger.info("Backup completed")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
s3_bucket_name = cast("str", s3_bucket_name)
|
|
206
|
+
|
|
207
|
+
s3_client = handle_s3_authentication(
|
|
208
|
+
bucket_name=s3_bucket_name,
|
|
209
|
+
aws_access_key_id=aws_access_key_id,
|
|
210
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
211
|
+
aws_region=aws_region,
|
|
212
|
+
aws_profile=aws_profile,
|
|
213
|
+
endpoint_url=aws_endpoint_url,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
timestamp = datetime.datetime.now(tz=datetime.UTC).strftime("%Y%m%d_%H%M%S")
|
|
217
|
+
snapshot_name = f"vault-snapshot-{timestamp}.snap"
|
|
218
|
+
s3_key = f"{s3_key_prefix}{snapshot_name}"
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
logger.info("Uploading raft snapshot to %s/%s...", s3_bucket_name, s3_key)
|
|
222
|
+
s3_client.upload_fileobj(
|
|
223
|
+
io.BytesIO(snapshot_data),
|
|
224
|
+
s3_bucket_name,
|
|
225
|
+
s3_key,
|
|
226
|
+
ExtraArgs={
|
|
227
|
+
"ContentType": "application/gzip",
|
|
228
|
+
"ChecksumAlgorithm": s3_checksum_algorithm,
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
except ClientError:
|
|
232
|
+
logger.exception("Uploading raft snapshot")
|
|
233
|
+
raise typer.Exit(1) from None
|
|
234
|
+
|
|
235
|
+
logger.info("Raft snapshot uploaded.")
|
|
236
|
+
logger.info("Backup completed")
|