summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AUTHORS3
-rw-r--r--CHANGELOG15
-rw-r--r--MANIFEST.in2
-rw-r--r--Makefile2
-rw-r--r--cobbler.spec13
-rw-r--r--cobbler/action_check.py16
-rw-r--r--cobbler/action_litesync.py8
-rw-r--r--cobbler/action_reposync.py12
-rw-r--r--cobbler/action_sync.py35
-rw-r--r--cobbler/api.py16
-rw-r--r--cobbler/cobblerd.py4
-rw-r--r--cobbler/collection.py59
-rw-r--r--cobbler/commands.py40
-rw-r--r--cobbler/demo_connect.py21
-rw-r--r--cobbler/item.py11
-rw-r--r--cobbler/item_distro.py11
-rw-r--r--cobbler/item_profile.py12
-rw-r--r--cobbler/item_repo.py10
-rw-r--r--cobbler/item_system.py10
-rw-r--r--cobbler/modules/authn_ldap.py113
-rw-r--r--cobbler/modules/authz_configfile.py64
-rw-r--r--cobbler/modules/authz_ownership.py178
-rw-r--r--cobbler/modules/cli_distro.py9
-rw-r--r--cobbler/modules/cli_profile.py10
-rw-r--r--cobbler/modules/cli_repo.py9
-rw-r--r--cobbler/modules/cli_system.py10
-rw-r--r--cobbler/remote.py121
-rw-r--r--cobbler/settings.py17
-rw-r--r--cobbler/utils.py68
-rw-r--r--cobbler/webui/CobblerWeb.py104
-rw-r--r--cobbler/webui/master.py4
-rw-r--r--config/modules.conf2
-rw-r--r--config/settings12
-rw-r--r--config/users.conf28
-rw-r--r--docs/cobbler.pod4
-rw-r--r--docs/wui.html25
-rw-r--r--setup.py5
-rw-r--r--tests/tests.py286
-rw-r--r--webui_templates/distro_edit.tmpl30
-rw-r--r--webui_templates/enoaccess.tmpl12
-rw-r--r--webui_templates/ksfile_edit.tmpl33
-rw-r--r--webui_templates/ksfile_view.tmpl6
-rw-r--r--webui_templates/profile_edit.tmpl32
-rw-r--r--webui_templates/repo_edit.tmpl38
-rw-r--r--webui_templates/system_edit.tmpl55
45 files changed, 1407 insertions, 168 deletions
diff --git a/AUTHORS b/AUTHORS
index c6d954c..3e4dcef 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -7,12 +7,15 @@ Cobbler is written & maintained by:
Patches and other contributions from:
+ David Brown <dmlb2000@gmail.com>
James Bowes <jbowes@redhat.com>
C. Daniel Chase <dan@cdchase.com>
Máirín Duffy <duffy@redhat.com>
Tru Huynh <tru@pasteur.fr>
Matt Hyclak <hyclak@math.ohiou.edu>
+ Pablo Iranzo Gómez <pablo.iranzo@redhat.com>
Mihai Ibanescu <mihai.ibanescu@gmail.com>
+ Vito Laurenza <vitolaurenza@gmail.com>
Adrian Likins <alikins@redhat.com>
David Lutterkort <dlutter@redhat.com>
Lester M. <needwork@gmail.com>
diff --git a/CHANGELOG b/CHANGELOG
index c3ea738..2bd50fa 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,12 +1,25 @@
Cobbler CHANGELOG
(all entries mdehaan@redhat.com unless noted otherwise)
-* Tue Apr 08 2008 - 0.8.3
+- Mon Mar 10 2008 - 0.9.0
+- patch to allow yumopts to override gpgcheck
+- applied patch to send hostname from ISC
+- added patch to allow --kopts/--ksmeta items to be cleared with --kopts=delete
+- tftpboot location is now inferred from xinetd config (added for F9 compat)
+- added authn_ldap and stub for authz_configfile
+- authz_configfile allows filtering ldap/other users by config file
+- WebUI now has checkbox on distro/profile for deleting child objects
+- cli has different semantics between "add" and "edit" now for safety reasons
+- cobbler wants to keep IPs/MACs unique now in configuration (can be disabled)
+- added --clobber option to allow add to overwrite existing objects (for scripts)
+
+- ??? - 0.8.3
- Make createrepo get run for local cobbler reposync invocations as needed
- fix WebUI documentation URL
- fix bug in /etc/cobbler/modules.conf regarding pluggable authn/z
- fix default flags for yumdownloader
- fix for RHEL 4u6 DVD/tree import x86_64 arch detection
+- fix for dnsmasq template file host config path
- fix dnsmasq template to point at the correct hosts file
- force all names to be alphanumeric
diff --git a/MANIFEST.in b/MANIFEST.in
index 357cdcc..4c8ed20 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -10,6 +10,7 @@ include config/modules.conf
include config/auth.conf
include config/settings
include config/users.digest
+include config/users.conf
recursive-include templates *.template
recursive-include kickstarts *.ks
include docs/cobbler.1.gz
@@ -21,7 +22,6 @@ include scripts/index.py
include scripts/cobblerd
include scripts/findks.cgi
include scripts/nopxe.cgi
-include scripts/webui.cgi
include scripts/gateway.py
include scripts/post_install_trigger.cgi
include scripts/cobbler_auth_help
diff --git a/Makefile b/Makefile
index c40b4ed..e742e39 100644
--- a/Makefile
+++ b/Makefile
@@ -38,10 +38,12 @@ install: clean manpage
devinstall:
cp /var/lib/cobbler/settings /tmp/cobbler_settings
cp /etc/cobbler/modules.conf /tmp/cobbler_modules.conf
+ cp /etc/cobbler/users.conf /tmp/cobbler_users.conf
-cp /etc/cobbler/users.digest /tmp/cobbler_users.digest
make install
cp /tmp/cobbler_settings /var/lib/cobbler/settings
cp /tmp/cobbler_modules.conf /etc/cobbler/modules.conf
+ cp /tmp/cobbler_users.conf /etc/cobbler/users.conf
-cp /tmp/cobbler_users.digest /etc/cobbler/users.digest
find /var/lib/cobbler/triggers | xargs chmod +x
chown -R apache /var/www/cobbler
diff --git a/cobbler.spec b/cobbler.spec
index a84d634..070bff3 100644
--- a/cobbler.spec
+++ b/cobbler.spec
@@ -2,8 +2,8 @@
Summary: Boot server configurator
Name: cobbler
AutoReq: no
-Version: 0.8.3
-Release: 2%{?dist}
+Version: 0.9.0
+Release: 1%{?dist}
Source0: %{name}-%{version}.tar.gz
License: GPLv2+
Group: Applications/System
@@ -128,6 +128,7 @@ test "x$RPM_BUILD_ROOT" != "x" && rm -rf $RPM_BUILD_ROOT
%config(noreplace) /etc/cobbler/rsync.exclude
%config(noreplace) /etc/logrotate.d/cobblerd_rotate
%config(noreplace) /etc/cobbler/modules.conf
+%config(noreplace) /etc/cobbler/users.conf
%dir %{python_sitelib}/cobbler
%dir %{python_sitelib}/cobbler/yaml
%dir %{python_sitelib}/cobbler/modules
@@ -190,7 +191,15 @@ test "x$RPM_BUILD_ROOT" != "x" && rm -rf $RPM_BUILD_ROOT
%changelog
+<<<<<<< HEAD:cobbler.spec
+* Tue Apr 08 2008 Michael DeHaan <mdehaan@redhat.com> - 0.9.0-1
+- Upstream changes (see CHANGELOG)
+- packaged /etc/cobbler/users.conf
+
+* Tue Apr 08 2008 Michael DeHaan <mdehaan@redhat.com> - 0.8.3-1
+=======
* Tue Apr 08 2008 Michael DeHaan <mdehaan@redhat.com> - 0.8.3-2
+>>>>>>> master:cobbler.spec
- Upstream changes (see CHANGELOG)
* Fri Mar 07 2008 Michael DeHaan <mdehaan@redhat.com> - 0.8.2-1
diff --git a/cobbler/action_check.py b/cobbler/action_check.py
index f7bc9d9..1fb4734 100644
--- a/cobbler/action_check.py
+++ b/cobbler/action_check.py
@@ -17,6 +17,7 @@ import os
import re
import sub_process
import action_sync
+import utils
from rhpl.translate import _, N_, textdomain, utf8
class BootCheck:
@@ -140,8 +141,9 @@ class BootCheck:
"""
Check if cobbler.conf's tftpboot directory exists
"""
- if not os.path.exists(self.settings.tftpboot):
- status.append(_("please create directory: %(dirname)s") % { "dirname" : self.settings.tftpboot })
+ bootloc = utils.tftpboot_location()
+ if not os.path.exists(bootloc):
+ status.append(_("please create directory: %(dirname)s") % { "dirname" : bootloc })
def check_tftpd_conf(self,status):
@@ -152,17 +154,15 @@ class BootCheck:
if os.path.exists(self.settings.tftpd_conf):
f = open(self.settings.tftpd_conf)
re_disable = re.compile(r'disable.*=.*yes')
- found_bootdir = False
for line in f.readlines():
if re_disable.search(line):
status.append(_("change 'disable' to 'no' in %(file)s") % { "file" : self.settings.tftpd_conf })
- if line.find("-s %s" % self.settings.tftpboot) != -1:
- found_bootdir = True
- if not found_bootdir:
- status.append(_("change 'server_args' to '-s %(args)s' in %(file)s") % { "file" : "/etc/xinetd.d/tftp", "args" : self.settings.tftpboot })
-
else:
status.append(_("file %(file)s does not exist") % { "file" : self.settings.tftpd_conf })
+
+ bootloc = utils.tftpboot_location()
+ if not os.path.exists(bootloc):
+ status.append(_("directory needs to be created: %s" % bootloc))
def check_dhcpd_conf(self,status):
diff --git a/cobbler/action_litesync.py b/cobbler/action_litesync.py
index 457f3af..9200a3e 100644
--- a/cobbler/action_litesync.py
+++ b/cobbler/action_litesync.py
@@ -66,12 +66,13 @@ class BootLiteSync:
self.add_single_profile(k.name)
def remove_single_distro(self, name):
+ bootloc = utils.tftpboot_location()
# delete distro YAML file in distros/$name in webdir
self.sync.rmfile(os.path.join(self.settings.webdir, "distros", name))
# delete contents of images/$name directory in webdir
self.sync.rmtree(os.path.join(self.settings.webdir, "images", name))
# delete contents of images/$name in tftpboot
- self.sync.rmtree(os.path.join(self.settings.tftpboot, "images", name))
+ self.sync.rmtree(os.path.join(bootloc, "images", name))
# delete potential symlink to tree in webdir/links
self.sync.rmfile(os.path.join(self.settings.webdir, "links", name))
@@ -127,6 +128,7 @@ class BootLiteSync:
self.sync.retemplate_yum_repos(system,False)
def remove_single_system(self, name):
+ bootloc = utils.tftpboot_location()
system_record = self.systems.find(name=name)
# rebuild system_list file in webdir
self.sync.write_listings()
@@ -152,7 +154,7 @@ class BootLiteSync:
if distro is not None and distro in [ "ia64", "IA64"]:
itanic = True
if not itanic:
- self.sync.rmfile(os.path.join(self.settings.tftpboot, "pxelinux.cfg", filename))
+ self.sync.rmfile(os.path.join(bootloc, "pxelinux.cfg", filename))
else:
- self.sync.rmfile(os.path.join(self.settings.tftpboot, filename))
+ self.sync.rmfile(os.path.join(bootloc, filename))
diff --git a/cobbler/action_reposync.py b/cobbler/action_reposync.py
index 32d38bd..c86e0be 100644
--- a/cobbler/action_reposync.py
+++ b/cobbler/action_reposync.py
@@ -235,6 +235,8 @@ class RepoSync:
config_file = open(fname, "w+")
config_file.write("[%s]\n" % repo.name)
config_file.write("name=%s\n" % repo.name)
+ optenabled = False
+ optgpgcheck = False
if output:
line = "baseurl=http://${server}/cobbler/repo_mirror/%s\n" % (repo.name)
config_file.write(line)
@@ -242,15 +244,21 @@ class RepoSync:
# add them to the file
for x in repo.yumopts:
config_file.write("%s=%s\n" % (x, repo.yumopts[x]))
+ if x == "enabled":
+ optenabled = True
+ if x == "gpgcheck":
+ optgpgcheck = True
else:
line = "baseurl=%s\n" % repo.mirror
http_server = "%s:%s" % (self.settings.server, self.settings.http_port)
line = line.replace("@@server@@",http_server)
config_file.write(line)
- config_file.write("enabled=1\n")
+ if not optenabled:
+ config_file.write("enabled=1\n")
config_file.write("priority=%s\n" % repo.priority)
# FIXME: potentially might want a way to turn this on/off on a per-repo basis
- config_file.write("gpgcheck=0\n")
+ if not optgpgcheck:
+ config_file.write("gpgcheck=0\n")
config_file.close()
return fname
diff --git a/cobbler/action_sync.py b/cobbler/action_sync.py
index 139066e..8a0eadf 100644
--- a/cobbler/action_sync.py
+++ b/cobbler/action_sync.py
@@ -30,6 +30,7 @@ import errno
import item_distro
import item_profile
+import item_repo
import item_system
from Cheetah.Template import Template
@@ -56,14 +57,15 @@ class BootSync:
self.repos = config.repos()
self.blend_cache = {}
self.load_snippet_cache()
+ self.bootloc = utils.tftpboot_location()
def run(self):
"""
Syncs the current configuration file with the config tree.
Using the Check().run_ functions previously is recommended
"""
- if not os.path.exists(self.settings.tftpboot):
- raise CX(_("cannot find directory: %s") % self.settings.tftpboot)
+ if not os.path.exists(self.bootloc):
+ raise CX(_("cannot find directory: %s") % self.bootloc)
# run pre-triggers...
utils.run_triggers(None, "/var/lib/cobbler/triggers/sync/pre/*")
@@ -103,9 +105,9 @@ class BootSync:
for loader in self.settings.bootloaders.keys():
path = self.settings.bootloaders[loader]
newname = os.path.basename(path)
- destpath = os.path.join(self.settings.tftpboot, newname)
+ destpath = os.path.join(self.bootloc, newname)
self.copyfile(path, destpath)
- self.copyfile("/var/lib/cobbler/menu.c32", os.path.join(self.settings.tftpboot, "menu.c32"))
+ self.copyfile("/var/lib/cobbler/menu.c32", os.path.join(self.bootloc, "menu.c32"))
def write_dhcp_file(self):
"""
@@ -163,6 +165,8 @@ class BootSync:
# the label the entry after the hostname if possible
if host is not None and host != "":
systxt = "\nhost %s {\n" % host
+ if self.settings.isc_set_host_name:
+ systxt = systxt + " option host-name = %s;\n" % host
else:
systxt = "\nhost generic%d {\n" % counter
@@ -275,8 +279,8 @@ class BootSync:
if x in ["kickstarts","kickstarts_sys","images","systems","distros","profiles","repo_profile","repo_system"]:
# clean out directory contents
self.rmtree_contents(path)
- self.rmtree_contents(os.path.join(self.settings.tftpboot, "pxelinux.cfg"))
- self.rmtree_contents(os.path.join(self.settings.tftpboot, "images"))
+ self.rmtree_contents(os.path.join(self.bootloc, "pxelinux.cfg"))
+ self.rmtree_contents(os.path.join(self.bootloc, "images"))
def copy_distros(self):
"""
@@ -294,7 +298,7 @@ class BootSync:
self.copy_single_distro_files(d)
def copy_single_distro_files(self, d):
- for dirtree in [self.settings.tftpboot, self.settings.webdir]:
+ for dirtree in [self.bootloc, self.settings.webdir]:
distros = os.path.join(dirtree, "images")
distro_dir = os.path.join(distros,d.name)
self.mkdir(distro_dir)
@@ -428,13 +432,16 @@ class BootSync:
buf = ""
blended = utils.blender(self.api, False, obj, self.blend_cache)
-
configs = self.get_repo_filenames(obj,is_profile)
+ repos = self.repos
+
for c in configs:
name = c.split("/")[-1].replace(".repo","")
(is_core, baseurl) = self.analyze_repo_config(c)
- buf = buf + "repo --name=%s --baseurl=%s\n" % (name, baseurl)
-
+ for repo in repos:
+ if repo.name == name:
+ if not repo.yumopts.has_key('enabled') or repo.yumopts['enabled'] == '1':
+ buf = buf + "repo --name=%s --baseurl=%s\n" % (name, baseurl)
return buf
def analyze_repo_config(self, filename):
@@ -757,7 +764,7 @@ class BootSync:
# for tftp only ...
if distro.arch in [ "x86", "x86_64", "standard"]:
# pxelinux wants a file named $name under pxelinux.cfg
- f2 = os.path.join(self.settings.tftpboot, "pxelinux.cfg", f1)
+ f2 = os.path.join(self.bootloc, "pxelinux.cfg", f1)
if distro.arch == "ia64":
# elilo expects files to be named "$name.conf" in the root
# and can not do files based on the MAC address
@@ -765,7 +772,7 @@ class BootSync:
print _("Warning: Itanium system object (%s) needs an IP address to PXE") % system.name
filename = "%s.conf" % utils.get_config_filename(system,interface=name)
- f2 = os.path.join(self.settings.tftpboot, filename)
+ f2 = os.path.join(self.bootloc, filename)
f3 = os.path.join(self.settings.webdir, "systems", f1)
@@ -792,7 +799,7 @@ class BootSync:
if default is not None:
return
- fname = os.path.join(self.settings.tftpboot, "pxelinux.cfg", "default")
+ fname = os.path.join(self.bootloc, "pxelinux.cfg", "default")
# read the default template file
template_src = open("/etc/cobbler/pxedefault.template")
@@ -814,7 +821,7 @@ class BootSync:
# save the template.
metadata = { "pxe_menu_items" : pxe_menu_items }
- outfile = os.path.join(self.settings.tftpboot, "pxelinux.cfg", "default")
+ outfile = os.path.join(self.bootloc, "pxelinux.cfg", "default")
self.apply_template(template_data, metadata, outfile)
template_src.close()
diff --git a/cobbler/api.py b/cobbler/api.py
index 13fc5f3..54c315b 100644
--- a/cobbler/api.py
+++ b/cobbler/api.py
@@ -221,21 +221,21 @@ class BootAPI:
self.log("new_repo",[is_subobject])
return self._config.new_repo(is_subobject=is_subobject)
- def add_distro(self, ref):
+ def add_distro(self, ref, check_for_duplicate_names=False):
self.log("add_distro",[ref.name])
- return self._config.distros().add(ref,save=True)
+ return self._config.distros().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names)
- def add_profile(self, ref):
+ def add_profile(self, ref, check_for_duplicate_names=False):
self.log("add_profile",[ref.name])
- return self._config.profiles().add(ref,save=True)
+ return self._config.profiles().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names)
- def add_system(self,ref):
+ def add_system(self, ref, check_for_duplicate_names=False, check_for_duplicate_netinfo=False):
self.log("add_system",[ref.name])
- return self._config.systems().add(ref,save=True)
+ return self._config.systems().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names,check_for_duplicate_netinfo=check_for_duplicate_netinfo)
- def add_repo(self,ref):
+ def add_repo(self, ref, check_for_duplicate_names=False):
self.log("add_repo",[ref.name])
- return self._config.repos().add(ref,save=True)
+ return self._config.repos().add(ref,save=True,check_for_duplicate_names=check_for_duplicate_names)
def find_distro(self, name=None, return_list=False, **kargs):
return self._config.distros().find(name=name, return_list=return_list, **kargs)
diff --git a/cobbler/cobblerd.py b/cobbler/cobblerd.py
index 3c06723..5640aec 100644
--- a/cobbler/cobblerd.py
+++ b/cobbler/cobblerd.py
@@ -108,7 +108,7 @@ def do_xmlrpc(bootapi, settings, port, logger):
# This is the simple XMLRPC API we provide to koan and other
# apps that do not need to manage Cobbler's config
- xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerXMLRPCInterface,True)
+ xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerXMLRPCInterface,False)
server = remote.CobblerXMLRPCServer(('', port))
server.logRequests = 0 # don't print stuff
@@ -124,7 +124,7 @@ def do_xmlrpc(bootapi, settings, port, logger):
def do_xmlrpc_rw(bootapi,settings,port,logger):
- xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerReadWriteXMLRPCInterface,False)
+ xinterface = remote.ProxiedXMLRPCInterface(bootapi,logger,remote.CobblerReadWriteXMLRPCInterface,True)
server = remote.CobblerReadWriteXMLRPCServer(('127.0.0.1', port))
server.logRequests = 0 # don't print stuff
logger.debug("XMLRPC (read-write variant) running on %s" % port)
diff --git a/cobbler/collection.py b/cobbler/collection.py
index 339a4b2..e63c3ca 100644
--- a/cobbler/collection.py
+++ b/cobbler/collection.py
@@ -35,7 +35,8 @@ class Collection(serializable.Serializable):
"""
self.config = config
self.clear()
- self.log_func = self.config.api.log
+ self.api = self.config.api
+ self.log_func = self.api.log
self.lite_sync = None
def factory_produce(self,config,seed_data):
@@ -125,10 +126,10 @@ class Collection(serializable.Serializable):
k.set_parent(newname)
else:
k.set_distro(newname)
- self.config.api.profiles().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers)
+ self.api.profiles().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers)
elif k.COLLECTION_TYPE == "system":
k.set_profile(newname)
- self.config.api.systems().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers)
+ self.api.systems().add(k, save=True, with_sync=with_sync, with_triggers=with_triggers)
elif k.COLLECTION_TYPE == "repo":
raise CX(_("internal error, not expected to have repo child objects"))
else:
@@ -139,7 +140,7 @@ class Collection(serializable.Serializable):
return True
- def add(self,ref,save=False,with_copy=False,with_triggers=True,with_sync=True,quick_pxe_update=False):
+ def add(self,ref,save=False,with_copy=False,with_triggers=True,with_sync=True,quick_pxe_update=False,check_for_duplicate_names=False,check_for_duplicate_netinfo=False):
"""
Add an object to the collection, if it's valid. Returns True
if the object was added to the collection. Returns False if the
@@ -167,6 +168,11 @@ class Collection(serializable.Serializable):
# if not saving the object, you can't run these features
with_triggers = False
with_sync = False
+
+ # Avoid adding objects to the collection
+ # if an object of the same/ip/mac already exists.
+ self.__duplication_checks(ref,check_for_duplicate_names,check_for_duplicate_netinfo)
+
if ref is None or not ref.is_valid():
raise CX(_("insufficient or invalid arguments supplied"))
@@ -220,6 +226,51 @@ class Collection(serializable.Serializable):
def _run_triggers(self,ref,globber):
return utils.run_triggers(ref,globber)
+ def __duplication_checks(self,ref,check_for_duplicate_names,check_for_duplicate_netinfo):
+ """
+ Prevents adding objects with the same name.
+ Prevents adding or editing to provide the same IP, or MAC.
+ Enforcement is based on whether the API caller requests it.
+ """
+
+ # always protect against duplicate names
+ if check_for_duplicate_names:
+ match = None
+ if isinstance(ref, item_system.System):
+ match = self.api.find_system(ref.name)
+ elif isinstance(ref, item_profile.Profile):
+ match = self.api.find_profile(ref.name)
+ elif isinstance(ref, item_distro.Distro):
+ match = self.api.find_distro(ref.name)
+ elif isinstance(ref, item_repo.Repo):
+ match = self.api.find_repo(ref.name)
+
+ if match:
+ raise CX(_("An object already exists with that name. Try 'edit'?"))
+
+ # the duplicate mac/ip checks can be disabled.
+ if not check_for_duplicate_netinfo:
+ return
+
+ if isinstance(ref, item_system.System):
+ for (name, intf) in ref.interfaces.iteritems():
+ match_ip = []
+ match_mac = []
+ input_mac = intf["mac_address"]
+ input_ip = intf["ip_address"]
+ if not self.api.settings().allow_duplicate_macs and input_mac is not None and input_mac != "":
+ match_mac = self.api.find_system(mac_address=input_mac,return_list=True)
+ if not self.api.settings().allow_duplicate_ips and input_ip is not None and input_ip != "":
+ match_ip = self.api.find_system(ip_address=input_ip,return_list=True)
+ # it's ok to conflict with your own net info.
+
+ for x in match_mac:
+ if x.name != ref.name:
+ raise CX(_("Can't save system %s. The MAC address (%s) is already used by system %s (%s)") % (ref.name, intf["mac_address"], x.name, name))
+ for x in match_ip:
+ if x.name != ref.name:
+ raise CX(_("Can't save system %s. The IP address (%s) is already used by system %s (%s)") % (ref.name, intf["ip_address"], x.name, name))
+
def printable(self):
"""
Creates a printable representation of the collection suitable
diff --git a/cobbler/commands.py b/cobbler/commands.py
index d97a6e1..2dc627b 100644
--- a/cobbler/commands.py
+++ b/cobbler/commands.py
@@ -241,6 +241,10 @@ class CobblerFunction:
Boilerplate for objects that offer add/edit/delete/remove/copy functionality.
"""
+ clobber = False
+ if "add" in self.args:
+ clobber = options.clobber
+
if "copy" in self.args: # or "rename" in self.args:
if self.options.newname:
obj = obj.make_clone()
@@ -251,10 +255,44 @@ class CobblerFunction:
opt_sync = not options.nosync
opt_triggers = not options.notriggers
+ # ** WARNING: COMPLICATED **
+ # what operation we call depends on what type of object we are editing
+ # and what the operation is. The details behind this is that the
+ # add operation has special semantics around adding objects that might
+ # clobber other objects, and we don't want that to happen. Edit
+ # does not have to check for named clobbering but still needs
+ # to check for IP/MAC clobbering in some scenarios (FIXME).
+ # this is all enforced by collections.py though we need to make
+ # the apppropriate call to add to invoke the safety code in the right
+ # places -- and not in places where the safety code will generate
+ # errors under legit circumstances.
+
if not ("rename" in self.args):
- rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers)
+ if "add" in self.args:
+ if obj.COLLECTION_TYPE == "system":
+ # duplicate names and netinfo are both bad.
+ if not clobber:
+ rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=True, check_for_duplicate_netinfo=True)
+ else:
+ rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=False, check_for_duplicate_netinfo=True)
+ else:
+ # duplicate names are bad
+ if not clobber:
+ rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=True, check_for_duplicate_netinfo=False)
+ else:
+ rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers, check_for_duplicate_names=False, check_for_duplicate_netinfo=False)
+ else:
+ # editing or copying (but not renaming), so duplicate netinfo
+ # CAN be bad, duplicate names are already handled, though
+ # we need to clean up checks around duplicate netinfo here
+ # (FIXME) so they are made and work.
+ rc = collect_fn().add(obj, save=True, with_sync=opt_sync, with_triggers=opt_triggers)
+
else:
+ # we are renaming here, so duplicate netinfo checks also
+ # need to be made.(FIXME)
rc = collect_fn().rename(obj, self.options.newname)
+
return rc
def reporting_sorter(self, a, b):
diff --git a/cobbler/demo_connect.py b/cobbler/demo_connect.py
index 0fa058b..94fa390 100644
--- a/cobbler/demo_connect.py
+++ b/cobbler/demo_connect.py
@@ -11,12 +11,25 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
"""
-from server.xmlrpcclient import ServerProxy
+from xmlrpclib import ServerProxy
+import optparse
if __name__ == "__main__":
- sp = ServerProxy("httpu:///var/lib/cobbler/sock")
- print sp.login("<system>","")
-
+ p = optparse.OptionParser()
+ p.add_option("-u","--user",dest="user",default="test")
+ p.add_option("-p","--pass",dest="password",default="test")
+
+ # NOTE: if you've changed your xmlrpc_rw port or
+ # disabled xmlrpc_rw this test probably won't work
+
+ sp = ServerProxy("http://127.0.0.1:25152")
+ (options, args) = p.parse_args()
+ print "- trying to login with user=%s" % options.user
+ token = sp.login(options.user,options.password)
+ print "- token: %s" % token
+ print "- authenticated ok, now seeing if user is authorized"
+ check = sp.check_access(token,"imaginary_method_name")
+ print "- access ok? %s" % check
diff --git a/cobbler/item.py b/cobbler/item.py
index 78c8041..454b704 100644
--- a/cobbler/item.py
+++ b/cobbler/item.py
@@ -51,7 +51,6 @@ class Item(serializable.Serializable):
self.clear(is_subobject) # reset behavior differs for inheritance cases
self.parent = '' # all objects by default are not subobjects
self.children = {} # caching for performance reasons, not serialized
-
self.log_func = self.config.api.log
def clear(self):
@@ -122,6 +121,16 @@ class Item(serializable.Serializable):
self.name = name
return True
+ def set_owners(self,data):
+ """
+ The owners field is a comment unless using an authz module that pays attention to it,
+ like authz_ownership, which ships with Cobbler but is off by default. Consult the Wiki
+ docs for more info on CustomizableAuthorization.
+ """
+ owners = utils.input_string_or_list(data)
+ self.owners = owners
+ return True
+
def set_kernel_options(self,options):
"""
Kernel options are a space delimited list,
diff --git a/cobbler/item_distro.py b/cobbler/item_distro.py
index f52ad0b..98eaae7 100644
--- a/cobbler/item_distro.py
+++ b/cobbler/item_distro.py
@@ -32,6 +32,7 @@ class Distro(item.Item):
Reset this object.
"""
self.name = None
+ self.owners = self.settings.default_ownership
self.kernel = (None, '<<inherit>>')[is_subobject]
self.initrd = (None, '<<inherit>>')[is_subobject]
self.kernel_options = ({}, '<<inherit>>')[is_subobject]
@@ -60,6 +61,7 @@ class Distro(item.Item):
"""
self.parent = self.load_item(seed_data,'parent')
self.name = self.load_item(seed_data,'name')
+ self.owners = self.load_item(seed_data,'owners',self.settings.default_ownership)
self.kernel = self.load_item(seed_data,'kernel')
self.initrd = self.load_item(seed_data,'initrd')
self.kernel_options = self.load_item(seed_data,'kernel_options')
@@ -75,6 +77,8 @@ class Distro(item.Item):
if self.ks_meta != "<<inherit>>" and type(self.ks_meta) != dict:
self.set_ksmeta(self.ks_meta)
+ self.set_owners(self.owners)
+
return self
def set_kernel(self,kernel):
@@ -165,7 +169,8 @@ class Distro(item.Item):
'breed' : self.breed,
'source_repos' : self.source_repos,
'parent' : self.parent,
- 'depth' : self.depth
+ 'depth' : self.depth,
+ 'owners' : self.owners
}
def printable(self):
@@ -181,6 +186,7 @@ class Distro(item.Item):
buf = buf + _("architecture : %s\n") % self.arch
buf = buf + _("ks metadata : %s\n") % self.ks_meta
buf = buf + _("breed : %s\n") % self.breed
+ buf = buf + _("owners : %s\n") % self.owners
return buf
def remote_methods(self):
@@ -191,7 +197,8 @@ class Distro(item.Item):
'kopts' : self.set_kernel_options,
'arch' : self.set_arch,
'ksmeta' : self.set_ksmeta,
- 'breed' : self.set_breed
+ 'breed' : self.set_breed,
+ 'owners' : self.set_owners
}
diff --git a/cobbler/item_profile.py b/cobbler/item_profile.py
index f229d4c..2e3a539 100644
--- a/cobbler/item_profile.py
+++ b/cobbler/item_profile.py
@@ -34,6 +34,7 @@ class Profile(item.Item):
Reset this object.
"""
self.name = None
+ self.owners = self.settings.default_ownership
self.distro = (None, '<<inherit>>')[is_subobject]
self.kickstart = (self.settings.default_kickstart , '<<inherit>>')[is_subobject]
self.kernel_options = ({}, '<<inherit>>')[is_subobject]
@@ -57,6 +58,7 @@ class Profile(item.Item):
self.parent = self.load_item(seed_data,'parent','')
self.name = self.load_item(seed_data,'name')
+ self.owners = self.load_item(seed_data,'owners',self.settings.default_ownership)
self.distro = self.load_item(seed_data,'distro')
self.kickstart = self.load_item(seed_data,'kickstart')
self.kernel_options = self.load_item(seed_data,'kernel_options')
@@ -87,6 +89,8 @@ class Profile(item.Item):
if self.repos != "<<inherit>>" and type(self.ks_meta) != list:
self.set_repos(self.repos)
+ self.set_owners(self.owners)
+
return self
def set_parent(self,parent_name):
@@ -328,6 +332,7 @@ class Profile(item.Item):
"""
return {
'name' : self.name,
+ 'owners' : self.owners,
'distro' : self.distro,
'kickstart' : self.kickstart,
'kernel_options' : self.kernel_options,
@@ -342,7 +347,8 @@ class Profile(item.Item):
'virt_type' : self.virt_type,
'virt_path' : self.virt_path,
'dhcp_tag' : self.dhcp_tag,
- 'server' : self.server
+ 'server' : self.server,
+
}
def printable(self):
@@ -366,6 +372,7 @@ class Profile(item.Item):
buf = buf + _("repos : %s\n") % self.repos
buf = buf + _("dhcp tag : %s\n") % self.dhcp_tag
buf = buf + _("server : %s\n") % self.server
+ buf = buf + _("owners : %s\n") % self.owners
return buf
def remote_methods(self):
@@ -385,6 +392,7 @@ class Profile(item.Item):
'virt-bridge' : self.set_virt_bridge,
'virt-cpus' : self.set_virt_cpus,
'dhcp-tag' : self.set_dhcp_tag,
- 'server' : self.set_server
+ 'server' : self.set_server,
+ 'owners' : self.set_owners
}
diff --git a/cobbler/item_repo.py b/cobbler/item_repo.py
index ca9e94f..1ebea30 100644
--- a/cobbler/item_repo.py
+++ b/cobbler/item_repo.py
@@ -39,6 +39,7 @@ class Repo(item.Item):
self.depth = 2 # arbitrary, as not really apart of the graph
self.arch = "" # use default arch
self.yumopts = {}
+ self.owners = self.settings.default_ownership
def from_datastruct(self,seed_data):
self.parent = self.load_item(seed_data, 'parent')
@@ -51,9 +52,11 @@ class Repo(item.Item):
self.arch = self.load_item(seed_data, 'arch')
self.depth = self.load_item(seed_data, 'depth', 2)
self.yumopts = self.load_item(seed_data, 'yumopts', {})
+ self.owners = self.load_item(seed_data, 'owners', self.settings.default_ownership)
- # force this to be saved as a boolean
+ # coerce types from input file
self.set_keep_updated(self.keep_updated)
+ self.set_owners(self.owners)
return self
@@ -153,6 +156,7 @@ class Repo(item.Item):
def to_datastruct(self):
return {
'name' : self.name,
+ 'owners' : self.owners,
'mirror' : self.mirror,
'keep_updated' : self.keep_updated,
'priority' : self.priority,
@@ -166,6 +170,7 @@ class Repo(item.Item):
def printable(self):
buf = _("repo : %s\n") % self.name
+ buf = buf + _("owners : %s\n") % self.owners
buf = buf + _("mirror : %s\n") % self.mirror
buf = buf + _("keep updated : %s\n") % self.keep_updated
buf = buf + _("priority : %s\n") % self.priority
@@ -202,6 +207,7 @@ class Repo(item.Item):
'priority' : self.set_priority,
'rpm-list' : self.set_rpm_list,
'createrepo-flags' : self.set_createrepo_flags,
- 'yumopts' : self.set_yumopts
+ 'yumopts' : self.set_yumopts,
+ 'owners' : self.set_owners
}
diff --git a/cobbler/item_system.py b/cobbler/item_system.py
index f5b16ca..dd7bd64 100644
--- a/cobbler/item_system.py
+++ b/cobbler/item_system.py
@@ -30,6 +30,7 @@ class System(item.Item):
def clear(self,is_subobject=False):
self.name = None
+ self.owners = self.settings.default_ownership
self.profile = None
self.kernel_options = {}
self.ks_meta = {}
@@ -80,6 +81,7 @@ class System(item.Item):
self.parent = self.load_item(seed_data, 'parent')
self.name = self.load_item(seed_data, 'name')
+ self.owners = self.load_item(seed_data, 'owners', self.settings.default_ownership)
self.profile = self.load_item(seed_data, 'profile')
self.kernel_options = self.load_item(seed_data, 'kernel_options', {})
self.ks_meta = self.load_item(seed_data, 'ks_meta', {})
@@ -126,8 +128,9 @@ class System(item.Item):
# explicitly re-call the set_name function to possibily populate MAC/IP.
self.set_name(self.name)
- # coerce this into a boolean
+ # coerce types from input file
self.set_netboot_enabled(self.netboot_enabled)
+ self.set_owners(self.owners)
return self
@@ -345,6 +348,7 @@ class System(item.Item):
def to_datastruct(self):
return {
'name' : self.name,
+ 'owners' : self.owners,
'profile' : self.profile,
'kernel_options' : self.kernel_options,
'ks_meta' : self.ks_meta,
@@ -369,6 +373,7 @@ class System(item.Item):
buf = buf + _("virt type : %s\n") % self.virt_type
buf = buf + _("virt path : %s\n") % self.virt_path
buf = buf + _("server : %s\n") % self.server
+ buf = buf + _("owners : %s\n") % self.owners
counter = 0
for (name,x) in self.interfaces.iteritems():
@@ -414,6 +419,7 @@ class System(item.Item):
'virt-type' : self.set_virt_type,
'modify-interface' : self.modify_interface,
'delete-interface' : self.delete_interface,
- 'server' : self.set_server
+ 'server' : self.set_server,
+ 'owners' : self.set_owners
}
diff --git a/cobbler/modules/authn_ldap.py b/cobbler/modules/authn_ldap.py
new file mode 100644
index 0000000..cec913b
--- /dev/null
+++ b/cobbler/modules/authn_ldap.py
@@ -0,0 +1,113 @@
+"""
+Authentication module that uses ldap
+Settings in /etc/cobbler/authn_ldap.conf
+Choice of authentication module is in /etc/cobbler/modules.conf
+
+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 distutils.sysconfig
+import sys
+import os
+from rhpl.translate import _, N_, textdomain, utf8
+import md5
+import traceback
+import ldap
+
+plib = distutils.sysconfig.get_python_lib()
+mod_path="%s/cobbler" % plib
+sys.path.insert(0, mod_path)
+
+import cexceptions
+import utils
+import api as cobbler_api
+
+def register():
+ """
+ The mandatory cobbler module registration hook.
+ """
+
+ return "authn"
+
+def authenticate(api_handle,username,password):
+ """
+ Validate an ldap bind, returning True/False
+ """
+
+ server = api_handle.settings().ldap_server
+ basedn = api_handle.settings().ldap_base_dn
+ port = api_handle.settings().ldap_port
+ tls = api_handle.settings().ldap_tls
+ anon_bind = api_handle.settings().ldap_anonymous_bind
+ prefix = api_handle.settings().ldap_search_prefix
+
+ # form our ldap uri based on connection port
+ if port == '389':
+ uri = 'ldap://' + server
+ elif port == '636':
+ uri = 'ldaps://' + server
+ else:
+ uri = 'ldap://' + "%s:%s" % (server,port)
+
+ # connect to LDAP host
+ dir = ldap.initialize(uri)
+
+ # start_tls if tls is 'on', 'true' or 'yes'
+ # and we're not already using old-SSL
+ tls = str(tls).lower()
+ if port != '636':
+ if tls in [ "on", "true", "yes", "1" ]:
+ try:
+ dir.start_tls_s()
+ except:
+ traceback.print_exc()
+ return False
+
+ # if we're not allowed to search anonymously,
+ # grok the search bind settings and attempt to bind
+ anon_bind = str(anon_bind).lower()
+ if anon_bind not in [ "on", "true", "yes", "1" ]:
+ searchdn = api_handle.settings().ldap_search_bind_dn
+ searchpw = api_handle.settings().ldap_search_passwd
+
+ if searchdn == '' or searchpw == '':
+ raise "Missing search bind settings"
+
+ try:
+ dir.simple_bind_s(searchdn, searchpw)
+ except:
+ traceback.print_exc()
+ return False
+
+ # perform a subtree search in basedn to find the full dn of the user
+ # TODO: what if username is a CN? maybe it goes into the config file as well?
+ filter = prefix + username
+ result = dir.search_s(basedn, ldap.SCOPE_SUBTREE, filter, [])
+ if result:
+ for dn,entry in result:
+ # username _should_ be unique so we should only have one result
+ # ignore entry; we don't need it
+ pass
+ else:
+ return False
+
+ try:
+ # attempt to bind as the user
+ dir.simple_bind_s(dn,password)
+ dir.unbind()
+ return True
+ except:
+ # traceback.print_exc()
+ return False
+ # catch-all
+ return False
+
+if __name__ == "__main__":
+ api_handle = cobbler_api.BootAPI()
+ print authenticate(api_handle, "guest", "guest")
+
diff --git a/cobbler/modules/authz_configfile.py b/cobbler/modules/authz_configfile.py
new file mode 100644
index 0000000..c183721
--- /dev/null
+++ b/cobbler/modules/authz_configfile.py
@@ -0,0 +1,64 @@
+"""
+Authorization module that allow users listed in
+/etc/cobbler/users.conf to be permitted to access resources.
+For instance, when using authz_ldap, you want to use authn_configfile,
+not authz_allowall, which will most likely NOT do what you want.
+
+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 distutils.sysconfig
+import ConfigParser
+import sys
+import os
+from rhpl.translate import _, N_, textdomain, utf8
+
+plib = distutils.sysconfig.get_python_lib()
+mod_path="%s/cobbler" % plib
+sys.path.insert(0, mod_path)
+
+import cexceptions
+import utils
+
+CONFIG_FILE='/etc/cobbler/users.conf'
+
+def register():
+ """
+ The mandatory cobbler module registration hook.
+ """
+ return "authz"
+
+def __parse_config():
+ if not os.path.exists(CONFIG_FILE):
+ return []
+ config = ConfigParser.SafeConfigParser()
+ config.read(CONFIG_FILE)
+ alldata = {}
+ groups = config.sections()
+ for g in groups:
+ alldata[str(g)] = {}
+ opts = config.options(g)
+ for o in opts:
+ alldata[g][o] = 1
+ return alldata
+
+
+def authorize(api_handle,user,resource,arg1=None,arg2=None):
+ """
+ Validate a user against a resource.
+ All users in the file are permitted by this module.
+ """
+
+ data = __parse_config()
+ for g in data:
+ if user in data[g]:
+ return 1
+ return 0
+
+if __name__ == "__main__":
+ print __parse_config()
diff --git a/cobbler/modules/authz_ownership.py b/cobbler/modules/authz_ownership.py
new file mode 100644
index 0000000..1fe25a9
--- /dev/null
+++ b/cobbler/modules/authz_ownership.py
@@ -0,0 +1,178 @@
+"""
+Authorization module that allow users listed in
+/etc/cobbler/users.conf to be permitted to access resources, with
+the further restriction that cobbler objects can be edited to only
+allow certain users/groups to access those specific objects.
+
+Copyright 2008, Red Hat, Inc
+Michael DeHaan <mdehaan@redhat.com>
+
+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 distutils.sysconfig
+import ConfigParser
+import sys
+import os
+from rhpl.translate import _, N_, textdomain, utf8
+
+plib = distutils.sysconfig.get_python_lib()
+mod_path="%s/cobbler" % plib
+sys.path.insert(0, mod_path)
+
+import cexceptions
+import utils
+
+
+def register():
+ """
+ The mandatory cobbler module registration hook.
+ """
+ return "authz"
+
+def __parse_config():
+ etcfile='/etc/cobbler/users.conf'
+ if not os.path.exists(etcfile):
+ raise CX(_("/etc/cobbler/users.conf does not exist"))
+ config = ConfigParser.ConfigParser()
+ config.read(etcfile)
+ alldata = {}
+ sections = config.sections()
+ for g in sections:
+ alldata[str(g)] = {}
+ opts = config.options(g)
+ for o in opts:
+ alldata[g][o] = 1
+ return alldata
+
+def __authorize_kickstart(api_handle, user, user_groups, kickstart):
+ # the authorization rules for kickstart editing are a bit
+ # of a special case. Non-admin users can edit a kickstart
+ # only if all objects that depend on that kickstart are
+ # editable by the user in question.
+ #
+ # Example:
+ # if Pinky owns ProfileA
+ # and the Brain owns ProfileB
+ # and both profiles use the same kickstart template
+ # and neither Pinky nor the Brain is an admin
+ # neither is allowed to edit the kickstart template
+ # because they would make unwanted changes to each other
+ #
+ # In the above scenario the UI will explain the problem
+ # and ask that the user asks the admin to resolve it if required.
+ # NOTE: this function is only called by authorize so admin users are
+ # cleared before this function is called.
+
+ lst = api_handle.find_profile(kickstart=kickstart, return_list=True)
+ lst.extend(api_handle.find_system(kickstart=kickstart, return_list=True))
+ for obj in lst:
+ if not __is_user_allowed(obj, user, user_groups):
+ return 0
+ return 1
+
+def __is_user_allowed(obj, user, user_groups):
+ if obj.owners == []:
+ # no ownership restrictions, cleared
+ return 1
+ for allowed in obj.owners:
+ if user == allowed:
+ # user match
+ return 1
+ # else look for a group match
+ for group in user_groups:
+ if group == allowed and user in user_groups[group]:
+ return 1
+ return 0
+
+
+
+def authorize(api_handle,user,resource,arg1=None,arg2=None):
+ """
+ Validate a user against a resource.
+ All users in the file are permitted by this module.
+ """
+
+ # everybody can get read-only access to everything
+ # if they pass authorization, they don't have to be in users.conf
+ if resource is not None:
+ for x in [ "get", "read", "/cobbler/web" ]:
+ if resource.startswith(x):
+ return 1
+
+ user_groups = __parse_config()
+
+ # classify the type of operation
+ modify_operation = False
+ for criteria in ["save","remove","modify","write","edit"]:
+ if resource.find(criteria) != -1:
+ modify_operation = True
+
+ # FIXME: is everyone allowed to copy? I think so.
+ # FIXME: deal with the problem of deleted parents and promotion
+
+ found_user = False
+ for g in user_groups:
+ for x in user_groups[g]:
+ if x == user:
+ found_user = True
+ # if user is in the admin group, always authorize
+ # regardless of the ownership of the object.
+ if g == "admins" or g == "admin":
+ return 1
+ break
+
+ if not found_user:
+ # if the user isn't anywhere in the file, reject regardless
+ # they can still use read-only XMLRPC
+ return 0
+ if not modify_operation:
+ # sufficient to allow access for non save/remove ops to all
+ # users for now, may want to refine later.
+ return 1
+
+ # now we have a modify_operation op, so we must check ownership
+ # of the object. remove ops pass in arg1 as a string name,
+ # saves pass in actual objects, so we must treat them differently.
+ # kickstarts are even more special so we call those out to another
+ # function, rather than going through the rest of the code here.
+
+ if resource.find("kickstart") != -1:
+ return __authorize_kickstart(api_handle,user,user_groups,arg1)
+
+ obj = None
+ if resource.find("remove") != -1:
+ if resource == "remove_distro":
+ obj = api_handle.find_distro(arg1)
+ elif resource == "remove_profile":
+ obj = api_handle.find_profile(arg1)
+ elif resource == "remove_system":
+ obj = api_handle.find_system(arg1)
+ elif resource == "remove_repo":
+ obj = api_handle.find_system(arg1)
+ elif resource.find("save") != -1 or resource.find("modify") != -1:
+ obj = arg1
+
+ # if the object has no ownership data, allow access regardless
+ if obj.owners is None or obj.owners == []:
+ return 1
+
+ return __is_user_allowed(obj,user,user_groups)
+
+
+if __name__ == "__main__":
+ # real tests are contained in tests/tests.py
+ import api as cobbler_api
+ api = cobbler_api.BootAPI()
+ print __parse_config()
+ print authorize(api, "admin1", "sync")
+ d = api.find_distro("F9B-i386")
+ d.set_owners(["allowed"])
+ api.add_distro(d)
+ print authorize(api, "admin1", "save_distro", d)
+ print authorize(api, "basement2", "save_distro", d)
diff --git a/cobbler/modules/cli_distro.py b/cobbler/modules/cli_distro.py
index 35f5a4b..8c7b70f 100644
--- a/cobbler/modules/cli_distro.py
+++ b/cobbler/modules/cli_distro.py
@@ -40,6 +40,9 @@ class DistroFunction(commands.CobblerFunction):
if not self.matches_args(args,["remove","report","list"]):
p.add_option("--arch", dest="arch", help="ex: x86, x86_64, ia64")
p.add_option("--breed", dest="breed", help="ex: redhat, debian, suse")
+ if self.matches_args(args,["add"]):
+ p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true")
+ if not self.matches_args(args,["remove","report","list"]):
p.add_option("--initrd", dest="initrd", help="absolute path to initrd.img (REQUIRED)")
p.add_option("--kernel", dest="kernel", help="absolute path to vmlinuz (REQUIRED)")
p.add_option("--kopts", dest="kopts", help="ex: 'noipv6'")
@@ -47,12 +50,16 @@ class DistroFunction(commands.CobblerFunction):
p.add_option("--name", dest="name", help="ex: 'RHEL-5-i386' (REQUIRED)")
+
+
if self.matches_args(args,["copy","rename"]):
p.add_option("--newname", dest="newname", help="for copy/rename commands")
if not self.matches_args(args,["remove","report","list"]):
p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed")
if not self.matches_args(args,["report","list"]):
p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution")
+ if not self.matches_args(args,["remove","report","list"]):
+ p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module")
if self.matches_args(args,["remove"]):
p.add_option("--recursive", action="store_true", dest="recursive", help="also delete child objects")
@@ -73,6 +80,8 @@ class DistroFunction(commands.CobblerFunction):
obj.set_ksmeta(self.options.ksmeta)
if self.options.breed:
obj.set_breed(self.options.breed)
+ if self.options.owners:
+ obj.set_owners(self.options.owners)
return self.object_manipulator_finish(obj, self.api.distros, self.options)
diff --git a/cobbler/modules/cli_profile.py b/cobbler/modules/cli_profile.py
index e9d1d23..0bf4b8c 100644
--- a/cobbler/modules/cli_profile.py
+++ b/cobbler/modules/cli_profile.py
@@ -36,6 +36,11 @@ class ProfileFunction(commands.CobblerFunction):
return [ "add", "edit", "copy", "rename", "remove", "list", "report" ]
def add_options(self, p, args):
+
+ if self.matches_args(args,["add"]):
+ p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true")
+
+
if not self.matches_args(args,["remove","report","list"]):
p.add_option("--distro", dest="distro", help="ex: 'RHEL-5-i386' (REQUIRED)")
@@ -46,6 +51,7 @@ class ProfileFunction(commands.CobblerFunction):
p.add_option("--kopts", dest="kopts", help="ex: 'noipv6'")
p.add_option("--name", dest="name", help="a name for the profile (REQUIRED)")
+
if "copy" in args or "rename" in args:
p.add_option("--newname", dest="newname")
@@ -53,6 +59,7 @@ class ProfileFunction(commands.CobblerFunction):
p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed")
if not self.matches_args(args,["report", "list"]):
p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution")
+ p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module")
if self.matches_args(args,["remove"]):
p.add_option("--recursive", action="store_true", dest="recursive", help="also delete child objects")
@@ -93,6 +100,9 @@ class ProfileFunction(commands.CobblerFunction):
if self.options.dhcp_tag: obj.set_dhcp_tag(self.options.dhcp_tag)
if self.options.server_override: obj.set_server(self.options.server)
+ if self.options.owners:
+ obj.set_owners(self.options.owners)
+
return self.object_manipulator_finish(obj, self.api.profiles, self.options)
diff --git a/cobbler/modules/cli_repo.py b/cobbler/modules/cli_repo.py
index 96afa6f..e8ec04a 100644
--- a/cobbler/modules/cli_repo.py
+++ b/cobbler/modules/cli_repo.py
@@ -37,9 +37,13 @@ class RepoFunction(commands.CobblerFunction):
def add_options(self, p, args):
+
if not self.matches_args(args,["remove","report","list"]):
p.add_option("--arch", dest="arch", help="overrides repo arch if required")
+ if self.matches_args(args,["add"]):
+ p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true")
+ if not self.matches_args(args,["remove","report","list"]):
p.add_option("--createrepo-flags", dest="createrepo_flags", help="additional flags for createrepo")
p.add_option("--keep-updated", dest="keep_updated", help="update on each reposync, yes/no")
@@ -59,6 +63,8 @@ class RepoFunction(commands.CobblerFunction):
p.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed")
if not self.matches_args(args,["report","list"]):
p.add_option("--no-triggers", action="store_true", dest="notriggers", help="suppress trigger execution")
+ if not self.matches_args(args,["remove","report","list"]):
+ p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module")
def run(self):
@@ -75,6 +81,9 @@ class RepoFunction(commands.CobblerFunction):
if self.options.mirror: obj.set_mirror(self.options.mirror)
if self.options.yumopts: obj.set_yumopts(self.options.yumopts)
+ if self.options.owners:
+ obj.set_owners(self.options.owners)
+
return self.object_manipulator_finish(obj, self.api.repos, self.options)
diff --git a/cobbler/modules/cli_system.py b/cobbler/modules/cli_system.py
index c463b8c..34c8886 100644
--- a/cobbler/modules/cli_system.py
+++ b/cobbler/modules/cli_system.py
@@ -37,6 +37,9 @@ class SystemFunction(commands.CobblerFunction):
def add_options(self, p, args):
+ if self.matches_args(args,["add"]):
+ p.add_option("--clobber", dest="clobber", help="allow add to overwrite existing objects", action="store_true")
+
if not self.matches_args(args,["remove","report","list"]):
p.add_option("--dhcp-tag", dest="dhcp_tag", help="for use in advanced DHCP configurations")
p.add_option("--gateway", dest="gateway", help="for static IP / templating usage")
@@ -63,6 +66,7 @@ class SystemFunction(commands.CobblerFunction):
if not self.matches_args(args,["remove","report","list"]):
+ p.add_option("--owners", dest="owners", help="specify owners for authz_ownership module")
p.add_option("--profile", dest="profile", help="name of cobbler profile (REQUIRED)")
p.add_option("--server-override", dest="server_override", help="overrides server value in settings file")
p.add_option("--subnet", dest="subnet", help="for static IP / templating usage")
@@ -99,8 +103,12 @@ class SystemFunction(commands.CobblerFunction):
if self.options.dhcp_tag: obj.set_dhcp_tag(self.options.dhcp_tag, my_interface)
if self.options.virt_bridge: obj.set_virt_bridge(self.options.virt_bridge, my_interface)
- return self.object_manipulator_finish(obj, self.api.systems, self.options)
+ if self.options.owners:
+ obj.set_owners(self.options.owners)
+
+ rc = self.object_manipulator_finish(obj, self.api.systems, self.options)
+ return rc
########################################################
diff --git a/cobbler/remote.py b/cobbler/remote.py
index 5131323..d9f937b 100644
--- a/cobbler/remote.py
+++ b/cobbler/remote.py
@@ -561,10 +561,6 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
FIXME: currently looks for users in /etc/cobbler/auth.conf
Would be very nice to allow for PAM and/or just Kerberos.
"""
- if not self.auth_enabled and input_user == "<system>":
- return True
- if self.auth_enabled and input_user == "<system>":
- return False
return self.api.authenticate(input_user,input_password)
def __validate_token(self,token):
@@ -579,11 +575,12 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
self.__invalidate_expired_tokens()
self.__invalidate_expired_objects()
- if not self.auth_enabled:
- user = self.get_user_from_token(token)
- if user == "<system>":
- self.token_cache[token] = (time.time(), user) # update to prevent timeout
- return True
+ #if not self.auth_enabled:
+ # user = self.get_user_from_token(token)
+ # # old stuff, preserving for future usage
+ # # if user == "<system>":
+ # # self.token_cache[token] = (time.time(), user) # update to prevent timeout
+ # # return True
if self.token_cache.has_key(token):
user = self.get_user_from_token(token)
@@ -596,12 +593,54 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
self.log("invalid token",token=token)
raise CX(_("invalid token: %s" % token))
+ def __name_to_object(self,resource,name):
+ if resource.find("distro") != -1:
+ return self.api.find_distro(name)
+ if resource.find("profile") != -1:
+ return self.api.find_profile(name)
+ if resource.find("system") != -1:
+ return self.api.find_system(name)
+ if resource.find("repo") != -1:
+ return self.api.find_repo(name)
+ return None
+
+ def check_access_no_fail(self,token,resource,arg1=None,arg2=None):
+ """
+ This is called by the WUI to decide whether an element
+ is editable or not. It differs form check_access in that
+ it is supposed to /not/ log the access checks (TBA) and does
+ not raise exceptions.
+ """
+
+ need_remap = False
+ for x in [ "distro", "profile", "system", "repo" ]:
+ if arg1 is not None and resource.find(x) != -1:
+ need_remap = True
+ break
+
+ if need_remap:
+ # we're called with an object name, but need an object
+ arg1 = self.__name_to_object(resource,arg1)
+
+ try:
+ self.check_access(token,resource,arg1,arg2)
+ return True
+ except:
+ utils.log_exc(self.logger)
+ return False
+
def check_access(self,token,resource,arg1=None,arg2=None):
validated = self.__validate_token(token)
+ user = self.get_user_from_token(token)
if not self.auth_enabled:
+ # for public read-only XMLRPC, permit access
+ self.log("permitting read-only access")
return True
- return self.__authorize(token,resource,arg1,arg2)
-
+ rc = self.__authorize(token,resource,arg1,arg2)
+ self.log("authorization result: %s" % rc)
+ if not rc:
+ raise CX(_("authorization failure for user %s" % user))
+ return rc
def login(self,login_user,login_password):
"""
@@ -621,7 +660,11 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
def __authorize(self,token,resource,arg1=None,arg2=None):
user = self.get_user_from_token(token)
- if self.api.authorize(user,resource,arg1,arg2):
+ args = [ resource, arg1, arg2 ]
+ self.log("calling authorize for resource %s" % args, user=user)
+
+ rc = self.api.authorize(user,resource,arg1,arg2)
+ if rc:
return True
else:
raise CX(_("user does not have access to resource: %s") % resource)
@@ -767,41 +810,56 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
found = self.api.repos().find(name)
return self.__store_object(found)
- def save_distro(self,object_id,token):
+ def save_distro(self,object_id,token,editmode="bypass"):
"""
Saves a newly created or modified distro object to disk.
"""
self.log("save_distro",object_id=object_id,token=token)
- self.check_access(token,"save_distro")
obj = self.__get_object(object_id)
- return self.api.distros().add(obj,save=True)
+ self.check_access(token,"save_distro",obj)
+ if editmode == "new":
+ return self.api.distros().add(obj,save=True,check_for_duplicate_names=True)
+ else:
+ return self.api.distros().add(obj,save=True)
- def save_profile(self,object_id,token):
+ def save_profile(self,object_id,token,editmode="bypass"):
"""
Saves a newly created or modified profile object to disk.
"""
self.log("save_profile",token=token,object_id=object_id)
- self.check_access(token,"save_profile")
obj = self.__get_object(object_id)
- return self.api.profiles().add(obj,save=True)
+ self.check_access(token,"save_profile",obj)
+ if editmode == "new":
+ return self.api.profiles().add(obj,save=True,check_for_duplicate_names=True)
+ else:
+ return self.api.profiles().add(obj,save=True)
- def save_system(self,object_id,token):
+ def save_system(self,object_id,token,editmode="bypass"):
"""
Saves a newly created or modified system object to disk.
"""
self.log("save_system",token=token,object_id=object_id)
- self.check_access(token,"save_system")
obj = self.__get_object(object_id)
- return self.api.systems().add(obj,save=True)
+ self.check_access(token,"save_system",obj)
+ if editmode == "new":
+ return self.api.systems().add(obj,save=True,check_for_duplicate_names=True,check_for_duplicate_netinfo=True)
+ elif editmode == "edit":
+ return self.api.systems().add(obj,save=True,check_for_duplicate_netinfo=True)
+ else:
+ return self.api.systems().add(obj,save=True)
+
- def save_repo(self,object_id,token=None):
+ def save_repo(self,object_id,token=None,editmode="bypass"):
"""
Saves a newly created or modified repo object to disk.
"""
self.log("save_repo",object_id=object_id,token=token)
- self.check_access(token,"save_repo")
obj = self.__get_object(object_id)
- return self.api.repos().add(obj,save=True)
+ self.check_access(token,"save_repo",obj)
+ if editmode == "new":
+ return self.api.repos().add(obj,save=True,check_for_duplicate_names=True)
+ else:
+ return self.api.repos().add(obj,save=True)
def copy_distro(self,object_id,newname,token=None):
"""
@@ -874,8 +932,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
Allows modification of certain attributes on newly created or
existing distro object handle.
"""
- self.check_access(token, "modify_distro", attribute, arg)
obj = self.__get_object(object_id)
+ self.check_access(token, "modify_distro", obj, attribute)
return self.__call_method(obj, attribute, arg)
def modify_profile(self,object_id,attribute,arg,token):
@@ -883,8 +941,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
Allows modification of certain attributes on newly created or
existing profile object handle.
"""
- self.check_access(token, "modify_profile", attribute, arg)
obj = self.__get_object(object_id)
+ self.check_access(token, "modify_profile", obj, attribute)
return self.__call_method(obj, attribute, arg)
def modify_system(self,object_id,attribute,arg,token):
@@ -892,8 +950,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
Allows modification of certain attributes on newly created or
existing system object handle.
"""
- self.check_access(token, "modify_system", attribute, arg)
obj = self.__get_object(object_id)
+ self.check_access(token, "modify_system", obj, attribute)
return self.__call_method(obj, attribute, arg)
def modify_repo(self,object_id,attribute,arg,token):
@@ -901,8 +959,8 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
Allows modification of certain attributes on newly created or
existing repo object handle.
"""
- self.check_access(token, "modify_repo", attribute, arg)
obj = self.__get_object(object_id)
+ self.check_access(token, "modify_repo", obj, attribute)
return self.__call_method(obj, attribute, arg)
def remove_distro(self,name,token,recursive=1):
@@ -910,7 +968,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
Deletes a distro from a collection. Note that this just requires the name
of the distro, not a handle.
"""
- self.log("remove_distro",name=name,token=token)
+ self.log("remove_distro (%s)" % recursive,name=name,token=token)
self.check_access(token, "remove_distro", name)
rc = self.api._config.distros().remove(name,recursive=True)
return rc
@@ -1007,7 +1065,10 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
"""
self.log("read_or_write_kickstart_template",name=kickstart_file,token=token)
- self.check_access(token,"read_or_write_kickstart_templates",kickstart_file,is_read)
+ if is_read:
+ self.check_access(token,"read_kickstart",kickstart_file)
+ else:
+ self.check_access(token,"modify_kickstart",kickstart_file)
if kickstart_file.find("..") != -1 or not kickstart_file.startswith("/"):
raise CX(_("tainted file location"))
diff --git a/cobbler/settings.py b/cobbler/settings.py
index 8c8be03..c201ddd 100644
--- a/cobbler/settings.py
+++ b/cobbler/settings.py
@@ -24,6 +24,8 @@ TESTMODE = False
DEFAULTS = {
"allow_cgi_mac_registration" : 0,
"allow_cgi_profile_change" : 0,
+ "allow_duplicate_macs" : 0,
+ "allow_duplicate_ips" : 0,
"bootloaders" : {
"standard" : "/usr/lib/syslinux/pxelinux.0",
"ia64" : "/var/lib/cobbler/elilo-3.6-ia64.efi"
@@ -33,13 +35,23 @@ DEFAULTS = {
"default_virt_type" : "auto",
"default_virt_file_size" : "5",
"default_virt_ram" : "512",
+ "default_ownership" : "admin",
"dhcpd_conf" : "/etc/dhcpd.conf",
"dhcpd_bin" : "/usr/sbin/dhcpd",
"dnsmasq_bin" : "/usr/sbin/dnsmasq",
"dnsmasq_conf" : "/etc/dnsmasq.conf",
"httpd_bin" : "/usr/sbin/httpd",
"http_port" : "80",
- "kerberos_realm" : "example.org",
+ "isc_set_host_name" : 0,
+ "ldap_server" : "grimlock.devel.redhat.com",
+ "ldap_base_dn" : "DC=devel,DC=redhat,DC=com",
+ "ldap_port" : 389,
+ "ldap_tls" : "on",
+ "ldap_anonymous_bind" : 1,
+ "ldap_search_bind_dn" : '',
+ "ldap_search_passwd" : '',
+ "ldap_search_prefix" : 'uid=',
+ "kerberos_realm" : "EXAMPLE.COM",
"kernel_options" : {
"lang" : " ",
"text" : None,
@@ -53,7 +65,6 @@ DEFAULTS = {
"server" : "127.0.0.1",
"snippetsdir" : "/var/lib/cobbler/snippets",
"syslog_port" : 25150,
- "tftpboot" : "/tftpboot",
"tftpd_bin" : "/usr/sbin/in.tftpd",
"tftpd_conf" : "/etc/xinetd.d/tftp",
"webdir" : "/var/www/cobbler",
@@ -101,7 +112,9 @@ class Settings(serializable.Serializable):
if datastruct is None:
print _("warning: not loading empty structure for %s") % self.filename()
return
+
self._attributes = datastruct
+
return self
def __getattr__(self,name):
diff --git a/cobbler/utils.py b/cobbler/utils.py
index 069d440..4d2b635 100644
--- a/cobbler/utils.py
+++ b/cobbler/utils.py
@@ -230,6 +230,22 @@ def find_kickstart(url):
return url
return None
+def input_string_or_list(options,delim=","):
+ """
+ Accepts a delimited list of stuff or a list, but always returns a list.
+ """
+ if options is None or options == "delete":
+ return []
+ elif type(options) == list:
+ return options
+ elif type(options) == str:
+ tokens = options.split(delim)
+ if delim == ",":
+ tokens = [t.lstrip().rstrip() for t in tokens]
+ return tokens
+ else:
+ raise CX(_("invalid input type"))
+
def input_string_or_hash(options,delim=","):
"""
Older cobbler files stored configurations in a flat way, such that all values for strings.
@@ -240,7 +256,7 @@ def input_string_or_hash(options,delim=","):
if options == "<<inherit>>":
options = {}
- if options is None:
+ if options is None or options == "delete":
return (True, {})
elif type(options) == list:
raise CX(_("No idea what to do with list: %s") % options)
@@ -261,7 +277,7 @@ def input_string_or_hash(options,delim=","):
options.pop('',None)
return (True, options)
else:
- raise CX(_("Foreign options type"))
+ raise CX(_("invalid input type"))
def grab_tree(api_handle, obj):
"""
@@ -468,3 +484,51 @@ def fix_mod_python_select_submission(repos):
repos = repos.lstrip().rstrip()
return repos
+def redhat_release():
+ if not os.path.exists("/bin/rpm"):
+ return ("unknown", 0)
+ args = ["/bin/rpm", "-q", "--whatprovides", "redhat-release"]
+ cmd = sub_process.Popen(args,shell=False,stdout=sub_process.PIPE)
+ data = cmd.communicate()[0]
+ data = data.rstrip().lower()
+ make = "other"
+ if data.find("redhat") != -1:
+ make = "redhat"
+ elif data.find("centos") != -1:
+ make = "centos"
+ elif data.find("fedora") != -1:
+ make = "fedora"
+ version = data.split("release-")[-1]
+ rest = 0
+ if version.find("-"):
+ parts = version.split("-")
+ version = parts[0]
+ rest = parts[1]
+ return (make, float(version), rest)
+
+def tftpboot_location():
+
+ # if possible, read from TFTP config file to get the location
+ if os.path.exists("/etc/xinetd.d/tftp"):
+ fd = open("/etc/xinetd.d/tftp")
+ lines = fd.read().split("\n")
+ for line in lines:
+ if line.find("server_args") != -1:
+ tokens = line.split(None)
+ mark = False
+ for t in tokens:
+ if t == "-s":
+ mark = True
+ elif mark:
+ return t
+
+ # otherwise, guess based on the distro
+ (make,version,rest) = redhat_release()
+ if make == "fedora" and version >= 9:
+ return "/var/lib/tftpboot"
+ return "/tftpboot"
+
+if __name__ == "__main__":
+ # print redhat_release()
+ print tftpboot_location()
+
diff --git a/cobbler/webui/CobblerWeb.py b/cobbler/webui/CobblerWeb.py
index 9a0cf90..628d776 100644
--- a/cobbler/webui/CobblerWeb.py
+++ b/cobbler/webui/CobblerWeb.py
@@ -60,6 +60,7 @@ class CobblerWeb(object):
# validate that our token is still good
try:
self.remote.token_check(self.token)
+ self.username = self.remote.get_user_from_token(self.token)
return True
except Exception, e:
if str(e).find("invalid token") != -1:
@@ -162,15 +163,27 @@ class CobblerWeb(object):
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,arch=None,breed=None,
- delete1=None,delete2=None,**args):
+ 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()
@@ -182,8 +195,12 @@ class CobblerWeb(object):
# handle deletes as a special case
if new_or_edit == 'edit' and delete1 and delete2:
- try:
- self.remote.remove_distro(name,self.token,1) # recursive
+ 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()
@@ -220,11 +237,14 @@ class CobblerWeb(object):
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)
- self.remote.save_distro(distro, 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))
@@ -288,7 +308,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',
+ kopts=None, ksmeta=None, owners=None, server_override=None, netboot='n',
delete1=None, delete2=None, **args):
if not self.__xmlrpc_setup():
@@ -332,6 +352,8 @@ class CobblerWeb(object):
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:
@@ -364,8 +386,7 @@ class CobblerWeb(object):
mods["gateway-%s" % interface] = gateway
self.remote.modify_system(system,'modify-interface', mods, self.token)
- # now commit the edits
- self.remote.save_system( system, self.token)
+ self.remote.save_system(system, self.token, editmode)
except Exception, e:
log_exc(self.apache)
@@ -390,9 +411,20 @@ class CobblerWeb(object):
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()
} )
@@ -427,10 +459,21 @@ class CobblerWeb(object):
input_profile = None
if name is not None:
- input_profile = self.remote.get_profile(name,True)
+ 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(),
@@ -441,9 +484,9 @@ class CobblerWeb(object):
def profile_save(self,new_or_edit=None,editmode='edit',name=None,oldname=None,
distro=None,kickstart=None,kopts=None,
- ksmeta=None,virtfilesize=None,virtram=None,virttype=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,**args):
+ parent=None,virtcpus=None,virtbridge=None,subprofile=None,server_override=None,recursive=False,**args):
if not self.__xmlrpc_setup():
return self.xmlrpc_auth_failure()
@@ -463,7 +506,11 @@ class CobblerWeb(object):
# handle deletes as a special case
if new_or_edit == 'edit' and delete1 and delete2:
try:
- self.remote.remove_profile(name,self.token,1)
+ 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()
@@ -495,6 +542,8 @@ class CobblerWeb(object):
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:
@@ -523,7 +572,7 @@ class CobblerWeb(object):
if dhcptag:
self.remote.modify_profile(profile, 'dhcp-tag', dhcptag, self.token)
- self.remote.save_profile(profile,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))
@@ -565,13 +614,24 @@ class CobblerWeb(object):
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,keep_updated=None,priority=99,
+ mirror=None,owners=None,keep_updated=None,priority=99,
rpm_list=None,createrepo_flags=None,arch=None,yumopts=None,
delete1=None,delete2=None,**args):
if not self.__xmlrpc_setup():
@@ -624,8 +684,10 @@ class CobblerWeb(object):
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)
+ self.remote.save_repo(repo, self.token, editmode)
except Exception, e:
log_exc(self.apache)
@@ -651,10 +713,15 @@ class CobblerWeb(object):
} )
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,
+ 'editable' : can_edit,
'ksdata': self.remote.read_or_write_kickstart_template(name,True,"",self.token)
} )
@@ -675,6 +742,13 @@ class CobblerWeb(object):
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:
diff --git a/cobbler/webui/master.py b/cobbler/webui/master.py
index 56b93f6..0a6e9dd 100644
--- a/cobbler/webui/master.py
+++ b/cobbler/webui/master.py
@@ -33,8 +33,8 @@ VFN=valueForName
currentTime=time.time
__CHEETAH_version__ = '2.0.1'
__CHEETAH_versionTuple__ = (2, 0, 1, 'final', 0)
-__CHEETAH_genTime__ = 1207678316.193969
-__CHEETAH_genTimestamp__ = 'Tue Apr 8 14:11:56 2008'
+__CHEETAH_genTime__ = 1207681739.292002
+__CHEETAH_genTimestamp__ = 'Tue Apr 8 15:08:59 2008'
__CHEETAH_src__ = 'webui_templates/master.tmpl'
__CHEETAH_srcLastModified__ = 'Fri Feb 15 14:47:43 2008'
__CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine'
diff --git a/config/modules.conf b/config/modules.conf
index 2d60d21..2daf0e4 100644
--- a/config/modules.conf
+++ b/config/modules.conf
@@ -9,4 +9,4 @@ repo = serializer_yaml
module = authn_configfile
[authorization]
-module = authn_allowall
+module = authz_allowall
diff --git a/config/settings b/config/settings
index ba709ae..8fc0fdf 100644
--- a/config/settings
+++ b/config/settings
@@ -1,6 +1,8 @@
---
allow_cgi_mac_registration: 0
allow_cgi_profile_change: 0
+allow_duplicate_macs: 0
+allow_duplicate_ips: 0
bootloaders:
ia64: /var/lib/cobbler/elilo-3.6-ia64.efi
standard: /usr/lib/syslinux/pxelinux.0
@@ -9,6 +11,7 @@ default_virt_bridge: xenbr0
default_virt_type: auto
default_virt_file_size: 5
default_virt_ram: 512
+default_ownership: "admin"
dhcpd_bin: /usr/sbin/dhcpd
dhcpd_conf: /etc/dhcpd.conf
dnsmasq_bin: /usr/sbin/dnsmasq
@@ -20,6 +23,14 @@ kernel_options:
ksdevice: eth0
lang: ' '
text: ~
+ldap_server: "ldap.example.com"
+ldap_base_dn: "DC=example,DC=com"
+ldap_port: 389
+ldap_tls: 1
+ldap_anonymous_bind: 1
+ldap_search_bind_dn: ''
+ldap_search_passwd: ''
+ldap_search_prefix: 'uid='
manage_dhcp: 0
manage_dhcp_mode: isc
next_server: '127.0.0.1'
@@ -28,7 +39,6 @@ run_post_install_trigger: 0
server: '127.0.0.1'
snippetsdir: /var/lib/cobbler/snippets
syslog_port: 25150
-tftpboot: /tftpboot
tftpd_bin: /usr/sbin/in.tftpd
tftpd_conf: /etc/xinetd.d/tftp
webdir: /var/www/cobbler
diff --git a/config/users.conf b/config/users.conf
new file mode 100644
index 0000000..129659a
--- /dev/null
+++ b/config/users.conf
@@ -0,0 +1,28 @@
+# Cobbler WebUI / Web Services authorization config file
+#
+# NOTICE:
+# this file is only used when /etc/cobbler/modules.conf
+# specifies an authorization mode of either:
+#
+# (A) authz_configfile
+# (B) authz_ownership
+#
+# For (A), any user in this file, in any group, are allowed
+# full access to any object in cobbler configuration.
+#
+# For (B), users in the "admins" group are allowed full access
+# to any object, otherwise users can only edit an object if
+# their username/group is listed as an owner of that object. If a
+# user is not listed in this file they will have no access.
+#
+# cobbler command line example:
+#
+# cobbler system edit --name=server1 --owner=dbas,mac,pete,jack
+#
+# NOTE: yes, you do need the equal sign after the names.
+# don't remove that part. It's reserved for future use.
+
+[admins]
+admin = ""
+cobbler = ""
+
diff --git a/docs/cobbler.pod b/docs/cobbler.pod
index a1f174f..719850d 100644
--- a/docs/cobbler.pod
+++ b/docs/cobbler.pod
@@ -102,6 +102,10 @@ arguments appropriately. Support for other types of distributions is possible
The file used for the answer file, regardless of the breed setting, is the value used for --kickstart when creating the profile.
+=item owners
+
+Users with small sites and a limited number of admins can probably ignore this option. All cobbler objects (distros, profiles, systems, and repos) can take a --owners parameter to specify what cobbler users can edit particular objects. This only applies to the Cobbler WebUI and XMLRPC interface, not the "cobbler" command line tool run from the shell. Furthermore, this is only respected by the "authz_ownership" module which must be enabled in /etc/cobbler/modules.conf. The value for --owners is a comma seperated list of users and groups as specified in /etc/cobbler/users.conf. For more information see the users.conf file as well as the Cobbler Wiki. In the default Cobbler configuration, this value is completely ignored, as is users.conf.
+
=back
=head2 ADDING A PROFILE
diff --git a/docs/wui.html b/docs/wui.html
index c05a6f6..ebb9b39 100644
--- a/docs/wui.html
+++ b/docs/wui.html
@@ -26,21 +26,23 @@
<h4>Welcome</h4>
<p>
-This is the Web UI for your local <A HREF="http://cobbler.et.redhat.com">Cobbler</A> Server.
-</p>
+This is the web configuration interface for a <A HREF="http://cobbler.et.redhat.com">Cobbler</A> Server. Cobbler is an automated net install and update server for Linux operating systems. You can use this web interface to decide what you want to install and where -- and then deploy that configuration using network booting (PXE), or do reinstalls and virtual installs with "koan". There is also a koan live CD if you can't set up a PXE environment and still have bare-metal install needs.</p>
<p>
-The Cobbler WebUI is designed for day-to-day usage of the Cobbler provisioning server. It performs
-<i>most</i> but not <i>all</i> of the functions Cobbler can perform. Nearly all of what you would
-need for routine maintaince of your deployment setups can be done through the web. If you have
+The Cobbler WebUI is designed for simplifying day-to-day usage of the Cobbler provisioning server. It performs
+<i>most</i> but not <i>all</i> of the functions the command line tool "cobbler" or the API can perform. If you have
not already done so, you may be interested to read more about Cobbler at <A HREF="http://cobbler.et.redhat.com">
-cobbler.et.redhat.com</A> and <A HREF="https://hosted.fedoraproject.org/projects/cobbler/">hosted.fedoraproject.org</A>.
+cobbler.et.redhat.com</A> and at <A HREF="https://fedorahosted.org/cobbler">fedorahosted.org</A>.
Those pages contain further documentation, tips & tricks, and links to the mailing list and users/developers
IRC channel.
</p>
<p>
-It is expected that you have read the <A HREF="/cobbler/webui/cobbler.html">Cobbler manpage</A>, which for the most part focuses on cobbler as run from the command line. You will need to use the command line some, so please do read the docs. For starters, you should have started your cobbler install with running "cobbler check" locally. If not,
+It is probably a good idea to read the <A HREF="/cobbler/webui/cobbler.html">Cobbler manpage</A>, which for the most part focuses on cobbler as run from the command line. Admins will need to use the command line some, so please do read the docs if you are in charge of the Cobbler server -- or if you need more detail about what some of the options mean.
+</p>
+
+<p>
+The Cobbler server install should have started your cobbler install with running "cobbler check" locally to resolve potential configuration problems. If not,
please do so now before continuing. This will make sure your installation is configured and ready to go. The rest
of this document will mainly be detailing the differences between the CLI (as described in the manpage) and the Web interface.
</p>
@@ -52,12 +54,15 @@ content from a DVD or an rsync mirror -- running cobbler import locally is also
</p>
<p>
-Another command that you cannot run locally is "cobbler reposync", which is a fairly long-running operation
+Another command that you cannot run via the web interface is "cobbler reposync", which can be a fairly long-running operation
that you may want to put on a crontab. You also cannot edit the cobbler settings file (this is more
of a precaution against locking yourself out of the WebUI), but you can view it. For instance this means
that if you want Cobbler to help manage your DHCP config (great!) you can do that through the Web UI
-but you have to turn that on in the settings file. Furthermore, files like /etc/cobbler/dhcp.template
-have to be edited locally.
+but you have to turn that on in the settings file (be sure to restart cobblerd after making changes). Furthermore, other configuration and template files in /etc/cobbler (that are not kickstart templates) also have to be edited locally.
+</p>
+
+<p>
+Your Cobbler server administrator may have set cobbler up to allow access to only certain objects for certain user accounts. In the event of this, you will see messages in the interface that indicate when you do not have permissions to perform certain actions. Contact your Cobbler server administrator to resolve these items or to learn more if you have questions.
</p>
<p>
diff --git a/setup.py b/setup.py
index acd94a7..51ffe80 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ import sys
from distutils.core import setup, Extension
import string
-VERSION = "0.8.3"
+VERSION = "0.9.0"
SHORT_DESC = "Network Boot and Update Server"
LONG_DESC = """
Cobbler is a network boot and update server. Cobbler supports PXE, provisioning virtualized images, and reinstalling existing Linux machines. The last two modes require a helper tool called 'koan' that integrates with cobbler. Cobbler's advanced features include importing distributions from DVDs and rsync mirrors, kickstart templating, integrated yum mirroring, and built-in DHCP Management. Cobbler has a Python API for integration with other GPL systems management applications.
@@ -74,6 +74,7 @@ if __name__ == "__main__":
(etcpath, ['config/modules.conf']),
(etcpath, ['config/users.digest']),
(etcpath, ['config/rsync.exclude']),
+ (etcpath, ['config/users.conf']),
(initpath, ['config/cobblerd']),
(cobpath, ['config/settings']),
@@ -137,6 +138,7 @@ if __name__ == "__main__":
(wwwtmpl, ['webui_templates/empty.tmpl']),
(wwwtmpl, ['webui_templates/blank.tmpl']),
+ (wwwtmpl, ['webui_templates/enoaccess.tmpl']),
(wwwtmpl, ['webui_templates/distro_list.tmpl']),
(wwwtmpl, ['webui_templates/distro_edit.tmpl']),
(wwwtmpl, ['webui_templates/profile_list.tmpl']),
@@ -157,7 +159,6 @@ if __name__ == "__main__":
# Web UI kickstart file editing
(wwwtmpl, ['webui_templates/ksfile_edit.tmpl']),
(wwwtmpl, ['webui_templates/ksfile_list.tmpl']),
- (wwwtmpl, ['webui_templates/ksfile_view.tmpl']),
# Web UI support files
(wwwgfx, ['docs/wui.html']),
diff --git a/tests/tests.py b/tests/tests.py
index 99ba0b9..4060eef 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -2,22 +2,13 @@
#
# Michael DeHaan <mdehaan@redhat.com>
-TRY_GRAPH = False
-HAS_GRAPH = False
-
-if TRY_GRAPH:
- try:
- import pycallgraph_mod as pycallgraph
- HAS_GRAPH = True
- except:
- pass
-
import sys
import unittest
import os
import subprocess
import tempfile
import shutil
+import traceback
from cobbler.cexceptions import *
@@ -25,6 +16,7 @@ from cobbler import settings
from cobbler import collection_distros
from cobbler import collection_profiles
from cobbler import collection_systems
+import cobbler.modules.authz_ownership as authz_module
from cobbler import api
@@ -54,11 +46,11 @@ class BootTest(unittest.TestCase):
except:
pass
- self.fk_initrd = os.path.join(self.topdir, FAKE_INITRD)
+ self.fk_initrd = os.path.join(self.topdir, FAKE_INITRD)
self.fk_initrd2 = os.path.join(self.topdir, FAKE_INITRD2)
self.fk_initrd3 = os.path.join(self.topdir, FAKE_INITRD3)
- self.fk_kernel = os.path.join(self.topdir, FAKE_KERNEL)
+ self.fk_kernel = os.path.join(self.topdir, FAKE_KERNEL)
self.fk_kernel2 = os.path.join(self.topdir, FAKE_KERNEL2)
self.fk_kernel3 = os.path.join(self.topdir, FAKE_KERNEL3)
@@ -75,9 +67,6 @@ class BootTest(unittest.TestCase):
shutil.rmtree(self.topdir,ignore_errors=True)
self.api = None
- if HAS_GRAPH:
- pycallgraph.save_dot("%s.dot" % self.__class__.__name__)
-
def make_basic_config(self):
distro = self.api.new_distro()
self.assertTrue(distro.set_name("testdistro0"))
@@ -85,7 +74,7 @@ class BootTest(unittest.TestCase):
self.assertTrue(distro.set_initrd(self.fk_initrd))
self.assertTrue(self.api.add_distro(distro))
self.assertTrue(self.api.find_distro(name="testdistro0"))
-
+
profile = self.api.new_profile()
self.assertTrue(profile.set_name("testprofile0"))
self.assertTrue(profile.set_distro("testdistro0"))
@@ -96,10 +85,271 @@ class BootTest(unittest.TestCase):
system = self.api.new_system()
self.assertTrue(system.set_name("drwily.rdu.redhat.com"))
self.assertTrue(system.set_mac_address("BB:EE:EE:EE:EE:FF","intf0"))
+ self.assertTrue(system.set_ip_address("192.51.51.50","intf0"))
self.assertTrue(system.set_profile("testprofile0"))
self.assertTrue(self.api.add_system(system))
self.assertTrue(self.api.find_system(name="drwily.rdu.redhat.com"))
+ repo = self.api.new_repo()
+ try:
+ os.makedirs("/tmp/test_example_cobbler_repo")
+ except:
+ pass
+ fd = open("/tmp/test_example_cobbler_repo/test.file", "w+")
+ fd.write("hello!")
+ fd.close()
+ self.assertTrue(repo.set_name("test_repo"))
+ self.assertTrue(repo.set_mirror("/tmp/test_example_cobbler_repo"))
+ self.assertTrue(self.api.repos().add(repo))
+
+class DuplicateNamesAndIpPrevention(BootTest):
+
+ """
+ The command line (and WebUI) have checks to prevent new system
+ additions from conflicting with existing systems and overwriting
+ them inadvertantly. This class tests that code. NOTE: General API
+ users will /not/ encounter these checks.
+ """
+
+ def test_duplicate_prevention(self):
+
+ # find things we are going to test with
+ distro1 = self.api.find_distro(name="testdistro0")
+ profile1 = self.api.find_profile(name="testprofile0")
+ system1 = self.api.find_system(name="drwily.rdu.redhat.com")
+ repo1 = self.api.find_repo(name="test_repo")
+
+ # make sure we can't overwrite a previous distro with
+ # the equivalent of an "add" (not an edit) on the
+ # command line.
+ distro2 = self.api.new_distro()
+ self.assertTrue(distro2.set_name("testdistro0"))
+ self.assertTrue(distro2.set_kernel(self.fk_kernel))
+ self.assertTrue(distro2.set_initrd(self.fk_initrd))
+ self.assertTrue(distro2.set_owners("canary"))
+ # this should fail
+ try:
+ self.api.add_distro(distro2,check_for_duplicate_names=True)
+ self.assertTrue(1==2,"distro add should fail")
+ except CobblerException:
+ pass
+ except:
+ self.assertTrue(1==2,"exception type")
+ # we caught the exception but make doubly sure there was no write
+ distro_check = self.api.find_distro(name="testdistro0")
+ self.assertTrue("canary" not in distro_check.owners)
+
+ # repeat the check for profiles
+ profile2 = self.api.new_profile()
+ self.assertTrue(profile2.set_name("testprofile0"))
+ self.assertTrue(profile2.set_distro("testdistro0"))
+ # this should fail
+ try:
+ self.api.add_profile(profile2,check_for_duplicate_names=True)
+ self.assertTrue(1==2,"profile add should fail")
+ except CobblerException:
+ pass
+ except:
+ traceback.print_exc()
+ self.assertTrue(1==2,"exception type")
+
+ # repeat the check for systems (just names this time)
+ system2 = self.api.new_system()
+ self.assertTrue(system2.set_name("drwily.rdu.redhat.com"))
+ self.assertTrue(system2.set_profile("testprofile0"))
+ # this should fail
+ try:
+ self.api.add_system(system2,check_for_duplicate_names=True)
+ self.assertTrue(1==2,"system add should fail")
+ except CobblerException:
+ pass
+ except:
+ traceback.print_exc()
+ self.assertTrue(1==2,"exception type")
+
+ # repeat the check for repos
+ repo2 = self.api.new_repo()
+ self.assertTrue(repo2.set_name("test_repo"))
+ self.assertTrue(repo2.set_mirror("http://imaginary"))
+ # self.failUnlessRaises(CobblerException,self.api.add_repo,[repo,check_for_duplicate_names=True])
+ try:
+ self.api.add_repo(repo2,check_for_duplicate_names=True)
+ self.assertTrue(1==2,"repo add should fail")
+ except CobblerException:
+ pass
+ except:
+ self.assertTrue(1==2,"exception type")
+
+ # now one more check to verify we can't add a system
+ # of a different name but duplicate netinfo.
+ system3 = self.api.new_system()
+ self.assertTrue(system3.set_name("unused_name"))
+ self.assertTrue(system3.set_profile("testprofile0"))
+ # MAC is initially accepted
+ self.assertTrue(system3.set_mac_address("BB:EE:EE:EE:EE:FF","intf3"))
+ # can't add as this MAC already exists!
+
+ #self.failUnlessRaises(CobblerException,self.api.add_system,[system3,check_for_duplicate_names=True,check_for_duplicate_netinfo=True)
+ try:
+ self.api.add_system(system3,check_for_duplicate_names=True,check_for_duplicate_netinfo=True)
+ except CobblerException:
+ pass
+ except:
+ traceback.print_exc()
+ self.assertTrue(1==2,"wrong exception type")
+
+ # set the MAC to a different value and try again
+ self.assertTrue(system3.set_mac_address("FF:EE:EE:EE:EE:DD","intf3"))
+ # it should work
+ self.assertTrue(self.api.add_system(system3,check_for_duplicate_names=True,check_for_duplicate_netinfo=True))
+ # now set the IP so that collides
+ self.assertTrue(system3.set_ip_address("192.51.51.50","intf6"))
+ # this should also fail
+
+ # self.failUnlessRaises(CobblerException,self.api.add_system,[system3,check_for_duplicate_names=True,check_for_duplicate_netinfo=True)
+ try:
+ self.api.add_system(system3,check_for_duplicate_names=True,check_for_duplicate_netinfo=True)
+ self.assertTrue(1==2,"system add should fail")
+ except CobblerException:
+ pass
+ except:
+ self.assertTrue(1==2,"wrong exception type")
+
+ # fix the IP and Mac back
+ self.assertTrue(system3.set_ip_address("192.86.75.30","intf6"))
+ self.assertTrue(system3.set_mac_address("AE:BE:DE:CE:AE:EE","intf3"))
+ # now it works again
+ # note that we will not check for duplicate names as we want
+ # to test this as an 'edit' operation.
+ self.assertTrue(self.api.add_system(system3,check_for_duplicate_names=False,check_for_duplicate_netinfo=True))
+
+ # FIXME: note -- how netinfo is handled when doing renames/copies/edits
+ # is more involved and we probably should add tests for that also.
+
+class Ownership(BootTest):
+
+ def test_ownership_params(self):
+
+ fd = open("/tmp/test_cobbler_kickstart","w+")
+ fd.write("")
+ fd.close()
+
+ # find things we are going to test with
+ distro = self.api.find_distro(name="testdistro0")
+ profile = self.api.find_profile(name="testprofile0")
+ system = self.api.find_system(name="drwily.rdu.redhat.com")
+ repo = self.api.find_repo(name="test_repo")
+
+ # as we didn't specify an owner for objects, the default
+ # ownership should be as specified in settings
+ default_owner = self.api.settings().default_ownership
+ for obj in [ distro, profile, system, repo ]:
+ self.assertTrue(obj is not None)
+ self.assertEquals(obj.owners, default_owner, "default owner for %s" % obj)
+
+ # verify we can test things
+ self.assertTrue(distro.set_owners(["superlab","basement1"]))
+ self.assertTrue(profile.set_owners(["superlab","basement1"]))
+ self.assertTrue(profile.set_kickstart("/tmp/test_cobbler_kickstart"))
+ self.assertTrue(system.set_owners(["superlab","basement1","basement3"]))
+ self.assertTrue(repo.set_owners([]))
+ self.api.add_distro(distro)
+ self.api.add_profile(profile)
+ self.api.add_system(system)
+ self.api.add_repo(repo)
+
+ # now edit the groups file. We won't test the full XMLRPC
+ # auth stack here, but just the module in question
+
+ authorize = authz_module.authorize
+
+ # if the users.conf file exists, back it up for the tests
+ if os.path.exists("/etc/cobbler/users.conf"):
+ shutil.copyfile("/etc/cobbler/users.conf","/tmp/cobbler_ubak")
+
+ fd = open("/etc/cobbler/users.conf","w+")
+ fd.write("\n")
+ fd.write("[admins]\n")
+ fd.write("admin1 = 1\n")
+ fd.write("\n")
+ fd.write("[superlab]\n")
+ fd.write("superlab1 = 1\n")
+ fd.write("superlab2 = 1\n")
+ fd.write("\n")
+ fd.write("[basement]\n")
+ fd.write("basement1 = 1\n")
+ fd.write("basement2 = 1\n")
+ fd.write("basement3 = 1\n")
+ fd.close()
+
+
+ xo = self.api.find_distro("testdistro0")
+ xn = "testdistro0"
+ ro = self.api.find_repo("test_repo")
+ rn = "test_repo"
+
+ # WARNING: complex test explanation follows!
+ # we must ensure those who can edit the kickstart are only those
+ # who can edit all objects that depend on the said kickstart
+ # in this test, superlab & basement1 can edit test_profile0
+ # superlab & basement1/3 can edit test_system0
+ # the systems share a common kickstart record (in this case
+ # explicitly set, which is a bit arbitrary as they are parent/child
+ # nodes, but the concept is not limited to this).
+ # Therefore the correct result is that the following users can edit:
+ # admin1, superlab1, superlab2
+ # And these folks can't
+ # basement1, basement2
+ # Basement2 is rejected because the kickstart is shared by something
+ # basmeent2 can not edit.
+
+ for user in [ "admin1", "superlab1", "superlab2", "basement1" ]:
+ self.assertTrue(1==authorize(self.api, user, "modify_kickstart", "/tmp/test_cobbler_kickstart"), "%s can modify_kickstart" % user)
+
+ for user in [ "basement2", "dne" ]:
+ self.assertTrue(0==authorize(self.api, user, "modify_kickstart", "/tmp/test_cobbler_kickstart"), "%s can modify_kickstart" % user)
+
+ # ensure admin1 can edit (he's an admin) and do other tasks
+ # same applies to basement1 who is explicitly added as a user
+ # and superlab1 who is in a group in the ownership list
+ for user in ["admin1","superlab1","basement1"]:
+ self.assertTrue(1==authorize(self.api, user, "save_distro", xo),"%s can save_distro" % user)
+ self.assertTrue(1==authorize(self.api, user, "modify_distro", xo),"%s can modify_distro" % user)
+ self.assertTrue(1==authorize(self.api, user, "copy_distro", xo),"%s can copy_distro" % user)
+ self.assertTrue(1==authorize(self.api, user, "remove_distro", xn),"%s can remove_distro" % user)
+
+ # ensure all users in the file can sync
+ for user in [ "admin1", "superlab1", "basement1", "basement2" ]:
+ self.assertTrue(1==authorize(self.api, user, "sync"))
+
+ # make sure basement2 can't edit (not in group)
+ # and same goes for "dne" (does not exist in users.conf)
+
+ for user in [ "basement2", "dne" ]:
+ self.assertTrue(0==authorize(self.api, user, "save_distro", xo), "user %s cannot save_distro" % user)
+ self.assertTrue(0==authorize(self.api, user, "modify_distro", xo), "user %s cannot modify_distro" % user)
+ self.assertTrue(0==authorize(self.api, user, "remove_distro", xn), "user %s cannot remove_distro" % user)
+
+ # basement2 is in the file so he can still copy
+ self.assertTrue(1==authorize(self.api, "basement2", "copy_distro", xo), "basement2 can copy_distro")
+
+ # dne can not copy or sync either (not in the users.conf)
+ self.assertTrue(0==authorize(self.api, "dne", "copy_distro", xo), "dne cannot copy_distro")
+ self.assertTrue(0==authorize(self.api, "dne", "sync"), "dne cannot sync")
+
+ # unlike the distro testdistro0, testrepo0 is unowned
+ # so any user in the file will be able to edit it.
+ for user in [ "admin1", "superlab1", "basement1", "basement2" ]:
+ self.assertTrue(1==authorize(self.api, user, "save_repo", ro), "user %s can save_repo" % user)
+
+ # though dne is still not listed and will be denied
+ self.assertTrue(0==authorize(self.api, "dne", "save_repo", ro), "dne cannot save_repo")
+
+ # if we survive, restore the users file as module testing is done
+ if os.path.exists("/tmp/cobbler_ubak"):
+ shutil.copyfile("/etc/cobbler/users.conf","/tmp/cobbler_ubak")
+
+
class MultiNIC(BootTest):
def test_multi_nic_support(self):
@@ -577,14 +827,10 @@ if __name__ == "__main__":
if not os.path.exists("setup.py"):
print "tests: must invoke from top level directory"
sys.exit(1)
- if HAS_GRAPH:
- pycallgraph.start_trace()
loader = unittest.defaultTestLoader
test_module = __import__("tests") # self import considered harmful?
tests = loader.loadTestsFromModule(test_module)
runner = unittest.TextTestRunner()
runner.run(tests)
- if HAS_GRAPH:
- pycallgraph.make_graph('cg_dot.png', tool='dot')
sys.exit(0)
diff --git a/webui_templates/distro_edit.tmpl b/webui_templates/distro_edit.tmpl
index 4729816..f72ab11 100644
--- a/webui_templates/distro_edit.tmpl
+++ b/webui_templates/distro_edit.tmpl
@@ -15,6 +15,11 @@ function disablename(value)
</script>
#end if
+#if $editable != True
+#set global $owners = $distro.owners
+#include "/usr/share/cobbler/webui_templates/enoaccess.tmpl"
+#end if
+
<form method="POST" action="$base_url">
<fieldset id="cform">
@@ -61,6 +66,8 @@ function disablename(value)
<p class="context-tip">How do you want to modify this object?</p>
</td>
</tr>
+ #else
+ <input type="hidden" name="editmode" value="new"/>
#end if
@@ -172,7 +179,27 @@ function disablename(value)
</td>
</tr>
+ <tr>
+ <td>
+ <label for="owners">Access Allowed For</label>
+ </td>
+ <td>
#if $distro
+ #set ownerslist = ','.join($distro.owners)
+ #end if
+ <input type="text" size="255" style="width: 400px;" name="owners" id="owners"
+ #if $distro
+ value="$ownerslist"
+ #else
+ value="$user"
+ #end if
+ />
+ <p class="context-tip">Applies only if using authz_ownership module, comma-delimited</p>
+ </td>
+ </tr>
+
+
+ #if $distro and $editable == True
<tr>
<td>
<label for="delete">Delete</label>
@@ -180,11 +207,13 @@ function disablename(value)
<td>
<input type="checkbox" name="delete1" value="delete1">Yes
<input type="checkbox" name="delete2" value="delete2">Really
+ <input type="checkbox" name="recursive" value="recursive">Delete child objects?
<p class="context-tip">Check both buttons and click save to delete this object</p>
</td>
</tr>
#end if
+ #if $editable == True
<tr>
<td>
</td>
@@ -193,6 +222,7 @@ function disablename(value)
<input type="reset" name="reset" value="Reset"/>
</td>
</tr>
+ #end if
</table>
</fieldset>
diff --git a/webui_templates/enoaccess.tmpl b/webui_templates/enoaccess.tmpl
new file mode 100644
index 0000000..b3a001e
--- /dev/null
+++ b/webui_templates/enoaccess.tmpl
@@ -0,0 +1,12 @@
+#set $myowners = ", ".join($owners)
+
+<blockquote>
+WARNING: You do not have permission to make changes to this
+object. To recieve access, contact your Cobbler server administrator.
+</br>
+
+#if $owners != []
+The access control list for this object is: <B><U>$myowners</U></B>.
+#end if
+</blockquote>
+
diff --git a/webui_templates/ksfile_edit.tmpl b/webui_templates/ksfile_edit.tmpl
index 5e0c5ae..87c9f41 100644
--- a/webui_templates/ksfile_edit.tmpl
+++ b/webui_templates/ksfile_edit.tmpl
@@ -1,7 +1,22 @@
#extends cobbler.webui.master
-#attr $title = "Cobbler: Edit Kickstart File $ksfile"
+##attr $title = "Cobbler: Edit Kickstart File $ksfile"
#block body
+
+#if $editable != True
+<blockquote>
+NOTE: You do not have permission to make changes to this
+kickstart template and can only read it. It is possible that
+other Cobbler users has secured permissions on Cobbler
+profiles/systems that depend on this template -- changing this
+template would ultimately affect those profile/system records which
+you do not have access to. Alternatively, you may not have access
+to edit *any* kickstart templates. Contact your Cobbler server administrator
+if you need to resolve this.
+</blockquote>
+</br>
+#end if
+
<form method="post" action="$base_url?mode=ksfile_save">
<input type="hidden" name="name" value="$name"/>
<fieldset id="cform">
@@ -10,8 +25,20 @@
<pre><textarea rows="40" cols="120" name="ksdata" id="ksdata">$ksdata</textarea></pre>
<br/>
- <input type="submit" name="submit" value="Save"/>
- <input type="reset" name="reset" value="Reset"/>
+ #if $editable == True
+ <input type="submit" name="submit" value="Save"/>
+ <input type="reset" name="reset" value="Reset"/>
+ #end if
+
</fieldset>
</form>
+
+#if $editable == True
+<br/>
+<blockquote>
+NOTE: Run a cobbler sync to after making changes here in order
+for kickstart files to be regenerated.
+</blockquote>
+#end if
+
#end block body
diff --git a/webui_templates/ksfile_view.tmpl b/webui_templates/ksfile_view.tmpl
deleted file mode 100644
index b6abf67..0000000
--- a/webui_templates/ksfile_view.tmpl
+++ /dev/null
@@ -1,6 +0,0 @@
-#extends cobbler.webui.master
-
-#block body
-<pre class="config_data">$ksdata</pre>
-#end block body
-
diff --git a/webui_templates/profile_edit.tmpl b/webui_templates/profile_edit.tmpl
index f9eca09..0c8f156 100644
--- a/webui_templates/profile_edit.tmpl
+++ b/webui_templates/profile_edit.tmpl
@@ -13,6 +13,11 @@ function disablename(value)
</script>
#end if
+#if $editable != True
+#set global $owners = $profile.owners
+#include "/usr/share/cobbler/webui_templates/enoaccess.tmpl"
+#end if
+
<form method="post" action="$base_url?mode=profile_save">
<fieldset id="cform">
@@ -75,6 +80,8 @@ function disablename(value)
<p class="context-tip">How do you want to modify this object?</p>
</td>
</tr>
+ #else
+ <input type="hidden" name="editmode" value="new"/>
#end if
#if $subprofile
@@ -308,8 +315,28 @@ function disablename(value)
</td>
</tr>
-
+ <tr>
+ <td>
+ <label for="owners">Access Allowed For</label>
+ </td>
+ <td>
#if $profile
+ #set ownerslist = ','.join($profile.owners)
+ #end if
+ <input type="text" size="255" style="width: 400px;" name="owners" id="owners"
+ #if $profile
+ value="$ownerslist"
+ #else
+ value="$user"
+ #end if
+
+ />
+ <p class="context-tip">Applies only if using authz_ownership module, comma-delimited</p>
+ </td>
+ </tr>
+
+
+ #if $profile and $editable == True
<tr>
<td>
<label for="delete">Delete</label>
@@ -317,11 +344,13 @@ function disablename(value)
<td>
<input type="checkbox" name="delete1" value="delete1">Yes
<input type="checkbox" name="delete2" value="delete2">Really
+ <input type="checkbox" name="recursive" value="recursive">Delete child objects?
<p class="context-tip">Check both buttons and click save to delete this object</p>
</td>
</tr>
#end if
+ #if $editable == True
<tr>
<td>
</td>
@@ -330,6 +359,7 @@ function disablename(value)
<input type="reset" name="reset" value="Reset"/>
</td>
</tr>
+ #end if
</fieldset>
#end block body
diff --git a/webui_templates/repo_edit.tmpl b/webui_templates/repo_edit.tmpl
index 445218f..30d516d 100644
--- a/webui_templates/repo_edit.tmpl
+++ b/webui_templates/repo_edit.tmpl
@@ -13,6 +13,10 @@ function disablename(value)
</script>
#end if
+#if $editable != True
+#set global $owners = $repo.owners
+#include "/usr/share/cobbler/webui_templates/enoaccess.tmpl"
+#end if
<form method="post" action="$base_url?mode=repo_save">
<fieldset id="cform">
@@ -58,6 +62,8 @@ function disablename(value)
<p class="context-tip">How do you want to modify this object?</p>
</td>
</tr>
+ #else
+ <input type="hidden" name="editmode" value="new"/>
#end if
<tr>
@@ -164,8 +170,27 @@ function disablename(value)
</td>
</tr>
-
+ <tr>
+ <td>
+ <label for="owners">Access Allowed For</label>
+ </td>
+ <td>
#if $repo
+ #set ownerslist = ','.join($repo.owners)
+ #end if
+ <input type="text" size="255" style="width: 400px;" name="owners" id="owners"
+ #if $repo
+ value="$ownerslist"
+ #else
+ value="$user"
+ #end if
+ />
+ <p class="context-tip">Applies only if using authz_ownership module, comma-delimited</p>
+ </td>
+ </tr>
+
+
+ #if $repo and $editable == True
<tr>
<td>
<label for="delete">Delete</label>
@@ -178,6 +203,7 @@ function disablename(value)
</tr>
#end if
+ #if $editable == True
<tr>
<td>
</td>
@@ -185,6 +211,7 @@ function disablename(value)
<input type="submit" name="submit" value="Save"/>
<input type="reset" name="reset" value="Reset"/>
</tr>
+ #end if
</table>
</fieldset>
@@ -192,10 +219,11 @@ function disablename(value)
<br/>
<blockquote>
-Note: Newly added repos contain no package content until "cobbler reposync" is run
-from the command line, which means that profiles relying on these repositories will
-not install. Placing "cobbler reposync" on a crontab to ensure frequent updates
-is recommended procedure.
+Note: Newly added repos contain no package content until
+"cobbler reposync" is run from the command line, which means
+that profiles relying on these repositories will not install.
+Placing "cobbler reposync" on a crontab to ensure frequent
+updates is recommended procedure.
</blockquote>
<br/>
diff --git a/webui_templates/system_edit.tmpl b/webui_templates/system_edit.tmpl
index 22bdda4..684c6d1 100644
--- a/webui_templates/system_edit.tmpl
+++ b/webui_templates/system_edit.tmpl
@@ -14,9 +14,11 @@
function delete_interface(num)
{
- #for $field in $fields
- document.getElementById("${field}-intf" + num).value = "";
- #end for
+ #if $editable == True
+ #for $field in $fields
+ document.getElementById("${field}-intf" + num).value = "";
+ #end for
+ #end if
toggleRowVisibility("id" + num);
}
@@ -65,7 +67,6 @@ function get_random_mac(field)
#set $defined_interfaces = [ "intf0" ]
#end if
-
###
### now generate the onload function.
###
@@ -87,6 +88,11 @@ function page_onload() {
}
</script>
+#if $editable != True
+#set global $owners = $system.owners
+#include "/usr/share/cobbler/webui_templates/enoaccess.tmpl"
+#end if
+
<form method="post" action="$base_url?mode=system_save">
<fieldset id="cform">
@@ -132,6 +138,8 @@ function page_onload() {
<p class="context-tip">How do you want to modify this object?</p>
</td>
</tr>
+ #else
+ <input type="hidden" name="editmode" value="new"/>
#end if
<tr id="id9002">
@@ -208,6 +216,26 @@ function page_onload() {
</td>
</tr>
+ <tr>
+ <td>
+ <label for="owners">Access Allowed For</label>
+ </td>
+ <td>
+ #if $system
+ #set ownerslist = ','.join($system.owners)
+ #end if
+ <input type="text" size="255" style="width: 400px;" name="owners" id="owners"
+ #if $system
+ value="$ownerslist"
+ #else
+ value="$user"
+ #end if
+ />
+ <p class="context-tip">Applies only if using authz_ownership module, comma-delimited</p>
+ </td>
+ </tr>
+
+
## ====================================== start of looping through interfaces
@@ -366,11 +394,20 @@ function page_onload() {
#if $interface != "intf0"
<tr class="listrow" id="child-id${counter}-8">
<td>
- <label for="enabled-$interface">Remove</label>
+ #if $editable == True
+ <label for="enabled-$interface">Remove</label>
+ #else
+ <label for="enabled-$interface">Hide</label>
+ #end if
</td>
<td>
- <input type="button" name="delete-$interface" value="remove" onclick="delete_interface($counter)">
- <p class="context-tip">Clicking this button removes the interface from the configuration.</p>
+ #if $editable == True
+ <input type="button" name="delete-$interface" value="remove" onclick="delete_interface($counter)">
+ <p class="context-tip">Clicking this button removes the interface from the configuration.</p>
+ #else
+ <input type="button" name="delete-$interface" value="hide" onclick="delete_interface($counter)">
+
+ #end if
</td>
</tr>
#end if
@@ -392,7 +429,7 @@ function page_onload() {
</td>
</tr>
- #if $system
+ #if $system and $editable == True
<tr id="id10001">
<td>
<label for="delete">Delete</label>
@@ -405,6 +442,7 @@ function page_onload() {
</tr>
#end if
+ #if $editable == True
<tr id="9008">
<td>
</td>
@@ -413,6 +451,7 @@ function page_onload() {
<input type="reset" name="reset" value="Reset"/>
</td>
</tr>
+ #end if
</table>