diff --git a/Makefile.am b/Makefile.am index 42b41b447c9e1d190af6bb81882f8f050905d932..bacdc3dc7f093142f1b6f1cb1f108164f9975074 100644 --- a/Makefile.am +++ b/Makefile.am @@ -199,9 +199,11 @@ maninput = $(patsubst %.7,%.7.in,$(patsubst %.8,%.8.in,$(man_MANS))) $(patsubst TEST_FILES = \ test/data/bdev-both.txt \ + test/data/bdev-8.3-both.txt \ test/data/bdev-disk.txt \ test/data/bdev-net.txt \ - test/data/proc_drbd8.txt + test/data/proc_drbd8.txt \ + test/data/proc_drbd83.txt dist_TESTS = \ test/ganeti.bdev_unittest.py \ @@ -222,7 +224,7 @@ nodist_TESTS = TESTS = $(dist_TESTS) $(nodist_TESTS) -TESTS_ENVIRONMENT = PYTHONPATH=.:$(top_builddir) +TESTS_ENVIRONMENT = PYTHONPATH=.:$(top_builddir) $(PYTHON) RAPI_RESOURCES = $(wildcard lib/rapi/*.py) diff --git a/daemons/ganeti-masterd b/daemons/ganeti-masterd index 4c6a9681cb63fba3c1f77e1a0fe5ff9c54e33a4d..6a73e4e23ea8fbab8ac4b853be5008c44d853b70 100755 --- a/daemons/ganeti-masterd +++ b/daemons/ganeti-masterd @@ -36,7 +36,6 @@ import collections import Queue import random import signal -import simplejson import logging from cStringIO import StringIO @@ -55,6 +54,7 @@ from ganeti import ssconf from ganeti import workerpool from ganeti import rpc from ganeti import bootstrap +from ganeti import serializer CLIENT_REQUEST_WORKERS = 16 @@ -152,7 +152,7 @@ class ClientRqHandler(SocketServer.BaseRequestHandler): logging.debug("client closed connection") break - request = simplejson.loads(msg) + request = serializer.LoadJson(msg) logging.debug("request: %s", request) if not isinstance(request, dict): logging.error("wrong request received: %s", msg) @@ -181,7 +181,7 @@ class ClientRqHandler(SocketServer.BaseRequestHandler): luxi.KEY_RESULT: result, } logging.debug("response: %s", response) - self.send_message(simplejson.dumps(response)) + self.send_message(serializer.DumpJson(response)) def read_message(self): while not self._msgs: diff --git a/daemons/ganeti-noded b/daemons/ganeti-noded index 3a2b4a092d5ae99cb4a35d0b8e917d6fffa5cdee..9600e30ec6576cf41f1ba360ba81e7688bba5626 100755 --- a/daemons/ganeti-noded +++ b/daemons/ganeti-noded @@ -721,7 +721,7 @@ def ParseOptions(): """ parser = OptionParser(description="Ganeti node daemon", - usage="%prog [-f] [-d]", + usage="%prog [-f] [-d] [-b ADDRESS]", version="%%prog (ganeti) %s" % constants.RELEASE_VERSION) @@ -731,6 +731,10 @@ def ParseOptions(): parser.add_option("-d", "--debug", dest="debug", help="Enable some debug messages", default=False, action="store_true") + parser.add_option("-b", "--bind", dest="bind_address", + help="Bind address", + default="", metavar="ADDRESS") + options, args = parser.parse_args() return options, args @@ -781,7 +785,7 @@ def main(): queue_lock = jstore.InitAndVerifyQueue(must_lock=False) mainloop = daemon.Mainloop() - server = NodeHttpServer(mainloop, "", port, + server = NodeHttpServer(mainloop, options.bind_address, port, ssl_params=ssl_params, ssl_verify_peer=True) server.Start() try: diff --git a/daemons/ganeti-watcher b/daemons/ganeti-watcher index 42a2eaf832857cb2163a7385fb962051af540ab4..2749de63f77a0a2ae6c99ed0e3b13642a0e9ba5d 100755 --- a/daemons/ganeti-watcher +++ b/daemons/ganeti-watcher @@ -80,6 +80,20 @@ def StartMaster(): return not result.failed +def EnsureDaemon(daemon): + """Check for and start daemon if not alive. + + """ + pidfile = utils.DaemonPidFileName(daemon) + pid = utils.ReadPidFile(pidfile) + if pid == 0 or not utils.IsProcessAlive(pid): # no file or dead pid + logging.debug("Daemon '%s' not alive, trying to restart", daemon) + result = utils.RunCmd([daemon]) + if not result: + logging.error("Can't start daemon '%s', failure %s, output: %s", + daemon, result.fail_reason, result.output) + + class WatcherState(object): """Interface to a state file recording restart attempts. @@ -255,10 +269,17 @@ def GetClusterData(): all_results = cli.PollJob(job_id, cl=client, feedback_fn=logging.debug) + logging.debug("Got data from cluster, writing instance status file") + result = all_results[0] smap = {} instances = {} + + # write the upfile + up_data = "".join(["%s %s\n" % (fields[0], fields[1]) for fields in result]) + utils.WriteFile(file_name=constants.INSTANCE_UPFILE, data=up_data) + for fields in result: (name, status, autostart, snodes) = fields @@ -291,6 +312,9 @@ class Watcher(object): master = client.QueryConfigValues(["master_node"])[0] if master != utils.HostInfo().name: raise NotMasterError("This is not the master node") + # first archive old jobs + self.ArchiveJobs(opts.job_age) + # and only then submit new ones self.instances, self.bootids, self.smap = GetClusterData() self.started_instances = set() self.opts = opts @@ -300,12 +324,12 @@ class Watcher(object): """ notepad = self.notepad - self.ArchiveJobs(self.opts.job_age) self.CheckInstances(notepad) self.CheckDisks(notepad) self.VerifyDisks() - def ArchiveJobs(self, age): + @staticmethod + def ArchiveJobs(age): """Archive old jobs. """ @@ -452,8 +476,12 @@ def main(): utils.SetupLogging(constants.LOG_WATCHER, debug=options.debug, stderr_logging=options.debug) - update_file = True + update_file = False try: + # on master or not, try to start the node dameon (use _PID but is + # the same as daemon name) + EnsureDaemon(constants.NODED_PID) + notepad = WatcherState() try: try: @@ -461,24 +489,30 @@ def main(): except errors.OpPrereqError: # this is, from cli.GetClient, a not-master case logging.debug("Not on master, exiting") + update_file = True sys.exit(constants.EXIT_SUCCESS) except luxi.NoMasterError, err: logging.warning("Master seems to be down (%s), trying to restart", str(err)) if not StartMaster(): logging.critical("Can't start the master, exiting") - update_file = False sys.exit(constants.EXIT_FAILURE) # else retry the connection client = cli.GetClient() + # we are on master now (use _PID but is the same as daemon name) + EnsureDaemon(constants.RAPI_PID) + try: watcher = Watcher(options, notepad) except errors.ConfigurationError: # Just exit if there's no configuration + update_file = True sys.exit(constants.EXIT_SUCCESS) watcher.Run() + update_file = True + finally: if update_file: notepad.Save() @@ -492,6 +526,10 @@ def main(): except errors.ResolverError, err: logging.error("Cannot resolve hostname '%s', exiting.", err.args[0]) sys.exit(constants.EXIT_NODESETUP_ERROR) + except errors.JobQueueFull: + logging.error("Job queue is full, can't query cluster state") + except errors.JobQueueDrainError: + logging.error("Job queue is drained, can't maintain cluster state") except Exception, err: logging.error(str(err), exc_info=True) sys.exit(constants.EXIT_FAILURE) diff --git a/doc/examples/ganeti.initd.in b/doc/examples/ganeti.initd.in index 10bc684da88732f8a882f9932d6d0dbe40a11966..b346530d9001ea025c02f84ec074e063785f7feb 100644 --- a/doc/examples/ganeti.initd.in +++ b/doc/examples/ganeti.initd.in @@ -16,17 +16,22 @@ DESC="Ganeti cluster" GANETIRUNDIR="@LOCALSTATEDIR@/run/ganeti" +GANETI_DEFAULTS_FILE="@SYSCONFDIR@/default/ganeti" + NODED_NAME="ganeti-noded" NODED="@PREFIX@/sbin/${NODED_NAME}" NODED_PID="${GANETIRUNDIR}/${NODED_NAME}.pid" +NODED_ARGS="" MASTERD_NAME="ganeti-masterd" MASTERD="@PREFIX@/sbin/${MASTERD_NAME}" MASTERD_PID="${GANETIRUNDIR}/${MASTERD_NAME}.pid" +MASTERD_ARGS="" RAPI_NAME="ganeti-rapi" RAPI="@PREFIX@/sbin/${RAPI_NAME}" RAPI_PID="${GANETIRUNDIR}/${RAPI_NAME}.pid" +RAPI_ARGS="" SCRIPTNAME="@SYSCONFDIR@/init.d/ganeti" @@ -34,6 +39,10 @@ test -f $NODED || exit 0 . /lib/lsb/init-functions +if [ -s $GANETI_DEFAULTS_FILE ]; then + . $GANETI_DEFAULTS_FILE +fi + check_config() { for fname in \ "@LOCALSTATEDIR@/lib/ganeti/server.pem" @@ -84,16 +93,16 @@ case "$1" in start) log_daemon_msg "Starting $DESC" "$NAME" check_config - start_action $NODED $NODED_PID - start_action $MASTERD $MASTERD_PID - start_action $RAPI $RAPI_PID - ;; + start_action $NODED $NODED_PID $NODED_ARGS + start_action $MASTERD $MASTERD_PID $MASTERD_ARGS + start_action $RAPI $RAPI_PID $RAPI_ARGS + ;; stop) log_daemon_msg "Stopping $DESC" "$NAME" stop_action $RAPI $RAPI_PID stop_action $MASTERD $MASTERD_PID stop_action $NODED $NODED_PID - ;; + ;; restart|force-reload) log_daemon_msg "Reloading $DESC" stop_action $RAPI $RAPI_PID @@ -103,11 +112,11 @@ case "$1" in start_action $NODED $NODED_PID start_action $MASTERD $MASTERD_PID start_action $RAPI $RAPI_PID - ;; + ;; *) log_success_msg "Usage: $SCRIPTNAME {start|stop|force-reload|restart}" exit 1 - ;; + ;; esac exit 0 diff --git a/lib/backend.py b/lib/backend.py index 18d439e869c08ff0f27edff13218e99dc81d7a6d..54dc0237e354a0f64496c28271472aedca725eed 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -1690,6 +1690,10 @@ def OSEnvironment(instance, debug=0): result['NIC_%d_FRONTEND_TYPE' % idx] = \ instance.hvparams[constants.HV_NIC_TYPE] + for source, kind in [(instance.beparams, "BE"), (instance.hvparams, "HV")]: + for key, value in source.items(): + result["INSTANCE_%s_%s" % (kind, key)] = str(value) + return result def BlockdevGrow(disk, amount): diff --git a/lib/bdev.py b/lib/bdev.py index 545cc0b5729e8baa89dbfdbee7d7cac3ee217e90..9d3f08b096e3c71fa215f1a268313fc483e1b26a 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -563,7 +563,7 @@ class DRBD8Status(object): """ UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$") - LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+st:([^/]+)/(\S+)" + LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)" "\s+ds:([^/]+)/(\S+)\s+.*$") SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*" "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$") @@ -896,15 +896,20 @@ class DRBD8(BaseDRBD): # value types value = pyp.Word(pyp.alphanums + '_-/.:') quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote - addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() + - number) + addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() + + pyp.Optional(pyp.Literal("ipv6")).suppress()) + addr_port = (addr_type + pyp.Word(pyp.nums + '.') + + pyp.Literal(':').suppress() + number) # meta device, extended syntax meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() + number + pyp.Word(']').suppress()) + # device name, extended syntax + device_value = pyp.Literal("minor").suppress() + number # a statement stmt = (~rbrace + keyword + ~lbrace + - pyp.Optional(addr_port ^ value ^ quoted ^ meta_value) + + pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^ + device_value) + pyp.Optional(defa) + semi + pyp.Optional(pyp.restOfLine).suppress()) diff --git a/lib/bootstrap.py b/lib/bootstrap.py index edee90d3c92f7eb2f9eae88096337f85ea174e31..0308484e03b81aea8739bc31598abcf359ff1ca9 100644 --- a/lib/bootstrap.py +++ b/lib/bootstrap.py @@ -25,7 +25,6 @@ import os import os.path -import sha import re import logging import tempfile diff --git a/lib/cli.py b/lib/cli.py index 6db7cea77126585f44bdd4d178a8b2eb9e189f90..17dc47c174d21617eb4b85943ec007fb27e784b0 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -815,6 +815,8 @@ def GenerateTable(headers, fields, separator, data, format = separator.replace("%", "%%").join(format_fields) for row in data: + if row is None: + continue for idx, val in enumerate(row): if unitfields.Matches(fields[idx]): try: @@ -840,6 +842,8 @@ def GenerateTable(headers, fields, separator, data, for line in data: args = [] + if line is None: + line = ['-' for _ in fields] for idx in xrange(len(fields)): if separator is None: args.append(mlens[idx]) diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 26ea707538b7e8b17db40968568f9d48fb5c0a4e..1ab20ac9203091a47f3951dd3f5855cda3cc64c4 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -25,7 +25,6 @@ import os import os.path -import sha import time import tempfile import re @@ -454,7 +453,8 @@ def _CheckNodeNotDrained(lu, node): def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status, - memory, vcpus, nics, disk_template, disks): + memory, vcpus, nics, disk_template, disks, + bep, hvp, hypervisor): """Builds instance related env variables for hooks This builds the hook environment from individual variables. @@ -480,6 +480,12 @@ def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status, @param disk_template: the distk template of the instance @type disks: list @param disks: the list of (size, mode) pairs + @type bep: dict + @param bep: the backend parameters for the instance + @type hvp: dict + @param hvp: the hypervisor parameters for the instance + @type hypervisor: string + @param hypervisor: the hypervisor for the instance @rtype: dict @return: the hook environment for this instance @@ -498,6 +504,7 @@ def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status, "INSTANCE_MEMORY": memory, "INSTANCE_VCPUS": vcpus, "INSTANCE_DISK_TEMPLATE": disk_template, + "INSTANCE_HYPERVISOR": hypervisor, } if nics: @@ -523,6 +530,10 @@ def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status, env["INSTANCE_DISK_COUNT"] = disk_count + for source, kind in [(bep, "BE"), (hvp, "HV")]: + for key, value in source.items(): + env["INSTANCE_%s_%s" % (kind, key)] = value + return env @@ -541,7 +552,9 @@ def _BuildInstanceHookEnvByObject(lu, instance, override=None): @return: the hook environment dictionary """ - bep = lu.cfg.GetClusterInfo().FillBE(instance) + cluster = lu.cfg.GetClusterInfo() + bep = cluster.FillBE(instance) + hvp = cluster.FillHV(instance) args = { 'name': instance.name, 'primary_node': instance.primary_node, @@ -553,6 +566,9 @@ def _BuildInstanceHookEnvByObject(lu, instance, override=None): 'nics': [(nic.ip, nic.bridge, nic.mac) for nic in instance.nics], 'disk_template': instance.disk_template, 'disks': [(disk.size, disk.mode) for disk in instance.disks], + 'bep': bep, + 'hvp': hvp, + 'hypervisor': instance.hypervisor, } if override: args.update(override) @@ -1524,8 +1540,11 @@ class LUSetClusterParams(LogicalUnit): """ if self.op.vg_name is not None: - if self.op.vg_name != self.cfg.GetVGName(): - self.cfg.SetVGName(self.op.vg_name) + new_volume = self.op.vg_name + if not new_volume: + new_volume = None + if new_volume != self.cfg.GetVGName(): + self.cfg.SetVGName(new_volume) else: feedback_fn("Cluster LVM configuration already in desired" " state, not changing") @@ -2441,6 +2460,10 @@ class LUQueryClusterInfo(NoHooksLU): for hypervisor in cluster.enabled_hypervisors]), "beparams": cluster.beparams, "candidate_pool_size": cluster.candidate_pool_size, + "default_bridge": cluster.default_bridge, + "master_netdev": cluster.master_netdev, + "volume_group_name": cluster.volume_group_name, + "file_storage_dir": cluster.file_storage_dir, } return result @@ -2755,15 +2778,48 @@ class LUStartupInstance(LogicalUnit): assert self.instance is not None, \ "Cannot retrieve locked instance %s" % self.op.instance_name + # extra beparams + self.beparams = getattr(self.op, "beparams", {}) + if self.beparams: + if not isinstance(self.beparams, dict): + raise errors.OpPrereqError("Invalid beparams passed: %s, expected" + " dict" % (type(self.beparams), )) + # fill the beparams dict + utils.ForceDictType(self.beparams, constants.BES_PARAMETER_TYPES) + self.op.beparams = self.beparams + + # extra hvparams + self.hvparams = getattr(self.op, "hvparams", {}) + if self.hvparams: + if not isinstance(self.hvparams, dict): + raise errors.OpPrereqError("Invalid hvparams passed: %s, expected" + " dict" % (type(self.hvparams), )) + + # check hypervisor parameter syntax (locally) + cluster = self.cfg.GetClusterInfo() + utils.ForceDictType(self.hvparams, constants.HVS_PARAMETER_TYPES) + filled_hvp = cluster.FillDict(cluster.hvparams[instance.hypervisor], + instance.hvparams) + filled_hvp.update(self.hvparams) + hv_type = hypervisor.GetHypervisor(instance.hypervisor) + hv_type.CheckParameterSyntax(filled_hvp) + _CheckHVParams(self, instance.all_nodes, instance.hypervisor, filled_hvp) + self.op.hvparams = self.hvparams + _CheckNodeOnline(self, instance.primary_node) bep = self.cfg.GetClusterInfo().FillBE(instance) # check bridges existance _CheckInstanceBridgesExist(self, instance) - _CheckNodeFreeMemory(self, instance.primary_node, - "starting instance %s" % instance.name, - bep[constants.BE_MEMORY], instance.hypervisor) + remote_info = self.rpc.call_instance_info(instance.primary_node, + instance.name, + instance.hypervisor) + remote_info.Raise() + if not remote_info.data: + _CheckNodeFreeMemory(self, instance.primary_node, + "starting instance %s" % instance.name, + bep[constants.BE_MEMORY], instance.hypervisor) def Exec(self, feedback_fn): """Start the instance. @@ -2778,7 +2834,8 @@ class LUStartupInstance(LogicalUnit): _StartInstanceDisks(self, instance, force) - result = self.rpc.call_instance_start(node_current, instance) + result = self.rpc.call_instance_start(node_current, instance, + self.hvparams, self.beparams) msg = result.RemoteFailMsg() if msg: _ShutdownInstanceDisks(self, instance) @@ -2860,7 +2917,7 @@ class LURebootInstance(LogicalUnit): " full reboot: %s" % msg) _ShutdownInstanceDisks(self, instance) _StartInstanceDisks(self, instance, ignore_secondaries) - result = self.rpc.call_instance_start(node_current, instance) + result = self.rpc.call_instance_start(node_current, instance, None, None) msg = result.RemoteFailMsg() if msg: _ShutdownInstanceDisks(self, instance) @@ -2960,7 +3017,8 @@ class LUReinstallInstance(LogicalUnit): remote_info = self.rpc.call_instance_info(instance.primary_node, instance.name, instance.hypervisor) - if remote_info.failed or remote_info.data: + remote_info.Raise() + if remote_info.data: raise errors.OpPrereqError("Instance '%s' is running on the node %s" % (self.op.instance_name, instance.primary_node)) @@ -3478,10 +3536,15 @@ class LUFailoverInstance(LogicalUnit): target_node = secondary_nodes[0] _CheckNodeOnline(self, target_node) _CheckNodeNotDrained(self, target_node) - # check memory requirements on the secondary node - _CheckNodeFreeMemory(self, target_node, "failing over instance %s" % - instance.name, bep[constants.BE_MEMORY], - instance.hypervisor) + + if instance.admin_up: + # check memory requirements on the secondary node + _CheckNodeFreeMemory(self, target_node, "failing over instance %s" % + instance.name, bep[constants.BE_MEMORY], + instance.hypervisor) + else: + self.LogInfo("Not checking memory on the secondary node as" + " instance will not be started") # check bridge existance brlist = [nic.bridge for nic in instance.nics] @@ -3550,7 +3613,7 @@ class LUFailoverInstance(LogicalUnit): raise errors.OpExecError("Can't activate the instance's disks") feedback_fn("* starting the instance on the target node") - result = self.rpc.call_instance_start(target_node, instance) + result = self.rpc.call_instance_start(target_node, instance, None, None) msg = result.RemoteFailMsg() if msg: _ShutdownInstanceDisks(self, instance) @@ -4300,6 +4363,7 @@ class LUCreateInstance(LogicalUnit): self.op.hvparams) hv_type = hypervisor.GetHypervisor(self.op.hypervisor) hv_type.CheckParameterSyntax(filled_hvp) + self.hv_full = filled_hvp # fill and remember the beparams dict utils.ForceDictType(self.op.beparams, constants.BES_PARAMETER_TYPES) @@ -4477,6 +4541,9 @@ class LUCreateInstance(LogicalUnit): nics=[(n.ip, n.bridge, n.mac) for n in self.nics], disk_template=self.op.disk_template, disks=[(d["size"], d["mode"]) for d in self.disks], + bep=self.be_full, + hvp=self.hv_full, + hypervisor=self.op.hypervisor, )) nl = ([self.cfg.GetMasterNode(), self.op.pnode] + @@ -4794,7 +4861,7 @@ class LUCreateInstance(LogicalUnit): self.cfg.Update(iobj) logging.info("Starting instance %s on node %s", instance, pnode_name) feedback_fn("* starting instance...") - result = self.rpc.call_instance_start(pnode_name, iobj) + result = self.rpc.call_instance_start(pnode_name, iobj, None, None) msg = result.RemoteFailMsg() if msg: raise errors.OpExecError("Could not start instance: %s" % msg) @@ -6242,7 +6309,7 @@ class LUExportInstance(LogicalUnit): finally: if self.op.shutdown and instance.admin_up: - result = self.rpc.call_instance_start(src_node, instance) + result = self.rpc.call_instance_start(src_node, instance, None, None) msg = result.RemoteFailMsg() if msg: _ShutdownInstanceDisks(self, instance) diff --git a/lib/constants.py b/lib/constants.py index 058f2dd62e7daef392d4424d4a3899127ad41f3e..d9edc139d835ffa96db84da0a009f7774721ff2e 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -96,6 +96,7 @@ CLUSTER_CONF_FILE = DATA_DIR + "/config.data" SSL_CERT_FILE = DATA_DIR + "/server.pem" RAPI_CERT_FILE = DATA_DIR + "/rapi.pem" WATCHER_STATEFILE = DATA_DIR + "/watcher.data" +INSTANCE_UPFILE = RUN_GANETI_DIR + "/instance-status" SSH_KNOWN_HOSTS_FILE = DATA_DIR + "/known_hosts" RAPI_USERS_FILE = DATA_DIR + "/rapi_users" QUEUE_DIR = DATA_DIR + "/queue" @@ -356,7 +357,7 @@ VNC_BASE_PORT = 5900 VNC_PASSWORD_FILE = _autoconf.SYSCONFDIR + "/ganeti/vnc-cluster-password" VNC_DEFAULT_BIND_ADDRESS = '0.0.0.0' -# Device types +# NIC types HT_NIC_RTL8139 = "rtl8139" HT_NIC_NE2K_PCI = "ne2k_pci" HT_NIC_NE2K_ISA = "ne2k_isa" @@ -366,25 +367,40 @@ HT_NIC_I8259ER = "i82559er" HT_NIC_PCNET = "pcnet" HT_NIC_E1000 = "e1000" HT_NIC_PARAVIRTUAL = HT_DISK_PARAVIRTUAL = "paravirtual" -HT_DISK_IOEMU = "ioemu" -HT_DISK_IDE = "ide" -HT_DISK_SCSI = "scsi" -HT_DISK_SD = "sd" -HT_DISK_MTD = "mtd" -HT_DISK_PFLASH = "pflash" HT_HVM_VALID_NIC_TYPES = frozenset([HT_NIC_RTL8139, HT_NIC_NE2K_PCI, HT_NIC_NE2K_ISA, HT_NIC_PARAVIRTUAL]) -HT_HVM_VALID_DISK_TYPES = frozenset([HT_DISK_PARAVIRTUAL, HT_DISK_IOEMU]) HT_KVM_VALID_NIC_TYPES = frozenset([HT_NIC_RTL8139, HT_NIC_NE2K_PCI, HT_NIC_NE2K_ISA, HT_NIC_I82551, HT_NIC_I85557B, HT_NIC_I8259ER, HT_NIC_PCNET, HT_NIC_E1000, HT_NIC_PARAVIRTUAL]) +# Disk types +HT_DISK_IOEMU = "ioemu" +HT_DISK_IDE = "ide" +HT_DISK_SCSI = "scsi" +HT_DISK_SD = "sd" +HT_DISK_MTD = "mtd" +HT_DISK_PFLASH = "pflash" + +HT_HVM_VALID_DISK_TYPES = frozenset([HT_DISK_PARAVIRTUAL, HT_DISK_IOEMU]) HT_KVM_VALID_DISK_TYPES = frozenset([HT_DISK_PARAVIRTUAL, HT_DISK_IDE, HT_DISK_SCSI, HT_DISK_SD, HT_DISK_MTD, HT_DISK_PFLASH]) +# Mouse types: +HT_MOUSE_MOUSE = "mouse" +HT_MOUSE_TABLET = "tablet" + +HT_KVM_VALID_MOUSE_TYPES = frozenset([HT_MOUSE_MOUSE, HT_MOUSE_TABLET]) + +# Boot order +HT_BO_CDROM = "cdrom" +HT_BO_DISK = "disk" +HT_BO_NETWORK = "network" + +HT_KVM_VALID_BO_TYPES = frozenset([HT_BO_CDROM, HT_BO_DISK, HT_BO_NETWORK]) + # Cluster Verify steps VERIFY_NPLUSONE_MEM = 'nplusone_mem' VERIFY_OPTIONAL_CHECKS = frozenset([VERIFY_NPLUSONE_MEM]) @@ -501,7 +517,7 @@ HVC_DEFAULTS = { HV_VNC_X509: '', HV_VNC_X509_VERIFY: False, HV_CDROM_IMAGE_PATH: '', - HV_BOOT_ORDER: "disk", + HV_BOOT_ORDER: HT_BO_DISK, HV_NIC_TYPE: HT_NIC_PARAVIRTUAL, HV_DISK_TYPE: HT_DISK_PARAVIRTUAL, HV_USB_MOUSE: '', diff --git a/lib/hypervisor/hv_base.py b/lib/hypervisor/hv_base.py index b094b37dfbf7dba6bfc3c45f073fb619d431c7e4..e5de1b7cc09423f391a61b246dacfb48d0dbb0a4 100644 --- a/lib/hypervisor/hv_base.py +++ b/lib/hypervisor/hv_base.py @@ -23,6 +23,9 @@ """ +import re + + from ganeti import errors @@ -187,3 +190,64 @@ class BaseHypervisor(object): """ pass + + def GetLinuxNodeInfo(self): + """For linux systems, return actual OS information. + + This is an abstraction for all non-hypervisor-based classes, where + the node actually sees all the memory and CPUs via the /proc + interface and standard commands. The other case if for example + xen, where you only see the hardware resources via xen-specific + tools. + + @return: a dict with the following keys (values in MiB): + - memory_total: the total memory size on the node + - memory_free: the available memory on the node for instances + - memory_dom0: the memory used by the node itself, if available + + """ + try: + fh = file("/proc/meminfo") + try: + data = fh.readlines() + finally: + fh.close() + except EnvironmentError, err: + raise errors.HypervisorError("Failed to list node info: %s" % (err,)) + + result = {} + sum_free = 0 + try: + for line in data: + splitfields = line.split(":", 1) + + if len(splitfields) > 1: + key = splitfields[0].strip() + val = splitfields[1].strip() + if key == 'MemTotal': + result['memory_total'] = int(val.split()[0])/1024 + elif key in ('MemFree', 'Buffers', 'Cached'): + sum_free += int(val.split()[0])/1024 + elif key == 'Active': + result['memory_dom0'] = int(val.split()[0])/1024 + except (ValueError, TypeError), err: + raise errors.HypervisorError("Failed to compute memory usage: %s" % + (err,)) + result['memory_free'] = sum_free + + cpu_total = 0 + try: + fh = open("/proc/cpuinfo") + try: + cpu_total = len(re.findall("(?m)^processor\s*:\s*[0-9]+\s*$", + fh.read())) + finally: + fh.close() + except EnvironmentError, err: + raise errors.HypervisorError("Failed to list node info: %s" % (err,)) + result['cpu_total'] = cpu_total + # FIXME: export correct data here + result['cpu_nodes'] = 1 + result['cpu_sockets'] = 1 + + return result diff --git a/lib/hypervisor/hv_fake.py b/lib/hypervisor/hv_fake.py index 48e645bf7a493075782a957079feaea2525da8ea..ccac8427730d419579e141910fba4f204f95b449 100644 --- a/lib/hypervisor/hv_fake.py +++ b/lib/hypervisor/hv_fake.py @@ -155,61 +155,19 @@ class FakeHypervisor(hv_base.BaseHypervisor): def GetNodeInfo(self): """Return information about the node. + This is just a wrapper over the base GetLinuxNodeInfo method. + @return: a dict with the following keys (values in MiB): - memory_total: the total memory size on the node - memory_free: the available memory on the node for instances - memory_dom0: the memory used by the node itself, if available """ - # global ram usage from the xm info command - # memory : 3583 - # free_memory : 747 - # note: in xen 3, memory has changed to total_memory - try: - fh = file("/proc/meminfo") - try: - data = fh.readlines() - finally: - fh.close() - except IOError, err: - raise errors.HypervisorError("Failed to list node info: %s" % err) - - result = {} - sum_free = 0 - for line in data: - splitfields = line.split(":", 1) - - if len(splitfields) > 1: - key = splitfields[0].strip() - val = splitfields[1].strip() - if key == 'MemTotal': - result['memory_total'] = int(val.split()[0])/1024 - elif key in ('MemFree', 'Buffers', 'Cached'): - sum_free += int(val.split()[0])/1024 - elif key == 'Active': - result['memory_dom0'] = int(val.split()[0])/1024 - result['memory_free'] = sum_free - + result = self.GetLinuxNodeInfo() # substract running instances all_instances = self.GetAllInstancesInfo() result['memory_free'] -= min(result['memory_free'], sum([row[2] for row in all_instances])) - - cpu_total = 0 - try: - fh = open("/proc/cpuinfo") - try: - cpu_total = len(re.findall("(?m)^processor\s*:\s*[0-9]+\s*$", - fh.read())) - finally: - fh.close() - except EnvironmentError, err: - raise errors.HypervisorError("Failed to list node info: %s" % err) - result['cpu_total'] = cpu_total - # FIXME: export correct data here - result['cpu_nodes'] = 1 - result['cpu_sockets'] = 1 - return result @classmethod diff --git a/lib/hypervisor/hv_kvm.py b/lib/hypervisor/hv_kvm.py index 358de7458ea39fb7822c85d4839570b340108b20..3dec1788fea2313be36399de92325eb877bbd125 100644 --- a/lib/hypervisor/hv_kvm.py +++ b/lib/hypervisor/hv_kvm.py @@ -236,9 +236,9 @@ class KVMHypervisor(hv_base.BaseHypervisor): kvm_cmd.extend(['-no-acpi']) hvp = instance.hvparams - boot_disk = hvp[constants.HV_BOOT_ORDER] == "disk" - boot_cdrom = hvp[constants.HV_BOOT_ORDER] == "cdrom" - boot_network = hvp[constants.HV_BOOT_ORDER] == "network" + boot_disk = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_DISK + boot_cdrom = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_CDROM + boot_network = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_NETWORK if boot_network: kvm_cmd.extend(['-boot', 'n']) @@ -618,57 +618,15 @@ class KVMHypervisor(hv_base.BaseHypervisor): def GetNodeInfo(self): """Return information about the node. + This is just a wrapper over the base GetLinuxNodeInfo method. + @return: a dict with the following keys (values in MiB): - memory_total: the total memory size on the node - memory_free: the available memory on the node for instances - memory_dom0: the memory used by the node itself, if available """ - # global ram usage from the xm info command - # memory : 3583 - # free_memory : 747 - # note: in xen 3, memory has changed to total_memory - try: - fh = file("/proc/meminfo") - try: - data = fh.readlines() - finally: - fh.close() - except EnvironmentError, err: - raise errors.HypervisorError("Failed to list node info: %s" % err) - - result = {} - sum_free = 0 - for line in data: - splitfields = line.split(":", 1) - - if len(splitfields) > 1: - key = splitfields[0].strip() - val = splitfields[1].strip() - if key == 'MemTotal': - result['memory_total'] = int(val.split()[0])/1024 - elif key in ('MemFree', 'Buffers', 'Cached'): - sum_free += int(val.split()[0])/1024 - elif key == 'Active': - result['memory_dom0'] = int(val.split()[0])/1024 - result['memory_free'] = sum_free - - cpu_total = 0 - try: - fh = open("/proc/cpuinfo") - try: - cpu_total = len(re.findall("(?m)^processor\s*:\s*[0-9]+\s*$", - fh.read())) - finally: - fh.close() - except EnvironmentError, err: - raise errors.HypervisorError("Failed to list node info: %s" % err) - result['cpu_total'] = cpu_total - # FIXME: export correct data here - result['cpu_nodes'] = 1 - result['cpu_sockets'] = 1 - - return result + return self.GetLinuxNodeInfo() @classmethod def GetShellCommandForConsole(cls, instance, hvparams, beparams): @@ -765,35 +723,39 @@ class KVMHypervisor(hv_base.BaseHypervisor): " an absolute path, if defined") boot_order = hvparams[constants.HV_BOOT_ORDER] - if boot_order not in ('cdrom', 'disk', 'network'): - raise errors.HypervisorError("The boot order must be 'cdrom', 'disk' or" - " 'network'") + if boot_order not in constants.HT_KVM_VALID_BO_TYPES: + raise errors.HypervisorError(\ + "The boot order must be one of %s" % + utils.CommaJoin(constants.HT_KVM_VALID_BO_TYPES)) - if boot_order == 'cdrom' and not iso_path: - raise errors.HypervisorError("Cannot boot from cdrom without an ISO path") + if boot_order == constants.HT_BO_CDROM and not iso_path: + raise errors.HypervisorError("Cannot boot from cdrom without an" + " ISO path") nic_type = hvparams[constants.HV_NIC_TYPE] if nic_type not in constants.HT_KVM_VALID_NIC_TYPES: - raise errors.HypervisorError("Invalid NIC type %s specified for the KVM" - " hypervisor. Please choose one of: %s" % - (nic_type, - constants.HT_KVM_VALID_NIC_TYPES)) - elif boot_order == 'network' and nic_type == constants.HT_NIC_PARAVIRTUAL: + raise errors.HypervisorError(\ + "Invalid NIC type %s specified for the KVM" + " hypervisor. Please choose one of: %s" % + (nic_type, utils.CommaJoin(constants.HT_KVM_VALID_NIC_TYPES))) + elif (boot_order == constants.HT_BO_NETWORK and + nic_type == constants.HT_NIC_PARAVIRTUAL): raise errors.HypervisorError("Cannot boot from a paravirtual NIC. Please" - " change the nic type.") + " change the NIC type.") disk_type = hvparams[constants.HV_DISK_TYPE] if disk_type not in constants.HT_KVM_VALID_DISK_TYPES: - raise errors.HypervisorError("Invalid disk type %s specified for the KVM" - " hypervisor. Please choose one of: %s" % - (disk_type, - constants.HT_KVM_VALID_DISK_TYPES)) + raise errors.HypervisorError(\ + "Invalid disk type %s specified for the KVM" + " hypervisor. Please choose one of: %s" % + (disk_type, utils.CommaJoin(constants.HT_KVM_VALID_DISK_TYPES))) mouse_type = hvparams[constants.HV_USB_MOUSE] - if mouse_type and mouse_type not in ('mouse', 'tablet'): - raise errors.HypervisorError("Invalid usb mouse type %s specified for" - " the KVM hyervisor. Please choose" - " 'mouse' or 'tablet'" % mouse_type) + if mouse_type and mouse_type not in constants.HT_KVM_VALID_MOUSE_TYPES: + raise errors.HypervisorError(\ + "Invalid usb mouse type %s specified for the KVM hypervisor. Please" + " choose one of %s" % + utils.CommaJoin(constants.HT_KVM_VALID_MOUSE_TYPES)) def ValidateParameters(self, hvparams): """Check the given parameters for validity. diff --git a/lib/hypervisor/hv_xen.py b/lib/hypervisor/hv_xen.py index 959958deb9cdedb5e810770ef0ee9263c015ef99..4579f4db8b5e6677ed08a80bfc6aceb647697f32 100644 --- a/lib/hypervisor/hv_xen.py +++ b/lib/hypervisor/hv_xen.py @@ -535,16 +535,16 @@ class XenHvmHypervisor(XenHypervisor): # device type checks nic_type = hvparams[constants.HV_NIC_TYPE] if nic_type not in constants.HT_HVM_VALID_NIC_TYPES: - raise errors.HypervisorError("Invalid NIC type %s specified for the Xen" - " HVM hypervisor. Please choose one of: %s" - % (nic_type, - constants.HT_HVM_VALID_NIC_TYPES)) + raise errors.HypervisorError(\ + "Invalid NIC type %s specified for the Xen" + " HVM hypervisor. Please choose one of: %s" + % (nic_type, utils.CommaJoin(constants.HT_HVM_VALID_NIC_TYPES))) disk_type = hvparams[constants.HV_DISK_TYPE] if disk_type not in constants.HT_HVM_VALID_DISK_TYPES: - raise errors.HypervisorError("Invalid disk type %s specified for the Xen" - " HVM hypervisor. Please choose one of: %s" - % (disk_type, - constants.HT_HVM_VALID_DISK_TYPES)) + raise errors.HypervisorError(\ + "Invalid disk type %s specified for the Xen" + " HVM hypervisor. Please choose one of: %s" + % (disk_type, utils.CommaJoin(constants.HT_HVM_VALID_DISK_TYPES))) # vnc_bind_address verification vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS] if vnc_bind_address: diff --git a/lib/opcodes.py b/lib/opcodes.py index 5be660bdc797b9b2e219b443b9367509f26c1051..535db910d0b38fba6c055dc24c0ed0ea1f943117 100644 --- a/lib/opcodes.py +++ b/lib/opcodes.py @@ -382,7 +382,7 @@ class OpStartupInstance(OpCode): """Startup an instance.""" OP_ID = "OP_INSTANCE_STARTUP" OP_DSC_FIELD = "instance_name" - __slots__ = ["instance_name", "force"] + __slots__ = ["instance_name", "force", "hvparams", "beparams"] class OpShutdownInstance(OpCode): diff --git a/lib/rapi/baserlib.py b/lib/rapi/baserlib.py index 0138e64fde7067fa12fd93555766ad9ab2719e03..2bca63ba07a99ff9ab05fc86718ba16d9d0011a8 100644 --- a/lib/rapi/baserlib.py +++ b/lib/rapi/baserlib.py @@ -23,12 +23,17 @@ """ +import logging + import ganeti.cli -import ganeti.opcodes from ganeti import luxi from ganeti import rapi from ganeti import http +from ganeti import ssconf +from ganeti import constants +from ganeti import opcodes +from ganeti import errors def BuildUriList(ids, uri_format, uri_fields=("name", "uri")): @@ -81,8 +86,22 @@ def _Tags_GET(kind, name=""): """Helper function to retrieve tags. """ - op = ganeti.opcodes.OpGetTags(kind=kind, name=name) - tags = ganeti.cli.SubmitOpCode(op) + if kind == constants.TAG_INSTANCE or kind == constants.TAG_NODE: + if not name: + raise http.HttpBadRequest("Missing name on tag request") + cl = GetClient() + if kind == constants.TAG_INSTANCE: + fn = cl.QueryInstances + else: + fn = cl.QueryNodes + result = fn(names=[name], fields=["tags"], use_locking=False) + if not result or not result[0]: + raise http.HttpBadGateway("Invalid response from tag query") + tags = result[0][0] + elif kind == constants.TAG_CLUSTER: + ssc = ssconf.SimpleStore() + tags = ssc.GetClusterTags() + return list(tags) @@ -90,18 +109,14 @@ def _Tags_PUT(kind, tags, name=""): """Helper function to set tags. """ - cl = luxi.Client() - return cl.SubmitJob([ganeti.opcodes.OpAddTags(kind=kind, name=name, - tags=tags)]) + return SubmitJob([opcodes.OpAddTags(kind=kind, name=name, tags=tags)]) def _Tags_DELETE(kind, tags, name=""): """Helper function to delete tags. """ - cl = luxi.Client() - return cl.SubmitJob([ganeti.opcodes.OpDelTags(kind=kind, name=name, - tags=tags)]) + return SubmitJob([opcodes.OpDelTags(kind=kind, name=name, tags=tags)]) def MapBulkFields(itemslist, fields): @@ -147,6 +162,51 @@ def MakeParamsDict(opts, params): return result +def SubmitJob(op, cl=None): + """Generic wrapper for submit job, for better http compatibility. + + @type op: list + @param op: the list of opcodes for the job + @type cl: None or luxi.Client + @param cl: optional luxi client to use + @rtype: string + @return: the job ID + + """ + try: + if cl is None: + cl = GetClient() + return cl.SubmitJob(op) + except errors.JobQueueFull: + raise http.HttpServiceUnavailable("Job queue is full, needs archiving") + except errors.JobQueueDrainError: + raise http.HttpServiceUnavailable("Job queue is drained, cannot submit") + except luxi.NoMasterError, err: + raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err)) + except luxi.TimeoutError, err: + raise http.HttpGatewayTimeout("Timeout while talking to the master" + " daemon. Error: %s" % str(err)) + +def GetClient(): + """Geric wrapper for luxi.Client(), for better http compatiblity. + + """ + try: + return luxi.Client() + except luxi.NoMasterError, err: + raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err)) + + +def FeedbackFn(ts, log_type, log_msg): + """Feedback logging function for http case. + + We don't have a stdout for printing log messages, so log them to the + http log at least. + + """ + logging.info("%s: %s", log_type, log_msg) + + class R_Generic(object): """Generic class for resources. diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index 79b95da16db810cf2c228db8d3f373e0fcdaab06..738f86f029f1a34e9da41e2f1256529de0f7674c 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -23,13 +23,14 @@ """ -import ganeti.opcodes +from ganeti import opcodes from ganeti import http -from ganeti import luxi from ganeti import constants +from ganeti import cli from ganeti.rapi import baserlib + I_FIELDS = ["name", "admin_state", "os", "pnode", "snodes", "disk_template", @@ -105,7 +106,7 @@ class R_2_info(baserlib.R_Generic): } """ - client = luxi.Client() + client = baserlib.GetClient() return client.QueryClusterInfo() @@ -123,12 +124,15 @@ class R_2_os(baserlib.R_Generic): Example: ["debian-etch"] """ - op = ganeti.opcodes.OpDiagnoseOS(output_fields=["name", "valid"], - names=[]) - diagnose_data = ganeti.cli.SubmitOpCode(op) + cl = baserlib.GetClient() + op = opcodes.OpDiagnoseOS(output_fields=["name", "valid"], names=[]) + job_id = baserlib.SubmitJob([op], cl) + # we use custom feedback function, instead of print we log the status + result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn) + diagnose_data = result[0] if not isinstance(diagnose_data, list): - raise http.HttpInternalServerError(message="Can't get OS list") + raise http.HttpBadGateway(message="Can't get OS list") return [row[0] for row in diagnose_data if row[1]] @@ -146,8 +150,9 @@ class R_2_jobs(baserlib.R_Generic): """ fields = ["id"] + cl = baserlib.GetClient() # Convert the list of lists to the list of ids - result = [job_id for [job_id] in luxi.Client().QueryJobs(None, fields)] + result = [job_id for [job_id] in cl.QueryJobs(None, fields)] return baserlib.BuildUriList(result, "/2/jobs/%s", uri_fields=("id", "uri")) @@ -176,7 +181,7 @@ class R_2_jobs_id(baserlib.R_Generic): "received_ts", "start_ts", "end_ts", ] job_id = self.items[0] - result = luxi.Client().QueryJobs([job_id, ], fields)[0] + result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0] if result is None: raise http.HttpNotFound() return baserlib.MapFields(fields, result) @@ -186,7 +191,7 @@ class R_2_jobs_id(baserlib.R_Generic): """ job_id = self.items[0] - result = luxi.Client().CancelJob(job_id) + result = baserlib.GetClient().CancelJob(job_id) return result @@ -237,7 +242,7 @@ class R_2_nodes(baserlib.R_Generic): @return: a dictionary with 'name' and 'uri' keys for each of them """ - client = luxi.Client() + client = baserlib.GetClient() if self.useBulk(): bulkdata = client.QueryNodes([], N_FIELDS, False) @@ -260,7 +265,7 @@ class R_2_nodes_name(baserlib.R_Generic): """ node_name = self.items[0] - client = luxi.Client() + client = baserlib.GetClient() result = client.QueryNodes(names=[node_name], fields=N_FIELDS, use_locking=self.useLocking()) @@ -326,7 +331,7 @@ class R_2_instances(baserlib.R_Generic): @return: a dictionary with 'name' and 'uri' keys for each of them. """ - client = luxi.Client() + client = baserlib.GetClient() use_locking = self.useLocking() if self.useBulk(): @@ -368,28 +373,27 @@ class R_2_instances(baserlib.R_Generic): "ip": fn("ip", None), "bridge": fn("bridge", None)}] - op = ganeti.opcodes.OpCreateInstance( - mode=constants.INSTANCE_CREATE, - instance_name=fn('name'), - disks=disks, - disk_template=fn('disk_template'), - os_type=fn('os'), - pnode=fn('pnode', None), - snode=fn('snode', None), - iallocator=fn('iallocator', None), - nics=nics, - start=fn('start', True), - ip_check=fn('ip_check', True), - wait_for_sync=True, - hypervisor=fn('hypervisor', None), - hvparams=hvparams, - beparams=beparams, - file_storage_dir=fn('file_storage_dir', None), - file_driver=fn('file_driver', 'loop'), - ) - - job_id = ganeti.cli.SendJob([op]) - return job_id + op = opcodes.OpCreateInstance( + mode=constants.INSTANCE_CREATE, + instance_name=fn('name'), + disks=disks, + disk_template=fn('disk_template'), + os_type=fn('os'), + pnode=fn('pnode', None), + snode=fn('snode', None), + iallocator=fn('iallocator', None), + nics=nics, + start=fn('start', True), + ip_check=fn('ip_check', True), + wait_for_sync=True, + hypervisor=fn('hypervisor', None), + hvparams=hvparams, + beparams=beparams, + file_storage_dir=fn('file_storage_dir', None), + file_driver=fn('file_driver', 'loop'), + ) + + return baserlib.SubmitJob([op]) class R_2_instances_name(baserlib.R_Generic): @@ -402,7 +406,7 @@ class R_2_instances_name(baserlib.R_Generic): """Send information about an instance. """ - client = luxi.Client() + client = baserlib.GetClient() instance_name = self.items[0] result = client.QueryInstances(names=[instance_name], fields=I_FIELDS, use_locking=self.useLocking()) @@ -413,10 +417,9 @@ class R_2_instances_name(baserlib.R_Generic): """Delete an instance. """ - op = ganeti.opcodes.OpRemoveInstance(instance_name=self.items[0], - ignore_failures=False) - job_id = ganeti.cli.SendJob([op]) - return job_id + op = opcodes.OpRemoveInstance(instance_name=self.items[0], + ignore_failures=False) + return baserlib.SubmitJob([op]) class R_2_instances_name_reboot(baserlib.R_Generic): @@ -440,14 +443,11 @@ class R_2_instances_name_reboot(baserlib.R_Generic): [constants.INSTANCE_REBOOT_HARD])[0] ignore_secondaries = bool(self.queryargs.get('ignore_secondaries', [False])[0]) - op = ganeti.opcodes.OpRebootInstance( - instance_name=instance_name, - reboot_type=reboot_type, - ignore_secondaries=ignore_secondaries) - - job_id = ganeti.cli.SendJob([op]) + op = opcodes.OpRebootInstance(instance_name=instance_name, + reboot_type=reboot_type, + ignore_secondaries=ignore_secondaries) - return job_id + return baserlib.SubmitJob([op]) class R_2_instances_name_startup(baserlib.R_Generic): @@ -468,12 +468,10 @@ class R_2_instances_name_startup(baserlib.R_Generic): """ instance_name = self.items[0] force_startup = bool(self.queryargs.get('force', [False])[0]) - op = ganeti.opcodes.OpStartupInstance(instance_name=instance_name, - force=force_startup) + op = opcodes.OpStartupInstance(instance_name=instance_name, + force=force_startup) - job_id = ganeti.cli.SendJob([op]) - - return job_id + return baserlib.SubmitJob([op]) class R_2_instances_name_shutdown(baserlib.R_Generic): @@ -490,11 +488,9 @@ class R_2_instances_name_shutdown(baserlib.R_Generic): """ instance_name = self.items[0] - op = ganeti.opcodes.OpShutdownInstance(instance_name=instance_name) - - job_id = ganeti.cli.SendJob([op]) + op = opcodes.OpShutdownInstance(instance_name=instance_name) - return job_id + return baserlib.SubmitJob([op]) class _R_Tags(baserlib.R_Generic): diff --git a/lib/rpc.py b/lib/rpc.py index 70dd3128ac955de41256b31a2d64c26631e3a9cc..48a48dc697cb67e44067fbc824c48dded3bb8771 100644 --- a/lib/rpc.py +++ b/lib/rpc.py @@ -260,7 +260,7 @@ class RpcRunner(object): self._cfg = cfg self.port = utils.GetNodeDaemonPort() - def _InstDict(self, instance): + def _InstDict(self, instance, hvp=None, bep=None): """Convert the given instance to a dict. This is done via the instance's ToDict() method and additionally @@ -268,6 +268,10 @@ class RpcRunner(object): @type instance: L{objects.Instance} @param instance: an Instance object + @type hvp: dict or None + @param hvp: a dictionary with overriden hypervisor parameters + @type bep: dict or None + @param bep: a dictionary with overriden backend parameters @rtype: dict @return: the instance dict, with the hvparams filled with the cluster defaults @@ -276,7 +280,11 @@ class RpcRunner(object): idict = instance.ToDict() cluster = self._cfg.GetClusterInfo() idict["hvparams"] = cluster.FillHV(instance) + if hvp is not None: + idict["hvparams"].update(hvp) idict["beparams"] = cluster.FillBE(instance) + if bep is not None: + idict["beparams"].update(bep) return idict def _ConnectList(self, client, node_list, call): @@ -425,14 +433,14 @@ class RpcRunner(object): """ return self._SingleNodeCall(node, "bridges_exist", [bridges_list]) - def call_instance_start(self, node, instance): + def call_instance_start(self, node, instance, hvp, bep): """Starts an instance. This is a single-node call. """ - return self._SingleNodeCall(node, "instance_start", - [self._InstDict(instance)]) + idict = self._InstDict(instance, hvp=hvp, bep=bep) + return self._SingleNodeCall(node, "instance_start", [idict]) def call_instance_shutdown(self, node, instance): """Stops an instance. diff --git a/lib/ssconf.py b/lib/ssconf.py index cce1141f073d4e807d49a24a6cbb5ab275b86dad..19a95c97a90d0165a14eb102ee756e58f17ce44a 100644 --- a/lib/ssconf.py +++ b/lib/ssconf.py @@ -250,6 +250,14 @@ class SimpleStore(object): nl = data.splitlines(False) return nl + def GetClusterTags(self): + """Return the cluster tags. + + """ + data = self._ReadFile(constants.SS_CLUSTER_TAGS) + nl = data.splitlines(False) + return nl + def GetMasterAndMyself(ss=None): """Get the master node and my own hostname. diff --git a/lib/utils.py b/lib/utils.py index 304877933020d271e251b06d05f9be8055fcea7a..ac781fb621c6bbed469d5d69f839148d66c7b72d 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -156,11 +156,18 @@ def RunCmd(cmd, env=None, output=None, cwd='/'): if env is not None: cmd_env.update(env) - if output is None: - out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd) - else: - status = _RunCmdFile(cmd, cmd_env, shell, output, cwd) - out = err = "" + try: + if output is None: + out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd) + else: + status = _RunCmdFile(cmd, cmd_env, shell, output, cwd) + out = err = "" + except OSError, err: + if err.errno == errno.ENOENT: + raise errors.OpExecError("Can't execute '%s': not found (%s)" % + (strcmd, err)) + else: + raise if status >= 0: exitcode = status @@ -1289,6 +1296,7 @@ def WriteFile(file_name, fn=None, data=None, dir_name, base_name = os.path.split(file_name) fd, new_name = tempfile.mkstemp('.new', base_name, dir_name) + do_remove = True # here we need to make sure we remove the temp file, if any error # leaves it in place try: @@ -1309,13 +1317,15 @@ def WriteFile(file_name, fn=None, data=None, os.utime(new_name, (atime, mtime)) if not dry_run: os.rename(new_name, file_name) + do_remove = False finally: if close: os.close(fd) result = None else: result = fd - RemoveFile(new_name) + if do_remove: + RemoveFile(new_name) return result @@ -1806,11 +1816,13 @@ def SafeEncode(text): """Return a 'safe' version of a source string. This function mangles the input string and returns a version that - should be safe to disply/encode as ASCII. To this end, we first + should be safe to display/encode as ASCII. To this end, we first convert it to ASCII using the 'backslashreplace' encoding which - should get rid of any non-ASCII chars, and then we again encode it - via 'string_escape' which converts '\n' into '\\n' so that log - messages remain one-line. + should get rid of any non-ASCII chars, and then we process it + through a loop copied from the string repr sources in the python; we + don't use string_escape anymore since that escape single quotes and + backslashes too, and that is too much; and that escaping is not + stable, i.e. string_escape(string_escape(x)) != string_escape(x). @type text: str or unicode @param text: input data @@ -1818,9 +1830,33 @@ def SafeEncode(text): @return: a safe version of text """ - text = text.encode('ascii', 'backslashreplace') - text = text.encode('string_escape') - return text + if isinstance(text, unicode): + # onli if unicode; if str already, we handle it below + text = text.encode('ascii', 'backslashreplace') + resu = "" + for char in text: + c = ord(char) + if char == '\t': + resu += r'\t' + elif char == '\n': + resu += r'\n' + elif char == '\r': + resu += r'\'r' + elif c < 32 or c >= 127: # non-printable + resu += "\\x%02x" % (c & 0xff) + else: + resu += char + return resu + + +def CommaJoin(names): + """Nicely join a set of identifiers. + + @param names: set, list or tuple + @return: a string with the formatted results + + """ + return ", ".join(["'%s'" % val for val in names]) def LockedMethod(fn): diff --git a/man/gnt-debug.sgml b/man/gnt-debug.sgml index 2e624b053c8c334dd07b0c531d3f528453a0194a..89fd9e4a1f3aecc63e36d2e9a7864f5c352781b3 100644 --- a/man/gnt-debug.sgml +++ b/man/gnt-debug.sgml @@ -146,12 +146,12 @@ <cmdsynopsis> <command>submit-job</command> - <arg choice="req">opcodes_file</arg> + <arg choice="req" rep="repeat">opcodes_file</arg> </cmdsynopsis> <para> - This command builds a list of opcodes from a JSON-format file - and submits them as a single job to the master daemon. It can + This command builds a list of opcodes from JSON-format files + and submits for each file a job to the master daemon. It can be used to test some options that are not available via the command line. </para> diff --git a/man/gnt-instance.sgml b/man/gnt-instance.sgml index b43f0da4095a7a1a72aef8ce520f9ca8f11d2cd0..8ca11dec891c56945ffccadecaa2fbe18c493f3a 100644 --- a/man/gnt-instance.sgml +++ b/man/gnt-instance.sgml @@ -1317,6 +1317,9 @@ instance5: 11225 <arg>--all</arg> </group> <sbr> + <arg>-H <option>key=value...</option></arg> + <arg>-B <option>key=value...</option></arg> + <sbr> <arg>--submit</arg> <sbr> <arg choice="opt" @@ -1385,6 +1388,23 @@ instance5: 11225 instance will be affected. </para> + <para> + The <option>-H</option> and <option>-B</option> options + specify extra, temporary hypervisor and backend parameters + that can be used to start an instance with modified + parameters. They can be useful for quick testing without + having to modify an instance back and forth, e.g.: + <screen> +# gnt-instance start -H root_args="single" instance1 +# gnt-instance start -B memory=2048 instance2 + </screen> + The first form will start the instance + <userinput>instance1</userinput> in single-user mode, and + the instance <userinput>instance2</userinput> with 2GB of + RAM (this time only, unless that is the actual instance + memory size already). + </para> + <para> The <option>--submit</option> option is used to send the job to the master daemon but not wait for its completion. The job diff --git a/scripts/gnt-cluster b/scripts/gnt-cluster index c8473fa16cb26bae2a52ecb3cd3c8efbbc7e651b..99cab31f28a71dbf7ae616dfb24bbf7d9c899cc8 100755 --- a/scripts/gnt-cluster +++ b/scripts/gnt-cluster @@ -243,6 +243,10 @@ def ShowClusterConfig(opts, args): ToStdout("Cluster parameters:") ToStdout(" - candidate pool size: %s", result["candidate_pool_size"]) + ToStdout(" - master netdev: %s", result["master_netdev"]) + ToStdout(" - default bridge: %s", result["default_bridge"]) + ToStdout(" - lvm volume group: %s", result["volume_group_name"]) + ToStdout(" - file storage path: %s", result["file_storage_dir"]) ToStdout("Default instance parameters:") for gr_name, gr_dict in result["beparams"].items(): @@ -463,6 +467,8 @@ def SetClusterParams(opts, args): if not opts.lvm_storage and opts.vg_name: ToStdout("Options --no-lvm-storage and --vg-name conflict.") return 1 + elif not opts.lvm_storage: + vg_name = '' hvlist = opts.enabled_hypervisors if hvlist is not None: @@ -476,7 +482,7 @@ def SetClusterParams(opts, args): beparams = opts.beparams utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES) - op = opcodes.OpSetClusterParams(vg_name=opts.vg_name, + op = opcodes.OpSetClusterParams(vg_name=vg_name, enabled_hypervisors=hvlist, hvparams=hvparams, beparams=beparams, diff --git a/scripts/gnt-debug b/scripts/gnt-debug index ff7e01b307474d5085c8e48440cf6f8418eefcea..d3bf05450c3c08cc6ad94de10d744dbc4d3273cd 100755 --- a/scripts/gnt-debug +++ b/scripts/gnt-debug @@ -71,12 +71,19 @@ def GenericOpCodes(opts, args): """ cl = cli.GetClient() - fname = args[0] - op_data = simplejson.loads(open(fname).read()) - op_list = [opcodes.OpCode.LoadOpCode(val) for val in op_data] - jid = cli.SendJob(op_list, cl=cl) - ToStdout("Job id: %s", jid) - cli.PollJob(jid, cl=cl) + job_data = [] + job_ids = [] + for fname in args: + op_data = simplejson.loads(open(fname).read()) + op_list = [opcodes.OpCode.LoadOpCode(val) for val in op_data] + job_data.append((fname, op_list)) + for fname, op_list in job_data: + jid = cli.SendJob(op_list, cl=cl) + ToStdout("File '%s', job id: %s", fname, jid) + job_ids.append(jid) + for jid in job_ids: + ToStdout("Waiting for job id %s", jid) + cli.PollJob(jid, cl=cl) return 0 @@ -139,11 +146,11 @@ commands = { help="Select nodes to sleep on"), ], "[opts...] <duration>", "Executes a TestDelay OpCode"), - 'submit-job': (GenericOpCodes, ARGS_ONE, + 'submit-job': (GenericOpCodes, ARGS_ATLEAST(1), [DEBUG_OPT, ], - "<op_list_file>", "Submits a job built from a json-file" - " with a list of serialized opcodes"), + "<op_list_file...>", "Submits jobs built from json files" + " containing a list of serialized opcodes"), 'allocator': (TestAllocator, ARGS_ONE, [DEBUG_OPT, make_option("--dir", dest="direction", diff --git a/scripts/gnt-instance b/scripts/gnt-instance index ecb93bbecb630e49719f2549a3171755a99d6d5f..a54672c9003f3e59572f8d57f882f3465359f4e0 100755 --- a/scripts/gnt-instance +++ b/scripts/gnt-instance @@ -687,6 +687,11 @@ def StartupInstance(opts, args): for name in inames: op = opcodes.OpStartupInstance(instance_name=name, force=opts.force) + # do not add these parameters to the opcode unless they're defined + if opts.hvparams: + op.hvparams = opts.hvparams + if opts.beparams: + op.beparams = opts.beparams jex.QueueJob(name, op) jex.WaitOrShow(not opts.submit_only) return 0 @@ -1449,8 +1454,14 @@ commands = { m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, + keyval_option("-H", "--hypervisor", type="keyval", + default={}, dest="hvparams", + help="Temporary hypervisor parameters"), + keyval_option("-B", "--backend", type="keyval", + default={}, dest="beparams", + help="Temporary backend parameters"), ], - "<instance>", "Starts an instance"), + "<instance>", "Starts an instance"), 'reboot': (RebootInstance, ARGS_ANY, [DEBUG_OPT, m_force_multi, diff --git a/scripts/gnt-job b/scripts/gnt-job index ce81709e15e2984d8689ea652cb574a0c23ea805..2da75a35b1da77b5ccf864de88ccfbc2aa0d95d3 100755 --- a/scripts/gnt-job +++ b/scripts/gnt-job @@ -85,7 +85,11 @@ def ListJobs(opts, args): headers = None # change raw values to nicer strings - for row in output: + for row_id, row in enumerate(output): + if row is None: + ToStderr("No such job: %s" % args[row_id]) + continue + for idx, field in enumerate(selected_fields): val = row[idx] if field == "status": diff --git a/test/data/bdev-8.3-both.txt b/test/data/bdev-8.3-both.txt new file mode 100644 index 0000000000000000000000000000000000000000..bc6e741cda853fe10ee910aa66170bcd4ab2308d --- /dev/null +++ b/test/data/bdev-8.3-both.txt @@ -0,0 +1,36 @@ +disk { + size 0s _is_default; # bytes + on-io-error detach; + fencing dont-care _is_default; + max-bio-bvecs 0 _is_default; +} +net { + timeout 60 _is_default; # 1/10 seconds + max-epoch-size 2048 _is_default; + max-buffers 2048 _is_default; + unplug-watermark 128 _is_default; + connect-int 10 _is_default; # seconds + ping-int 10 _is_default; # seconds + sndbuf-size 131070 _is_default; # bytes + ko-count 0 _is_default; + after-sb-0pri discard-zero-changes; + after-sb-1pri consensus; + after-sb-2pri disconnect _is_default; + rr-conflict disconnect _is_default; + ping-timeout 5 _is_default; # 1/10 seconds +} +syncer { + rate 61440k; # bytes/second + after -1 _is_default; + al-extents 257; +} +protocol C; +_this_host { + device minor 0; + disk "/dev/xenvg/test.data"; + meta-disk "/dev/xenvg/test.meta" [ 0 ]; + address ipv4 192.168.1.1:11000; +} +_remote_host { + address ipv4 192.168.1.2:11000; +} diff --git a/test/data/proc_drbd83.txt b/test/data/proc_drbd83.txt new file mode 100644 index 0000000000000000000000000000000000000000..114944c723814c8d59a9617d61f2370d59795b50 Binary files /dev/null and b/test/data/proc_drbd83.txt differ diff --git a/test/ganeti.bdev_unittest.py b/test/ganeti.bdev_unittest.py index 2db785c7d27daecdc6c5cf7ab32f7678be115ac1..b2299b18d481eed28302323743072481ddd6a529 100755 --- a/test/ganeti.bdev_unittest.py +++ b/test/ganeti.bdev_unittest.py @@ -61,7 +61,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): """Test drbdsetup show parser creation""" bdev.DRBD8._GetShowParser() - def testParserBoth(self): + def testParserBoth80(self): """Test drbdsetup show parser for disk and network""" data = self._ReadTestData("bdev-both.txt") result = bdev.DRBD8._GetDevInfo(data) @@ -70,7 +70,18 @@ class TestDRBD8Runner(testutils.GanetiTestCase): "Wrong local disk info") self.failUnless(self._has_net(result, ("192.168.1.1", 11000), ("192.168.1.2", 11000)), - "Wrong network info") + "Wrong network info (8.0.x)") + + def testParserBoth83(self): + """Test drbdsetup show parser for disk and network""" + data = self._ReadTestData("bdev-8.3-both.txt") + result = bdev.DRBD8._GetDevInfo(data) + self.failUnless(self._has_disk(result, "/dev/xenvg/test.data", + "/dev/xenvg/test.meta"), + "Wrong local disk info") + self.failUnless(self._has_net(result, ("192.168.1.1", 11000), + ("192.168.1.2", 11000)), + "Wrong network info (8.2.x)") def testParserNet(self): """Test drbdsetup show parser for disk and network""" @@ -103,8 +114,11 @@ class TestDRBD8Status(testutils.GanetiTestCase): """Read in txt data""" testutils.GanetiTestCase.setUp(self) proc_data = self._TestDataFilename("proc_drbd8.txt") + proc83_data = self._TestDataFilename("proc_drbd83.txt") self.proc_data = bdev.DRBD8._GetProcData(filename=proc_data) + self.proc83_data = bdev.DRBD8._GetProcData(filename=proc83_data) self.mass_data = bdev.DRBD8._MassageProcData(self.proc_data) + self.mass83_data = bdev.DRBD8._MassageProcData(self.proc83_data) def testIOErrors(self): """Test handling of errors while reading the proc file.""" @@ -116,6 +130,7 @@ class TestDRBD8Status(testutils.GanetiTestCase): def testMinorNotFound(self): """Test not-found-minor in /proc""" self.failUnless(9 not in self.mass_data) + self.failUnless(9 not in self.mass83_data) def testLineNotMatch(self): """Test wrong line passed to DRBD8Status""" @@ -123,45 +138,51 @@ class TestDRBD8Status(testutils.GanetiTestCase): def testMinor0(self): """Test connected, primary device""" - stats = bdev.DRBD8Status(self.mass_data[0]) - self.failUnless(stats.is_in_use) - self.failUnless(stats.is_connected and stats.is_primary and - stats.peer_secondary and stats.is_disk_uptodate) + for data in [self.mass_data, self.mass83_data]: + stats = bdev.DRBD8Status(data[0]) + self.failUnless(stats.is_in_use) + self.failUnless(stats.is_connected and stats.is_primary and + stats.peer_secondary and stats.is_disk_uptodate) def testMinor1(self): """Test connected, secondary device""" - stats = bdev.DRBD8Status(self.mass_data[1]) - self.failUnless(stats.is_in_use) - self.failUnless(stats.is_connected and stats.is_secondary and - stats.peer_primary and stats.is_disk_uptodate) + for data in [self.mass_data, self.mass83_data]: + stats = bdev.DRBD8Status(data[1]) + self.failUnless(stats.is_in_use) + self.failUnless(stats.is_connected and stats.is_secondary and + stats.peer_primary and stats.is_disk_uptodate) def testMinor2(self): """Test unconfigured device""" - stats = bdev.DRBD8Status(self.mass_data[2]) - self.failIf(stats.is_in_use) + for data in [self.mass_data, self.mass83_data]: + stats = bdev.DRBD8Status(data[2]) + self.failIf(stats.is_in_use) def testMinor4(self): """Test WFconn device""" - stats = bdev.DRBD8Status(self.mass_data[4]) - self.failUnless(stats.is_in_use) - self.failUnless(stats.is_wfconn and stats.is_primary and - stats.rrole == 'Unknown' and - stats.is_disk_uptodate) + for data in [self.mass_data, self.mass83_data]: + stats = bdev.DRBD8Status(data[4]) + self.failUnless(stats.is_in_use) + self.failUnless(stats.is_wfconn and stats.is_primary and + stats.rrole == 'Unknown' and + stats.is_disk_uptodate) def testMinor6(self): """Test diskless device""" - stats = bdev.DRBD8Status(self.mass_data[6]) - self.failUnless(stats.is_in_use) - self.failUnless(stats.is_connected and stats.is_secondary and - stats.peer_primary and stats.is_diskless) + for data in [self.mass_data, self.mass83_data]: + stats = bdev.DRBD8Status(data[6]) + self.failUnless(stats.is_in_use) + self.failUnless(stats.is_connected and stats.is_secondary and + stats.peer_primary and stats.is_diskless) def testMinor8(self): """Test standalone device""" - stats = bdev.DRBD8Status(self.mass_data[8]) - self.failUnless(stats.is_in_use) - self.failUnless(stats.is_standalone and - stats.rrole == 'Unknown' and - stats.is_disk_uptodate) + for data in [self.mass_data, self.mass83_data]: + stats = bdev.DRBD8Status(data[8]) + self.failUnless(stats.is_in_use) + self.failUnless(stats.is_standalone and + stats.rrole == 'Unknown' and + stats.is_disk_uptodate) if __name__ == '__main__': unittest.main() diff --git a/test/ganeti.constants_unittest.py b/test/ganeti.constants_unittest.py index 7652f869b8108f1cfe980376238e58b0fc3e5d76..e5263d43c2d340b4bbd94325784dad206f591cd3 100755 --- a/test/ganeti.constants_unittest.py +++ b/test/ganeti.constants_unittest.py @@ -23,6 +23,7 @@ import unittest +import re from ganeti import constants @@ -54,5 +55,17 @@ class TestConstants(unittest.TestCase): constants.CONFIG_REVISION)) +class TestParameterNames(unittest.TestCase): + """HV/BE parameter tests""" + VALID_NAME = re.compile("^[a-zA-Z_][a-zA-Z0-9_]*$") + + def testNoDashes(self): + for kind, source in [('hypervisor', constants.HVS_PARAMETER_TYPES), + ('backend', constants.BES_PARAMETER_TYPES)]: + for key in source: + self.failUnless(self.VALID_NAME.match(key), + "The %s parameter '%s' contains invalid characters" % + (kind, key)) + if __name__ == '__main__': unittest.main() diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py index 1c2992c3d67ab4353c4e9fc9c582133cbc3b2e23..ef3d626e4f178288ad062f4ec758c71be8ab19c4 100755 --- a/test/ganeti.utils_unittest.py +++ b/test/ganeti.utils_unittest.py @@ -33,6 +33,7 @@ import socket import shutil import re import select +import string import ganeti import testutils @@ -44,7 +45,7 @@ from ganeti.utils import IsProcessAlive, RunCmd, \ ParseUnit, AddAuthorizedKey, RemoveAuthorizedKey, \ ShellQuote, ShellQuoteArgs, TcpPing, ListVisibleFiles, \ SetEtcHostsEntry, RemoveEtcHostsEntry, FirstFree, OwnIpAddress, \ - TailFile, ForceDictType + TailFile, ForceDictType, SafeEncode from ganeti.errors import LockError, UnitParseError, GenericError, \ ProgrammerError @@ -969,5 +970,24 @@ class TestForceDictType(unittest.TestCase): self.assertRaises(errors.TypeEnforcementError, self._fdt, {'d': '4 L'}) +class TestSafeEncode(unittest.TestCase): + """Test case for SafeEncode""" + + def testAscii(self): + for txt in [string.digits, string.letters, string.punctuation]: + self.failUnlessEqual(txt, SafeEncode(txt)) + + def testDoubleEncode(self): + for i in range(255): + txt = SafeEncode(chr(i)) + self.failUnlessEqual(txt, SafeEncode(txt)) + + def testUnicode(self): + # 1024 is high enough to catch non-direct ASCII mappings + for i in range(1024): + txt = SafeEncode(unichr(i)) + self.failUnlessEqual(txt, SafeEncode(txt)) + + if __name__ == '__main__': unittest.main()