Subversion Repositories configs

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
186 - 1
"""
2
Copyright 2008-2019 VMware, Inc.  All rights reserved. -- VMware Confidential
3
 
4
VMware VMX component installer.
5
"""
6
 
7
GCONF_DEFAULTS = 'xml:readwrite:/etc/gconf/gconf.xml.defaults'
8
DEST = LIBDIR/'vmware'
9
CONFIG = DEST/'setup/vmware-config'
10
CUPSLIBDIR = LIBDIR/'cups'
11
SETTINGS = \
12
    {'libdir': DEST,
13
     'bindir': BINDIR,
14
     'initscriptdir': INITSCRIPTDIR,
15
     'gksu.rootMethod': 'sudo' if 'SUDO_USER' in ENV else 'su',
16
     'NETWORKING': 'yes',
17
     'VMBLOCK_CONFED': 'yes',
18
     'VMCI_CONFED': 'yes',
19
     'VSOCK_CONFED': 'yes',
20
     'authd.fullpath': SBINDIR/'vmware-authd',
21
    }
22
 
23
vmwareSentinel = '# Automatically generated by the VMware Installer - DO NOT REMOVE\n'
24
 
25
MIN_NESTED_TOOLS_VERSION = '8.9.0'
26
MIN_EQUIVALENT_KERNEL_VERSION = (3, 9)
27
# Present requirement is a RHEL6-like environment.
28
MIN_GLIBC_VERSION = (2, 12)
29
MIN_KERNEL_VERSION = (2, 6, 32)
30
 
31
# Player and Workstation both depend on some configuration living
32
# in /etc/vmware
33
ETCDIR = Destination('/etc/vmware')
34
 
35
class VMX(Installer):
36
   def InitializeQuestions(self, old, new, upgrade):
37
      self.AddQuestion('ClosePrograms', key='ClosePrograms', text='',
38
                       required=True, default='Yes', level='REQUIRED')
39
 
40
      # Check whether we're in a VM or on real hardware.  If we're in a VM, verify that the
41
      # proper version of Tools is already installed.
42
      self.inVM = False
43
      self.checkVMTempFile = self.GetFilePath('extra/checkvm')
44
      # Get the directory above our installer and write checkvm to it so we can run it
45
      tmpdir = path(ENV['VMWARE_INSTALLER']).dirname().dirname()
46
      self.checkvmBin = tmpdir/'checkvm'
47
      path(self.checkVMTempFile).copy(self.checkvmBin)
48
      self.checkvmBin.chmod(0o755)
49
      path(self.checkVMTempFile).remove()
50
 
51
      # Check the environment for forceInstallInVM.  This check is here in InitializeQuestions
52
      # because it is run before InitializeInstall, where this variable is also used.
53
      self.forceInstallInVM = False
54
      try:
55
         txt = ENV['VMWARE_FORCE_INSTALL_IN_VM']
56
         if txt == 'yes':
57
            self.forceInstallInVM = True
58
      except KeyError:
59
         pass # Okay if it hasn't been set
60
 
61
      ret = self.RunCommand(self.checkvmBin, ignoreErrors=True)
62
      if ret.retCode == 0:
63
         # We are running in a virtual machine, don't install VSock and VMCI
64
         log.Info('Running inside a virtual machine!')
65
         self.inVM = True
66
         if self.forceInstallInVM:
67
            log.Info('But forcing install as if we are not.')
68
            self.inVM = False
69
 
70
         if self.inVM:
71
            # Now we need to check if Tools is installed.  If they are, we need to see if
72
            # they are a recent enough version to make this work.  If they aren't, then
73
            # notify the user that they are out of luck and must upgrade to a newer version
74
            # of Tools.
75
            minVersion = MIN_NESTED_TOOLS_VERSION
76
            toolsVersion = self.getToolsVersion()
77
            if not self.forceInstallInVM: # Only check if they haven't been overridden
78
               if not toolsVersion:
79
                  # vmci & vsock is upstreamed since kernel 3.9, and kernel 3.9 doesn't
80
                  # need vmblock. So, in a distro with kernel later than 3.9, even if
81
                  # there's no entire vmware tools, driver version is also acceptable.
82
                  kernelVersion = self.getHostKernelVersion()
83
                  if self.isVersionOkay(kernelVersion, MIN_EQUIVALENT_KERNEL_VERSION):
84
                     log.Info('Kernel version is acceptable.')
85
                  else:
86
                     log.Info('Kernel version is too old. Related modules will be configured.')
87
                     msg = 'You are installing inside a virtual machine. It is' \
88
                           ' recommended that you should install at least version' \
89
                           ' %s of VMware Tools before installing this product.' \
90
                           ' This product will work without VMware Tools, but if' \
91
                           ' you choose to install VMware Tools after installing' \
92
                           ' this product, the two products may not work together.' \
93
                           % minVersion
94
                     log.Warn(msg)
95
                     self.UserMessage ('Warning: %s' % msg)
96
                     self.inVM = False
97
               elif not self.isToolsVersionOkay(toolsVersion, minVersion):
98
                  self.UserMessage('You must have at least version %s of VMware' % minVersion + \
99
                                   ' Tools installed to install this product inside a virtual machine.' + \
100
                                   ' Your version is %s.  Please update VMware Tools.' % toolsVersion)
101
                  raise Exception('Tools version %s is too low to install inside a VM.')
102
      elif ret.retCode == 1:
103
         # We are on normal hardware.
104
         log.Info('Running on a real machine!')
105
         self.inVM = False
106
      else:
107
         log.Warn('Something went wrong detecting whether we are in a VM or not.  Assuming we are not.')
108
         self.inVM = False
109
 
110
 
111
   def InitializeInstall(self, old, new, upgrade):
112
      if self._checkXenPresence():
113
         log.Warn('This system is running a Xen kernel. You cannot run VMs under the Xen kernel.')
114
 
115
      if path('/dev/kvm').exists():
116
         log.Warn('This system has KVM enabled. You cannot run VMs with KVM enabled.')
117
 
118
      self.AddTarget('File', 'bin/*', BINDIR)
119
      self.AddTarget('File', 'sbin/*', SBINDIR)
120
      self.AddTarget('File', 'lib/*', DEST)
121
      self.AddTarget('File', 'roms/*', DEST/'roms')
122
      self.AddTarget('File', 'etc/init.d/vmware', SYSCONFDIR/'init.d/vmware')
123
 
124
      # Install the proper modules.xml file depending on whether we're installing on a real
125
      # machine or inside a VM
126
      if not self.inVM or self.forceInstallInVM:
127
         self.AddTarget('File', 'extra/modules.xml', DEST/'modules/modules.xml')
128
      else:
129
         # Use the nested VM modules.xml file and mark modules that Tools provides as unconfigured
130
         # so Modconfig doesn't try to rebuild them and the init script doesn't try to manage them
131
         self.AddTarget('File', 'extra/modules.nestedVM.xml', DEST/'modules/modules.xml')
132
         SETTINGS['VMCI_CONFED'] = 'no'
133
         SETTINGS['VSOCK_CONFED'] = 'no'
134
         SETTINGS['VMBLOCK_CONFED'] = 'no'
135
 
136
      # Symlink all binaries to appLoader.
137
      for i in ('vmware-modconfig', 'vmware-modconfig-console',
138
                'vmware-gksu', 'vmware-vmblock-fuse'):
139
        self.AddTarget('Link', DEST/'bin/appLoader', DEST/'bin'/i)
140
 
141
      self.SetPermission(DEST/'bin/*', BINARY)
142
      self.SetPermission(SYSCONFDIR/'init.d/vmware', BINARY)
143
      self.SetPermission(DEST/'bin/vmware-vmx*', SETUID)
144
      self.SetPermission(DEST/'lib/libvmware-gksu.so/gksu-run-helper', BINARY)
145
      self.SetPermission(SBINDIR/'vmware-authd', SETUID)
146
 
147
      # modprobe.d script.  Only install this on systems where modprobe.d
148
      # exists.
149
      if path('/etc/modprobe.d').exists():
150
         self.AddTarget('File', 'etc/modprobe.d/modprobe-vmware-fuse.conf',
151
                        '/etc/modprobe.d/vmware-fuse.conf')
152
 
153
      # Add a link for internationalization directory
154
      self.AddTarget('Link', DEST/'icu', SYSCONFDIR/'vmware/icu')
155
 
156
   def PreTransactionUninstall(self, old, new, upgrade):
157
      self.AddQuestion('ClosePrograms', key='ClosePrograms', text='',
158
                       required=True, default='Yes', level='REQUIRED')
159
 
160
   def PreInstall(self, old, new, upgrade):
161
      # Remove all modules in case some were left behind somehow.  For
162
      # example, the installation database could have been blown away.
163
      ret, kvers, _ = self.RunCommand('uname', '-r')
164
      kvers = kvers.strip()
165
      modules = ('vmmon', 'vmnet')
166
      if not self.inVM:
167
         modules = modules + ('vmblock', 'vmci', 'vsock')
168
      base = path('/lib/modules/%s/misc' % kvers)
169
 
170
      for module in modules:
171
         for ext in ('o', 'ko'):
172
            mod = '%s.%s' % (module, ext)
173
            (base/mod).remove(ignore_errors=True)
174
 
175
   def PreUninstall(self, old, new, upgrade):
176
      script = INITSCRIPTDIR/'vmware'
177
 
178
      # Stop and deconfigure services
179
      if ENV.get('VMWARE_SKIP_SERVICES'):
180
         log.Info('Skipping stopping services')
181
      elif INITSCRIPTDIR and self._scriptRunnable(script) and self.RunCommand(script, 'stop', ignoreErrors=True).retCode != 0:
182
         log.Error('Unable to stop VMware services')
183
 
184
      # Deconfigure services
185
      inits = self.LoadInclude('initscript')
186
      inits.DeconfigureService('vmware')
187
 
188
      # This must be the last thing done since the actions above may
189
      # depend on it.
190
      for key in list(SETTINGS.keys()):
191
         self.RunCommand(CONFIG, '-d', key)
192
 
193
      # Remove prelink appLoader exclusion.
194
      self._configurePrelink(False)
195
 
196
   def PostUninstall(self, old, new, upgrade):
197
      # Modules have been removed during uninstall.  We need to be sure to
198
      # run depmod to pick up that change.  Don't fail if for some reason
199
      # depmod can't be executed though.
200
      self.RunCommand('depmod', '-a', ignoreErrors=True)
201
 
202
   def PostInstall(self, old, new, upgrade):
203
      for key, val in list(SETTINGS.items()):
204
         self.RunCommand(CONFIG, '-s', key, val)
205
 
206
      bootstrap = ETCDIR/'bootstrap'
207
      # Create the bootstrap file.
208
      bootstrap.unlink(ignore_errors=True)
209
      # Fill it.
210
      for i in ('PREFIX', 'BINDIR', 'SBINDIR', 'LIBDIR', 'DATADIR',
211
                'SYSCONFDIR', 'DOCDIR', 'MANDIR', 'INCLUDEDIR', 'INITDIR',
212
                'INITSCRIPTDIR'):
213
         bootstrap.write_text('%s="%s"\n' % (i, globals()[i]), append=True)
214
      # Register it.
215
      # XXX: Skip registration for this change.  Allow the installer to handle
216
      # deletion.  A later change will correct this.
217
      # self.RegisterFile(bootstrap, fileType='ConfigFile')
218
 
219
      # Configure Gtk+.
220
      # @todo: make it its own component
221
      libconf = DEST/'libconf'
222
      replace = libconf/'etc/gtk-3.0/gdk-pixbuf.loaders'
223
      template = '@@LIBCONF_DIR@@'
224
 
225
      self.RunCommand('sed', '-e', 's,%s,%s,g' % (template, libconf),
226
                      '-i', replace)
227
 
228
      # Add prelink appLoader exclusion.
229
      self._configurePrelink(True)
230
 
231
      # Set up virtual networking
232
      self._configureNetworking(old, new)
233
 
234
      # Set up the vmware service script
235
      inits = self.LoadInclude('initscript')
236
      inits.ConfigureService('vmware',
237
                             'This service starts and stops VMware services',
238
                             '$network $syslog', # Start
239
                             '$network $syslog', # Stop
240
                             '',
241
                             '',
242
                             19,
243
                             8)
244
 
245
      # Make sure to start services
246
      script = INITSCRIPTDIR/'vmware'
247
      if INITSCRIPTDIR and script.exists():
248
         self.RunCommand(script, 'stop', ignoreErrors=True)
249
         self.RunCommand(script, 'start')
250
 
251
      # If no INITDIR was given, notify the user that the vmware service must
252
      # be manually set up
253
      if not INITDIR:
254
         self.UserMessage('No rc*.d style init script directories were given to the installer. '
255
                          'You must manually add the necessary links to ensure that the vmware '
256
                          'service at %s is automatically started and stopped on startup and shutdown.' % str(INITSCRIPTDIR/'vmware'))
257
 
258
   def _scriptRunnable(self, script):
259
      """ Returns True if the script exists and is in a runnable state """
260
      return script.isexe() and script.isfile() and self.RunCommand(script, 'validate').retCode == 100
261
 
262
   def _AddLineToFile(self, fil, text, addToEnd=True):
263
      """
264
      Add a line/consecutive lines to a file, surrounded by the VMware Sentinel.
265
      ### This method only adds a single line to a file ###
266
      ### You cannot make more than one modification per file ###
267
 
268
      @param fil: The file name.  Either a string or path object
269
      @param text: The text to add.  This function appends a \n
270
      @param addToEnd: Add to the end of the file?  If false, to the beginning.
271
 
272
      @return: True on successful modification, False if the file did not exist.
273
      """
274
      fil = path(fil) # Make sure it's a path object
275
      if fil.exists():
276
         txt = fil.text()
277
         # Modify the text
278
         if addToEnd:
279
            txt = ''.join([txt, vmwareSentinel, text, '\n', vmwareSentinel])
280
         else:
281
            txt = ''.join([vmwareSentinel, text, '\n', vmwareSentinel, txt])
282
         # Write it back to the file
283
         fil.write_text(txt)
284
         return True
285
      else:
286
         log.Info('Attempted to modify file %s, does not exist.' % fil)
287
         return False
288
 
289
   def _RemoveLineFromFile(self, fil):
290
      """
291
      Remove a line bracketed by the VMware Sentinel
292
 
293
      @param fil: The file name.  Either a string or path object
294
 
295
      @return: True on successful modification, False if the file did not exist.
296
      """
297
      fil = path(fil) # Make sure it's a path object
298
      if fil.exists():
299
         txt = fil.text()
300
         m = re.sub(vmwareSentinel + '.*\n' + vmwareSentinel,
301
                    '', txt, re.DOTALL)
302
         fil.write_text(m)
303
         return True
304
      else:
305
         log.Info('Attempted to modify file %s, does not exist.' % fil)
306
         return False
307
 
308
   def _configurePrelink(self, enable):
309
      """
310
      Configures prelinking by adding appLoader exclusion.
311
 
312
      @param enable: True if to add exclusion, False if to remove it.
313
      """
314
      prelink = path('/etc/prelink.conf')
315
 
316
      if prelink.exists():
317
         # XXX: It would probably be good to refactor this into some
318
         # sort of helper function that can add and remove lines from
319
         # files.
320
         skipPrelink = [ '# appLoader will segfault if prelinked.',
321
                         '-b %s' % (DEST/'bin/appLoader') ]
322
 
323
         lines = prelink.lines(encoding='utf-8', retain=False)
324
 
325
         # Strip whitespace from lines so that trailing whitespace
326
         # won't impact matching lines.
327
         lines = [line.strip() for line in lines]
328
 
329
         if enable:
330
            if skipPrelink[-1] not in lines:
331
               lines += skipPrelink
332
               log.Info('Added appLoader prelink exclusion to %s.', prelink)
333
            else:
334
               log.Warn('appLoader skip prelinking already present.')
335
         else:
336
            found = False
337
 
338
            for line in skipPrelink:
339
               if line in lines:
340
                  found = True
341
                  lines.remove(line)
342
 
343
            if found:
344
               log.Info('Removed appLoader prelink exclusion from %s.', prelink)
345
 
346
         # XXX: This can technically fail.  Actually, there are a
347
         # whole host of things that can fail in this function.  On
348
         # one hand we would like to catch errors and correct them
349
         # while being fairly resilient at installation time.
350
         #
351
         # One option might be to have a @failok decoration for things
352
         # that can fail loudly for internal builds but do not cause
353
         # installations to fail in release builds.
354
         prelink.write_lines(lines, encoding='utf-8')
355
      else:
356
         log.Info('Prelink not present, skipping configuration.')
357
 
358
   def _validate_cpu_flags(self, reqFlags):
359
      """
360
      Checks to ensure that every CPU on the given machine has all the
361
      flags passed in the reqFlags list.
362
      @param: A list of the flags that must be present.
363
      @returns: True on all necessary flags present, False othewise.
364
      """
365
      flagSec = False
366
      cpuInfoFile = '/proc/cpuinfo'
367
      try:
368
         cpuInfo = open(cpuInfoFile, mode='r')
369
         for line in cpuInfo:
370
            if line.startswith('flags'):
371
               flagSec = True
372
               flagsFound = line.split(' ')
373
               for flag in reqFlags:
374
                  if flag not in flagsFound:
375
                     return False
376
         return flagSec
377
      except IOError:
378
         raise Exception('Could not open %s' % (cpuInfoFile))
379
      finally:
380
         if cpuInfo:
381
            cpuInfo.close()
382
 
383
   def PreTransactionInstall(self, old, new, upgrade):
384
      if ENV.get('VMWARE_SKIP_VERSION_CHECKS'):
385
         return  # skip all checks
386
 
387
      # CPU flags check
388
      # TODO: also check for vt / ept / (AMD equivalent)
389
      reqFlags = ['lm']
390
      cpuOk = self._validate_cpu_flags(reqFlags)
391
      kernelOk = self.isVersionOkay(self.getHostKernelVersion(),
392
                                    MIN_KERNEL_VERSION)
393
      glibcOk = self.isVersionOkay(self.getHostGlibcVersion(),
394
                                   MIN_GLIBC_VERSION)
395
      if not cpuOk:
396
         self.UserMessage('One or more of your processors does not have the '
397
                          'necessary 64bit extensions to run VMware '
398
                          'virtual machines.')
399
      if not kernelOk:
400
         self.UserMessage('The Linux kernel version on this system is '
401
                          'insufficient for running VMware virtual machines. '
402
                          'Linux kernel version %s or later is required.'
403
                          % str(MIN_KERNEL_VERSION))
404
      if not glibcOk:
405
         self.UserMessage('The glibc library version on this system is '
406
                          'insufficient for running VMware virtual machines. '
407
                          'Glibc library version %s or later is required.'
408
                          % str(MIN_GLIBC_VERSION))
409
      if not cpuOk or not glibcOk or not kernelOk:
410
         raise Exception('Host not supported by this product.')
411
 
412
   def PostTransactionInstall(self, old, new, upgrade):
413
      # Build modules with modconfig.  This can't happen during the other install
414
      # phases because VMIS has locked the database and modconfig invokes another
415
      # version of VMIS to register the compiled modules.
416
      if ENV.get('VMWARE_SKIP_MODULES'):
417
         log.Info('Skipping kernel module installation')
418
      else:
419
         # run depmod -a before modconfig in case we are upgrading from a version
420
         # that did not properly do this after uninstalling modules. modconfig
421
         # uses modules.dep to determine upstream status of modules, therefore
422
         # it might get confused if modules.dep is not up-to-date.
423
         self.RunCommand('depmod', '-a', ignoreErrors=True)
424
         ret = self.RunCommand(BINDIR/'vmware-modconfig', '--console', '--install-all')
425
         if ret.retCode == 0:
426
            log.Info('Successfully installed kernel modules')
427
         else:
428
            log.Info('Unable to install kernel modules')
429
            log.Info('stdout: %s' % ret.stdout)
430
            log.Info('stderr: %s' % ret.stderr)
431
 
432
   def _checkXenPresence(self):
433
      """
434
      Checks whether this install is within a Xen domain,
435
      that is running on a Xen kernel.  Returns True if so.
436
      """
437
      xenTest = path('/proc/xen/capabilities')
438
      return xenTest.exists() and len(xenTest.text())
439
 
440
   def _escape(self, string):
441
      """ Escapes a string for use in a shell context """
442
      # XXX: Borrowed from util/shell.py: Escape.  Break that into a component-side
443
      # include file and remove this method once that's done.
444
      return "'%s'" % string.replace("'", '"\'"')
445
 
446
   def _configureNetworking(self, old, new):
447
      if 'VMWARE_SKIP_NETWORKING' in ENV:
448
         return
449
 
450
      vnetlib = BINDIR/'vmware-networks'
451
 
452
      backup = ENV.get('VMWARE_RESTORE_NETWORKING')
453
      backup = backup and path(backup)
454
 
455
      locations = ENV.get('VMWARE_MIGRATE_NETWORKING')
456
      locations = locations and path(locations)
457
 
458
      if backup and backup.owner == 'root':
459
         log.Info('Restoring previous network settings from %s', backup)
460
         backup.copy('/etc/vmware/networking')
461
         backup.remove()
462
         old = 1
463
      elif locations and locations.owner == 'root' and not old:
464
         log.Info('Migrating old network settings from %s', locations)
465
         if self.RunCommand(vnetlib, '--migrate-network-settings', locations, ignoreErrors=True).retCode == 0:
466
            locations.remove()
467
            return True
468
         else:
469
            log.Info('Migrating network settings failed, forcing new network settings')
470
            old = 0
471
            new = 1
472
 
473
      # old might be None so make it 0 for the purposes of vnetlib.
474
      old = old or 0
475
 
476
      return self.RunCommand(vnetlib, '--postinstall', '%s,%s,%s' % ('vmware-player', old, new),
477
                             ignoreErrors=True).retCode == 0
478
 
479
   def getToolsVersion(self):
480
      toolboxCmd = path('/usr/bin/vmware-toolbox-cmd')
481
      toolbox = path('/usr/bin/vmware-toolbox')
482
      ret = None
483
      if toolboxCmd:
484
         ret = self.RunCommand(toolboxCmd, '--version', ignoreErrors=True)
485
      elif toolbox:
486
         ret = self.RunCommand(toolbox, '--version', ignoreErrors=True)
487
      else:
488
         # Cannot find Tools version...
489
         return None
490
 
491
      if ret and ret.retCode == 0:
492
         toolsVersion = ret.stdout
493
         # Parse it
494
         toolsVersion = re.findall('\d+\.\d+\.\d+', toolsVersion)
495
         if toolsVersion:
496
            toolsVersion = toolsVersion[0]
497
            log.Info('Found Tools version %s' % toolsVersion)
498
            return toolsVersion
499
         else:
500
            # Something went wrong in the process...  No Tools version found...
501
            return None
502
      else:
503
         return None
504
 
505
   def isToolsVersionOkay(self, version, minVersion):
506
      if not version:
507
         return False
508
      versions = self.LoadInclude('versions')
509
      if versions.CompareVersionString(version, minVersion) >= 0:
510
         log.Info('Tools version is compatible.')
511
         return True
512
      else:
513
         log.Info('Tools version is too old.')
514
         return False
515
 
516
   def getHostGlibcVersion(self):
517
      """Return host's glibc version as a tuple."""
518
      try:
519
         import subprocess
520
         out = subprocess.check_output(["ldd", "--version"]).decode()
521
         brand = out.split('\n', 1)[0]
522
      except subprocess.CalledProcessError:
523
         log.Info('Unable to query glibc version.')
524
         return None
525
      # brand = full ldd brand string, e.g. 'ldd (Distro GLIBC N.NN) 2.17'
526
      log.Info('Glibc brand string ' + brand)
527
      try:
528
          # versionStr = glibc version, e.g. '2.17'
529
          versionStr = brand.split(' ')[-1]
530
          glibcVersion = tuple([int(x) for x in versionStr.split(".")])
531
          log.Info('Found glibc version ' + str(glibcVersion))
532
          return glibcVersion
533
      except Exception as e:
534
          log.Info('Unable to parse glibc version: ' + str(e))
535
          return None
536
 
537
   def getHostKernelVersion(self):
538
      """Return host's kernel version as a tuple."""
539
      import platform
540
      # releaseStr = full kernel version, e.g. '3.10.0-327.el7.x86_64'
541
      releaseStr = platform.release()
542
      log.Info('Kernel release string ' + releaseStr)
543
      # versionStr = kernel numeric version, e.g. '3.10.0'
544
      try:
545
          versionStr = releaseStr.split("-")[0]
546
          kernelVersion = tuple([int(x) for x in versionStr.split(".")])
547
          log.Info('Found Linux kernel version ' + str(kernelVersion))
548
          return kernelVersion
549
      except Exception as e:
550
          log.Info('Unable to parse kernel version: ' + str(e))
551
          return None
552
 
553
   def isVersionOkay(self, version, minVersion):
554
      if not version:
555
         return True  # Do not error out on parse failure
556
      elif version < minVersion:
557
         return False
558
      else:
559
         return True