def test_mem_formatValue(self): '''Ensure that memory criteria is formatted appropriately''' fmt = AIdb.formatValue('MINmem', self.mem) self.assertEqual(fmt.split(' ')[0], self.mem) self.assertEqual(fmt.split(' ')[1], 'MB') fmt = AIdb.formatValue('MINmem', self.mem, units=False) self.assertEqual(fmt, self.mem)
def test_mem_formatValue(self): '''Ensure that memory criteria is formatted appropriately''' fmt = AIdb.formatValue('MINmem', self.mem) self.assertEqual(fmt.split(' ')[0], self.mem) self.assertEqual(fmt.split(' ')[1], 'MB') fmt = AIdb.formatValue('MINmem', self.mem, units=False) self.assertEqual(fmt, self.mem)
def test_other_formatValue(self): '''Ensure that formatValue does nothing with all other criteria''' fmt = AIdb.formatValue('arch', self.arch) self.assertEqual(fmt, self.arch) fmt = AIdb.formatValue('platform', self.platform) self.assertEqual(fmt, self.platform) fmt = AIdb.formatValue('cpu', self.cpu) self.assertEqual(fmt, self.cpu)
def test_other_formatValue(self): '''Ensure that formatValue does nothing with all other criteria''' fmt = AIdb.formatValue('arch', self.arch) self.assertEqual(fmt, self.arch) fmt = AIdb.formatValue('platform', self.platform) self.assertEqual(fmt, self.platform) fmt = AIdb.formatValue('cpu', self.cpu) self.assertEqual(fmt, self.cpu)
def remove_client_dhcp_config(client_id): ''' If a local DHCP server is running, remove any client configuration for this client from its configuration. If not, inform end-user that the client-service binding should no longer be referenced in the DHCP configuration. ''' server = dhcp.DHCPServer() if server.is_configured(): # A local DHCP server is configured. Check for a host entry and remove # it if found. mac_address = client_id[2:] mac_address = AIdb.formatValue('mac', mac_address) if server.host_is_configured(mac_address): print cw(_("Removing host entry '%s' from local DHCP " "configuration.") % mac_address) server.remove_host(mac_address) if server.is_online(): try: server.control('restart') except dhcp.DHCPServerError as err: print >> sys.stderr, cw(_("Unable to restart the DHCP " "SMF service: %s" % err)) return else: # No local DHCP configuration, inform user that it needs to be # unconfigured elsewhere. print cw(_("No local DHCP configuration found. Unless it will be " "reused, the bootfile '%s' may be removed from the DHCP " "configuration\n" % client_id))
def remove_client_dhcp_config(client_id): ''' If a local DHCP server is running, remove any client configuration for this client from its configuration. If not, inform end-user that the client-service binding should no longer be referenced in the DHCP configuration. ''' server = dhcp.DHCPServer() if server.is_configured(): # A local DHCP server is configured. Check for a host entry and remove # it if found. mac_address = client_id[2:] mac_address = AIdb.formatValue('mac', mac_address) if server.host_is_configured(mac_address): print cw( _("Removing host entry '%s' from local DHCP " "configuration.") % mac_address) server.remove_host(mac_address) if server.is_online(): try: server.control('restart') except dhcp.DHCPServerError as err: print >> sys.stderr, cw( _("Unable to restart the DHCP " "SMF service: %s" % err)) return else: # No local DHCP configuration, inform user that it needs to be # unconfigured elsewhere. print cw( _("No local DHCP configuration found. Unless it will be " "reused, the bootfile '%s' may be removed from the DHCP " "configuration\n" % client_id))
def find_clients(lservices, sname=None): """ find_clients() returns a dictionary that contains a list of dictionaries. The service name is the key for the main dictionary and the client, image path, and arch are members of the subdictionary, as follows: { 'service1': [ { 'ipath':<path1>, 'client':<client1>, 'arch': <arch>}, .... ], .... } Args lservices = config.get_all_service_props() sname - service name, if only interesetd in clients of a specific service Returns dictionary of a list of dictionaries Raises None """ sdict = dict() for servicename in lservices.keys(): if sname and sname != servicename: continue try: service = AIService(servicename) except VersionError as version_err: warn_version(version_err) continue arch = which_arch(service) image_path = [service.image.path] client_info = config.get_clients(servicename) for clientkey in client_info: # strip off the leading '01' and reinsert ':'s client = AIdb.formatValue('mac', clientkey[2:]) tdict = {'client': client, 'ipath': image_path, 'arch': arch} if servicename in sdict: # existing service name slist = sdict[servicename] slist.extend([tdict]) sdict[servicename] = slist else: # new service name key sdict[servicename] = [tdict] return sdict
def find_clients(lservices, sname=None): """ find_clients() returns a dictionary that contains a list of dictionaries. The service name is the key for the main dictionary and the client, image path, and arch are members of the subdictionary, as follows: { 'service1': [ { 'ipath':<path1>, 'client':<client1>, 'arch': <arch>}, .... ], .... } Args lservices = config.get_all_service_props() sname - service name, if only interesetd in clients of a specific service Returns dictionary of a list of dictionaries Raises None """ sdict = dict() for servicename in lservices.keys(): if sname and sname != servicename: continue try: service = AIService(servicename) except VersionError as version_err: warn_version(version_err) continue arch = which_arch(service) image_path = [service.image.path] client_info = config.get_clients(servicename) for clientkey in client_info: # strip off the leading '01' and reinsert ':'s client = AIdb.formatValue('mac', clientkey[2:]) tdict = {'client': client, 'ipath': image_path, 'arch': arch} if servicename in sdict: # existing service name slist = sdict[servicename] slist.extend([tdict]) sdict[servicename] = slist else: # new service name key sdict[servicename] = [tdict] return sdict
def get_criteria_info(mancriteria): """ Iterates over the criteria which consists of a dictionary with possibly arch, min memory, max memory, min ipv4, max ipv4, min mac, max mac, cpu, platform, min network and max network converting it into a dictionary with on arch, mem, ipv4, mac, cpu, platform, network. Any min/max attributes are stored as a range within the new dictionary. Args criteria = dictionary of the criteria Returns dictionary of combinded min/max and other criteria maximum criteria width Raises None """ tdict = { 'arch': '', 'mem': '', 'ipv4': '', 'mac': '', 'platform': '', 'network': '', 'cpu': '' } twidth = 0 for key in mancriteria.keys(): if mancriteria[key] is None or mancriteria[key] == '': continue # no criteria for instance key twidth = max(twidth, len(key.lstrip('MAX').lstrip('MIN'))) svalue = AIdb.formatValue(key, mancriteria[key]) if key.find('MAX') == 0 or key.find('MIN') == 0: tkey = key[3:] # strip off the MAX or MIN for a new keyname if tdict[tkey] != '': # dealing with range if tdict[tkey] != svalue: if max(svalue, tdict[tkey]) == svalue: tdict[tkey] = tdict[tkey] + ' - ' + svalue else: tdict[tkey] = svalue + ' - ' + tdict[tkey] else: # first value, not range yet tdict[tkey] = svalue else: # not a range manifest criteria tdict[key] = svalue return tdict, twidth
def list_manifests(service): '''Replies to the client with criteria list for a service. The output should be similar to installadm list. Args service - the name of the service being listed Returns None Raises None ''' print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<html>' print '<head>' sys.stdout.write('<title>%s %s</title>' % (_('Manifest list for'), service)) print '</head><body>' port = 0 try: smf.AISCF(FMRI="system/install/server") except KeyError: # report the internal error to error_log and requesting client sys.stderr.write(_("error:The system does not have the " "system/install/server SMF service.")) sys.stdout.write(_("error:The system does not have the " "system/install/server SMF service.")) return services = config.get_all_service_names() if not services: # report the error to the requesting client only sys.stdout.write(_('error:no services on this server.\n')) return found = False if config.is_service(service): service_ctrl = AIService(service) found = True # assume new service setup path = service_ctrl.database_path if os.path.exists(path): try: aisql = AIdb.DB(path) aisql.verifyDBStructure() except StandardError as err: # report the internal error to error_log and # requesting client sys.stderr.write(_('error:AI database access ' 'error\n%s\n') % err) sys.stdout.write(_('error:AI database access ' 'error\n%s\n') % err) return # generate the list of criteria for the criteria table header criteria_header = E.TR() for crit in AIdb.getCriteria(aisql.getQueue(), strip=False): criteria_header.append(E.TH(crit)) # generate the manifest rows for the criteria table body names = AIdb.getManNames(aisql.getQueue()) table_body = E.TR() allcrit = AIdb.getCriteria(aisql.getQueue(), strip=False) colspan = str(max(len(list(allcrit)), 1)) for manifest in names: # iterate through each manifest (and instance) for instance in range(0, AIdb.numInstances(manifest, aisql.getQueue())): table_body.append(E.TR()) # print the manifest name only once (from instance 0) if instance == 0: href = '../' + service + '/' + manifest row = str(AIdb.numInstances(manifest, aisql.getQueue())) table_body.append(E.TD( E.A(manifest, href=href, rowspan=row))) else: table_body.append(E.TD()) crit_pairs = AIdb.getManifestCriteria(manifest, instance, aisql.getQueue(), onlyUsed=True, humanOutput=True) # crit_pairs is an SQLite3 row object which doesn't # support iteritems(), etc. for crit in crit_pairs.keys(): formatted_val = AIdb.formatValue(crit, crit_pairs[crit]) # if we do not get back a valid value ensure a # hyphen is printed (prevents "" from printing) if formatted_val and crit_pairs[crit]: table_body.append(E.TD(formatted_val, align="center")) else: table_body.append(E.TD(lxml.etree.Entity("nbsp"), align="center")) # print the default manifest at the end of the table, # which has the same colspan as the Criteria List label else: href = '../' + service + '/default.xml' table_body.append(E.TR(E.TD(E.A("Default", href=href)), E.TD(lxml.etree.Entity("nbsp"), colspan=colspan, align="center"))) web_page = E.HTML( E.HEAD( E.TITLE(_("OmniOS Automated " "Installation Webserver")) ), E.BODY( E.H1(_("Welcome to the OmniOS " "Automated Installation webserver!")), E.P(_("Service '%s' has the following " "manifests available, served to clients " "matching required criteria.") % service), E.TABLE( E.TR( E.TH(_("Manifest"), rowspan="2"), E.TH(_("Criteria List"), colspan=colspan)), criteria_header, table_body, border="1", align="center"), ) ) print lxml.etree.tostring(web_page, pretty_print=True) # service is not found, provide available services on host if not found: sys.stdout.write(_('Service <i>%s</i> not found. ') % service) sys.stdout.write(_('Available services are:<p><ol><i>')) host = socket.gethostname() for service_name in config.get_all_service_names(): # assume new service setup port = config.get_service_port(service_name) sys.stdout.write('<a href="http://%s:%d/cgi-bin/' 'cgi_get_manifest.py?version=%s&service=%s">%s</a><br>\n' % (host, port, VERSION, service_name, service_name)) sys.stdout.write('</i></ol>%s' % _('Please select a service ' 'from the above list.')) print '</body></html>'
def get_criteria_info(crit_dict): """ Iterates over the criteria which consists of a dictionary with possibly arch, min memory, max memory, min ipv4, max ipv4, min mac, max mac, cpu, platform, min network, max network and zonename converting it into a dictionary with arch, mem, ipv4, mac, cpu, platform, network and zonename. Any min/max attributes are stored as a range within the new dictionary. Args crit_dict = dictionary of the criteria Returns dictionary of combined min/max and other criteria, formatted with possible endings such as MB maximum criteria width Raises None """ if crit_dict is None: return dict(), 0 # tdict values are formatted strings, with possible endings # such as MB. tdict = dict() crit_width = 0 for key in crit_dict.keys(): if key == 'service': continue is_range_crit = key.startswith('MIN') or key.startswith('MAX') # strip off the MAX or MIN for a new keyname if is_range_crit: keyname = key[3:] # strip off the MAX or MIN for a new keyname else: keyname = key tdict.setdefault(keyname, '') db_value = crit_dict[key] if not db_value and not is_range_crit: # For non-range (value) criteria, None means it isn't set. # For range criteria, None means unbounded if the other # criteria in the MIN/MAX pair is set. continue # value criteria not set crit_width = max(crit_width, len(keyname)) fmt_value = AIdb.formatValue(key, db_value) if is_range_crit: if not db_value: fmt_value = "unbounded" if tdict[keyname] != '': if tdict[keyname] != fmt_value: # dealing with range if key.startswith('MAX'): tdict[keyname] = tdict[keyname] + ' - ' + fmt_value else: tdict[keyname] = fmt_value + ' - ' + tdict[keyname] elif tdict[keyname] == "unbounded": # MIN and MAX both unbounded, which means neither is # set in db. Clear tdict value. tdict[keyname] = '' # no values for range, reset tdict else: # first value, not range yet tdict[keyname] = fmt_value # if the partner MIN/MAX criterion is not set in the db, # handle now because otherwise it won't be processed. if key.startswith('MIN'): if 'MAX' + keyname not in crit_dict.keys(): if fmt_value == "unbounded": tdict[keyname] = '' else: tdict[keyname] = tdict[keyname] + ' - unbounded' else: if 'MIN' + keyname not in crit_dict.keys(): if fmt_value == "unbounded": tdict[keyname] = '' else: tdict[keyname] = 'unbounded - ' + tdict[keyname] else: tdict[keyname] = fmt_value return tdict, crit_width
def setup_x86_client(service, mac_address, bootargs=''): ''' Set up an x86 client Creates a relative symlink from the <svcname>'s bootfile to /etc/netboot/<client_id> Creates /etc/netboot/menu.lst.<client_id> boot configuration file Adds client info to AI_SERVICE_DIR_PATH/<svcname>/.config file Arguments: image_path - directory path to AI image mac_address - client MAC address (as formed by MACAddress class, i.e., 'ABABABABABAB') bootargs = bootargs of client (x86) Returns: Nothing ''' # create a client-identifier (01 + MAC ADDRESS) client_id = "01" + mac_address menulst = os.path.join(service.config_dir, grub.MENULST) client_menulst = _menulst_path(client_id) # copy service's menu.lst file to menu.lst.<client_id> try: shutil.copy(menulst, client_menulst) except IOError as err: print >> sys.stderr, cw( _("Unable to copy grub menu.lst file: %s") % err.strerror) return # create a symlink from the boot directory to the sevice's bootfile. # note this must be relative from the boot directory. bootfile, pxegrub_path = _pxegrub_path(client_id) os.symlink("./" + service.dhcp_bootfile, pxegrub_path) clientinfo = {config.FILES: [client_menulst, pxegrub_path]} # if the client specifies bootargs, use them. Otherwise, inherit # the bootargs specified in the service (do nothing) if bootargs: grub.update_bootargs(client_menulst, service.bootargs, bootargs) clientinfo[config.BOOTARGS] = bootargs config.add_client_info(service.name, client_id, clientinfo) # Configure DHCP for this client if the configuration is local, otherwise # suggest the configuration addition. Note we only need to do this for # x86-based clients, not SPARC. server = dhcp.DHCPServer() if server.is_configured(): # We'll need the actual hardware ethernet address for the DHCP entry, # rather than the non-delimited string that 'mac_address' is. full_mac = AIdb.formatValue('mac', mac_address) try: print cw( _("Adding host entry for %s to local DHCP configuration.") % full_mac) server.add_host(full_mac, bootfile) except dhcp.DHCPServerError as err: print cw( _("Unable to add host (%s) to DHCP configuration: %s") % (full_mac, err)) return if server.is_online(): try: server.control('restart') except dhcp.DHCPServerError as err: print >> sys.stderr, cw( _("\nUnable to restart the DHCP SMF " "service: %s\n" % err)) return else: print cw( _("\nLocal DHCP configuration complete, but the DHCP " "server SMF service is offline. To enable the " "changes made, enable: %s.\nPlease see svcadm(1M) " "for further information.\n") % dhcp.DHCP_SERVER_IPV4_SVC) else: # No local DHCP, tell the user all about their boot configuration valid_nets = list(com.get_valid_networks()) if valid_nets: server_ip = valid_nets[0] print _(_PXE_CLIENT_DHCP_CONFIG % (server_ip, bootfile)) if len(valid_nets) > 1: print cw( _("\nNote: determined more than one IP address " "configured for use with AI. Please ensure the above " "'Boot server IP' is correct.\n"))
def index(self): """ The server's main page """ # generate the list of criteria for the criteria table header criteriaHeader = E.TR() for crit in AIdb.getCriteria(self.AISQL.getQueue(), strip=False): criteriaHeader.append(E.TH(crit)) # generate the manifest rows for the criteria table body names = AIdb.getManNames(self.AISQL.getQueue()) tableBody = E.TR() for manifest in names: # iterate through each manifest (and instance) for instance in range(0, AIdb.numInstances(manifest, self.AISQL.getQueue())): tableBody.append(E.TR()) # print the manifest name only once (key off instance 0) if instance == 0: tableBody.append( E.TD(E.A(manifest, href="/manifests/" + manifest, rowspan=str(AIdb.numInstances(manifest, self.AISQL.getQueue()))) ) ) else: tableBody.append(E.TD()) critPairs = AIdb.getManifestCriteria(manifest, instance, self.AISQL.getQueue(), onlyUsed=True, humanOutput=True) # critPairs is an SQLite3 row object which doesn't support # iteritems(), etc. for crit in critPairs.keys(): formatted_val = AIdb.formatValue(crit, critPairs[crit]) # if we do not get back a valid value ensure a hyphen is # printed (this prevents "" from printing) if formatted_val and critPairs[crit]: tableBody.append(E.TD(formatted_val, align="center")) else: tableBody.append(E.TD(lxml.etree.Entity("nbsp"), align="center")) # print the default manifest at the end of the table else: tableBody.append( E.TR( E.TD( E.A("Default", href="/manifests/default.xml")), E.TD(lxml.etree.Entity("nbsp"), colspan=str(max(len(list( AIdb.getCriteria(self.AISQL.getQueue(), strip=False))), 1)), align="center") ) ) web_page = \ E.HTML( E.HEAD( E.TITLE(_("%s A/I Webserver") % _DISTRIBUTION) ), E.BODY( E.H1(_("Welcome to the %s A/I " "webserver!") % _DISTRIBUTION), E.P(_("This server has the following " "manifests available, served to clients " "matching required criteria.")), E.TABLE( E.TR( E.TH(_("Manifest"), rowspan="2"), E.TH(_("Criteria List"), colspan=str(max(len(list( AIdb.getCriteria(self.AISQL.\ getQueue(), strip=False))), 1))) ), criteriaHeader, tableBody, border="1", align="center" ), ) ) return lxml.etree.tostring(web_page, pretty_print=True)
def test_network_formatValue(self): '''Ensure that ipv4 network criteria is formatted appropriately''' fmt = AIdb.formatValue('network', self.network) self.assertTrue(self.is_ipv4(fmt))
def test_mac_formatValue(self): '''Ensure that mac criteria is formatted appropriately''' fmt = AIdb.formatValue('MINmac', self.mac) for k, bits in enumerate(fmt.split(':')): self.assertEqual(bits, self.mac[k * 2:(k * 2) + 2])
def send_manifest(form_data, port=0, servicename=None, protocolversion=COMPATIBILITY_VERSION, no_default=False): '''Replies to the client with matching service for a service. Args form_data - the postData passed in from the client request port - the port of the old client servicename - the name of the service being used protocolversion - the version of the AI service RE: handshake no_default - boolean flag to signify whether or not we should hand back the default manifest and profiles if one cannot be matched based on the client criteria. Returns None Raises None ''' # figure out the appropriate path for the AI database, # and get service name if necessary. # currently service information is stored in a port directory. # When the cherrypy webserver new service directories should be # separated via service-name only. Old services will still use # port numbers as the separation mechanism. path = None found_servicename = None service = None port = str(port) if servicename: service = AIService(servicename) path = service.database_path else: for name in config.get_all_service_names(): if config.get_service_port(name) == port: found_servicename = name service = AIService(name) path = service.database_path break # Check to insure that a valid path was found if not path or not os.path.exists(path): print 'Content-Type: text/html' # HTML is following print # blank line, end of headers if servicename: print '<pre><b>Error</b>:unable to find<i>', servicename + '</i>.' else: print '<pre><b>Error</b>:unable to find<i>', port + '</i>.' print 'Available services are:<p><ol><i>' hostname = socket.gethostname() for name in config.get_all_service_names(): port = config.get_service_port(name) sys.stdout.write('<a href="http://%s:%d/cgi-bin/' 'cgi_get_manifest.py?version=%s&service=%s">%s</a><br>\n' % (hostname, port, VERSION, name, name)) print '</i></ol>Please select a service from the above list.' return if found_servicename: servicename = found_servicename # load to the AI database aisql = AIdb.DB(path) aisql.verifyDBStructure() # convert the form data into a criteria dictionary criteria = dict() orig_data = form_data while form_data: try: [key_value, form_data] = form_data.split(';', 1) except (ValueError, NameError, TypeError, KeyError): key_value = form_data form_data = '' try: [key, value] = key_value.split('=') criteria[key] = value except (ValueError, NameError, TypeError, KeyError): criteria = dict() # Generate templating dictionary from criteria template_dict = dict() for crit in criteria: template_dict["AI_" + crit.upper()] = \ AIdb.formatValue(crit, criteria[crit], units=False) # find the appropriate manifest try: manifest = AIdb.findManifest(criteria, aisql) except StandardError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre><b>Error</b>:findManifest criteria<br>' print err, '<br>' print '<ol>servicename =', servicename print 'port =', port print 'path =', path print 'form_data =', orig_data print 'criteria =', criteria print 'servicename found by port =', found_servicename, '</ol>' print '</pre>' return # check if findManifest() returned a number equal to 0 # (means we got no manifests back -- thus we serve the default if desired) if manifest is None and not no_default: manifest = service.get_default_manifest() # if we have a manifest to return, prepare its return if manifest is not None: try: # construct the fully qualified filename filename = os.path.abspath(os.path.join(service.manifest_dir, manifest)) # open and read the manifest with open(filename, 'rb') as mfp: manifest_str = mfp.read() # maintain compability with older AI client if servicename is None or \ float(protocolversion) < float(PROFILES_VERSION): content_type = mimetypes.types_map.get('.xml', 'text/plain') print 'Content-Length:', len(manifest_str) # Length of the file print 'Content-Type:', content_type # XML is following print # blank line, end of headers print manifest_str logging.info('Manifest sent from %s.' % filename) return except OSError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre>' # report the internal error to error_log and requesting client sys.stderr.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) sys.stdout.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) print '</pre>' return # get AI service image path service = AIService(servicename) image_dir = service.image.path # construct object to contain MIME multipart message outermime = MIMEMultipart() client_msg = list() # accumulate message output for AI client # If we have a manifest, attach it to the return message if manifest is not None: # add manifest as attachment msg = MIMEText(manifest_str, 'xml') # indicate manifest using special name msg.add_header('Content-Disposition', 'attachment', filename=sc.AI_MANIFEST_ATTACHMENT_NAME) outermime.attach(msg) # add manifest as an attachment # search for any profiles matching client criteria # formulate database query to profiles table q_str = "SELECT DISTINCT name, file FROM " + \ AIdb.PROFILES_TABLE + " WHERE " nvpairs = list() # accumulate criteria values from post-data # for all AI client criteria for crit in AIdb.getCriteria(aisql.getQueue(), table=AIdb.PROFILES_TABLE, onlyUsed=False): if crit not in criteria: msgtxt = _("Warning: client criteria \"%s\" not provided in " "request. Setting value to NULL for profile lookup.") \ % crit client_msg += [msgtxt] logging.warn(msgtxt) # fetch only global profiles destined for all clients if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): nvpairs += ["MIN" + crit + " IS NULL"] nvpairs += ["MAX" + crit + " IS NULL"] else: nvpairs += [crit + " IS NULL"] continue # prepare criteria value to add to query envval = AIdb.sanitizeSQL(criteria[crit]) if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. if no_default: if crit == "mac": nvpairs += ["(HEX(MIN" + crit + ")<=HEX(X'" + envval + \ "'))"] nvpairs += ["(HEX(MAX" + crit + ")>=HEX(X'" + envval + \ "'))"] else: nvpairs += ["(MIN" + crit + "<='" + envval + "')"] nvpairs += ["(MAX" + crit + ">='" + envval + "')"] else: if crit == "mac": nvpairs += ["(MIN" + crit + " IS NULL OR " "HEX(MIN" + crit + ")<=HEX(X'" + envval + "'))"] nvpairs += ["(MAX" + crit + " IS NULL OR HEX(MAX" + crit + ")>=HEX(X'" + envval + "'))"] else: nvpairs += ["(MIN" + crit + " IS NULL OR MIN" + crit + "<='" + envval + "')"] nvpairs += ["(MAX" + crit + " IS NULL OR MAX" + crit + ">='" + envval + "')"] else: # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. # # Also, since this is a non-range criteria, the value stored # in the DB may be a whitespace separated list of single # values. We use a special user-defined function in the # determine if the given criteria is in that textual list. if no_default: nvpairs += ["(is_in_list('" + crit + "', '" + envval + \ "', " + crit + ", 'None') == 1)"] else: nvpairs += ["(" + crit + " IS NULL OR is_in_list('" + crit + \ "', '" + envval + "', " + crit + ", 'None') == 1)"] if len(nvpairs) > 0: q_str += " AND ".join(nvpairs) # issue database query logging.info("Profile query: " + q_str) query = AIdb.DBrequest(q_str) aisql.getQueue().put(query) query.waitAns() if query.getResponse() is None or len(query.getResponse()) == 0: msgtxt = _("No profiles found.") client_msg += [msgtxt] logging.info(msgtxt) else: for row in query.getResponse(): profpath = row['file'] profname = row['name'] if profname is None: # should not happen profname = 'unnamed' try: if profpath is None: msgtxt = "Database record error - profile path is " \ "empty." client_msg += [msgtxt] logging.error(msgtxt) continue msgtxt = _('Processing profile %s') % profname client_msg += [msgtxt] logging.info(msgtxt) with open(profpath, 'r') as pfp: raw_profile = pfp.read() # do any template variable replacement {{AI_xxx}} tmpl_profile = sc.perform_templating(raw_profile, template_dict) # precautionary validation of profile, logging only sc.validate_profile_string(tmpl_profile, image_dir, dtd_validation=True, warn_if_dtd_missing=True) except IOError as err: msgtxt = _("Error: I/O error: ") + str(err) client_msg += [msgtxt] logging.error(msgtxt) continue except OSError: msgtxt = _("Error: OS error on profile ") + profpath client_msg += [msgtxt] logging.error(msgtxt) continue except KeyError: msgtxt = _('Error: could not find criteria to substitute ' 'in template: ') + profpath client_msg += [msgtxt] logging.error(msgtxt) logging.error('Profile with template substitution error:' + raw_profile) continue except lxml.etree.XMLSyntaxError as err: # log validation error and proceed msgtxt = _( 'Warning: syntax error found in profile: ') \ + profpath client_msg += [msgtxt] logging.error(msgtxt) for error in err.error_log: msgtxt = _('Error: ') + error.message client_msg += [msgtxt] logging.error(msgtxt) logging.info([_('Profile failing validation: ') + lxml.etree.tostring(root)]) # build MIME message and attach to outer MIME message msg = MIMEText(tmpl_profile, 'xml') # indicate in header that this is an attachment msg.add_header('Content-Disposition', 'attachment', filename=profname) # attach this profile to the manifest and any other profiles outermime.attach(msg) msgtxt = _('Parsed and loaded profile: ') + profname client_msg += [msgtxt] logging.info(msgtxt) # any profiles and AI manifest have been attached to MIME message # specially format list of messages for display on AI client console if client_msg: outtxt = '' for msgtxt in client_msg: msgtxt = _('SC profile locator:') + msgtxt outtxt += str(msgtxt) + '\n' # add AI client console messages as single plain text attachment msg = MIMEText(outtxt, 'plain') # create MIME message outermime.attach(msg) # attach MIME message to response print outermime.as_string() # send MIME-formatted message
def test_ipv4_formatValue(self): '''Ensure that ipv4 criteria is formatted appropriately''' fmt = AIdb.formatValue('MINipv4', self.ipv4) self.assertTrue(self.is_ipv4(fmt))
def send_manifest(form_data, port=0, servicename=None, protocolversion=COMPATIBILITY_VERSION, no_default=False): '''Replies to the client with matching service for a service. Args form_data - the postData passed in from the client request port - the port of the old client servicename - the name of the service being used protocolversion - the version of the AI service RE: handshake no_default - boolean flag to signify whether or not we should hand back the default manifest and profiles if one cannot be matched based on the client criteria. Returns None Raises None ''' # figure out the appropriate path for the AI database, # and get service name if necessary. # currently service information is stored in a port directory. # When the cherrypy webserver new service directories should be # separated via service-name only. Old services will still use # port numbers as the separation mechanism. path = None found_servicename = None service = None port = str(port) if servicename: service = AIService(servicename) path = service.database_path else: for name in config.get_all_service_names(): if config.get_service_port(name) == port: found_servicename = name service = AIService(name) path = service.database_path break # Check to insure that a valid path was found if not path or not os.path.exists(path): print 'Content-Type: text/html' # HTML is following print # blank line, end of headers if servicename: print '<pre><b>Error</b>:unable to find<i>', servicename + '</i>.' else: print '<pre><b>Error</b>:unable to find<i>', port + '</i>.' print 'Available services are:<p><ol><i>' hostname = socket.gethostname() for name in config.get_all_service_names(): port = config.get_service_port(name) sys.stdout.write( '<a href="http://%s:%d/cgi-bin/' 'cgi_get_manifest.py?version=%s&service=%s">%s</a><br>\n' % (hostname, port, VERSION, name, name)) print '</i></ol>Please select a service from the above list.' return if found_servicename: servicename = found_servicename # load to the AI database aisql = AIdb.DB(path) aisql.verifyDBStructure() # convert the form data into a criteria dictionary criteria = dict() orig_data = form_data while form_data: try: [key_value, form_data] = form_data.split(';', 1) except (ValueError, NameError, TypeError, KeyError): key_value = form_data form_data = '' try: [key, value] = key_value.split('=') criteria[key] = value except (ValueError, NameError, TypeError, KeyError): criteria = dict() # Generate templating dictionary from criteria template_dict = dict() for crit in criteria: template_dict["AI_" + crit.upper()] = \ AIdb.formatValue(crit, criteria[crit], units=False) # find the appropriate manifest try: manifest = AIdb.findManifest(criteria, aisql) except StandardError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre><b>Error</b>:findManifest criteria<br>' print err, '<br>' print '<ol>servicename =', servicename print 'port =', port print 'path =', path print 'form_data =', orig_data print 'criteria =', criteria print 'servicename found by port =', found_servicename, '</ol>' print '</pre>' return # check if findManifest() returned a number equal to 0 # (means we got no manifests back -- thus we serve the default if desired) if manifest is None and not no_default: manifest = service.get_default_manifest() # if we have a manifest to return, prepare its return if manifest is not None: try: # construct the fully qualified filename filename = os.path.abspath( os.path.join(service.manifest_dir, manifest)) # open and read the manifest with open(filename, 'rb') as mfp: manifest_str = mfp.read() # maintain compability with older AI client if servicename is None or \ float(protocolversion) < float(PROFILES_VERSION): content_type = mimetypes.types_map.get('.xml', 'text/plain') print 'Content-Length:', len( manifest_str) # Length of the file print 'Content-Type:', content_type # XML is following print # blank line, end of headers print manifest_str logging.info('Manifest sent from %s.' % filename) return except OSError as err: print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<pre>' # report the internal error to error_log and requesting client sys.stderr.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) sys.stdout.write(_('error:manifest (%s) %s\n') % \ (str(manifest), err)) print '</pre>' return # get AI service image path service = AIService(servicename) image_dir = service.image.path # construct object to contain MIME multipart message outermime = MIMEMultipart() client_msg = list() # accumulate message output for AI client # If we have a manifest, attach it to the return message if manifest is not None: # add manifest as attachment msg = MIMEText(manifest_str, 'xml') # indicate manifest using special name msg.add_header('Content-Disposition', 'attachment', filename=sc.AI_MANIFEST_ATTACHMENT_NAME) outermime.attach(msg) # add manifest as an attachment # search for any profiles matching client criteria # formulate database query to profiles table q_str = "SELECT DISTINCT name, file FROM " + \ AIdb.PROFILES_TABLE + " WHERE " nvpairs = list() # accumulate criteria values from post-data # for all AI client criteria for crit in AIdb.getCriteria(aisql.getQueue(), table=AIdb.PROFILES_TABLE, onlyUsed=False): if crit not in criteria: msgtxt = _("Warning: client criteria \"%s\" not provided in " "request. Setting value to NULL for profile lookup.") \ % crit client_msg += [msgtxt] logging.warn(msgtxt) # fetch only global profiles destined for all clients if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): nvpairs += ["MIN" + crit + " IS NULL"] nvpairs += ["MAX" + crit + " IS NULL"] else: nvpairs += [crit + " IS NULL"] continue # prepare criteria value to add to query envval = AIdb.sanitizeSQL(criteria[crit]) if AIdb.isRangeCriteria(aisql.getQueue(), crit, AIdb.PROFILES_TABLE): # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. if no_default: if crit == "mac": nvpairs += ["(HEX(MIN" + crit + ")<=HEX(X'" + envval + \ "'))"] nvpairs += ["(HEX(MAX" + crit + ")>=HEX(X'" + envval + \ "'))"] else: nvpairs += ["(MIN" + crit + "<='" + envval + "')"] nvpairs += ["(MAX" + crit + ">='" + envval + "')"] else: if crit == "mac": nvpairs += [ "(MIN" + crit + " IS NULL OR " "HEX(MIN" + crit + ")<=HEX(X'" + envval + "'))" ] nvpairs += [ "(MAX" + crit + " IS NULL OR HEX(MAX" + crit + ")>=HEX(X'" + envval + "'))" ] else: nvpairs += [ "(MIN" + crit + " IS NULL OR MIN" + crit + "<='" + envval + "')" ] nvpairs += [ "(MAX" + crit + " IS NULL OR MAX" + crit + ">='" + envval + "')" ] else: # If no default profiles are requested, then we mustn't allow # this criteria to be NULL. It must match the client's given # value for this criteria. # # Also, since this is a non-range criteria, the value stored # in the DB may be a whitespace separated list of single # values. We use a special user-defined function in the # determine if the given criteria is in that textual list. if no_default: nvpairs += ["(is_in_list('" + crit + "', '" + envval + \ "', " + crit + ", 'None') == 1)"] else: nvpairs += ["(" + crit + " IS NULL OR is_in_list('" + crit + \ "', '" + envval + "', " + crit + ", 'None') == 1)"] if len(nvpairs) > 0: q_str += " AND ".join(nvpairs) # issue database query logging.info("Profile query: " + q_str) query = AIdb.DBrequest(q_str) aisql.getQueue().put(query) query.waitAns() if query.getResponse() is None or len(query.getResponse()) == 0: msgtxt = _("No profiles found.") client_msg += [msgtxt] logging.info(msgtxt) else: for row in query.getResponse(): profpath = row['file'] profname = row['name'] if profname is None: # should not happen profname = 'unnamed' try: if profpath is None: msgtxt = "Database record error - profile path is " \ "empty." client_msg += [msgtxt] logging.error(msgtxt) continue msgtxt = _('Processing profile %s') % profname client_msg += [msgtxt] logging.info(msgtxt) with open(profpath, 'r') as pfp: raw_profile = pfp.read() # do any template variable replacement {{AI_xxx}} tmpl_profile = sc.perform_templating( raw_profile, template_dict) # precautionary validation of profile, logging only sc.validate_profile_string(tmpl_profile, image_dir, dtd_validation=True, warn_if_dtd_missing=True) except IOError as err: msgtxt = _("Error: I/O error: ") + str(err) client_msg += [msgtxt] logging.error(msgtxt) continue except OSError: msgtxt = _("Error: OS error on profile ") + profpath client_msg += [msgtxt] logging.error(msgtxt) continue except KeyError: msgtxt = _('Error: could not find criteria to substitute ' 'in template: ') + profpath client_msg += [msgtxt] logging.error(msgtxt) logging.error('Profile with template substitution error:' + raw_profile) continue except lxml.etree.XMLSyntaxError as err: # log validation error and proceed msgtxt = _( 'Warning: syntax error found in profile: ') \ + profpath client_msg += [msgtxt] logging.error(msgtxt) for error in err.error_log: msgtxt = _('Error: ') + error.message client_msg += [msgtxt] logging.error(msgtxt) logging.info([ _('Profile failing validation: ') + lxml.etree.tostring(root) ]) # build MIME message and attach to outer MIME message msg = MIMEText(tmpl_profile, 'xml') # indicate in header that this is an attachment msg.add_header('Content-Disposition', 'attachment', filename=profname) # attach this profile to the manifest and any other profiles outermime.attach(msg) msgtxt = _('Parsed and loaded profile: ') + profname client_msg += [msgtxt] logging.info(msgtxt) # any profiles and AI manifest have been attached to MIME message # specially format list of messages for display on AI client console if client_msg: outtxt = '' for msgtxt in client_msg: msgtxt = _('SC profile locator:') + msgtxt outtxt += str(msgtxt) + '\n' # add AI client console messages as single plain text attachment msg = MIMEText(outtxt, 'plain') # create MIME message outermime.attach(msg) # attach MIME message to response print outermime.as_string() # send MIME-formatted message
def setup_x86_client(service, mac_address, bootargs='', suppress_dhcp_msgs=False): ''' Set up an x86 client Creates relative symlink(s) in /etc/netboot:: <client_id>.<archtype> -> ./<svcname>/<bootfile_path> e.g., 01223344223344.bios -> ./mysvc/boot/grub/pxegrub Creates /etc/netboot/<cfg_file>.<client_id> boot configuration file Adds client info to AI_SERVICE_DIR_PATH/<svcname>/.config file Arguments: service - the AIService to associate with client mac_address - client MAC address (as formed by MACAddress class, i.e., 'ABABABABABAB') bootargs = bootargs of client (x86) suppress_dhcp_msgs - if True, suppresses output of DHCP configuration messages Returns: Nothing ''' # create a client-identifier (01 + MAC ADDRESS) client_id = "01" + mac_address clientinfo = dict() svcgrub = grubcfg(service.name, path=service.image.path, config_dir=service.config_dir) # Call setup_client - it will return netconfig_files, config_files, # and tuples, e.g.: # netconfig_files: ['/etc/netboot/menu.lst.01234234234234'] # config_files: ['/etc/netboot/menu.conf.01234234234234'] # boot_tuples: [('00:00', 'bios', 'mysvc/boot/grub/pxegrub2'), # ('00:07', 'uefi', 'mysvc/boot/grub/grub2netx64.efi')] # If the client specifies bootargs, use them. Otherwise, inherit # the bootargs specified in the service. netconfig_files, config_files, boot_tuples = \ svcgrub.setup_client(mac_address, bootargs=bootargs, service_bootargs=service.bootargs) # update the bootargs in the service .config file if bootargs: clientinfo[config.BOOTARGS] = bootargs # keep track of client files to delete when client is removed client_files = list(netconfig_files) client_files.extend(config_files) # create symlink(s) from the boot directory to the sevice's bootfile(s). # note these must be relative from the boot directory. for arch, archtype, relpath in boot_tuples: # name of symlink is /etc/netboot/<clientid>.<archtype> bootfile_symlink = _bootfile_path(client_id, archtype) dotpath = './' + relpath logging.debug('creating symlink %s->%s', bootfile_symlink, dotpath) os.symlink(dotpath, bootfile_symlink) client_files.append(bootfile_symlink) # if this is archtype bios, create <clientid> symlink to # <clientid>.bios for backward compatibility with existing dhcp # servers. if archtype == 'bios': clientid_path = os.path.join(com.BOOT_DIR, client_id) dot_client_arch = './' + client_id + '.' + archtype logging.debug('creating symlink %s->%s', clientid_path, dot_client_arch) os.symlink(dot_client_arch, clientid_path) logging.debug('adding client_files to .config: %s', client_files) clientinfo[config.FILES] = client_files config.add_client_info(service.name, client_id, clientinfo) # Configure DHCP for this client if the configuration is local, otherwise # suggest the configuration addition. Note we only need to do this for # x86-based clients, not SPARC. server = dhcp.DHCPServer() if server.is_configured(): # We'll need the actual hardware ethernet address for the DHCP entry, # rather than the non-delimited string that 'mac_address' is. full_mac = AIdb.formatValue('mac', mac_address) try: if not suppress_dhcp_msgs: print cw( _("Adding host entry for %s to local DHCP " "configuration.") % full_mac) server.add_option_arch() server.add_host(full_mac, boot_tuples) except dhcp.DHCPServerError as err: print cw( _("Unable to add host (%(mac)s) to DHCP " "configuration: %(error)s") % { 'mac': full_mac, 'error': err }) return if server.is_online(): try: server.control('restart') except dhcp.DHCPServerError as err: print >> sys.stderr, cw( _("\nUnable to restart the DHCP SMF " "service: %s\n" % err)) return elif not suppress_dhcp_msgs: print cw( _("\nLocal DHCP configuration complete, but the DHCP " "server SMF service is offline. To enable the " "changes made, enable: %s.\nPlease see svcadm(1M) " "for further information.\n") % dhcp.DHCP_SERVER_IPV4_SVC) else: # No local DHCP, tell the user all about their boot configuration valid_nets = list(com.get_valid_networks()) if valid_nets: server_ip = valid_nets[0] if not suppress_dhcp_msgs: boofile_text = '\n' for archval, archtype, relpath in boot_tuples: bootfilename = client_id + '.' + archtype boofile_text = (boofile_text + '\t' + archtype + ' clients (arch ' + archval + '): ' + bootfilename + '\n') print _(_PXE_CLIENT_DHCP_CONFIG % (server_ip, boofile_text)) if len(valid_nets) > 1: print cw( _("\nNote: determined more than one IP address " "configured for use with AI. Please ensure the " "above 'Boot server IP' is correct.\n"))
def find_sparc_clients(lservices, sname = None): """ find_sparc_clients() searches /etc/netboot for all clients and returns a dictionary that contains a list of dictionaries. The service name is the key for the main dictionary and the client and image path are members of the subdictionary. The dictionary will look something like: { 'service1': [ { 'ipath':<path1>, 'client':<client1> }, .... ], .... } The information is spread out across a couple of different files within the server. The Client is embedded within a directory path (/etc/netboot/<IP Network>/01<client>). The Image Path is in the wanboot.conf file pointed to by the Client path. The Service Name is contained in the install.conf file pointed to by the Image Path. We first get the IP addresses for the host. Then while only using IPv4 address we iterate over the client directories within /etc/netboot/<IP Network> to get the Image Path and Service Name. The client and image path are then added to the named service dictionary list. Args lservices = sname = Returns dictionary of a list of dictionaries Raises None """ installconf = 'install.conf' wanbootconf = 'wanboot.conf' def get_image_path(lpath): """ gets the Image Path for the client pointed to by lpath. The Image Path on Sparc is stored in the wanboot.conf file. Args lpath = path for directory which contains wanboot.conf file Returns image path for client Raises None """ try: confpath = os.path.join(lpath, wanbootconf) sinfo = os.stat(confpath) fp = open(confpath) fstr = fp.read(sinfo.st_size) fp.close() except (OSError, IOError): sys.stderr.write("%s: error: while accessing wanboot.conf file" % \ sys.argv[0]) return start = fstr.find('boot_file=')+len('boot_file=') end = fstr[start:].find('/platform') return fstr[start:start+end] def get_service_name(lpath): """ gets the Service Name for the client from the lpath pointed to by the wanboot.conf file. The Service Name is in the Image Path install.conf file. Args lpath = path to directory containing install.conf file Returns install service for client Raises None """ try: confpath = os.path.join(lpath, installconf) sinfo = os.stat(confpath) fp = open(confpath) fstr = fp.read(sinfo.st_size) fp.close() except (OSError, IOError): sys.stderr.write("%s: error: while accessing " "install.conf file\n" % \ sys.argv[0]) return start = fstr.find('install_service=')+len('install_service=') end = fstr[start:].find('\n') return fstr[start:start+end] # start of find_sparc_clients if not os.path.exists('/etc/netboot'): return {} sdict = {} hostname = socket.getfqdn() ipaddr = socket.gethostbyname(hostname) # get the Network IP path for the host end = ipaddr.rfind('.') path = os.path.join('/etc/netboot', ipaddr[:end]+'.0') try: for clientdir in os.listdir(path): # strip off the 01 in the clientdir client = AIdb.formatValue('mac', clientdir[2:]) # get the Image from the clientdir/wanboot.conf file ipath = get_image_path(os.path.join(path, clientdir)) if not os.path.exists(ipath): continue # get the service name from the ipath/install.conf file servicename = get_service_name(ipath) # store the client and image path in the # dictionary under the service name. First # check to see if the service name key # already exists. if servicename in lservices and \ (not sname or servicename == sname): tdict = {'client':client, 'ipath':[ipath], 'arch':'Sparc'} if sdict.has_key(servicename): # existing service name key slist = sdict[servicename] slist.extend([tdict]) sdict[servicename] = slist else: # new service name key sdict[servicename] = [tdict] except OSError: return {} return sdict
def test_ipv4_formatValue(self): '''Ensure that ipv4 criteria is formatted appropriately''' fmt = AIdb.formatValue('MINipv4', self.ipv4) self.assertTrue(self.is_ipv4(fmt))
def test_network_formatValue(self): '''Ensure that ipv4 network criteria is formatted appropriately''' fmt = AIdb.formatValue('network', self.network) self.assertTrue(self.is_ipv4(fmt))
def find_x86_clients(lservices, sname = None): """ find_x86_clients() searches TFTPDir for all clients and returns a dictionary that contains a list of dictionaries. The service name is the key for the main dictionary and the client and image path are members of the subdictionary. The dictionary will look something like: { 'service1': [ { 'ipath':<path1>, 'client':<client1> }, .... ], .... } The information is contained within the menu.lst.01<client> file. Though not the best approach architecturally it is the only method currently available. We first get the TFTProot directory. Then iterate over the files within the TFTProot directory to get the client menu which contains the Image Path and Service Name. The client and image path are then added to the named service dictionary list. """ def get_menu_info(path): """ Reads TFTPboot/menu.list file pointed to by 'path' via GrubMenu class in installadm_common.py. Getting the install path from the install_media field, service name from install_service Args path = path for the menu.list.01<client> file. Returns a dictionary of services made up of a list of tuples of port, install path Raises None """ iaddr = 'install_svc_address=' iserv = 'install_service=' imedia = 'install_media=http://' rdict = {} menu = com.GrubMenu(file_name=path) entries = menu.entries[0] # for entries in menu.entries: if entries: if menu[entries].has_key('kernel$'): mdict = menu[entries]['kernel$'] start = mdict.find(iserv)+len(iserv) service = mdict[start:].split(',')[0] start = mdict.find(iaddr)+len(iaddr) port = mdict[start:].split(',')[0].split(':')[-1] start = mdict.find(imedia)+len(imedia) pstart = mdict[start:].split(',')[0].find('/') path = mdict[start:].split(',')[0][pstart:] if rdict.has_key(service): tlist = rdict[service] tlist.extend([(port, path)]) rdict[service] = tlist else: rdict[service] = [(port, path)] return rdict # start of find_x86_clients sdict = {} tftp_dir = com.find_TFTP_root() if tftp_dir and os.path.exists(tftp_dir): for filenames in os.listdir(tftp_dir): if filenames.find("menu.lst.01") >= 0: path = os.path.join(tftp_dir, filenames) pservices = get_menu_info(path) tdict = {'client':'', 'ipath':'', 'arch':'x86'} for servicename in pservices: if servicename in lservices and \ (not sname or servicename == sname): client = AIdb.formatValue('mac', filenames[11:]) tdict['client'] = client # create a list of image_paths for the client ipath = [] for tup in pservices[servicename]: ipath.insert(0, tup[1]) tdict['ipath'] = ipath if sdict.has_key(servicename): # existing service name slist = sdict[servicename] slist.extend([tdict]) sdict[servicename] = slist else: # new service name key sdict[servicename] = [tdict] return sdict
def get_criteria_info(crit_dict): """ Iterates over the criteria which consists of a dictionary with possibly arch, min memory, max memory, min ipv4, max ipv4, min mac, max mac, cpu, platform, min network, max network and zonename converting it into a dictionary with arch, mem, ipv4, mac, cpu, platform, network and zonename. Any min/max attributes are stored as a range within the new dictionary. Args crit_dict = dictionary of the criteria Returns dictionary of combined min/max and other criteria, formatted with possible endings such as MB maximum criteria width Raises None """ if crit_dict is None: return dict(), 0 # tdict values are formatted strings, with possible endings # such as MB. tdict = dict() crit_width = 0 for key in crit_dict.keys(): if key == 'service': continue is_range_crit = key.startswith('MIN') or key.startswith('MAX') # strip off the MAX or MIN for a new keyname if is_range_crit: keyname = key[3:] # strip off the MAX or MIN for a new keyname else: keyname = key tdict.setdefault(keyname, '') db_value = crit_dict[key] if not db_value and not is_range_crit: # For non-range (value) criteria, None means it isn't set. # For range criteria, None means unbounded if the other # criteria in the MIN/MAX pair is set. continue # value criteria not set crit_width = max(crit_width, len(keyname)) fmt_value = AIdb.formatValue(key, db_value) if is_range_crit: if not db_value: fmt_value = "unbounded" if tdict[keyname] != '': if tdict[keyname] != fmt_value: # dealing with range if key.startswith('MAX'): tdict[keyname] = tdict[keyname] + ' - ' + fmt_value else: tdict[keyname] = fmt_value + ' - ' + tdict[keyname] elif tdict[keyname] == "unbounded": # MIN and MAX both unbounded, which means neither is # set in db. Clear tdict value. tdict[keyname] = '' # no values for range, reset tdict else: # first value, not range yet tdict[keyname] = fmt_value # if the partner MIN/MAX criterion is not set in the db, # handle now because otherwise it won't be processed. if key.startswith('MIN'): if 'MAX' + keyname not in crit_dict.keys(): if fmt_value == "unbounded": tdict[keyname] = '' else: tdict[keyname] = tdict[keyname] + ' - unbounded' else: if 'MIN' + keyname not in crit_dict.keys(): if fmt_value == "unbounded": tdict[keyname] = '' else: tdict[keyname] = 'unbounded - ' + tdict[keyname] else: tdict[keyname] = fmt_value return tdict, crit_width
def setup_x86_client(service, mac_address, bootargs=''): ''' Set up an x86 client Creates a relative symlink from the <svcname>'s bootfile to /etc/netboot/<client_id> Creates /etc/netboot/menu.lst.<client_id> boot configuration file Adds client info to AI_SERVICE_DIR_PATH/<svcname>/.config file Arguments: image_path - directory path to AI image mac_address - client MAC address (as formed by MACAddress class, i.e., 'ABABABABABAB') bootargs = bootargs of client (x86) Returns: Nothing ''' # create a client-identifier (01 + MAC ADDRESS) client_id = "01" + mac_address menulst = os.path.join(service.config_dir, grub.MENULST) client_menulst = _menulst_path(client_id) # copy service's menu.lst file to menu.lst.<client_id> try: shutil.copy(menulst, client_menulst) except IOError as err: print >> sys.stderr, cw(_("Unable to copy grub menu.lst file: %s") % err.strerror) return # create a symlink from the boot directory to the sevice's bootfile. # note this must be relative from the boot directory. bootfile, pxegrub_path = _pxegrub_path(client_id) os.symlink("./" + service.dhcp_bootfile, pxegrub_path) clientinfo = {config.FILES: [client_menulst, pxegrub_path]} # if the client specifies bootargs, use them. Otherwise, inherit # the bootargs specified in the service (do nothing) if bootargs: grub.update_bootargs(client_menulst, service.bootargs, bootargs) clientinfo[config.BOOTARGS] = bootargs config.add_client_info(service.name, client_id, clientinfo) # Configure DHCP for this client if the configuration is local, otherwise # suggest the configuration addition. Note we only need to do this for # x86-based clients, not SPARC. server = dhcp.DHCPServer() if server.is_configured(): # We'll need the actual hardware ethernet address for the DHCP entry, # rather than the non-delimited string that 'mac_address' is. full_mac = AIdb.formatValue('mac', mac_address) try: print cw(_("Adding host entry for %s to local DHCP configuration.") % full_mac) server.add_host(full_mac, bootfile) except dhcp.DHCPServerError as err: print cw(_("Unable to add host (%s) to DHCP configuration: %s") % (full_mac, err)) return if server.is_online(): try: server.control('restart') except dhcp.DHCPServerError as err: print >> sys.stderr, cw(_("\nUnable to restart the DHCP SMF " "service: %s\n" % err)) return else: print cw(_("\nLocal DHCP configuration complete, but the DHCP " "server SMF service is offline. To enable the " "changes made, enable: %s.\nPlease see svcadm(1M) " "for further information.\n") % dhcp.DHCP_SERVER_IPV4_SVC) else: # No local DHCP, tell the user all about their boot configuration valid_nets = list(com.get_valid_networks()) if valid_nets: server_ip = valid_nets[0] print _(_PXE_CLIENT_DHCP_CONFIG % (server_ip, bootfile)) if len(valid_nets) > 1: print cw(_("\nNote: determined more than one IP address " "configured for use with AI. Please ensure the above " "'Boot server IP' is correct.\n"))
def test_arch_formatValue(self): '''Ensure that ipv4 criteria is formatted appropriately''' fmt = AIdb.formatValue('MINipv4', self.ipv4) for octet in fmt.split('.'): self.assertEqual(octet, str(int(octet)))
def list_manifests(service): '''Replies to the client with criteria list for a service. The output should be similar to installadm list. Args service - the name of the service being listed Returns None Raises None ''' print 'Content-Type: text/html' # HTML is following print # blank line, end of headers print '<html>' print '<head>' sys.stdout.write('<title>%s %s</title>' % (_('Manifest list for'), service)) print '</head><body>' port = 0 try: smf.AISCF(FMRI="system/install/server") except KeyError: # report the internal error to error_log and requesting client sys.stderr.write( _("error:The system does not have the " "system/install/server SMF service.")) sys.stdout.write( _("error:The system does not have the " "system/install/server SMF service.")) return services = config.get_all_service_names() if not services: # report the error to the requesting client only sys.stdout.write(_('error:no services on this server.\n')) return found = False if config.is_service(service): service_ctrl = AIService(service) found = True # assume new service setup path = service_ctrl.database_path if os.path.exists(path): try: aisql = AIdb.DB(path) aisql.verifyDBStructure() except StandardError as err: # report the internal error to error_log and # requesting client sys.stderr.write( _('error:AI database access ' 'error\n%s\n') % err) sys.stdout.write( _('error:AI database access ' 'error\n%s\n') % err) return # generate the list of criteria for the criteria table header criteria_header = E.TR() for crit in AIdb.getCriteria(aisql.getQueue(), strip=False): criteria_header.append(E.TH(crit)) # generate the manifest rows for the criteria table body names = AIdb.getManNames(aisql.getQueue()) table_body = E.TR() allcrit = AIdb.getCriteria(aisql.getQueue(), strip=False) colspan = str(max(len(list(allcrit)), 1)) for manifest in names: # iterate through each manifest (and instance) for instance in range( 0, AIdb.numInstances(manifest, aisql.getQueue())): table_body.append(E.TR()) # print the manifest name only once (from instance 0) if instance == 0: href = '../' + service + '/' + manifest row = str(AIdb.numInstances(manifest, aisql.getQueue())) table_body.append( E.TD(E.A(manifest, href=href, rowspan=row))) else: table_body.append(E.TD()) crit_pairs = AIdb.getManifestCriteria(manifest, instance, aisql.getQueue(), onlyUsed=True, humanOutput=True) # crit_pairs is an SQLite3 row object which doesn't # support iteritems(), etc. for crit in crit_pairs.keys(): formatted_val = AIdb.formatValue( crit, crit_pairs[crit]) # if we do not get back a valid value ensure a # hyphen is printed (prevents "" from printing) if formatted_val and crit_pairs[crit]: table_body.append( E.TD(formatted_val, align="center")) else: table_body.append( E.TD(lxml.etree.Entity("nbsp"), align="center")) # print the default manifest at the end of the table, # which has the same colspan as the Criteria List label else: href = '../' + service + '/default.xml' table_body.append( E.TR( E.TD(E.A("Default", href=href)), E.TD(lxml.etree.Entity("nbsp"), colspan=colspan, align="center"))) web_page = E.HTML( E.HEAD(E.TITLE(_("OmniOS Automated " "Installation Webserver"))), E.BODY( E.H1( _("Welcome to the OmniOS " "Automated Installation webserver!")), E.P( _("Service '%s' has the following " "manifests available, served to clients " "matching required criteria.") % service), E.TABLE(E.TR(E.TH(_("Manifest"), rowspan="2"), E.TH(_("Criteria List"), colspan=colspan)), criteria_header, table_body, border="1", align="center"), )) print lxml.etree.tostring(web_page, pretty_print=True) # service is not found, provide available services on host if not found: sys.stdout.write(_('Service <i>%s</i> not found. ') % service) sys.stdout.write(_('Available services are:<p><ol><i>')) host = socket.gethostname() for service_name in config.get_all_service_names(): # assume new service setup port = config.get_service_port(service_name) sys.stdout.write( '<a href="http://%s:%d/cgi-bin/' 'cgi_get_manifest.py?version=%s&service=%s">%s</a><br>\n' % (host, port, VERSION, service_name, service_name)) sys.stdout.write('</i></ol>%s' % _('Please select a service ' 'from the above list.')) print '</body></html>'
def test_mac_formatValue(self): '''Ensure that mac criteria is formatted appropriately''' fmt = AIdb.formatValue('MINmac', self.mac) for k, bits in enumerate(fmt.split(':')): self.assertEqual(bits, self.mac[k * 2:(k * 2) + 2])