Tab-completion, APL, Storages

The interactive shell provides tab-completion for LXC-/VM-/Node-names and IDs.

CLI provides storage-management, including listing storages and there
contents.
Also added: Listing and downloading of APLs to storages.
This commit is contained in:
Denis Knauf 2021-11-25 15:55:07 +01:00
parent 38ea2125ff
commit ca44934546
8 changed files with 242 additions and 27 deletions

View file

@ -13,6 +13,17 @@ require_relative 'cli/node'
class UsageError <RuntimeError
end
class DenCli::Sub
def provide_help name: nil, aliases: nil, min: nil
base = self
name = :help if name.nil?
aliases = %w[-h --help] if aliases.nil?
min = 1 if min.nil?
#p name: name, aliases: aliases, min: min
cmd( name, '', aliases: aliases, min: min) {|*args| help *args }
end
end
class PVE::Cli
attr_reader :cfg, :cli
@ -153,6 +164,36 @@ class PVE::Cli
opt( :fire, "-f", "--[no-]fire", "Do not wait till running", default: false)
end
def complete_lxc f
Proxmox::LXC.all.
flat_map {|x| [x.name, x.vmid.to_s] }.
select {|x| f =~ x }
end
def complete_qemu f
Proxmox::Qemu.all.
flat_map {|x| [x.name, x.vmid.to_s] }.
select {|x| f =~ x }
end
def complete_node f
Proxmox::Qemu.all.
map {|x| x.name }.
select {|x| f =~ x }
end
def completion_helper *pre, arg, &exe
if pre.empty?
connect
xs = yield /\A#{Regexp.quote arg}/
STDOUT.print "\a" if xs.empty?
xs
else
STDOUT.print "\a"
[]
end
end
def prepare
cli_node
cli_ct
@ -170,4 +211,23 @@ class PVE::Cli
STDERR.puts $!
exit 1
end
def appliances node, regexp, system, applications
system = applications = true if system.nil? and applications.nil?
node = node ? Proxmox::Node.find_by_name!( node) : Proxmox::Node.all.first
to = TablizedOutput.new %w<Section Package Version OS Template Description>, format: %w[> > > > > <]
node.aplinfo.
select {|a| 'system' == a.section ? system : applications}.
each do |apl|
to.push [
apl.section,
apl.package,
apl.version,
apl.os,
apl.template,
apl.description,
]
end
to.print order: [1,2]
end
end

View file

@ -26,9 +26,8 @@ def cli_base
else
lambda {|n| to.virt n }
end
nodes = Proxmox::Node.all
nodes = node ? Proxmox::Node.find_by_name( name) : Proxmox::Node.all
nodes.
select {|n| not node or n === node }.
flat_map {|n| [ n.method(:lxc), n.method(:qemu) ] }.
each {|m| m.call.each &push }
to.print order: sort.each_char.map {|c| (2*c.ord[5]-1) * (' sainhucmd'.index( c.downcase)) }
@ -151,19 +150,29 @@ def cli_base
}
end
cli.cmd :enter, "Enter Console of CT/Node", &lambda {|name_or_id|
cli.cmd( :enter, "Enter Console of CT/Node", &lambda {|name_or_id|
connect
th = Proxmox::LXC.find( name_or_id) || Proxmox::Node.find_by_name( name_or_id)
raise UsageError, "Container or Node not found: #{name_or_id}" unless th
STDERR.puts "! #{$?.exitstatus}" unless th.enter
}
}).
completion do |*pre, arg|
completion_helper *pre, arg do |f|
complete_lxc( f) + complete_node( f)
end
end
cli.cmd :run, "Starts CT/VM", aliases: %w[start star], &lambda {|name_or_id|
cli.cmd( :run, "Starts CT/VM", aliases: %w[start star], &lambda {|name_or_id|
connect
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
raise UsageError, "Container or Node not found: #{name_or_id}" unless th
start th
}
}).
completion do |*pre, arg|
completion_helper *pre, arg do |f|
complete_lxc( f) + complete_qemu( f)
end
end
#cli.cmd :reboot, "Reboot CT/VM (not implemented, yet)", min: 6, &lambda {|name_or_id|
# connect
@ -172,12 +181,17 @@ def cli_base
# reboot th
#}
cli.cmd :stop, "Stops CT/VM", min: 4, &lambda {|name_or_id|
cli.cmd( :stop, "Stops CT/VM", min: 4, &lambda {|name_or_id|
connect
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
raise UsageError, "Container or Node not found: #{name_or_id}" unless th
stop th
}
}).
completion do |*pre, arg|
completion_helper *pre, arg do |f|
complete_lxc( f) + complete_qemu( f)
end
end
cli.cmd 'help', '', aliases: ['-h', '--help'], &lambda {|*args| help cli, *args }

View file

@ -3,8 +3,7 @@ def cli_ct
cli.sub :ct, "Containers", aliases: %w[lx lxc] do |ct_cli|
ct_cli.cmd :list, "List CT-IDs", aliases: ['ls'], &lambda {|node=nil|
connect
nodes = Proxmox::Node.all
nodes = nodes.select {|n| node == n.name } if node
nodes = node ? Proxmox::Node.find_by_name( name) : Proxmox::Node.all
nodes.flat_map do |n|
n.lxc.map {|c| c.vmid.to_i }
end.sort.each {|c| puts c }

View file

@ -15,7 +15,7 @@ def cli_ha
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
raise UsageError, "Container or VirtualMachine not found: #{name_or_id}" unless th
ha = th.ha
raise UsageError, "#{th.sid} is already High-Available" unless ha.active?
raise UsageError, "#{th.sid} is already High-Available" if ha.active?
ha.create group: group, comment: comment, max_relocate: max_relocate, max_restart: max_restart
}).tap {|cl| opts_ha cl }
@ -24,7 +24,7 @@ def cli_ha
th = Proxmox::LXC.find( name_or_id) || Proxmox::Qemu.find_by_name( name_or_id)
raise UsageError, "Container or VirtualMachine not found: #{name_or_id}" unless th
ha = th.ha
raise UsageError, "#{th.sid} is not High-Available" if ha.active?
raise UsageError, "#{th.sid} is not High-Available" unless ha.active?
ha.delete
}
@ -52,7 +52,7 @@ def cli_ha
ha = ha.create unless ha.active?
ha.disabled! if force and ha.error?
ha.started!
}).opt( :force, "-f", "--force", "If CT/VM is in error-state, first disable HA, than try to start.")
}).opt( :force, "-f", "--force", "If CT/VM is in error-state, first reset HA, than try to start.")
hacli.cmd :stopped, "CT/VM should be in state stopped. By starting CT/VM via pct/e state will be changed in HA, too.", min: 3, &lambda {|name_or_id|
connect

97
lib/pve/cli/storage.rb Normal file
View file

@ -0,0 +1,97 @@
class PVE::Cli
def cli_storage
cli.sub :storage, "Storages", min: 3 do |cli_sm|
cli_sm.cmd :list, "List Storages", aliases: ['ls'], &lambda {|node=nil|
connect
nodes = node ? [Proxmox::Node.find_by_name!( node)] : Proxmox::Node.all
nodes.flat_map do |n|
n.lxc.map {|c| c.vmid.to_i }
end.sort.each {|c| puts c }
}
cli_sm.cmd :status, "List Storages with status", aliases: [nil], &lambda {|node=nil|
connect
to = TablizedOutput.new %w[A E S Storage Host Type]
nodes = node ? [Proxmox::Node.find_by_name!( node)] : Proxmox::Node.all
nodes.each do |n|
n.storage.each do |v|
to.push [
case v.active
when 1 then ColoredString.new 'Y', "32"
when 0 then ColoredString.new 'n', "31"
else v.active.to_s
end,
case v.enabled
when 1 then ColoredString.new 'Y', "32"
when 0 then ColoredString.new 'n', "31"
else v.enabled.to_s
end,
1 == v.shared ? 's' : 'l', v.storage, v.node.node, v.type
]
end
end
to.print order: [4,5]
}
cli_sm.sub :content, "Content of Storage", aliases: ['cnt'] do |cli_cnt|
cli_cnt.cmd :list, "List Content", aliases: ['ls'], &lambda {|node=nil, storage|
connect
node = node ? Proxmox::Node.find_by_name!( node) : Proxmox::Node.all.first
storage = node.storage.select {|sm| storage == sm.storage }.first
storage.content.each {|c| puts c.to_s }
}
cli_cnt.cmd( :help, '', aliases: ['-h', '--help']) {|*args| help cli_cnt, *args }
end
cli_sm.cmd( :help, '', aliases: ['-h', '--help']) {|*args| help cli_sm, *args }
#cli_sm.provide_help
end
cli.sub :apl, "Appliances - Downloadable container images", min: 3 do |cli_apl|
cli_apl.cmd( :content, "Table of all provided appliances", aliases: [nil], &lambda {|node:, regexp:, system:, applications:|
connect
appliances node, regexp, system, applications
}).
opt( :node, '-n=NODE', '--node', 'Ask this node for appliances (any node should list the same)', default: nil).
opt( :regexp, '-r=REGEXP', '--regexp', 'Filter by template', default: nil).
opt( :system, '-s', '--system', 'Only system templates', default: nil).
opt( :applications, '-a', '--applications', 'Only applications (non system) templates', default: nil)
cli_apl.cmd( :system, "Table of provided systems", aliases: [nil], &lambda {|node:, regexp:|
connect
appliances node, regexp, true, nil
}).
opt( :node, '-n=NODE', '--node', 'Ask this node for appliances (any node should list the same)', default: nil).
opt( :regexp, '-r=REGEXP', '--regexp', 'Filter by template', default: nil)
cli_apl.cmd( :applications, "Table of provided applications", aliases: [nil], &lambda {|node:, regexp:|
connect
appliances node, regexp, nil, true
}).
opt( :node, '-n=NODE', '--node', 'Ask this node for appliances (any node should list the same)', default: nil).
opt( :regexp, '-r=REGEXP', '--regexp', 'Filter by template', default: nil)
cli_apl.cmd( :list, "List provided appliances", aliases: ['ls'], &lambda {|node=nil, regexp:|
connect
node = node ? Proxmox::Node.find_by_name!( node) : Proxmox::Node.all.first
node.aplinfo.each do |apl|
puts apl.template
end
}).
opt( :regexp, '-r=REGEXP', '--regexp', 'Filters by name', default: nil)
cli_apl.cmd :download, "Download appliance", aliases: ['dl'], min: 2, &lambda {|template, node, storage=nil|
storage ||= 'local'
connect
node = Proxmox::Node.find_by_name! node
apl = node.aplinfo.find {|apl| apl.template == template }
raise UsageError, "Appliance not found" unless apl
task = apl.download storage
wait task, text: "Download #{apl.template} on #{node.node} to #{storage}"
}
cli_apl.cmd( :help, '', aliases: ['-h', '--help']) {|*args| help cli_apl, *args }
end
end
end

View file

@ -84,9 +84,10 @@ end
class TablizedOutput
def initialize header, stdout: nil
def initialize header, stdout: nil, format: nil
@header = header.map &:to_s
@columnc = header.size
@format = format || ['>']*@columnc
@maxs = header.map &:length
@stdout ||= STDOUT
@lines = []
@ -138,7 +139,7 @@ class TablizedOutput
end
def print order: nil
format = "#{(["\e[%%sm%% %ds\e[0m"] * @columnc).join( ' ') % @maxs}\n"
format = "#{@format.map {|f| "\e[%%sm%%#{case f when '<' then '-' else ' ' end}%ds\e[0m"}.join( ' ') % @maxs}\n"
ls = @lines
if order
eval <<-EOC, binding, __FILE__, 1+__LINE__

View file

@ -129,8 +129,8 @@ module Proxmox
end
end
def respond_to? method
super or instance_variable_defined?( "@#{method}")
def respond_to? method, also_private = false
instance_variable_defined?( "@#{method}") or super(method, also_private)
end
def method_missing method, *args, &exe
@ -154,7 +154,6 @@ module Proxmox
private :__update__
def refresh!
p self: self, refresh: @rest_prefix
__update__ rest_get( @rest_prefix)
end
end
@ -201,6 +200,20 @@ module Proxmox
rest_get( "#{@rest_prefix}/lxc").map {|d| LXC.send :__new__, d.merge( node: self, t: 'ct') }
end
def aplinfo
return [] if offline?
rest_get( "#{@rest_prefix}/aplinfo").map do |d|
AplInfo.send :__new__, d.merge( node: self, t: 'apl')
end
end
def storage
return [] if offline?
rest_get( "#{@rest_prefix}/storage").map do |d|
Storage.send :__new__, d.merge( node: self, t: 'sm')
end
end
def qemu
return [] if offline?
rest_get( "#{@rest_prefix}/qemu").map {|d| Qemu.send :__new__, d.merge( node: self, t: 'qm') }
@ -638,20 +651,51 @@ module Proxmox
self
end
def started?
'started' == self.state
def started?() 'started' == self.state end
def stopped?() 'stopped' == self.state end
def error?() 'error' == self.state end
def active?() ! ! digest end
end
class Storage < Base
def rest_prefix
@rest_prefix ||= "/nodes/#{@node.node}/storage/#{@storage}"
end
def stopped?
'stopped' == self.state
def content
rest_get( "#{@rest_prefix}/content").map do |c|
Content.send :__new__, c.merge( node: @node, storage: self, t: 'smc')
end
end
def error?
'error' == self.state
def initialize() rest_prefix end
def to_s() "#{@node.node}:#{@storage}" end
class Content < Base
def rest_prefix
@rest_prefix ||= "/nodes/#{@node.node}/storage/#{@storage}/content/#{@content}"
end
def initialize() rest_prefix end
def to_s() "#{node.node} #{volid}" end
end
end
class AplInfo < Base
def rest_prefix
@rest_prefix ||= "/nodes/#{@node.node}/aplinfo"
end
def active?
! ! digest
def initialize() rest_prefix end
def name() @template end
def system?() 'system' == @section end
def debian?() %r[\Adebian-] =~ @os end
def lxc?() 'lxc' == @type end
def download storage
upid = rest_post "#{@rest_prefix}", template: @template, storage: storage.to_s
Task.send :__new__, node: @node, host: self, upid: upid
end
end
end

View file

@ -187,7 +187,7 @@ module PVE::CTTemplate
case ostype
when 'debian'
# TODO: how to determine which template?
'local:vztmpl/debian-10-standard_10.7-1_amd64.tar.gz'
'local:vztmpl/debian-11-standard_11.0-1_amd64.tar.gz'
else
raise ArgumentError, "OS-Template for ostype #{ostype} not found or ostemplate not provided."
end