feat: SFTP-Anbindung und Zugangsdatenart im Datenmodell
Neuer Anbindungstyp sftp (share = entfernter Basispfad, port, host_key), credentials.kind (password|ssh_key) und public_key. Migration 0001 vor dem ersten Release angepasst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a60daf0039
commit
278180c541
+4
-2
@@ -24,6 +24,7 @@ dependencies = [
|
|||||||
"pydantic-settings>=2.3",
|
"pydantic-settings>=2.3",
|
||||||
"itsdangerous>=2.2",
|
"itsdangerous>=2.2",
|
||||||
"httpx>=0.27",
|
"httpx>=0.27",
|
||||||
|
"paramiko>=3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -31,6 +32,7 @@ dev = [
|
|||||||
"pytest>=8.2",
|
"pytest>=8.2",
|
||||||
"ruff>=0.6",
|
"ruff>=0.6",
|
||||||
"mypy>=1.11",
|
"mypy>=1.11",
|
||||||
|
"types-paramiko",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -52,14 +54,14 @@ target-version = "py312"
|
|||||||
src = ["src", "tests"]
|
src = ["src", "tests"]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "W", "I", "B", "UP", "S", "SIM", "RUF", "PTH"]
|
select = ["E", "F", "W", "I", "B", "UP", "S", "SIM", "RUF"]
|
||||||
ignore = [
|
ignore = [
|
||||||
"S603", # subprocess ohne shell=True ist genau das gewünschte Muster
|
"S603", # subprocess ohne shell=True ist genau das gewünschte Muster
|
||||||
"RUF001", "RUF002", "RUF003", # deutsche Texte mit Typografie
|
"RUF001", "RUF002", "RUF003", # deutsche Texte mit Typografie
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"tests/**" = ["S101", "S105", "S106", "S108"]
|
"tests/**" = ["S101", "S105", "S106", "S107", "S108"]
|
||||||
"src/netmover/migrations/**" = ["E501"]
|
"src/netmover/migrations/**" = ["E501"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
|||||||
@@ -23,9 +23,22 @@ def upgrade() -> None:
|
|||||||
"credentials",
|
"credentials",
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column("name", sa.String(length=100), nullable=False),
|
sa.Column("name", sa.String(length=100), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"kind",
|
||||||
|
sa.Enum(
|
||||||
|
"password",
|
||||||
|
"ssh_key",
|
||||||
|
name="credential_kind",
|
||||||
|
native_enum=False,
|
||||||
|
create_constraint=True,
|
||||||
|
length=24,
|
||||||
|
),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
sa.Column("username", sa.String(length=255), nullable=False),
|
sa.Column("username", sa.String(length=255), nullable=False),
|
||||||
sa.Column("domain", sa.String(length=255), nullable=True),
|
sa.Column("domain", sa.String(length=255), nullable=True),
|
||||||
sa.Column("secret_enc", sa.Text(), nullable=False),
|
sa.Column("secret_enc", sa.Text(), nullable=False),
|
||||||
|
sa.Column("public_key", sa.Text(), nullable=True),
|
||||||
sa.Column("created_at", netmover.models.UTCDateTime(), nullable=False),
|
sa.Column("created_at", netmover.models.UTCDateTime(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint("id", name=op.f("pk_credentials")),
|
sa.PrimaryKeyConstraint("id", name=op.f("pk_credentials")),
|
||||||
sa.UniqueConstraint("name", name=op.f("uq_credentials_name")),
|
sa.UniqueConstraint("name", name=op.f("uq_credentials_name")),
|
||||||
@@ -47,6 +60,7 @@ def upgrade() -> None:
|
|||||||
"cifs",
|
"cifs",
|
||||||
"nfs4",
|
"nfs4",
|
||||||
"nfs3",
|
"nfs3",
|
||||||
|
"sftp",
|
||||||
name="mount_type",
|
name="mount_type",
|
||||||
native_enum=False,
|
native_enum=False,
|
||||||
create_constraint=True,
|
create_constraint=True,
|
||||||
@@ -56,7 +70,9 @@ def upgrade() -> None:
|
|||||||
),
|
),
|
||||||
sa.Column("server", sa.String(length=255), nullable=False),
|
sa.Column("server", sa.String(length=255), nullable=False),
|
||||||
sa.Column("share", sa.String(length=1024), nullable=False),
|
sa.Column("share", sa.String(length=1024), nullable=False),
|
||||||
sa.Column("mountpoint", sa.String(length=1024), nullable=False),
|
sa.Column("port", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("host_key", sa.Text(), nullable=True),
|
||||||
|
sa.Column("mountpoint", sa.String(length=1024), nullable=True),
|
||||||
sa.Column("cred_id", sa.Integer(), nullable=True),
|
sa.Column("cred_id", sa.Integer(), nullable=True),
|
||||||
sa.Column("uid", sa.Integer(), nullable=True),
|
sa.Column("uid", sa.Integer(), nullable=True),
|
||||||
sa.Column("gid", sa.Integer(), nullable=True),
|
sa.Column("gid", sa.Integer(), nullable=True),
|
||||||
|
|||||||
+22
-2
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Abweichungen bzw. Ergänzungen gegenüber dem Mindestumfang aus der Spezifikation:
|
Abweichungen bzw. Ergänzungen gegenüber dem Mindestumfang aus der Spezifikation:
|
||||||
|
|
||||||
|
* Typ ``sftp``: Ziel per SSH/SFTP ohne Kernel-Mount (z. B. Paperless im LXC).
|
||||||
|
``share`` ist dort der entfernte Basispfad, dazu ``port`` und ``host_key``
|
||||||
|
(beim ersten Test festgehaltener Hostschlüssel, danach verbindlich).
|
||||||
|
* ``credentials.kind`` (``password`` | ``ssh_key``) und ``credentials.public_key``.
|
||||||
* ``credentials.domain`` – Windows-/AD-Freigaben brauchen oft eine Domäne.
|
* ``credentials.domain`` – Windows-/AD-Freigaben brauchen oft eine Domäne.
|
||||||
* ``mounts.state_since`` – für „hängt seit …“ / „gestört seit …“ auf der Statusseite.
|
* ``mounts.state_since`` – für „hängt seit …“ / „gestört seit …“ auf der Statusseite.
|
||||||
* ``mounts.write_ok`` / ``mounts.root_squash`` / ``mounts.last_test_*`` – Ergebnis
|
* ``mounts.write_ok`` / ``mounts.root_squash`` / ``mounts.last_test_*`` – Ergebnis
|
||||||
@@ -93,6 +97,16 @@ class MountType(enum.StrEnum):
|
|||||||
CIFS = "cifs"
|
CIFS = "cifs"
|
||||||
NFS4 = "nfs4"
|
NFS4 = "nfs4"
|
||||||
NFS3 = "nfs3"
|
NFS3 = "nfs3"
|
||||||
|
SFTP = "sftp"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_kernel_mount(self) -> bool:
|
||||||
|
return self is not MountType.SFTP
|
||||||
|
|
||||||
|
|
||||||
|
class CredentialKind(enum.StrEnum):
|
||||||
|
PASSWORD = "password" # noqa: S105 – Art der Zugangsdaten, kein Passwort
|
||||||
|
SSH_KEY = "ssh_key"
|
||||||
|
|
||||||
|
|
||||||
class MountState(enum.StrEnum):
|
class MountState(enum.StrEnum):
|
||||||
@@ -145,9 +159,13 @@ class Credential(Base):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
name: Mapped[str] = mapped_column(String(100), unique=True)
|
name: Mapped[str] = mapped_column(String(100), unique=True)
|
||||||
|
kind: Mapped[CredentialKind] = mapped_column(
|
||||||
|
_enum(CredentialKind, "credential_kind"), default=CredentialKind.PASSWORD
|
||||||
|
)
|
||||||
username: Mapped[str] = mapped_column(String(255))
|
username: Mapped[str] = mapped_column(String(255))
|
||||||
domain: Mapped[str | None] = mapped_column(String(255))
|
domain: Mapped[str | None] = mapped_column(String(255))
|
||||||
secret_enc: Mapped[str] = mapped_column(Text)
|
secret_enc: Mapped[str] = mapped_column(Text) # Passwort bzw. privater Schlüssel (Fernet)
|
||||||
|
public_key: Mapped[str | None] = mapped_column(Text) # nur ssh_key, OpenSSH-Format
|
||||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
|
||||||
|
|
||||||
# passive_deletes="all": das ORM soll cred_id nicht still auf NULL setzen,
|
# passive_deletes="all": das ORM soll cred_id nicht still auf NULL setzen,
|
||||||
@@ -164,7 +182,9 @@ class Mount(Base):
|
|||||||
type: Mapped[MountType] = mapped_column(_enum(MountType, "mount_type"))
|
type: Mapped[MountType] = mapped_column(_enum(MountType, "mount_type"))
|
||||||
server: Mapped[str] = mapped_column(String(255))
|
server: Mapped[str] = mapped_column(String(255))
|
||||||
share: Mapped[str] = mapped_column(String(1024))
|
share: Mapped[str] = mapped_column(String(1024))
|
||||||
mountpoint: Mapped[str] = mapped_column(String(1024), unique=True)
|
port: Mapped[int | None] = mapped_column(Integer) # nur sftp
|
||||||
|
host_key: Mapped[str | None] = mapped_column(Text) # nur sftp, "<typ> <base64>"
|
||||||
|
mountpoint: Mapped[str | None] = mapped_column(String(1024), unique=True) # NULL bei sftp
|
||||||
cred_id: Mapped[int | None] = mapped_column(ForeignKey("credentials.id", ondelete="RESTRICT"))
|
cred_id: Mapped[int | None] = mapped_column(ForeignKey("credentials.id", ondelete="RESTRICT"))
|
||||||
uid: Mapped[int | None] = mapped_column(Integer)
|
uid: Mapped[int | None] = mapped_column(Integer)
|
||||||
gid: Mapped[int | None] = mapped_column(Integer)
|
gid: Mapped[int | None] = mapped_column(Integer)
|
||||||
|
|||||||
Reference in New Issue
Block a user