class TortugaProxyConfig(TortugaCli): def __init__(self): super(TortugaProxyConfig, self).__init__(validArgCount=4) self._cm = ConfigManager() self._kitApi = KitApi() def parseArgs(self, usage=None): # TODO: add stuff here self.addOption('-f', '--force', action='store_true', default=False, dest='bForce', help='Override built-in sanity checks') self.addOption('-n', '--dry-run', action='store_true', dest='bDryRun', default=False, help='Do not write anything to disk') super().parseArgs(usage=usage) def runCommand(self): self.parseArgs() if self.getNArgs() < 1: self.usage() return action = self.getArgs()[0] if action == 'list': self._listProxies() elif action == 'add': if self.getNArgs() != 3: self.usage() return self._addProxy() elif action == 'delete': self._deleteProxy() else: raise InvalidArgument('Unknown directive [%s]' % (action)) def _getProxyCfg(self): \ # pylint: disable=no-self-use cfg = configparser.ConfigParser() cfg.read('/opt/tortuga/config/base/apache-component.conf') return cfg def __get_proxy_set(self, cfg): \ # pylint: disable=no-self-use return set(cfg.get('proxy', 'proxy_list').split(' ')) \ if cfg.has_option('proxy', 'proxy_list') else set() def _getProxyMap(self, cfg): proxyMap = {} if not cfg.has_section('proxy'): return proxyMap proxy_option_list = self.__get_proxy_set(cfg) for opt in proxy_option_list: if not cfg.has_option('proxy', opt): continue proxyMap[opt] = cfg.get('proxy', opt) return proxyMap def _writeProxyMap(self, cfg, proxyMap): if not cfg.has_section('proxy'): cfg.add_section('proxy') # Determine differences between what exists on disk and what has # just been removed. for deleted_option in self.__get_proxy_set(cfg) - set(proxyMap.keys()): if not cfg.has_option('proxy', deleted_option): continue cfg.remove_option('proxy', deleted_option) cfg.set('proxy', 'proxy_list', ' '.join(list(proxyMap.keys()))) for key, value in proxyMap.items(): cfg.set('proxy', key, value) proxyDict = dict(cfg.items('proxy')) if self.getArgs().bDryRun: print('[dryrun] %s' % (pprint.pformat(proxyDict))) return with open('/opt/tortuga/config/base/apache-component.conf', 'w') as fp: cfg.write(fp) def _addProxy(self): proxy_from = self.getArgs()[1] proxy_to = self.getArgs()[2] cfg = self._getProxyCfg() proxyMap = self._getProxyMap(cfg) if proxy_from in proxyMap: if proxy_to == proxyMap[proxy_from]: print('Proxy already mapped') sys.exit(1) if not self.getArgs().bForce: print('URI [%s] is already proxied to [%s]' % (proxy_from, proxyMap[proxy_from])) sys.exit(1) proxyMap[proxy_from] = proxy_to self._writeProxyMap(cfg, proxyMap) def __find_kit_by_name_and_version(self, os_name, os_version): """ Iterate over list of all installed kits looking for a name and version match only. Returns Kit object, otherwise None. """ kit = None for kit in self._kitApi.getKitList(): if kit.getName() == os_name and \ kit.getVersion() == os_version: break else: return None return kit def __get_existing_kit_by_url(self, proxy_uri): """ Given a proxy URI, determine if the path matches that of an installed kit. Returns Kit object or None. """ uri_parts = proxy_uri.split('/') if len(uri_parts) != 5: # Short-circuit any check if the URI is longer/shorter than # a properly formatted Tortuga kit URL. return None # Check if this URI is formatted like valid OS kit URL os_name = uri_parts[2] os_version = uri_parts[3] os_arch = uri_parts[4] fake_url = self._cm.getYumRootUrl( 'INSTALLER') + '/%s/%s/%s' % (os_name, os_version, os_arch) o = urllib.parse.urlparse(fake_url) if o.path != proxy_uri: # The paths don't start with the requisite Tortuga path return None version = os_version.split('-') # Check if supplied 'version' element of the path matches the # Tortuga convention. if len(version) == 1: # Possibly an OS kit # bOsKit = True if os_arch == 'x86_64' else None pass elif len(version) == 2: # Possibly a non-OS kit. Non-OS kits must have the 'arch' # set to 'noarch' # bOsKit = False if os_arch == 'noarch' else None pass else: # version element doesn't match Tortuga format return None return self.__find_kit_by_name_and_version(os_name, os_version) def _deleteProxy(self): proxy_path = self.getArgs()[1] cfg = self._getProxyCfg() proxyMap = self._getProxyMap(cfg) if proxy_path in proxyMap: existingKit = self.__get_existing_kit_by_url(proxy_path) if existingKit and not self.getArgs().bForce: print('WARNING: an installed %s [%s] matches this URL.' ' Use \'--force\' to override this sanity check.' % ('OS kit' if existingKit.getIsOs() else 'kit', existingKit)) sys.exit(1) if proxy_path not in proxyMap: print('Error: proxy path [%s] not found' % (proxy_path)) sys.exit(1) del proxyMap[proxy_path] self._writeProxyMap(cfg, proxyMap) def _listProxies(self): cfg = self._getProxyCfg() for key, value in self._getProxyMap(cfg).items(): print('%s -> %s' % (key, value))
class OSSupport(OsSupportBase): def __init__(self, osFamilyInfo): super(OSSupport, self).__init__(osFamilyInfo) self._cm = ConfigManager() self._globalParameterDbApi = GlobalParameterDbApi() try: depot_dir = \ self._globalParameterDbApi.getParameter('depot').getValue() except ParameterNotFound: # Fallback to legacy default depot_dir = '/depot' self._cm.setDepotDir(depot_dir) def getPXEReinstallSnippet(self, ksurl, node, hardwareprofile=None, softwareprofile=None): \ # pylint: disable=no-self-use # General kickstart/kernel parameters # Find the first nic marked as bootable nics = [nic for nic in node.nics if nic.boot] if not nics: raise NicNotFound( 'Node [%s] does not have a bootable NIC' % (node.name)) # Choose the first one nic = nics[0] if hardwareprofile is None: hardwareprofile = node.hardwareprofile if softwareprofile is None: softwareprofile = node.softwareprofile # Use settings from software profile, if defined, otherwise use # settings from hardware profile. bootParams = getBootParameters(hardwareprofile, softwareprofile) kernel = bootParams['kernel'] kernelParams = bootParams['kernelParams'] initrd = bootParams['initrd'] bootargs = [ ] if softwareprofile.os.family.version == '7': # RHEL 7.x bootargs.append('inst.ks=%s' % (ksurl)) else: # RHEL 5.x and 6.x bootargs.append('ks=%s' % (ksurl)) bootargs.append('ksdevice=%s' % (nic.networkdevice.name)) # Append kernel parameters, if defined. if kernelParams: bootargs.append(kernelParams) result = '''\ kernel %s append initrd=%s %s''' % (kernel, initrd, ' '.join(bootargs)) return result def __get_kickstart_network_entry(self, dbNode, hardwareprofile, nic): \ # pylint: disable=no-self-use bProvisioningNic = nic.network == hardwareprofile.nics[0].network installer_private_ip = hardwareprofile.nics[0].ip if not bProvisioningNic and not nic.network.usingDhcp and not nic.ip: # Unconfigured public static IP network return None bActivate = False # By default, all interfaces are enabled at on boot bOnBoot = True # Use the network device name, as specified in the hardware profile netargs = [ 'network --device %s' % (nic.networkdevice.name) ] if bProvisioningNic: netargs.append( '--bootproto %s' % ( 'static' if bProvisioningNic or not nic.network.usingDhcp else 'dhcp')) netargs.append('--ip=%s' % (nic.ip)) netargs.append('--netmask=%s' % (nic.network.netmask)) netargs.append('--nameserver=%s' % (installer_private_ip)) bActivate = True else: if nic.network and nic.network.usingDhcp: netargs.append('--bootproto dhcp') else: netargs.append('--bootproto static') if nic.ip: netargs.append('--ip=%s' % (nic.ip)) netargs.append('--netmask=%s' % (nic.network.netmask)) else: # Do not enable interface if it's not configured netargs.append('--onboot=no') bOnBoot = False # Store provisioning network interface device name for # later reference in the template # Ensure all interfaces are activated if bActivate: netargs.append('--activate') bDefaultRoute = True if bProvisioningNic: # This is the nic connected to the provisioning network. if len(dbNode.nics) > 1: # Disable the default route on the management network. netargs.append('--nodefroute') bDefaultRoute = False else: # Disable DNS for all interfaces other than the # provisioning network if bOnBoot: netargs.append('--nodns') if nic.network.gateway and bDefaultRoute: netargs.append('--gateway %s' % (nic.network.gateway)) return ' '.join(netargs) def __validate_node(self, node): \ # pylint: disable=no-self-use """ Raises: NodeNotFound NicNotFound """ if not node.name: raise NodeNotFound('Node must have a name') if not node.nics: raise NicNotFound('Node [%s] has no associated nics' % ( node.name)) def __kickstart_get_timezone(self): tz = self._globalParameterDbApi.getParameter( 'Timezone_zone').getValue() # Ensure timezone does not contain any spaces return tz.replace(' ', '_') def __kickstart_get_network_section(self, node, hardwareprofile): # Ensure nics are processed in order (ie. eth0, eth1, eth2...) nics = node.nics nics.sort(key=lambda nic: nic.networkdevice.name) network_entries = [] hostname_set = False # Iterate over nics, adding 'network' Kickstart entries for each for nic in nics: networkString = self.__get_kickstart_network_entry( node, hardwareprofile, nic) if not networkString: continue if not hostname_set and nic.boot and \ nic.network.type == 'provision': networkString += ' --hostname=%s' % (node.name) hostname_set = True network_entries.append(networkString) return '\n'.join(network_entries) def __kickstart_get_repos(self, dbSwProfile, installer_private_ip): repo_entries = [] for dbComponent in dbSwProfile.components: dbKit = dbComponent.kit if dbKit.isOs or dbKit.name != 'base': # Do not add repos for OS kits continue kitVer = '%s-%s' % (dbKit.version, dbKit.iteration) kitArch = 'noarch' subpath = '%s/%s/%s' % (dbKit.name, kitVer, kitArch) # Check if repository actually exists if not os.path.exists(os.path.join(self._cm.getDepotDir(), 'kits', subpath, 'repodata', 'repomd.xml')): # Repository for specified kit is empty. Nothing to do... continue url = self._cm.getYumRootUrl(installer_private_ip) + \ '/' + subpath repo_entries.append( 'repo --name %s --baseurl=%s' % (dbKit.name, url)) subpath = '3rdparty/%s/%s/%s' % (dbSwProfile.os.family.name, dbSwProfile.os.family.version, dbSwProfile.os.arch) if os.path.exists(os.path.join(self._cm.getRoot(), 'repos', subpath, 'repodata/repomd.xml')): # Third-party repository contains packages, include it in # Kickstart url = '%s/%s' % ( self._cm.getYumRootUrl(installer_private_ip), subpath) repo_entries.append( 'repo --name tortuga-third-party --baseurl=%s' % (url)) return repo_entries def __get_kickstart_template(self, swprofile): ksTemplate = os.path.join( self._cm.getKitConfigBase(), 'kickstart-%s.tmpl' % (swprofile.os.family.name.encode('ascii'))) if not os.path.exists(ksTemplate): ksTemplate = os.path.join( self._cm.getKitConfigBase(), 'kickstart-%s.tmpl' % (swprofile.name.encode('ascii'))) if not os.path.exists(ksTemplate): ksTemplate = os.path.join( self._cm.getKitConfigBase(), 'kickstart.tmpl') return ksTemplate def __kickstart_get_partition_section(self, softwareprofile): buf = """\ #!/bin/sh # Determine how many drives we have """ # Temporary workaround for RHEL 5.7 based distros # https://bugzilla.redhat.com/show_bug.cgi?format=multiple&id=709880 if softwareprofile.os.version == '5.7': buf += 'set $(PYTHONPATH=/usr/lib/booty list-harddrives)\n' else: buf += 'set $(list-harddrives)\n' buf += """ d1=$1 d2=$3 d3=$5 d4=$7 """ clearpartstr = ''' cat >/tmp/partinfo << __PARTINFO__ zerombr ''' disksToPreserve = [] # Need to get the drives to clear clearpartstr += 'clearpart ' driveNumbers = [] for dbPartition in softwareprofile.partitions: disk = dbPartition.device.split('.')[0] if disk not in driveNumbers: driveNumbers.append(disk) if not dbPartition.preserve: # This is a partition to clear if len(driveNumbers) == 1: # First drive clearpartstr += ('--all --initlabel' ' --drives="${d%s:-nodisk}' % ( disk)) else: clearpartstr += ',${d%s:-nodisk}' % (disk) else: disksToPreserve.append(disk) clearpartstr += "--none" if not driveNumbers else '"' clearpartstr += '\n' for diskNum in driveNumbers: if diskNum in disksToPreserve: continue buf += ''' dd if=/dev/zero of=$d%s bs=512 count=1 ''' % (diskNum) buf += clearpartstr bootloaderLocation = "mbr" # Now create partitions for dbPartition in softwareprofile.partitions: if dbPartition.bootLoader: # Can't control the partition in anaconda...it will be on # the drive with the boot partition bootloaderLocation = 'partition' buf += self._processPartition(dbPartition) # now do the bootloader buf += ( 'bootloader --location=%s --driveorder=${d1:-nodisk}\n' % ( bootloaderLocation)) buf += '__PARTINFO__\n' return buf def __get_template_subst_dict(self, node, hardwareprofile, softwareprofile): hardwareprofile = hardwareprofile \ if hardwareprofile else node.hardwareprofile softwareprofile = softwareprofile \ if softwareprofile else node.softwareprofile installer_public_fqdn = socket.getfqdn() installer_hostname = installer_public_fqdn.split('.')[0] installer_private_ip = hardwareprofile.nics[0].ip try: private_domain = self._globalParameterDbApi.\ getParameter('DNSZone').getValue() except ParameterNotFound: private_domain = None installer_private_fqdn = '%s%s%s' % ( installer_hostname, get_installer_hostname_suffix( hardwareprofile.nics[0], enable_interface_aliases=None), '.%s' % (private_domain) if private_domain else '') vals = node.name.split('.', 1) domain = vals[1].lower() if len(vals) == 2 else '' d = { 'fqdn': node.name, 'domain': domain, 'hostname': installer_hostname, 'installer_private_fqdn': installer_private_fqdn, 'installer_private_domain': private_domain, 'installer_private_ip': installer_private_ip, 'puppet_master_fqdn': installer_public_fqdn, 'installer_public_fqdn': installer_public_fqdn, 'ntpserver': installer_private_ip, 'os': softwareprofile.os.name, 'osfamily': softwareprofile.os.family.name, 'osfamilyvers': int(softwareprofile.os.family.version), # These are deprecated and included for backwards compatibility # only. Do not reference them in any new kickstart templates. 'primaryinstaller': installer_private_fqdn, 'puppetserver': installer_public_fqdn, 'installerip': installer_private_ip, } # Add entry for install package source d['url'] = '%s/%s/%s/%s' % ( self._cm.getYumRootUrl(installer_private_fqdn), softwareprofile.os.name, softwareprofile.os.version, softwareprofile.os.arch) d['lang'] = 'en_US.UTF-8' d['keyboard'] = 'us' d['networkcfg'] = self.__kickstart_get_network_section( node, hardwareprofile) d['rootpw'] = self._generatePassword() d['timezone'] = self.__kickstart_get_timezone() d['includes'] = '%include /tmp/partinfo' d['repos'] = '\n'.join( self.__kickstart_get_repos( softwareprofile, installer_private_fqdn)) # Retain this for backwards compatibility with legacy Kickstart # templates d['packages'] = '\n'.join([]) d['prescript'] = self.__kickstart_get_partition_section( softwareprofile) d['installer_url'] = self._cm.getInstallerUrl(installer_private_fqdn) d['cfmstring'] = self._cm.getCfmPassword() return d def getKickstartFileContents(self, node, hardwareprofile, softwareprofile): # Perform basic sanity checking before proceeding self.__validate_node(node) template_subst_dict = self.__get_template_subst_dict( node, hardwareprofile, softwareprofile) with open(self.__get_kickstart_template(softwareprofile)) as fp: tmpl = fp.read() return Template(tmpl).render(template_subst_dict) def _generatePassword(self): \ # pylint: disable=no-self-use # Generate a random password, used when creating a Kickstart file # for package-based node provisioning. strlength = 8 strchars = string.ascii_letters + string.digits rootpw = ''.join([choice(strchars) for _ in range(strlength)]) rootpw = crypt.crypt(str(rootpw), str(time.time())) return rootpw def __get_partition_mountpoint(self, dbPartition): \ # pylint: disable=no-self-use if not dbPartition.mountPoint: if dbPartition.fsType == 'swap': mountPoint = 'swap' else: # Any partition that does not have a mountpoint defined # is ignored. return None else: mountPoint = dbPartition.mountPoint return mountPoint def _processPartition(self, dbPartition): \ # pylint: disable=no-self-use mountPoint = dbPartition.mountPoint \ if dbPartition.mountPoint else \ self.__get_partition_mountpoint(dbPartition) if not mountPoint: return '' result = '' # All partitions must have a mount point and partition type result = 'part %s --fstype %s' % (mountPoint, dbPartition.fsType) # This will throw an exception if the size stored in the # partition settings is not an integer. if dbPartition.size: result += ' --size=%d' % (dbPartition.size) else: # If partition size is not set or is zero, use '--recommended' flag if mountPoint == 'swap': result += ' --recommended' disk, part = dbPartition.device.split('.') optionsList = dbPartition.options.split(',') \ if dbPartition.options else [] if dbPartition.grow is not None: result += ' --grow' if dbPartition.maxSize is not None: result += ' --maxsize %d' % (dbPartition.maxSize) if optionsList: # Add the fs options... result += ' --fsoptions="%s"' % (','.join(optionsList)) result += ' --noformat --onpart=${d%s:-nodisk}%s' % (disk, part) \ if dbPartition.preserve else \ ' --ondisk=${d%s:-nodisk}' % str(disk) result += '\n' return result