text: #
  #
  
  # Copyright (C) 2010, 2011, 2012 Google Inc.
  # All rights reserved.
  #
  # Redistribution and use in source and binary forms, with or without
  # modification, are permitted provided that the following conditions are
  # met:
  #
  # 1. Redistributions of source code must retain the above copyright notice,
  # this list of conditions and the following disclaimer.
  #
  # 2. Redistributions in binary form must reproduce the above copyright
  # notice, this list of conditions and the following disclaimer in the
  # documentation and/or other materials provided with the distribution.
  #
  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
  
  """QA tests for node groups.
  
  """
  
  from ganeti import constants
  from ganeti import netutils
  from ganeti import query
  from ganeti import utils
  
  import qa_iptables
  import qa_config
  import qa_utils
  
  from qa_utils import AssertCommand, AssertEqual, GetCommandOutput
  
  
  def GetDefaultGroup():
    """Returns the default node group.
  
    """
    groups = qa_config.get("groups", {})
    return groups.get("group-with-nodes", constants.INITIAL_NODE_GROUP_NAME)
  
  
  def ConfigureGroups():
    """Configures groups and nodes for tests such as custom SSH ports.
  
    """
  
    defgroup = GetDefaultGroup()
    nodes = qa_config.get("nodes")
    options = qa_config.get("options", {})
  
    # Clear any old configuration
    qa_iptables.CleanRules(nodes)
  
    # Custom SSH ports:
    ssh_port = options.get("ssh-port")
    default_ssh_port = netutils.GetDaemonPort(constants.SSH)
    if (ssh_port is not None) and (ssh_port != default_ssh_port):
      ModifyGroupSshPort(qa_iptables.GLOBAL_RULES, defgroup, nodes, ssh_port)
  
  
  def ModifyGroupSshPort(ipt_rules, group, nodes, ssh_port):
    """Modifies the node group settings and sets up iptable rules.
  
    For each pair of nodes add two rules that affect SSH connections from one
    to the other one.
    The first one redirects port 22 to some unused port so that connecting
    through 22 fails. The second redirects port `ssh_port` to port 22.
    Together this results in master seeing the SSH daemons on the nodes on
    `ssh_port` instead of 22.
    """
    default_ssh_port = netutils.GetDaemonPort(constants.SSH)
    all_nodes = qa_config.get("nodes")
    AssertCommand(["gnt-group", "modify",
                   "--node-parameters=ssh_port=" + str(ssh_port),
                   group])
    for node in nodes:
      ipt_rules.RedirectPort(node.primary, "localhost",
                             default_ssh_port, 65535)
      ipt_rules.RedirectPort(node.primary, "localhost",
                             ssh_port, default_ssh_port)
      for node2 in all_nodes:
        ipt_rules.RedirectPort(node2.primary, node.primary,
                               default_ssh_port, 65535)
        ipt_rules.RedirectPort(node2.primary, node.primary,
                               ssh_port, default_ssh_port)
  
  
  def TestGroupAddRemoveRename():
    """gnt-group add/remove/rename"""
    existing_group_with_nodes = GetDefaultGroup()
  
    (group1, group2, group3) = qa_utils.GetNonexistentGroups(3)
  
    AssertCommand(["gnt-group", "add", group1])
    AssertCommand(["gnt-group", "add", group2])
    AssertCommand(["gnt-group", "add", group2], fail=True)
    AssertCommand(["gnt-group", "add", existing_group_with_nodes], fail=True)
  
    AssertCommand(["gnt-group", "rename", group1, group2], fail=True)
    AssertCommand(["gnt-group", "rename", group1, group3])
  
    try:
      AssertCommand(["gnt-group", "rename", existing_group_with_nodes, group1])
  
      AssertCommand(["gnt-group", "remove", group2])
      AssertCommand(["gnt-group", "remove", group3])
      AssertCommand(["gnt-group", "remove", group1], fail=True)
    finally:
      # Try to ensure idempotency re groups that already existed.
      AssertCommand(["gnt-group", "rename", group1, existing_group_with_nodes])
  
  
  def TestGroupAddWithOptions():
    """gnt-group add with options"""
    (group1, ) = qa_utils.GetNonexistentGroups(1)
  
    AssertCommand(["gnt-group", "add", "--alloc-policy", "notvalid", group1],
                  fail=True)
  
    AssertCommand(["gnt-group", "add", "--alloc-policy", "last_resort",
                   "--node-parameters", "oob_program=/bin/true", group1])
  
    AssertCommand(["gnt-group", "remove", group1])
  
  
  class NewGroupCtx(object):
    """Creates a new group and disposes afterwards."""
  
    def __enter__(self):
      (self._group, ) = qa_utils.GetNonexistentGroups(1)
      AssertCommand(["gnt-group", "add", self._group])
      return self._group
  
    def __exit__(self, exc_type, exc_val, exc_tb):
      AssertCommand(["gnt-group", "remove", self._group])
  
  
  def _GetGroupIPolicy(groupname):
    """Return the run-time values of the cluster-level instance policy.
  
    @type groupname: string
    @param groupname: node group name
    @rtype: tuple
    @return: (policy, specs), where:
        - policy is a dictionary of the policy values, instance specs excluded
        - specs is a dictionary containing only the specs, using the internal
          format (see L{constants.IPOLICY_DEFAULTS} for an example), but without
          the standard values
  
    """
    info = qa_utils.GetObjectInfo(["gnt-group", "info", groupname])
    assert len(info) == 1
    policy = info[0]["Instance policy"]
  
    (ret_policy, ret_specs) = qa_utils.ParseIPolicy(policy)
  
    # Sanity checks
    assert "minmax" in ret_specs
    assert len(ret_specs["minmax"]) > 0
    assert len(ret_policy) > 0
    return (ret_policy, ret_specs)
  
  
  def _TestGroupSetISpecs(groupname, new_specs=None, diff_specs=None,
                          fail=False, old_values=None):
    """Change instance specs on a group.
  
    At most one of new_specs or diff_specs can be specified.
  
    @type groupname: string
    @param groupname: group name
    @type new_specs: dict
    @param new_specs: new complete specs, in the same format returned by
        L{_GetGroupIPolicy}
    @type diff_specs: dict
    @param diff_specs: partial specs, it can be an incomplete specifications, but
        if min/max specs are specified, their number must match the number of the
        existing specs
    @type fail: bool
    @param fail: if the change is expected to fail
    @type old_values: tuple
    @param old_values: (old_policy, old_specs), as returned by
        L{_GetGroupIPolicy}
    @return: same as L{_GetGroupIPolicy}
  
    """
    build_cmd = lambda opts: ["gnt-group", "modify"] + opts + [groupname]
    get_policy = lambda: _GetGroupIPolicy(groupname)
    return qa_utils.TestSetISpecs(
      new_specs=new_specs, diff_specs=diff_specs,
      get_policy_fn=get_policy, build_cmd_fn=build_cmd,
      fail=fail, old_values=old_values)
  
  
  def _TestGroupModifyISpecs(groupname):
    # This test is built on the assumption that the default ipolicy holds for
    # the node group under test
    old_values = _GetGroupIPolicy(groupname)
    samevals = dict((p, 4) for p in constants.ISPECS_PARAMETERS)
    base_specs = {
      constants.ISPECS_MINMAX: [{
        constants.ISPECS_MIN: samevals,
        constants.ISPECS_MAX: samevals,
        }],
      }
    mod_values = _TestGroupSetISpecs(groupname, new_specs=base_specs,
                                     old_values=old_values)
    for par in constants.ISPECS_PARAMETERS:
      # First make sure that the test works with good values
      good_specs = {
        constants.ISPECS_MINMAX: [{
          constants.ISPECS_MIN: {par: 8},
          constants.ISPECS_MAX: {par: 8},
          }],
        }
      mod_values = _TestGroupSetISpecs(groupname, diff_specs=good_specs,
                                       old_values=mod_values)
      bad_specs = {
        constants.ISPECS_MINMAX: [{
          constants.ISPECS_MIN: {par: 8},
          constants.ISPECS_MAX: {par: 4},
          }],
        }
      _TestGroupSetISpecs(groupname, diff_specs=bad_specs, fail=True,
                          old_values=mod_values)
    AssertCommand(["gnt-group", "modify", "--ipolicy-bounds-specs", "default",
                   groupname])
    AssertEqual(_GetGroupIPolicy(groupname), old_values)
  
    # Get the ipolicy command (from the cluster config)
    mnode = qa_config.GetMasterNode()
    addcmd = GetCommandOutput(mnode.primary, utils.ShellQuoteArgs([
      "gnt-group", "show-ispecs-cmd", "--include-defaults", groupname,
      ]))
    modcmd = ["gnt-group", "modify"]
    opts = addcmd.split()
    assert opts[0:2] == ["gnt-group", "add"]
    for k in range(2, len(opts) - 1):
      if opts[k].startswith("--ipolicy-"):
        assert k + 2 <= len(opts)
        modcmd.extend(opts[k:k + 2])
    modcmd.append(groupname)
    # Apply the ipolicy to the group and verify the result
    AssertCommand(modcmd)
    new_addcmd = GetCommandOutput(mnode.primary, utils.ShellQuoteArgs([
      "gnt-group", "show-ispecs-cmd", groupname,
      ]))
    AssertEqual(addcmd, new_addcmd)
  
  
  def _TestGroupModifyIPolicy(groupname):
    _TestGroupModifyISpecs(groupname)
  
    # We assume that the default ipolicy holds
    (old_policy, old_specs) = _GetGroupIPolicy(groupname)
    for (par, setval, iname, expval) in [
      ("vcpu-ratio", 1.5, None, 1.5),
      ("spindle-ratio", 1.5, None, 1.5),
      ("disk-templates", constants.DT_PLAIN,
       "allowed disk templates", constants.DT_PLAIN)
      ]:
      if not iname:
        iname = par
      build_cmdline = lambda val: ["gnt-group", "modify", "--ipolicy-" + par,
                                   str(val), groupname]
  
      AssertCommand(build_cmdline(setval))
      (new_policy, new_specs) = _GetGroupIPolicy(groupname)
      AssertEqual(new_specs, old_specs)
      for (p, val) in new_policy.items():
        if p == iname:
          AssertEqual(val, expval)
        else:
          AssertEqual(val, old_policy[p])
  
      AssertCommand(build_cmdline("default"))
      (new_policy, new_specs) = _GetGroupIPolicy(groupname)
      AssertEqual(new_specs, old_specs)
      AssertEqual(new_policy, old_policy)
  
  
  def TestGroupModify():
    """gnt-group modify"""
    # This tests assumes LVM to be enabled, thus it should skip if
    # this is not the case
    if not qa_config.IsStorageTypeSupported(constants.ST_LVM_VG):
      return
    (group1, ) = qa_utils.GetNonexistentGroups(1)
  
    AssertCommand(["gnt-group", "add", group1])
  
    try:
      _TestGroupModifyIPolicy(group1)
      AssertCommand(["gnt-group", "modify", "--alloc-policy", "unallocable",
                     "--node-parameters", "oob_program=/bin/false", group1])
      AssertCommand(["gnt-group", "modify",
                     "--alloc-policy", "notvalid", group1], fail=True)
      AssertCommand(["gnt-group", "modify",
                     "--node-parameters", "spindle_count=10", group1])
      if qa_config.TestEnabled("htools"):
        AssertCommand(["hbal", "-L", "-G", group1])
      AssertCommand(["gnt-group", "modify",
                     "--node-parameters", "spindle_count=default", group1])
    finally:
      AssertCommand(["gnt-group", "remove", group1])
  
  
  def TestGroupList():
    """gnt-group list"""
    qa_utils.GenericQueryTest("gnt-group", query.GROUP_FIELDS.keys())
  
  
  def TestGroupListFields():
    """gnt-group list-fields"""
    qa_utils.GenericQueryFieldsTest("gnt-group", query.GROUP_FIELDS.keys())
  
  
  def TestAssignNodesIncludingSplit(orig_group, node1, node2):
    """gnt-group assign-nodes --force
  
    Expects node1 and node2 to be primary and secondary for a common instance.
  
    """
    assert node1 != node2
  
    (other_group, ) = qa_utils.GetNonexistentGroups(1)
  
    master_node = qa_config.GetMasterNode().primary
  
    def AssertInGroup(group, nodes):
      real_output = GetCommandOutput(master_node,
                                     "gnt-node list --no-headers -o group " +
                                     utils.ShellQuoteArgs(nodes))
      AssertEqual(real_output.splitlines(), [group] * len(nodes))
  
    AssertInGroup(orig_group, [node1, node2])
    AssertCommand(["gnt-group", "add", other_group])
  
    try:
      AssertCommand(["gnt-group", "assign-nodes", other_group, node1, node2])
      AssertInGroup(other_group, [node1, node2])
  
      # This should fail because moving node1 to orig_group would leave their
      # common instance split between orig_group and other_group.
      AssertCommand(["gnt-group", "assign-nodes", orig_group, node1], fail=True)
      AssertInGroup(other_group, [node1, node2])
  
      AssertCommand(["gnt-group", "assign-nodes", "--force", orig_group, node1])
      AssertInGroup(orig_group, [node1])
      AssertInGroup(other_group, [node2])
  
      AssertCommand(["gnt-group", "assign-nodes", orig_group, node2])
      AssertInGroup(orig_group, [node1, node2])
    finally:
      AssertCommand(["gnt-group", "remove", other_group])
  