Source code for cobbler.remote

from past.builtins import cmp
from future import standard_library
standard_library.install_aliases()
from builtins import str
from builtins import range
from builtins import object
from past.utils import old_div
import base64
import errno
import fcntl
import os
import random
import xmlrpc.server
from socketserver import ThreadingMixIn
import stat
from threading import Thread
import time

from cobbler import autoinstall_manager
from cobbler import clogger
from cobbler import configgen
from cobbler.items import package, system, image, profile, repo, mgmtclass, distro, file
from cobbler import tftpgen
from cobbler import utils
from cobbler.cexceptions import CX


EVENT_TIMEOUT = 7 * 24 * 60 * 60        # 1 week
CACHE_TIMEOUT = 10 * 60                 # 10 minutes

# task codes
EVENT_RUNNING = "running"
EVENT_COMPLETE = "complete"
EVENT_FAILED = "failed"

# normal events
EVENT_INFO = "notification"


[docs]class CobblerThread(Thread): """ Code for Cobbler's XMLRPC API. """ def __init__(self, event_id, remote, logatron, options, task_name, api): Thread.__init__(self) self.event_id = event_id self.remote = remote self.logger = logatron if options is None: options = {} self.options = options self.task_name = task_name self.api = api
[docs] def on_done(self): pass
[docs] def run(self): time.sleep(1) try: if utils.run_triggers(self.api, None, "/var/lib/cobbler/triggers/task/%s/pre/*" % self.task_name, self.options, self.logger): self.remote._set_task_state(self, self.event_id, EVENT_FAILED) return False rc = self._run(self) if rc is not None and not rc: self.remote._set_task_state(self, self.event_id, EVENT_FAILED) else: self.remote._set_task_state(self, self.event_id, EVENT_COMPLETE) self.on_done() utils.run_triggers(self.api, None, "/var/lib/cobbler/triggers/task/%s/post/*" % self.task_name, self.options, self.logger) return rc except: utils.log_exc(self.logger) self.remote._set_task_state(self, self.event_id, EVENT_FAILED) return False
# *********************************************************************
[docs]class CobblerXMLRPCInterface(object): """ This is the interface used for all XMLRPC methods, for instance, as used by koan or CobblerWeb. Most read-write operations require a token returned from "login". Read operations do not. """ def __init__(self, api): """ Constructor. Requires a Cobbler API handle. """ self.api = api self.logger = self.api.logger self.token_cache = {} self.object_cache = {} self.timestamp = self.api.last_modified_time() self.events = {} self.shared_secret = utils.get_shared_secret() random.seed(time.time()) self.tftpgen = tftpgen.TFTPGen(api._collection_mgr, self.logger) self.autoinstall_mgr = autoinstall_manager.AutoInstallationManager(api._collection_mgr)
[docs] def check(self, token): """ Returns a list of all the messages/warnings that are things that admin may want to correct about the configuration of the cobbler server. This has nothing to do with "check_access" which is an auth/authz function in the XMLRPC API. """ self.check_access(token, "check") return self.api.check(logger=self.logger)
[docs] def background_buildiso(self, options, token): """ Generates an ISO in /var/www/cobbler/pub that can be used to install profiles without using PXE. """ # FIXME: better use webdir from the settings? webdir = "/var/www/cobbler/" if os.path.exists("/srv/www"): webdir = "/srv/www/cobbler/" def runner(self): self.remote.api.build_iso( self.options.get("iso", webdir + "/pub/generated.iso"), self.options.get("profiles", None), self.options.get("systems", None), self.options.get("buildisodir", None), self.options.get("distro", None), self.options.get("standalone", False), self.options.get("airgapped", False), self.options.get("source", None), self.options.get("exclude_dns", False), self.options.get("mkisofs_opts", None), self.logger ) def on_done(self): if self.options.get("iso", "") == webdir + "/pub/generated.iso": msg = "ISO now available for <A HREF=\"/cobbler/pub/generated.iso\">download</A>" self.remote._new_event(msg) return self.__start_task(runner, token, "buildiso", "Build Iso", options, on_done)
[docs] def background_aclsetup(self, options, token): def runner(self): self.remote.api.acl_config( self.options.get("adduser", None), self.options.get("addgroup", None), self.options.get("removeuser", None), self.options.get("removegroup", None), self.logger ) return self.__start_task(runner, token, "aclsetup", "(CLI) ACL Configuration", options)
[docs] def background_dlcontent(self, options, token): """ Download bootloaders and other support files. """ def runner(self): self.remote.api.dlcontent(self.options.get("force", False), self.logger) return self.__start_task(runner, token, "get_loaders", "Download Bootloader Content", options)
[docs] def background_sync(self, options, token): def runner(self): self.remote.api.sync(self.options.get("verbose", False), logger=self.logger) return self.__start_task(runner, token, "sync", "Sync", options)
[docs] def background_validate_autoinstall_files(self, options, token): def runner(self): return self.remote.api.validate_autoinstall_files(logger=self.logger) return self.__start_task(runner, token, "validate_autoinstall_files", "Automated installation files validation", options)
[docs] def background_replicate(self, options, token): def runner(self): # FIXME: defaults from settings here should come from views, fix in views.py self.remote.api.replicate( self.options.get("master", None), self.options.get("port", ""), self.options.get("distro_patterns", ""), self.options.get("profile_patterns", ""), self.options.get("system_patterns", ""), self.options.get("repo_patterns", ""), self.options.get("image_patterns", ""), self.options.get("mgmtclass_patterns", ""), self.options.get("package_patterns", ""), self.options.get("file_patterns", ""), self.options.get("prune", False), self.options.get("omit_data", False), self.options.get("sync_all", False), self.options.get("use_ssl", False), self.logger ) return self.__start_task(runner, token, "replicate", "Replicate", options)
[docs] def background_import(self, options, token): def runner(self): self.remote.api.import_tree( self.options.get("path", None), self.options.get("name", None), self.options.get("available_as", None), self.options.get("autoinstall_file", None), self.options.get("rsync_flags", None), self.options.get("arch", None), self.options.get("breed", None), self.options.get("os_version", None), self.logger ) return self.__start_task(runner, token, "import", "Media import", options)
[docs] def background_reposync(self, options, token): def runner(self): # NOTE: WebUI passes in repos here, CLI passes only: repos = options.get("repos", []) only = options.get("only", None) if only is not None: repos = [only] nofail = options.get("nofail", len(repos) > 0) if len(repos) > 0: for name in repos: self.remote.api.reposync( tries=self.options.get("tries", 3), name=name, nofail=nofail, logger=self.logger) else: self.remote.api.reposync( tries=self.options.get("tries", 3), name=None, nofail=nofail, logger=self.logger) return self.__start_task(runner, token, "reposync", "Reposync", options)
[docs] def background_power_system(self, options, token): def runner(self): for x in self.options.get("systems", []): try: system_id = self.remote.get_system_handle(x, token) system = self.remote.__get_object(system_id) self.remote.api.power_system(system, self.options.get("power", ""), logger=self.logger) except Exception as e: self.logger.warning("failed to execute power task on %s, exception: %s" % (str(x), str(e))) self.check_access(token, "power_system") return self.__start_task(runner, token, "power", "Power management (%s)" % options.get("power", ""), options)
[docs] def power_system(self, system_id, power, token): """Execute power task synchronously. Returns true if the operation succeeded or if the system is powered on (in case of status). False otherwise. :param token: token from login() call, all tasks require tokens :param system_id: system handle :param power: power operation (on/off/status/reboot) """ system = self.__get_object(system_id) self.check_access(token, "power_system", system) result = self.api.power_system(system, power, logger=self.logger) return True if result is None else result
[docs] def background_signature_update(self, options, token): def runner(self): self.remote.api.signature_update(self.logger) self.check_access(token, "sigupdate") return self.__start_task(runner, token, "sigupdate", "Updating Signatures", options)
[docs] def get_events(self, for_user=""): """ Returns a dict(key=event id) = [ statetime, name, state, [read_by_who] ] If for_user is set to a string, it will only return events the user has not seen yet. If left unset, it will return /all/ events. """ # return only the events the user has not seen self.events_filtered = {} for (k, x) in list(self.events.items()): if for_user in x[3]: pass else: self.events_filtered[k] = x # mark as read so user will not get events again if for_user is not None and for_user != "": for (k, x) in list(self.events.items()): if for_user in x[3]: pass else: self.events[k][3].append(for_user) return self.events_filtered
[docs] def get_event_log(self, event_id): """ Returns the contents of a task log. Events that are not task-based do not have logs. """ event_id = str(event_id).replace("..", "").replace("/", "") path = "/var/log/cobbler/tasks/%s.log" % event_id self._log("getting log for %s" % event_id) if os.path.exists(path): fh = open(path, "r") data = str(fh.read()) fh.close() return data else: return "?"
def __generate_event_id(self, optype): (year, month, day, hour, minute, second, weekday, julian, dst) = time.localtime() return "%04d-%02d-%02d_%02d%02d%02d_%s" % (year, month, day, hour, minute, second, optype) def _new_event(self, name): event_id = self.__generate_event_id("event") event_id = str(event_id) self.events[event_id] = [float(time.time()), str(name), EVENT_INFO, []] def __start_task(self, thr_obj_fn, token, role_name, name, args, on_done=None): """ Starts a new background task. token -- token from login() call, all tasks require tokens role_name -- used to check token against authn/authz layers thr_obj_fn -- function handle to run in a background thread name -- display name to show in logs/events args -- usually this is a single dict, containing options on_done -- an optional second function handle to run after success (and only success) Returns a task id. """ self.check_access(token, role_name) event_id = self.__generate_event_id(role_name) # use short form for logfile suffix event_id = str(event_id) self.events[event_id] = [float(time.time()), str(name), EVENT_RUNNING, []] self._log("start_task(%s); event_id(%s)" % (name, event_id)) logatron = clogger.Logger("/var/log/cobbler/tasks/%s.log" % event_id) thr_obj = CobblerThread(event_id, self, logatron, args, role_name, self.api) thr_obj._run = thr_obj_fn if on_done is not None: thr_obj.on_done = on_done.__get__(thr_obj, CobblerThread) thr_obj.start() return event_id def _set_task_state(self, thread_obj, event_id, new_state): event_id = str(event_id) if event_id in self.events: self.events[event_id][2] = new_state self.events[event_id][3] = [] # clear the list of who has read it if thread_obj is not None: if new_state == EVENT_COMPLETE: thread_obj.logger.info("### TASK COMPLETE ###") if new_state == EVENT_FAILED: thread_obj.logger.error("### TASK FAILED ###")
[docs] def get_task_status(self, event_id): event_id = str(event_id) if event_id in self.events: return self.events[event_id] else: raise CX("no event with that id")
[docs] def last_modified_time(self, token=None): """ Return the time of the last modification to any object. Used to verify from a calling application that no cobbler objects have changed since last check. """ return self.api.last_modified_time()
[docs] def ping(self): """ Deprecated method. Now does nothing. """ return True
[docs] def get_user_from_token(self, token): """ Given a token returned from login, return the username that logged in with it. """ if token not in self.token_cache: raise CX("invalid token: %s" % token) else: return self.token_cache[token][1]
def _log(self, msg, user=None, token=None, name=None, object_id=None, attribute=None, debug=False, error=False): """ Helper function to write data to the log file from the XMLRPC remote implementation. Takes various optional parameters that should be supplied when known. """ # add the user editing the object, if supplied m_user = "?" if user is not None: m_user = user if token is not None: try: m_user = self.get_user_from_token(token) except: # invalid or expired token? m_user = "???" msg = "REMOTE %s; user(%s)" % (msg, m_user) if name is not None: msg = "%s; name(%s)" % (msg, name) if object_id is not None: msg = "%s; object_id(%s)" % (msg, object_id) # add any attributes being modified, if any if attribute: msg = "%s; attribute(%s)" % (msg, attribute) # log to the correct logger if error: logger = self.logger.error elif debug: logger = self.logger.debug else: logger = self.logger.info logger(msg) def __sort(self, data, sort_field=None): """ Helper function used by the various find/search functions to return object representations in order. """ sort_fields = ["name"] sort_rev = False if sort_field is not None: if sort_field.startswith("!"): sort_field = sort_field[1:] sort_rev = True sort_fields.insert(0, sort_field) sortdata = [(x.sort_key(sort_fields), x) for x in data] if sort_rev: sortdata.sort(lambda a, b: cmp(b, a)) else: sortdata.sort() return [x for (key, x) in sortdata] def __paginate(self, data, page=None, items_per_page=None, token=None): """ Helper function to support returning parts of a selection, for example, for use in a web app where only a part of the results are to be presented on each screen. """ default_page = 1 default_items_per_page = 25 try: page = int(page) if page < 1: page = default_page except: page = default_page try: items_per_page = int(items_per_page) if items_per_page <= 0: items_per_page = default_items_per_page except: items_per_page = default_items_per_page num_items = len(data) num_pages = (old_div((num_items - 1), items_per_page)) + 1 if num_pages == 0: num_pages = 1 if page > num_pages: page = num_pages start_item = (items_per_page * (page - 1)) end_item = start_item + items_per_page if start_item > num_items: start_item = num_items - 1 if end_item > num_items: end_item = num_items data = data[start_item:end_item] if page > 1: prev_page = page - 1 else: prev_page = None if page < num_pages: next_page = page + 1 else: next_page = None return (data, { 'page': page, 'prev_page': prev_page, 'next_page': next_page, 'pages': list(range(1, num_pages + 1)), 'num_pages': num_pages, 'num_items': num_items, 'start_item': start_item, 'end_item': end_item, 'items_per_page': items_per_page, 'items_per_page_list': [10, 20, 50, 100, 200, 500], }) def __get_object(self, object_id): """ Helper function. Given an object id, return the actual object. """ if object_id.startswith("___NEW___"): return self.object_cache[object_id][1] (otype, oname) = object_id.split("::", 1) return self.api.get_item(otype, oname)
[docs] def get_item(self, what, name, flatten=False): """ Returns a dict describing a given object. what -- "distro", "profile", "system", "image", "repo", etc name -- the object name to retrieve flatten -- reduce dicts to string representations (True/False) """ self._log("get_item(%s,%s)" % (what, name)) item = self.api.get_item(what, name) if item is not None: item = item.to_dict() if flatten: item = utils.flatten(item) return self.xmlrpc_hacks(item)
[docs] def get_distro(self, name, flatten=False, token=None, **rest): return self.get_item("distro", name, flatten=flatten)
[docs] def get_profile(self, name, flatten=False, token=None, **rest): return self.get_item("profile", name, flatten=flatten)
[docs] def get_system(self, name, flatten=False, token=None, **rest): return self.get_item("system", name, flatten=flatten)
[docs] def get_repo(self, name, flatten=False, token=None, **rest): return self.get_item("repo", name, flatten=flatten)
[docs] def get_image(self, name, flatten=False, token=None, **rest): return self.get_item("image", name, flatten=flatten)
[docs] def get_mgmtclass(self, name, flatten=False, token=None, **rest): return self.get_item("mgmtclass", name, flatten=flatten)
[docs] def get_package(self, name, flatten=False, token=None, **rest): return self.get_item("package", name, flatten=flatten)
[docs] def get_file(self, name, flatten=False, token=None, **rest): return self.get_item("file", name, flatten=flatten)
[docs] def get_items(self, what): """ Returns a list of dicts. what is the name of a cobbler object type, as described for get_item. Individual list elements are the same for get_item. """ items = [x.to_dict() for x in self.api.get_items(what)] for item in items: if "autoinstall" in item: self._log("autoinstall legacy field added as kickstart") kick_dict = {"kickstart": item.get("autoinstall")} item.update(kick_dict) if "autoinstall_meta" in item: self._log("autoinstall_meta legacy field added as ks_meta") kick_meta_dict = {"ks_meta": item.get("autoinstall_meta")} item.update(kick_meta_dict) return self.xmlrpc_hacks(items)
[docs] def get_item_names(self, what): """ Returns a list of object names (keys) for the given object type. This is just like get_items, but transmits less data. """ return [x.name for x in self.api.get_items(what)]
[docs] def get_distros(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("distro")
[docs] def get_profiles(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("profile")
[docs] def get_systems(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("system")
[docs] def get_repos(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("repo")
[docs] def get_images(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("image")
[docs] def get_mgmtclasses(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("mgmtclass")
[docs] def get_packages(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("package")
[docs] def get_files(self, page=None, results_per_page=None, token=None, **rest): return self.get_items("file")
[docs] def find_items(self, what, criteria=None, sort_field=None, expand=True): """Works like get_items but also accepts criteria as a dict to search on. Example: ``{ "name" : "*.example.org" }`` Wildcards work as described by 'pydoc fnmatch'. :returns A list of dicts. """ self._log("find_items(%s); criteria(%s); sort(%s)" % (what, criteria, sort_field)) items = self.api.find_items(what, criteria=criteria) items = self.__sort(items, sort_field) if not expand: items = [x.name for x in items] else: items = [x.to_dict() for x in items] return self.xmlrpc_hacks(items)
[docs] def find_distro(self, criteria=None, expand=False, token=None, **rest): return self.find_items("distro", criteria, expand=expand)
[docs] def find_profile(self, criteria=None, expand=False, token=None, **rest): return self.find_items("profile", criteria, expand=expand)
[docs] def find_system(self, criteria=None, expand=False, token=None, **rest): return self.find_items("system", criteria, expand=expand)
[docs] def find_repo(self, criteria=None, expand=False, token=None, **rest): return self.find_items("repo", criteria, expand=expand)
[docs] def find_image(self, criteria=None, expand=False, token=None, **rest): return self.find_items("image", criteria, expand=expand)
[docs] def find_mgmtclass(self, criteria=None, expand=False, token=None, **rest): return self.find_items("mgmtclass", criteria, expand=expand)
[docs] def find_package(self, criteria=None, expand=False, token=None, **rest): return self.find_items("package", criteria, expand=expand)
[docs] def find_file(self, criteria=None, expand=False, token=None, **rest): return self.find_items("file", criteria, expand=expand)
[docs] def find_items_paged(self, what, criteria=None, sort_field=None, page=None, items_per_page=None, token=None): """Returns a list of dicts as with find_items but additionally supports returning just a portion of the total list, for instance in supporting a web app that wants to show a limited amount of items per page. """ self._log("find_items_paged(%s); criteria(%s); sort(%s)" % (what, criteria, sort_field), token=token) items = self.api.find_items(what, criteria=criteria) items = self.__sort(items, sort_field) (items, pageinfo) = self.__paginate(items, page, items_per_page) items = [x.to_dict() for x in items] return self.xmlrpc_hacks({ 'items': items, 'pageinfo': pageinfo })
[docs] def has_item(self, what, name, token=None): """ Returns True if a given collection has an item with a given name, otherwise returns False. """ self._log("has_item(%s)" % what, token=token, name=name) found = self.api.get_item(what, name) if found is None: return False else: return True
[docs] def get_item_handle(self, what, name, token=None): """ Given the name of an object (or other search parameters), return a reference (object id) that can be used with modify_* functions or save_* functions to manipulate that object. """ found = self.api.get_item(what, name) if found is None: raise CX("internal error, unknown %s name %s" % (what, name)) return "%s::%s" % (what, found.name)
[docs] def get_distro_handle(self, name, token): return self.get_item_handle("distro", name, token)
[docs] def get_profile_handle(self, name, token): return self.get_item_handle("profile", name, token)
[docs] def get_system_handle(self, name, token): return self.get_item_handle("system", name, token)
[docs] def get_repo_handle(self, name, token): return self.get_item_handle("repo", name, token)
[docs] def get_image_handle(self, name, token): return self.get_item_handle("image", name, token)
[docs] def get_mgmtclass_handle(self, name, token): return self.get_item_handle("mgmtclass", name, token)
[docs] def get_package_handle(self, name, token): return self.get_item_handle("package", name, token)
[docs] def get_file_handle(self, name, token): return self.get_item_handle("file", name, token)
[docs] def remove_item(self, what, name, token, recursive=True): """ Deletes an item from a collection. Note that this requires the name of the distro, not an item handle. """ self._log("remove_item (%s, recursive=%s)" % (what, recursive), name=name, token=token) obj = self.api.get_item(what, name) self.check_access(token, "remove_%s" % what, obj) self.api.remove_item(what, name, delete=True, with_triggers=True, recursive=recursive, logger=self.logger) return True
[docs] def remove_distro(self, name, token, recursive=True): return self.remove_item("distro", name, token, recursive)
[docs] def remove_profile(self, name, token, recursive=True): return self.remove_item("profile", name, token, recursive)
[docs] def remove_system(self, name, token, recursive=True): return self.remove_item("system", name, token, recursive)
[docs] def remove_repo(self, name, token, recursive=True): return self.remove_item("repo", name, token, recursive)
[docs] def remove_image(self, name, token, recursive=True): return self.remove_item("image", name, token, recursive)
[docs] def remove_mgmtclass(self, name, token, recursive=True): return self.remove_item("mgmtclass", name, token, recursive)
[docs] def remove_package(self, name, token, recursive=True): return self.remove_item("package", name, token, recursive)
[docs] def remove_file(self, name, token, recursive=True): return self.remove_item("file", name, token, recursive)
[docs] def copy_item(self, what, object_id, newname, token=None): """ Creates a new object that matches an existing object, as specified by an id. """ self._log("copy_item(%s)" % what, object_id=object_id, token=token) self.check_access(token, "copy_%s" % what) obj = self.__get_object(object_id) self.api.copy_item(what, obj, newname, logger=self.logger) return True
[docs] def copy_distro(self, object_id, newname, token=None): return self.copy_item("distro", object_id, newname, token)
[docs] def copy_profile(self, object_id, newname, token=None): return self.copy_item("profile", object_id, newname, token)
[docs] def copy_system(self, object_id, newname, token=None): return self.copy_item("system", object_id, newname, token)
[docs] def copy_repo(self, object_id, newname, token=None): return self.copy_item("repo", object_id, newname, token)
[docs] def copy_image(self, object_id, newname, token=None): return self.copy_item("image", object_id, newname, token)
[docs] def copy_mgmtclass(self, object_id, newname, token=None): return self.copy_item("mgmtclass", object_id, newname, token)
[docs] def copy_package(self, object_id, newname, token=None): return self.copy_item("package", object_id, newname, token)
[docs] def copy_file(self, object_id, newname, token=None): return self.copy_item("file", object_id, newname, token)
[docs] def rename_item(self, what, object_id, newname, token=None): """ Renames an object specified by object_id to a new name. """ self._log("rename_item(%s)" % what, object_id=object_id, token=token) obj = self.__get_object(object_id) self.api.rename_item(what, obj, newname, logger=self.logger) return True
[docs] def rename_distro(self, object_id, newname, token=None): return self.rename_item("distro", object_id, newname, token)
[docs] def rename_profile(self, object_id, newname, token=None): return self.rename_item("profile", object_id, newname, token)
[docs] def rename_system(self, object_id, newname, token=None): return self.rename_item("system", object_id, newname, token)
[docs] def rename_repo(self, object_id, newname, token=None): return self.rename_item("repo", object_id, newname, token)
[docs] def rename_image(self, object_id, newname, token=None): return self.rename_item("image", object_id, newname, token)
[docs] def rename_mgmtclass(self, object_id, newname, token=None): return self.rename_item("mgmtclass", object_id, newname, token)
[docs] def rename_package(self, object_id, newname, token=None): return self.rename_item("package", object_id, newname, token)
[docs] def rename_file(self, object_id, newname, token=None): return self.rename_item("file", object_id, newname, token)
[docs] def new_item(self, what, token, is_subobject=False): """Creates a new (unconfigured) object, returning an object handle that can be used. Creates a new (unconfigured) object, returning an object handle that can be used with ``modify_*`` methods and then finally ``save_*`` methods. The handle only exists in memory until saved. "what" specifies the type of object: ``distro``, ``profile``, ``system``, ``repo``, or ``image`` """ self._log("new_item(%s)" % what, token=token) self.check_access(token, "new_%s" % what) if what == "distro": d = distro.Distro(self.api._collection_mgr, is_subobject=is_subobject) elif what == "profile": d = profile.Profile(self.api._collection_mgr, is_subobject=is_subobject) elif what == "system": d = system.System(self.api._collection_mgr, is_subobject=is_subobject) elif what == "repo": d = repo.Repo(self.api._collection_mgr, is_subobject=is_subobject) elif what == "image": d = image.Image(self.api._collection_mgr, is_subobject=is_subobject) elif what == "mgmtclass": d = mgmtclass.Mgmtclass(self.api._collection_mgr, is_subobject=is_subobject) elif what == "package": d = package.Package(self.api._collection_mgr, is_subobject=is_subobject) elif what == "file": d = file.File(self.api._collection_mgr, is_subobject=is_subobject) else: raise CX("internal error, collection name is %s" % what) key = "___NEW___%s::%s" % (what, self.__get_random(25)) self.object_cache[key] = (time.time(), d) return key
[docs] def new_distro(self, token): return self.new_item("distro", token)
[docs] def new_profile(self, token): return self.new_item("profile", token)
[docs] def new_subprofile(self, token): return self.new_item("profile", token, is_subobject=True)
[docs] def new_system(self, token): return self.new_item("system", token)
[docs] def new_repo(self, token): return self.new_item("repo", token)
[docs] def new_image(self, token): return self.new_item("image", token)
[docs] def new_mgmtclass(self, token): return self.new_item("mgmtclass", token)
[docs] def new_package(self, token): return self.new_item("package", token)
[docs] def new_file(self, token): return self.new_item("file", token)
[docs] def modify_item(self, what, object_id, attribute, arg, token): """ Adjusts the value of a given field, specified by 'what' on a given object id. Allows modification of certain attributes on newly created or existing distro object handle. """ self._log("modify_item(%s)" % what, object_id=object_id, attribute=attribute, token=token) obj = self.__get_object(object_id) self.check_access(token, "modify_%s" % what, obj, attribute) method = obj.get_setter_methods().get(attribute, None) if method is None: # it's ok, the CLI will send over lots of junk we can't process # (like newname or in-place) so just go with it. return False # raise CX("object has no method: %s" % attribute) method(arg) return True
[docs] def modify_distro(self, object_id, attribute, arg, token): return self.modify_item("distro", object_id, attribute, arg, token)
[docs] def modify_profile(self, object_id, attribute, arg, token): return self.modify_item("profile", object_id, attribute, arg, token)
[docs] def modify_system(self, object_id, attribute, arg, token): return self.modify_item("system", object_id, attribute, arg, token)
[docs] def modify_image(self, object_id, attribute, arg, token): return self.modify_item("image", object_id, attribute, arg, token)
[docs] def modify_repo(self, object_id, attribute, arg, token): return self.modify_item("repo", object_id, attribute, arg, token)
[docs] def modify_mgmtclass(self, object_id, attribute, arg, token): return self.modify_item("mgmtclass", object_id, attribute, arg, token)
[docs] def modify_package(self, object_id, attribute, arg, token): return self.modify_item("package", object_id, attribute, arg, token)
[docs] def modify_file(self, object_id, attribute, arg, token): return self.modify_item("file", object_id, attribute, arg, token)
[docs] def modify_setting(self, setting_name, value, token): self._log("modify_setting(%s)" % setting_name, token=token) self.check_access(token, "modify_setting") try: self.api.settings().set(setting_name, value) return 0 except: return 1
def __is_interface_field(self, f): if f in ("delete_interface", "rename_interface"): return True for x in system.NETWORK_INTERFACE_FIELDS: if f == x[0]: return True return False
[docs] def xapi_object_edit(self, object_type, object_name, edit_type, attributes, token): """Extended API: New style object manipulations, 2.0 and later. Extended API: New style object manipulations, 2.0 and later preferred over using ``new_*``, ``modify_*```, ``save_*`` directly. Though we must preserve the old ways for backwards compatibility these cause much less XMLRPC traffic. edit_type - One of 'add', 'rename', 'copy', 'remove' Ex: xapi_object_edit("distro","el5","add",{"kernel":"/tmp/foo","initrd":"/tmp/foo"},token) """ if object_name.strip() == "": raise CX("xapi_object_edit() called without an object name") self.check_access(token, "xedit_%s" % object_type, token) if edit_type == "add" or edit_type == "rename": handle = 0 if edit_type == "rename": tmp_name = attributes["newname"] else: tmp_name = object_name try: handle = self.get_item_handle(object_type, tmp_name) except: pass if handle != 0: raise CX("it seems unwise to overwrite the object %s, try 'edit'", tmp_name) if edit_type == "add": is_subobject = object_type == "profile" and "parent" in attributes if is_subobject and "distro" in attributes: raise CX("You can't change both 'parent' and 'distro'") if object_type == "system": if "profile" not in attributes and "image" not in attributes: raise CX("You must specify a 'profile' or 'image' for new systems") handle = self.new_item(object_type, token, is_subobject=is_subobject) else: handle = self.get_item_handle(object_type, object_name) if edit_type == "rename": self.rename_item(object_type, handle, attributes["newname"], token) handle = self.get_item_handle(object_type, attributes["newname"], token) if edit_type == "copy": is_subobject = object_type == "profile" and "parent" in attributes if is_subobject: if "distro" in attributes: raise CX("You can't change both 'parent' and 'distro'") self.copy_item(object_type, handle, attributes["newname"], token) handle = self.get_item_handle("profile", attributes["newname"], token) self.modify_item("profile", handle, "parent", attributes["parent"], token) else: self.copy_item(object_type, handle, attributes["newname"], token) handle = self.get_item_handle(object_type, attributes["newname"], token) if edit_type in ["copy", "rename"]: del attributes["name"] del attributes["newname"] if edit_type != "remove": # FIXME: this doesn't know about interfaces yet! # if object type is system and fields add to dict and then # modify when done, rather than now. imods = {} # FIXME: needs to know about how to delete interfaces too! for (k, v) in list(attributes.items()): if object_type != "system" or not self.__is_interface_field(k): # in place modifications allow for adding a key/value pair while keeping other k/v # pairs intact. if k in ["autoinstall_meta", "kernel_options", "kernel_options_post", "template_files", "boot_files", "fetchable_files", "params"] and \ "in_place" in attributes and attributes["in_place"]: details = self.get_item(object_type, object_name) v2 = details[k] (ok, input) = utils.input_string_or_dict(v) for (a, b) in list(input.items()): if a.startswith("~") and len(a) > 1: del v2[a[1:]] else: v2[a] = b v = v2 self.modify_item(object_type, handle, k, v, token) else: modkey = "%s-%s" % (k, attributes.get("interface", "")) imods[modkey] = v if object_type == "system": if "delete_interface" not in attributes and "rename_interface" not in attributes: self.modify_system(handle, 'modify_interface', imods, token) elif "delete_interface" in attributes: self.modify_system(handle, 'delete_interface', attributes.get("interface", ""), token) elif "rename_interface" in attributes: ifargs = [attributes.get("interface", ""), attributes.get("rename_interface", "")] self.modify_system(handle, 'rename_interface', ifargs, token) else: # remove item recursive = attributes.get("recursive", False) if object_type == "profile" and recursive is False: childs = len(self.api.find_items(object_type, criteria={'parent': attributes['name']})) if childs > 0: raise CX("Can't delete this profile there are %s subprofiles and 'recursive' is set to 'False'" % childs) self.remove_item(object_type, object_name, token, recursive=recursive) return True # FIXME: use the bypass flag or not? self.save_item(object_type, handle, token) return True
[docs] def save_item(self, what, object_id, token, editmode="bypass"): """ Saves a newly created or modified object to disk. Calling save is required for any changes to persist. """ self._log("save_item(%s)" % what, object_id=object_id, token=token) obj = self.__get_object(object_id) self.check_access(token, "save_%s" % what, obj) if editmode == "new": self.api.add_item(what, obj, check_for_duplicate_names=True, logger=self.logger) else: self.api.add_item(what, obj, logger=self.logger) return True
[docs] def save_distro(self, object_id, token, editmode="bypass"): return self.save_item("distro", object_id, token, editmode=editmode)
[docs] def save_profile(self, object_id, token, editmode="bypass"): return self.save_item("profile", object_id, token, editmode=editmode)
[docs] def save_system(self, object_id, token, editmode="bypass"): return self.save_item("system", object_id, token, editmode=editmode)
[docs] def save_image(self, object_id, token, editmode="bypass"): return self.save_item("image", object_id, token, editmode=editmode)
[docs] def save_repo(self, object_id, token, editmode="bypass"): return self.save_item("repo", object_id, token, editmode=editmode)
[docs] def save_mgmtclass(self, object_id, token, editmode="bypass"): return self.save_item("mgmtclass", object_id, token, editmode=editmode)
[docs] def save_package(self, object_id, token, editmode="bypass"): return self.save_item("package", object_id, token, editmode=editmode)
[docs] def save_file(self, object_id, token, editmode="bypass"): return self.save_item("file", object_id, token, editmode=editmode)
[docs] def get_autoinstall_templates(self, token=None, **rest): """ Returns all of the automatic OS installation templates that are in use by the system. """ self._log("get_autoinstall_templates", token=token) # self.check_access(token, "get_autoinstall_templates") return self.autoinstall_mgr.get_autoinstall_templates()
[docs] def get_autoinstall_snippets(self, token=None, **rest): """ Returns all the automatic OS installation templates' snippets. """ self._log("get_autoinstall_snippets", token=token) return self.autoinstall_mgr.get_autoinstall_snippets()
[docs] def is_autoinstall_in_use(self, ai, token=None, **rest): self._log("is_autoinstall_in_use", token=token) return self.autoinstall_mgr.is_autoinstall_in_use(ai)
[docs] def generate_autoinstall(self, profile=None, system=None, REMOTE_ADDR=None, REMOTE_MAC=None, **rest): self._log("generate_autoinstall") try: return self.autoinstall_mgr.generate_autoinstall(profile, system) except Exception: utils.log_exc(self.logger) return "# This automatic OS installation file had errors that prevented it from being rendered correctly.\n# The cobbler.log should have information relating to this failure."
[docs] def generate_profile_autoinstall(self, profile): return self.generate_autoinstall(profile=profile)
[docs] def generate_system_autoinstall(self, system): return self.generate_autoinstall(system=system)
[docs] def generate_gpxe(self, profile=None, system=None, **rest): self._log("generate_gpxe") return self.api.generate_gpxe(profile, system)
[docs] def generate_bootcfg(self, profile=None, system=None, **rest): self._log("generate_bootcfg") return self.api.generate_bootcfg(profile, system)
[docs] def generate_script(self, profile=None, system=None, name=None, **rest): self._log("generate_script, name is %s" % str(name)) return self.api.generate_script(profile, system, name)
[docs] def get_blended_data(self, profile=None, system=None): if profile is not None and profile != "": obj = self.api.find_profile(profile) if obj is None: raise CX("profile not found: %s" % profile) elif system is not None and system != "": obj = self.api.find_system(system) if obj is None: raise CX("system not found: %s" % system) else: raise CX("internal error, no system or profile specified") return self.xmlrpc_hacks(utils.blender(self.api, True, obj))
[docs] def get_settings(self, token=None, **rest): """ Return the contents of /etc/cobbler/settings, which is a dict. """ self._log("get_settings", token=token) results = self.api.settings().to_dict() self._log("my settings are: %s" % results, debug=True) return self.xmlrpc_hacks(results)
[docs] def get_signatures(self, token=None, **rest): """ Return the contents of the API signatures """ self._log("get_signatures", token=token) results = self.api.get_signatures() return self.xmlrpc_hacks(results)
[docs] def get_valid_breeds(self, token=None, **rest): """ Return the list of valid breeds as read in from the distro signatures data """ self._log("get_valid_breeds", token=token) results = utils.get_valid_breeds() results.sort() return self.xmlrpc_hacks(results)
[docs] def get_valid_os_versions_for_breed(self, breed, token=None, **rest): """ Return the list of valid os_versions for the given breed """ self._log("get_valid_os_versions_for_breed", token=token) results = utils.get_valid_os_versions_for_breed(breed) results.sort() return self.xmlrpc_hacks(results)
[docs] def get_valid_os_versions(self, token=None, **rest): """ Return the list of valid os_versions as read in from the distro signatures data """ self._log("get_valid_os_versions", token=token) results = utils.get_valid_os_versions() results.sort() return self.xmlrpc_hacks(results)
[docs] def get_valid_archs(self, token=None): """ Return the list of valid architectures as read in from the distro signatures data """ self._log("get_valid_archs", token=token) results = utils.get_valid_archs() results.sort() return self.xmlrpc_hacks(results)
[docs] def get_repo_config_for_profile(self, profile_name, **rest): """ Return the yum configuration a given profile should use to obtain all of it's cobbler associated repos. """ obj = self.api.find_profile(profile_name) if obj is None: return "# object not found: %s" % profile_name return self.api.get_repo_config_for_profile(obj)
[docs] def get_repo_config_for_system(self, system_name, **rest): """ Return the yum configuration a given profile should use to obtain all of it's cobbler associated repos. """ obj = self.api.find_system(system_name) if obj is None: return "# object not found: %s" % system_name return self.api.get_repo_config_for_system(obj)
[docs] def get_template_file_for_profile(self, profile_name, path, **rest): """ Return the templated file requested for this profile """ obj = self.api.find_profile(profile_name) if obj is None: return "# object not found: %s" % profile_name return self.api.get_template_file_for_profile(obj, path)
[docs] def get_template_file_for_system(self, system_name, path, **rest): """ Return the templated file requested for this system """ obj = self.api.find_system(system_name) if obj is None: return "# object not found: %s" % system_name return self.api.get_template_file_for_system(obj, path)
[docs] def register_new_system(self, info, token=None, **rest): """ If register_new_installs is enabled in settings, this allows /usr/bin/cobbler-register (part of the koan package) to add new system records remotely if they don't already exist. There is a cobbler_register snippet that helps with doing this automatically for new installs but it can also be used for existing installs. See "AutoRegistration" on the Wiki. """ enabled = self.api.settings().register_new_installs if not str(enabled) in ["1", "y", "yes", "true"]: raise CX("registration is disabled in cobbler settings") # validate input name = info.get("name", "") profile = info.get("profile", "") hostname = info.get("hostname", "") interfaces = info.get("interfaces", {}) ilen = len(list(interfaces.keys())) if name == "": raise CX("no system name submitted") if profile == "": raise CX("profile not submitted") if ilen == 0: raise CX("no interfaces submitted") if ilen >= 64: raise CX("too many interfaces submitted") # validate things first name = info.get("name", "") inames = list(interfaces.keys()) if self.api.find_system(name=name): raise CX("system name conflicts") if hostname != "" and self.api.find_system(hostname=hostname): raise CX("hostname conflicts") for iname in inames: mac = info["interfaces"][iname].get("mac_address", "") ip = info["interfaces"][iname].get("ip_address", "") if ip.find("/") != -1: raise CX("no CIDR ips are allowed") if mac == "": raise CX("missing MAC address for interface %s" % iname) if mac != "": system = self.api.find_system(mac_address=mac) if system is not None: raise CX("mac conflict: %s" % mac) if ip != "": system = self.api.find_system(ip_address=ip) if system is not None: raise CX("ip conflict: %s" % ip) # looks like we can go ahead and create a system now obj = self.api.new_system() obj.set_profile(profile) obj.set_name(name) if hostname != "": obj.set_hostname(hostname) obj.set_netboot_enabled(False) for iname in inames: if info["interfaces"][iname].get("bridge", "") == 1: # don't add bridges continue mac = info["interfaces"][iname].get("mac_address", "") ip = info["interfaces"][iname].get("ip_address", "") netmask = info["interfaces"][iname].get("netmask", "") if mac == "?": # see koan/utils.py for explanation of network info discovery continue obj.set_mac_address(mac, iname) if hostname != "": obj.set_dns_name(hostname, iname) if ip != "" and ip != "?": obj.set_ip_address(ip, iname) if netmask != "" and netmask != "?": obj.set_netmask(netmask, iname) self.api.add_system(obj, logger=self.logger) return 0
[docs] def disable_netboot(self, name, token=None, **rest): """ This is a feature used by the pxe_just_once support, see manpage. Sets system named "name" to no-longer PXE. Disabled by default as this requires public API access and is technically a read-write operation. """ self._log("disable_netboot", token=token, name=name) # used by nopxe.cgi if not self.api.settings().pxe_just_once: # feature disabled! return False if str(self.api.settings().nopxe_with_triggers).upper() in ["1", "Y", "YES", "TRUE"]: # triggers should be enabled when calling nopxe triggers_enabled = True else: triggers_enabled = False systems = self.api.systems() obj = systems.find(name=name) if obj is None: # system not found! return False obj.set_netboot_enabled(0) # disabling triggers and sync to make this extremely fast. systems.add(obj, save=True, with_triggers=triggers_enabled, with_sync=False, quick_pxe_update=True) # re-generate dhcp configuration self.api.sync_dhcp(logger=self.logger) return True
[docs] def upload_log_data(self, sys_name, file, size, offset, data, token=None, **rest): """ This is a logger function used by the "anamon" logging system to upload all sorts of misc data from Anaconda. As it's a bit of a potential log-flooder, it's off by default and needs to be enabled in /etc/cobbler/settings. """ self._log("upload_log_data (file: '%s', size: %s, offset: %s)" % (file, size, offset), token=token, name=sys_name) # Check if enabled in self.api.settings() if not self.api.settings().anamon_enabled: # feature disabled! return False # Find matching system record systems = self.api.systems() obj = systems.find(name=sys_name) if obj is None: # system not found! self._log("upload_log_data - WARNING - system '%s' not found in cobbler" % sys_name, token=token, name=sys_name) return self.__upload_file(sys_name, file, size, offset, data)
def __upload_file(self, sys_name, file, size, offset, data): ''' system: the name of the system name: the name of the file size: size of contents (bytes) data: base64 encoded file contents offset: the offset of the chunk files can be uploaded in chunks, if so the size describes the chunk rather than the whole file. the offset indicates where the chunk belongs the special offset -1 is used to indicate the final chunk ''' contents = base64.decodestring(data) del data if offset != -1: if size is not None: if size != len(contents): return False # XXX - have an incoming dir and move after upload complete # SECURITY - ensure path remains under uploadpath tt = str.maketrans("/", "+") fn = str.translate(file, tt) if fn.startswith('..'): raise CX("invalid filename used: %s" % fn) # FIXME ... get the base dir from cobbler settings() udir = "/var/log/cobbler/anamon/%s" % sys_name if not os.path.isdir(udir): os.mkdir(udir, 0o755) fn = "%s/%s" % (udir, fn) try: st = os.lstat(fn) except OSError as e: if e.errno == errno.ENOENT: pass else: raise else: if not stat.S_ISREG(st.st_mode): raise CX("destination not a file: %s" % fn) fd = os.open(fn, os.O_RDWR | os.O_CREAT, 0o644) # log_error("fd=%r" %fd) try: if offset == 0 or (offset == -1 and size == len(contents)): # truncate file fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) try: os.ftruncate(fd, 0) # log_error("truncating fd %r to 0" %fd) finally: fcntl.lockf(fd, fcntl.LOCK_UN) if offset == -1: os.lseek(fd, 0, 2) else: os.lseek(fd, offset, 0) # write contents fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, len(contents), 0, 2) try: os.write(fd, contents) # log_error("wrote contents") finally: fcntl.lockf(fd, fcntl.LOCK_UN, len(contents), 0, 2) if offset == -1: if size is not None: # truncate file fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) try: os.ftruncate(fd, size) # log_error("truncating fd %r to size %r" % (fd,size)) finally: fcntl.lockf(fd, fcntl.LOCK_UN) finally: os.close(fd) return True
[docs] def run_install_triggers(self, mode, objtype, name, ip, token=None, **rest): """ This is a feature used to run the pre/post install triggers. See CobblerTriggers on Wiki for details """ self._log("run_install_triggers", token=token) if mode != "pre" and mode != "post" and mode != "firstboot": return False if objtype != "system" and objtype != "profile": return False # the trigger script is called with name,mac, and ip as arguments 1,2, and 3 # we do not do API lookups here because they are rather expensive at install # time if reinstalling all of a cluster all at once. # we can do that at "cobbler check" time. utils.run_triggers(self.api, None, "/var/lib/cobbler/triggers/install/%s/*" % mode, additional=[objtype, name, ip], logger=self.logger) return True
[docs] def version(self, token=None, **rest): """ Return the cobbler version for compatibility testing with remote applications. See api.py for documentation. """ self._log("version", token=token) return self.api.version()
[docs] def extended_version(self, token=None, **rest): """ Returns the full dictionary of version information. See api.py for documentation. """ self._log("version", token=token) return self.api.version(extended=True)
[docs] def get_distros_since(self, mtime): """ Return all of the distro objects that have been modified after mtime. """ data = self.api.get_distros_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_profiles_since(self, mtime): """ See documentation for get_distros_since """ data = self.api.get_profiles_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_systems_since(self, mtime): """ See documentation for get_distros_since """ data = self.api.get_systems_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_repos_since(self, mtime): """ See documentation for get_distros_since """ data = self.api.get_repos_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_images_since(self, mtime): """ See documentation for get_distros_since """ data = self.api.get_images_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_mgmtclasses_since(self, mtime): """ See documentation for get_distros_since """ data = self.api.get_mgmtclasses_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_packages_since(self, mtime): """ See documentation for get_distros_since """ data = self.api.get_packages_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_files_since(self, mtime): """ See documentation for get_distros_since """ data = self.api.get_files_since(mtime, collapse=True) return self.xmlrpc_hacks(data)
[docs] def get_repos_compatible_with_profile(self, profile=None, token=None, **rest): """ Get repos that can be used with a given profile name """ self._log("get_repos_compatible_with_profile", token=token) profile = self.api.find_profile(profile) if profile is None: return -1 results = [] distro = profile.get_conceptual_parent() repos = self.get_repos() for r in repos: # there be dragons! # accept all repos that are src/noarch # but otherwise filter what repos are compatible # with the profile based on the arch of the distro. if r["arch"] is None or r["arch"] in ["", "noarch", "src"]: results.append(r) else: # some backwards compatibility fuzz # repo.arch is mostly a text field # distro.arch is i386/x86_64 if r["arch"] in ["i386", "x86", "i686"]: if distro.arch in ["i386", "x86"]: results.append(r) elif r["arch"] in ["x86_64"]: if distro.arch in ["x86_64"]: results.append(r) else: if distro.arch == r["arch"]: results.append(r) return results
# this is used by the puppet external nodes feature
[docs] def find_system_by_dns_name(self, dns_name): # FIXME: expose generic finds for other methods # WARNING: this function is /not/ expected to stay in cobbler long term system = self.api.find_system(dns_name=dns_name) if system is None: return {} else: return self.get_system_as_rendered(system.name)
[docs] def get_distro_as_rendered(self, name, token=None, **rest): """ Get distribution after passing through Cobbler's inheritance engine. @param str name distro name @param str token authentication token """ self._log("get_distro_as_rendered", name=name, token=token) obj = self.api.find_distro(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_profile_as_rendered(self, name, token=None, **rest): """ Get profile after passing through Cobbler's inheritance engine. @param str name profile name @param str token authentication token """ self._log("get_profile_as_rendered", name=name, token=token) obj = self.api.find_profile(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_system_as_rendered(self, name, token=None, **rest): """ Get profile after passing through Cobbler's inheritance engine. @param str name system name @param str token authentication token """ self._log("get_system_as_rendered", name=name, token=token) obj = self.api.find_system(name=name) if obj is not None: _dict = utils.blender(self.api, True, obj) # Generate a pxelinux.cfg? image_based = False profile = obj.get_conceptual_parent() distro = profile.get_conceptual_parent() # the management classes stored in the system are just a list # of names, so we need to turn it into a full list of dictionaries # (right now we just use the params field) mcs = _dict["mgmt_classes"] _dict["mgmt_classes"] = {} for m in mcs: c = self.api.find_mgmtclass(name=m) if c: _dict["mgmt_classes"][m] = c.to_dict() arch = None if distro is None and profile.COLLECTION_TYPE == "image": image_based = True arch = profile.arch else: arch = distro.arch if obj.is_management_supported(): if not image_based: _dict["pxelinux.cfg"] = self.tftpgen.write_pxe_file( None, obj, profile, distro, arch) else: _dict["pxelinux.cfg"] = self.tftpgen.write_pxe_file( None, obj, None, None, arch, image=profile) return self.xmlrpc_hacks(_dict) return self.xmlrpc_hacks({})
[docs] def get_repo_as_rendered(self, name, token=None, **rest): """ Get repository after passing through Cobbler's inheritance engine. @param str name repository name @param str token authentication token """ self._log("get_repo_as_rendered", name=name, token=token) obj = self.api.find_repo(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_image_as_rendered(self, name, token=None, **rest): """ Get repository after passing through Cobbler's inheritance engine. @param str name image name @param str token authentication token """ self._log("get_image_as_rendered", name=name, token=token) obj = self.api.find_image(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_mgmtclass_as_rendered(self, name, token=None, **rest): """ Get management class after passing through Cobbler's inheritance engine @param str name management class name @param str token authentication token """ self._log("get_mgmtclass_as_rendered", name=name, token=token) obj = self.api.find_mgmtclass(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_package_as_rendered(self, name, token=None, **rest): """ Get package after passing through Cobbler's inheritance engine @param str name package name @param str token authentication token """ self._log("get_package_as_rendered", name=name, token=token) obj = self.api.find_package(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_file_as_rendered(self, name, token=None, **rest): """ Get file after passing through Cobbler's inheritance engine @param str name file name @param str token authentication token """ self._log("get_file_as_rendered", name=name, token=token) obj = self.api.find_file(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_distro_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: The name of the distro to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired distro or '~'. """ self._log("get_distro_for_koan", name=name, token=token) obj = self.api.find_distro(name=name) if obj is not None: _dict = utils.blender(self.api, True, obj) _dict["ks_meta"] = _dict["autoinstall_meta"] return self.xmlrpc_hacks(_dict) return self.xmlrpc_hacks({})
[docs] def get_profile_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: The name of the profile to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired profile or '~'. """ self._log("get_profile_for_koan", name=name, token=token) obj = self.api.find_profile(name=name) if obj is not None: _dict = utils.blender(self.api, True, obj) _dict["kickstart"] = _dict["autoinstall"] _dict["ks_meta"] = _dict["autoinstall_meta"] return self.xmlrpc_hacks(_dict) return self.xmlrpc_hacks({})
[docs] def get_system_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: The name of the system to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired system or '~'. """ self._log("get_system_as_rendered", name=name, token=token) obj = self.api.find_system(name=name) if obj is not None: _dict = utils.blender(self.api, True, obj) # Generate a pxelinux.cfg? image_based = False profile = obj.get_conceptual_parent() distro = profile.get_conceptual_parent() # the management classes stored in the system are just a list # of names, so we need to turn it into a full list of dictionaries # (right now we just use the params field) mcs = _dict["mgmt_classes"] _dict["mgmt_classes"] = {} for m in mcs: c = self.api.find_mgmtclass(name=m) if c: _dict["mgmt_classes"][m] = c.to_dict() arch = None if distro is None and profile.COLLECTION_TYPE == "image": image_based = True arch = profile.arch else: arch = distro.arch if obj.is_management_supported(): if not image_based: _dict["pxelinux.cfg"] = self.tftpgen.write_pxe_file( None, obj, profile, distro, arch) else: _dict["pxelinux.cfg"] = self.tftpgen.write_pxe_file( None, obj, None, None, arch, image=profile) # Add legacy fields to the system _dict["kickstart"] = _dict["autoinstall"] _dict["ks_meta"] = _dict["autoinstall_meta"] return self.xmlrpc_hacks(_dict) return self.xmlrpc_hacks({})
[docs] def get_repo_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: The name of the repo to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired repo or '~'. """ self._log("get_repo_for_koan", name=name, token=token) obj = self.api.find_repo(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_image_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: The name of the image to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired image or '~' """ self._log("get_image_for_koan", name=name, token=token) obj = self.api.find_image(name=name) if obj is not None: _dict = utils.blender(self.api, True, obj) _dict["kickstart"] = _dict["autoinstall"] return self.xmlrpc_hacks(_dict) return self.xmlrpc_hacks({})
[docs] def get_mgmtclass_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: Name of the mgmtclass to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired mgmtclass or `~`. """ self._log("get_mgmtclass_for_koan", name=name, token=token) obj = self.api.find_mgmtclass(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_package_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: Name of the package to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired package or '~'. """ self._log("get_package_for_koan", name=name, token=token) obj = self.api.find_package(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_file_for_koan(self, name, token=None, **rest): """ This is a legacy function for 2.6.6 releases. :param name: Name of the file to get. :param token: Auth token to authenticate against the api. :param rest: This is dropped in this method since it is not needed here. :return: The desired file or '~'. """ self._log("get_file_for_koan", name=name, token=token) obj = self.api.find_file(name=name) if obj is not None: return self.xmlrpc_hacks(utils.blender(self.api, True, obj)) return self.xmlrpc_hacks({})
[docs] def get_random_mac(self, virt_type="xenpv", token=None, **rest): """ Wrapper for utils.get_random_mac Used in the webui """ self._log("get_random_mac", token=None) return utils.get_random_mac(self.api, virt_type)
[docs] def xmlrpc_hacks(self, data): """ Convert None in XMLRPC to just '~' to make extra sure a client that can't allow_none can deal with this. ALSO: a weird hack ensuring that when dicts with integer keys (or other types) are transmitted with string keys. """ return utils.strip_none(data)
[docs] def get_status(self, mode="normal", token=None, **rest): """ Returns the same information as `cobbler status` While a read-only operation, this requires a token because it's potentially a fair amount of I/O """ self.check_access(token, "sync") return self.api.status(mode=mode, logger=self.logger)
def __get_random(self, length): urandom = open("/dev/urandom", 'rb') b64 = base64.b64encode(urandom.read(length)) urandom.close() return b64.decode() def __make_token(self, user): """ Returns a new random token. """ b64 = self.__get_random(25) self.token_cache[b64] = (time.time(), user) return b64 def __invalidate_expired_tokens(self): """ Deletes any login tokens that might have expired. Also removes expired events """ timenow = time.time() for token in list(self.token_cache.keys()): (tokentime, user) = self.token_cache[token] if (timenow > tokentime + self.api.settings().auth_token_expiration): self._log("expiring token", token=token, debug=True) del self.token_cache[token] # and also expired objects for oid in list(self.object_cache.keys()): (tokentime, entry) = self.object_cache[oid] if (timenow > tokentime + CACHE_TIMEOUT): del self.object_cache[oid] for tid in list(self.events.keys()): (eventtime, name, status, who) = self.events[tid] if (timenow > eventtime + EVENT_TIMEOUT): del self.events[tid] # logfile cleanup should be dealt w/ by logrotate def __validate_user(self, input_user, input_password): """ Returns whether this user/pass combo should be given access to the cobbler read-write API. For the system user, this answer is always "yes", but it is only valid for the socket interface. FIXME: currently looks for users in /etc/cobbler/auth.conf Would be very nice to allow for PAM and/or just Kerberos. """ return self.api.authenticate(input_user, input_password) def __validate_token(self, token): """ Checks to see if an API method can be called when the given token is passed in. Updates the timestamp of the token automatically to prevent the need to repeatedly call login(). Any method that needs access control should call this before doing anything else. """ self.__invalidate_expired_tokens() if token in self.token_cache: user = self.get_user_from_token(token) if user == "<system>": # system token is only valid over Unix socket return False self.token_cache[token] = (time.time(), user) # update to prevent timeout return True else: self._log("invalid token", token=token) return False 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) if resource.find("mgmtclass") != -1: return self.api.find_mgmtclass(name) if resource.find("package") != -1: return self.api.find_package(name) if resource.find("file") != -1: return self.api.find_file(name) return None
[docs] 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", "image", "mgmtclass", "package", "file"]: 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
[docs] def check_access(self, token, resource, arg1=None, arg2=None): user = self.get_user_from_token(token) if user == "<DIRECT>": self._log("CLI Authorized", debug=True) return True rc = self.api.authorize(user, resource, arg1, arg2) self._log("%s authorization result: %s" % (user, rc), debug=True) if not rc: raise CX("authorization failure for user %s" % user) return rc
[docs] def get_authn_module_name(self, token): user = self.get_user_from_token(token) if user != "<DIRECT>": raise CX("authorization failure for user %s attempting to access authn module name" % user) return self.api.get_module_name_from_file("authentication", "module")
[docs] def login(self, login_user, login_password): """ Takes a username and password, validates it, and if successful returns a random login token which must be used on subsequent method calls. The token will time out after a set interval if not used. Re-logging in permitted. """ # if shared secret access is requested, don't bother hitting the auth # plugin if login_user == "": if login_password == self.shared_secret: return self.__make_token("<DIRECT>") else: utils.die(self.logger, "login failed") # this should not log to disk OR make events as we're going to # call it like crazy in CobblerWeb. Just failed attempts. if self.__validate_user(login_user, login_password): token = self.__make_token(login_user) return token else: utils.die(self.logger, "login failed (%s)" % login_user)
[docs] def logout(self, token): """ Retires a token ahead of the timeout. """ self._log("logout", token=token) if token in self.token_cache: del self.token_cache[token] return True return False
[docs] def token_check(self, token): """ Checks to make sure a token is valid or not """ return self.__validate_token(token)
[docs] def sync_dhcp(self, token): """ Run sync code, which should complete before XMLRPC timeout. We can't do reposync this way. Would be nice to send output over AJAX/other later. """ self._log("sync_dhcp", token=token) self.check_access(token, "sync") self.api.sync_dhcp(logger=self.logger) return True
[docs] def sync(self, token): """ Run sync code, which should complete before XMLRPC timeout. We can't do reposync this way. Would be nice to send output over AJAX/other later. """ # FIXME: performance self._log("sync", token=token) self.check_access(token, "sync") self.api.sync(logger=self.logger) return True
[docs] def read_autoinstall_template(self, file_path, token): """ Read an automatic OS installation template file @param str file_path automatic OS installation template file path @param ? token @return str file content """ what = "read_autoinstall_template" self._log(what, name=file_path, token=token) self.check_access(token, what, file_path, True) return self.autoinstall_mgr.read_autoinstall_template(file_path)
[docs] def write_autoinstall_template(self, file_path, data, token): """ Write an automatic OS installation template file @param str file_path automatic OS installation template file path @param str data new file content @param ? token @return bool if operation was successful """ what = "write_autoinstall_template" self._log(what, name=file_path, token=token) self.check_access(token, what, file_path, True) self.autoinstall_mgr.write_autoinstall_template(file_path, data) return True
[docs] def remove_autoinstall_template(self, file_path, token): """ Remove an automatic OS installation template file @param str file_path automatic OS installation template file path @param ? token @return bool if operation was successful """ what = "write_autoinstall_template" self._log(what, name=file_path, token=token) self.check_access(token, what, file_path, True) self.autoinstall_mgr.remove_autoinstall_template(file_path) return True
[docs] def read_autoinstall_snippet(self, file_path, token): """ Read an automatic OS installation snippet file @param str file_path automatic OS installation snippet file path @param ? token @return str file content """ what = "read_autoinstall_snippet" self._log(what, name=file_path, token=token) self.check_access(token, what, file_path, True) return self.autoinstall_mgr.read_autoinstall_snippet(file_path)
[docs] def write_autoinstall_snippet(self, file_path, data, token): """ Write an automatic OS installation snippet file @param str file_path automatic OS installation snippet file path @param str data new file content @param ? token @return bool if operation was successful """ what = "write_autoinstall_snippet" self._log(what, name=file_path, token=token) self.check_access(token, what, file_path, True) self.autoinstall_mgr.write_autoinstall_snippet(file_path, data) return True
[docs] def remove_autoinstall_snippet(self, file_path, token): """ Remove an automated OS installation snippet file @param str file_path automated OS installation snippet file path @param ? token @return bool if operation was successful """ what = "write_autoinstall_snippet" self._log(what, name=file_path, token=token) self.check_access(token, what, file_path, True) self.autoinstall_mgr.remove_autoinstall_snippet(file_path) return True
[docs] def get_config_data(self, hostname): """ Generate configuration data for the system specified by hostname. """ self._log("get_config_data for %s" % hostname) obj = configgen.ConfigGen(hostname) return obj.gen_config_data_for_koan()
[docs] def clear_system_logs(self, object_id, token=None, logger=None): """ clears console logs of a system """ obj = self.__get_object(object_id) self.check_access(token, "clear_system_logs", obj) self.api.clear_logs(obj, logger=logger) return True
# *********************************************************************************
[docs]class CobblerXMLRPCServer(ThreadingMixIn, xmlrpc.server.SimpleXMLRPCServer): def __init__(self, args): self.allow_reuse_address = True xmlrpc.server.SimpleXMLRPCServer.__init__(self, args)
# *********************************************************************************
[docs]class ProxiedXMLRPCInterface(object): def __init__(self, api, proxy_class): self.proxied = proxy_class(api) self.logger = self.proxied.api.logger def _dispatch(self, method, params, **rest): if method.startswith('_'): raise CX("forbidden method") if not hasattr(self.proxied, method): raise CX("unknown remote method '%s'" % method) method_handle = getattr(self.proxied, method) # FIXME: see if this works without extra boilerplate try: return method_handle(*params) except Exception as e: utils.log_exc(self.logger) raise e
# EOF