From def3ad55ea2f09e699cc8f989fbf52dad21d2ad8 Mon Sep 17 00:00:00 2001 From: Maxim Babichev Date: Tue, 30 Jul 2024 22:53:29 +0300 Subject: [PATCH] agent register --- .golangci.yml | 4 +- cmd/agent.go | 58 ++--- cmd/register.go | 45 ++++ cmd/root.go | 1 - internal/app/register.go | 16 ++ internal/build/agent.go | 32 +++ internal/infra/templater/generator.go | 332 ++++++++++++++++++++++++++ internal/infra/templater/openwrt.stub | 48 ++++ internal/infra/templater/systemd.stub | 42 ++++ pkg/featnix/common.go | 108 +++++++++ pkg/featnix/linux.go | 147 ++++++++++++ pkg/featnix/systemd.go | 40 ++++ 12 files changed, 841 insertions(+), 32 deletions(-) create mode 100644 cmd/register.go create mode 100644 internal/app/register.go create mode 100644 internal/infra/templater/generator.go create mode 100644 internal/infra/templater/openwrt.stub create mode 100644 internal/infra/templater/systemd.stub create mode 100644 pkg/featnix/common.go create mode 100644 pkg/featnix/linux.go create mode 100644 pkg/featnix/systemd.go diff --git a/.golangci.yml b/.golangci.yml index 6d90f4c..300eed7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,8 +11,6 @@ linters: # not relevant - varnamelen - wrapcheck - # - paralleltest - # - exhaustruct linters-settings: lll: line-length: 140 @@ -33,6 +31,8 @@ issues: linters: - gochecknoglobals - gochecknoinits + - exhaustruct + - mnd - path: (.+)_test.go linters: - dupl \ No newline at end of file diff --git a/cmd/agent.go b/cmd/agent.go index 65eadd6..8a66143 100644 --- a/cmd/agent.go +++ b/cmd/agent.go @@ -9,46 +9,46 @@ import ( "github.com/bavix/vakeel/pkg/ctxid" ) -// cfg is the configuration for the Vakeel agent. -// -//nolint:exhaustruct -var cfg *config.Config = &config.Config{} - -// agentCmd is the command for the Vakeel agent. -// -//nolint:exhaustruct -var agentCmd = &cobra.Command{ - Use: "agent", - Short: "Run the Vakeel agent", - // RunE is the function that will be executed when the agent command is called. - // It creates a new builder with the configuration and calls the AgentApp method of the builder. - // The AgentApp method establishes a connection to the Vakeel server and starts sending update requests. - RunE: func(cmd *cobra.Command, _ []string) error { - // Create a new context with the ID value from the configuration. - ctx := ctxid.WithID(cmd.Context(), cfg.ID) - - // Create a new builder with the configuration. - builder := build.New(cfg) - - // Call the AgentApp method of the builder and pass the context of the command. - // The AgentApp method returns an error if the connection or the update service call fails. - return builder.AgentApp(builder.Logger(ctx)) - }, -} - // init registers the agent command to the root command. // -//nolint:mnd +// The agent command is responsible for running the Vakeel agent. It establishes +// a connection to the Vakeel server and starts sending update requests. func init() { - rootCmd.AddCommand(agentCmd) + // Create a new configuration object. + cfg := &config.Config{} + + // Create a new agent command. + agentCmd := &cobra.Command{ + Use: "agent", + Short: "Run the Vakeel agent", + // RunE is the function that will be executed when the agent command is called. + // It creates a new builder with the configuration and calls the AgentApp method of the builder. + // The AgentApp method establishes a connection to the Vakeel server and starts sending update requests. + RunE: func(cmd *cobra.Command, _ []string) error { + // Create a new context with the ID value from the configuration. + ctx := ctxid.WithID(cmd.Context(), cfg.ID) + + // Create a new builder with the configuration. + builder := build.New(cfg) + + // Call the AgentApp method of the builder and pass the context of the command. + // The AgentApp method returns an error if the connection or the update service call fails. + return builder.AgentApp(builder.Logger(ctx)) + }, + } // Set the default value of the host flag to "127.0.0.1". agentCmd.Flags(). StringVarP(&cfg.Host, "host", "H", "127.0.0.1", "Host for agent, i.e. the IP address of the Vakeel server.") + // Set the default value of the port flag to 4643. agentCmd.Flags(). IntVarP(&cfg.Port, "port", "p", 4643, "Port for agent, i.e. the port number of the Vakeel server.") + // Set the default value of the id flag to uuid.Nil.String(). agentCmd.Flags(). StringVar(&cfg.ID, "id", uuid.Nil.String(), "ID of agent, i.e. the UUID of the Vakeel agent.") + + // Add the agent command to the root command. + rootCmd.AddCommand(agentCmd) } diff --git a/cmd/register.go b/cmd/register.go new file mode 100644 index 0000000..0f14ad7 --- /dev/null +++ b/cmd/register.go @@ -0,0 +1,45 @@ +//go:build linux || darwin + +package cmd + +import ( + "github.com/google/uuid" + "github.com/spf13/cobra" + + "github.com/bavix/vakeel/internal/build" + "github.com/bavix/vakeel/internal/config" + "github.com/bavix/vakeel/pkg/ctxid" +) + +func init() { + // Create a new configuration object. + cfg := &config.Config{} + + registerCmd := &cobra.Command{ + Use: "register", + Short: "todo short", + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := ctxid.WithID(cmd.Context(), cfg.ID) + + builder := build.New(cfg) + + return builder.AgentRegisterApp(builder.Logger(ctx)) + }, + } + + // Set the default value of the host flag to "127.0.0.1". + registerCmd.Flags(). + StringVarP(&cfg.Host, "host", "H", "127.0.0.1", "Host for agent, i.e. the IP address of the Vakeel server.") + // Set the default value of the port flag to 4643. + registerCmd.Flags(). + IntVarP(&cfg.Port, "port", "p", 4643, "Port for agent, i.e. the port number of the Vakeel server.") + // Set the default value of the id flag to a new UUID. + // The flag is used to set the ID of the agent, i.e. the UUID of the Vakeel agent. + // The UUID is generated using uuid.New() and converted to a string using uuid.String(). + registerCmd.Flags(). + StringVar(&cfg.ID, "id", uuid.New().String(), "ID of agent, i.e. the UUID of the Vakeel agent."+ + "If not provided, a new UUID will be generated.") + + rootCmd.AddCommand(registerCmd) +} diff --git a/cmd/root.go b/cmd/root.go index d857545..96c7283 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" ) -//nolint:exhaustruct var rootCmd = &cobra.Command{ Use: "vakeel", Short: "Agent for vakeel-way", diff --git a/internal/app/register.go b/internal/app/register.go new file mode 100644 index 0000000..6ff373a --- /dev/null +++ b/internal/app/register.go @@ -0,0 +1,16 @@ +package app + +import ( + "context" +) + +type RegisterUseCase interface { + Register() error +} + +func AgentRegister( + _ context.Context, + register RegisterUseCase, +) error { + return register.Register() +} diff --git a/internal/build/agent.go b/internal/build/agent.go index f550075..a4d0a3b 100644 --- a/internal/build/agent.go +++ b/internal/build/agent.go @@ -10,6 +10,8 @@ import ( "github.com/bavix/vakeel-way/pkg/api/vakeel_way" "github.com/bavix/vakeel/internal/app" + "github.com/bavix/vakeel/internal/infra/templater" + "github.com/bavix/vakeel/pkg/ctxid" ) // AgentApp creates a gRPC client and connects to the server's update service. @@ -34,3 +36,33 @@ func (b *Builder) AgentApp(ctx context.Context) error { return app.Agent(ctx, serviceClient) } + +// AgentRegisterApp is a method of the Builder struct. +// +// It registers the agent application with the server. +// It returns an error if the registration fails. +// +// ctx: The context.Context to use for the gRPC call. +// +// Returns: +// An error if the registration fails. +func (b *Builder) AgentRegisterApp(ctx context.Context) error { + // Create a new templater.New instance with the context ID, host, and port from the config. + // The templater.New instance generates the stub agent template. + generate, err := templater.New(ctxid.ID(ctx), b.config.Host, b.config.Port) + if err != nil { + return err + } + + // Call the AgentRegister function of the app package. + // It creates a gRPC client insecure connection to the server. + // It registers the agent application with the server. + // It returns an error if the registration fails. + // + // ctx: The context.Context to use for the gRPC call. + // generate: The templater.New instance that generates the stub agent template. + // + // Returns: + // An error if the registration fails. + return app.AgentRegister(ctx, generate) +} diff --git a/internal/infra/templater/generator.go b/internal/infra/templater/generator.go new file mode 100644 index 0000000..37d331a --- /dev/null +++ b/internal/infra/templater/generator.go @@ -0,0 +1,332 @@ +package templater + +import ( + "bytes" + _ "embed" + "errors" + "os" + "os/exec" + "path/filepath" + "text/template" + + "github.com/google/uuid" + + "github.com/bavix/vakeel/pkg/featnix" +) + +// errUnsupportedOS is the error returned when the operating system is unsupported. +var errUnsupportedOS = errors.New("unsupported operating system") + +// errStubNotFound is the error returned when the stub template is not found. +// +// It is used when the stub template file is not found during the generation of the +// init script or systemd service file. +var errStubNotFound = errors.New("stub template not found") + +// openwrtServicePath is the path to the init script for the vakeel agent +// on OpenWRT systems. +// +// The init script is responsible for starting and stopping the vakeel agent. +const openwrtServicePath = "/tmp/etc/init.d/vakeel" + +// systemdServicePath is the path to the systemd service file for the vakeel agent +// on systems that use systemd as the service manager. +// +// The systemd service file defines the configuration for the vakeel agent as a +// systemd service. It specifies the command to start the agent, the behavior +// when the agent crashes or is terminated, and other options. +const systemdServicePath = "/tmp/etc/systemd/system/vakeel.service" + +//go:embed openwrt.stub +var openwrtTemplate string + +//go:embed systemd.stub +var systemdTemplate string + +// Data contains the data used to fill the stub agent template. +type Data struct { + // AppPath is the path to the application binary. + AppPath string + // ID is the ID of the agent. + ID uuid.UUID + // Host is the hostname of the vakeel server. + Host string + // Port is the port of the vakeel server. + Port int +} + +// ServiceGenerator is a template for creating stub agents. +// +// It contains the data used to fill the stub agent template. +type ServiceGenerator struct { + // context contains the context used to fill the stub agent template. + // It contains the path to the application binary, the ID of the agent, + // the hostname of the vakeel server, and the port of the vakeel server. + context Data +} + +// New creates a new ServiceGenerator instance with the given ID, host, and port. +// +// Parameters: +// - id: The ID of the agent. +// - host: The hostname of the vakeel server. +// - port: The port of the vakeel server. +// +// Returns: +// - *ServiceGenerator: A pointer to the ServiceGenerator instance. +// - error: An error if any. +func New( + id uuid.UUID, // The ID of the agent. + host string, // The hostname of the vakeel server. + port int, // The port of the vakeel server. +) (*ServiceGenerator, error) { + // Get the path to the application binary. + // + // The os.Executable function returns the path name for the executable file that is currently + // running. If there is an error, it returns the error. + appPath, err := os.Executable() + if err != nil { + return nil, err // Return the error if any. + } + + // Create a new ServiceGenerator instance with the given ID, host, port, and appPath. + return &ServiceGenerator{ + // Initialize the context with the given ID, host, port, and appPath. + context: Data{ + AppPath: appPath, // The path to the application binary. + ID: id, // The ID of the agent. + Host: host, // The hostname of the vakeel server. + Port: port, // The port of the vakeel server. + }, + }, nil +} + +// stub returns the stub agent template based on the operating system. +// +// This function checks the operating system and returns the appropriate template +// based on the result. If the operating system is OpenWrt, it returns the +// openwrtTemplate. If the operating system supports systemd, it returns the +// systemdTemplate. If the operating system is neither OpenWrt nor supports +// systemd, it returns nil. +// +// The stub agent template is a Go template that is used to create stub agents. +// The template contains placeholders for the application binary path, agent ID, +// the hostname of the vakeel server, and the port of the vakeel server. +// +// Returns: +// +// *string: The stub agent template. Nil if the operating system is neither +// OpenWrt nor supports systemd. +func (t *ServiceGenerator) stub() *string { + // Check if the operating system is OpenWrt. + // If it is, return the openwrtTemplate. + if featnix.IsOpenWrt() { + return &openwrtTemplate + } + + // Check if the operating system supports systemd. + // If it does, return the systemdTemplate. + if featnix.HasSystemd() { + return &systemdTemplate + } + + // The operating system is neither OpenWrt nor supports systemd. + // Return nil. + return nil +} + +// Register registers the stub agent service file based on the operating system. +// +// This function calls the generate function to generate the stub agent service +// file. Then, it checks if the operating system supports systemd or is OpenWrt. +// If it does, it enables and starts the service using the systemctl or +// /etc/init.d/vakeel commands respectively. If the operating system is neither +// OpenWrt nor supports systemd, it returns an error indicating that the +// operating system is unsupported. +// +// Returns: +// +// error: An error if the operating system is unsupported or if there is an +// error enabling or starting the service. Nil if the registration is +// successful. +func (t *ServiceGenerator) Register() error { + if _, err := t.generate(); err != nil { + return err + } + + if featnix.HasSystemd() { + return t.registerSystemd() + } + + if featnix.IsOpenWrt() { + return t.registerOpenWrt() + } + + return errStubNotFound +} + +// registerSystemd enables and starts the Vakeel service using systemctl. +// +// This function uses the systemctl command to enable and start the Vakeel service. +// If there is an error enabling or starting the service, it will be returned. +// +// Returns: +// +// error: An error if there is an error enabling or starting the service. +// Nil if the service is enabled and started successfully. +func (t *ServiceGenerator) registerSystemd() error { + // Enable the Vakeel service using systemctl. + // If there is an error enabling the service, return the error. + if err := exec.Command("systemctl", "enable", "vakeel.service").Run(); err != nil { + return err + } + + // Start the Vakeel service using systemctl. + // If there is an error starting the service, return the error. + return exec.Command("systemctl", "start", "vakeel.service").Run() +} + +// registerOpenWrt registers the Vakeel service using /etc/init.d/vakeel. +// +// This function uses the /etc/init.d/vakeel script to enable and start the Vakeel +// service. If there is an error enabling or starting the service, it will be +// returned. +// +// Returns: +// +// error: An error if there is an error enabling or starting the service. +// Nil if the service is enabled and started successfully. +func (t *ServiceGenerator) registerOpenWrt() error { + // Enable the Vakeel service using /etc/init.d/vakeel. + // If there is an error enabling the service, return the error. + if err := exec.Command(openwrtServicePath, "enable").Run(); err != nil { + return err + } + + // Start the Vakeel service using /etc/init.d/vakeel. + // If there is an error starting the service, return the error. + return exec.Command(openwrtServicePath, "start").Run() +} + +// generate generates the stub agent service file based on the operating system. +// +// This function gets the stub agent template based on the operating system, +// parses the template, and executes it with the StubTemplate instance as the +// data. Finally, it returns the generated template as a string. +// +// Returns: +// +// string: The generated stub agent service file. +// error: An error if there is an error parsing or executing the template. +func (t *ServiceGenerator) generate() (string, error) { + // Render the stub agent template. + // The template is parsed and executed with the StubTemplate instance as the data. + // The generated template is returned as a string. + content, err := t.render() + if err != nil { + return "", err + } + + // Check if the operating system is OpenWrt. + // If it is, write the content to the openwrt service file. + if featnix.IsOpenWrt() { + return t.writeToFile(openwrtServicePath, content) + } + + // Check if the operating system supports systemd. + // If it does, write the content to the systemd service file. + if featnix.HasSystemd() { + return t.writeToFile(systemdServicePath, content) + } + + // If the operating system is neither OpenWrt nor supports systemd, + // return an error indicating that the operating system is unsupported. + return "", errUnsupportedOS +} + +// writeToFile writes the content to the specified file path, if the file does not already exist. +// +// Parameters: +// - filePath: The path to the file where the content will be written. +// - content: The content to be written to the file. +// +// Returns: +// - string: The file path. +// - error: An error if there is an error writing the file. +func (t *ServiceGenerator) writeToFile(filePath string, content string) (string, error) { + // Create the directory for the file if it doesn't exist. + err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm) + if err != nil { + return "", err + } + + // Check if the file already exists. + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + // If the file exists, return the file path. + return filePath, nil + } + + // Open the file for writing. + f, err := os.Create(filePath) + if err != nil { + return "", err + } + defer f.Close() + + // Write the content to the file. + _, err = f.WriteString(content) + if err != nil { + return "", err + } + + // Return the file path. + return filePath, nil +} + +// render generates the stub agent service file based on the operating system. +// +// This function gets the stub agent template based on the operating system, +// parses the template, and executes it with the StubTemplate instance as the +// data. Finally, it returns the generated template as a string. +// +// Returns: +// +// string: The generated stub agent service file. +// error: An error if there is an error parsing or executing the template. +func (t *ServiceGenerator) render() (string, error) { + // Get the stub agent template based on the operating system. + stubTemplate := t.stub() + + // Return an error if the stub agent template is nil. + if stubTemplate == nil { + return "", errStubNotFound + } + + // Parse the stub agent template. + // + // The template is parsed using the template package's Parse function. The + // template is named "stub" and the template string is stored in the + // stubTemplate variable. + tmpl, err := template.New("stub").Parse(*stubTemplate) + if err != nil { + // Return an error if there is an error parsing the template. + return "", err + } + + // Execute the template with the StubTemplate instance as the data. + // + // The template is executed using the Execute function of the template. The + // result is stored in a buffer. + var buf bytes.Buffer + + err = tmpl.Execute(&buf, t.context) + if err != nil { + // Return an error if there is an error executing the template. + return "", err + } + + // Return the generated template as a string. + // + // The generated template is stored in the buffer and returned as a string. + return buf.String(), nil +} diff --git a/internal/infra/templater/openwrt.stub b/internal/infra/templater/openwrt.stub new file mode 100644 index 0000000..a1764ce --- /dev/null +++ b/internal/infra/templater/openwrt.stub @@ -0,0 +1,48 @@ +#!/bin/sh /etc/rc.common +# Generated by vakeel. Do not edit manually. + +# Start value for procd. +# This value is used to specify the order in which services are started. +# Services with lower values are started first. +# +# The value 10 is used here to ensure that the vakeel agent service is started after +# essential services like DHCP and DNS. +START=10 + +# Flag to enable procd. +# This flag is used to enable the use of procd to manage services. +# +# procd is a service manager for OpenWrt. It allows you to manage services +# using a simple configuration file. +# +# The flag is set to 1 to enable the use of procd. +USE_PROCD=1 + +# start_service function starts the agent service +# +# This function starts the agent service using procd. It creates a new procd instance, +# sets the command for the service, and enables respawn for the service. +# +# No parameters are required. +# +# Returns: +# void +start_service() { + # Create a new procd instance + # This function creates a new procd instance that will be used to start the agent service. + procd_open_instance + + # Set the command for the service + # This function sets the command for the agent service. The command is the path to the + # application binary followed by the arguments. + procd_set_param command "{{ .AppPath }} agent --id={{ .ID }} --host={{ .Host }} --port={{ .Port }}" + + # Enable respawn for the service + # This function enables respawn for the agent service. This means that if the service + # crashes, it will be automatically restarted. + procd_set_param respawn + + # Close the procd instance + # This function closes the procd instance that was used to start the agent service. + procd_close_instance +} diff --git a/internal/infra/templater/systemd.stub b/internal/infra/templater/systemd.stub new file mode 100644 index 0000000..cf4ecea --- /dev/null +++ b/internal/infra/templater/systemd.stub @@ -0,0 +1,42 @@ +# Generated by vakeel. Do not edit manually. + +# systemd unit file for the Vakeel agent +# +# This unit file specifies the configuration for the Vakeel agent as a systemd service. +# The service will be restarted automatically if it crashes or terminates. +# +# The service is configured to run after the network service and is enabled by default. +# The service is installed to the "multi-user.target" unit, which means it will be started +# when the system boots up. + +[Unit] +# Description of the service +Description=Vakeel Agent + +# Specifies the dependencies of the service +# This service will be started after the "network.target" unit +After=network.target + +[Service] +# Specifies the type of service +# "simple" type means that the service will be started and stopped as a simple process +Type=simple + +# Specifies the behavior of the service when it crashes or terminates +# "restart" option means that the service will be automatically restarted if it crashes or terminates +Restart=always + +# Specifies the command to start the service +# The command is constructed using the values of the template variables +# {{ .AppPath }} represents the path to the Vakeel application binary +# {{ .ID }} represents the UUID of the agent +# {{ .Host }} represents the hostname or IP address of the Vakeel server +# {{ .Port }} represents the port number of the Vakeel server +ExecStart={{ .AppPath }} agent --id={{ .ID }} --host={{ .Host }} --port={{ .Port }} + +[Install] +# Specifies the target unit that the service is installed to +# "multi-user.target" is the default target unit that starts when the system boots up +# This service will be started when the system boots up +WantedBy=multi-user.target + diff --git a/pkg/featnix/common.go b/pkg/featnix/common.go new file mode 100644 index 0000000..97a73f7 --- /dev/null +++ b/pkg/featnix/common.go @@ -0,0 +1,108 @@ +package featnix + +import ( + "bufio" + "os" + "strings" +) + +// OSReleaseField represents a field in the /etc/os-release file. +// +// The /etc/os-release file contains information about the operating system. +// This file is used by various Linux distributions to provide information +// about the distribution and its version. +// +// The file contains key-value pairs of the form "KEY=VALUE". +// The keys and values are separated by an equal sign. +// +// The OSReleaseField type represents a field in the /etc/os-release file. +type OSReleaseField string + +// String returns the string representation of an OSReleaseField. +// +// It returns the string value of the OSReleaseFieldID constant. +// +// The String method satisfies the Stringer interface. +func (f OSReleaseField) String() string { + return string(OSReleaseFieldID) +} + +// OSReleaseFieldID is the constant representing the "ID" field in the +// "/etc/os-release" file. +const OSReleaseFieldID OSReleaseField = "ID" + +// osReleasePath is the path to the file that contains the operating system release +// information on Linux systems. +// +// The ID field in the file is used to identify the Linux distribution. +// The file is typically located at "/etc/os-release". +const osReleasePath = "/etc/os-release" + +// readOSReleaseField reads the contents of the specified file and returns the value of the +// specified field. +// +// Parameters: +// - filePath: The path to the file to read. +// - field: The field to search for in the file. +// +// Returns: +// - The value of the specified field in the file. If the field is not found, an empty string +// is returned. +func readOSReleaseField(filePath, field string) string { + // Open the file. + file, err := os.Open(filePath) + if err != nil { + return "" // Return an empty string if the file cannot be opened. + } + defer file.Close() + + // Create a scanner to read the file line by line. + scanner := bufio.NewScanner(file) + + // Iterate over each line in the file. + for scanner.Scan() { + // Read the line. + line := scanner.Text() + // Split the line into key-value pairs. + parts := strings.Split(line, "=") + + // Check if the line contains the specified field. + if len(parts) == 2 && parts[0] == field { + // Trim the value of the field to remove any surrounding quotes. + value := strings.Trim(parts[1], "\"'") + + return value // Return the value of the field. + } + } + + // Return an empty string if the field is not found. + return "" +} + +// readOSReleaseID reads the contents of the /etc/os-release file and returns the value of the +// "ID" field as an OSReleaseID. +// +// This function reads the contents of the file line by line and retrieves the value of the "ID" +// field. The value is returned as an OSReleaseID type. If the "ID" field is not found in the file, +// an empty string is returned. +// +// Returns: +// +// OSReleaseID: The value of the "ID" field in the file as an OSReleaseID. If the "ID" field +// is not found in the file, an empty string is returned. +func readOSReleaseID() OSReleaseID { + // Read the contents of the file specified by osReleasePath and find the value of the "ID" field. + // The value of the "ID" field is returned as an OSReleaseID type. + // If the "ID" field is not found in the file, an empty string is returned. + // + // Call the readOSReleaseField function to read the contents of the file and find the value of the "ID" field. + // The readOSReleaseField function takes the file path and the field name as parameters. + // It returns a string containing the value of the field. + // Convert the string to an OSReleaseID type and return it. + return OSReleaseID( + readOSReleaseField( + osReleasePath, // The path to the file to read. + OSReleaseFieldID.String(), // The field to search for in the file. + ), + ) +} diff --git a/pkg/featnix/linux.go b/pkg/featnix/linux.go new file mode 100644 index 0000000..7ee3c04 --- /dev/null +++ b/pkg/featnix/linux.go @@ -0,0 +1,147 @@ +package featnix + +// OSReleaseID represents the ID field in the /etc/os-release file. +// It is used to identify the distribution of the operating system. +type OSReleaseID string + +const ( + // OSReleaseIDUnknown represents an unknown OS. + OSReleaseIDUnknown OSReleaseID = "" + + // OSReleaseIDDebian represents Debian. + OSReleaseIDDebian OSReleaseID = "debian" + + // OSReleaseIDUbuntu represents Ubuntu. + OSReleaseIDUbuntu OSReleaseID = "ubuntu" + + // OSReleaseIDFedora represents Fedora. + OSReleaseIDFedora OSReleaseID = "fedora" + + // OSReleaseIDFreeBSD represents FreeBSD. + OSReleaseIDFreeBSD OSReleaseID = "freebsd" + + // OSReleaseIDArch represents Arch Linux. + OSReleaseIDArch OSReleaseID = "arch" + + // OSReleaseIDOpenWrt represents OpenWrt. + OSReleaseIDOpenWrt OSReleaseID = "openwrt" + + // OSReleaseIDAlpine represents Alpine Linux. + OSReleaseIDAlpine OSReleaseID = "alpine" + + // OSReleaseIDOpenSUSE represents openSUSE. + OSReleaseIDOpenSUSE OSReleaseID = "opensuse" +) + +// IsDebian checks if the current system is running Debian by reading the contents +// of the /etc/os-release file. +// +// Returns: +// +// bool: True if the system is running Debian, false otherwise. +func IsDebian() bool { + return readOSReleaseID() == OSReleaseIDDebian +} + +// IsUbuntu checks if the current operating system is Ubuntu by reading the contents of the +// /etc/os-release file. +// +// Returns: +// +// bool: True if the operating system is Ubuntu, false otherwise. +func IsUbuntu() bool { + return readOSReleaseID() == OSReleaseIDUbuntu +} + +// IsFedora checks if the current system is running Fedora by reading the contents +// of the /etc/os-release file. +// +// Returns: +// +// bool: True if the system is running Fedora, false otherwise. +func IsFedora() bool { + // Read the contents of the /etc/os-release file and check if the field "ID" + // contains the string "fedora". If it does, it returns true indicating that + // Fedora is running. Otherwise, it returns false. + return readOSReleaseID() == OSReleaseIDFedora +} + +// IsFreeBSD checks if the current system is running FreeBSD by reading the contents +// of the /etc/os-release file. +// +// It returns true if the field "ID" in the /etc/os-release file contains the string "freebsd", +// indicating that FreeBSD is running. Otherwise, it returns false. +// +// Returns: +// +// bool: True if the system is running FreeBSD, false otherwise. +func IsFreeBSD() bool { + // Read the contents of the /etc/os-release file and check if the field "ID" + // contains the string "freebsd". If it does, it returns true indicating that + // FreeBSD is running. Otherwise, it returns false. + return readOSReleaseID() == OSReleaseIDFreeBSD +} + +// IsArchLinux checks if the current system is running Arch Linux by reading the contents +// of the /etc/os-release file. +// +// It returns true if the field "ID" in the /etc/os-release file contains the string "arch", +// indicating that Arch Linux is running. Otherwise, it returns false. +// +// Returns: +// +// bool: True if the system is running Arch Linux, false otherwise. +func IsArchLinux() bool { + // Read the contents of the /etc/os-release file and check if the field "ID" + // contains the string "arch". If it does, it returns true indicating that + // Arch Linux is running. Otherwise, it returns false. + return readOSReleaseID() == OSReleaseIDArch +} + +// IsOpenWrt checks if the current system is running OpenWrt by reading the contents +// of the /etc/os-release file. +// +// It returns true if the field "ID" in the /etc/os-release file contains the string "openwrt", +// indicating that OpenWrt is running. Otherwise, it returns false. +// +// Returns: +// +// bool: True if the system is running OpenWrt, false otherwise. +func IsOpenWrt() bool { + // Read the contents of the /etc/os-release file and check if the field "ID" + // contains the string "openwrt". If it does, it returns true indicating that + // OpenWrt is running. Otherwise, it returns false. + return readOSReleaseID() == OSReleaseIDOpenWrt +} + +// IsAlpine checks if the current system is running Alpine Linux by reading the contents +// of the /etc/os-release file. +// +// It returns true if the field "ID" in the /etc/os-release file contains the string "alpine", +// indicating that Alpine Linux is running. Otherwise, it returns false. +// +// Returns: +// +// bool: True if the system is running Alpine Linux, false otherwise. +func IsAlpine() bool { + // Reads the contents of the /etc/os-release file and checks if the field "ID" + // contains the string "alpine". If it does, it returns true indicating that + // Alpine Linux is running. Otherwise, it returns false. + return readOSReleaseID() == OSReleaseIDAlpine +} + +// IsOpenSUSE checks if the current system is running openSUSE by reading the contents +// of the /etc/os-release file. +// +// It returns true if the field "ID" in the /etc/os-release file contains the string "opensuse", +// indicating that openSUSE is running. Otherwise, it returns false. +// +// Returns: +// +// bool: True if the system is running openSUSE, false otherwise. +func IsOpenSUSE() bool { + // Read the contents of the /etc/os-release file and check if the field "ID" + // contains the string "opensuse". If it does, it returns true indicating that + // openSUSE is running. Otherwise, it returns false. + return readOSReleaseID() == OSReleaseIDOpenSUSE +} diff --git a/pkg/featnix/systemd.go b/pkg/featnix/systemd.go new file mode 100644 index 0000000..c3779d3 --- /dev/null +++ b/pkg/featnix/systemd.go @@ -0,0 +1,40 @@ +package featnix + +import ( + "os" + "os/exec" +) + +// HasSystemd checks if the systemd service manager is available. +// +// It checks if systemd is installed by checking for the presence of certain directories +// and the 'systemctl' command in the PATH. +// +// Returns: +// +// bool: True if systemd service manager is available, false otherwise. +func HasSystemd() bool { + // Directories where systemd stores its state files, unit files, and drop-in unit files. + // These directories are checked to determine if systemd is installed. + dirs := []string{ + "/run/systemd/system", // State files. + "/usr/lib/systemd/system", // Unit files. + "/etc/systemd/system", // Drop-in unit files. + } + + // Check if any of the directories exist. + for _, dir := range dirs { + // Check if the directory exists. + // If the directory exists, it indicates that systemd is installed. + if _, err := os.Stat(dir); err == nil { + return true + } + } + + // Check if the 'systemctl' command is in the PATH. + // If the command is in the PATH, it indicates that systemd is installed. + _, err := exec.LookPath("systemctl") + + // If there is no error, systemctl command is found in the PATH and systemd is installed. + return err == nil +}