The attached patch is the first step towards an authorization system for cobbler. It only adds tags for systems and user support. The tags do nothing yet, but will come into play with later patches. Michael, you can apply if you want or do the sensible thing and wait until this does something useful. I'll try to push my branch to the public repository later if people want to try that rather than patches. The authorization support I have in mind uses these generic tags to grant users access to systems and profiles. I think profiles will have inheritable tags, but will not be editable by non-superuser users, since this is probably what most people want. Basically, if a user has a tag that a system (or its upstream profile(s)) also has, they have r/w access. Otherwise, it's a deny-all policy. Users can be granted superuser access with the --superuser flag which is only available on the CLI for now. It looks like it will be really easy to support authorization in both the webui and CLI. The CLI support will come via sudo and its SUDO_USER environment variable. That way users can be given access to run the CLI as root, but only for given systems. It will be up to each sysadmin out there to determine whether they want to risk giving sudo access to cobbler as root and trust cobbler's code. I'm definitely open to discussion about how the authorization stuff plays out. Right now I'm sticking to the KISS principle and trying to keep things very flexible. -Al
From 0beb3357bb1d30c3530ede59051712df92e78d08 Mon Sep 17 00:00:00 2001 From: Al Tobey <tobert@xxxxxxxxx> Date: Sun, 4 Nov 2007 16:11:53 -0800 Subject: [PATCH] Add users and tags. * add user objects and all the bits need for them - they don't do anything yet * add tags to systems and profiles - also useless so far - will be used for user authorization - will be used for UI updates to split systems up logically for nicer reports --- cobbler.spec | 1 + cobbler/action_sync.py | 1 + cobbler/api.py | 12 ++++ cobbler/cobbler.py | 71 +++++++++++++++++++++++++- cobbler/collection.py | 3 + cobbler/collection_users.py | 57 ++++++++++++++++++++ cobbler/config.py | 21 +++++++- cobbler/item_profile.py | 17 ++++++ cobbler/item_system.py | 16 ++++++ cobbler/item_user.py | 103 +++++++++++++++++++++++++++++++++++++ cobbler/remote.py | 70 ++++++++++++++++++++++++- cobbler/webui/CobblerWeb.py | 98 ++++++++++++++++++++++++++++++++++- cobbler/webui/master.py | 38 ++++++++----- docs/cobbler.pod | 50 ++++++++++++++++-- setup.py | 8 +++- webui_templates/master.tmpl | 2 + webui_templates/profile_edit.tmpl | 15 +++++ webui_templates/profile_list.tmpl | 3 + webui_templates/system_edit.tmpl | 14 +++++ webui_templates/system_list.tmpl | 5 ++ webui_templates/user_edit.tmpl | 99 +++++++++++++++++++++++++++++++++++ webui_templates/user_list.tmpl | 41 +++++++++++++++ 22 files changed, 716 insertions(+), 29 deletions(-) create mode 100644 cobbler/collection_users.py create mode 100644 cobbler/item_user.py create mode 100644 webui_templates/user_edit.tmpl create mode 100644 webui_templates/user_list.tmpl diff --git a/cobbler.spec b/cobbler.spec index 26a5d62..4c5fa3b 100644 --- a/cobbler.spec +++ b/cobbler.spec @@ -178,6 +178,7 @@ test "x$RPM_BUILD_ROOT" != "x" && rm -rf $RPM_BUILD_ROOT %defattr(664,root,root) %config(noreplace) /var/lib/cobbler/settings +%config(noreplace) /var/lib/cobbler/users %config(noreplace) /var/lib/cobbler/snippets/partition_select /var/lib/cobbler/elilo-3.6-ia64.efi /var/lib/cobbler/menu.c32 diff --git a/cobbler/action_sync.py b/cobbler/action_sync.py index 764958c..6187205 100644 --- a/cobbler/action_sync.py +++ b/cobbler/action_sync.py @@ -53,6 +53,7 @@ class BootSync: self.systems = config.systems() self.settings = config.settings() self.repos = config.repos() + self.users = config.users() self.load_snippet_cache() def run(self): diff --git a/cobbler/api.py b/cobbler/api.py index 2a51c1a..f36996c 100644 --- a/cobbler/api.py +++ b/cobbler/api.py @@ -89,6 +89,12 @@ class BootAPI: """ return self._config.repos() + def users(self): + """ + Return the current list of users + """ + return self._config.users() + def settings(self): """ Return the application configuration @@ -120,6 +126,12 @@ class BootAPI: """ return self._config.new_repo(is_subobject=is_subobject) + def new_user(self,is_subobject=False): + """ + Create a blank, unconfigured user, unattached to a collection + """ + return self._config.new_user() + def auto_add_repos(self): """ Import any repos this server knows about and mirror them. diff --git a/cobbler/cobbler.py b/cobbler/cobbler.py index 78351f6..6bb6013 100755 --- a/cobbler/cobbler.py +++ b/cobbler/cobbler.py @@ -83,6 +83,16 @@ class BootCLI: 'report' : self.repo_report, 'sync' : self.reposync } + self.commands['user'] = { + 'add' : self.user_add, + 'edit' : self.user_edit, + 'rename' : self.user_rename, + 'copy' : self.user_copy, + 'delete' : self.user_remove, + 'remove' : self.user_remove, + 'list' : self.user_list, + 'report' : self.user_report, + } self.commands['toplevel'] = { '-v' : self.version, '--version' : self.version, @@ -98,6 +108,7 @@ class BootCLI: 'system' : self.system, 'repos' : self.repo, 'repo' : self.repo, + 'user' : self.user, 'sync' : self.sync, 'reposync' : self.reposync, 'import' : self.import_tree, @@ -150,6 +161,12 @@ class BootCLI: else: return self.__print_sorted(self.api.repos()) + def user_report(self,args): + if len(args) > 0: + return self.__list_names2(self.api.users(), args) + else: + return self.__print_sorted(self.api.users()) + def report(self,args): args.append("") # filler @@ -167,6 +184,9 @@ class BootCLI: if a == '--systems' or len(args) == 1: self.system_report([]) match = True + if a == '--users' or len(args) == 1: + self.user_report([]) + match = True if not match and a is not None and a != "": raise CX(_("cobbler does not understand '%(command)s'") % { "command" : a }) match = False @@ -227,6 +247,12 @@ class BootCLI: return self.__list_names2(self.api.repos(),args) else: return self.__list_names(self.api.repos()) + + def user_list(self, args): + if len(args) > 0: + return self.__list_names2(self.api.users(),args) + else: + return self.__list_names(self.api.users()) ############################################################### # UTILITY FUNCTIONS @@ -322,6 +348,9 @@ class BootCLI: def repo_remove(self,args): return self.__generic_remove(args,"repo","name",self.api.repos) + + def user_remove(self,args): + return self.__generic_remove(args,"user","name",self.api.users) #################################################################### # COPY FUNCTIONS @@ -342,6 +371,10 @@ class BootCLI: exc = _("repository does not exist") self.__generic_copy(args,self.api.repos,self.__repo_control,exc) + def user_copy(self,args): + exc = _("user does not exist") + self.__generic_copy(args,self.api.users,self.__user_control,exc) + ##################################################################### # RENAME FUNCTIONS @@ -361,6 +394,10 @@ class BootCLI: exc = _("repository does not exist") self.__generic_rename(args,self.api.repos,self.__repo_control,exc) + def user_rename(self,args): + exc = _("user does not exist") + self.__generic_rename(args,self.api.users,self.__user_control,exc) + ##################################################################### # EDIT FUNCTIONS @@ -375,11 +412,15 @@ class BootCLI: def system_edit(self,args): exc = _("system does not exist") self.__generic_edit(args,self.api.systems,self.__system_control,exc) - + def repo_edit(self,args): exc = _("repository does not exist") self.__generic_edit(args,self.api.repos,self.__repo_control,exc) - + + def user_edit(self,args): + exc = _("user does not exist") + self.__generic_edit(args,self.api.users,self.__user_control,exc) + ##################################################################### # ADD FUNCTIONS @@ -421,6 +462,8 @@ class BootCLI: does_inherit = self.__prescan_for_inheritance_args(args) self.__generic_add(args,self.api.new_repo,self.__repo_control,does_inherit) + def user_add(self,args): + self.__generic_add(args,self.api.new_user,self.__user_control,None) ############################################################### # CONTROL IMPLEMENTATIONS @@ -439,6 +482,7 @@ class BootCLI: '--kick-start' : lambda(a) : profile.set_kickstart(a), '--answers' : lambda(a) : profile.set_kickstart(a), '--kopts' : lambda(a) : profile.set_kernel_options(a), + '--tags' : lambda(a) : profile.set_tags(a), '--virt-file-size' : lambda(a) : profile.set_virt_file_size(a), '--virt-ram' : lambda(a) : profile.set_virt_ram(a), '--virt-bridge' : lambda(a) : profile.set_virt_bridge(a), @@ -578,6 +622,7 @@ class BootCLI: '--dhcp-tag7' : lambda(a) : sys.set_dhcp_tag(a,"intf7"), '--kickstart' : lambda(a) : sys.set_kickstart(a), '--netboot-enabled' : lambda(a) : sys.set_netboot_enabled(a), + '--tags' : lambda(a) : sys.set_tags(a), '--virt-path' : lambda(a) : sys.set_virt_path(a), '--virt-type' : lambda(a) : sys.set_virt_type(a), '--server-override' : lambda(a) : sys.set_server(a) @@ -587,6 +632,22 @@ class BootCLI: self.api.systems().add(sys, with_copy=True) return self.apply_args(args,commands,on_ok) + def __user_control(self,args,user,newname=None): + """ + Create/edit a user: 'cobbler user add --name='joe' --tags="foo,bar" --superuser=[y/n] ... + """ + commands = { + '--name' : lambda(a): user.set_name(a), + '--newname' : lambda(a): True, + '--tags' : lambda(a): user.set_tags(a), + '--superuser' : lambda(a): user.set_superuser(a) + } + def on_ok(): + if newname is not None: + user.set_name(newname) + self.api.users().add(user, with_copy=True) + return self.apply_args(args,commands,on_ok) + ################################################################################### # PARSING FUNCTIONS @@ -763,6 +824,12 @@ class BootCLI: """ return self.relay_args(args, self.commands['repo']) + def user(self,args): + """ + Handles any of the 'cobbler user' subcommands + """ + return self.relay_args(args, self.commands['user']) + #################################################### def main(): diff --git a/cobbler/collection.py b/cobbler/collection.py index 8e6be39..014517c 100644 --- a/cobbler/collection.py +++ b/cobbler/collection.py @@ -24,6 +24,7 @@ import item_system import item_profile import item_distro import item_repo +import item_user from rhpl.translate import _, N_, textdomain, utf8 @@ -142,6 +143,8 @@ class Collection(serializable.Serializable): lite_sync.add_single_distro(ref.name) elif isinstance(ref, item_repo.Repo): pass + elif isinstance(ref, item_user.User): + pass else: print _("Internal error. Object type not recognized: %s") % type(ref) diff --git a/cobbler/collection_users.py b/cobbler/collection_users.py new file mode 100644 index 0000000..cf710b1 --- /dev/null +++ b/cobbler/collection_users.py @@ -0,0 +1,57 @@ +""" +Cobbler user collection. + +Copyright 2006, Red Hat, Inc +Michael DeHaan <mdehaan@xxxxxxxxxx> +Albert Tobey <tobert@xxxxxxxxx> + +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 item_user as user +import utils +import collection +from cexceptions import * +from rhpl.translate import _, N_, textdomain, utf8 + +TESTMODE = False + +#-------------------------------------------- + +class Users(collection.Collection): + + def collection_type(self): + return "user" + + def factory_produce(self,config,seed_data): + """ + Return a system forged from seed_data + """ + return user.User(config).from_datastruct(seed_data) + + def remove(self,name,with_delete=True): + """ + Remove element named 'name' from the collection + """ + + name = name.lower() + obj = self.find(name=name) + + if obj is not None: + if with_delete: + self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/user/pre/*") + + del self.listing[name] + self.config.serialize_delete(self, obj) + + if with_delete: + self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/user/post/*") + + return True + raise CX(_("cannot delete an object that does not exist")) + diff --git a/cobbler/config.py b/cobbler/config.py index 258b241..ddf897c 100644 --- a/cobbler/config.py +++ b/cobbler/config.py @@ -19,11 +19,13 @@ import item_distro as distro import item_profile as profile import item_system as system import item_repo as repo +import item_user as user import collection_distros as distros import collection_profiles as profiles import collection_systems as systems import collection_repos as repos +import collection_users as users import modules.serializer_yaml as serializer_yaml import settings @@ -58,19 +60,22 @@ class Config: self._repos = repos.Repos(weakref.proxy(self)) self._profiles = profiles.Profiles(weakref.proxy(self)) self._systems = systems.Systems(weakref.proxy(self)) + self._users = users.Users(weakref.proxy(self)) self._settings = settings.Settings() # not a true collection self._serialize_graph_classes = [ self._distros, self._repos, self._profiles, - self._systems + self._systems, + self._users ] self._graph_classes = [ self._settings, self._distros, self._repos, self._profiles, - self._systems + self._systems, + self._users ] def __cmp(self,a,b): @@ -106,6 +111,12 @@ class Config: """ return self._repos + def users(self): + """ + Return the definitive copy of the Users collection + """ + return self._users + def new_distro(self,is_subobject=False): """ Create a new distro object with a backreference to this object @@ -130,6 +141,12 @@ class Config: """ return repo.Repo(weakref.proxy(self),is_subobject=is_subobject) + def new_user(self,is_subobject=False): + """ + Create a new user. + """ + return user.User(weakref.proxy(self),is_subobject=False) + def clear(self): """ Forget about all loaded configuration data diff --git a/cobbler/item_profile.py b/cobbler/item_profile.py index ddb36ba..b494e95 100644 --- a/cobbler/item_profile.py +++ b/cobbler/item_profile.py @@ -43,6 +43,7 @@ class Profile(item.Item): self.virt_ram = (512, '<<inherit>>')[is_subobject] self.repos = ([], '<<inherit>>')[is_subobject] self.depth = 1 + self.tags = ([], '<<inherit>>')[is_subobject] self.virt_type = (self.settings.default_virt_type, '<<inherit>>')[is_subobject] self.virt_path = ("", '<<inherit>>')[is_subobject] self.virt_bridge = (self.settings.default_virt_bridge, '<<inherit>>')[is_subobject] @@ -65,6 +66,7 @@ class Profile(item.Item): self.depth = self.load_item(seed_data,'depth', 1) self.dhcp_tag = self.load_item(seed_data,'dhcp_tag', 'default') self.server = self.load_item(seed_data,'server', '<<inherit>>') + self.tags = self.load_item(seed_data,'tags', []) # backwards compatibility if type(self.repos) != list: @@ -183,6 +185,18 @@ class Profile(item.Item): return True raise CX(_("kickstart not found")) + def set_tags(self,tags): + """ + A list of tags for the profile. Completely arbitrary and user-defined. + """ + if hasattr(tags, 'split'): + tags = [ tag.strip() for tag in tags.split(',') ] + elif not hasattr(tags, '__iter__'): + raise CX(_("Invalid type for tags. Must be a string (comma-delimited) or list.")) + + self.tags = tags + return True + def set_virt_cpus(self,num): """ For Virt only. Set the number of virtual CPUs to give to the @@ -337,6 +351,7 @@ class Profile(item.Item): 'repos' : self.repos, 'parent' : self.parent, 'depth' : self.depth, + 'tags' : self.tags, 'virt_type' : self.virt_type, 'virt_path' : self.virt_path, 'dhcp_tag' : self.dhcp_tag, @@ -355,6 +370,7 @@ class Profile(item.Item): buf = buf + _("kickstart : %s\n") % self.kickstart buf = buf + _("kernel options : %s\n") % self.kernel_options buf = buf + _("ks metadata : %s\n") % self.ks_meta + buf = buf + _("tags : %s\n") % self.tags buf = buf + _("virt file size : %s\n") % self.virt_file_size buf = buf + _("virt ram : %s\n") % self.virt_ram buf = buf + _("virt type : %s\n") % self.virt_type @@ -377,6 +393,7 @@ class Profile(item.Item): 'virt-file-size' : self.set_virt_file_size, 'virt-ram' : self.set_virt_ram, 'ksmeta' : self.set_ksmeta, + 'tags' : self.set_tags, 'repos' : self.set_repos, 'virt-path' : self.set_virt_path, 'virt-type' : self.set_virt_type, diff --git a/cobbler/item_system.py b/cobbler/item_system.py index 9fa4504..739bfa1 100644 --- a/cobbler/item_system.py +++ b/cobbler/item_system.py @@ -37,6 +37,7 @@ class System(item.Item): self.netboot_enabled = (1, '<<inherit>>')[is_subobject] self.depth = 2 self.kickstart = "<<inherit>>" # use value in profile + self.tags = "<<inherit>>" # "" self.virt_path = "<<inherit>>" # "" self.virt_type = "<<inherit>>" # "" self.server = "<<inherit>>" # "" (or settings) @@ -85,6 +86,7 @@ class System(item.Item): self.ks_meta = self.load_item(seed_data, 'ks_meta', {}) self.depth = self.load_item(seed_data, 'depth', 2) self.kickstart = self.load_item(seed_data, 'kickstart', '<<inherit>>') + self.tags = self.load_item(seed_data, 'tags', '<<inherit>>') self.virt_path = self.load_item(seed_data, 'virt_path', '<<inherit>>') self.virt_type = self.load_item(seed_data, 'virt_type', '<<inherit>>') self.netboot_enabled = self.load_item(seed_data, 'netboot_enabled', 1) @@ -261,6 +263,18 @@ class System(item.Item): return True raise CX(_("invalid profile name")) + def set_tags(self,tags): + """ + A list of tags for the server. Completely arbitrary and user-defined. + """ + if hasattr(tags, 'split'): + tags = [ tag.strip() for tag in tags.split(',') ] + elif not hasattr(tags, '__iter__'): + raise CX(_("Invalid type for tags. Must be a string (comma-delimited) or list.")) + + self.tags = tags + return True + def set_virt_path(self,path): """ Virtual storage location suggestion, can be overriden by koan. @@ -340,6 +354,7 @@ class System(item.Item): 'parent' : self.parent, 'depth' : self.depth, 'kickstart' : self.kickstart, + 'tags' : self.tags, 'virt_type' : self.virt_type, 'virt_path' : self.virt_path, 'interfaces' : self.interfaces, @@ -398,6 +413,7 @@ class System(item.Item): 'hostname' : self.set_hostname, 'kickstart' : self.set_kickstart, 'netboot-enabled' : self.set_netboot_enabled, + 'tags' : self.set_tags, 'virt-path' : self.set_virt_path, 'virt-type' : self.set_virt_type, 'modify-interface' : self.modify_interface, diff --git a/cobbler/item_user.py b/cobbler/item_user.py new file mode 100644 index 0000000..ecf436d --- /dev/null +++ b/cobbler/item_user.py @@ -0,0 +1,103 @@ +""" +A Cobbler user. Just for the webui at the moment, but support for environment +varibles other than REMOTE_USER (e.g. SUDO_USER) may be added in the future. + +Copyright 2006, Red Hat, Inc +Michael DeHaan <mdehaan@xxxxxxxxxx> +Albert Tobey <tobert@xxxxxxxxx> + +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 utils +import item +from cexceptions import * +from rhpl.translate import _, N_, textdomain, utf8 + +class User(item.Item): + + TYPE_NAME = _("user") + COLLECTION_TYPE = "user" + + def make_clone(self): + ds = self.to_datastruct() + cloned = User(self.config) + cloned.from_datastruct(ds) + return cloned + + def clear(self,is_subobject=False): + self.name = None + self.tags = [] + self.superuser = 0 + + def from_datastruct(self,seed_data): + self.name = self.load_item(seed_data, 'name') + self.tags = self.load_item(seed_data, 'tags') + self.superuser = self.load_item(seed_data, 'superuser') + return self + + def set_name(self,name): + """ + The user name. Think system user name. + """ + self.name = name + return True + + def set_tags(self,tags): + """ + A list of tags for the user. + """ + if hasattr(tags, 'split'): + tags = [ tag.strip() for tag in tags.split(',') ] + elif not hasattr(tags, '__iter__'): + raise CX(_("Invalid type for tags. Must be a string (comma-delimited) or list.")) + + self.tags = tags + return True + + def set_superuser(self,superuser): + """ + Specify whether or not a user has full access or not. + """ + if type(superuser) == bool: + self.superuser = superuser + return True + if not str(superuser).lower() in ["yes","y","yup","yeah","1","true"]: + self.superuser = False + else: + self.superuser = True + return True + + def is_valid(self): + """ + A user must have a name to be valid. + """ + if self.name is None: + return False + return True + + def to_datastruct(self): + return { + 'name' : self.name, + 'tags' : self.tags, + 'superuser' : self.superuser + } + + def printable(self): + buf = _("user : %s\n") % self.name + buf = buf + _("tags : %s\n") % ', '.join(self.tags) + buf = buf + _("superuser flag : %s\n") % self.superuser + return buf + + def remote_methods(self): + # set_superuser not allowed remotely, only on command line + return { + 'name' : self.set_name, + 'tags' : self.set_tags + } + diff --git a/cobbler/remote.py b/cobbler/remote.py index 7b92a17..cc449ca 100644 --- a/cobbler/remote.py +++ b/cobbler/remote.py @@ -32,6 +32,7 @@ import item_distro import item_profile import item_system import item_repo +import item_user config_parser = ConfigParser.ConfigParser() auth_conf = open("/etc/cobbler/auth.conf") @@ -182,6 +183,12 @@ class CobblerXMLRPCInterface: """ return self.__get_all("repo",page,results_per_page) + def get_users(self,page=None,results_per_page=None,token=None): + """ + Returns all cobbler users as an array of hashes. + """ + return self.__get_all("user",page,results_per_page) + def __get_specific(self,collection_fn,name,flatten=False): """ Internal function to return a hash representation of a given object if it exists, @@ -220,6 +227,12 @@ class CobblerXMLRPCInterface: """ return self.__get_specific(self.api.repos,name,flatten=flatten) + def get_user(self,name,flatten=False,token=None): + """ + Returns the user named "name" as a hash. + """ + return self.__get_specific(self.api.users,name,flatten=flatten) + def get_distro_as_rendered(self,name,token=None): """ Return the distribution as passed through cobbler's @@ -545,7 +558,15 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): """ self.__validate_token(token) return self.__store_object(item_repo.Repo(self.api._config)) - + + def new_user(self,token): + """ + Creates a new empty user object. See the documentation + for new_distro as it works exactly the same. + """ + self.__validate_token(token) + return self.__store_object(item_user.User(self.api._config)) + def get_distro_handle(self,name,token): """ Given the name of an distro (or other search parameters), return an @@ -582,7 +603,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): def get_repo_handle(self,name,token): """ Given the name of an repo (or other search parameters), return an - object id that can be passed in to modify_repo() or save_pro() + object id that can be passed in to modify_repo() or save_repo() commands. Raises an exception if no object can be matched. """ self.__validate_token(token) @@ -590,6 +611,17 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): found = self.api.repos().find(name) return self.__store_object(found) + def get_user_handle(self,name,token): + """ + Given the name of an user (or other search parameters), return an + object id that can be passed in to modify_user() or save_user() + commands. Raises an exception if no object can be matched. + """ + self.__validate_token(token) + self._refresh() + found = self.api.users().find(name) + return self.__store_object(found) + def save_distro(self,object_id,token): """ Saves a newly created or modified distro object to disk. @@ -622,6 +654,14 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): obj = self.__get_object(object_id) return self.api.repos().add(obj,with_copy=True) + def save_user(self,object_id,token=None): + """ + Saves a newly created or modified user object to disk. + """ + self.__validate_token(token) + obj = self.__get_object(object_id) + return self.api.users().add(obj,with_copy=True) + def __call_method(self, obj, attribute, arg): """ Internal function used by the modify routines. @@ -667,6 +707,15 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): obj = self.__get_object(object_id) return self.__call_method(obj, attribute, arg) + def modify_user(self,object_id,attribute,arg,token): + """ + Allows modification of certain attributes on newly created or + existing user object handle. + """ + self.__validate_token(token) + obj = self.__get_object(object_id) + return self.__call_method(obj, attribute, arg) + def distro_remove(self,name,token): """ Deletes a distro from a collection. Note that this just requires the name @@ -703,6 +752,15 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface): rc = self.api._config.repos().remove(name) return rc + def user_remove(self,name,token): + """ + Deletes a user from a collection. Note that this just requires the name + of the user, not a handle. + """ + self.__validate_token(token) + rc = self.api._config.users().remove(name) + return rc + def sync(self,token): """ Applies changes in Cobbler to the filesystem. @@ -839,6 +897,13 @@ if __name__ == "__main__": remote.modify_repo(repo_id, 'mirror', 'rsync://mirror.example.org/foo', token) remote.save_repo(repo_id, token) + # now add a user + user_id = remote.new_user(token) + remote.modify_user(user_id, 'name', 'testuser', token) + remote.modify_user(user_id, 'tags', 'foo, bar , baz', token) + remote.modify_user(user_id, 'tags', 'barf, foo, bar', token) + remote.save_user(user_id, token) + # now add a profile profile_id = remote.new_profile(token) remote.modify_profile(profile_id, 'name', 'example-profile', token) @@ -857,6 +922,7 @@ if __name__ == "__main__": print remote.get_profiles() print remote.get_systems() print remote.get_repos() + print remote.get_users() print remote.get_system("AA:BB:AA:BB:AA:BB",True) # flattened diff --git a/cobbler/webui/CobblerWeb.py b/cobbler/webui/CobblerWeb.py index 169cd99..b3ad9a9 100644 --- a/cobbler/webui/CobblerWeb.py +++ b/cobbler/webui/CobblerWeb.py @@ -378,7 +378,7 @@ class CobblerWeb(object): def system_save(self,name=None,oldname=None,editmode="edit",profile=None, new_or_edit=None, kopts=None, ksmeta=None, server_override=None, netboot='n', - delete1=None, delete2=None, **args): + tags=None, delete1=None, delete2=None, **args): if not self.__xmlrpc_setup(): return self.xmlrpc_auth_failure() @@ -437,6 +437,8 @@ class CobblerWeb(object): self.remote.modify_system(system, 'netboot-enabled', netboot, self.token) if server_override: self.remote.modify_system(system, 'server', server_override, self.token) + if tags: + self.remote.modify_system(system, 'tags', tags, self.token) for x in range(0,7): interface = "intf%s" % x @@ -543,7 +545,7 @@ class CobblerWeb(object): } ) def profile_save(self,new_or_edit=None,editmode='edit',name=None,oldname=None, - distro=None,kickstart=None,kopts=None, + distro=None,kickstart=None,kopts=None,tags=None, ksmeta=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,**args): @@ -596,6 +598,8 @@ class CobblerWeb(object): self.remote.modify_profile(profile, 'kopts', kopts, self.token) if ksmeta: self.remote.modify_profile(profile, 'ksmeta', ksmeta, self.token) + if tags: + self.remote.modify_profile(profile, 'tags', tags, self.token) if virtfilesize: self.remote.modify_profile(profile, 'virt-file-size', virtfilesize, self.token) if virtram: @@ -734,6 +738,92 @@ class CobblerWeb(object): return self.repo_list() # ------------------------------------------------------------------------ # + # Users + # ------------------------------------------------------------------------ # + + def user_list(self,page=None,limit=None): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + (page, results_per_page, pages) = self.__compute_pagination(page,limit,"user") + users = self.remote.get_users(page,results_per_page) + + if len(users) > 0: + return self.__render( 'user_list.tmpl', { + 'users' : users, + 'pages' : pages, + 'page' : page, + 'results_per_page' : results_per_page + }) + else: + return self.__render('empty.tmpl', {}) + + def user_edit(self, name=None): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + input_user = None + if name is not None: + input_user = self.remote.get_user(name, True) + input_user['superuser'] = self.__truth(input_user['superuser']) + + return self.__render( 'user_edit.tmpl', { + 'user': input_user, + } ) + + def user_save(self,name=None,oldname=None,new_or_edit=None,editmode="edit", + superuser=None,tags=None,delete1=None,delete2=None,**args): + if not self.__xmlrpc_setup(): + return self.xmlrpc_auth_failure() + + if superuser is not None: + return self.error_page("Nice try, chimp. Move along ...") + + # pre-command parameter checking + if name is None and editmode=='edit' 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.user_remove(name,self.token) + except Exception, e: + return self.error_page("could not delete %s, %s" % (name,str(e))) + return self.user_list() + + # grab a reference to the object + if new_or_edit == "edit" and editmode == "edit": + try: + user = self.remote.get_user_handle( name, self.token) + except: + return self.error_page("Failed to lookup user: %s" % name) + else: + user = self.remote.new_user(self.token) + + try: + self.remote.modify_user(user, 'name', name, self.token) + if tags: + self.remote.modify_system(user, 'tags', tags, self.token) + + self.remote.save_user(user, self.token) + + except Exception, e: + log_exc() + return self.error_page("Error while saving user: %s" % str(e)) + + if editmode == "rename" and name != oldname: + try: + self.remote.user_remove(oldname, self.token) + except Exception, e: + return self.error_page("Rename unsuccessful. Object %s was copied instead, and the old copy (%s) still remains. Reason: %s" % (name, oldname, str(e))) + + return self.user_list() + + # ------------------------------------------------------------------------ # # Kickstart files # ------------------------------------------------------------------------ # @@ -831,6 +921,10 @@ class CobblerWeb(object): repo_list.exposed = True repo_save.exposed = True + user_edit.exposed = True + user_list.exposed = True + user_save.exposed = True + settings_view.exposed = True ksfile_edit.exposed = True ksfile_save.exposed = True diff --git a/cobbler/webui/master.py b/cobbler/webui/master.py index 9dd79d5..6d880ba 100644 --- a/cobbler/webui/master.py +++ b/cobbler/webui/master.py @@ -31,12 +31,12 @@ VFFSL=valueFromFrameOrSearchList VFSL=valueFromSearchList VFN=valueForName currentTime=time.time -__CHEETAH_version__ = '2.0rc8' -__CHEETAH_versionTuple__ = (2, 0, 0, 'candidate', 8) -__CHEETAH_genTime__ = 1193780329.9635999 -__CHEETAH_genTimestamp__ = 'Tue Oct 30 17:38:49 2007' +__CHEETAH_version__ = '2.0' +__CHEETAH_versionTuple__ = (2, 0, 0, 'final', 0) +__CHEETAH_genTime__ = 1194218458.5938289 +__CHEETAH_genTimestamp__ = 'Sun Nov 4 15:20:58 2007' __CHEETAH_src__ = 'webui_templates/master.tmpl' -__CHEETAH_srcLastModified__ = 'Mon Oct 29 16:39:12 2007' +__CHEETAH_srcLastModified__ = 'Sun Nov 4 15:20:50 2007' __CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine' if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple: @@ -69,7 +69,7 @@ class master(Template): - ## CHEETAH: generated from #block body at line 53, col 1. + ## CHEETAH: generated from #block body at line 55, col 1. trans = KWS.get("trans") if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)): trans = self.transaction # is None unless self.awake() was called @@ -175,32 +175,40 @@ class master(Template): _v = VFFSL(SL,"base_url",True) # '$base_url' on line 39, col 22 if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 39, col 22. write('''/repo_list" class="menu">Repos</a></li> + <li><a href="''') + _v = VFFSL(SL,"base_url",True) # '$base_url' on line 40, col 22 + if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 40, col 22. + write('''/user_list" class="menu">Users</a></li> <li><hr/></li> <li>ADD</li> <li><a href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 42, col 22 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 42, col 22. - write('''/distro_edit" class="menu">Distro</a></li> - <li><a href="''') _v = VFFSL(SL,"base_url",True) # '$base_url' on line 43, col 22 if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 43, col 22. - write('''/profile_edit" class="menu">Profile</a></li> + write('''/distro_edit" class="menu">Distro</a></li> <li><a href="''') _v = VFFSL(SL,"base_url",True) # '$base_url' on line 44, col 22 if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 44, col 22. - write('''/subprofile_edit" class="menu">Subprofile</a></li> + write('''/profile_edit" class="menu">Profile</a></li> <li><a href="''') _v = VFFSL(SL,"base_url",True) # '$base_url' on line 45, col 22 if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 45, col 22. - write('''/system_edit" class="menu">System</a></li> + write('''/subprofile_edit" class="menu">Subprofile</a></li> <li><a href="''') _v = VFFSL(SL,"base_url",True) # '$base_url' on line 46, col 22 if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 46, col 22. + write('''/system_edit" class="menu">System</a></li> + <li><a href="''') + _v = VFFSL(SL,"base_url",True) # '$base_url' on line 47, col 22 + if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 47, col 22. write('''/repo_edit" class="menu">Repo</a></li> + <li><a href="''') + _v = VFFSL(SL,"base_url",True) # '$base_url' on line 48, col 22 + if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 48, col 22. + write('''/user_edit" class="menu">User</a></li> <li><hr/><br/></li> <li><a class="button sync" href="''') - _v = VFFSL(SL,"base_url",True) # '$base_url' on line 48, col 42 - if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 48, col 42. + _v = VFFSL(SL,"base_url",True) # '$base_url' on line 50, col 42 + if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 50, col 42. write('''/sync">Sync</a></li> </ul> </div> diff --git a/docs/cobbler.pod b/docs/cobbler.pod index 5d53811..84ddd44 100644 --- a/docs/cobbler.pod +++ b/docs/cobbler.pod @@ -108,7 +108,7 @@ The file used for the answer file, regardless of the breed setting, is the value A profile associates a distribution to additional specialized options, such as a kickstart automation file. Profiles are the core unit of provisioning and at least one profile must exist for every distribution to be provisioned. A profile might represent, for instance, a web server or desktop configuration. In this way, profiles define a role to be performed. -B<cobbler profile add --name=string --distro=string [--kickstart=path] [--kopts=string] [--ksmeta=string] [--virt-file-size=gigabytes] [--virt-ram=megabytes] [--virt-type=string] [--virt-cpus=integer] [--virt-path=string] [--virt-bridge=string] [--server-override]> +B<cobbler profile add --name=string --distro=string [--kickstart=path] [--kopts=string] [--ksmeta=string] [--virt-file-size=gigabytes] [--virt-ram=megabytes] [--virt-type=string] [--virt-cpus=integer] [--virt-path=string] [--virt-bridge=string] [--server-override] [--tags=string]> Arguments are as listed for distributions, save for the removal of "arch" and "breed", and with the additions listed below: @@ -130,6 +130,9 @@ If this parameter is not provided, the kickstart file will default to /etc/cobbl When using kickstart files, they can be placed anywhere on the filesystem, but the recommended path is /var/lib/cobbler/kickstarts. +=item tags + +A list of basically arbitrary tags for the system. These are used for authorization in the webui and command line when run under sudo. See USER ACCESS CONTROL / AUTHORIZATION. =item virt-file-size @@ -176,19 +179,19 @@ Example: If profile A has --kopts="x=7 y=2", B inherits from A, and B has --kop Example: If profile B has --virt-ram=256 and A has --virt-ram of 512, profile B will use the value 256. Example: If profile A has a --virt-file-size of 5 and B does not specify a size, B will use the value from A. -=back - =item server-override This parameter should be useful only in select circumstances. If machines are on a subnet that cannot access the cobbler server using the name/IP as configured in the cobbler settings file, use this parameter to override that server name. See also --dhcp-tag for configuring the next server and DHCP informmation of the system if you are also using Cobbler to help manage your DHCP configuration. +=back + =head2 ADDING A SYSTEM System records map a piece of hardware (or a virtual machine) with the cobbler profile to be assigned to run on it. This may be thought of as chosing a role for a specific system. Note that if provisioning via koan and PXE menus alone, it is not required to create system records, though they are useful when system specific customizations are required. One such customization would be defining the MAC address. If there is a specific role inteded for a given machine, system records should be created for it. -B<cobbler system add --name=string --profile=string [--mac=macaddress] [--ip=ipaddress] [--hostname=hostname] [--kopts=string] [--ksmeta=string] [--kickstart=path] [--netboot-enabled=Y/N] [--server-override=string]> +B<cobbler system add --name=string --profile=string [--mac=macaddress] [--ip=ipaddress] [--hostname=hostname] [--kopts=string] [--ksmeta=string] [--kickstart=path] [--netboot-enabled=Y/N] [--server-override=string] [--tags=taglist]> Adds a cobbler System to the configuration. Arguments are specified as per "profile add" with the following changes: @@ -265,7 +268,13 @@ If your system has multiple network interfaces, use --dhcp-tag0=x, --dhcp-tag1=y This is described further on the Cobbler Wiki. -=end +=item --tags + +Assign a comma-delimited list of tags to the server. Tags may contain whitespace inside, but leading/trailing whitespace will be stripped. Tags may be used for per-system authorization and search/grouping in the CLI and webui. See USER ACCESS CONTROL / AUTHORIZATION. + +=item virt-file-size + +=back =head2 ADDING A REPOSITORY TO MIRROR @@ -346,6 +355,27 @@ Specifies what architecture the repository should use. By default the current s =back +=head2 ADDING A USER + +Users can be used to allow people access to some profiles/systems without giving them complete control over cobbler. Of course, superusers have complete control. +B<cobbler user add --name=string [--superuser=Y/N] [--tags=string]> + +=over + +=item name + +Just a normal day-to-day username. It's probably good practice to follow similar constraints to system usernames, though cobbler doesn't force it. + +=item superuser + +Set a user to be a superuser. This means complete access to everything without any checking. + +=item tags + +A list of tags that can be matched against systems/profiles for authorization. They can also be used for list displays and searching. + +=back + =head2 DISPLAYING CONFIGURATION ENTRIES The following commands are usable regardless of how you are using cobbler. @@ -405,6 +435,16 @@ Sync should be run whenever files in /var/lib/cobbler are manually edited (which If using cobbler to manage a DHCP server (see the advanced section of this manpage), sync does need to be run after systems are added to regenerate and reload the DHCP configuration. +=head2 USER ACCESS CONTROL / AUTHORIZATION + +If one of a user's tags appears in the tags of a system or its profile, the user is allowed to edit it. The webui implements this using the REMOTE_USER environment variable, while the CLI implements it using the SUDO_USER variable when run under sudo. + +So, if a user has tags "hamsters, dogs", system1 has tags "dogs, cats", and system2 has tags "cats, mice", the user will be able to edit system1 and not sytem2. + +Systems always inherit tags from profiles/subprofiles, so if system2's profile has "hamsters" in its list, the user with "hamsters, dogs" will be able to edit it. + +To avoid all of this entirely and stick to the old behavior, simply set all users to be superusers. + =head1 EXAMPLES =head2 IMPORT WORKFLOW diff --git a/setup.py b/setup.py index a780c21..3735a11 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,7 @@ if __name__ == "__main__": (etcpath, ['config/rsync.exclude']), (initpath, ['config/cobblerd']), (cobpath, ['config/settings']), + (cobpath, ['config/users']), # backups for upgrades (backpath, []), @@ -147,6 +148,8 @@ if __name__ == "__main__": (wwwtmpl, ['webui_templates/system_edit.tmpl']), (wwwtmpl, ['webui_templates/repo_list.tmpl']), (wwwtmpl, ['webui_templates/repo_edit.tmpl']), + (wwwtmpl, ['webui_templates/user_list.tmpl']), + (wwwtmpl, ['webui_templates/user_edit.tmpl']), # Web UI common templates (wwwtmpl, ['webui_templates/paginate.tmpl']), @@ -183,6 +186,8 @@ if __name__ == "__main__": ("%sadd/system/post" % trigpath, []), ("%sadd/repo/pre" % trigpath, []), ("%sadd/repo/post" % trigpath, []), + ("%sadd/user/pre" % trigpath, []), + ("%sadd/user/post" % trigpath, []), ("%sdelete/distro/pre" % trigpath, []), ("%sdelete/distro/post" % trigpath, []), ("%sdelete/profile/pre" % trigpath, []), @@ -191,7 +196,8 @@ if __name__ == "__main__": ("%sdelete/system/post" % trigpath, []), ("%sdelete/repo/pre" % trigpath, []), ("%sdelete/repo/post" % trigpath, []), - ("%sdelete/repo/post" % trigpath, []), + ("%sdelete/user/pre" % trigpath, []), + ("%sdelete/user/post" % trigpath, []), ("%ssync/pre" % trigpath, []), ("%ssync/post" % trigpath, [ "triggers/restart-services.trigger" ]) ], diff --git a/webui_templates/master.tmpl b/webui_templates/master.tmpl index abd09af..df66360 100644 --- a/webui_templates/master.tmpl +++ b/webui_templates/master.tmpl @@ -37,6 +37,7 @@ <li><a href="$base_url/system_list" class="menu">Systems</a></li> <li><a href="$base_url/ksfile_list" class="menu">Kickstarts</a></li> <li><a href="$base_url/repo_list" class="menu">Repos</a></li> + <li><a href="$base_url/user_list" class="menu">Users</a></li> <li><hr/></li> <li>ADD</li> <li><a href="$base_url/distro_edit" class="menu">Distro</a></li> @@ -44,6 +45,7 @@ <li><a href="$base_url/subprofile_edit" class="menu">Subprofile</a></li> <li><a href="$base_url/system_edit" class="menu">System</a></li> <li><a href="$base_url/repo_edit" class="menu">Repo</a></li> + <li><a href="$base_url/user_edit" class="menu">User</a></li> <li><hr/><br/></li> <li><a class="button sync" href="$base_url/sync">Sync</a></li> </ul> diff --git a/webui_templates/profile_edit.tmpl b/webui_templates/profile_edit.tmpl index 44a7a82..5fc81ce 100644 --- a/webui_templates/profile_edit.tmpl +++ b/webui_templates/profile_edit.tmpl @@ -164,6 +164,21 @@ function disablename(value) <tr> <td> + <label for="tags">Tags</label> + </td> + <td> + <input type="text" size="128" style="width: 150px;" name="tags" id="tags" + #if $profile + #set $t = ', '.join( $profile.tags ) + value="$t" + #end if + /> + <p class="context-tip">A list of tags to associate with this profile.</p> + </td> + </tr> + + <tr> + <td> <label for="virtfilesize">Virt Disk (GB)</label> </td> <td> diff --git a/webui_templates/profile_list.tmpl b/webui_templates/profile_list.tmpl index 047714a..5721f9a 100644 --- a/webui_templates/profile_list.tmpl +++ b/webui_templates/profile_list.tmpl @@ -14,6 +14,7 @@ <tr> <th class="text">Name</th> <th class="text">Parent</th> + <th class="text">Tags</th> <th class="text">Kickstart</th> </tr> </thead> @@ -26,6 +27,7 @@ #set $tr_class = "rowodd" #end if #set $evenodd += 1 + #set $tags = ', '.join($profile.tags) <tr class="$tr_class"> <td> @@ -38,6 +40,7 @@ <a href="$base_url/profile_edit?name=$profile.parent">$profile.parent</A> #end if </td> + <td>$tags</td> <td>$profile.kickstart</td> </tr> #end for diff --git a/webui_templates/system_edit.tmpl b/webui_templates/system_edit.tmpl index 83d5908..36fd511 100644 --- a/webui_templates/system_edit.tmpl +++ b/webui_templates/system_edit.tmpl @@ -208,6 +208,20 @@ function page_onload() { </td> </tr> + <tr> + <td> + <label for="tags">Tags</label> + </td> + <td> + <input type="text" size="128" style="width: 150px;" name="tags" id="tags" + #if $system + #set $t = ', '.join( $system.tags ) + value="$t" + #end if + /> + <p class="context-tip">A list of tags to associate with this server.</p> + </td> + </tr> ## ====================================== start of looping through interfaces diff --git a/webui_templates/system_list.tmpl b/webui_templates/system_list.tmpl index 2e5833a..0820140 100644 --- a/webui_templates/system_list.tmpl +++ b/webui_templates/system_list.tmpl @@ -2,10 +2,12 @@ #block body + #if $pages > 0 ## ==== BEGIN PAGE NAVIGATION ==== #set global what="system" #include "/usr/share/cobbler/webui_templates/paginate.tmpl" ## ==== END PAGE NAVIGATION ==== + #end if <table class="sortable"> @@ -14,6 +16,7 @@ <tr> <th class="text">Name</th> <th class="text">Profile</th> + <th class="text">Tags</th> ## FIXME: how to handle for multiple interface listing? <th class="text">MAC</th> ## <th class="text">IP</th> ## <th class="text">Hostname</th> @@ -28,6 +31,7 @@ #set $tr_class = "rowodd" #end if #set $evenodd += 1 + #set $tags = ', '.join($system.tags) <tr class="$tr_class"> <td> @@ -36,6 +40,7 @@ <td> <a href="$base_url/profile_edit?name=${system.profile}">${system.profile}</a> </td> + <td>$tags</td> ## <td> ${system.mac_address} </td> ## <td> ${system.ip_address} </td> ## <td> ${system.hostname} </td> diff --git a/webui_templates/user_edit.tmpl b/webui_templates/user_edit.tmpl new file mode 100644 index 0000000..fc75509 --- /dev/null +++ b/webui_templates/user_edit.tmpl @@ -0,0 +1,99 @@ +#extends cobbler.webui.master +#block body + +#if $user +<script language="javascript"> +function disablename(value) +{ + document.getElementById("name").disabled=value; + if (value) { + document.getElementById("name").value = "$user.name"; + } +} +</script> +#end if + +<form method="post" action="$base_url/user_save"> +<fieldset id="cform"> + #if $user + <legend>Editing User</legend> + <input type="hidden" name="new_or_edit" value="edit"/> + <input type="hidden" name="oldname" value="$user.name"/> + #else + <legend>Adding a User</legend> + <input type="hidden" name="new_or_edit" value="new"/> + #end if + + <table border=0> + + <tr> + <td> + <label for="name">User Name</label> + </td> + <td> + #if $user + <input type="text" size="128" style="width: 150px;" name="name" id="name" disabled="true" + #else + <input type="text" size="128" style="width: 150px;" name="name" id="name" + #end if + #if $user + value="$user.name" + #end if + /> + <p class="context-tip">Example: joe_smith</p> + </td> + </tr> + + #if $user + <tr> + <td> + <label for="mode">Edit Mode</label> + </td> + <td> + <input type="radio" name="editmode" value="edit" checked onclick="javascript:disablename(true)">Edit + <input type="radio" name="editmode" value="rename" onclick="javascript:disablename(false)">Rename + Edit + <input type="radio" name="editmode" value="copy" onclick="javascript:disablename(false)">Copy + Edit + <p class="context-tip">How do you want to modify this object?</p> + </td> + </tr> + #end if + + <tr> + <td> + <label for="tags">Tags</label> + </td> + <td> + <input type="text" size="128" style="width: 150px;" name="tags" id="tags" + #if $user + #set $t = ', '.join( $user.tags ) + value="$t" + #end if + /> + <p class="context-tip">A list of tags to associate with this user.</p> + </td> + </tr> + + #if $user + <tr> + <td> + <label for="delete">Delete</label> + </td> + <td> + <input type="checkbox" name="delete1" value="delete1">Yes + <input type="checkbox" name="delete2" value="delete2">Really + <p class="context-tip">Check both buttons and click save to delete this object</p> + </td> + </tr> + #end if + + <tr> + <td> + </td> + <td> + <input type="submit" name="submit" value="Save"/> + <input type="reset" name="reset" value="Reset"/> + </td> + </tr> + +</fieldset> +#end block body diff --git a/webui_templates/user_list.tmpl b/webui_templates/user_list.tmpl new file mode 100644 index 0000000..bfd4462 --- /dev/null +++ b/webui_templates/user_list.tmpl @@ -0,0 +1,41 @@ +#extends cobbler.webui.master + +#block body + + ## ==== BEGIN PAGE NAVIGATION ==== + #set global what="user" + #include "/usr/share/cobbler/webui_templates/paginate.tmpl" + ## ==== END PAGE NAVIGATION ==== + + +<table class="sortable"> + <thead> + <caption>Cobbler Users</caption> + <tr> + <th class="text">Name</th> + <th class="text">Tags</th> + <th class="text">Is Superuser?</th> + </tr> + </thead> + <tbody> + #set $evenodd = 1 + #for $user in $users + #if $evenodd % 2 == 0 + #set $tr_class = "roweven" + #else + #set $tr_class = "rowodd" + #end if + #set $evenodd += 1 + #set $tags = ', '.join($user.tags) + + <tr class="$tr_class"> + <td> + <a href="$base_url/user_edit?name=$user.name">$user.name</a> + </td> + <td>$tags</td> + <td>$user.superuser</td> + </tr> + #end for + </tbody> +</table> +#end block body -- 1.5.3.4
_______________________________________________ et-mgmt-tools mailing list et-mgmt-tools@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/et-mgmt-tools