diff --git a/NEWS b/NEWS index 6709d3fa226d38a03e182369a338da5ffdecc222..cbbf76a78c842add206e4ed73dc8dd07847c047d 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,33 @@ +Version 2.0.2 + - Added experimental support for stripped logical volumes; this should + enhance performance but comes with a higher complexity in the block + device handling; stripping is only enabled when passing + --with-lvm-stripecount=N to configure, but codepaths are affected + even in the non-stripped mode + - Improved resiliency against transient failures at the end of DRBD + resyncs, and in general of DRBD resync checks + - Fixed a couple of issues with exports and snapshot errors + - Fixed a couple of issues in instance listing + - Added display of the disk size in βgnt-instance infoβ + - Fixed checking for valid OSes in instance creation + - Fixed handling of the βvcpusβ parameter in instance listing and in + general of invalid parameters + - Fixed http server library, and thus RAPI, to handle invalid + username/password combinations correctly; this means that now they + report unauthorized for queries too, not only for modifications, + allowing earlier detect of configuration problems + - Added a new βroleβ node list field, equivalent to the master/master + candidate/drained/offline flags combinations + - Fixed cluster modify and changes of candidate pool size + - Fixed cluster verify error messages for wrong files on regular nodes + - Fixed a couple of issues with node demotion from master candidate + role + - Fixed node readd issues + - Added non-interactive mode for βganeti-masterd --no-votingβ startup + - Added a new β--no-votingβ option for masterfailover to fix failover + on two-nodes clusters when the former master node is unreachable + - Added instance reinstall over RAPI + Version 2.0.1 - added -H/-B startup parameters to gnt-instance, which will allow re-adding the start in single-user option (regression from 1.2) diff --git a/configure.ac b/configure.ac index f9eddc061afd6ecae7d20feeaa648f3a54bd7dd4..3b4f0e200ae9cec60ed702c8cda0ac5c7e62f53d 100644 --- a/configure.ac +++ b/configure.ac @@ -1,7 +1,7 @@ # Configure script for Ganeti m4_define([gnt_version_major], [2]) m4_define([gnt_version_minor], [0]) -m4_define([gnt_version_revision], [1]) +m4_define([gnt_version_revision], [2]) m4_define([gnt_version_suffix], []) m4_define([gnt_version_full], m4_format([%d.%d.%d%s], @@ -123,10 +123,10 @@ AC_SUBST(SOCAT_PATH, $socat_path) AC_ARG_WITH([lvm-stripecount], [AS_HELP_STRING([--with-lvm-stripecount=NUM], [the number of stripes to use for LVM volumes] - [ (default is 3)] + [ (default is 1)] )], [lvm_stripecount="$withval"], - [lvm_stripecount="3"]) + [lvm_stripecount="1"]) AC_SUBST(LVM_STRIPECOUNT, $lvm_stripecount) # Check common programs diff --git a/lib/bdev.py b/lib/bdev.py index 4971b53b1e33d1a9164c811f4aae5f62c2d0b0fe..9ac41e50e6db08d3b7d99c5bf0d2ed185f5825cc 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -299,7 +299,7 @@ class LogicalVolume(BlockDev): self._vg_name, self._lv_name = unique_id self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name) self._degraded = True - self.major = self.minor = None + self.major = self.minor = self.pe_size = self.stripe_count = None self.Attach() @classmethod @@ -411,19 +411,30 @@ class LogicalVolume(BlockDev): """ self.attached = False result = utils.RunCmd(["lvs", "--noheadings", "--separator=,", - "-olv_attr,lv_kernel_major,lv_kernel_minor", - self.dev_path]) + "--units=m", "--nosuffix", + "-olv_attr,lv_kernel_major,lv_kernel_minor," + "vg_extent_size,stripes", self.dev_path]) if result.failed: logging.error("Can't find LV %s: %s, %s", self.dev_path, result.fail_reason, result.output) return False - out = result.stdout.strip().rstrip(',') + # the output can (and will) have multiple lines for multi-segment + # LVs, as the 'stripes' parameter is a segment one, so we take + # only the last entry, which is the one we're interested in; note + # that with LVM2 anyway the 'stripes' value must be constant + # across segments, so this is a no-op actually + out = result.stdout.splitlines() + if not out: # totally empty result? splitlines() returns at least + # one line for any non-empty string + logging.error("Can't parse LVS output, no lines? Got '%s'", str(out)) + return False + out = out[-1].strip().rstrip(',') out = out.split(",") - if len(out) != 3: - logging.error("Can't parse LVS output, len(%s) != 3", str(out)) + if len(out) != 5: + logging.error("Can't parse LVS output, len(%s) != 5", str(out)) return False - status, major, minor = out[:3] + status, major, minor, pe_size, stripes = out if len(status) != 6: logging.error("lvs lv_attr is not 6 characters (%s)", status) return False @@ -434,8 +445,22 @@ class LogicalVolume(BlockDev): except ValueError, err: logging.error("lvs major/minor cannot be parsed: %s", str(err)) + try: + pe_size = int(float(pe_size)) + except (TypeError, ValueError), err: + logging.error("Can't parse vg extent size: %s", err) + return False + + try: + stripes = int(stripes) + except (TypeError, ValueError), err: + logging.error("Can't parse the number of stripes: %s", err) + return False + self.major = major self.minor = minor + self.pe_size = pe_size + self.stripe_count = stripes self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing # storage self.attached = True @@ -554,6 +579,13 @@ class LogicalVolume(BlockDev): """Grow the logical volume. """ + if self.pe_size is None or self.stripe_count is None: + if not self.Attach(): + _ThrowError("Can't attach to LV during Grow()") + full_stripe_size = self.pe_size * self.stripe_count + rest = amount % full_stripe_size + if rest != 0: + amount += full_stripe_size - rest # we try multiple algorithms since the 'best' ones might not have # space available in the right place, but later ones might (since # they have less constraints); also note that only recent LVM @@ -1609,7 +1641,8 @@ class DRBD8(BaseDRBD): if len(self._children) != 2 or None in self._children: _ThrowError("drbd%d: cannot grow diskless device", self.minor) self._children[0].Grow(amount) - result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"]) + result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s", + "%dm" % (self.size + amount)]) if result.failed: _ThrowError("drbd%d: resize failed: %s", self.minor, result.output) diff --git a/lib/bootstrap.py b/lib/bootstrap.py index a94a9e5ff914def403a3be4ab72d554121b9c030..2b34f4111bc71c3be7aa6847544cb7e258147a80 100644 --- a/lib/bootstrap.py +++ b/lib/bootstrap.py @@ -139,6 +139,14 @@ def InitCluster(cluster_name, mac_prefix, if config.ConfigWriter.IsCluster(): raise errors.OpPrereqError("Cluster is already initialised") + if not enabled_hypervisors: + raise errors.OpPrereqError("Enabled hypervisors list must contain at" + " least one member") + invalid_hvs = set(enabled_hypervisors) - constants.HYPER_TYPES + if invalid_hvs: + raise errors.OpPrereqError("Enabled hypervisors contains invalid" + " entries: %s" % invalid_hvs) + hostname = utils.HostInfo() if hostname.ip.startswith("127."): diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 88d600d076a315e73f16e6089db978b4839bc572..c337cf0a811f674ab982e80088d83f85057729d5 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -1566,6 +1566,13 @@ class LUSetClusterParams(LogicalUnit): if self.op.enabled_hypervisors is not None: self.hv_list = self.op.enabled_hypervisors + if not self.hv_list: + raise errors.OpPrereqError("Enabled hypervisors list must contain at" + " least one member") + invalid_hvs = set(self.hv_list) - constants.HYPER_TYPES + if invalid_hvs: + raise errors.OpPrereqError("Enabled hypervisors contains invalid" + " entries: %s" % invalid_hvs) else: self.hv_list = cluster.enabled_hypervisors @@ -4736,7 +4743,7 @@ class LUCreateInstance(LogicalUnit): disks=[(d["size"], d["mode"]) for d in self.disks], bep=self.be_full, hvp=self.hv_full, - hypervisor=self.op.hypervisor, + hypervisor_name=self.op.hypervisor, )) nl = ([self.cfg.GetMasterNode(), self.op.pnode] + diff --git a/lib/config.py b/lib/config.py index d7523635177fed59b1c0f7b0d6e455435a75255f..ba4bbdf6137099c77cc8d19ac6506cb231d8b0f7 100644 --- a/lib/config.py +++ b/lib/config.py @@ -273,6 +273,20 @@ class ConfigWriter: data = self._config_data seen_lids = [] seen_pids = [] + + # global cluster checks + if not data.cluster.enabled_hypervisors: + result.append("enabled hypervisors list doesn't have any entries") + invalid_hvs = set(data.cluster.enabled_hypervisors) - constants.HYPER_TYPES + if invalid_hvs: + result.append("enabled hypervisors contains invalid entries: %s" % + invalid_hvs) + + if data.cluster.master_node not in data.nodes: + result.append("cluster has invalid primary node '%s'" % + data.cluster.master_node) + + # per-instance checks for instance_name in data.instances: instance = data.instances[instance_name] if instance.primary_node not in data.nodes: @@ -1158,32 +1172,6 @@ class ConfigWriter: constants.SS_RELEASE_VERSION: constants.RELEASE_VERSION, } - @locking.ssynchronized(_config_lock) - def InitConfig(self, version, cluster_config, master_node_config): - """Create the initial cluster configuration. - - It will contain the current node, which will also be the master - node, and no instances. - - @type version: int - @param version: Configuration version - @type cluster_config: objects.Cluster - @param cluster_config: Cluster configuration - @type master_node_config: objects.Node - @param master_node_config: Master node configuration - - """ - nodes = { - master_node_config.name: master_node_config, - } - - self._config_data = objects.ConfigData(version=version, - cluster=cluster_config, - nodes=nodes, - instances={}, - serial_no=1) - self._WriteConfig() - @locking.ssynchronized(_config_lock, shared=1) def GetVGName(self): """Return the volume group name. diff --git a/lib/rapi/baserlib.py b/lib/rapi/baserlib.py index 0950373fd35f200a99d3e2885f0b997940980e3b..c75dae18232291035c753c818633d2663f6178c4 100644 --- a/lib/rapi/baserlib.py +++ b/lib/rapi/baserlib.py @@ -250,6 +250,18 @@ class R_Generic(object): " '%s' parameter" % (name,)) return val + def _checkStringVariable(self, name, default=None): + """Return the parsed value of an int argument. + + """ + val = self.queryargs.get(name, default) + if isinstance(val, list): + if val: + val = val[0] + else: + val = default + return val + def getBodyParameter(self, name, *args): """Check and return the value for a given parameter. diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py index 9c99dcfa6c9ccae0deabd0adbf531e6128f3f5b5..c3c12e6c77450eea2898d34b4c5031fb81001c12 100644 --- a/lib/rapi/connector.py +++ b/lib/rapi/connector.py @@ -160,6 +160,8 @@ CONNECTOR.update({ re.compile(r'^/2/instances/([\w\._-]+)/tags$'): rlib2.R_2_instances_name_tags, re.compile(r'^/2/instances/([\w\._-]+)/reboot$'): rlib2.R_2_instances_name_reboot, + re.compile(r'^/2/instances/([\w\._-]+)/reinstall$'): + rlib2.R_2_instances_name_reinstall, re.compile(r'^/2/instances/([\w\._-]+)/shutdown$'): rlib2.R_2_instances_name_shutdown, re.compile(r'^/2/instances/([\w\._-]+)/startup$'): diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index 550692acd51e1be983fe7dd705e127e7de701011..9c8e2a09f582ad9e09d7656871a0b3abb1e5e58c 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -431,6 +431,36 @@ class R_2_instances_name_shutdown(baserlib.R_Generic): return baserlib.SubmitJob([op]) +class R_2_instances_name_reinstall(baserlib.R_Generic): + """/2/instances/[instance_name]/reinstall resource. + + Implements an instance reinstall. + + """ + + DOC_URI = "/2/instances/[instance_name]/reinstall" + + def POST(self): + """Reinstall an instance. + + The URI takes os=name and nostartup=[0|1] optional + parameters. By default, the instance will be started + automatically. + + """ + instance_name = self.items[0] + ostype = self._checkStringVariable('os') + nostartup = self._checkIntVariable('nostartup') + ops = [ + opcodes.OpShutdownInstance(instance_name=instance_name), + opcodes.OpReinstallInstance(instance_name=instance_name, os_type=ostype), + ] + if not nostartup: + ops.append(opcodes.OpStartupInstance(instance_name=instance_name, + force=False)) + return baserlib.SubmitJob(ops) + + class _R_Tags(baserlib.R_Generic): """ Quasiclass for tagging resources diff --git a/man/gnt-node.sgml b/man/gnt-node.sgml index 99f80bffd62d957cd01b81439fdfc98fea3d672a..ea02f1944c46e1501f3e690de9508d0da19bf555 100644 --- a/man/gnt-node.sgml +++ b/man/gnt-node.sgml @@ -414,13 +414,18 @@ <varlistentry> <term>drained</term> <listitem> - <simpara>whether the node is drained or not</simpara> + <simpara>whether the node is drained or not; the cluster + still communicates with drained nodes but excludes them + from allocation operations</simpara> </listitem> </varlistentry> <varlistentry> <term>offline</term> <listitem> - <simpara>whether the node is offline or not</simpara> + <simpara>whether the node is offline or not; if offline, + the cluster does not communicate with offline nodes; + useful for nodes that are not reachable in order to + avoid delays</simpara> </listitem> </varlistentry> <varlistentry> diff --git a/scripts/gnt-cluster b/scripts/gnt-cluster index b41a380c27e21b6937294bc5aedf39dfa89c8968..5e3420126de9543f3dab7f347c0012e030306808 100755 --- a/scripts/gnt-cluster +++ b/scripts/gnt-cluster @@ -79,11 +79,6 @@ def InitCluster(opts, args): hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv]) utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES) - for hv in hvlist: - if hv not in constants.HYPER_TYPES: - ToStderr("invalid hypervisor: %s", hv) - return 1 - bootstrap.InitCluster(cluster_name=args[0], secondary_ip=opts.secondary_ip, vg_name=vg_name,