VirtualBox

source: vbox/trunk/src/VBox/Frontends/VBoxShell/vboxshell.py@ 20897

Last change on this file since 20897 was 20897, checked in by vboxsync, 16 years ago

Python: cleanup, refactoring, don't fail on timed wait requests on Darwin, just return, shell improvments

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 22.2 KB
Line 
1#!/usr/bin/python
2#
3# Copyright (C) 2009 Sun Microsystems, Inc.
4#
5# This file is part of VirtualBox Open Source Edition (OSE), as
6# available from http://www.virtualbox.org. This file is free software;
7# you can redistribute it and/or modify it under the terms of the GNU
8# General Public License (GPL) as published by the Free Software
9# Foundation, in version 2 as it comes in the "COPYING" file of the
10# VirtualBox OSE distribution. VirtualBox OSE is distributed in the
11# hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
12#
13# Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
14# Clara, CA 95054 USA or visit http://www.sun.com if you need
15# additional information or have any questions.
16#
17#
18#################################################################################
19# This program is a simple interactive shell for VirtualBox. You can query #
20# information and issue commands from a simple command line. #
21# #
22# It also provides you with examples on how to use VirtualBox's Python API. #
23# This shell is even somewhat documented and supports TAB-completion and #
24# history if you have Python readline installed. #
25# #
26# Enjoy. #
27################################################################################
28
29import os,sys
30import traceback
31
32# Simple implementation of IConsoleCallback, one can use it as skeleton
33# for custom implementations
34class GuestMonitor:
35 def __init__(self, mach):
36 self.mach = mach
37
38 def onMousePointerShapeChange(self, visible, alpha, xHot, yHot, width, height, shape):
39 print "%s: onMousePointerShapeChange: visible=%d" %(self.mach.name, visible)
40 def onMouseCapabilityChange(self, supportsAbsolute, needsHostCursor):
41 print "%s: onMouseCapabilityChange: needsHostCursor=%d" %(self.mach.name, needsHostCursor)
42
43 def onKeyboardLedsChange(self, numLock, capsLock, scrollLock):
44 print "%s: onKeyboardLedsChange capsLock=%d" %(self.mach.name, capsLock)
45
46 def onStateChange(self, state):
47 print "%s: onStateChange state=%d" %(self.mach.name, state)
48
49 def onAdditionsStateChange(self):
50 print "%s: onAdditionsStateChange" %(self.mach.name)
51
52 def onDVDDriveChange(self):
53 print "%s: onDVDDriveChange" %(self.mach.name)
54
55 def onFloppyDriveChange(self):
56 print "%s: onFloppyDriveChange" %(self.mach.name)
57
58 def onNetworkAdapterChange(self, adapter):
59 print "%s: onNetworkAdapterChange" %(self.mach.name)
60
61 def onSerialPortChange(self, port):
62 print "%s: onSerialPortChange" %(self.mach.name)
63
64 def onParallelPortChange(self, port):
65 print "%s: onParallelPortChange" %(self.mach.name)
66
67 def onStorageControllerChange(self):
68 print "%s: onStorageControllerChange" %(self.mach.name)
69
70 def onVRDPServerChange(self):
71 print "%s: onVRDPServerChange" %(self.mach.name)
72
73 def onUSBControllerChange(self):
74 print "%s: onUSBControllerChange" %(self.mach.name)
75
76 def onUSBDeviceStateChange(self, device, attached, error):
77 print "%s: onUSBDeviceStateChange" %(self.mach.name)
78
79 def onSharedFolderChange(self, scope):
80 print "%s: onSharedFolderChange" %(self.mach.name)
81
82 def onRuntimeError(self, fatal, id, message):
83 print "%s: onRuntimeError fatal=%d message=%s" %(self.mach.name, fatal, message)
84
85 def onCanShowWindow(self):
86 print "%s: onCanShowWindow" %(self.mach.name)
87 return True
88
89 def onShowWindow(self, winId):
90 print "%s: onShowWindow: %d" %(self.mach.name, winId)
91
92class VBoxMonitor:
93 def __init__(self, vbox):
94 self.vbox = vbox
95 pass
96
97 def onMachineStateChange(self, id, state):
98 print "onMachineStateChange: %s %d" %(id, state)
99
100 def onMachineDataChange(self,id):
101 print "onMachineDataChange: %s" %(id)
102
103 def onExtraDataCanChange(self, id, key, value):
104 print "onExtraDataCanChange: %s %s=>%s" %(id, key, value)
105 return True, ""
106
107 def onExtraDataChange(self, id, key, value):
108 print "onExtraDataChange: %s %s=>%s" %(id, key, value)
109
110 def onMediaRegistred(self, id, type, registred):
111 print "onMediaRegistred: %s" %(id)
112
113 def onMachineRegistred(self, id, registred):
114 print "onMachineRegistred: %s" %(id)
115
116 def onSessionStateChange(self, id, state):
117 print "onSessionStateChange: %s %d" %(id, state)
118
119 def onSnapshotTaken(self, mach, id):
120 print "onSnapshotTaken: %s %s" %(mach, id)
121
122 def onSnapshotDiscarded(self, mach, id):
123 print "onSnapshotDiscarded: %s %s" %(mach, id)
124
125 def onSnapshotChange(self, mach, id):
126 print "onSnapshotChange: %s %s" %(mach, id)
127
128 def onGuestPropertyChange(self, id, name, newValue, flags):
129 print "onGuestPropertyChange: %s: %s=%s" %(id, name, newValue)
130
131g_hasreadline = 1
132try:
133 import readline
134 import rlcompleter
135except:
136 g_hasreadline = 0
137
138
139if g_hasreadline:
140 class CompleterNG(rlcompleter.Completer):
141 def __init__(self, dic, ctx):
142 self.ctx = ctx
143 return rlcompleter.Completer.__init__(self,dic)
144
145 def complete(self, text, state):
146 """
147 taken from:
148 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496812
149 """
150 if text == "":
151 return ['\t',None][state]
152 else:
153 return rlcompleter.Completer.complete(self,text,state)
154
155 def global_matches(self, text):
156 """
157 Compute matches when text is a simple name.
158 Return a list of all names currently defined
159 in self.namespace that match.
160 """
161
162 matches = []
163 n = len(text)
164
165 for list in [ self.namespace ]:
166 for word in list:
167 if word[:n] == text:
168 matches.append(word)
169
170
171 try:
172 for m in getMachines(self.ctx):
173 # although it has autoconversion, we need to cast
174 # explicitly for subscripts to work
175 word = str(m.name)
176 if word[:n] == text:
177 matches.append(word)
178 word = str(m.id)
179 if word[0] == '{':
180 word = word[1:-1]
181 if word[:n] == text:
182 matches.append(word)
183 except Exception,e:
184 traceback.print_exc()
185 print e
186
187 return matches
188
189
190def autoCompletion(commands, ctx):
191 if not g_hasreadline:
192 return
193
194 comps = {}
195 for (k,v) in commands.items():
196 comps[k] = None
197 completer = CompleterNG(comps, ctx)
198 readline.set_completer(completer.complete)
199 readline.parse_and_bind("tab: complete")
200
201g_verbose = True
202
203def split_no_quotes(s):
204 return s.split()
205
206def createVm(ctx,name,kind,base):
207 mgr = ctx['mgr']
208 vb = ctx['vb']
209 mach = vb.createMachine(name, kind, base,
210 "00000000-0000-0000-0000-000000000000")
211 mach.saveSettings()
212 print "created machine with UUID",mach.id
213 vb.registerMachine(mach)
214
215def removeVm(ctx,mach):
216 mgr = ctx['mgr']
217 vb = ctx['vb']
218 id = mach.id
219 print "removing machine ",mach.name,"with UUID",id
220 session = ctx['global'].openMachineSession(id)
221 mach=session.machine
222 for d in mach.getHardDiskAttachments():
223 mach.detachHardDisk(d.controller, d.port, d.device)
224 ctx['global'].closeMachineSession(session)
225 mach = vb.unregisterMachine(id)
226 if mach:
227 mach.deleteSettings()
228
229def startVm(ctx,mach,type):
230 mgr = ctx['mgr']
231 vb = ctx['vb']
232 perf = ctx['perf']
233 session = mgr.getSessionObject(vb)
234 uuid = mach.id
235 progress = vb.openRemoteSession(session, uuid, type, "")
236 progress.waitForCompletion(-1)
237 completed = progress.completed
238 rc = int(progress.resultCode)
239 print "Completed:", completed, "rc:",hex(rc&0xffffffff)
240 if rc == 0:
241 # we ignore exceptions to allow starting VM even if
242 # perf collector cannot be started
243 if perf:
244 try:
245 perf.setup(['*'], [mach], 10, 15)
246 except Exception,e:
247 print e
248 if g_verbose:
249 traceback.print_exc()
250 pass
251 # if session not opened, close doesn't make sense
252 session.close()
253 else:
254 # Not yet implemented error string query API for remote API
255 if not ctx['remote']:
256 print session.QueryErrorObject(rc)
257
258def getMachines(ctx):
259 return ctx['global'].getArray(ctx['vb'], 'machines')
260
261def asState(var):
262 if var:
263 return 'on'
264 else:
265 return 'off'
266
267def guestStats(ctx,mach):
268 if not ctx['perf']:
269 return
270 for metric in ctx['perf'].query(["*"], [mach]):
271 print metric['name'], metric['values_as_string']
272
273def guestExec(ctx, machine, console, cmds):
274 exec cmds
275
276def monitorGuest(ctx, machine, console, dur):
277 import time
278 cb = ctx['global'].createCallback('IConsoleCallback', GuestMonitor, machine)
279 console.registerCallback(cb)
280 if dur == -1:
281 # not infinity, but close enough
282 dur = 100000
283 try:
284 end = time.time() + dur
285 while time.time() < end:
286 ctx['global'].waitForEvents(500)
287 # We need to catch all exceptions here, otherwise callback will never be unregistered
288 except:
289 pass
290 console.unregisterCallback(cb)
291
292
293def monitorVbox(ctx, dur):
294 import time
295 vbox = ctx['vb']
296 cb = ctx['global'].createCallback('IVirtualBoxCallback', VBoxMonitor, vbox)
297 vbox.registerCallback(cb)
298 if dur == -1:
299 # not infinity, but close enough
300 dur = 100000
301 try:
302 end = time.time() + dur
303 while time.time() < end:
304 ctx['global'].waitForEvents(500)
305 # We need to catch all exceptions here, otherwise callback will never be unregistered
306 except:
307 if g_verbose:
308 traceback.print_exc()
309 vbox.unregisterCallback(cb)
310
311def cmdExistingVm(ctx,mach,cmd,args):
312 mgr=ctx['mgr']
313 vb=ctx['vb']
314 session = mgr.getSessionObject(vb)
315 uuid = mach.id
316 try:
317 progress = vb.openExistingSession(session, uuid)
318 except Exception,e:
319 print "Session to '%s' not open: %s" %(mach.name,e)
320 if g_verbose:
321 traceback.print_exc()
322 return
323 if session.state != ctx['ifaces'].SessionState_Open:
324 print "Session to '%s' in wrong state: %s" %(mach.name, session.state)
325 return
326 # unfortunately IGuest is suppressed, thus WebServices knows not about it
327 # this is an example how to handle local only functionality
328 if ctx['remote'] and cmd == 'stats2':
329 print 'Trying to use local only functionality, ignored'
330 return
331 console=session.console
332 ops={'pause' : lambda: console.pause(),
333 'resume': lambda: console.resume(),
334 'powerdown': lambda: console.powerDown(),
335 'stats': lambda: guestStats(ctx, mach),
336 'guest': lambda: guestExec(ctx, mach, console, args),
337 'monitorGuest': lambda: monitorGuest(ctx, mach, console, args)
338 }
339 try:
340 ops[cmd]()
341 except Exception, e:
342 print 'failed: ',e
343 if g_verbose:
344 traceback.print_exc()
345
346 session.close()
347
348# can cache known machines, if needed
349def machById(ctx,id):
350 mach = None
351 for m in getMachines(ctx):
352 if m.name == id:
353 mach = m
354 break
355 mid = str(m.id)
356 if mid[0] == '{':
357 mid = mid[1:-1]
358 if mid == id:
359 mach = m
360 break
361 return mach
362
363def argsToMach(ctx,args):
364 if len(args) < 2:
365 print "usage: %s [vmname|uuid]" %(args[0])
366 return None
367 id = args[1]
368 m = machById(ctx, id)
369 if m == None:
370 print "Machine '%s' is unknown, use list command to find available machines" %(id)
371 return m
372
373def helpCmd(ctx, args):
374 if len(args) == 1:
375 print "Help page:"
376 for i in commands:
377 print " ",i,":", commands[i][0]
378 else:
379 c = commands.get(args[1], None)
380 if c == None:
381 print "Command '%s' not known" %(args[1])
382 else:
383 print " ",args[1],":", c[0]
384 return 0
385
386def listCmd(ctx, args):
387 for m in getMachines(ctx):
388 print "Machine '%s' [%s], state=%s" %(m.name,m.id,m.sessionState)
389 return 0
390
391def infoCmd(ctx,args):
392 import time
393 if (len(args) < 2):
394 print "usage: info [vmname|uuid]"
395 return 0
396 mach = argsToMach(ctx,args)
397 if mach == None:
398 return 0
399 os = ctx['vb'].getGuestOSType(mach.OSTypeId)
400 print " Name: ",mach.name
401 print " ID: ",mach.id
402 print " OS Type: ",os.description
403 print " CPUs: %d" %(mach.CPUCount)
404 print " RAM: %dM" %(mach.memorySize)
405 print " VRAM: %dM" %(mach.VRAMSize)
406 print " Monitors: %d" %(mach.monitorCount)
407 print " Clipboard mode: %d" %(mach.clipboardMode)
408 print " Machine status: " ,mach.sessionState
409 bios = mach.BIOSSettings
410 print " ACPI: %s" %(asState(bios.ACPIEnabled))
411 print " APIC: %s" %(asState(bios.IOAPICEnabled))
412 print " PAE: %s" %(asState(mach.PAEEnabled))
413 print " Hardware virtualization: ",asState(mach.HWVirtExEnabled)
414 print " VPID support: ",asState(mach.HWVirtExVPIDEnabled)
415 print " Hardware 3d acceleration: ",asState(mach.accelerate3DEnabled)
416 print " Nested paging: ",asState(mach.HWVirtExNestedPagingEnabled)
417 print " Last changed: ",time.asctime(time.localtime(mach.lastStateChange/1000))
418
419 return 0
420
421def startCmd(ctx, args):
422 mach = argsToMach(ctx,args)
423 if mach == None:
424 return 0
425 if len(args) > 2:
426 type = args[2]
427 else:
428 type = "gui"
429 startVm(ctx, mach, type)
430 return 0
431
432def createCmd(ctx, args):
433 if (len(args) < 3 or len(args) > 4):
434 print "usage: create name ostype <basefolder>"
435 return 0
436 name = args[1]
437 oskind = args[2]
438 if len(args) == 4:
439 base = args[3]
440 else:
441 base = ''
442 try:
443 ctx['vb'].getGuestOSType(oskind)
444 except Exception, e:
445 print 'Unknown OS type:',oskind
446 return 0
447 createVm(ctx, name, oskind, base)
448 return 0
449
450def removeCmd(ctx, args):
451 mach = argsToMach(ctx,args)
452 if mach == None:
453 return 0
454 removeVm(ctx, mach)
455 return 0
456
457def pauseCmd(ctx, args):
458 mach = argsToMach(ctx,args)
459 if mach == None:
460 return 0
461 cmdExistingVm(ctx, mach, 'pause', '')
462 return 0
463
464def powerdownCmd(ctx, args):
465 mach = argsToMach(ctx,args)
466 if mach == None:
467 return 0
468 cmdExistingVm(ctx, mach, 'powerdown', '')
469 return 0
470
471def resumeCmd(ctx, args):
472 mach = argsToMach(ctx,args)
473 if mach == None:
474 return 0
475 cmdExistingVm(ctx, mach, 'resume', '')
476 return 0
477
478def statsCmd(ctx, args):
479 mach = argsToMach(ctx,args)
480 if mach == None:
481 return 0
482 cmdExistingVm(ctx, mach, 'stats', '')
483 return 0
484
485def guestCmd(ctx, args):
486 if (len(args) < 3):
487 print "usage: guest name commands"
488 return 0
489 mach = argsToMach(ctx,args)
490 if mach == None:
491 return 0
492 cmdExistingVm(ctx, mach, 'guest', ' '.join(args[2:]))
493 return 0
494
495def setvarCmd(ctx, args):
496 if (len(args) < 4):
497 print "usage: setvar [vmname|uuid] expr value"
498 return 0
499 mach = argsToMach(ctx,args)
500 if mach == None:
501 return 0
502 session = ctx['mgr'].getSessionObject(vbox)
503 vbox.openSession(session, mach.id)
504 mach = session.machine
505 expr = 'mach.'+args[2]+' = '+args[3]
506 print "Executing",expr
507 try:
508 exec expr
509 except Exception, e:
510 print 'failed: ',e
511 if g_verbose:
512 traceback.print_exc()
513 mach.saveSettings()
514 session.close()
515 return 0
516
517def quitCmd(ctx, args):
518 return 1
519
520def aliasesCmd(ctx, args):
521 for (k,v) in aliases.items():
522 print "'%s' is an alias for '%s'" %(k,v)
523 return 0
524
525def verboseCmd(ctx, args):
526 global g_verbose
527 g_verbose = not g_verbose
528 return 0
529
530def hostCmd(ctx, args):
531 host = ctx['vb'].host
532 cnt = host.processorCount
533 print "Processor count:",cnt
534 for i in range(0,cnt):
535 print "Processor #%d speed: %dMHz" %(i,host.getProcessorSpeed(i))
536
537 if ctx['perf']:
538 for metric in ctx['perf'].query(["*"], [host]):
539 print metric['name'], metric['values_as_string']
540
541 return 0
542
543
544def monitorGuestCmd(ctx, args):
545 if (len(args) < 2):
546 print "usage: monitorGuest name (duration)"
547 return 0
548 mach = argsToMach(ctx,args)
549 if mach == None:
550 return 0
551 dur = 5
552 if len(args) > 2:
553 dur = float(args[2])
554 cmdExistingVm(ctx, mach, 'monitorGuest', dur)
555 return 0
556
557def monitorVboxCmd(ctx, args):
558 if (len(args) > 2):
559 print "usage: monitorVbox (duration)"
560 return 0
561 dur = 5
562 if len(args) > 1:
563 dur = float(args[1])
564 monitorVbox(ctx, dur)
565 return 0
566
567def getAdapterType(ctx, type):
568 if (type == ctx['global'].constants.NetworkAdapterType_Am79C970A or
569 type == ctx['global'].constants.NetworkAdapterType_Am79C973):
570 return "pcnet"
571 elif (type == ctx['global'].constants.NetworkAdapterType_I82540EM or
572 type == ctx['global'].constants.NetworkAdapterType_I82545EM or
573 type == ctx['global'].constants.NetworkAdapterType_I82543GC):
574 return "e1000"
575 elif (type == ctx['global'].constants.NetworkAdapterType_Null):
576 return None
577 else:
578 raise Exception("Unknown adapter type: "+type)
579
580
581def portForwardCmd(ctx, args):
582 if (len(args) != 5):
583 print "usage: portForward <vm> <adapter> <hostPort> <guestPort>"
584 return 0
585 mach = argsToMach(ctx,args)
586 if mach == None:
587 return 0
588 adapterNum = int(args[2])
589 hostPort = int(args[3])
590 guestPort = int(args[4])
591 proto = "TCP"
592 session = ctx['global'].openMachineSession(mach.id)
593 mach = session.machine
594
595 adapter = mach.getNetworkAdapter(adapterNum)
596 adapterType = getAdapterType(ctx, adapter.adapterType)
597
598 profile_name = proto+"_"+str(hostPort)+"_"+str(guestPort)
599 config = "VBoxInternal/Devices/" + adapterType + "/"
600 config = config + str(adapter.slot) +"/LUN#0/Config/" + profile_name
601
602 mach.setExtraData(config + "/Protocol", proto)
603 mach.setExtraData(config + "/HostPort", str(hostPort))
604 mach.setExtraData(config + "/GuestPort", str(guestPort))
605
606 mach.saveSettings()
607 session.close()
608
609 return 0
610
611def evalCmd(ctx, args):
612 expr = ' '.join(args[1:])
613 try:
614 exec expr
615 except Exception, e:
616 print 'failed: ',e
617 if g_verbose:
618 traceback.print_exc()
619 return 0
620
621aliases = {'s':'start',
622 'i':'info',
623 'l':'list',
624 'h':'help',
625 'a':'aliases',
626 'q':'quit', 'exit':'quit',
627 'v':'verbose'}
628
629commands = {'help':['Prints help information', helpCmd],
630 'start':['Start virtual machine by name or uuid', startCmd],
631 'create':['Create virtual machine', createCmd],
632 'remove':['Remove virtual machine', removeCmd],
633 'pause':['Pause virtual machine', pauseCmd],
634 'resume':['Resume virtual machine', resumeCmd],
635 'stats':['Stats for virtual machine', statsCmd],
636 'powerdown':['Power down virtual machine', powerdownCmd],
637 'list':['Shows known virtual machines', listCmd],
638 'info':['Shows info on machine', infoCmd],
639 'aliases':['Shows aliases', aliasesCmd],
640 'verbose':['Toggle verbosity', verboseCmd],
641 'setvar':['Set VMs variable: setvar Fedora BIOSSettings.ACPIEnabled True', setvarCmd],
642 'eval':['Evaluate arbitrary Python construction: eval for m in getMachines(ctx): print m.name,"has",m.memorySize,"M"', evalCmd],
643 'quit':['Exits', quitCmd],
644 'host':['Show host information', hostCmd],
645 'guest':['Execute command for guest: guest Win32 console.mouse.putMouseEvent(20, 20, 0, 0)', guestCmd],
646 'monitorGuest':['Monitor what happens with the guest for some time: monitorGuest Win32 10', monitorGuestCmd],
647 'monitorVbox':['Monitor what happens with Virtual Box for some time: monitorVbox 10', monitorVboxCmd],
648 'portForward':['Setup permanent port forwarding for a VM, takes adapter number host port and guest port: portForward Win32 0 8080 80', portForwardCmd],
649 }
650
651def runCommand(ctx, cmd):
652 if len(cmd) == 0: return 0
653 args = split_no_quotes(cmd)
654 if len(args) == 0: return 0
655 c = args[0]
656 if aliases.get(c, None) != None:
657 c = aliases[c]
658 ci = commands.get(c,None)
659 if ci == None:
660 print "Unknown command: '%s', type 'help' for list of known commands" %(c)
661 return 0
662 return ci[1](ctx, args)
663
664
665def interpret(ctx):
666 vbox = ctx['vb']
667 print "Running VirtualBox version %s" %(vbox.version)
668 ctx['perf'] = ctx['global'].getPerfCollector(ctx['vb'])
669
670 autoCompletion(commands, ctx)
671
672 # to allow to print actual host information, we collect info for
673 # last 150 secs maximum, (sample every 10 secs and keep up to 15 samples)
674 if ctx['perf']:
675 try:
676 ctx['perf'].setup(['*'], [vbox.host], 10, 15)
677 except:
678 pass
679
680 while True:
681 try:
682 cmd = raw_input("vbox> ")
683 done = runCommand(ctx, cmd)
684 if done != 0: break
685 except KeyboardInterrupt:
686 print '====== You can type quit or q to leave'
687 break
688 except EOFError:
689 break;
690 except Exception,e:
691 print e
692 if g_verbose:
693 traceback.print_exc()
694
695 try:
696 # There is no need to disable metric collection. This is just an example.
697 if ct['perf']:
698 ctx['perf'].disable(['*'], [vbox.host])
699 except:
700 pass
701
702
703from vboxapi import VirtualBoxManager
704
705def main(argv):
706 style = None
707 if len(argv) > 1:
708 if argv[1] == "-w":
709 style = "WEBSERVICE"
710
711 g_virtualBoxManager = VirtualBoxManager(style, None)
712 ctx = {'global':g_virtualBoxManager,
713 'mgr':g_virtualBoxManager.mgr,
714 'vb':g_virtualBoxManager.vbox,
715 'ifaces':g_virtualBoxManager.constants,
716 'remote':g_virtualBoxManager.remote,
717 'type':g_virtualBoxManager.type
718 }
719 interpret(ctx)
720 g_virtualBoxManager.deinit()
721 del g_virtualBoxManager
722
723if __name__ == '__main__':
724 main(sys.argv)
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette