Skip to content
Merged
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
13089bf
initial service linux file with notes
Dabnsky Oct 7, 2024
c60d152
added parse and collect data from cloudfuse.service file
Dabnsky Oct 8, 2024
a7d254e
added data collect and user create draft
Dabnsky Oct 8, 2024
0cd2984
Added copy and systemctl commands to the installCmd
Dabnsky Oct 8, 2024
74faea2
added wrapper for data collection from file. started composing defaul…
Dabnsky Oct 9, 2024
b301ade
added wrapper function to edit service file for config or mount Paths
Dabnsky Oct 9, 2024
3fecd43
added sudo to commands
Dabnsky Oct 9, 2024
cf992a8
-fixed permission issues for mount
Dabnsky Oct 10, 2024
8e86337
added service removal automation.
Dabnsky Oct 10, 2024
2129c19
Added argument support and replaced default mount and config paths wi…
Dabnsky Oct 29, 2024
3ebbe21
correct Short and Long description for install
Dabnsky Nov 13, 2024
497706c
corrected usage
Dabnsky Nov 13, 2024
439ddcb
use os.stat to check file exists
Dabnsky Nov 13, 2024
be9c13b
changed modifyServiceFile() to createServiceFile()
Dabnsky Nov 14, 2024
60a3893
adjusted newServiceFile() to have correct paths and directly write fi…
Dabnsky Nov 15, 2024
8a2c390
adjusted collectServiceData to collectServiceUser
Dabnsky Nov 18, 2024
7d217ef
moved newServiceFile()
Dabnsky Nov 18, 2024
ce37b74
-added support for referencial path arguments
Dabnsky Nov 19, 2024
08feb84
-added service user getting added to current user without sudo
Dabnsky Nov 22, 2024
dbce110
adjusted how the service user gets assigned the appropriate group for…
Dabnsky Nov 22, 2024
429b77d
added --flag string support and added user specification support
Dabnsky Nov 22, 2024
ec849db
removed servOpts var
Dabnsky Nov 22, 2024
4d9390a
added required flags and unsilenced usage with cobra
Dabnsky Nov 22, 2024
ec38957
support servcie user already existing and setting permissions as needed.
Dabnsky Nov 22, 2024
bf51d9b
updated long install description
Dabnsky Dec 19, 2024
5ca9a39
replaced relative path support with filepath.IsAbs
Dabnsky Dec 19, 2024
b6bdefb
removed "error" from error msg
Dabnsky Dec 19, 2024
3f78e7e
removed mount path empty check
Dabnsky Dec 19, 2024
63b1be9
removed old TODO usage comment
Dabnsky Dec 19, 2024
ac1a870
updated short and long descriptions for uninstall
Dabnsky Dec 19, 2024
43fd36d
removed space before "[unit]" in template
Dabnsky Dec 20, 2024
dac35d0
removed unused dir variable
Dabnsky Dec 20, 2024
77a1eea
initiated err variable
Dabnsky Dec 20, 2024
ae54de5
corrected filepath.Abs usage
Dabnsky Dec 20, 2024
a07dd71
set up wrapper function for abs path
Dabnsky Dec 20, 2024
2c760f7
added error return for getAbsPath()
Dabnsky Jan 7, 2025
a908035
added err vars to calling getAbsPath()
Dabnsky Jan 7, 2025
72e00fd
moved string variables out of global scope
Dabnsky Jan 7, 2025
a8e503c
added a todo note for uninstall's mountPath
Dabnsky Jan 7, 2025
dbca1a0
added "cloudfuse-" to service filename
Dabnsky Jan 7, 2025
17b6d81
moved string vars back into global scope so init() can use them
Dabnsky Jan 7, 2025
0a195ae
used getAbsPath in uninstall and revised logic on finding and deletni…
Dabnsky Jan 7, 2025
26aab67
corrected how serviceFile is determined for uninstall
Dabnsky Jan 7, 2025
453f216
default service user value set to 'cloudfuse'
Dabnsky Jan 7, 2025
3b14cd5
changed useradd -r to -m instead
Dabnsky Jan 7, 2025
5113875
added TODO note on suggesting usermod commands instead of implementin…
Dabnsky Jan 7, 2025
3b5b42f
replaced usermod commands with println output suggestions
Dabnsky Jan 7, 2025
6bf3c5e
replaced perscriptive suggestion for a generic one
Dabnsky Jan 9, 2025
e4977a6
removed sudo from commands
Dabnsky Jan 9, 2025
d692d20
changed the service template to have mount and config set directly
Dabnsky Jan 9, 2025
bd54f84
removed isAbs check in getAbsPath()
Dabnsky Jan 16, 2025
8e2c372
removed old TODO notes
Dabnsky Jan 16, 2025
02541d3
removed "--user" from example
Dabnsky Jan 16, 2025
23ebf74
changed service name to have full path with '-' characters
Dabnsky Jan 16, 2025
1ddeb0a
replaced cmd running 'rm' command with os.remove.
Dabnsky Jan 16, 2025
2b6a6bd
added wrapper function for generating service file name
Dabnsky Jan 23, 2025
29e02cd
corrected getAbsPath argument
Dabnsky Jan 23, 2025
aff4253
removed spaces
Dabnsky Jan 23, 2025
d699358
Merge commit 'be485d5a7f3444e7d49e1ba755f72dda6bc786e5' into cloudfus…
Dabnsky Jan 23, 2025
6fbaadb
removed var declaration
Dabnsky Jan 28, 2025
c8a7eb5
added error handle wrapper function for init()
Dabnsky Jan 28, 2025
9280183
removed absPath helper function
Dabnsky Jan 28, 2025
cc01a7b
corrected uninstallCmd parameter from serviceName to mountPath
Dabnsky Jan 28, 2025
a029d67
removed new line from end of printeln string
Dabnsky Jan 28, 2025
bd4297c
Merge commit 'f4c829e9bff0690c81a3123615c92846d97f0591' into cloudfus…
Dabnsky Jan 28, 2025
150fd6c
updated year on copyright
Dabnsky Jan 28, 2025
9e4722f
removed "error" from message
Dabnsky Jan 28, 2025
1a5e99c
removed 'error' from all error messages
Dabnsky Jan 29, 2025
99bf7bf
fixed typo
Dabnsky Jan 30, 2025
c9f9379
fixed another typo
Dabnsky Jan 30, 2025
887b6e3
Merge commit 'f3e7f236258dd800659bc6a83c9bcc99268c087b' into cloudfus…
Dabnsky Feb 4, 2025
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
246 changes: 246 additions & 0 deletions cmd/service_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
//go:build linux

/*
Licensed under the MIT License <http://opensource.org/licenses/MIT>.

Copyright © 2023-2025 Seagate Technology LLC and/or its Affiliates

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*/

package cmd

import (
"errors"
"fmt"
"html/template"
"io/fs"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"

"github.com/Seagate/cloudfuse/common"
"github.com/spf13/cobra"
)

type serviceOptions struct {
ConfigFile string
MountPath string
ServiceUser string
}

// Section defining all the command that we have in secure feature
var serviceCmd = &cobra.Command{
Use: "service",
Short: "Manage cloudfuse startup process on Linux",
Long: "Manage cloudfuse startup process on Linux",
SuggestFor: []string{"ser", "serv"},
Example: "cloudfuse service install",
FlagErrorHandling: cobra.ExitOnError,
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("missing command options\n\nDid you mean this?\n\tcloudfuse service install\n\nRun 'cloudfuse service --help' for usage")
},
}

var mountPath string
var configPath string
var serviceUser string

var installCmd = &cobra.Command{
Use: "install",
Comment thread
foodprocessor marked this conversation as resolved.
Short: "Installs a service file for a single mount with Cloudfuse. Requires elevated permissions.",
Long: "Installs a service file for a single mount with Cloudfuse. Requires elevated permissions.",
SuggestFor: []string{"ins", "inst"},
Example: "cloudfuse service install --mount-path=<path/to/mount/point> --config-file=<path/to/config/file>",
FlagErrorHandling: cobra.ExitOnError,
RunE: func(cmd *cobra.Command, args []string) error {

mountPath, err := filepath.Abs(mountPath)
if err != nil {
return fmt.Errorf("couldn't couldn't determine absolute path from string [%s]", err.Error())
}
configPath, err := filepath.Abs(configPath)
if err != nil {
return fmt.Errorf("couldn't couldn't determine absolute path from string [%s]", err.Error())
}

mountExists := common.DirectoryExists(mountPath)
if !mountExists {
return fmt.Errorf("the mount path provided does not exist")
}
_, err = os.Stat(configPath)
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("the config file path provided does not exist")
}
//create the new user and set permissions
err = setUser(serviceUser, mountPath, configPath)
if err != nil {
fmt.Println("could not set up service user ", err)
return err
}

serviceName, err := newService(mountPath, configPath, serviceUser)
if err != nil {
return fmt.Errorf("unable to create service file: [%s]", err.Error())
}
// run systemctl daemon-reload
systemctlDaemonReloadCmd := exec.Command("systemctl", "daemon-reload")
err = systemctlDaemonReloadCmd.Run()
if err != nil {
return fmt.Errorf("failed to run 'systemctl daemon-reload' command [%s]", err.Error())
}
// Enable the service to start at system boot
systemctlEnableCmd := exec.Command("systemctl", "enable", serviceName)
err = systemctlEnableCmd.Run()
if err != nil {
return fmt.Errorf("failed to run 'systemctl daemon-reload' command due to following [%s]", err.Error())
}
return nil
},
}

var uninstallCmd = &cobra.Command{
Comment thread
foodprocessor marked this conversation as resolved.
Use: "uninstall",
Short: "Uninstall a startup process for Cloudfuse.",
Long: "Uninstall a startup process for Cloudfuse.",
SuggestFor: []string{"uninst", "uninstal"},
Example: "cloudfuse service uninstall --mount-path=<path/to/mount/path>",
FlagErrorHandling: cobra.ExitOnError,
RunE: func(cmd *cobra.Command, args []string) error {
// get absolute path of provided relative mount path

mountPath, err := filepath.Abs(mountPath)
if err != nil {
return fmt.Errorf("couldn't determine absolute path from string [%s]", err.Error())
}
serviceName, serviceFilePath := getService(mountPath)
if _, err := os.Stat(serviceFilePath); err == nil {
removeFileCmd := exec.Command("rm", serviceFilePath)
err := removeFileCmd.Run()
if err != nil {
return fmt.Errorf("failed to delete "+serviceName+" file from /etc/systemd/system [%s]", err.Error())
}
} else if os.IsNotExist(err) {
return fmt.Errorf("failed to delete "+serviceName+" file from /etc/systemd/system [%s]", err.Error())
}
// reload daemon
systemctlDaemonReloadCmd := exec.Command("systemctl", "daemon-reload")
err = systemctlDaemonReloadCmd.Run()
if err != nil {
return fmt.Errorf("failed to run 'systemctl daemon-reload' command [%s]", err.Error())
}
return nil
},
}

//--------------- command section ends

func newService(mountPath string, configPath string, serviceUser string) (string, error) {
serviceTemplate := `
[Unit]
Description=Cloudfuse is an open source project developed to provide a virtual filesystem backed by S3 or Azure storage.
After=network-online.target
Requires=network-online.target

[Service]
# User service will run as.
User={{.ServiceUser}}

# Under the hood
Type=forking
ExecStart=/usr/bin/cloudfuse mount {{.MountPath}} --config-file={{.ConfigFile}} -o allow_other
ExecStop=/usr/bin/fusermount -u {{.MountPath}} -z

[Install]
WantedBy=multi-user.target
`
config := serviceOptions{
ConfigFile: configPath,
MountPath: mountPath,
ServiceUser: serviceUser,
}

tmpl, err := template.New("service").Parse(serviceTemplate)
if err != nil {
return "", fmt.Errorf("could not create a new service file: [%s]", err.Error())
}
serviceName, serviceFilePath := getService(mountPath)
err = os.Remove(serviceFilePath)
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("failed to replace the service file [%s]", err.Error())
}

var newFile *os.File
newFile, err = os.Create(serviceFilePath)
if err != nil {
return "", fmt.Errorf("could not create new service file: [%s]", err.Error())
}

err = tmpl.Execute(newFile, config)
if err != nil {
return "", fmt.Errorf("could not create new service file: [%s]", err.Error())
}
return serviceName, nil
}

func setUser(serviceUser string, mountPath string, configPath string) error {
_, err := user.Lookup(serviceUser)
if err != nil {
if strings.Contains(err.Error(), "unknown user") {
// create the user
userAddCmd := exec.Command("useradd", "-m", serviceUser)
err = userAddCmd.Run()
if err != nil {
return fmt.Errorf("failed to create user [%s]", err.Error())
}
fmt.Println("user " + serviceUser + " has been created")
}
}
// advise on required permissions
fmt.Println("ensure the user, " + serviceUser + ", has the following access: \n" + mountPath + ": read, write, and execute \n" + configPath + ": read")
return nil
}

func getService(mountPath string) (string, string) {
serviceName := strings.Replace(mountPath, "/", "-", -1)
serviceFile := "cloudfuse" + serviceName + ".service"
serviceFilePath := "/etc/systemd/system/" + serviceFile
return serviceName, serviceFilePath
}

func markFlagErrorChk(cmd *cobra.Command, flagName string) {
if err := cmd.MarkFlagRequired(flagName); err != nil {
panic(fmt.Sprintf("Failed to mark flag as required: %v", err))
}
}

func init() {
rootCmd.AddCommand(serviceCmd)
rootCmd.SilenceUsage = false
serviceCmd.AddCommand(installCmd)
installCmd.Flags().StringVar(&mountPath, "mount-path", "", "Input mount path")
installCmd.Flags().StringVar(&configPath, "config-file", "", "Input config file")
installCmd.Flags().StringVar(&serviceUser, "user", "cloudfuse", "Input service user")
markFlagErrorChk(installCmd, "mount-path")
markFlagErrorChk(installCmd, "config-file")
serviceCmd.AddCommand(uninstallCmd)
uninstallCmd.Flags().StringVar(&mountPath, "mount-path", "", "Input mount path")
markFlagErrorChk(uninstallCmd, "mount-path")
}