# Web Interface for Cobbler # # Copyright 2007 Albert P. Tobey # additions: Michael DeHaan # # This software may be freely redistributed under the terms of the GNU # general public license. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import exceptions import xmlrpclib from Cheetah.Template import Template import os import traceback import string from cobbler.utils import * import sys def log_exc(apache): """ Log active traceback to logfile. """ (t, v, tb) = sys.exc_info() apache.log_error("Exception occured: %s" % t ) apache.log_error("Exception value: %s" % v) apache.log_error("Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tb)))) class CobblerWeb(object): """ The Cobbler web interface uses a MVC-style pattern. This is the model. The view uses Cheetah templates. The controller is a small shim to make it all run either under cgi-bin or CherryPy. Supporting other Python frameworks should be trivial. """ def __init__(self, server=None, base_url='/', username=None, password=None, token=None, apache=None): self.server = server self.base_url = base_url self.remote = None self.token = token self.username = username self.password = password self.logout = None self.apache = apache def __xmlrpc_setup(self): """ Sets up the connection to the Cobbler XMLRPC server. Right now, the r/w server is required. In the future, it may be possible to instantiate a r/o webui that doesn't need to login. """ # changed to always create a new connection object self.remote = xmlrpclib.Server(self.server, allow_none=True) # else if we do have a token, try to use it... if self.token is not None: # validate that our token is still good try: self.remote.token_check(self.token) self.username = self.remote.get_user_from_token(self.token) # ensure config is up2date self.remote.update(self.token) return True except Exception, e: if str(e).find("invalid token") != -1: self.apache.log_error("cobbler token timeout for: %s" % self.username) log_exc(self.apache) self.token = None else: raise e # if we (still) don't have a token, login for the first time elif self.password and self.username: try: self.token = self.remote.login( self.username, self.password ) except Exception, e: self.apache.log_error("cobbler login failed for: %s" % self.username) log_exc(self.apache) return False self.password = None # don't need it anymore, get rid of it # ensure configuration is up2date self.remote.update(self.token) return True # login failed return False def __render(self, template, data): """ Call the templating engine (Cheetah), wrapping up the location of files while we're at it. """ data['base_url'] = self.base_url filepath = os.path.join("/usr/share/cobbler/webui_templates/",template) tmpl = Template( file=filepath, searchList=[data] ) return str(tmpl) def modes(self): """ Returns a list of methods in this object that can be run as web modes. In the background, it is using function attributes similarly to how CherryPy does. """ retval = list() for m in dir(self): func = getattr( self, m ) if hasattr(func, 'exposed') and getattr(func,'exposed'): retval.append(m) return retval # ------------------------------------------------------------------------ # # Index # ------------------------------------------------------------------------ # def index(self,**args): return self.__render( 'index.tmpl', { } ) def menu(self,**args): return self.__render( 'blank.tmpl', { } ) # ------------------------------------------------------------------------ # # Settings # ------------------------------------------------------------------------ # # FIXME: does it make sense to be able to edit settings? Probably not, # as you could disable a lot of functionality if you aren't careful # including your ability to fix it back. def settings_view(self,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() return self.__render( 'item.tmpl', { 'item_data': self.remote.get_settings(), 'caption': "Cobbler Settings" } ) # ------------------------------------------------------------------------ # # Distributions # ------------------------------------------------------------------------ # def distro_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() (page, results_per_page, pages) = self.__compute_pagination(page,limit,"distro") distros = self.remote.get_distros(page,results_per_page) if len(distros) > 0: return self.__render( 'distro_list.tmpl', { 'distros' : distros, 'pages' : pages, 'page' : page, 'results_per_page' : results_per_page }) else: return self.__render('empty.tmpl', {}) def distro_edit(self, name=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() input_distro = None if name is not None: input_distro = self.remote.get_distro(name, True) can_edit = self.remote.check_access_no_fail(self.token,"modify_distro",name) else: can_edit = self.remote.check_access_no_fail(self.token,"new_distro",None) if not can_edit: return self.__render('message.tmpl', { 'message1' : "Access denied.", 'message2' : "You do not have permission to create new objects." }) return self.__render( 'distro_edit.tmpl', { 'user' : self.username, 'edit' : True, 'editable' : can_edit, 'distro': input_distro, } ) def distro_save(self,name=None,oldname=None,new_or_edit=None,editmode='edit',kernel=None, initrd=None,kopts=None,ksmeta=None,owners=None,arch=None,breed=None, delete1=None,delete2=None,recursive=False,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() # pre-command paramter checking # HTML forms do not transmit disabled fields if name is None and oldname is not None: name = oldname # handle deletes as a special case if new_or_edit == 'edit' and delete1 and delete2: try: if recursive is None: self.remote.remove_distro(name,self.token,False) else: self.remote.remove_distro(name,self.token,True) except Exception, e: return self.error_page("could not delete %s, %s" % (name,str(e))) return self.distro_list() if name is None: return self.error_page("name is required") if kernel is None or not str(kernel).startswith("/"): return self.error_page("kernel must be specified as an absolute path") if initrd is None or not str(initrd).startswith("/"): return self.error_page("initrd must be specified as an absolute path") if (editmode == 'rename' or editmode == 'copy') and name == oldname: return self.error_page("The name has not been changed.") # grab a reference to the object if new_or_edit == "edit" and editmode in [ "edit", "rename" ]: try: if editmode == "edit": distro = self.remote.get_distro_handle( name, self.token) else: distro = self.remote.get_distro_handle( oldname, self.token) except: log_exc(self.apache) return self.error_page("Failed to lookup distro: %s" % name) else: distro = self.remote.new_distro(self.token) try: if editmode != "rename" and name: self.remote.modify_distro(distro, 'name', name, self.token) self.remote.modify_distro(distro, 'kernel', kernel, self.token) self.remote.modify_distro(distro, 'initrd', initrd, self.token) if kopts: self.remote.modify_distro(distro, 'kopts', kopts, self.token) if ksmeta: self.remote.modify_distro(distro, 'ksmeta', ksmeta, self.token) if owners: self.remote.modify_distro(distro, 'owners', owners, self.token) if arch: self.remote.modify_distro(distro, 'arch', arch, self.token) if breed: self.remote.modify_distro(distro, 'breed', breed, self.token) # now time to save, do we want to run duplication checks? self.remote.save_distro(distro, self.token, editmode) except Exception, e: log_exc(self.apache) return self.error_page("Error while saving distro: %s" % str(e)) if editmode == "rename" and name != oldname: try: self.remote.rename_distro(distro, name, self.token) except Exception, e: return self.error_page("Rename unsuccessful.") return self.distro_list() # ------------------------------------------------------------------------ # # Systems # ------------------------------------------------------------------------ # # if the system list is huge, this will probably need to use an # iterator so the list doesn't get copied around def __compute_pagination(self,page,results_per_page,collection_type): default_page = 0 default_results_per_page = 50 total_size = self.remote.get_size(collection_type) try: page = int(page) except: page = default_page try: results_per_page = int(results_per_page) except: results_per_page = default_results_per_page if page < 0: page = default_page if results_per_page <= 0: results_per_page = default_results_per_page pages = total_size / results_per_page return (page, results_per_page, pages) def system_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() (page, results_per_page, pages) = self.__compute_pagination(page,limit,"system") systems = self.remote.get_systems(page,results_per_page) if len(systems) > 0: return self.__render( 'system_list.tmpl', { 'systems' : systems, 'pages' : pages, 'page' : page, 'results_per_page' : results_per_page } ) else: return self.__render('empty.tmpl',{}) def system_save(self,name=None,oldname=None,editmode="edit",profile=None, new_or_edit=None, kopts=None, ksmeta=None, owners=None, server_override=None, netboot='n', virtpath=None,virtram=None,virttype=None,virtcpus=None,virtfilesize=None,delete1=None, delete2=None, **args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() # parameter checking if name is None and oldname is not None: name = oldname if name is None: return self.error_page("System name parameter is REQUIRED.") if (editmode == 'rename' or editmode == 'copy') and name == oldname: return self.error_page("The name has not been changed.") # handle deletes as a special case if new_or_edit == 'edit' and delete1 and delete2: try: self.remote.remove_system(name,self.token) except Exception, e: return self.error_page("could not delete %s, %s" % (name,str(e))) return self.system_list() # grab a reference to the object if new_or_edit == "edit" and editmode in [ "edit", "rename" ] : try: if editmode == "edit": system = self.remote.get_system_handle( name, self.token ) else: system = self.remote.get_system_handle( oldname, self.token ) except: return self.error_page("Failed to lookup system: %s" % name) else: system = self.remote.new_system( self.token ) # go! try: if editmode != "rename" and name: self.remote.modify_system(system, 'name', name, self.token ) self.remote.modify_system(system, 'profile', profile, self.token) if kopts: self.remote.modify_system(system, 'kopts', kopts, self.token) if ksmeta: self.remote.modify_system(system, 'ksmeta', ksmeta, self.token) if owners: self.remote.modify_system(system, 'owners', owners, self.token) if netboot: self.remote.modify_system(system, 'netboot-enabled', netboot, self.token) if server_override: self.remote.modify_system(system, 'server', server_override, self.token) if virtfilesize: self.remote.modify_system(system, 'virt-file-size', virtfilesize, self.token) if virtcpus: self.remote.modify_system(system, 'virt-cpus', virtcpus, self.token) if virtram: self.remote.modify_system(system, 'virt-ram', virtram, self.token) if virttype: self.remote.modify_system(system, 'virt-type', virttype, self.token) if virtpath: self.remote.modify_system(system, 'virt-path', virtpath, self.token) for x in range(0,7): interface = "intf%s" % x macaddress = args.get("macaddress-%s" % interface, "") ipaddress = args.get("ipaddress-%s" % interface, "") hostname = args.get("hostname-%s" % interface, "") virtbridge = args.get("virtbridge-%s" % interface, "") dhcptag = args.get("dhcptag-%s" % interface, "") subnet = args.get("subnet-%s" % interface, "") gateway = args.get("gateway-%s" % interface, "") if not (macaddress != "" or ipaddress != "" or hostname != "" or virtbridge != "" or dhcptag != "" or subnet != "" or gateway != ""): # if we have nothing to modify, request that we remove the interface unless it's the # the first interface, in which case it is NOT removeable if not interface == "intf0": self.remote.modify_system(system,'delete-interface', interface, self.token) else: # it looks like we have at least one value to submit, just send the ones over that are # /not/ None (just to be paranoid about XMLRPC and allow-none) mods = {} mods["macaddress-%s" % interface] = macaddress mods["ipaddress-%s" % interface] = ipaddress mods["hostname-%s" % interface] = hostname mods["virtbridge-%s" % interface] = virtbridge mods["dhcptag-%s" % interface] = dhcptag mods["subnet-%s" % interface] = subnet mods["gateway-%s" % interface] = gateway self.remote.modify_system(system,'modify-interface', mods, self.token) self.remote.save_system(system, self.token, editmode) except Exception, e: log_exc(self.apache) return self.error_page("Error while saving system: %s" % str(e)) if editmode == "rename" and name != oldname: try: self.remote.rename_system(system, name, self.token) except Exception, e: return self.error_page("Rename unsuccessful") return self.system_list() def system_edit(self, name=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() input_system = None if name is not None: input_system = self.remote.get_system(name,True) can_edit = self.remote.check_access_no_fail(self.token,"modify_system",name) else: can_edit = self.remote.check_access_no_fail(self.token,"new_system",None) if not can_edit: return self.__render('message.tmpl', { 'message1' : "Access denied.", 'message2' : "You do not have permission to create new objects." }) return self.__render( 'system_edit.tmpl', { 'user' : self.username, 'edit' : True, 'editable' : can_edit, 'system': input_system, 'profiles': self.remote.get_profiles() } ) # ------------------------------------------------------------------------ # # Profiles # ------------------------------------------------------------------------ # def profile_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() (page, results_per_page, pages) = self.__compute_pagination(page,limit,"profile") profiles = self.remote.get_profiles(page,results_per_page) if len(profiles) > 0: return self.__render( 'profile_list.tmpl', { 'profiles' : profiles, 'pages' : pages, 'page' : page, 'results_per_page' : results_per_page } ) else: return self.__render('empty.tmpl', {}) def subprofile_edit(self, name=None,**spam): return self.profile_edit(name,1) def profile_edit(self, name=None, subprofile=0, **spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() input_profile = None if name is not None: input_profile = self.remote.get_profile(name,True) can_edit = self.remote.check_access_no_fail(self.token,"modify_profile",name) else: can_edit = self.remote.check_access_no_fail(self.token,"new_profile",None) if not can_edit: return self.__render('message.tmpl', { 'message1' : "Access denied.", 'message2' : "You do not have permission to create new objects." }) return self.__render( 'profile_edit.tmpl', { 'user' : self.username, 'edit' : True, 'editable' : can_edit, 'profile': input_profile, 'distros': self.remote.get_distros(), 'profiles': self.remote.get_profiles(), 'repos': self.remote.get_repos(), 'ksfiles': self.remote.get_kickstart_templates(self.token), 'subprofile': subprofile } ) def profile_save(self,new_or_edit=None,editmode='edit',name=None,oldname=None, distro=None,kickstart=None,kopts=None, ksmeta=None,owners=None,virtfilesize=None,virtram=None,virttype=None, virtpath=None,repos=None,dhcptag=None,delete1=None,delete2=None, parent=None,virtcpus=None,virtbridge=None,subprofile=None,server_override=None,recursive=False,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() # pre-command parameter checking if name is None and oldname is not None: name = oldname if name is None: return self.error_page("A name has not been specified.") if distro is None and str(subprofile) == "0" : return self.error_page("A distribution has not been specified.") if parent is None and str(subprofile) == "1" : return self.error_page("A parent profile has not been specified.") if (editmode == 'rename' or editmode == 'copy') and name == oldname: return self.error_page("The name has not been changed") # handle deletes as a special case if new_or_edit == 'edit' and delete1 and delete2: try: if recursive: self.remote.remove_profile(name,self.token,True) else: self.remote.remove_profile(name,self.token,False) except Exception, e: return self.error_page("could not delete %s, %s" % (name,str(e))) return self.profile_list() # grab a reference to the object if new_or_edit == "edit" and editmode in [ "edit", "rename" ] : try: if editmode == "edit": profile = self.remote.get_profile_handle( name, self.token ) else: profile = self.remote.get_profile_handle( oldname, self.token ) except: return self.error_page("Failed to lookup profile: %s" % name) else: if str(subprofile) != "1": profile = self.remote.new_profile(self.token) else: profile = self.remote.new_subprofile(self.token) try: if editmode != "rename" and name: self.remote.modify_profile(profile, 'name', name, self.token) if str(subprofile) != "1" and distro: self.remote.modify_profile(profile, 'distro', distro, self.token) if str(subprofile) == "1" and parent: self.remote.modify_profile(profile, 'parent', parent, self.token) if kickstart: self.remote.modify_profile(profile, 'kickstart', kickstart, self.token) if kopts: self.remote.modify_profile(profile, 'kopts', kopts, self.token) if owners: self.remote.modify_profile(profile, 'owners', owners, self.token) if ksmeta: self.remote.modify_profile(profile, 'ksmeta', ksmeta, self.token) if virtfilesize: self.remote.modify_profile(profile, 'virt-file-size', virtfilesize, self.token) if virtram: self.remote.modify_profile(profile, 'virt-ram', virtram, self.token) if virttype: self.remote.modify_profile(profile, 'virt-type', virttype, self.token) if virtpath: self.remote.modify_profile(profile, 'virt-path', virtpath, self.token) if virtbridge: self.remote.modify_profile(profile, 'virt-bridge', virtbridge, self.token) if virtcpus: self.remote.modify_profile(profile, 'virt-cpus', virtcpus, self.token) if server_override: self.remote.modify_profile(profile, 'server', server_override, self.token) if repos is None: repos = [] elif type(repos) == type(str()): repos = [ repos ] if type(repos) == type([]): if '--none--' in repos: repos.remove( '--none--' ) self.remote.modify_profile(profile, 'repos', repos, self.token) if dhcptag: self.remote.modify_profile(profile, 'dhcp-tag', dhcptag, self.token) self.remote.save_profile(profile,self.token, editmode) except Exception, e: log_exc(self.apache) return self.error_page("Error while saving profile: %s" % str(e)) if editmode == "rename" and name != oldname: try: self.remote.rename_profile(profile, name, self.token) except Exception, e: return self.error_page("Rename unsuccessful.") return self.profile_list() # ------------------------------------------------------------------------ # # Repos # ------------------------------------------------------------------------ # def repo_list(self,page=None,limit=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() (page, results_per_page, pages) = self.__compute_pagination(page,limit,"repo") repos = self.remote.get_repos(page,results_per_page) if len(repos) > 0: return self.__render( 'repo_list.tmpl', { 'repos' : repos, 'pages' : pages, 'page' : page, 'results_per_page' : results_per_page }) else: return self.__render('empty.tmpl', {}) def repo_edit(self, name=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() input_repo = None if name is not None: input_repo = self.remote.get_repo(name, True) can_edit = self.remote.check_access_no_fail(self.token,"modify_repo",name) else: can_edit = self.remote.check_access_no_fail(self.token,"new_repo",None) if not can_edit: return self.__render('message.tmpl', { 'message1' : "Access denied.", 'message2' : "You do not have permission to create new objects." }) return self.__render( 'repo_edit.tmpl', { 'user' : self.username, 'repo': input_repo, 'editable' : can_edit } ) def repo_save(self,name=None,oldname=None,new_or_edit=None,editmode="edit", mirror=None,owners=None,keep_updated=None,mirror_locally=0,priority=99, rpm_list=None,createrepo_flags=None,arch=None,yumopts=None, delete1=None,delete2=None,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() # pre-command parameter checking if name is None and oldname is not None: name = oldname if name is None: return self.error_page("name is required") if (editmode == 'rename' or editmode == 'copy') and name == oldname: return self.error_page("The name has not been changed.") # handle deletes as a special case if new_or_edit == 'edit' and delete1 and delete2: try: self.remote.remove_repo(name,self.token) except Exception, e: return self.error_page("could not delete %s, %s" % (name,str(e))) return self.repo_list() # more parameter checking if mirror is None: return self.error_page("mirror is required") # grab a reference to the object if new_or_edit == "edit" and editmode in [ "edit", "rename" ]: try: if editmode == "edit": repo = self.remote.get_repo_handle( name, self.token) else: repo = self.remote.get_repo_handle( oldname, self.token) except: return self.error_page("Failed to lookup repo: %s" % name) else: repo = self.remote.new_repo(self.token) try: if editmode != "rename" and name: self.remote.modify_repo(repo, 'name', name, self.token) self.remote.modify_repo(repo, 'mirror', mirror, self.token) self.remote.modify_repo(repo, 'keep-updated', keep_updated, self.token) self.remote.modify_repo(repo, 'priority', priority, self.token) self.remote.modify_repo(repo, 'mirror-locally', mirror_locally, self.token) if rpm_list: self.remote.modify_repo(repo, 'rpm-list', rpm_list, self.token) if createrepo_flags: self.remote.modify_repo(repo, 'createrepo-flags', createrepo_flags, self.token) if arch: self.remote.modify_repo(repo, 'arch', arch, self.token) if yumopts: self.remote.modify_repo(repo, 'yumopts', yumopts, self.token) if owners: self.remote.modify_repo(repo, 'owners', owners, self.token) self.remote.save_repo(repo, self.token, editmode) except Exception, e: log_exc(self.apache) return self.error_page("Error while saving repo: %s" % str(e)) if editmode == "rename" and name != oldname: try: self.remote.rename_repo(repo, name, self.token) except Exception, e: return self.error_page("Rename unsuccessful.") return self.repo_list() # ------------------------------------------------------------------------ # # Kickstart files # ------------------------------------------------------------------------ # def ksfile_list(self,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() return self.__render( 'ksfile_list.tmpl', { 'ksfiles': self.remote.get_kickstart_templates(self.token) } ) def ksfile_new(self, name=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() can_edit = self.remote.check_access_no_fail(self.token,"add_kickstart",name) return self.__render( 'ksfile_new.tmpl', { 'editable' : can_edit, 'ksdata': '' } ) def ksfile_edit(self, name=None,**spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() can_edit = self.remote.check_access_no_fail(self.token,"modify_kickstart",name) return self.__render( 'ksfile_edit.tmpl', { 'name': name, 'deleteable' : not self.remote.is_kickstart_in_use(name,self.token), 'editable' : can_edit, 'ksdata': self.remote.read_or_write_kickstart_template(name,True,"",self.token) } ) def ksfile_save(self, name=None, ksdata=None, delete1=None, delete2=None, isnew=None, **args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() try: if delete1 and delete2: self.remote.read_or_write_kickstart_template(name,False,-1,self.token) if isnew is not None: name = "/var/lib/cobbler/kickstarts/" + name if not delete1 and not delete2: self.remote.read_or_write_kickstart_template(name,False,ksdata,self.token) except Exception, e: return self.error_page("An error occurred while trying to save kickstart file %s:

%s" % (name,str(e))) return self.ksfile_list() # ------------------------------------------------------------------------ # # Miscellaneous # ------------------------------------------------------------------------ # def sync(self,**args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() can_edit = self.remote.check_access_no_fail(self.token,"sync",None) if not can_edit: return self.__render('message.tmpl', { 'message1' : "Access denied.", 'message2' : "You do not have permission to create new objects." }) try: rc = self.remote.sync(self.token) if not rc: return self.error_page("Sync failed. Try debugging locally.") except Exception, e: log_exc(self.apache) return self.error_page("Sync encountered an exception: %s" % str(e)) return self.__render('message.tmpl', { 'message1' : "Sync complete.", 'message2' : "Cobbler config has been applied to filesystem." }) def random_mac(self, **spam): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() mac = self.remote.get_random_mac() return mac def error_page(self, message, **spam): # hack to remove some junk from remote fault errors so they # look as if they were locally generated and not exception-based. if message.endswith(">"): message = message[:-2] message = message.replace(":","",1) return self.__render( 'error_page.tmpl', { 'message': message } ) def xmlrpc_auth_failure(self, **spam): return self.__render( 'error_page.tmpl', { 'message': "XMLRPC Authentication Error. See Apache logs for details." } ) # make CherryPy and related frameworks able to use this module easily # by borrowing the 'exposed' function attritbute standard and using # it for the modes() method modes.exposed = False error_page.exposed = False index.exposed = True menu.exposed = True distro_edit.exposed = True distro_list.exposed = True distro_save.exposed = True subprofile_edit.exposed = True profile_edit.exposed = True profile_list.exposed = True profile_save.exposed = True system_edit.exposed = True system_list.exposed = True system_save.exposed = True repo_edit.exposed = True repo_list.exposed = True repo_save.exposed = True settings_view.exposed = True ksfile_edit.exposed = True ksfile_new.exposed = True ksfile_save.exposed = True ksfile_list.exposed = True sync.exposed = True random_mac.exposed = True class CobblerWebAuthException(exceptions.Exception): pass