Class: Pvectl::Commands::Service

Inherits:
Object
  • Object
show all
Defined in:
lib/pvectl/commands/service.rb,
sig/pvectl/commands/service.rbs

Overview

Top-level pvectl service command for managing systemd services on Proxmox nodes.

Exposes four lifecycle sub-commands:

  • service start <name> start a service
  • service stop <name> stop a service (irreversible — requires --yes or prompt)
  • service restart <name> hard restart a service (irreversible — requires --yes or prompt)
  • service reload <name> reload (graceful where supported)

Each sub-command requires --node NODE (falls back to default-node from the active context configuration when not provided).

Examples:

Register with the CLI

Commands::Service.register(cli)

Constant Summary collapse

CONFIRMABLE_OPERATIONS =

All confirmable operations (stop and restart can disrupt running workloads).

i[stop restart].freeze
OPERATIONS =

All supported operations.

i[start stop restart reload].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(operation, args, options, global_options, prompt: $stdin, output: $stdout) ⇒ Service

Initializes a service lifecycle command.



163
164
165
166
167
168
169
170
# File 'lib/pvectl/commands/service.rb', line 163

def initialize(operation, args, options, global_options, prompt: $stdin, output: $stdout)
  @operation = operation
  @args = Array(args).compact
  @options = options
  @global_options = global_options
  @prompt = prompt
  @output = output
end

Class Method Details

.execute(operation, args, options, global_options) ⇒ Integer

Executes the command.



151
152
153
# File 'lib/pvectl/commands/service.rb', line 151

def self.execute(operation, args, options, global_options)
  new(operation, args, options, global_options).execute
end

.register(cli) ⇒ void

This method returns an undefined value.

Registers the service command and all sub-commands with the CLI.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/pvectl/commands/service.rb', line 32

def self.register(cli)
  cli.desc "Manage systemd services on Proxmox nodes"
  cli.long_desc "    Manage systemd services running on Proxmox VE nodes. Wraps the Proxmox\n    /nodes/{node}/services API to start, stop, restart, or reload daemons\n    such as pveproxy, pvedaemon, corosync, and others.\n\n    SUB-COMMANDS\n      service start NAME      Start a stopped service\n      service stop NAME       Stop a running service (irreversible)\n      service restart NAME    Hard restart a service (irreversible)\n      service reload NAME     Reload a service (graceful where supported)\n\n    EXAMPLES\n      Restart the API proxy on a single node (with confirmation skipped):\n        $ pvectl service restart pveproxy --node pve1 --yes\n\n      Start a stopped service on the default node:\n        $ pvectl service start cron\n\n      Stop a service after interactive confirmation:\n        $ pvectl service stop pve-firewall --node pve1\n\n      Reload the syslog daemon (no interruption to running workloads):\n        $ pvectl service reload syslog --node pve1\n\n    NOTES\n      --node defaults to the context's default-node if configured.\n\n      stop and restart are irreversible and require either an interactive\n      \"yes\" confirmation or the --yes flag to skip the prompt.\n\n      Restarting pveproxy or corosync can momentarily disconnect the\n      current API session and break cluster membership respectively. Use\n      --yes only when you understand the impact.\n\n      Operations are asynchronous \u2014 the result includes the Proxmox task\n      UPID which can be inspected with `pvectl get tasks` and\n      `pvectl logs task UPID`.\n\n    SEE ALSO\n      pvectl help get             List resources (try `get services`)\n      pvectl help logs            Inspect task output\n  HELP\n  cli.command :service do |c|\n    # Shared flags declared on the parent so all subcommands inherit them.\n    # Avoids GLI flag-redefinition errors when the same flag is needed by\n    # multiple sibling subcommands (e.g. start/stop/restart/reload all\n    # need --node and --yes).\n    c.desc \"Node name (defaults to context default-node)\"\n    c.flag [:node], arg_name: \"NODE\"\n\n    c.desc \"Skip interactive confirmation prompt\"\n    c.switch [:yes, :y], negatable: false\n\n    OPERATIONS.each do |op|\n      register_subcommand(c, op)\n    end\n  end\nend\n"

.register_subcommand(parent, operation) ⇒ void

This method returns an undefined value.

Registers a single lifecycle sub-command.



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/pvectl/commands/service.rb', line 98

def self.register_subcommand(parent, operation)
  parent.desc "#{operation.capitalize} a systemd service on a Proxmox node"
  parent.long_desc subcommand_long_desc(operation)
  parent.arg_name "SERVICE_NAME"
  parent.command operation do |sub|
    sub.action do |global_options, options, args|
      exit_code = execute(operation, args, options, global_options)
      exit exit_code if exit_code != 0
    end
  end
end

.subcommand_long_desc(operation) ⇒ String

Builds the man-page-style long_desc for a sub-command.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/pvectl/commands/service.rb', line 114

def self.subcommand_long_desc(operation)
  action = operation.to_s
  confirm_note =
    if CONFIRMABLE_OPERATIONS.include?(operation)
      "This operation is irreversible. Without --yes, pvectl will\n  prompt for interactive confirmation before contacting the API."
    else
      "No confirmation is required for this operation."
    end

  "    \#{action.capitalize} a systemd service on a Proxmox node.\n\n    EXAMPLES\n      $ pvectl service \#{action} pveproxy --node pve1\n      $ pvectl service \#{action} pveproxy --node pve1 --yes\n\n    NOTES\n      \#{confirm_note}\n\n      Restarting pveproxy or corosync can momentarily disconnect the\n      current API session and break cluster membership respectively.\n\n      --node defaults to the context's default-node.\n\n    SEE ALSO\n      pvectl help service     Parent command\n      pvectl help get         List resources (try `get services`)\n  HELP\nend\n"

Instance Method Details

#config_error(message) ⇒ Integer

Outputs a config error.



287
288
289
290
# File 'lib/pvectl/commands/service.rb', line 287

def config_error(message)
  $stderr.puts "Error: #{message}"
  ExitCodes::CONFIG_ERROR
end

#confirm!(service_name, node) ⇒ Boolean

Confirms the operation when required.



223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/pvectl/commands/service.rb', line 223

def confirm!(service_name, node)
  return true unless CONFIRMABLE_OPERATIONS.include?(@operation)
  return true if @options[:yes]

  warning = service_warning(service_name)
  @output.puts "About to #{@operation} service '#{service_name}' on node '#{node}'."
  @output.puts warning if warning
  @output.print "Continue? [y/N] "
  answer = @prompt.gets&.strip&.downcase
  confirmed = %w[y yes].include?(answer)
  @output.puts "Aborted." unless confirmed
  confirmed
end

#executeInteger

Executes the command.



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/pvectl/commands/service.rb', line 175

def execute
  return usage_error("service name is required") if @args.empty?

  service_name = @args.first
  load_config
  node = resolve_node
  return config_error("node is required (provide --node or configure default-node)") unless node

  return ExitCodes::SUCCESS unless confirm!(service_name, node)

  result = perform(service_name, node)
  output_result(result)
  result.failed? ? ExitCodes::GENERAL_ERROR : ExitCodes::SUCCESS
rescue Pvectl::Config::ConfigNotFoundError,
       Pvectl::Config::InvalidConfigError,
       Pvectl::Config::ContextNotFoundError,
       Pvectl::Config::ClusterNotFoundError,
       Pvectl::Config::UserNotFoundError => e
  $stderr.puts "Error: #{e.message}"
  ExitCodes::CONFIG_ERROR
rescue StandardError => e
  $stderr.puts "Error: #{e.message}"
  ExitCodes::GENERAL_ERROR
end

#load_configvoid

This method returns an undefined value.

Loads configuration.



205
206
207
208
209
# File 'lib/pvectl/commands/service.rb', line 205

def load_config
  service = Pvectl::Config::Service.new
  service.load(config: @global_options[:config])
  @config = service.current_config
end

#output_result(result) ⇒ void

This method returns an undefined value.

Outputs the operation result using the configured formatter.



266
267
268
269
270
271
272
# File 'lib/pvectl/commands/service.rb', line 266

def output_result(result)
  format = @global_options[:output] || "table"
  color = @global_options[:color]
  formatter = Pvectl::Formatters::Registry.for(format)
  presenter = Pvectl::Presenters::NodeOperationResult.new
  @output.puts formatter.format([result], presenter, color: color)
end

#perform(service_name, node) ⇒ Models::NodeOperationResult

Performs the API call via ServiceLifecycle.



255
256
257
258
259
260
# File 'lib/pvectl/commands/service.rb', line 255

def perform(service_name, node)
  connection = Pvectl::Connection.new(@config)
  repository = Pvectl::Repositories::Service.new(connection)
  lifecycle = Pvectl::Services::ServiceLifecycle.new(service_repository: repository)
  lifecycle.execute(operation: @operation, node: node, service: service_name)
end

#resolve_nodeString?

Resolves the node from --node option or default-node config.



214
215
216
# File 'lib/pvectl/commands/service.rb', line 214

def resolve_node
  @options[:node] || @config&.default_node
end

#service_warning(service_name) ⇒ String?

Returns a warning string for sensitive services, or nil.



241
242
243
244
245
246
247
248
# File 'lib/pvectl/commands/service.rb', line 241

def service_warning(service_name)
  case service_name
  when "pveproxy", "pvedaemon"
    "Warning: this may disconnect the current API session."
  when "corosync", "pve-cluster"
    "Warning: this can disrupt cluster membership and quorum."
  end
end

#usage_error(message) ⇒ Integer

Outputs a usage error.



278
279
280
281
# File 'lib/pvectl/commands/service.rb', line 278

def usage_error(message)
  $stderr.puts "Error: #{message}"
  ExitCodes::USAGE_ERROR
end