Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1868ddc
snap: stage samba/ctdb and add smbd service (experiment)
UtkarshBhatthere Jul 10, 2026
26fffa8
api: add SMBSpec types for native SMB
UtkarshBhatthere Jul 11, 2026
20c7b71
daemon: add smb service placement
UtkarshBhatthere Jul 11, 2026
1df9276
daemon: add smb cluster endpoints and orchestration
UtkarshBhatthere Jul 11, 2026
20ece8c
snap: productize samba/ctdb packaging for native SMB
UtkarshBhatthere Jul 11, 2026
8046564
snap: ship real mgr/smb module and fix samba part priming
UtkarshBhatthere Jul 11, 2026
1822d30
cli: add hidden smb debug commands
UtkarshBhatthere Jul 11, 2026
760b9e0
daemon: pass request context to HospitalityCheck
UtkarshBhatthere Jul 11, 2026
7668c22
daemon: add smb cephx keyring management
UtkarshBhatthere Jul 11, 2026
bf10e63
daemon: add smb config rendering
UtkarshBhatthere Jul 11, 2026
f593b82
daemon: add smb node lifecycle
UtkarshBhatthere Jul 11, 2026
6252b61
daemon,snap: fix ctdb runtime paths and nodes-file self-inclusion
UtkarshBhatthere Jul 11, 2026
5318d52
daemon: keep the ctdb layout bind target on disable
UtkarshBhatthere Jul 11, 2026
218f2f8
snap: idle ctdbd until its config is rendered
UtkarshBhatthere Jul 11, 2026
76cadea
daemon: fan out smb node operations over direct member clients
UtkarshBhatthere Jul 11, 2026
ac19028
daemon: make the ctdb cluster lock line identical across nodes
UtkarshBhatthere Jul 11, 2026
278f762
orch: add smb service support
UtkarshBhatthere Jul 11, 2026
d405a0b
daemon: map the smb group to ctdbd in startup re-enablement
UtkarshBhatthere Jul 11, 2026
f35482d
daemon: point cephfs-proxy rejections at samba-vfs/new
UtkarshBhatthere Jul 11, 2026
4b3160a
daemon: seed smb passdb users from spec user_sources
UtkarshBhatthere Jul 11, 2026
4054262
daemon: converge smb keyrings on regenerate
UtkarshBhatthere Jul 11, 2026
f64da2f
daemon: anchor CTDB_BASE symlinks at the stable snap path
UtkarshBhatthere Jul 11, 2026
408c08d
orch: describe smb services with their stored SMBSpec
UtkarshBhatthere Jul 11, 2026
ed7462c
tests: add smb-tests robot suite
UtkarshBhatthere Jul 11, 2026
353a811
docs: add smb how-to
UtkarshBhatthere Jul 11, 2026
c691bfc
snap,daemon: default the mgr/smb share provider to direct vfs
UtkarshBhatthere Jul 11, 2026
bb1d31c
tests: create the smb share imperatively in smb-tests
UtkarshBhatthere Jul 11, 2026
33ebd79
docs: use the imperative smb share create
UtkarshBhatthere Jul 11, 2026
3f1ccf4
snap: apply the mgr-smb provider patch in its own part
UtkarshBhatthere Jul 11, 2026
95efb05
tests: stub updateConfigFunc in the smb placement suite
UtkarshBhatthere Jul 12, 2026
fce3393
docs: reword 'retryable' flagged by the docs spell check
UtkarshBhatthere Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/snap/how-to/enable-smb.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
.. _enable-smb:

Serve SMB shares from MicroCeph
===============================

MicroCeph can serve CephFS subvolumes over SMB using Samba, clustered
with CTDB for high availability. The feature is driven entirely through
the upstream ``ceph smb`` manager module: MicroCeph acts as its
orchestrator backend and deploys ``smbd``/``ctdbd`` on the placed nodes.

.. note::

SMB support currently requires the snap to be installed in devmode:
strictly confined ``smbd`` needs ``setgroups`` and the ``setuid``/
``setgid`` capabilities, which no existing snapd interface grants. A
dedicated ``smb-support`` interface is being proposed to snapd; until
it lands, install with ``--devmode``.

Prerequisites
-------------

- A bootstrapped MicroCeph cluster with OSDs and a CephFS filesystem.
- One unused IP address per placed node, in the nodes' subnet, to serve
as CTDB public addresses (VIPs). Clients connect to these.

Enable the orchestrator backend
-------------------------------

The ``smb`` manager module submits deployment specs to an orchestrator.
Point it at MicroCeph's:

.. code-block:: none

$ sudo microceph.ceph mgr module enable smb
$ sudo microceph.ceph mgr module enable microceph
$ sudo microceph.ceph orch set backend microceph
$ sudo microceph.ceph orch status
Backend: microceph
Available: Yes

Prepare the share path and users
--------------------------------

Create a subvolume to back the share. Setting the mode at creation
avoids having to mount the filesystem just to fix permissions:

.. code-block:: none

$ sudo microceph.ceph fs subvolume create newfs s1 --mode 0777

SMB users authenticate against Samba's clustered password database,
which MicroCeph seeds automatically, but each one must map to a system
user present on every placed node:

.. code-block:: none

$ sudo useradd -M -s /usr/sbin/nologin smbuser

Create the SMB cluster
----------------------

Create a CTDB-clustered SMB cluster with user authentication, placed on
three nodes, listing one public address per node:

.. code-block:: none

$ sudo microceph.ceph smb cluster create dev user \
--define-user-pass=smbuser%s3cr3t \
--placement=count:3 --clustering=always \
--public-addrs=10.0.0.200/24 \
--public-addrs=10.0.0.201/24 \
--public-addrs=10.0.0.202/24

Create the share
----------------

.. code-block:: none

$ sudo microceph.ceph smb share create dev share1 newfs / --subvolume=s1

.. note::

MicroCeph serves shares from ``smbd`` via direct libcephfs, so its
build of the ``smb`` module expands the default share provider to
the non-proxied ``samba-vfs/new`` variant. Shares explicitly
requesting ``samba-vfs/proxied`` are rejected: MicroCeph does not
deploy the cephfs-proxy daemon.

Inspect the deployment:

.. code-block:: none

$ sudo microceph.ceph smb show
$ sudo microceph.ceph orch ls
NAME PORTS RUNNING PLACEMENT
smb.dev 3/3 count:3

Connect from a client
---------------------

Any SMB client can connect through a public address:

.. code-block:: none

$ smbclient //10.0.0.200/share1 -U smbuser%s3cr3t
smb: \> put file.txt

Failover semantics
------------------

When a node fails, CTDB moves its public addresses to a surviving node.
This is reconnect-based failover, not transparent state migration: open
sessions against a failed node drop, and clients re-establish them
against the same address once it is re-hosted (typically well under two
minutes with default timers). Applications should treat an SMB session
drop as transient and retry the connection.

Remove the cluster
------------------

Removal is also driven through the manager module:

.. code-block:: none

$ sudo microceph.ceph smb share rm dev share1
$ sudo microceph.ceph smb cluster rm dev

This stops and removes ``smbd``/``ctdbd`` from all placed nodes and
deletes the per-cluster service state. The CephFS data backing the
share is left untouched.
1 change: 1 addition & 0 deletions docs/snap/how-to/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Follow these guides to learn how to make use of the storage provided by your clu

mount-block-device
mount-cephfs-share
Serve SMB shares <enable-smb>


Contact us
Expand Down
2 changes: 2 additions & 0 deletions microceph-orch/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/
.venv/
12 changes: 12 additions & 0 deletions microceph-orch/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ dependencies = [

[tool.uv.sources]
snap-helpers = { git = "https://github.com/albertodonato/snap-helpers" }

[dependency-groups]
dev = [
"pytest>=8",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/microceph"]
12 changes: 12 additions & 0 deletions microceph-orch/src/microceph/client/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ def list_disks(self) -> list[dict]:
disks = self._get("/1.0/disks")
return disks.get("metadata")

def apply_smb(self, spec_json: str) -> None:
"""Apply an SMBSpec JSON document cluster-wide."""
self._put("/1.0/services/smb", data=spec_json)

def remove_smb(self, cluster_id: str) -> None:
"""Remove an smb cluster from all its member nodes."""
self._delete("/1.0/services/smb", json={"cluster_id": cluster_id})

def list_smb(self) -> list[dict]:
"""List smb clusters with their specs and placement."""
return self._get("/1.0/services/smb").get("metadata")

def get_status(self) -> dict[str, dict]:
"""Get status of the cluster."""
cluster = self._get("/1.0/status")
Expand Down
44 changes: 43 additions & 1 deletion microceph-orch/src/microceph/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
MONSpec,
MDSSpec,
NFSServiceSpec,
SMBSpec,
)

from mgr_module import MgrModule
Expand Down Expand Up @@ -166,6 +167,17 @@ def describe_service(self,
recorded_services = self.microceph.services.list_services()
service_hostlist = self._get_service_hostlist(recorded_services)

# smb specs are stored verbatim in microcephd; reuse them so the
# description carries a valid SMBSpec (a generic ServiceSpec with
# service_type='smb' dispatches to SMBSpec and fails validation
# without cluster_id).
smb_specs = {}
if any(name.split('.')[0] == 'smb' for name in service_hostlist):
try:
smb_specs = {st['cluster_id']: st['spec'] for st in self.microceph.services.list_smb()}
except RemoteException as e:
logger.warning(f"failed to fetch smb specs: {e}")

service_descs = []
for svc_name, hostlist in service_hostlist.items():
spec = None
Expand All @@ -176,7 +188,12 @@ def describe_service(self,
if service_type and svc_type != service_type:
continue

if svc_type in daemon_spec_map:
if svc_type == 'smb':
if svc_id not in smb_specs:
logger.warning(f"no stored spec for smb cluster '{svc_id}'; skipping")
continue
spec = ServiceSpec.from_json(smb_specs[svc_id])
elif svc_type in daemon_spec_map:
spec = daemon_spec_map[svc_type](
service_id=svc_id, service_type=svc_type, placement=PlacementSpec(hosts=hostlist, count=len(hostlist))
)
Expand Down Expand Up @@ -221,6 +238,9 @@ def list_daemons(self,
info = json.loads(svc['info'])
svc_ip = None if "0.0.0.0" in info['bind_address'] else info['bind_address']
svc_ports = [info['bind_port']]

if svc_daemon_type == 'smb':
svc_ports = [445]

descriptions.append(DaemonDescription(
service_name=svc_name,
Expand Down Expand Up @@ -256,6 +276,28 @@ def get_inventory(self,

return inventory

@handle_orch_error
def apply_smb(self, spec: SMBSpec) -> str:
"""Deploy the smb cluster described by an mgr/smb SMBSpec."""
logger.info(f"applying smb spec for cluster {spec.cluster_id}")
# ServiceSpec.to_json() nests subclass fields (cluster_id, config_uri,
# ...) under a "spec" key; microcephd's SMBSpec wire format is flat.
data = dict(spec.to_json())
data.update(data.pop('spec', {}))
self.microceph.services.apply_smb(json.dumps(data))
return f"Scheduled smb.{spec.service_id} update..."

@handle_orch_error
def remove_service(self, service_name: str, force: bool = False) -> str:
"""Remove a service; only smb.<cluster_id> services are supported."""
svc_type, svc_id = self._elaborate_service(service_name)
if svc_type != 'smb' or not svc_id:
raise NotImplementedError(f"removing service {service_name} is not supported")

logger.info(f"removing smb cluster {svc_id}")
self.microceph.services.remove_smb(svc_id)
return f"Removed service {service_name}"

def apply_rbd_mirror(self, spec: ServiceSpec) -> OrchResult[str]:
logger.info(f"Received Apply Request for RBD Mirror: Spec: {vars(spec).items()}")
raise NotImplementedError()
Expand Down
88 changes: 88 additions & 0 deletions microceph-orch/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# SPDX-FileCopyrightText: 2026 - Canonical Ltd
# SPDX-License-Identifier: Apache-2.0
#
# The mgr-runtime packages (mgr_module, orchestrator, ceph.deployment)
# only exist inside a ceph-mgr daemon; stub them so importing the
# microceph package (whose __init__ pulls in module.py) works under
# pytest. Also stub snaphelpers, which requires snap environment vars.

import sys
import types


def _module(name, **attrs):
mod = types.ModuleType(name)
for key, value in attrs.items():
setattr(mod, key, value)
sys.modules.setdefault(name, mod)
return sys.modules[name]


def _cls(name):
"""A distinct permissive stub class per name (multiple inheritance
forbids reusing one class as several bases)."""

def __init__(self, *args, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)

return type(
name,
(object,),
{
"__init__": __init__,
# Tolerate generic annotations like OrchResult[str].
"__class_getitem__": classmethod(lambda cls, item: cls),
# Mirror ServiceSpec.from_json: build an instance carrying the
# document's keys as attributes.
"from_json": classmethod(lambda cls, data: cls(**data)),
},
)


def _identity_decorator(fn):
return fn


_module(
"ceph",
)
_module(
"ceph.deployment",
)
_module(
"ceph.deployment.inventory",
Device=_cls("Device"),
Devices=_cls("Devices"),
)
_module(
"ceph.deployment.service_spec",
ServiceSpec=_cls("ServiceSpec"),
PlacementSpec=_cls("PlacementSpec"),
RGWSpec=_cls("RGWSpec"),
MONSpec=_cls("MONSpec"),
MDSSpec=_cls("MDSSpec"),
NFSServiceSpec=_cls("NFSServiceSpec"),
SMBSpec=_cls("SMBSpec"),
)
_module(
"mgr_module",
MgrModule=_cls("MgrModule"),
NotifyType=_cls("NotifyType"),
)
_module(
"orchestrator",
Orchestrator=_cls("Orchestrator"),
HostSpec=_cls("HostSpec"),
InventoryFilter=_cls("InventoryFilter"),
InventoryHost=_cls("InventoryHost"),
ServiceDescription=_cls("ServiceDescription"),
DaemonDescription=_cls("DaemonDescription"),
CLICommandMeta=type,
handle_orch_error=_identity_decorator,
OrchResult=_cls("OrchResult"),
)
_module(
"snaphelpers",
Snap=_cls("Snap"),
)
Loading
Loading