rvc 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -52,6 +52,15 @@ the "vm" module, but since it is a common operation it has been aliased to
52
52
  Commands and paths can be tab completed in the usual fashion. Whitespace must
53
53
  be escaped with a backslash.
54
54
 
55
+ === Wildcards
56
+
57
+ Many commands such as "vm.on" can operate on multiple objects at once. RVC
58
+ supports simple globbing using "*" as well as advanced regex syntax. To use a
59
+ regex, prefix the path element with "%". For example: "vm.on myvms/%^(linux|windows)"
60
+ will power on VMs whose names start with "linux" or "windows" in the "myvms" folder.
61
+ Note that it is necessary to explicitly anchor the pattern with "^" and "$" if you
62
+ wish to match the whole path element.
63
+
55
64
  === Marks
56
65
 
57
66
  192.168.1.105:/> mark a dc/vm/foo
data/TODO CHANGED
@@ -1,4 +1,8 @@
1
1
  add commands for more entities
2
2
  add fake subfolders for more entities
3
3
  add fastpass authentication
4
- globbing?
4
+
5
+ commands:
6
+ storage vmotion
7
+ edit clusters
8
+ add/remove hosts from clusters
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.2.0
data/bin/rvc CHANGED
@@ -81,8 +81,13 @@ if $opts[:path]
81
81
  lookup(parent_path).CreateFolder(:name => File.basename($opts[:path]))
82
82
  CMD.basic.cd $opts[:path]
83
83
  end
84
- elsif ARGV.size == 1
85
- CMD.basic.cd $shell.connections.keys.first
84
+ elsif $shell.connections.size == 1
85
+ conn_name, conn = $shell.connections.first
86
+ if conn.serviceContent.about.apiType == 'VirtualCenter'
87
+ CMD.basic.cd conn_name
88
+ else
89
+ CMD.basic.cd "#{conn_name}/ha-datacenter/vm"
90
+ end
86
91
  end
87
92
 
88
93
  unless CMD.vmrc.find_vmrc
@@ -91,7 +91,7 @@ module Completion
91
91
  last = trailing_slash ? '' : (els.pop || '')
92
92
  els.map! { |x| x.gsub '\\', '' }
93
93
  base_loc = absolute ? Location.new($shell.fs.root) : $shell.fs.loc
94
- found_loc = $shell.fs.traverse(base_loc, els) or return []
94
+ found_loc = $shell.fs.traverse(base_loc, els).first or return []
95
95
  cur = found_loc.obj
96
96
  els.unshift '' if absolute
97
97
  children = Cache[cur, :children] rescue []
@@ -21,7 +21,12 @@
21
21
  class RbVmomi::VIM::ComputeResource
22
22
  # TODO expand, optimize
23
23
  def display_info
24
+ stats = self.stats
25
+ pct_cpu_used = 100.0*stats[:usedCPU]/stats[:totalCPU]
26
+ pct_mem_used = 100.0*stats[:usedMem]/stats[:totalMem]
24
27
  puts "name: #{name}"
28
+ puts "cpu #{stats[:totalCPU]} GHz (#{pct_cpu_used.to_i}% used)"
29
+ puts "memory #{stats[:totalMem]} GB (#{pct_mem_used.to_i}% used)"
25
30
  puts "hosts:"
26
31
  host.each do |h|
27
32
  puts " #{h.name}"
data/lib/rvc/fs.rb CHANGED
@@ -52,7 +52,9 @@ end
52
52
  class FS
53
53
  attr_reader :root, :loc, :marks
54
54
 
55
- MARK_REGEX = /^~(?:([\d\w]*|~|@))$/
55
+ MARK_PATTERN = /^~(?:([\d\w]*|~|@))$/
56
+ REGEX_PATTERN = /^%/
57
+ GLOB_PATTERN = /\*/
56
58
 
57
59
  def initialize root
58
60
  @root = root
@@ -68,15 +70,13 @@ class FS
68
70
  @loc.path * '/'
69
71
  end
70
72
 
71
- def lookup path
72
- (lookup_loc(path) || return).obj
73
- end
74
-
75
- def cd path
76
- new_loc = lookup_loc(path) or return false
73
+ def cd new_loc
77
74
  mark '~', @loc
78
75
  @loc = new_loc
79
- true
76
+ end
77
+
78
+ def lookup path
79
+ lookup_loc(path).map(&:obj)
80
80
  end
81
81
 
82
82
  def lookup_loc path
@@ -85,30 +85,51 @@ class FS
85
85
  traverse(base_loc, els)
86
86
  end
87
87
 
88
- def traverse base_loc, els
89
- loc = base_loc.dup
90
- els.each_with_index do |el,i|
91
- case el
92
- when '.'
93
- when '..'
94
- loc.pop unless loc.obj == @root
95
- when '...'
96
- loc.push(el, loc.obj.parent) unless loc.obj == @root
97
- when MARK_REGEX
98
- return unless i == 0
99
- loc = @marks[$1] or return
100
- loc = loc.dup
88
+ def traverse_one loc, el, first
89
+ case el
90
+ when '.'
91
+ [loc]
92
+ when '..'
93
+ loc.pop unless loc.obj == @root
94
+ [loc]
95
+ when '...'
96
+ loc.push(el, loc.obj.parent) unless loc.obj == @root
97
+ [loc]
98
+ when MARK_PATTERN
99
+ return unless first
100
+ loc = @marks[$1] or return []
101
+ [loc.dup]
102
+ when REGEX_PATTERN
103
+ regex = Regexp.new($')
104
+ loc.obj.children.
105
+ select { |k,v| k =~ regex }.
106
+ map { |k,v| loc.dup.tap { |x| x.push(k, v) } }
107
+ when GLOB_PATTERN
108
+ regex = glob_to_regex el
109
+ loc.obj.children.
110
+ select { |k,v| k =~ regex }.
111
+ map { |k,v| loc.dup.tap { |x| x.push(k, v) } }
112
+ else
113
+ # XXX check for ambiguous child
114
+ if first and el =~ /^\d+$/ and @marks.member? el
115
+ loc = @marks[el].dup
101
116
  else
102
- # XXX check for ambiguous child
103
- if i == 0 and el =~ /^\d+$/ and @marks.member? el
104
- loc = @marks[el].dup
105
- else
106
- x = loc.obj.traverse_one(el) or return
107
- loc.push el, x
108
- end
117
+ x = loc.obj.traverse_one(el) or return []
118
+ loc.push el, x
109
119
  end
120
+ [loc]
121
+ end
122
+ end
123
+
124
+ # Starting from base_loc, traverse each path element in els. Since the path
125
+ # may contain wildcards, this function returns a list of matches.
126
+ def traverse base_loc, els
127
+ locs = [base_loc.dup]
128
+ els.each_with_index do |el,i|
129
+ locs.map! { |loc| traverse_one loc, el, i==0 }
130
+ locs.flatten!
110
131
  end
111
- loc
132
+ locs
112
133
  end
113
134
 
114
135
  def mark key, loc
@@ -118,6 +139,10 @@ class FS
118
139
  @marks[key] = loc
119
140
  end
120
141
  end
142
+
143
+ def glob_to_regex str
144
+ Regexp.new "^#{Regexp.escape(str.gsub('*', "\0")).gsub("\0", ".*")}$"
145
+ end
121
146
  end
122
147
 
123
148
  end
@@ -42,7 +42,7 @@ rvc_alias :help
42
42
  HELP_ORDER = %w(basic vm)
43
43
 
44
44
  def help path
45
- obj = lookup(path) if path
45
+ obj = lookup_single(path) if path
46
46
 
47
47
  if obj
48
48
  puts "Relevant commands for #{obj.class}:"
@@ -119,7 +119,9 @@ end
119
119
  rvc_alias :cd
120
120
 
121
121
  def cd path
122
- $shell.fs.cd(path) or err "Not found: #{path.inspect}"
122
+ # XXX check for multiple matches
123
+ new_loc = $shell.fs.lookup_loc(path).first or err "Not found: #{path.inspect}"
124
+ $shell.fs.cd(new_loc)
123
125
  $shell.fs.mark '', find_ancestor_loc(RbVmomi::VIM::Datacenter)
124
126
  $shell.fs.mark '@', find_ancestor_loc(RbVmomi::VIM)
125
127
  $shell.fs.marks.delete_if { |k,v| k =~ /^\d+$/ }
@@ -141,7 +143,8 @@ rvc_alias :ls
141
143
  rvc_alias :ls, :l
142
144
 
143
145
  def ls path
144
- loc = $shell.fs.lookup_loc(path) or err "Not found: #{path.inspect}"
146
+ # XXX check for multiple matches
147
+ loc = $shell.fs.lookup_loc(path).first or err "Not found: #{path.inspect}"
145
148
  obj = loc.obj
146
149
  children = obj.children
147
150
  name_map = children.invert
@@ -177,7 +180,7 @@ def ls path
177
180
 
178
181
  results.each do |r|
179
182
  name = name_map[r.obj]
180
- text = r.obj.ls_text r
183
+ text = r.obj.ls_text(r) rescue " (error)"
181
184
  realname = r['name'] if name != r['name']
182
185
  puts "#{i} #{name}#{realname && " [#{realname}]"}#{text}"
183
186
  mark_loc = loc.dup.tap { |x| x.push name, r.obj }
@@ -227,7 +230,8 @@ rvc_alias :mark, :m
227
230
 
228
231
  def mark key, path
229
232
  err "invalid mark name" unless key =~ /^\w+$/
230
- obj = $shell.fs.lookup_loc(path) or err "Not found: #{path.inspect}"
233
+ # XXX aggregate marks
234
+ obj = $shell.fs.lookup_loc(path).first or err "Not found: #{path.inspect}"
231
235
  $shell.fs.mark key, obj
232
236
  end
233
237
 
@@ -271,6 +275,6 @@ rvc_alias :mkdir
271
275
 
272
276
  # TODO dispatch to datastore.mkdir if path is in a datastore
273
277
  def mkdir path
274
- parent = lookup! File.dirname(path), RbVmomi::VIM::Folder
278
+ parent = lookup_single! File.dirname(path), RbVmomi::VIM::Folder
275
279
  parent.CreateFolder(:name => File.basename(path))
276
280
  end
@@ -0,0 +1,63 @@
1
+ # Copyright (c) 2011 VMware, Inc. All Rights Reserved.
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ opts :create do
22
+ summary "Create a cluster"
23
+ arg :dest, nil, :lookup_parent => VIM::Folder
24
+ end
25
+
26
+ def create dest
27
+ folder, name = *dest
28
+ folder.CreateClusterEx(:name => name, :spec => {})
29
+ end
30
+
31
+
32
+ opts :add_host do
33
+ summary "Add a host to a cluster"
34
+ arg :cluster, nil, :lookup => VIM::ClusterComputeResource
35
+ arg :hostname, nil
36
+ opt :username, "Username", :short => 'u', :default => 'root'
37
+ opt :password, "Password", :short => 'p', :default => ''
38
+ end
39
+
40
+ def add_host cluster, hostname, opts
41
+ sslThumbprint = nil
42
+ while true
43
+ spec = {
44
+ :force => false,
45
+ :hostName => hostname,
46
+ :userName => opts[:username],
47
+ :password => opts[:password],
48
+ :sslThumbprint => sslThumbprint,
49
+ }
50
+ task = cluster.AddHost_Task :spec => spec,
51
+ :asConnected => false
52
+ begin
53
+ task.wait_for_completion
54
+ rescue VIM::SSLVerifyFault
55
+ puts "SSL thumbprint: #{$!.fault.thumbprint}"
56
+ $stdout.write "Accept this thumbprint? (y/n) "
57
+ $stdout.flush
58
+ answer = $stdin.readline.chomp
59
+ err "Aborted" unless answer == 'y' or answer == 'yes'
60
+ sslThumbprint = $!.fault.thumbprint
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2011 VMware, Inc. All Rights Reserved.
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ opts :create do
22
+ summary "Create a datacenter"
23
+ arg :dest, nil, :lookup_parent => [VIM::Folder, VIM]
24
+ end
25
+
26
+ def create dest
27
+ folder, name = *dest
28
+ folder = folder.rootFolder if folder.is_a? VIM
29
+ folder.CreateDatacenter(:name => name)
30
+ end
@@ -25,7 +25,7 @@ opts :download do
25
25
  end
26
26
 
27
27
  def download datastore_path, local_path
28
- file = lookup(datastore_path)
28
+ file = lookup_single(datastore_path)
29
29
  err "not a datastore file" unless file.is_a? RbVmomi::VIM::Datastore::FakeDatastoreFile
30
30
  file.datastore.download file.path, local_path
31
31
  end
@@ -38,7 +38,7 @@ end
38
38
 
39
39
  def upload local_path, datastore_path
40
40
  datastore_dir_path = File.dirname datastore_path
41
- dir = lookup(datastore_dir_path)
41
+ dir = lookup_single(datastore_dir_path)
42
42
  err "datastore directory does not exist" unless dir.is_a? RbVmomi::VIM::Datastore::FakeDatastoreFolder
43
43
  err "local file does not exist" unless File.exists? local_path
44
44
  real_datastore_path = "#{dir.path}/#{File.basename(datastore_path)}"
@@ -52,7 +52,7 @@ end
52
52
 
53
53
  def mkdir datastore_path
54
54
  datastore_dir_path = File.dirname datastore_path
55
- dir = lookup(datastore_dir_path)
55
+ dir = lookup_single(datastore_dir_path)
56
56
  err "datastore directory does not exist" unless dir.is_a? RbVmomi::VIM::Datastore::FakeDatastoreFolder
57
57
  ds = dir.datastore
58
58
  dc = ds.path.find { |o,x| o.is_a? RbVmomi::VIM::Datacenter }[0]
@@ -92,3 +92,30 @@ end
92
92
  def exit_maintenance_mode hosts, opts
93
93
  tasks hosts, :ExitMaintenanceMode, :timeout => opts[:timeout]
94
94
  end
95
+
96
+
97
+ opts :disconnect do
98
+ summary "Disconnect a host"
99
+ arg :host, nil, :lookup => VIM::HostSystem, :multi => true
100
+ end
101
+
102
+ def disconnect hosts
103
+ tasks hosts, :DisconnectHost
104
+ end
105
+
106
+
107
+ opts :reconnect do
108
+ summary "Reconnect a host"
109
+ arg :host, nil, :lookup => VIM::HostSystem, :multi => true
110
+ opt :username, "Username", :short => 'u', :default => 'root'
111
+ opt :password, "Password", :short => 'p', :default => ''
112
+ end
113
+
114
+ def reconnect hosts, opts
115
+ spec = {
116
+ :force => false,
117
+ :userName => opts[:username],
118
+ :password => opts[:password],
119
+ }
120
+ tasks hosts, :ReconnectHost
121
+ end
@@ -68,21 +68,51 @@ def suspend vms
68
68
  end
69
69
 
70
70
 
71
+ opts :shutdown_guest do
72
+ summary "Shut down guest OS"
73
+ arg :vm, nil, :multi => true, :lookup => VIM::VirtualMachine
74
+ end
75
+
76
+ def shutdown_guest vms
77
+ vms.each(&:ShutdownGuest)
78
+ end
79
+
80
+
81
+ opts :standby_guest do
82
+ summary "Suspend guest OS"
83
+ arg :vm, nil, :multi => true, :lookup => VIM::VirtualMachine
84
+ end
85
+
86
+ def standby_guest vms
87
+ vms.each(&:StandbyGuest)
88
+ end
89
+
90
+
91
+ opts :reboot_guest do
92
+ summary "Reboot guest OS"
93
+ arg :vm, nil, :multi => true, :lookup => VIM::VirtualMachine
94
+ end
95
+
96
+ def reboot_guest vms
97
+ vms.each(&:RebootGuest)
98
+ end
99
+
100
+
71
101
  opts :create do
72
102
  summary "Create a new VM"
73
- arg :name, "Name"
103
+ arg :name, "Destination", :lookup_parent => VIM::Folder
74
104
  opt :pool, "Resource pool", :short => 'p', :type => :string, :lookup => VIM::ResourcePool
75
105
  opt :host, "Host", :short => 'h', :type => :string, :lookup => VIM::HostSystem
76
106
  opt :datastore, "Datastore", :short => 'd', :type => :string, :lookup => VIM::Datastore
77
107
  end
78
108
 
79
- def create name, opts
109
+ def create dest, opts
80
110
  err "must specify resource pool (--pool)" unless opts[:pool]
81
111
  err "must specify datastore (--datastore)" unless opts[:datastore]
82
- vmFolder = lookup!(File.dirname(name), VIM::Folder)
112
+ vmFolder, name = *dest
83
113
  datastore_path = "[#{opts[:datastore].name}]"
84
114
  config = {
85
- :name => File.basename(name),
115
+ :name => name,
86
116
  :guestId => 'otherGuest',
87
117
  :files => { :vmPathName => datastore_path },
88
118
  :numCPUs => 1,
@@ -99,7 +129,7 @@ def create name, opts
99
129
  :operation => :add,
100
130
  :fileOperation => :create,
101
131
  :device => VIM.VirtualDisk(
102
- :key => 0,
132
+ :key => -1,
103
133
  :backing => VIM.VirtualDiskFlatVer2BackingInfo(
104
134
  :fileName => datastore_path,
105
135
  :diskMode => :persistent,
@@ -112,7 +142,7 @@ def create name, opts
112
142
  }, {
113
143
  :operation => :add,
114
144
  :device => VIM.VirtualCdrom(
115
- :key => 0,
145
+ :key => -2,
116
146
  :connectable => {
117
147
  :allowGuestControl => true,
118
148
  :connected => true,
@@ -127,7 +157,7 @@ def create name, opts
127
157
  }, {
128
158
  :operation => :add,
129
159
  :device => VIM.VirtualE1000(
130
- :key => 0,
160
+ :key => -3,
131
161
  :deviceInfo => {
132
162
  :label => 'Network Adapter 1',
133
163
  :summary => 'VM Network'
@@ -341,13 +371,15 @@ end
341
371
  opts :ssh do
342
372
  summary "SSH to a VM"
343
373
  arg :vm, nil, :lookup => VIM::VirtualMachine
374
+ arg :cmd, "Optional command", :required => false, :default => nil
375
+ opt :login, "Username", :short => 'l', :default => 'root'
344
376
  end
345
377
 
346
378
  rvc_alias :ssh
347
379
 
348
- def ssh vm
380
+ def ssh vm, cmd, opts
349
381
  ip = vm_ip vm
350
- ssh_cmd = "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@#{ip}"
382
+ ssh_cmd = "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -l #{opts[:login]} #{ip} #{cmd}"
351
383
  system_fg(ssh_cmd)
352
384
  end
353
385
 
@@ -610,8 +642,8 @@ def find_vmx_files ds
610
642
  files
611
643
  end
612
644
 
613
- def change_device_connectivity id, label, connected
614
- dev = vm(id).config.hardware.device.find { |x| x.deviceInfo.label == label }
645
+ def change_device_connectivity vm, label, connected
646
+ dev = vm.config.hardware.device.find { |x| x.deviceInfo.label == label }
615
647
  err "no such device" unless dev
616
648
  dev.connectable.connected = connected
617
649
  spec = {
@@ -619,7 +651,7 @@ def change_device_connectivity id, label, connected
619
651
  { :operation => :edit, :device => dev },
620
652
  ]
621
653
  }
622
- vm(id).ReconfigVM_Task(:spec => spec).wait_for_completion
654
+ vm.ReconfigVM_Task(:spec => spec).wait_for_completion
623
655
  end
624
656
 
625
657
  def vm_ip vm
@@ -55,18 +55,21 @@ class OptionParser < Trollop::Parser
55
55
  @has_options
56
56
  end
57
57
 
58
- def arg name, description, opts={}
59
- opts = {
58
+ def arg name, description, spec={}
59
+ spec = {
60
+ :description => description,
60
61
  :required => true,
61
62
  :default => nil,
62
63
  :multi => false,
63
- }.merge opts
64
- opts[:default] = [] if opts[:multi] and opts[:default].nil?
64
+ }.merge spec
65
+ spec[:default] = [] if spec[:multi] and spec[:default].nil?
65
66
  fail "Multi argument must be the last one" if @seen_multi
66
- fail "Can't have required argument after optional ones" if opts[:required] and @seen_not_required
67
- @applicable << opts[:lookup] if opts[:lookup]
68
- @args << [name, description, opts[:required], opts[:default], opts[:multi], opts[:lookup]]
69
- text " #{name}: " + [description, opts[:lookup]].compact.join(' ')
67
+ fail "Can't have required argument after optional ones" if spec[:required] and @seen_not_required
68
+ fail "lookup and lookup_parent are mutually exclusive" if spec[:lookup] and spec[:lookup_parent]
69
+ @applicable << spec[:lookup] if spec[:lookup]
70
+ @applicable << spec[:lookup_parent] if spec[:lookup_parent]
71
+ @args << [name,spec]
72
+ text " #{name}: " + [description, spec[:lookup], spec[:lookup_parent]].compact.join(' ')
70
73
  end
71
74
 
72
75
  def parse argv
@@ -74,35 +77,48 @@ class OptionParser < Trollop::Parser
74
77
 
75
78
  @specs.each do |name,spec|
76
79
  next unless klass = spec[:lookup] and path = opts[name]
77
- opts[name] = lookup! path, klass
80
+ opts[name] = lookup_single! path, klass
78
81
  end
79
82
 
80
83
  argv = leftovers
81
84
  args = []
82
- @args.each do |name,desc,required,default,multi,lookup_klass|
83
- if multi
84
- err "missing argument '#{name}'" if required and argv.empty?
85
- a = (argv.empty? ? default : argv.dup)
86
- a.map! { |x| lookup! x, lookup_klass } if lookup_klass
85
+ @args.each do |name,spec|
86
+ if spec[:multi]
87
+ err "missing argument '#{name}'" if spec[:required] and argv.empty?
88
+ a = (argv.empty? ? spec[:default] : argv.dup)
89
+ a = a.map { |x| postprocess_arg x, spec }.inject(:+)
90
+ err "no matches for '#{name}'" if spec[:required] and a.empty?
87
91
  args << a
88
92
  argv.clear
89
93
  else
90
94
  x = argv.shift
91
- err "missing argument '#{name}'" if required and x.nil?
92
- x = default if x.nil?
93
- x = lookup! x, lookup_klass if lookup_klass
94
- args << x
95
+ err "missing argument '#{name}'" if spec[:required] and x.nil?
96
+ x = spec[:default] if x.nil?
97
+ a = x.nil? ? [] : postprocess_arg(x, spec)
98
+ err "more than one match for #{name}" if a.size > 1
99
+ err "no match for '#{name}'" if spec[:required] and a.empty?
100
+ args << a.first
95
101
  end
96
102
  end
97
103
  err "too many arguments" unless argv.empty?
98
104
  return args, opts
99
105
  end
100
106
 
107
+ def postprocess_arg x, spec
108
+ if spec[:lookup]
109
+ lookup! x, spec[:lookup]
110
+ elsif spec[:lookup_parent]
111
+ lookup!(File.dirname(x), spec[:lookup_parent]).map { |y| [y, File.basename(x)] }
112
+ else
113
+ [x]
114
+ end
115
+ end
116
+
101
117
  def educate
102
- arg_texts = @args.map do |name,desc,required,default,multi,lookup_klass|
118
+ arg_texts = @args.map do |name,spec|
103
119
  text = name
104
- text = "[#{text}]" if not required
105
- text = "#{text}..." if multi
120
+ text = "[#{text}]" if not spec[:required]
121
+ text = "#{text}..." if spec[:multi]
106
122
  text
107
123
  end
108
124
  arg_texts.unshift "[opts]" if has_options?
data/lib/rvc/shell.rb CHANGED
@@ -80,7 +80,7 @@ class Shell
80
80
  return unless cmd
81
81
  err "invalid command" unless cmd.is_a? String
82
82
  case cmd
83
- when RVC::FS::MARK_REGEX
83
+ when RVC::FS::MARK_PATTERN
84
84
  CMD.basic.cd cmd
85
85
  else
86
86
  if cmd.include? '.'
@@ -192,11 +192,11 @@ class RubyEvaluator
192
192
  end
193
193
 
194
194
  def dc
195
- @fs.lookup("~")
195
+ @fs.lookup("~").first
196
196
  end
197
197
 
198
198
  def conn
199
- @fs.lookup("~@")
199
+ @fs.lookup("~@").first
200
200
  end
201
201
 
202
202
  def method_missing sym, *a
data/lib/rvc/util.rb CHANGED
@@ -22,17 +22,34 @@ module RVC
22
22
  module Util
23
23
  extend self
24
24
 
25
+ # XXX just use an optional argument for type
25
26
  def lookup path
26
27
  $shell.fs.lookup path
27
28
  end
28
29
 
29
- def lookup! path, type
30
- lookup(path).tap do |obj|
31
- err "Not found: #{path.inspect}" unless obj
32
- err "Expected #{type} but got #{obj.class} at #{path.inspect}" unless obj.is_a? type
30
+ def lookup_single path
31
+ objs = lookup(path, type)
32
+ err "Not found: #{path.inspect}" if objs.empty?
33
+ err "More than one match for #{path.inspect}" if objs.size > 1
34
+ objs.first
35
+ end
36
+
37
+ def lookup! path, types
38
+ types = [types] unless types.is_a? Enumerable
39
+ lookup(path).tap do |objs|
40
+ objs.each do |obj|
41
+ err "Expected #{types*'/'} but got #{obj.class} at #{path.inspect}" unless types.any? { |type| obj.is_a? type }
42
+ end
33
43
  end
34
44
  end
35
45
 
46
+ def lookup_single! path, type
47
+ objs = lookup!(path, type)
48
+ err "Not found: #{path.inspect}" if objs.empty?
49
+ err "More than one match for #{path.inspect}" if objs.size > 1
50
+ objs.first
51
+ end
52
+
36
53
  def menu items
37
54
  items.each_with_index { |x, i| puts "#{i} #{x}" }
38
55
  input = Readline.readline("? ", false)
data/test/test_fs.rb CHANGED
@@ -3,10 +3,16 @@ require 'rvc'
3
3
  require 'inventory_fixtures'
4
4
 
5
5
  class FSTest < Test::Unit::TestCase
6
+ NodeDaa = FixtureNode.new 'Daa'
7
+ NodeDab = FixtureNode.new 'Dab'
8
+ NodeDabc = FixtureNode.new 'Dabc'
9
+ NodeDac = FixtureNode.new 'Dac'
6
10
  NodeB = FixtureNode.new 'B'
7
11
  NodeC = FixtureNode.new 'C'
12
+ NodeD = FixtureNode.new('D', 'daa' => NodeDaa, 'dab' => NodeDab,
13
+ 'dabc' => NodeDabc, 'dac' => NodeDac)
8
14
  NodeA = FixtureNode.new('A', 'b' => NodeB, 'c' => NodeC)
9
- Root = FixtureNode.new('ROOT', 'a' => NodeA)
15
+ Root = FixtureNode.new('ROOT', 'a' => NodeA, 'd' => NodeD)
10
16
 
11
17
  def setup
12
18
  @context = RVC::FS.new Root
@@ -25,31 +31,31 @@ class FSTest < Test::Unit::TestCase
25
31
  end
26
32
 
27
33
  def test_lookup_simple
28
- assert_equal nil, @context.lookup('nonexistent')
29
- assert_equal Root, @context.lookup('.')
30
- assert_equal Root, @context.lookup('..')
31
- assert_equal Root, @context.lookup('...')
32
- assert_equal NodeA, @context.lookup('a')
33
- assert_equal NodeB, @context.lookup('a/b')
34
- assert_equal NodeC, @context.lookup('a/b/../c')
35
- assert_equal NodeC, @context.lookup('a/b/.../c')
34
+ assert_equal [], @context.lookup('nonexistent')
35
+ assert_equal [Root], @context.lookup('.')
36
+ assert_equal [Root], @context.lookup('..')
37
+ assert_equal [Root], @context.lookup('...')
38
+ assert_equal [NodeA], @context.lookup('a')
39
+ assert_equal [NodeB], @context.lookup('a/b')
40
+ assert_equal [NodeC], @context.lookup('a/b/../c')
41
+ assert_equal [NodeC], @context.lookup('a/b/.../c')
36
42
  end
37
43
 
38
44
  def test_lookup_loc_nonexistent
39
45
  loc = @context.lookup_loc 'nonexistent'
40
- assert_equal nil, loc
46
+ assert_equal [], loc
41
47
  end
42
48
 
43
49
  def test_lookup_loc_simple
44
50
  %w(a /a ./a ./a/.).each do |path|
45
- loc = @context.lookup_loc path
51
+ loc = @context.lookup_loc(path)[0]
46
52
  assert_equal NodeA, loc.obj
47
53
  assert_equal ['', 'a'], loc.path
48
54
  assert_equal [['', Root], ['a', NodeA]], loc.stack
49
55
  end
50
56
 
51
57
  %w(a/b /a/b ./a/b /a/b/.).each do |path|
52
- loc = @context.lookup_loc path
58
+ loc = @context.lookup_loc(path)[0]
53
59
  assert_equal NodeB, loc.obj
54
60
  assert_equal ['', 'a', 'b'], loc.path
55
61
  assert_equal [['', Root], ['a', NodeA], ['b', NodeB]], loc.stack
@@ -57,57 +63,82 @@ class FSTest < Test::Unit::TestCase
57
63
  end
58
64
 
59
65
  def test_lookup_loc_parent
60
- loc = @context.lookup_loc '..'
66
+ loc = @context.lookup_loc('..')[0]
61
67
  assert_equal [['', Root]], loc.stack
62
68
 
63
- loc = @context.lookup_loc 'a/..'
69
+ loc = @context.lookup_loc('a/..')[0]
64
70
  assert_equal [['', Root]], loc.stack
65
71
 
66
- loc = @context.lookup_loc 'a/b/..'
72
+ loc = @context.lookup_loc('a/b/..')[0]
67
73
  assert_equal [['', Root], ['a', NodeA]], loc.stack
68
74
  end
69
75
 
70
76
  def test_lookup_loc_realparent
71
- loc = @context.lookup_loc '...'
77
+ loc = @context.lookup_loc('...')[0]
72
78
  assert_equal [['', Root]], loc.stack
73
79
 
74
- loc = @context.lookup_loc 'a/...'
80
+ loc = @context.lookup_loc('a/...')[0]
75
81
  assert_equal [['', Root], ['a', NodeA], ['...', Root]], loc.stack
76
82
 
77
- loc = @context.lookup_loc 'a/b/...'
83
+ loc = @context.lookup_loc('a/b/...')[0]
78
84
  assert_equal [['', Root], ['a', NodeA], ['b', NodeB], ['...', NodeA]], loc.stack
79
85
  end
80
86
 
81
87
  def test_lookup_loc_mark
82
- b_loc = @context.lookup_loc 'a/b'
88
+ b_loc = @context.lookup_loc('a/b')[0]
83
89
  assert_not_nil b_loc
84
90
 
85
- loc = @context.lookup_loc '~foo'
91
+ loc = @context.lookup_loc('~foo')[0]
86
92
  assert_equal nil, loc
87
93
 
88
94
  ['foo', '~', '7', ''].each do |mark|
89
95
  @context.mark mark, b_loc
90
- loc = @context.lookup_loc "~#{mark}"
96
+ loc = @context.lookup_loc("~#{mark}")[0]
91
97
  assert_equal [['', Root], ['a', NodeA], ['b', NodeB]], loc.stack
92
98
 
93
99
  @context.mark mark, nil
94
- loc = @context.lookup_loc "~#{mark}"
100
+ loc = @context.lookup_loc("~#{mark}")[0]
95
101
  assert_equal nil, loc
96
102
  end
97
103
 
98
104
  @context.mark '7', b_loc
99
- loc = @context.lookup_loc "7"
105
+ loc = @context.lookup_loc("7")[0]
100
106
  assert_equal [['', Root], ['a', NodeA], ['b', NodeB]], loc.stack
101
107
 
102
108
  @context.mark '7', nil
103
- loc = @context.lookup_loc "7"
109
+ loc = @context.lookup_loc("7")[0]
104
110
  assert_equal nil, loc
105
111
  end
106
112
 
107
113
  def test_cd
108
- assert_equal false, @context.cd("nonexistent")
109
114
  assert_equal [['', Root]], @context.loc.stack
110
- assert_equal true, @context.cd("a")
115
+ @context.cd(@context.lookup_loc("a")[0])
111
116
  assert_equal [['', Root], ['a', NodeA]], @context.loc.stack
112
117
  end
118
+
119
+ def test_regex
120
+ daa = [['', Root], ['d', NodeD], ['daa', NodeDaa]]
121
+ dab = [['', Root], ['d', NodeD], ['dab', NodeDab]]
122
+ dabc = [['', Root], ['d', NodeD], ['dabc', NodeDabc]]
123
+ dac = [['', Root], ['d', NodeD], ['dac', NodeDac]]
124
+ locs = @context.lookup_loc '/d/%^daa'
125
+ assert_equal [daa], locs.map(&:stack)
126
+ locs = @context.lookup_loc '/d/%^daa.*'
127
+ assert_equal [daa], locs.map(&:stack)
128
+ locs = @context.lookup_loc '/d/%^da.*c'
129
+ assert_equal [dabc, dac], locs.map(&:stack)
130
+ end
131
+
132
+ def test_glob
133
+ daa = [['', Root], ['d', NodeD], ['daa', NodeDaa]]
134
+ dab = [['', Root], ['d', NodeD], ['dab', NodeDab]]
135
+ dabc = [['', Root], ['d', NodeD], ['dabc', NodeDabc]]
136
+ dac = [['', Root], ['d', NodeD], ['dac', NodeDac]]
137
+ locs = @context.lookup_loc '/d/*daa*'
138
+ assert_equal [daa], locs.map(&:stack)
139
+ locs = @context.lookup_loc '/d/d*a'
140
+ assert_equal [daa], locs.map(&:stack)
141
+ locs = @context.lookup_loc '/d/da*c'
142
+ assert_equal [dabc, dac], locs.map(&:stack)
143
+ end
113
144
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: rvc
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.1.0
5
+ version: 1.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Rich Lane
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-04-05 00:00:00 -07:00
13
+ date: 2011-04-12 00:00:00 -07:00
14
14
  default_executable: rvc
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -81,6 +81,8 @@ files:
81
81
  - lib/rvc/inventory.rb
82
82
  - lib/rvc/modules.rb
83
83
  - lib/rvc/modules/basic.rb
84
+ - lib/rvc/modules/cluster.rb
85
+ - lib/rvc/modules/datacenter.rb
84
86
  - lib/rvc/modules/datastore.rb
85
87
  - lib/rvc/modules/host.rb
86
88
  - lib/rvc/modules/resource_pool.rb