Skip to content

Native SMB support via mgr/smb (phase 1)#802

Draft
UtkarshBhatthere wants to merge 31 commits into
mainfrom
smb-native-phase1
Draft

Native SMB support via mgr/smb (phase 1)#802
UtkarshBhatthere wants to merge 31 commits into
mainfrom
smb-native-phase1

Conversation

@UtkarshBhatthere

Copy link
Copy Markdown
Contributor

Adds native SMB file sharing to MicroCeph: CephFS subvolumes served by Samba, CTDB-clustered for HA, driven end to end by the upstream ceph smb manager module. MicroCeph implements the mgr/smb orchestrator contract, so the user-facing surface (CLI, declarative resources, dashboard) is the documented upstream experience.

Jira: CEPH-1240

Architecture

ceph smb (mgr/smb, packaged in-snap) -> microceph orchestrator module (apply_smb / remove_service / describe_service) -> microcephd REST (/1.0/services/smb*) -> per-node rendering (sambacc-based smb.conf, native ctdb.conf/nodes/public_addresses) -> ctdbd manages smbd via a snapctl event script. The SMBSpec is stored verbatim as the grouped-service config (NFS precedent); apply is idempotent and convergent. Passdb users are seeded automatically from the spec's user_sources on every apply.

Highlights

  • Samba/CTDB/sambacc packaged in the snap; RADOS mutex helper as the CTDB cluster lock; runtime paths survive snap refreshes.
  • Strict spec validation with actionable errors (unsupported features/fields, placement limits, node-disjoint hospitality on port 445).
  • The packaged mgr/smb carries a one-hunk patch (patches/mgr-smb/) expanding the default share provider to samba-vfs/new: MicroCeph serves shares via direct libcephfs and deploys no cephfs-proxy daemon, so the plain ceph smb share create works natively. Explicit samba-vfs/proxied requests are rejected. An upstream RFE to make the default configurable will retire the patch.
  • Hidden microceph smb debug CLI for support.

Testing

  • Go unit suites: validation/placement, apply orchestration + seeding, keyrings, rendering (golden files), lifecycle (incl. snap-refresh and regenerate regressions), user seeding, startup re-enablement.
  • microceph-orch pytest (wire format, routing, describe): 9 passed.
  • Robot: new smb-tests suite (backend enable, imperative cluster+share, smbclient roundtrip via VIP, VIP-holder kill/failover/rejoin, removal) resolving under --dryrun; pure parsers pytest-covered.
  • Manual 3-node E2E on LXD VMs: full mgr-driven lifecycle, VIP failover in ~40s, dashboard /api/smb verified.

Notes for reviewers

  • Until the proposed snapd smb-support interface lands, SMB needs the snap in devmode (confined smbd is blocked on setgroups + setuid/setgid; minimal grants were proven by local profile patching and are written up for the snapd proposal).
  • AD membership (domain feature) is deliberately rejected in this phase; standalone user auth only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RHqYMvUeUXxJTsCNdHmwYt

Package samba, samba-vfs-modules and ctdb from the noble archive and
expose an smbd app (disabled by default) driven by a config under
SNAP_DATA/conf/samba. No placement or API integration; this is the
packaging half of the native SMB experiment.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Add Go types mirroring the upstream mgr/smb SMBSpec service spec JSON
(service_spec.py): SMBSpec, SMBPlacementSpec, SMBPublicAddrSpec and the
SMBDestination union decoder, plus the upstream cluster id regex from
mgr/smb validation.py. Unknown fields are tolerated so newer mgr
serializations do not break decoding.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Add SMBServicePlacement implementing PlacementIntf: SMBSpec payload
validation (upstream cluster id regex, feature gate accepting only
'clustered', rejection of Phase-1-unsupported fields), hospitality
checks (port 445 free, smb clusters node-disjoint), and DB recording
via GroupedServicesQuery with the spec JSON stored verbatim as group
config. ServiceInit and PostPlacementCheck are documented no-ops until
the M3 lifecycle work.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Add the cluster-scoped /1.0/services/smb endpoint (PUT SMBSpec apply,
DELETE cluster removal, GET status) and the node-scoped
/1.0/services/smb/node endpoint used by the per-node fan-out. ApplySMB
resolves spec placement (hosts/count) against cluster members, diffs it
with recorded membership and converges idempotently; specs are
canonicalized (json.Compact) so stored group configs compare stably.
Extend GroupedServicesQuery with group member/config accessors.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Tighten the samba part to a whitelist prime: the smbd/ctdb daemons and
tools, ctdb helpers (incl. ctdb_mutex_ceph_rados_helper), samba module
dir and the exact ELF NEEDED closure of those binaries, dropping docs,
man pages, AD client tools and python bindings. Add the ctdbd app
(snapd-managed, install-mode disable) with its start wrapper pointing
CTDB_BASE at SNAP_DATA, layouts for /etc/ctdb and /usr/share/ctdb, a
snapctl-based replacement 50.samba event script (CTDB manages smbd via
snapd, not systemd), and a sambacc part pinned to 0.9 for config
translation.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Replace the samba part's prime whitelist with negative-list pruning:
snapcraft resolves shared staged files against every staging part's
filter, so whitelisting inside the samba part silently dropped libs
other parts prime (ceph-mgr's libpython3.12, dashboard's markdown and
dnspython, libldap/libicu symlink targets). Exclude only samba-owned
paths: docs, man pages, GPO templates, and AD/DC/NetBIOS tooling.

Retire stub patch 0003 and add an mgr-smb part that sparse-clones the
ceph tag matching the staged PPA debs (asserted against apt-cache
policy at build time) and installs src/pybind/mgr/smb into the snap's
mgr module path.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Add 'microceph smb apply-spec/rm/list' hitting the services/smb
endpoints directly. Hidden: the supported control plane is mgr/smb via
the orchestrator; these keep E2E honest before microceph-orch gains
smb support, and remain for support tooling.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
The smb hospitality check reads group membership from the cluster DB,
and microcluster transactions require the request context (its logger);
context.Background() fails with 'Logger does not exist on context',
caught live on the first end-to-end spec apply. Thread ctx through
PlacementIntf.HospitalityCheck for all services.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Add per-node daemon key handling mirroring cephadm's SMBService key
model: mon caps allow reads plus this cluster's smb/config/ config-key
prefix; osd caps derive from the spec's RADOS URIs with enhanced rwx on
the smb pool's cluster.meta. object prefix (CTDB reclock). Caps
converge on re-apply via auth caps rather than get-or-create. The
include_ceph_users data-path keys (owned by mgr/smb) are fetched into
standard-named keyring files under the conf dir, where the default
/etc/ceph search path finds them; entity names are validated before
touching disk. Wiring into the enable flow lands with M3 lifecycle.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Render the per-node SMB config set: fetch the mgr/smb sambacc document
from config_uri, translate it for this backend (classic ceph vfs
module, since the snap's samba 4.19 has no ceph_new; inject a
microceph globals section carrying the proven snap paths, netbios
name and CTDB wiring) and emit smb.conf through the bundled sambacc
print-config (pure-python path). CTDB files render natively: ctdb.conf
points the 4.19 'cluster lock' at the bundled rados mutex helper with
a per-cluster object in the lock pool's default namespace (the 4.19
helper has no namespace support, so the namespaced cluster_lock_uri
object stays with mgr/smb untouched), nodes carries caller-ordered
private IPs, public_addresses resolves each VIP's interface on-node.
Clustered specs additionally get an osd cap for the reclock prefix.
All writes are atomic tmp+rename; golden-file tests cover each
rendered file.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Wire the smb placement flow end to end: ServiceInit now creates the
per-cluster directories (root-owned; confined daemons have no
dac_override), ensures keyrings, renders configs with row-id-ordered
node IPs (CTDB node numbers are line indices, so ordering must be
stable and append-only), populates CTDB_BASE (stock deb-enabled event
script links, the snapctl 50.samba script, functions, script.options)
and starts ctdbd; PostPlacementCheck verifies ctdbd stays up. Disable
stops ctdbd/smbd, removes keyrings, configs, runtime and per-cluster
data dirs, then the DB record. Membership or spec changes now trigger
a serialized regenerate (re-render + ctdbd restart) across all desired
members via a node-scoped POST, so every nodes file converges to the
same content. VIP interfaces resolve against local subnets.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Live single-node bring-up surfaced four gaps, all fixed at the root:
ctdbd's compiled-in paths need overriding inside the snap ([database]
directories and the logging location in the rendered ctdb.conf, plus
CTDB_EVENTD/CTDB_*_HELPER/CTDB_HELPER_BINDIR exports and a /run/ctdb
pid dir in the start wrapper); CTDB_BASE additionally needs notify.sh
linked. The nodes file rendered empty on first enable because
rendering runs before DbUpdate records the local node; the local node
is now appended when its row is missing, preserving row-id order.
With these, ctdbd reaches recovery NORMAL and takes the RADOS cluster
lock with the per-node daemon key.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Removing conf/ctdb outright leaves the snap namespace's /etc/ctdb
layout bind pointing at a dead inode, so services read an empty ghost
directory until the namespace is rebuilt (observed live after a
disable/re-enable cycle). Clear the directory's contents instead.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
An enabled ctdbd starting before the deployment engine has rendered
ctdb.conf (snap refresh, node reboot) crashlooped into systemd's
restart rate limit, which then blocked the legitimate start during
enable. Wait for the config file instead of exiting.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
The leader-proxied UseTarget fan-out canceled long-running enable and
regenerate legs mid-flight ('context canceled' surfacing at the next
DB or client call on the target node), observed repeatedly during the
three-node apply. Member names and addresses now come from the local
trust store (heartbeat-fresh, no network call) and each node operation
connects straight to its target via Connect().Member(), removing the
proxy hop entirely. Cluster-scope client timeouts sized to the full
multi-node apply (900s) and node-scope ones to a single node's enable
(300s); the unused GetClusterMemberAddresses client wrap is reverted.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
CTDB shuts a node down when its cluster lock command line differs from
the leader's, and ours differed twice per node: the helper path used
the revision-specific $SNAP directory (x8 vs x10 across nodes) and
the lock entity was the per-node daemon key. The lock line now uses
the /snap/<name>/current alias and a shared per-cluster entity
(client.smb.<cluster_id>) that every node ensures on enable; the
reclock cap moves from the daemon keys to that entity, and RemoveSMB
retires it after the last node leaves.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Implement apply_smb and remove_service('smb.<id>') in the microceph
orchestrator module, forwarding the SMBSpec to microcephd over the
unix-socket REST client (PUT /1.0/services/smb, DELETE on remove).
ServiceSpec.to_json() nests subclass fields (cluster_id, config_uri,
...) under a 'spec' key; apply_smb flattens that into microcephd's
flat SMBSpec wire format, which the daemon validates strictly (an
unflattened payload fails with "cluster_id '' is not a valid ID").

Adds pytest coverage: conftest stubs the mgr runtime imports
(mgr_module, orchestrator, ceph.deployment, snaphelpers) so the module
imports outside a mgr, FakeSession asserts the wire format, and module
routing tests exercise apply/remove dispatch with a fake spec that
mirrors the real nested to_json() shape. pyproject gains a dev
dependency group (pytest) and a hatchling build so the package is
installable; .gitignore keeps __pycache__ and .venv out of the tree.

uv run pytest tests/ -> 7 passed

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
The grouped-service re-enable loop passes the group's service name to
snapctl, but the smb group has no snap app of its own: ctdbd is the
unit to start, and it brings up smbd via its event script. On every
daemon start with an smb cluster in the DB this logged 'snapctl start
microceph.smb: unknown service' and left an inactive smb node down
after reboot.

go test ./ceph/ -run Start -> PASS (incl. TestReEnableSMBGroupStartsCtdbd)

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
mgr/smb expands the default 'samba-vfs' share provider to the proxied
variant, so every default 'ceph smb share create' produces a spec with
the cephfs-proxy feature and fails validation. Name the working
provider in the error so users are not left guessing.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
The enable flow rendered configs and started services but never
imported user_sources into the passdb, leaving every mgr-created
cluster with no way to authenticate (M3 seeded users by hand).
ApplySMB now ends with a node-scoped seeding call on the first placed
member: fetch each user_sources document (both rados:// pool objects
and the rados:mon-config-key: scheme mgr/smb actually publishes),
parse the sambacc users JSON and smbpasswd -s -a each entry against
the rendered smb.conf, password over stdin. Seeding runs on every
apply, not just spec changes, because the document behind a URI can
change while the URI stays identical; the passdb is CTDB-replicated
so one member covers the cluster. Imports retry while CTDB finishes
recovery. Matching system users remain the admin's task in Phase 1.

go test ./ceph/ ./api/... ./client/... -> ok

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
A spec change with steady membership only regenerates: enable is the
sole place keyrings were fetched, so entities added by later spec
revisions never reached the nodes. Concretely, the first share on an
mgr-created cluster adds client.smb.fs.cluster.<id> (the vfs_ceph
user) to include_ceph_users, the regenerated smb.conf references it,
and every tree connect fails NT_STATUS_UNSUCCESSFUL on the missing
keyring. EnsureSMBKeyrings is idempotent and also re-converges
URI-derived caps, so regenerate now runs it before rendering.

go test ./ceph/ -> ok

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
populateCTDBBase linked event scripts, functions and notify.sh into
the revisioned $SNAP dir. snapd keeps only two revisions, so the
second refresh after an enable garbage-collects the link targets and
ctdbd crash-loops on 'Failed to run init event'. Link through
/snap/<name>/current instead (SnapStable, already used for the
cluster lock helper line). The lifecycle test env now keeps Snap and
SnapStable distinct so a regression fails the symlink assertions.

go test ./ceph/ -> ok

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
ceph orch ls constructs a spec per service; a generic ServiceSpec
with service_type='smb' dispatches to SMBSpec through the service
registry and fails validation without a cluster_id, breaking orch ls
for every deployment with an smb cluster. describe_service now pulls
the verbatim specs microcephd stores (one list_smb call when any smb
service is recorded) and rebuilds them via ServiceSpec.from_json,
skipping clusters with no stored spec.

uv run pytest tests/ -> 9 passed

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Covers the Phase 1 mgr-driven lifecycle on a 3-node inner cluster:
orchestrator backend enablement, cluster+share creation via ceph smb
(declarative apply for the share, since the imperative default
provider expands to the proxied vfs), roundtrip via a CTDB public
address with smbclient, VIP-holder kill/failover/rejoin, and cluster
removal. The snap is re-installed in devmode by the suite (confined
smbd needs the pending smb-support snapd interface).

Per the harness rules, decisions parse in Python: smb_ops.py carries
the pure helpers (VIP derivation from the cluster CIDR, ctdb -X
status health count, VIP-to-pnn lookup, share spec document) with
pytest coverage, and the harness class gains the ctdb exec wrapper,
a _poll_until-based health waiter and the devmode reinstall step.

tox -e robot -- --dryrun --test-suite smb-tests -> 5 tests resolve
pytest test_harness_helpers.py -> 116 passed

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
How-to page for serving SMB shares: devmode requirement and the
pending smb-support snapd interface, orchestrator backend enablement,
subvolume and system-user preparation, CTDB-clustered cluster create
with public addresses, declarative share creation (and why the
imperative default provider is rejected), client connection, the
reconnect-based failover semantics, and removal. Indexed under
'Consuming cluster storage'.

docs: make html (--fail-on-warning) -> build succeeded

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
Upstream expands the abbreviated 'samba-vfs' provider to the proxied
variant, so the plain 'ceph smb share create' always produced a spec
with the cephfs-proxy feature and failed microcephd validation,
breaking the native CLI experience the mgr/smb integration exists
for. Patch the staged smb module so the default expands to
samba-vfs/new: microceph serves shares via direct libcephfs and
deploys no proxy daemon. Stored resources keep the abbreviated
provider, so the same document still expands to proxied on cephadm.
An explicit samba-vfs/proxied request keeps its upstream meaning and
is still rejected; the rejection message no longer blames the
default.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
With the default provider patched to the non-proxied vfs, the
declarative-apply workaround (yaml document shipped base64 into the
container) collapses to one 'ceph smb share create' line; drop the
now-unused smb_share_spec_yaml helper and its pytest case.

tox -e robot -- --dryrun --test-suite smb-tests -> 5 tests resolve
pytest test_harness_helpers.py -> 115 passed

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
The provider workaround is gone; document the plain CLI and note the
patched default expansion plus the proxied-provider rejection.

docs: make html (--fail-on-warning) -> build succeeded

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
The ceph part's patch loop runs against CRAFT_STAGE, where another
part's files are only present if that part happened to stage first;
patching mgr/smb from there failed on a clean rebuild (and the loop
also tripped over the retired stub patch still cached in the part
source). Apply the provider-default patch from the mgr-smb part's
own override-build against its CRAFT_PART_INSTALL, and move it to
patches/mgr-smb/ so the ceph loop's glob no longer picks it up.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
EnableService now renders ceph.conf via updateConfigFunc before enabling
any service (main's deferred bootstrap change), so the SMB placement
tests must bypass the database-dependent rendering the same way the NFS
and generic placement suites do.

Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
@UtkarshBhatthere
UtkarshBhatthere marked this pull request as draft July 12, 2026 09:25
Assisted-by: claude-code:claude-fable-5
Signed-off-by: Utkarsh Bhatt <utkarsh_bhatt@outlook.com>
@UtkarshBhatthere

Copy link
Copy Markdown
Contributor Author

Spec-review follow-up on custom_ports: checkSMBUnsupportedFields (microceph/ceph/service_placement_smb.go:77) currently rejects it alongside the permanently-unsupported fields. Upstream mgr/smb supports custom ports end to end (validated in mgr/smb/resources.py, and for clustered deployments the handler emits smb ports = <port> into the generated globals), so this should be treated as a temporary rejection, not a design exclusion.

Decision from spec review: allow the upstream smb port remap in a follow-up PR; not a blocker for this one. Unblocking means consuming the spec value at the three sites that hardcode 445 today:

  • the hospitality-check test bind (smbPort const, microceph/ceph/service_placement_smb.go:15)
  • the config renderer (emit the smb ports global when a custom port is set, microceph/ceph/smb_config.go)
  • the daemon description (svc_ports = [445], microceph-orch/src/microceph/module.py)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant