Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cloudfuse service linux cmd #338

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
50 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
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
273 changes: 273 additions & 0 deletions cmd/service_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
//go:build linux

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

Copyright © 2023-2024 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",
foodprocessor marked this conversation as resolved.
Show resolved Hide 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> --user=<username>",
FlagErrorHandling: cobra.ExitOnError,
RunE: func(cmd *cobra.Command, args []string) error {

var err error
mountPath, err = getAbsPath(mountPath)
if err != nil {
return err
}
configPath, err = getAbsPath(configPath)
if err != nil {
return err
}

mountExists := common.DirectoryExists(mountPath)
if !mountExists {
return fmt.Errorf("the mount path provided does not exist")
// TODO: add useage output upon failure with input

Check failure on line 88 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / Lint

`useage` is a misspelling of `usage` (misspell)

Check failure on line 88 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / Check for spelling errors

useage ==> usage
}
// TODO: consider logging a warning if the mount path is empty

_, err = os.Stat(configPath)
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("error, the configfile path provided does not exist")
}

//create the new user and set permissions
err = setUser(serviceUser, mountPath, configPath)
if err != nil {
fmt.Println("Error setting permissions for user:", err)
return err
}

serviceFile, err := newServiceFile(mountPath, configPath, serviceUser)
if err != nil {
return fmt.Errorf("error when attempting 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 due to following error: [%s]", err.Error())
}

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

var serviceName string
var uninstallCmd = &cobra.Command{
foodprocessor marked this conversation as resolved.
Show resolved Hide 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
var err error
mountPath, err = getAbsPath(serviceName)
if err != nil {
return err
}

folderList := strings.Split(mountPath, "/")
serviceFile := "cloudfuse-" + folderList[len(folderList)-1] + ".service"
serviceFilePath := "/etc/systemd/system/" + serviceFile
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 due to following error: [%s]", err.Error())
}
} else if os.IsNotExist(err) {
return fmt.Errorf("failed to delete "+serviceName+" file from /etc/systemd/system due to following error: [%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 due to following error: [%s]", err.Error())
}
return nil
},
}

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

func newServiceFile(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("error creating new service file: [%s]", err.Error())
}

folderList := strings.Split(mountPath, "/")
serviceName := "cloudfuse-" + folderList[len(folderList)-1] + ".service"
servicePath := "/etc/systemd/system/" + serviceName

var newFile *os.File
if _, err = os.Stat(servicePath); os.IsNotExist(err) {
newFile, err = os.Create(servicePath)
if err != nil {
return "", fmt.Errorf("error creating new service file: [%s]", err.Error())
}
} else {
delServFileCmd := exec.Command("rm", servicePath)
err = delServFileCmd.Run()
if err != nil {
return "", fmt.Errorf("failed to replace the service file due to the following error: [%s]", err.Error())
}
newFile, err = os.Create(servicePath)
if err != nil {
return "", fmt.Errorf("error creating new service file: [%s]", err.Error())
}
}

err = tmpl.Execute(newFile, config)
if err != nil {
return "", fmt.Errorf("error creating 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 due to following error: [%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 \n")

return nil
}

// takes a file or folder name and returns its absolute path
func getAbsPath(leaf string) (string, error) {
var absPath string
var err error
if !filepath.IsAbs(leaf) {
absPath, err = filepath.Abs(leaf)
if err != nil {
return "", fmt.Errorf("couldn't format the path string", err.Error())

Check failure on line 255 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / Lint

printf: fmt.Errorf call has arguments but no formatting directives (govet)

Check failure on line 255 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / Build and Test on Linux

fmt.Errorf call has arguments but no formatting directives

Check failure on line 255 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / BuildAndTest-Coverage (1.23.2, linux)

fmt.Errorf call has arguments but no formatting directives
}
}
return absPath, 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")
installCmd.MarkFlagRequired("mount-path")

Check failure on line 268 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value of `installCmd.MarkFlagRequired` is not checked (errcheck)
installCmd.MarkFlagRequired("config-file")

Check failure on line 269 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value of `installCmd.MarkFlagRequired` is not checked (errcheck)
serviceCmd.AddCommand(uninstallCmd)
uninstallCmd.Flags().StringVar(&serviceName, "mount-path", "", "Input mount path")
uninstallCmd.MarkFlagRequired("mount-path")

Check failure on line 272 in cmd/service_linux.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value of `uninstallCmd.MarkFlagRequired` is not checked (errcheck)
}
Loading