diff --git a/NEWS b/NEWS index 568f37653a92530e8b9153a4d5f06860a3018736..2a09b2fbd19795848a6026ed9c024d60b83e124d 100644 --- a/NEWS +++ b/NEWS @@ -5,7 +5,7 @@ News Version 2.7.0 beta1 ------------------- -*(Released Wed, 6 Mar 2013)* +*(Released Wed, 6 Feb 2013)* - ``gnt-instance batch-create`` has been changed to use the bulk create opcode from Ganeti. This lead to incompatible changes in the format of @@ -48,10 +48,12 @@ Version 2.7.0 beta1 <rapi>` interface and when an instance allocator is used. If the ``opportunistic_locking`` parameter is set the opcode will try to acquire as many locks as possible, but will not wait for any locks - held by other opcodes. If the not enough resources can be found to + held by other opcodes. If not enough resources can be found to allocate the instance, the temporary error code :pyeval:`errors.ECODE_TEMP_NORES` is returned. The operation can be retried thereafter, with or without opportunistic locking. +- The functionality for allocating multiple instances at once has been + overhauled and is now also available through :doc:`RAPI <rapi>`. - Man pages can now be included when the documentation is built, in which case the output is in ``doc/man-html``. The configure-time option is ``--enable-manpages-in-doc``. Sphinx 1.0 or higher is diff --git a/doc/design-2.7.rst b/doc/design-2.7.rst index 1c27e2483b55edd464b95fbaac8f03c3c6bc92bf..3b5303dfc23c536b6a8f23d660cdcd290b4bdd5b 100644 --- a/doc/design-2.7.rst +++ b/doc/design-2.7.rst @@ -11,6 +11,8 @@ The following design documents have been implemented in Ganeti 2.7: - :doc:`design-virtual-clusters` - :doc:`design-network` - :doc:`design-linuxha` +- :doc:`design-shared-storage` (Updated to reflect the new ExtStorage + Interface) The following designs have been partially implemented in Ganeti 2.7: diff --git a/doc/design-shared-storage.rst b/doc/design-shared-storage.rst index 05e91c08d8ef672aff47a23adcaf911ea54498ce..5c9a9a508cd0503509047349cf3ac7b178593ed5 100644 --- a/doc/design-shared-storage.rst +++ b/doc/design-shared-storage.rst @@ -1,9 +1,9 @@ -====================================== -Ganeti shared storage support for 2.3+ -====================================== +============================= +Ganeti shared storage support +============================= This document describes the changes in Ganeti 2.3+ compared to Ganeti -2.3 storage model. +2.3 storage model. It also documents the ExtStorage Interface. .. contents:: :depth: 4 .. highlight:: shell-example diff --git a/doc/install.rst b/doc/install.rst index ec1c6ab3d0721947a3d73c36b271b93c99e1d4e9..8f753d656c2cac3dbd0d525b49268ea80568ab13 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -721,10 +721,19 @@ modify``. Your instance types, networking environment, hypervisor type and version may all affect what kind of parameters should be used on your cluster. -For example kvm instances are by default configured to use a host -kernel, and to be reached via serial console, which works nice for Linux -paravirtualized instances. If you want fully virtualized instances you -may want to handle their kernel inside the instance, and to use VNC. +.. admonition:: KVM + + Instances are by default configured to use a host kernel, and to be + reached via serial console, which works nice for Linux paravirtualized + instances. If you want fully virtualized instances you may want to + handle their kernel inside the instance, and to use VNC. + + Some versions of KVM have a bug that will make an instance hang when + configured to use the serial console (which is the default) unless a + connection is made to it within about 2 seconds of the instance's + startup. For such case it's recommended to disable the + ``serial_console`` option. + Joining the nodes to the cluster ++++++++++++++++++++++++++++++++ diff --git a/lib/backend.py b/lib/backend.py index 9996017dc5d92148b90924b5b5a58a6486787b15..17d746b2cbb99eef2c21b60d07feefeb9394cea4 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -2559,11 +2559,6 @@ def OSEnvironment(instance, inst_os, debug=0): if nic.netinfo: nobj = objects.Network.FromDict(nic.netinfo) result.update(nobj.HooksDict("NIC_%d_" % idx)) - elif nic.network: - # FIXME: broken network reference: the instance NIC specifies a network, - # but the relevant network entry was not in the config. This should be - # made impossible. - result["INSTANCE_NIC%d_NETWORK" % idx] = nic.network if constants.HV_NIC_TYPE in instance.hvparams: result["NIC_%d_FRONTEND_TYPE" % idx] = \ instance.hvparams[constants.HV_NIC_TYPE] diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py index c92c1bbd02510d25658126838fe48ed4fbab900c..4f1538d59dd52625893a438470feadce8a15ac9b 100644 --- a/lib/client/gnt_instance.py +++ b/lib/client/gnt_instance.py @@ -1179,10 +1179,13 @@ def ShowInstanceConfig(opts, args): FormatParameterDict(buf, instance["be_instance"], be_actual, level=2) # TODO(ganeti 2.7) rework the NICs as well buf.write(" - NICs:\n") - for idx, (ip, mac, mode, link, network, _) in enumerate(instance["nics"]): + for idx, (ip, mac, mode, link, _, netinfo) in enumerate(instance["nics"]): + network_name = None + if netinfo: + network_name = netinfo["name"] buf.write(" - nic/%d: MAC: %s, IP: %s," " mode: %s, link: %s, network: %s\n" % - (idx, mac, ip, mode, link, network)) + (idx, mac, ip, mode, link, network_name)) buf.write(" Disk template: %s\n" % instance["disk_template"]) buf.write(" Disks:\n") diff --git a/lib/client/gnt_network.py b/lib/client/gnt_network.py index 17c1bace7f63f3b430f52f61d480ebff0c898533..558903fb94d564c7fa7c3a5c709d4f3087ab8cff 100644 --- a/lib/client/gnt_network.py +++ b/lib/client/gnt_network.py @@ -252,7 +252,7 @@ def ShowNetworkConfig(_, args): l = lambda value: ", ".join(str(idx) + ":" + str(ip) for idx, (ip, net) in enumerate(value) - if net == name) + if net == uuid) ToStdout(" %s : %s", inst, l(zip(ips, networks))) else: diff --git a/lib/cmdlib.py b/lib/cmdlib.py index bf49c6891c7faa6646f1bd7933e4dcb5aedabff6..133a043f9094c99f03a51a1457b13ec250f22771 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -1557,9 +1557,8 @@ def _NICToTuple(lu, nic): link = filled_params[constants.NIC_LINK] netinfo = None if nic.network: - net_uuid = lu.cfg.LookupNetwork(nic.network) - netinfo = objects.Network.ToDict(lu.cfg.GetNetwork(net_uuid)) - + nobj = lu.cfg.GetNetwork(nic.network) + netinfo = objects.Network.ToDict(nobj) return (nic.ip, nic.mac, mode, link, nic.network, netinfo) @@ -5752,6 +5751,7 @@ class _InstanceQuery(_QueryBase): lu.needed_locks[locking.LEVEL_INSTANCE] = self.wanted lu.needed_locks[locking.LEVEL_NODEGROUP] = [] lu.needed_locks[locking.LEVEL_NODE] = [] + lu.needed_locks[locking.LEVEL_NETWORK] = [] lu.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE self.do_grouplocks = (self.do_locking and @@ -5771,6 +5771,12 @@ class _InstanceQuery(_QueryBase): elif level == locking.LEVEL_NODE: lu._LockInstancesNodes() # pylint: disable=W0212 + elif level == locking.LEVEL_NETWORK: + lu.needed_locks[locking.LEVEL_NETWORK] = \ + frozenset(net_uuid + for instance_name in lu.owned_locks(locking.LEVEL_INSTANCE) + for net_uuid in lu.cfg.GetInstanceNetworks(instance_name)) + @staticmethod def _CheckGroupLocks(lu): owned_instances = frozenset(lu.owned_locks(locking.LEVEL_INSTANCE)) @@ -5861,10 +5867,17 @@ class _InstanceQuery(_QueryBase): nodes = None groups = None + if query.IQ_NETWORKS in self.requested_data: + net_uuids = itertools.chain(*(lu.cfg.GetInstanceNetworks(i.name) + for i in instance_list)) + networks = dict((uuid, lu.cfg.GetNetwork(uuid)) for uuid in net_uuids) + else: + networks = None + return query.InstanceQueryData(instance_list, lu.cfg.GetClusterInfo(), disk_usage, offline_nodes, bad_nodes, live_data, wrongnode_inst, consinfo, - nodes, groups) + nodes, groups, networks) class LUQuery(NoHooksLU): @@ -9952,8 +9965,9 @@ def _ComputeNics(op, cluster, default_ip, cfg, ec_id): check_params = cluster.SimpleFillNIC(nicparams) objects.NIC.CheckParameterSyntax(check_params) + net_uuid = cfg.LookupNetwork(net) nics.append(objects.NIC(mac=mac, ip=nic_ip, - network=net, nicparams=nicparams)) + network=net_uuid, nicparams=nicparams)) return nics @@ -10689,14 +10703,15 @@ class LUInstanceCreate(LogicalUnit): # Fill in any IPs from IP pools. This must happen here, because we need to # know the nic's primary node, as specified by the iallocator for idx, nic in enumerate(self.nics): - net = nic.network - if net is not None: - netparams = self.cfg.GetGroupNetParams(net, self.pnode.name) + net_uuid = nic.network + if net_uuid is not None: + nobj = self.cfg.GetNetwork(net_uuid) + netparams = self.cfg.GetGroupNetParams(net_uuid, self.pnode.name) if netparams is None: raise errors.OpPrereqError("No netparams found for network" " %s. Propably not connected to" " node's %s nodegroup" % - (net, self.pnode.name), + (nobj.name, self.pnode.name), errors.ECODE_INVAL) self.LogInfo("NIC/%d inherits netparams %s" % (idx, netparams.values())) @@ -10704,19 +10719,19 @@ class LUInstanceCreate(LogicalUnit): if nic.ip is not None: if nic.ip.lower() == constants.NIC_IP_POOL: try: - nic.ip = self.cfg.GenerateIp(net, self.proc.GetECId()) + nic.ip = self.cfg.GenerateIp(net_uuid, self.proc.GetECId()) except errors.ReservationError: raise errors.OpPrereqError("Unable to get a free IP for NIC %d" " from the address pool" % idx, errors.ECODE_STATE) - self.LogInfo("Chose IP %s from network %s", nic.ip, net) + self.LogInfo("Chose IP %s from network %s", nic.ip, nobj.name) else: try: - self.cfg.ReserveIp(net, nic.ip, self.proc.GetECId()) + self.cfg.ReserveIp(net_uuid, nic.ip, self.proc.GetECId()) except errors.ReservationError: raise errors.OpPrereqError("IP address %s already in use" " or does not belong to network %s" % - (nic.ip, net), + (nic.ip, nobj.name), errors.ECODE_NOTUNIQUE) # net is None, ip None or given @@ -12817,12 +12832,13 @@ class LUInstanceQueryData(NoHooksLU): self.needed_locks[locking.LEVEL_NODEGROUP] = [] self.needed_locks[locking.LEVEL_NODE] = [] + self.needed_locks[locking.LEVEL_NETWORK] = [] self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE def DeclareLocks(self, level): if self.op.use_locking: + owned_instances = self.owned_locks(locking.LEVEL_INSTANCE) if level == locking.LEVEL_NODEGROUP: - owned_instances = self.owned_locks(locking.LEVEL_INSTANCE) # Lock all groups used by instances optimistically; this requires going # via the node before it's locked, requiring verification later on @@ -12835,6 +12851,13 @@ class LUInstanceQueryData(NoHooksLU): elif level == locking.LEVEL_NODE: self._LockInstancesNodes() + elif level == locking.LEVEL_NETWORK: + self.needed_locks[locking.LEVEL_NETWORK] = \ + frozenset(net_uuid + for instance_name in owned_instances + for net_uuid in + self.cfg.GetInstanceNetworks(instance_name)) + def CheckPrereq(self): """Check prerequisites. @@ -12844,6 +12867,7 @@ class LUInstanceQueryData(NoHooksLU): owned_instances = frozenset(self.owned_locks(locking.LEVEL_INSTANCE)) owned_groups = frozenset(self.owned_locks(locking.LEVEL_NODEGROUP)) owned_nodes = frozenset(self.owned_locks(locking.LEVEL_NODE)) + owned_networks = frozenset(self.owned_locks(locking.LEVEL_NETWORK)) if self.wanted_names is None: assert self.op.use_locking, "Locking was not used" @@ -12855,7 +12879,8 @@ class LUInstanceQueryData(NoHooksLU): _CheckInstancesNodeGroups(self.cfg, instances, owned_groups, owned_nodes, None) else: - assert not (owned_instances or owned_groups or owned_nodes) + assert not (owned_instances or owned_groups or + owned_nodes or owned_networks) self.wanted_instances = instances.values() @@ -12939,7 +12964,6 @@ class LUInstanceQueryData(NoHooksLU): for node in nodes.values())) group2name_fn = lambda uuid: groups[uuid].name - for instance in self.wanted_instances: pnode = nodes[instance.primary_node] @@ -13383,7 +13407,7 @@ class LUInstanceSetParams(LogicalUnit): nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes) return (nl, nl) - def _PrepareNicModification(self, params, private, old_ip, old_net, + def _PrepareNicModification(self, params, private, old_ip, old_net_uuid, old_params, cluster, pnode): update_params_dict = dict([(key, params[key]) @@ -13393,13 +13417,21 @@ class LUInstanceSetParams(LogicalUnit): req_link = update_params_dict.get(constants.NIC_LINK, None) req_mode = update_params_dict.get(constants.NIC_MODE, None) - new_net = params.get(constants.INIC_NETWORK, old_net) - if new_net is not None: - netparams = self.cfg.GetGroupNetParams(new_net, pnode) - if netparams is None: + new_net_uuid = None + new_net_uuid_or_name = params.get(constants.INIC_NETWORK, old_net_uuid) + if new_net_uuid_or_name: + new_net_uuid = self.cfg.LookupNetwork(new_net_uuid_or_name) + new_net_obj = self.cfg.GetNetwork(new_net_uuid) + + if old_net_uuid: + old_net_obj = self.cfg.GetNetwork(old_net_uuid) + + if new_net_uuid: + netparams = self.cfg.GetGroupNetParams(new_net_uuid, pnode) + if not netparams: raise errors.OpPrereqError("No netparams found for the network" - " %s, probably not connected" % new_net, - errors.ECODE_INVAL) + " %s, probably not connected" % + new_net_obj.name, errors.ECODE_INVAL) new_params = dict(netparams) else: new_params = _GetUpdatedParams(old_params, update_params_dict) @@ -13438,7 +13470,7 @@ class LUInstanceSetParams(LogicalUnit): elif mac in (constants.VALUE_AUTO, constants.VALUE_GENERATE): # otherwise generate the MAC address params[constants.INIC_MAC] = \ - self.cfg.GenerateMAC(new_net, self.proc.GetECId()) + self.cfg.GenerateMAC(new_net_uuid, self.proc.GetECId()) else: # or validate/reserve the current one try: @@ -13447,61 +13479,61 @@ class LUInstanceSetParams(LogicalUnit): raise errors.OpPrereqError("MAC address '%s' already in use" " in cluster" % mac, errors.ECODE_NOTUNIQUE) - elif new_net != old_net: + elif new_net_uuid != old_net_uuid: - def get_net_prefix(net): + def get_net_prefix(net_uuid): mac_prefix = None - if net: - uuid = self.cfg.LookupNetwork(net) - mac_prefix = self.cfg.GetNetwork(uuid).mac_prefix + if net_uuid: + nobj = self.cfg.GetNetwork(net_uuid) + mac_prefix = nobj.mac_prefix return mac_prefix - new_prefix = get_net_prefix(new_net) - old_prefix = get_net_prefix(old_net) + new_prefix = get_net_prefix(new_net_uuid) + old_prefix = get_net_prefix(old_net_uuid) if old_prefix != new_prefix: params[constants.INIC_MAC] = \ - self.cfg.GenerateMAC(new_net, self.proc.GetECId()) + self.cfg.GenerateMAC(new_net_uuid, self.proc.GetECId()) - #if there is a change in nic-network configuration + #if there is a change in nic's ip/network configuration new_ip = params.get(constants.INIC_IP, old_ip) - if (new_ip, new_net) != (old_ip, old_net): + if (new_ip, new_net_uuid) != (old_ip, old_net_uuid): if new_ip: - if new_net: - if new_ip.lower() == constants.NIC_IP_POOL: - try: - new_ip = self.cfg.GenerateIp(new_net, self.proc.GetECId()) - except errors.ReservationError: - raise errors.OpPrereqError("Unable to get a free IP" - " from the address pool", - errors.ECODE_STATE) - self.LogInfo("Chose IP %s from pool %s", new_ip, new_net) - params[constants.INIC_IP] = new_ip - elif new_ip != old_ip or new_net != old_net: - try: - self.LogInfo("Reserving IP %s in pool %s", new_ip, new_net) - self.cfg.ReserveIp(new_net, new_ip, self.proc.GetECId()) - except errors.ReservationError: - raise errors.OpPrereqError("IP %s not available in network %s" % - (new_ip, new_net), - errors.ECODE_NOTUNIQUE) - elif new_ip.lower() == constants.NIC_IP_POOL: - raise errors.OpPrereqError("ip=pool, but no network found", - errors.ECODE_INVAL) + if new_ip.lower() == constants.NIC_IP_POOL: + if not new_net_uuid: + raise errors.OpPrereqError("ip=pool, but no network found", + errors.ECODE_INVAL) + try: + new_ip = self.cfg.GenerateIp(new_net_uuid, self.proc.GetECId()) + except errors.ReservationError: + raise errors.OpPrereqError("Unable to get a free IP" + " from the address pool", + errors.ECODE_STATE) + self.LogInfo("Chose IP %s from network %s", new_ip, new_net_obj.name) + params[constants.INIC_IP] = new_ip + elif new_ip != old_ip or new_net_uuid != old_net_uuid: + try: + self.cfg.ReserveIp(new_net_uuid, new_ip, self.proc.GetECId()) + self.LogInfo("Reserving IP %s in network %s", + new_ip, new_net_obj.name) + except errors.ReservationError: + raise errors.OpPrereqError("IP %s not available in network %s" % + (new_ip, new_net_obj.name), + errors.ECODE_NOTUNIQUE) # new net is None - elif self.op.conflicts_check: + elif not new_net_uuid and self.op.conflicts_check: _CheckForConflictingIp(self, new_ip, pnode) - if old_ip and old_net: + if old_ip: try: - self.cfg.ReleaseIp(old_net, old_ip, self.proc.GetECId()) - except errors.AddressPoolError, err: - logging.warning("Releasing IP address '%s' from network '%s'" - " failed: %s", old_ip, old_net, err) + self.cfg.ReleaseIp(old_net_uuid, old_ip, self.proc.GetECId()) + except errors.AddressPoolError: + logging.warning("Release IP %s not contained in network %s", + old_ip, old_net_obj.name) # there are no changes in (net, ip) tuple - elif (old_net is not None and + elif (old_net_uuid is not None and (req_link is not None or req_mode is not None)): raise errors.OpPrereqError("Not allowed to change link or mode of" " a NIC that is connected to a network", @@ -16536,8 +16568,6 @@ class _NetworkQuery(_QueryBase): network_uuids = self._GetNames(lu, all_networks.keys(), locking.LEVEL_NETWORK) - name_to_uuid = dict((n.name, n.uuid) for n in all_networks.values()) - do_instances = query.NETQ_INST in self.requested_data do_groups = query.NETQ_GROUP in self.requested_data @@ -16562,10 +16592,8 @@ class _NetworkQuery(_QueryBase): network_to_instances = dict((uuid, []) for uuid in network_uuids) for instance in all_instances.values(): for nic in instance.nics: - if nic.network: - net_uuid = name_to_uuid[nic.network] - if net_uuid in network_uuids: - network_to_instances[net_uuid].append(instance.name) + if nic.network in network_uuids: + network_to_instances[nic.network].append(instance.name) break if query.NETQ_STATS in self.requested_data: @@ -16797,7 +16825,7 @@ class LUNetworkDisconnect(LogicalUnit): self.connected = False return - _NetworkConflictCheck(self, lambda nic: nic.network == self.network_name, + _NetworkConflictCheck(self, lambda nic: nic.network == self.network_uuid, "disconnect from") def Exec(self, feedback_fn): diff --git a/lib/config.py b/lib/config.py index 4f8c36db0d01fa76ee81ed01ebf5b46b7bd2b0cd..13f9823604416439c49f819a866ff298a2b65890 100644 --- a/lib/config.py +++ b/lib/config.py @@ -270,13 +270,12 @@ class ConfigWriter: """ return self._config_data.cluster.SimpleFillDP(group.diskparams) - def _UnlockedGetNetworkMACPrefix(self, net): + def _UnlockedGetNetworkMACPrefix(self, net_uuid): """Return the network mac prefix if it exists or the cluster level default. """ prefix = None - if net: - net_uuid = self._UnlockedLookupNetwork(net) + if net_uuid: nobj = self._UnlockedGetNetwork(net_uuid) if nobj.mac_prefix: prefix = nobj.mac_prefix @@ -302,14 +301,14 @@ class ConfigWriter: return GenMac @locking.ssynchronized(_config_lock, shared=1) - def GenerateMAC(self, net, ec_id): + def GenerateMAC(self, net_uuid, ec_id): """Generate a MAC for an instance. This should check the current instances for duplicates. """ existing = self._AllMACs() - prefix = self._UnlockedGetNetworkMACPrefix(net) + prefix = self._UnlockedGetNetworkMACPrefix(net_uuid) gen_mac = self._GenerateOneMAC(prefix) return self._temporary_ids.Generate(existing, gen_mac, ec_id) @@ -358,21 +357,20 @@ class ConfigWriter: (constants.RELEASE_ACTION, address, net_uuid)) @locking.ssynchronized(_config_lock, shared=1) - def ReleaseIp(self, net, address, ec_id): + def ReleaseIp(self, net_uuid, address, ec_id): """Give a specified IP address back to an IP pool. This is just a wrapper around _UnlockedReleaseIp. """ - net_uuid = self._UnlockedLookupNetwork(net) - self._UnlockedReleaseIp(net_uuid, address, ec_id) + if net_uuid: + self._UnlockedReleaseIp(net_uuid, address, ec_id) @locking.ssynchronized(_config_lock, shared=1) - def GenerateIp(self, net, ec_id): + def GenerateIp(self, net_uuid, ec_id): """Find a free IPv4 address for an instance. """ - net_uuid = self._UnlockedLookupNetwork(net) nobj = self._UnlockedGetNetwork(net_uuid) pool = network.AddressPool(nobj) @@ -404,12 +402,12 @@ class ConfigWriter: address, net_uuid)) @locking.ssynchronized(_config_lock, shared=1) - def ReserveIp(self, net, address, ec_id): + def ReserveIp(self, net_uuid, address, ec_id): """Reserve a given IPv4 address for use by an instance. """ - net_uuid = self._UnlockedLookupNetwork(net) - return self._UnlockedReserveIp(net_uuid, address, ec_id) + if net_uuid: + return self._UnlockedReserveIp(net_uuid, address, ec_id) @locking.ssynchronized(_config_lock, shared=1) def ReserveLV(self, lv_name, ec_id): @@ -1452,10 +1450,9 @@ class ConfigWriter: instance = self._UnlockedGetInstanceInfo(instance_name) for nic in instance.nics: - if nic.network is not None and nic.ip is not None: - net_uuid = self._UnlockedLookupNetwork(nic.network) + if nic.network and nic.ip: # Return all IP addresses to the respective address pools - self._UnlockedCommitIp(constants.RELEASE_ACTION, net_uuid, nic.ip) + self._UnlockedCommitIp(constants.RELEASE_ACTION, nic.network, nic.ip) del self._config_data.instances[instance_name] self._config_data.cluster.serial_no += 1 @@ -1573,6 +1570,24 @@ class ConfigWriter: return frozenset(self._UnlockedGetNodeInfo(node_name).group for node_name in nodes) + @locking.ssynchronized(_config_lock, shared=1) + def GetInstanceNetworks(self, instance_name): + """Returns set of network UUIDs for instance's nics. + + @rtype: frozenset + + """ + instance = self._UnlockedGetInstanceInfo(instance_name) + if not instance: + raise errors.ConfigurationError("Unknown instance '%s'" % instance_name) + + networks = set() + for nic in instance.nics: + if nic.network: + networks.add(nic.network) + + return frozenset(networks) + @locking.ssynchronized(_config_lock, shared=1) def GetMultiInstanceInfo(self, instances): """Get the configuration of multiple instances. @@ -2487,6 +2502,8 @@ class ConfigWriter: @raises errors.OpPrereqError: when the target network cannot be found """ + if target is None: + return None if target in self._config_data.networks: return target for net in self._config_data.networks.values(): @@ -2526,20 +2543,19 @@ class ConfigWriter: self._config_data.cluster.serial_no += 1 self._WriteConfig() - def _UnlockedGetGroupNetParams(self, net, node): + def _UnlockedGetGroupNetParams(self, net_uuid, node): """Get the netparams (mode, link) of a network. Get a network's netparams for a given node. - @type net: string - @param net: network name + @type net_uuid: string + @param net_uuid: network uuid @type node: string @param node: node name @rtype: dict or None @return: netparams """ - net_uuid = self._UnlockedLookupNetwork(net) node_info = self._UnlockedGetNodeInfo(node) nodegroup_info = self._UnlockedGetNodeGroup(node_info.group) netparams = nodegroup_info.networks.get(net_uuid, None) @@ -2547,11 +2563,11 @@ class ConfigWriter: return netparams @locking.ssynchronized(_config_lock, shared=1) - def GetGroupNetParams(self, net, node): + def GetGroupNetParams(self, net_uuid, node): """Locking wrapper of _UnlockedGetGroupNetParams() """ - return self._UnlockedGetGroupNetParams(net, node) + return self._UnlockedGetGroupNetParams(net_uuid, node) @locking.ssynchronized(_config_lock, shared=1) def CheckIPInNodeGroup(self, ip, node): diff --git a/lib/query.py b/lib/query.py index ef52d0899cd4a5593faa961cc177109c6f8a7b0b..a9e027456ac98f2ce92645fd8b02ee17b62669fb 100644 --- a/lib/query.py +++ b/lib/query.py @@ -90,7 +90,8 @@ from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER, IQ_LIVE, IQ_DISKUSAGE, IQ_CONSOLE, - IQ_NODES) = range(100, 105) + IQ_NODES, + IQ_NETWORKS) = range(100, 106) (LQ_MODE, LQ_OWNER, @@ -1383,7 +1384,7 @@ class InstanceQueryData: """ def __init__(self, instances, cluster, disk_usage, offline_nodes, bad_nodes, - live_data, wrongnode_inst, console, nodes, groups): + live_data, wrongnode_inst, console, nodes, groups, networks): """Initializes this class. @param instances: List of instance objects @@ -1402,6 +1403,8 @@ class InstanceQueryData: @param console: Per-instance console information @type nodes: dict; node name as key @param nodes: Node objects + @type networks: dict; net_uuid as key + @param networks: Network objects """ assert len(set(bad_nodes) & set(offline_nodes)) == len(offline_nodes), \ @@ -1419,6 +1422,7 @@ class InstanceQueryData: self.console = console self.nodes = nodes self.groups = groups + self.networks = networks # Used for individual rows self.inst_hvparams = None @@ -1569,6 +1573,20 @@ def _GetInstNic(index, cb): return fn +def _GetInstNicNetworkName(ctx, _, nic): # pylint: disable=W0613 + """Get a NIC's Network. + + @type ctx: L{InstanceQueryData} + @type nic: L{objects.NIC} + @param nic: NIC object + + """ + if nic.network is None: + return _FS_UNAVAIL + else: + return ctx.networks[nic.network].name + + def _GetInstNicNetwork(ctx, _, nic): # pylint: disable=W0613 """Get a NIC's Network. @@ -1615,6 +1633,27 @@ def _GetInstNicBridge(ctx, index, _): return _FS_UNAVAIL +def _GetInstAllNicNetworkNames(ctx, inst): + """Get all network names for an instance. + + @type ctx: L{InstanceQueryData} + @type inst: L{objects.Instance} + @param inst: Instance object + + """ + result = [] + + for nic in inst.nics: + name = None + if nic.network: + name = ctx.networks[nic.network].name + result.append(name) + + assert len(result) == len(inst.nics) + + return result + + def _GetInstAllNicBridges(ctx, inst): """Get all network bridges for an instance. @@ -1697,6 +1736,9 @@ def _GetInstanceNetworkFields(): (_MakeField("nic.networks", "NIC_networks", QFT_OTHER, "List containing each interface's network"), IQ_CONFIG, 0, lambda ctx, inst: [nic.network for nic in inst.nics]), + (_MakeField("nic.networks.names", "NIC_networks_names", QFT_OTHER, + "List containing each interface's network"), + IQ_NETWORKS, 0, _GetInstAllNicNetworkNames) ] # NICs by number @@ -1721,6 +1763,9 @@ def _GetInstanceNetworkFields(): (_MakeField("nic.network/%s" % i, "NicNetwork/%s" % i, QFT_TEXT, "Network of %s network interface" % numtext), IQ_CONFIG, 0, _GetInstNic(i, _GetInstNicNetwork)), + (_MakeField("nic.network.name/%s" % i, "NicNetworkName/%s" % i, QFT_TEXT, + "Network name of %s network interface" % numtext), + IQ_NETWORKS, 0, _GetInstNic(i, _GetInstNicNetworkName)), ]) aliases = [ diff --git a/man/ganeti-extstorage-interface.rst b/man/ganeti-extstorage-interface.rst index 4b1e0b14694db4cac1e8a3094f601252faff8c91..81c03c8db971bba164a022a19837d2739eaa2300 100644 --- a/man/ganeti-extstorage-interface.rst +++ b/man/ganeti-extstorage-interface.rst @@ -66,7 +66,6 @@ VOL_METADATA EXECUTABLE SCRIPTS ------------------ - create ~~~~~~ @@ -198,7 +197,6 @@ The script should return ``0`` on success. TEXT FILES ---------- - parameters.list ~~~~~~~~~~~~~~~ @@ -213,6 +211,37 @@ The parameters can then be used during instance add as follows:: # gnt-instance add --disk=0:fromsnap="file_name",nas_ip="1.2.3.4" ... +EXAMPLES +-------- + +In the following examples we assume that you have already installed +successfully two ExtStorage providers: ``pvdr1`` and ``pvdr2`` + +Add a new instance with a 10G first disk provided by ``pvdr1`` and a 20G +second disk provided by ``pvdr2``:: + + # gnt-instance add -t ext --disk=0:size=10G,provider=pvdr1 + --disk=1:size=20G,provider=pvdr2 + +Add a new instance with a 5G first disk provided by provider ``pvdr1`` +and also pass the ``prm1``, ``prm2`` parameters to the provider, with +the corresponding values ``val1``, ``val2``:: + + # gnt-instance add -t ext + --disk=0:size=5G,provider=pvdr1,prm1=val1,prm2=val2 + +Modify an existing instance of disk type ``ext`` by adding a new 30G +disk provided by provider ``pvdr2``:: + + # gnt-instance modify --disk 1:add,size=30G,provider=pvdr2 <instance> + +Modify an existing instance of disk type ``ext`` by adding 2 new disks, +of different providers, passing one parameter for the first one:: + + # gnt-instance modify --disk 2:add,size=3G,provider=pvdr1,prm1=val1 + --disk 3:add,size=5G,provider=pvdr2 + <instance> + NOTES ----- diff --git a/man/gnt-instance.rst b/man/gnt-instance.rst index 1cb945de8ae5c9762e4ed993812e08ca2360e8e8..d5c7d098c09ac36f28371405a3abebd906874d6a 100644 --- a/man/gnt-instance.rst +++ b/man/gnt-instance.rst @@ -29,6 +29,7 @@ ADD | **add** | {-t|\--disk-template {diskless | file \| plain \| drbd \| rbd}} | {\--disk=*N*: {size=*VAL* \| adopt=*LV*}[,vg=*VG*][,metavg=*VG*][,mode=*ro\|rw*] +| \| {size=*VAL*,provider=*PROVIDER*}[,param=*value*... ][,mode=*ro\|rw*] | \| {-s|\--os-size} *SIZE*} | [\--no-ip-check] [\--no-name-check] [\--no-start] [\--no-install] | [\--net=*N* [:options...] \| \--no-nics] @@ -50,12 +51,20 @@ The ``disk`` option specifies the parameters for the disks of the instance. The numbering of disks starts at zero, and at least one disk needs to be passed. For each disk, either the size or the adoption source needs to be given, and optionally the access mode (read-only or -the default of read-write) and the LVM volume group can also be -specified (via the ``vg`` key). For DRBD devices, a different VG can -be specified for the metadata device using the ``metavg`` key. The -size is interpreted (when no unit is given) in mebibytes. You can also -use one of the suffixes *m*, *g* or *t* to specify the exact the units -used; these suffixes map to mebibytes, gibibytes and tebibytes. +the default of read-write). The size is interpreted (when no unit is +given) in mebibytes. You can also use one of the suffixes *m*, *g* or +*t* to specify the exact the units used; these suffixes map to +mebibytes, gibibytes and tebibytes. For LVM and DRBD devices, the LVM +volume group can also be specified (via the ``vg`` key). For DRBD +devices, a different VG can be specified for the metadata device using +the ``metavg`` key. For ExtStorage devices, also the ``provider`` +option is mandatory, to specify which ExtStorage provider to use. + +When creating ExtStorage disks, also arbitrary parameters can be passed, +to the ExtStorage provider. Those parameters are passed as additional +comma separated options. Therefore, an ExtStorage disk provided by +provider ``pvdr1`` with parameters ``param1``, ``param2`` would be +passed as ``--disk 0:size=10G,provider=pvdr1,param1=val1,param2=val2``. When using the ``adopt`` key in the disk definition, Ganeti will reuse those volumes (instead of creating new ones) as the @@ -75,6 +84,10 @@ The minimum disk specification is therefore ``--disk 0:size=20G`` (or can be specified as ``--disk 0:size=20G --disk 1:size=4G --disk 2:size=100G``. +The minimum information needed to specify an ExtStorage disk are the +``size`` and the ``provider``. For example: +``--disk 0:size=20G,provider=pvdr1``. + The ``--no-ip-check`` skips the checks that are done to see if the instance's IP is not already alive (i.e. reachable from the master node). @@ -479,7 +492,11 @@ serial\_console Valid for the KVM hypervisor. This boolean option specifies whether to emulate a serial console - for the instance. + for the instance. Note that some versions of KVM have a bug that + will make an instance hang when configured to use the serial console + unless a connection is made to it within about 2 seconds of the + instance's startup. For such case it's recommended to disable this + option, which is enabled by default. serial\_speed Valid for the KVM hypervisor. @@ -717,6 +734,9 @@ diskless file Disk devices will be regular files. +sharedfile + Disk devices will be regulare files on a shared directory. + plain Disk devices will be logical volumes. @@ -726,6 +746,12 @@ drbd rbd Disk devices will be rbd volumes residing inside a RADOS cluster. +blockdev + Disk devices will be adopted pre-existent block devices. + +ext + Disk devices will be provided by external shared storage, + through the ExtStorage Interface using ExtStorage providers. The optional second value of the ``-n (--node)`` is used for the drbd template type and specifies the remote node. @@ -779,6 +805,13 @@ Example:: -B maxmem=512 -o debian-etch -n node1.example.com instance1.example.com # gnt-instance add -t drbd --disk 0:size=30g -B maxmem=512 -o debian-etch \ -n node1.example.com:node2.example.com instance2.example.com + # gnt-instance add -t rbd --disk 0:size=30g -B maxmem=512 -o debian-etch \ + -n node1.example.com instance1.example.com + # gnt-instance add -t ext --disk 0:size=30g,provider=pvdr1 -B maxmem=512 \ + -o debian-etch -n node1.example.com instance1.example.com + # gnt-instance add -t ext --disk 0:size=30g,provider=pvdr1,param1=val1 \ + --disk 1:size=40g,provider=pvdr2,param2=val2,param3=val3 -B maxmem=512 \ + -o debian-etch -n node1.example.com instance1.example.com BATCH-CREATE @@ -994,7 +1027,9 @@ MODIFY | [{-B|\--backend-parameters} *BACKEND\_PARAMETERS*] | [{-m|\--runtime-memory} *SIZE*] | [\--net add*[:options]* \| \--net [*N*:]remove \| \--net *N:options*] -| [\--disk add:size=*SIZE*[,vg=*VG*][,metavg=*VG*] \| \--disk [*N*:]remove \| +| [\--disk add:size=*SIZE*[,vg=*VG*][,metavg=*VG*] \| +| \--disk add:size=*SIZE*,provider=*PROVIDER*[,param=*value*... ] \| +| \--disk [*N*:]remove \| | \--disk *N*:mode=*MODE*] | [{-t|\--disk-template} plain | {-t|\--disk-template} drbd -n *new_secondary*] [\--no-wait-for-sync] | [\--os-type=*OS* [\--force-variant]] @@ -1028,15 +1063,18 @@ memory to the given size (in MB if a different suffix is not specified), by ballooning it up or down to the new value. The ``--disk add:size=``*SIZE* option adds a disk to the instance. The -optional ``vg=``*VG* option specifies an LVM volume group other than -the default volume group to create the disk on. For DRBD disks, the +optional ``vg=``*VG* option specifies an LVM volume group other than the +default volume group to create the disk on. For DRBD disks, the ``metavg=``*VG* option specifies the volume group for the metadata -device. ``--disk`` *N*``:add,size=``**SIZE** can be used to add a -disk at a specific index. The ``--disk remove`` option will remove the -last disk of the instance. Use ``--disk `` *N*``:remove`` to remove a -disk by its index. The ``--disk`` *N*``:mode=``*MODE* option will change -the mode of the Nth disk of the instance between read-only (``ro``) and -read-write (``rw``). +device. When adding an ExtStorage disk the ``provider=``*PROVIDER* +option is also mandatory and specifies the ExtStorage provider. Also, +for ExtStorage disks arbitrary parameters can be passed as additional +comma separated options, same as in the **add** command. ``--disk`` +*N*``:add,size=``**SIZE** can be used to add a disk at a specific index. +The ``--disk remove`` option will remove the last disk of the instance. +Use ``--disk `` *N*``:remove`` to remove a disk by its index. The +``--disk`` *N*``:mode=``*MODE* option will change the mode of the Nth +disk of the instance between read-only (``ro``) and read-write (``rw``). The ``--net add:``*options* and ``--net`` *N*``:add,``*options* option will add a new network interface to the instance. The available options @@ -1472,7 +1510,10 @@ GROW-DISK | {*instance*} {*disk*} {*amount*} Grows an instance's disk. This is only possible for instances having a -plain, drbd, file, sharedfile or rbd disk template. +plain, drbd, file, sharedfile, rbd or ext disk template. For the ext +template to work, the ExtStorage provider should also support growing. +This means having a ``grow`` script that actually grows the volume of +the external shared storage. Note that this command only change the block device size; it will not grow the actual filesystems, partitions, etc. that live on that @@ -1572,16 +1613,21 @@ FAILOVER Failover will stop the instance (if running), change its primary node, and if it was originally running it will start it again (on the new -primary). This only works for instances with drbd template (in which -case you can only fail to the secondary node) and for externally -mirrored templates (blockdev and rbd) (which can change to any other -node). - -If the instance's disk template is of type blockdev or rbd, then you -can explicitly specify the target node (which can be any node) using -the ``-n`` or ``--target-node`` option, or specify an iallocator plugin -using the ``-I`` or ``--iallocator`` option. If you omit both, the default -iallocator will be used to specify the target node. +primary). This works for instances with drbd template (in which case you +can only fail to the secondary node) and for externally mirrored +templates (sharedfile, blockdev, rbd and ext) (in which case you can +fail to any other node). + +If the instance's disk template is of type sharedfile, blockdev, rbd or +ext, then you can explicitly specify the target node (which can be any +node) using the ``-n`` or ``--target-node`` option, or specify an +iallocator plugin using the ``-I`` or ``--iallocator`` option. If you +omit both, the default iallocator will be used to specify the target +node. + +If the instance's disk template is of type drbd, the target node is +automatically selected as the drbd's secondary node. Changing the +secondary node is possible with a replace-disks operation. Normally the failover will check the consistency of the disks before failing over the instance. If you are trying to migrate instances off @@ -1606,6 +1652,10 @@ Example:: # gnt-instance failover instance1.example.com +For externally mirrored templates also ``-n`` is available:: + + # gnt-instance failover -n node3.example.com instance1.example.com + MIGRATE ^^^^^^^ @@ -1618,21 +1668,25 @@ MIGRATE | **migrate** [-f] \--cleanup [\--submit] {*instance*} Migrate will move the instance to its secondary node without shutdown. -As with failover, it only works for instances having the drbd disk -template or an externally mirrored disk template type such as blockdev -or rbd. - -If the instance's disk template is of type blockdev or rbd, then you can -explicitly specify the target node (which can be any node) using the -``-n`` or ``--target-node`` option, or specify an iallocator plugin -using the ``-I`` or ``--iallocator`` option. If you omit both, the -default iallocator will be used to specify the target node. -Alternatively, the default iallocator can be requested by specifying -``.`` as the name of the plugin. - -The migration command needs a perfectly healthy instance, as we rely -on the dual-master capability of drbd8 and the disks of the instance -are not allowed to be degraded. +As with failover, it works for instances having the drbd disk template +or an externally mirrored disk template type such as sharedfile, +blockdev, rbd or ext. + +If the instance's disk template is of type sharedfile, blockdev, rbd or +ext, then you can explicitly specify the target node (which can be any +node) using the ``-n`` or ``--target-node`` option, or specify an +iallocator plugin using the ``-I`` or ``--iallocator`` option. If you +omit both, the default iallocator will be used to specify the target +node. Alternatively, the default iallocator can be requested by +specifying ``.`` as the name of the plugin. + +If the instance's disk template is of type drbd, the target node is +automatically selected as the drbd's secondary node. Changing the +secondary node is possible with a replace-disks operation. + +The migration command needs a perfectly healthy instance for drbd +instances, as we rely on the dual-master capability of drbd8 and the +disks of the instance are not allowed to be degraded. The ``--non-live`` and ``--migration-mode=non-live`` options will switch (for the hypervisors that support it) between a "fully live" @@ -1647,7 +1701,7 @@ option is passed, depends on the hypervisor parameters (and can be viewed with the **gnt-cluster info** command). If the ``--cleanup`` option is passed, the operation changes from -migration to attempting recovery from a failed previous migration. In +migration to attempting recovery from a failed previous migration. In this mode, Ganeti checks if the instance runs on the correct node (and updates its configuration if not) and ensures the instances' disks are configured correctly. In this mode, the ``--non-live`` option is @@ -1704,7 +1758,7 @@ MOVE | [-n *node*] [\--shutdown-timeout=*N*] [\--submit] [\--ignore-ipolicy] | {*instance*} -Move will move the instance to an arbitrary node in the cluster. This +Move will move the instance to an arbitrary node in the cluster. This works only for instances having a plain or file disk template. Note that since this operation is done via data copy, it will take a diff --git a/test/py/ganeti.query_unittest.py b/test/py/ganeti.query_unittest.py index 024102aac3982bfe5e0682f65704599859c0983b..e1b8600f7e8c51e288b12cdc538d928e72811729 100755 --- a/test/py/ganeti.query_unittest.py +++ b/test/py/ganeti.query_unittest.py @@ -617,7 +617,7 @@ class TestInstanceQuery(unittest.TestCase): ] iqd = query.InstanceQueryData(instances, cluster, None, [], [], {}, - set(), {}, None, None) + set(), {}, None, None, None) self.assertEqual(q.Query(iqd), [[(constants.RS_NORMAL, "inst1"), (constants.RS_NORMAL, 128), @@ -646,7 +646,7 @@ class TestInstanceQuery(unittest.TestCase): q = self._Create(selected) self.assertEqual(q.RequestedData(), set([query.IQ_CONFIG, query.IQ_LIVE, query.IQ_DISKUSAGE, - query.IQ_CONSOLE, query.IQ_NODES])) + query.IQ_CONSOLE, query.IQ_NODES, query.IQ_NETWORKS])) cluster = objects.Cluster(cluster_name="testcluster", hvparams=constants.HVC_DEFAULTS, @@ -823,7 +823,7 @@ class TestInstanceQuery(unittest.TestCase): iqd = query.InstanceQueryData(instances, cluster, disk_usage, offline_nodes, bad_nodes, live_data, - wrongnode_inst, consinfo, {}, {}) + wrongnode_inst, consinfo, {}, {}, {}) result = q.Query(iqd) self.assertEqual(len(result), len(instances)) self.assert_(compat.all(len(row) == len(selected) diff --git a/tools/cfgupgrade b/tools/cfgupgrade index bccb34e4a80fefdab809f44a9cd174fd149413f2..4fa49d98b00c209a06c0138bb40317c5d2106b34 100755 --- a/tools/cfgupgrade +++ b/tools/cfgupgrade @@ -111,6 +111,20 @@ def UpgradeGroups(config_data): group["networks"] = {} +def UpgradeInstances(config_data): + network2uuid = dict((n["name"], n["uuid"]) + for n in config_data["networks"].values()) + for inst in config_data["instances"].values(): + for nic in inst["nics"]: + name = nic.get("network", None) + if name: + uuid = network2uuid.get(name, None) + if uuid: + print("NIC with network name %s found." + " Substituting with uuid %s." % (name, uuid)) + nic["network"] = uuid + + def main(): """Main program. @@ -294,6 +308,7 @@ def main(): UpgradeNetworks(config_data) UpgradeGroups(config_data) + UpgradeInstances(config_data) try: logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)