"""
Command line interface for cobbler.
"""
from future import standard_library
standard_library.install_aliases()
from builtins import str
from builtins import object
import optparse
import os
import sys
import time
import traceback
import xmlrpc.client
from cobbler import field_info
from cobbler.items import package, system, image, profile, repo, mgmtclass, distro, file
from cobbler import settings
from cobbler import utils
from cobbler.cexceptions import NotImplementedException
OBJECT_ACTIONS_MAP = {
"distro": "add copy edit find list remove rename report".split(" "),
"profile": "add copy dumpvars edit find get-autoinstall list remove rename report".split(" "),
"system": "add copy dumpvars edit find get-autoinstall list remove rename report poweron poweroff powerstatus reboot".split(" "),
"image": "add copy edit find list remove rename report".split(" "),
"repo": "add copy edit find list remove rename report".split(" "),
"mgmtclass": "add copy edit find list remove rename report".split(" "),
"package": "add copy edit find list remove rename report".split(" "),
"file": "add copy edit find list remove rename report".split(" "),
"setting": "edit report".split(" "),
"signature": "reload report update".split(" ")
}
OBJECT_TYPES = list(OBJECT_ACTIONS_MAP.keys())
# would like to use from_iterable here, but have to support python 2.4
OBJECT_ACTIONS = []
for actions in list(OBJECT_ACTIONS_MAP.values()):
OBJECT_ACTIONS += actions
DIRECT_ACTIONS = "aclsetup buildiso import list replicate report reposync sync validate-autoinstalls version signature get-loaders hardlink".split()
####################################################
[docs]def report_items(remote, otype):
if otype == "setting":
items = remote.get_settings()
keys = list(items.keys())
keys.sort()
for key in keys:
item = {'name': key, 'value': items[key]}
report_item(remote, otype, item=item)
elif otype == "signature":
items = remote.get_signatures()
total_breeds = 0
total_sigs = 0
if "breeds" in items:
print("Currently loaded signatures:")
bkeys = list(items["breeds"].keys())
bkeys.sort()
total_breeds = len(bkeys)
for breed in bkeys:
print("%s:" % breed)
oskeys = list(items["breeds"][breed].keys())
oskeys.sort()
if len(oskeys) > 0:
total_sigs += len(oskeys)
for osversion in oskeys:
print("\t%s" % osversion)
else:
print("\t(none)")
print("\n%d breeds with %d total signatures loaded" % (total_breeds, total_sigs))
else:
print("No breeds found in the signature, a signature update is recommended")
sys.exit(1)
else:
items = remote.get_items(otype)
for x in items:
report_item(remote, otype, item=x)
[docs]def report_item(remote, otype, item=None, name=None):
if item is None:
if otype == "setting":
cur_settings = remote.get_settings()
try:
item = {'name': name, 'value': cur_settings[name]}
except:
print("Setting not found: %s" % name)
sys.exit(1)
elif otype == "signature":
items = remote.get_signatures()
total_sigs = 0
if "breeds" in items:
print("Currently loaded signatures:")
if name in items["breeds"]:
print("%s:" % name)
oskeys = list(items["breeds"][name].keys())
oskeys.sort()
if len(oskeys) > 0:
total_sigs += len(oskeys)
for osversion in oskeys:
print("\t%s" % osversion)
else:
print("\t(none)")
print("\nBreed '%s' has %d total signatures" % (name, total_sigs))
else:
print("No breed named '%s' found" % name)
sys.exit(1)
else:
print("No breeds found in the signature, a signature update is recommended")
sys.exit(1)
return
else:
item = remote.get_item(otype, name)
if item == "~":
print("No %s found: %s" % (otype, name))
sys.exit(1)
if otype == "distro":
data = utils.to_string_from_fields(item, distro.FIELDS)
elif otype == "profile":
data = utils.to_string_from_fields(item, profile.FIELDS)
elif otype == "system":
data = utils.to_string_from_fields(item, system.FIELDS,
system.NETWORK_INTERFACE_FIELDS)
elif otype == "repo":
data = utils.to_string_from_fields(item, repo.FIELDS)
elif otype == "image":
data = utils.to_string_from_fields(item, image.FIELDS)
elif otype == "mgmtclass":
data = utils.to_string_from_fields(item, mgmtclass.FIELDS)
elif otype == "package":
data = utils.to_string_from_fields(item, package.FIELDS)
elif otype == "file":
data = utils.to_string_from_fields(item, file.FIELDS)
elif otype == "setting":
data = "%-40s: %s" % (item['name'], item['value'])
print(data)
[docs]def list_items(remote, otype):
items = remote.get_item_names(otype)
items.sort()
for x in items:
print(" %s" % x)
[docs]def n2s(data):
"""
Return spaces for None
"""
if data is None:
return ""
return data
[docs]def opt(options, k, defval=""):
"""
Returns an option from an Optparse values instance
"""
try:
data = getattr(options, k)
except:
# FIXME: debug only
# traceback.print_exc()
return defval
return n2s(data)
def _add_parser_option_from_field(parser, field, settings):
# extract data from field dictionary
name = field[0]
default = field[1]
if isinstance(default, str) and default.startswith("SETTINGS:"):
setting_name = default.replace("SETTINGS:", "", 1)
default = settings[setting_name]
description = field[3]
tooltip = field[5]
choices = field[6]
if choices and default not in choices:
raise Exception("field %s default value (%s) is not listed in choices (%s)" % (name, default, str(choices)))
if tooltip != "":
description += " (%s)" % tooltip
# generate option string
option_string = "--%s" % name.replace("_", "-")
# generate option aliases
aliases = []
for deprecated_field in list(field_info.DEPRECATED_FIELDS.keys()):
if field_info.DEPRECATED_FIELDS[deprecated_field] == name:
aliases.append("--%s" % deprecated_field)
# add option to parser
if isinstance(choices, list) and len(choices) != 0:
description += " (valid options: %s)" % ",".join(choices)
parser.add_option(option_string, dest=name, help=description, choices=choices)
for alias in aliases:
parser.add_option(alias, dest=name, help=description, choices=choices)
else:
parser.add_option(option_string, dest=name, help=description)
for alias in aliases:
parser.add_option(alias, dest=name, help=description)
[docs]def add_options_from_fields(object_type, parser, fields, network_interface_fields, settings, object_action):
if object_action in ["add", "edit", "find", "copy", "rename"]:
for field in fields:
_add_parser_option_from_field(parser, field, settings)
# system object
if object_type == "system":
for field in network_interface_fields:
_add_parser_option_from_field(parser, field, settings)
parser.add_option("--interface", dest="interface", help="the interface to operate on (can only be specified once per command line)")
if object_action in ["add", "edit"]:
parser.add_option("--delete-interface", dest="delete_interface", action="store_true")
parser.add_option("--rename-interface", dest="rename_interface")
if object_action in ["copy", "rename"]:
parser.add_option("--newname", help="new object name")
if object_action not in ["find"] and object_type != "setting":
parser.add_option("--in-place", action="store_true", default=False, dest="in_place", help="edit items in kopts or autoinstall without clearing the other items")
elif object_action == "remove":
parser.add_option("--name", help="%s name to remove" % object_type)
parser.add_option("--recursive", action="store_true", dest="recursive", help="also delete child objects")
# FIXME: not supported in 2.0 ?
# if not object_action in ["dumpvars","find","remove","report","list"]:
# parser.add_option("--no-sync", action="store_true", dest="nosync", help="suppress sync for speed")
[docs]class CobblerCLI(object):
def __init__(self):
# Load server ip and ports from local config
self.url_cobbler_api = utils.local_get_cobbler_api_url()
self.url_cobbler_xmlrpc = utils.local_get_cobbler_xmlrpc_url()
# FIXME: allow specifying other endpoints, and user+pass
self.parser = optparse.OptionParser()
self.remote = xmlrpc.client.Server(self.url_cobbler_api)
self.shared_secret = utils.get_shared_secret()
[docs] def start_task(self, name, options):
options = utils.strip_none(vars(options), omit_none=True)
fn = getattr(self.remote, "background_%s" % name)
return fn(options, self.token)
[docs] def get_object_type(self, args):
"""
If this is a CLI command about an object type, e.g. "cobbler distro add", return the type, like "distro"
"""
if len(args) < 2:
return None
elif args[1] in OBJECT_TYPES:
return args[1]
return None
[docs] def get_object_action(self, object_type, args):
"""
If this is a CLI command about an object type, e.g. "cobbler distro add", return the action, like "add"
"""
if object_type is None or len(args) < 3:
return None
if args[2] in OBJECT_ACTIONS_MAP[object_type]:
return args[2]
return None
[docs] def get_direct_action(self, object_type, args):
"""
If this is a general command, e.g. "cobbler hardlink", return the action, like "hardlink"
"""
if object_type is not None:
return None
elif len(args) < 2:
return None
elif args[1] == "--help":
return None
elif args[1] == "--version":
return "version"
else:
return args[1]
[docs] def check_setup(self):
"""
Detect permissions and service accessibility problems and provide
nicer error messages for them.
"""
s = xmlrpc.client.Server(self.url_cobbler_xmlrpc)
try:
s.ping()
except Exception as e:
print("cobblerd does not appear to be running/accessible: %s" % repr(e), file=sys.stderr)
sys.exit(411)
s = xmlrpc.client.Server(self.url_cobbler_api)
try:
s.ping()
except:
print("httpd does not appear to be running and proxying cobbler, or SELinux is in the way. Original traceback:", file=sys.stderr)
traceback.print_exc()
sys.exit(411)
if not os.path.exists("/var/lib/cobbler/web.ss"):
print("Missing login credentials file. Has cobblerd failed to start?", file=sys.stderr)
sys.exit(411)
if not os.access("/var/lib/cobbler/web.ss", os.R_OK):
print("User cannot run command line, need read access to /var/lib/cobbler/web.ss", file=sys.stderr)
sys.exit(411)
[docs] def run(self, args):
"""
Process the command line and do what the user asks.
"""
self.token = self.remote.login("", self.shared_secret)
object_type = self.get_object_type(args)
object_action = self.get_object_action(object_type, args)
direct_action = self.get_direct_action(object_type, args)
try:
if object_type is not None:
if object_action is not None:
self.object_command(object_type, object_action)
else:
self.print_object_help(object_type)
elif direct_action is not None:
self.direct_command(direct_action)
else:
self.print_help()
except xmlrpc.client.Fault as err:
if err.faultString.find("cobbler.cexceptions.CX") != -1:
print(self.cleanup_fault_string(err.faultString))
else:
print("### ERROR ###")
print("Unexpected remote error, check the server side logs for further info")
print(err.faultString)
sys.exit(1)
[docs] def cleanup_fault_string(self, str):
"""
Make a remote exception nicely readable by humans so it's not evident that is a remote
fault. Users should not have to understand tracebacks.
"""
if str.find(">:") != -1:
(first, rest) = str.split(">:", 1)
if rest.startswith("\"") or rest.startswith("\'"):
rest = rest[1:]
if rest.endswith("\"") or rest.endswith("\'"):
rest = rest[:-1]
return rest
else:
return str
[docs] def get_fields(self, object_type):
"""
For a given name of an object type, return the FIELDS data structure.
"""
# FIXME: this should be in utils, or is it already?
if object_type == "distro":
return distro.FIELDS
elif object_type == "profile":
return profile.FIELDS
elif object_type == "system":
return system.FIELDS
elif object_type == "repo":
return repo.FIELDS
elif object_type == "image":
return image.FIELDS
elif object_type == "mgmtclass":
return mgmtclass.FIELDS
elif object_type == "package":
return package.FIELDS
elif object_type == "file":
return file.FIELDS
elif object_type == "setting":
return settings.FIELDS
[docs] def object_command(self, object_type, object_action):
"""
Process object-based commands such as "distro add" or "profile rename"
"""
task_id = -1 # if assigned, we must tail the logfile
settings = self.remote.get_settings()
fields = self.get_fields(object_type)
network_interface_fields = None
if object_type == "system":
network_interface_fields = system.NETWORK_INTERFACE_FIELDS
if object_action in ["add", "edit", "copy", "rename", "find", "remove"]:
add_options_from_fields(object_type, self.parser, fields,
network_interface_fields, settings, object_action)
elif object_action in ["list"]:
pass
elif object_action not in ("reload", "update"):
self.parser.add_option("--name", dest="name", help="name of object")
elif object_action == "reload":
self.parser.add_option("--filename", dest="filename", help="filename to load data from")
(options, args) = self.parser.parse_args()
# the first three don't require a name
if object_action == "report":
if options.name is not None:
report_item(self.remote, object_type, None, options.name)
else:
report_items(self.remote, object_type)
elif object_action == "list":
list_items(self.remote, object_type)
elif object_action == "find":
items = self.remote.find_items(object_type, utils.strip_none(vars(options), omit_none=True), "name", False)
for item in items:
print(item)
elif object_action in OBJECT_ACTIONS:
if opt(options, "name") == "" and object_action not in ("reload", "update"):
print("--name is required")
sys.exit(1)
if object_action in ["add", "edit", "copy", "rename", "remove"]:
try:
if object_type == "setting":
settings = self.remote.get_settings()
if options.value is None:
raise RuntimeError("You must specify a --value when editing a setting")
elif not settings.get('allow_dynamic_settings', False):
raise RuntimeError("Dynamic settings changes are not enabled. Change the allow_dynamic_settings to 1 and restart cobblerd to enable dynamic settings changes")
elif options.name == 'allow_dynamic_settings':
raise RuntimeError("Cannot modify that setting live")
elif self.remote.modify_setting(options.name, options.value, self.token):
raise RuntimeError("Changing the setting failed")
else:
self.remote.xapi_object_edit(object_type, options.name, object_action, utils.strip_none(vars(options), omit_none=True), self.token)
except xmlrpc.client.Fault as xxx_todo_changeme:
(err) = xxx_todo_changeme
(etype, emsg) = err.faultString.split(":", 1)
print("exception on server: %s" % emsg)
sys.exit(1)
except RuntimeError as xxx_todo_changeme1:
(err) = xxx_todo_changeme1
print(err.args[0])
sys.exit(1)
elif object_action == "get-autoinstall":
if object_type == "profile":
data = self.remote.generate_profile_autoinstall(options.name)
elif object_type == "system":
data = self.remote.generate_system_autoinstall(options.name)
print(data)
elif object_action == "dumpvars":
if object_type == "profile":
data = self.remote.get_blended_data(options.name, "")
elif object_type == "system":
data = self.remote.get_blended_data("", options.name)
# FIXME: pretty-printing and sorting here
keys = list(data.keys())
keys.sort()
for x in keys:
print("%s: %s" % (x, data[x]))
elif object_action in ["poweron", "poweroff", "powerstatus", "reboot"]:
power = {}
power["power"] = object_action.replace("power", "")
power["systems"] = [options.name]
task_id = self.remote.background_power_system(power, self.token)
elif object_action == "update":
task_id = self.remote.background_signature_update(utils.strip_none(vars(options), omit_none=True), self.token)
elif object_action == "reload":
filename = opt(options, "filename", "/var/lib/cobbler/distro_signatures.json")
try:
utils.load_signatures(filename, cache=True)
except:
print("There was an error loading the signature data in %s." % filename)
print("Please check the JSON file or run 'cobbler signature update'.")
return
else:
print("Signatures were successfully loaded")
else:
raise NotImplementedException()
else:
raise NotImplementedException()
# FIXME: add tail/polling code here
if task_id != -1:
self.print_task(task_id)
self.follow_task(task_id)
# BOOKMARK
[docs] def direct_command(self, action_name):
"""
Process non-object based commands like "sync" and "hardlink"
"""
task_id = -1 # if assigned, we must tail the logfile
self.parser.set_usage('Usage: %%prog %s [options]' % (action_name))
if action_name == "buildiso":
defaultiso = os.path.join(os.getcwd(), "generated.iso")
self.parser.add_option("--iso", dest="iso", default=defaultiso, help="(OPTIONAL) output ISO to this file")
self.parser.add_option("--profiles", dest="profiles", help="(OPTIONAL) use these profiles only")
self.parser.add_option("--systems", dest="systems", help="(OPTIONAL) use these systems only")
self.parser.add_option("--tempdir", dest="buildisodir", help="(OPTIONAL) working directory")
self.parser.add_option("--distro", dest="distro", help="(OPTIONAL) used with --standalone and --airgapped to create a distro-based ISO including all associated profiles/systems")
self.parser.add_option("--standalone", dest="standalone", action="store_true", help="(OPTIONAL) creates a standalone ISO with all required distro files, but without any added repos")
self.parser.add_option("--airgapped", dest="airgapped", action="store_true", help="(OPTIONAL) creates a standalone ISO with all distro and repo files for disconnected system installation")
self.parser.add_option("--source", dest="source", help="(OPTIONAL) used with --standalone to specify a source for the distribution files")
self.parser.add_option("--exclude-dns", dest="exclude_dns", action="store_true", help="(OPTIONAL) prevents addition of name server addresses to the kernel boot options")
self.parser.add_option("--mkisofs-opts", dest="mkisofs_opts", help="(OPTIONAL) extra options for mkisofs")
(options, args) = self.parser.parse_args()
task_id = self.start_task("buildiso", options)
elif action_name == "replicate":
self.parser.add_option("--master", dest="master", help="Cobbler server to replicate from.")
self.parser.add_option("--port", dest="port", help="Remote port.")
self.parser.add_option("--distros", dest="distro_patterns", help="patterns of distros to replicate")
self.parser.add_option("--profiles", dest="profile_patterns", help="patterns of profiles to replicate")
self.parser.add_option("--systems", dest="system_patterns", help="patterns of systems to replicate")
self.parser.add_option("--repos", dest="repo_patterns", help="patterns of repos to replicate")
self.parser.add_option("--image", dest="image_patterns", help="patterns of images to replicate")
self.parser.add_option("--mgmtclasses", dest="mgmtclass_patterns", help="patterns of mgmtclasses to replicate")
self.parser.add_option("--packages", dest="package_patterns", help="patterns of packages to replicate")
self.parser.add_option("--files", dest="file_patterns", help="patterns of files to replicate")
self.parser.add_option("--omit-data", dest="omit_data", action="store_true", help="do not rsync data")
self.parser.add_option("--sync-all", dest="sync_all", action="store_true", help="sync all data")
self.parser.add_option("--prune", dest="prune", action="store_true", help="remove objects (of all types) not found on the master")
self.parser.add_option("--use-ssl", dest="use_ssl", action="store_true", help="use ssl to access the Cobbler master server api")
(options, args) = self.parser.parse_args()
task_id = self.start_task("replicate", options)
elif action_name == "aclsetup":
self.parser.add_option("--adduser", dest="adduser", help="give acls to this user")
self.parser.add_option("--addgroup", dest="addgroup", help="give acls to this group")
self.parser.add_option("--removeuser", dest="removeuser", help="remove acls from this user")
self.parser.add_option("--removegroup", dest="removegroup", help="remove acls from this group")
(options, args) = self.parser.parse_args()
task_id = self.start_task("aclsetup", options)
elif action_name == "version":
version = self.remote.extended_version()
print("Cobbler %s" % version["version"])
print(" source: %s, %s" % (version["gitstamp"], version["gitdate"]))
print(" build time: %s" % version["builddate"])
elif action_name == "hardlink":
(options, args) = self.parser.parse_args()
task_id = self.start_task("hardlink", options)
elif action_name == "reserialize":
(options, args) = self.parser.parse_args()
task_id = self.start_task("reserialize", options)
elif action_name == "status":
(options, args) = self.parser.parse_args()
print(self.remote.get_status("text", self.token))
elif action_name == "validate-autoinstalls":
(options, args) = self.parser.parse_args()
task_id = self.start_task("validate_autoinstall_files", options)
elif action_name == "get-loaders":
self.parser.add_option("--force", dest="force", action="store_true", help="overwrite any existing content in /var/lib/cobbler/loaders")
(options, args) = self.parser.parse_args()
task_id = self.start_task("dlcontent", options)
elif action_name == "import":
self.parser.add_option("--arch", dest="arch", help="OS architecture being imported")
self.parser.add_option("--breed", dest="breed", help="the breed being imported")
self.parser.add_option("--os-version", dest="os_version", help="the version being imported")
self.parser.add_option("--path", dest="path", help="local path or rsync location")
self.parser.add_option("--name", dest="name", help="name, ex 'RHEL-5'")
self.parser.add_option("--available-as", dest="available_as", help="tree is here, don't mirror")
self.parser.add_option("--autoinstall", dest="autoinstall_file", help="assign this autoinstall file")
self.parser.add_option("--rsync-flags", dest="rsync_flags", help="pass additional flags to rsync")
(options, args) = self.parser.parse_args()
if options.path and "rsync://" not in options.path:
# convert relative path to absolute path
options.path = os.path.abspath(options.path)
task_id = self.start_task("import", options)
elif action_name == "reposync":
self.parser.add_option("--only", dest="only", help="update only this repository name")
self.parser.add_option("--tries", dest="tries", help="try each repo this many times", default=1)
self.parser.add_option("--no-fail", dest="nofail", help="don't stop reposyncing if a failure occurs", action="store_true")
(options, args) = self.parser.parse_args()
task_id = self.start_task("reposync", options)
elif action_name == "aclsetup":
(options, args) = self.parser.parse_args()
# FIXME: missing options, add them here
task_id = self.start_task("aclsetup", options)
elif action_name == "check":
results = self.remote.check(self.token)
ct = 0
if len(results) > 0:
print("The following are potential configuration items that you may want to fix:\n")
for r in results:
ct += 1
print("%s: %s" % (ct, r))
print("\nRestart cobblerd and then run 'cobbler sync' to apply changes.")
else:
print("No configuration problems found. All systems go.")
elif action_name == "sync":
(options, args) = self.parser.parse_args()
self.parser.add_option("--verbose", dest="verbose", action="store_true", help="run sync with more output")
task_id = self.start_task("sync", options)
elif action_name == "report":
(options, args) = self.parser.parse_args()
print("distros:\n==========")
report_items(self.remote, "distro")
print("\nprofiles:\n==========")
report_items(self.remote, "profile")
print("\nsystems:\n==========")
report_items(self.remote, "system")
print("\nrepos:\n==========")
report_items(self.remote, "repo")
print("\nimages:\n==========")
report_items(self.remote, "image")
print("\nmgmtclasses:\n==========")
report_items(self.remote, "mgmtclass")
print("\npackages:\n==========")
report_items(self.remote, "package")
print("\nfiles:\n==========")
report_items(self.remote, "file")
elif action_name == "list":
# no tree view like 1.6? This is more efficient remotely
# for large configs and prevents xfering the whole config
# though we could consider that...
(options, args) = self.parser.parse_args()
print("distros:")
list_items(self.remote, "distro")
print("\nprofiles:")
list_items(self.remote, "profile")
print("\nsystems:")
list_items(self.remote, "system")
print("\nrepos:")
list_items(self.remote, "repo")
print("\nimages:")
list_items(self.remote, "image")
print("\nmgmtclasses:")
list_items(self.remote, "mgmtclass")
print("\npackages:")
list_items(self.remote, "package")
print("\nfiles:")
list_items(self.remote, "file")
else:
print("No such command: %s" % action_name)
sys.exit(1)
# FIXME: run here
# FIXME: add tail/polling code here
if task_id != -1:
self.print_task(task_id)
self.follow_task(task_id)
return True
[docs] def print_task(self, task_id):
print("task started: %s" % task_id)
events = self.remote.get_events()
(etime, name, status, who_viewed) = events[task_id]
atime = time.asctime(time.localtime(etime))
print("task started (id=%s, time=%s)" % (name, atime))
[docs] def follow_task(self, task_id):
logfile = "/var/log/cobbler/tasks/%s.log" % task_id
# adapted from: http://code.activestate.com/recipes/157035/
file = open(logfile, 'r')
# Find the size of the file and move to the end
# st_results = os.stat(filename)
# st_size = st_results[6]
# file.seek(st_size)
while 1:
where = file.tell()
line = file.readline()
if line.find("### TASK COMPLETE ###") != -1:
print("*** TASK COMPLETE ***")
sys.exit(0)
if line.find("### TASK FAILED ###") != -1:
print("!!! TASK FAILED !!!")
sys.exit(1)
if not line:
time.sleep(1)
file.seek(where)
else:
if line.find(" | "):
line = line.split(" | ")[-1]
print(line, end='')
[docs] def print_object_help(self, object_type):
"""
Prints the subcommands for a given object, e.g. "cobbler distro --help"
"""
commands = OBJECT_ACTIONS_MAP[object_type]
commands.sort()
print("usage\n=====")
for c in commands:
print("cobbler %s %s" % (object_type, c))
sys.exit(2)
[docs] def print_help(self):
"""
Prints general-top level help, e.g. "cobbler --help" or "cobbler" or "cobbler command-does-not-exist"
"""
print("usage\n=====")
print("cobbler <distro|profile|system|repo|image|mgmtclass|package|file> ... ")
print(" [add|edit|copy|get-autoinstall*|list|remove|rename|report] [options|--help]")
print("cobbler <%s> [options|--help]" % "|".join(DIRECT_ACTIONS))
sys.exit(2)
[docs]def main():
"""
CLI entry point
"""
cli = CobblerCLI()
cli.check_setup()
rc = cli.run(sys.argv)
if rc or rc is None:
sys.exit(0)
elif not rc:
sys.exit(1)
return sys.exit(rc)
if __name__ == "__main__":
main()