mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-14 00:46:34 +03:00
612 lines
17 KiB
Go
612 lines
17 KiB
Go
// SPDX-License-Identifier: BSD-2-Clause
|
|
//
|
|
// Copyright (c) 2025 The FreeBSD Foundation.
|
|
//
|
|
// This software was developed by Hayzam Sherif <hayzam@alchemilla.io>
|
|
// of Alchemilla Ventures Pvt. Ltd. <hello@alchemilla.io>,
|
|
// under sponsorship from the FreeBSD Foundation.
|
|
|
|
package samba
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/alchemillahq/gzfs"
|
|
sambaModels "github.com/alchemillahq/sylve/internal/db/models/samba"
|
|
"github.com/alchemillahq/sylve/internal/logger"
|
|
"github.com/alchemillahq/sylve/pkg/system"
|
|
"github.com/alchemillahq/sylve/pkg/utils"
|
|
|
|
iface "github.com/alchemillahq/sylve/pkg/network/iface"
|
|
)
|
|
|
|
const (
|
|
sambaACLType = "nfsv4"
|
|
sambaACLMode = "restricted"
|
|
sambaACLInherit = "passthrough"
|
|
)
|
|
|
|
func (s *Service) GetGlobalConfig() (sambaModels.SambaSettings, error) {
|
|
var settings sambaModels.SambaSettings
|
|
if err := s.DB.First(&settings).Error; err != nil {
|
|
return sambaModels.SambaSettings{}, fmt.Errorf("failed to retrieve Samba settings: %w", err)
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
func (s *Service) SetGlobalConfig(
|
|
ctx context.Context,
|
|
unixCharset string,
|
|
workgroup string,
|
|
serverString string,
|
|
interfaces string,
|
|
bindInterfacesOnly bool,
|
|
appleExtensions bool) error {
|
|
if unixCharset == "" || workgroup == "" || serverString == "" {
|
|
return fmt.Errorf("unixCharset, workgroup, and serverString cannot be empty")
|
|
}
|
|
|
|
if interfaces == "" {
|
|
interfaces = "lo0"
|
|
}
|
|
|
|
supportedCharsets := utils.GetSupportedCharsets()
|
|
|
|
if !utils.StringInSlice(unixCharset, supportedCharsets) {
|
|
return fmt.Errorf("unsupported unixCharset: %s", unixCharset)
|
|
}
|
|
|
|
if !utils.IsValidWorkgroup(workgroup) {
|
|
return fmt.Errorf("invalid workgroup name: %s", workgroup)
|
|
}
|
|
|
|
if !utils.IsValidServerString(serverString) {
|
|
return fmt.Errorf("invalid server string: %s", serverString)
|
|
}
|
|
|
|
interfacesList := strings.Split(interfaces, ",")
|
|
interfacesList = utils.RemoveDuplicates(interfacesList)
|
|
|
|
for _, eIface := range interfacesList {
|
|
eIface = strings.TrimSpace(eIface)
|
|
_, err := iface.Get(eIface)
|
|
if err != nil && !strings.Contains(err.Error(), "not found") {
|
|
return fmt.Errorf("invalid interface '%s': %w", eIface, err)
|
|
} else if err != nil && strings.Contains(err.Error(), "not found") {
|
|
logger.L.Warn().Str("interface", eIface).Msg("Interface not found, continuing without it")
|
|
interfacesList = utils.RemoveStringFromSlice(interfacesList, eIface)
|
|
}
|
|
}
|
|
|
|
if len(interfacesList) > 0 {
|
|
interfaces = strings.Join(interfacesList, ",")
|
|
} else {
|
|
interfaces = "lo0"
|
|
}
|
|
|
|
var settings sambaModels.SambaSettings
|
|
if err := s.DB.First(&settings).Error; err != nil {
|
|
return fmt.Errorf("failed to retrieve Samba settings: %w", err)
|
|
}
|
|
|
|
settings.UnixCharset = unixCharset
|
|
settings.Workgroup = workgroup
|
|
settings.ServerString = serverString
|
|
settings.Interfaces = interfaces
|
|
settings.BindInterfacesOnly = bindInterfacesOnly
|
|
settings.AppleExtensions = appleExtensions
|
|
|
|
if err := s.DB.Save(&settings).Error; err != nil {
|
|
return fmt.Errorf("failed to update Samba settings: %w", err)
|
|
}
|
|
|
|
return s.WriteConfig(ctx, true)
|
|
}
|
|
|
|
func (s *Service) hasGuestOnlyShares() (bool, error) {
|
|
var count int64
|
|
if err := s.DB.Model(&sambaModels.SambaShare{}).Where("guest_ok = ?", true).Count(&count).Error; err != nil {
|
|
return false, fmt.Errorf("failed_to_check_guest_shares: %w", err)
|
|
}
|
|
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (s *Service) ensureSambaDatasetACLProperties(
|
|
ctx context.Context,
|
|
dataset *gzfs.Dataset,
|
|
strict bool,
|
|
) error {
|
|
if dataset == nil {
|
|
err := fmt.Errorf("dataset_not_found")
|
|
if strict {
|
|
return err
|
|
}
|
|
|
|
logger.L.Warn().Err(err).Msg("failed_to_enforce_samba_dataset_acl_properties")
|
|
return nil
|
|
}
|
|
|
|
if dataset.Type != gzfs.DatasetTypeFilesystem {
|
|
err := fmt.Errorf("dataset_not_filesystem: %s", dataset.Name)
|
|
if strict {
|
|
return err
|
|
}
|
|
|
|
logger.L.Warn().Err(err).Str("dataset", dataset.Name).Msg("failed_to_enforce_samba_dataset_acl_properties")
|
|
return nil
|
|
}
|
|
|
|
if err := dataset.SetProperties(
|
|
ctx,
|
|
"acltype", sambaACLType,
|
|
"aclmode", sambaACLMode,
|
|
"aclinherit", sambaACLInherit,
|
|
); err != nil {
|
|
wrapped := fmt.Errorf("failed_to_set_samba_acl_properties_for_dataset_%s: %w", dataset.Name, err)
|
|
if strict {
|
|
return wrapped
|
|
}
|
|
|
|
logger.L.Warn().Err(wrapped).Str("dataset", dataset.Name).Msg("failed_to_enforce_samba_dataset_acl_properties")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func uniquePrincipalNames(names []string) []string {
|
|
seen := make(map[string]struct{}, len(names))
|
|
out := make([]string, 0, len(names))
|
|
|
|
for _, name := range names {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
if _, exists := seen[name]; exists {
|
|
continue
|
|
}
|
|
|
|
seen[name] = struct{}{}
|
|
out = append(out, name)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func normalizeSambaPrincipalNames(input sambaPrincipalNames) sambaPrincipalNames {
|
|
normalized := sambaPrincipalNames{
|
|
ReadUsers: uniquePrincipalNames(input.ReadUsers),
|
|
WriteUsers: uniquePrincipalNames(input.WriteUsers),
|
|
ReadGroups: uniquePrincipalNames(input.ReadGroups),
|
|
WriteGroups: uniquePrincipalNames(input.WriteGroups),
|
|
}
|
|
|
|
writeUsers := make(map[string]struct{}, len(normalized.WriteUsers))
|
|
for _, user := range normalized.WriteUsers {
|
|
writeUsers[user] = struct{}{}
|
|
}
|
|
|
|
filteredReadUsers := make([]string, 0, len(normalized.ReadUsers))
|
|
for _, user := range normalized.ReadUsers {
|
|
if _, exists := writeUsers[user]; exists {
|
|
continue
|
|
}
|
|
filteredReadUsers = append(filteredReadUsers, user)
|
|
}
|
|
normalized.ReadUsers = filteredReadUsers
|
|
|
|
writeGroups := make(map[string]struct{}, len(normalized.WriteGroups))
|
|
for _, group := range normalized.WriteGroups {
|
|
writeGroups[group] = struct{}{}
|
|
}
|
|
|
|
filteredReadGroups := make([]string, 0, len(normalized.ReadGroups))
|
|
for _, group := range normalized.ReadGroups {
|
|
if _, exists := writeGroups[group]; exists {
|
|
continue
|
|
}
|
|
filteredReadGroups = append(filteredReadGroups, group)
|
|
}
|
|
normalized.ReadGroups = filteredReadGroups
|
|
|
|
return normalized
|
|
}
|
|
|
|
func mergePrincipalNames(lists ...[]string) []string {
|
|
merged := make([]string, 0)
|
|
for _, list := range lists {
|
|
merged = append(merged, list...)
|
|
}
|
|
return uniquePrincipalNames(merged)
|
|
}
|
|
|
|
func (s *Service) syncSambaDatasetPrincipalACLs(
|
|
mountpoint string,
|
|
previous sambaPrincipalNames,
|
|
desired sambaPrincipalNames,
|
|
strict bool,
|
|
) error {
|
|
if mountpoint == "" || mountpoint == "-" {
|
|
err := fmt.Errorf("dataset_not_mounted")
|
|
if strict {
|
|
return err
|
|
}
|
|
|
|
logger.L.Warn().Err(err).Str("mountpoint", mountpoint).Msg("failed_to_enforce_samba_dataset_principal_acls")
|
|
return nil
|
|
}
|
|
|
|
previous = normalizeSambaPrincipalNames(previous)
|
|
desired = normalizeSambaPrincipalNames(desired)
|
|
|
|
removeACL := func(principalType string, principalName string, permissionSet string) {
|
|
entry := fmt.Sprintf("%s:%s:%s:fd:allow", principalType, principalName, permissionSet)
|
|
|
|
if _, err := utils.RunCommand("/bin/setfacl", "-x", entry, mountpoint); err != nil {
|
|
logger.L.Warn().
|
|
Err(err).
|
|
Str("principal", principalName).
|
|
Str("principal_type", principalType).
|
|
Str("permission_set", permissionSet).
|
|
Str("mountpoint", mountpoint).
|
|
Msg("failed_to_remove_samba_dataset_principal_acl_entry")
|
|
}
|
|
}
|
|
|
|
addACL := func(principalType string, principalName string, permissionSet string) error {
|
|
entry := fmt.Sprintf("%s:%s:%s:fd:allow", principalType, principalName, permissionSet)
|
|
|
|
_, err := utils.RunCommand("/bin/setfacl", "-m", entry, mountpoint)
|
|
if err != nil {
|
|
wrapped := fmt.Errorf(
|
|
"failed_to_set_acl_for_%s_%s_on_%s: %w",
|
|
principalType,
|
|
principalName,
|
|
mountpoint,
|
|
err,
|
|
)
|
|
if strict {
|
|
return wrapped
|
|
}
|
|
|
|
logger.L.Warn().
|
|
Err(wrapped).
|
|
Str("principal", principalName).
|
|
Str("principal_type", principalType).
|
|
Str("permission_set", permissionSet).
|
|
Str("mountpoint", mountpoint).
|
|
Msg("failed_to_enforce_samba_dataset_principal_acls")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
targetUsers := mergePrincipalNames(previous.ReadUsers, previous.WriteUsers, desired.ReadUsers, desired.WriteUsers)
|
|
targetGroups := mergePrincipalNames(previous.ReadGroups, previous.WriteGroups, desired.ReadGroups, desired.WriteGroups)
|
|
|
|
for _, user := range targetUsers {
|
|
removeACL("u", user, "read_set")
|
|
removeACL("u", user, "modify_set")
|
|
}
|
|
|
|
for _, group := range targetGroups {
|
|
removeACL("g", group, "read_set")
|
|
removeACL("g", group, "modify_set")
|
|
}
|
|
|
|
for _, user := range desired.ReadUsers {
|
|
if err := addACL("u", user, "read_set"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, user := range desired.WriteUsers {
|
|
if err := addACL("u", user, "modify_set"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, group := range desired.ReadGroups {
|
|
if err := addACL("g", group, "read_set"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, group := range desired.WriteGroups {
|
|
if err := addACL("g", group, "modify_set"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) GlobalConfig() (string, error) {
|
|
settings, err := s.GetGlobalConfig()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get global Samba settings: %w", err)
|
|
}
|
|
|
|
var config string
|
|
config += "# === This file is automatically generated by Sylve, don't edit! ===\n"
|
|
|
|
config += "[global]\n"
|
|
config += fmt.Sprintf("unix charset = %s\n", settings.UnixCharset)
|
|
config += fmt.Sprintf("workgroup = %s\n", settings.Workgroup)
|
|
config += fmt.Sprintf("server string = %s\n", settings.ServerString)
|
|
|
|
interfaces := settings.Interfaces
|
|
if interfaces == "" {
|
|
interfaces = "lo0"
|
|
} else {
|
|
interfaces = strings.ReplaceAll(interfaces, ",", " ")
|
|
}
|
|
|
|
config += fmt.Sprintf("interfaces = %s\n", interfaces)
|
|
|
|
if settings.BindInterfacesOnly {
|
|
config += "bind interfaces only = yes\n"
|
|
} else {
|
|
config += "bind interfaces only = no\n"
|
|
}
|
|
|
|
hasGuestShares, err := s.hasGuestOnlyShares()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if hasGuestShares {
|
|
config += "map to guest = Bad User\n"
|
|
}
|
|
|
|
if settings.AppleExtensions {
|
|
config += "min protocol = SMB2\n"
|
|
config += "ea support = yes\n"
|
|
config += "vfs objects = fruit streams_xattr full_audit zfsacl\n"
|
|
config += "fruit:metadata = stream\n"
|
|
config += "fruit:model = MacSamba\n"
|
|
config += "fruit:veto_appledouble = no\n"
|
|
config += "fruit:nfs_aces = no\n"
|
|
config += "fruit:wipe_intentionally_left_blank_rfork = yes\n"
|
|
config += "fruit:delete_empty_adfiles = yes\n"
|
|
config += "fruit:posix_rename = yes\n"
|
|
} else {
|
|
config += "vfs objects = full_audit zfsacl\n"
|
|
}
|
|
config += "inherit acls = yes\n"
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func (s *Service) ShareConfig(ctx context.Context) (string, error) {
|
|
shares := []sambaModels.SambaShare{}
|
|
if err := s.DB.
|
|
Preload("ReadOnlyUsers").
|
|
Preload("WriteableUsers").
|
|
Preload("ReadOnlyGroups").
|
|
Preload("WriteableGroups").
|
|
Find(&shares).Error; err != nil {
|
|
return "", fmt.Errorf("failed to retrieve Samba shares: %w", err)
|
|
}
|
|
|
|
var datasets = make(map[string]*gzfs.Dataset)
|
|
for _, share := range shares {
|
|
if _, exists := datasets[share.Dataset]; !exists {
|
|
ds, err := s.GZFS.ZFS.GetByGUID(ctx, share.Dataset, false)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to fetch dataset for share %s: %v", share.Name, err)
|
|
}
|
|
|
|
if ds == nil {
|
|
return "", fmt.Errorf("dataset for share %s not found", share.Name)
|
|
}
|
|
|
|
if ds.Mountpoint == "-" || ds.Mountpoint == "" {
|
|
return "", fmt.Errorf("dataset %s for share %s is not mounted", ds.Name, share.Name)
|
|
}
|
|
|
|
// Best-effort during config generation so a single property-set failure
|
|
// doesn't prevent Samba from reloading otherwise valid share config.
|
|
_ = s.ensureSambaDatasetACLProperties(ctx, ds, false)
|
|
|
|
datasets[share.Dataset] = ds
|
|
}
|
|
}
|
|
|
|
var config strings.Builder
|
|
for _, share := range shares {
|
|
dataset := datasets[share.Dataset]
|
|
|
|
config.WriteString(fmt.Sprintf("[%s]\n", share.Name))
|
|
config.WriteString(fmt.Sprintf("\tpath = %s\n", dataset.Mountpoint))
|
|
|
|
if share.GuestOk {
|
|
config.WriteString(fmt.Sprintf("\tguest ok = yes\n"))
|
|
config.WriteString("\tguest only = yes\n")
|
|
|
|
if share.ReadOnly {
|
|
config.WriteString("\tread only = yes\n")
|
|
} else {
|
|
config.WriteString("\tread only = no\n")
|
|
}
|
|
} else {
|
|
config.WriteString(fmt.Sprintf("\tguest ok = no\n"))
|
|
}
|
|
|
|
principals := namesFromShareAssociations(share)
|
|
principals = normalizeSambaPrincipalNames(principals)
|
|
|
|
if !share.GuestOk {
|
|
// Best-effort during config generation to avoid breaking Samba reload.
|
|
_ = s.syncSambaDatasetPrincipalACLs(dataset.Mountpoint, sambaPrincipalNames{}, principals, false)
|
|
}
|
|
|
|
readUsers := principals.ReadUsers
|
|
writeUsers := principals.WriteUsers
|
|
readGroups := principals.ReadGroups
|
|
writeGroups := principals.WriteGroups
|
|
|
|
validUsers := make([]string, 0, len(readUsers)+len(writeUsers)+len(readGroups)+len(writeGroups))
|
|
validUsers = append(validUsers, readUsers...)
|
|
validUsers = append(validUsers, writeUsers...)
|
|
for _, group := range readGroups {
|
|
validUsers = append(validUsers, "@"+group)
|
|
}
|
|
for _, group := range writeGroups {
|
|
validUsers = append(validUsers, "@"+group)
|
|
}
|
|
validUsers = uniquePrincipalNames(validUsers)
|
|
|
|
writeList := make([]string, 0, len(writeUsers)+len(writeGroups))
|
|
writeList = append(writeList, writeUsers...)
|
|
for _, group := range writeGroups {
|
|
writeList = append(writeList, "@"+group)
|
|
}
|
|
writeList = uniquePrincipalNames(writeList)
|
|
|
|
readPrincipalCount := len(readUsers) + len(readGroups)
|
|
writePrincipalCount := len(writeUsers) + len(writeGroups)
|
|
|
|
if !share.GuestOk && len(validUsers) > 0 {
|
|
config.WriteString(fmt.Sprintf("\tvalid users = %s\n", strings.Join(validUsers, " ")))
|
|
}
|
|
|
|
if !share.GuestOk {
|
|
if writePrincipalCount == 0 || readPrincipalCount > 0 {
|
|
config.WriteString("\tread only = yes\n")
|
|
} else {
|
|
config.WriteString("\tread only = no\n")
|
|
}
|
|
}
|
|
|
|
if !share.GuestOk && len(writeList) > 0 {
|
|
config.WriteString(fmt.Sprintf("\twrite list = %s\n", strings.Join(writeList, " ")))
|
|
}
|
|
|
|
config.WriteString(fmt.Sprintf("\tcreate mask = %s\n", share.CreateMask))
|
|
config.WriteString(fmt.Sprintf("\tdirectory mask = %s\n", share.DirectoryMask))
|
|
if share.TimeMachine {
|
|
config.WriteString("\tfruit:time machine = yes\n")
|
|
if share.TimeMachineMaxSize > 0 {
|
|
config.WriteString(fmt.Sprintf("\tfruit:time machine max size = %dG\n", share.TimeMachineMaxSize))
|
|
}
|
|
}
|
|
config.WriteString("\tfull_audit:prefix = sylve-smb-al|%u|%I|%m|%S|%P\n")
|
|
config.WriteString("\tfull_audit:success = openat close read write renameat unlinkat mkdirat create_file connect disconnect\n")
|
|
config.WriteString("\tfull_audit:failure = all !getwd !get_real_filename !fgetxattr !fget_dos_attributes\n")
|
|
config.WriteString("\tfull_audit:facility = LOCAL7\n")
|
|
config.WriteString("\tfull_audit:priority = ALERT\n")
|
|
config.WriteString("\tfull_audit:syslog = true\n")
|
|
config.WriteString("\tfull_audit:log_secdesc = true\n")
|
|
|
|
config.WriteString("\n\n")
|
|
}
|
|
|
|
return config.String(), nil
|
|
}
|
|
|
|
func (s *Service) WriteAvahiConfig() error {
|
|
var shares []sambaModels.SambaShare
|
|
if err := s.DB.Where("time_machine = ?", true).Find(&shares).Error; err != nil {
|
|
return fmt.Errorf("failed to retrieve Time Machine shares: %w", err)
|
|
}
|
|
|
|
var diskEntries string
|
|
for i, share := range shares {
|
|
diskEntries += fmt.Sprintf("\t\t<txt-record>dk%d=adVN=%s,adVF=0x82</txt-record>\n", i, share.Name)
|
|
}
|
|
|
|
xml := fmt.Sprintf(`<?xml version="1.0" standalone='no'?>
|
|
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
|
<service-group>
|
|
<name replace-wildcards="yes">%%h</name>
|
|
<service>
|
|
<type>_smb._tcp</type>
|
|
<port>445</port>
|
|
</service>
|
|
<service>
|
|
<type>_device-info._tcp</type>
|
|
<port>0</port>
|
|
<txt-record>model=RackMac</txt-record>
|
|
</service>
|
|
<service>
|
|
<type>_adisk._tcp</type>
|
|
<txt-record>sys=waMa=0,adVF=0x100</txt-record>
|
|
%s </service>
|
|
</service-group>`, diskEntries)
|
|
|
|
dir := "/usr/local/etc/avahi/services"
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create avahi services directory: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(dir+"/timemachine.service", []byte(xml), 0644); err != nil {
|
|
return fmt.Errorf("failed to write Avahi config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) WriteConfig(ctx context.Context, reload bool) error {
|
|
gCfg, err := s.GlobalConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if gCfg == "" {
|
|
return fmt.Errorf("global configuration is empty")
|
|
}
|
|
|
|
shareCfg, err := s.ShareConfig(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get share configuration: %w", err)
|
|
}
|
|
|
|
fullConfig := gCfg + "\n" + shareCfg
|
|
filePath := "/usr/local/etc/smb4.conf"
|
|
|
|
if err := os.WriteFile(filePath, []byte(fullConfig), 0644); err != nil {
|
|
return fmt.Errorf("failed to write Samba configuration to %s: %w", filePath, err)
|
|
}
|
|
|
|
settings, err := s.GetGlobalConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get global config for avahi management: %w", err)
|
|
}
|
|
|
|
if settings.AppleExtensions {
|
|
if err := s.WriteAvahiConfig(); err != nil {
|
|
logger.L.Warn().Err(err).Msg("failed to write avahi config")
|
|
}
|
|
if err := system.ServiceAction("dbus", "onerestart"); err != nil {
|
|
logger.L.Warn().Err(err).Msg("failed to restart dbus")
|
|
}
|
|
if err := system.ServiceAction("avahi-daemon", "onerestart"); err != nil {
|
|
logger.L.Warn().Err(err).Msg("failed to restart avahi-daemon")
|
|
}
|
|
} else {
|
|
avahiPath := "/usr/local/etc/avahi/services/timemachine.service"
|
|
if _, err := os.Stat(avahiPath); err == nil {
|
|
if err := os.Remove(avahiPath); err != nil {
|
|
logger.L.Warn().Err(err).Msg("failed to remove avahi timemachine service file")
|
|
}
|
|
}
|
|
if err := system.ServiceAction("avahi-daemon", "onestop"); err != nil {
|
|
logger.L.Warn().Err(err).Msg("failed to stop avahi-daemon")
|
|
}
|
|
}
|
|
|
|
if reload {
|
|
if err := system.ServiceAction("samba_server", "onerestart"); err != nil {
|
|
return fmt.Errorf("failed to restart Samba service: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|