Add basic support for disk parameters
authorAndrea Spadaccini <spadaccio@google.com>
Mon, 21 Nov 2011 13:43:09 +0000 (13:43 +0000)
committerAndrea Spadaccini <spadaccio@google.com>
Thu, 1 Dec 2011 11:03:54 +0000 (11:03 +0000)
objects.py:
  * add disk parameters to Disk, Cluster, NodeGroup.

constants.py:
  * add dictionaries that will hold types and default values for disk
    parameters (for now, empty).

test/ganeti.constants_unittest.py:
  * add unit tests for consistency in disk parameters default values.

rest of files:
  * add to gnt-cluster and gnt-group the options to manipulate disk
    parameters.

Signed-off-by: Andrea Spadaccini <spadaccio@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

lib/bootstrap.py
lib/cli.py
lib/client/gnt_cluster.py
lib/client/gnt_group.py
lib/cmdlib.py
lib/constants.py
lib/masterd/instance.py
lib/objects.py
lib/opcodes.py
test/ganeti.constants_unittest.py

index bc4fd15..62d59ce 100644 (file)
@@ -286,11 +286,11 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
                 master_netmask, master_netdev, file_storage_dir,
                 shared_file_storage_dir, candidate_pool_size, secondary_ip=None,
                 vg_name=None, beparams=None, nicparams=None, ndparams=None,
-                hvparams=None, enabled_hypervisors=None, modify_etc_hosts=True,
-                modify_ssh_setup=True, maintain_node_health=False,
-                drbd_helper=None, uid_pool=None, default_iallocator=None,
-                primary_ip_version=None, prealloc_wipe_disks=False,
-                use_external_mip_script=False):
+                hvparams=None, diskparams=None, enabled_hypervisors=None,
+                modify_etc_hosts=True, modify_ssh_setup=True,
+                maintain_node_health=False, drbd_helper=None, uid_pool=None,
+                default_iallocator=None, primary_ip_version=None,
+                prealloc_wipe_disks=False, use_external_mip_script=False):
   """Initialise the cluster.
 
   @type candidate_pool_size: int
@@ -426,6 +426,17 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
     hv_class = hypervisor.GetHypervisor(hv_name)
     hv_class.CheckParameterSyntax(hv_params)
 
+  # diskparams is a mapping of disk-template->diskparams dict
+  for template, dt_params in diskparams.items():
+    param_keys = set(dt_params.keys())
+    default_param_keys = set(constants.DISK_DT_DEFAULTS[template].keys())
+    if not (param_keys <= default_param_keys):
+      unknown_params = param_keys - default_param_keys
+      raise errors.OpPrereqError("Invalid parameters for disk template %s:"
+                                 " %s" % (template,
+                                          utils.CommaJoin(unknown_params)))
+    utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
+
   # set up ssh config and /etc/hosts
   sshline = utils.ReadFile(constants.SSH_HOST_RSA_PUB)
   sshkey = sshline.split(" ")[1]
@@ -473,6 +484,7 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
     nicparams={constants.PP_DEFAULT: nicparams},
     ndparams=ndparams,
     hvparams=hvparams,
+    diskparams=diskparams,
     candidate_pool_size=candidate_pool_size,
     modify_etc_hosts=modify_etc_hosts,
     modify_ssh_setup=modify_ssh_setup,
@@ -542,6 +554,7 @@ def InitConfig(version, cluster_config, master_node_config,
     uuid=uuid_generator.Generate([], utils.NewUUID, _INITCONF_ECID),
     name=constants.INITIAL_NODE_GROUP_NAME,
     members=[master_node_config.name],
+    diskparams=cluster_config.diskparams,
     )
   nodegroups = {
     default_nodegroup.uuid: default_nodegroup,
index 63f2dfb..cde9991 100644 (file)
@@ -69,6 +69,7 @@ __all__ = [
   "DEBUG_SIMERR_OPT",
   "DISKIDX_OPT",
   "DISK_OPT",
+  "DISK_PARAMS_OPT",
   "DISK_TEMPLATE_OPT",
   "DRAINED_OPT",
   "DRY_RUN_OPT",
@@ -752,6 +753,11 @@ HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
                         default={}, dest="hvparams",
                         help="Hypervisor parameters")
 
+DISK_PARAMS_OPT = cli_option("-D", "--disk-parameters", dest="diskparams",
+                             help="Disk template parameters, in the format"
+                             " template:option=value,option=value,...",
+                             type="identkeyval", action="append", default=[])
+
 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
                             help="Hypervisor and hypervisor options, in the"
                             " format hypervisor:option=value,option=value,...",
index 0410c54..15a99c8 100644 (file)
@@ -98,6 +98,16 @@ def InitCluster(opts, args):
   beparams = opts.beparams
   nicparams = opts.nicparams
 
+  diskparams = dict(opts.diskparams)
+
+  # check the disk template types here, as we cannot rely on the type check done
+  # by the opcode parameter types
+  diskparams_keys = set(diskparams.keys())
+  if not (diskparams_keys <= constants.DISK_TEMPLATES):
+    unknown = utils.NiceSort(diskparams_keys - constants.DISK_TEMPLATES)
+    ToStderr("Disk templates unknown: %s" % utils.CommaJoin(unknown))
+    return 1
+
   # prepare beparams dict
   beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
   utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
@@ -120,6 +130,14 @@ def InitCluster(opts, args):
     hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
     utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
 
+  # prepare diskparams dict
+  for templ in constants.DISK_TEMPLATES:
+    if templ not in diskparams:
+      diskparams[templ] = {}
+    diskparams[templ] = objects.FillDict(constants.DISK_DT_DEFAULTS[templ],
+                                         diskparams[templ])
+    utils.ForceDictType(diskparams[templ], constants.DISK_DT_TYPES)
+
   if opts.candidate_pool_size is None:
     opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
 
@@ -164,6 +182,7 @@ def InitCluster(opts, args):
                         beparams=beparams,
                         nicparams=nicparams,
                         ndparams=ndparams,
+                        diskparams=diskparams,
                         candidate_pool_size=opts.candidate_pool_size,
                         modify_etc_hosts=opts.modify_etc_hosts,
                         modify_ssh_setup=opts.modify_ssh_setup,
@@ -876,7 +895,8 @@ def SetClusterParams(opts, args):
   if not (not opts.lvm_storage or opts.vg_name or
           not opts.drbd_storage or opts.drbd_helper or
           opts.enabled_hypervisors or opts.hvparams or
-          opts.beparams or opts.nicparams or opts.ndparams or
+          opts.beparams or opts.nicparams or
+          opts.ndparams or opts.diskparams or
           opts.candidate_pool_size is not None or
           opts.uid_pool is not None or
           opts.maintain_node_health is not None or
@@ -916,6 +936,11 @@ def SetClusterParams(opts, args):
   for hv_params in hvparams.values():
     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
 
+  diskparams = dict(opts.diskparams)
+
+  for dt_params in hvparams.values():
+    utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
+
   beparams = opts.beparams
   utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
 
@@ -963,6 +988,7 @@ def SetClusterParams(opts, args):
                                   beparams=beparams,
                                   nicparams=nicparams,
                                   ndparams=ndparams,
+                                  diskparams=diskparams,
                                   candidate_pool_size=opts.candidate_pool_size,
                                   maintain_node_health=mnh,
                                   uid_pool=uid_pool,
@@ -1376,7 +1402,8 @@ commands = {
      NOMODIFY_SSH_SETUP_OPT, SECONDARY_IP_OPT, VG_NAME_OPT,
      MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
      DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT,
-     NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT, USE_EXTERNAL_MIP_SCRIPT],
+     NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT, USE_EXTERNAL_MIP_SCRIPT,
+     DISK_PARAMS_OPT],
     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
   "destroy": (
     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
@@ -1453,7 +1480,7 @@ commands = {
      MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT,
      DRBD_HELPER_OPT, NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT,
      RESERVED_LVS_OPT, DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT,
-     NODE_PARAMS_OPT, USE_EXTERNAL_MIP_SCRIPT],
+     NODE_PARAMS_OPT, USE_EXTERNAL_MIP_SCRIPT, DISK_PARAMS_OPT],
     "[opts...]",
     "Alters the parameters of the cluster"),
   "renew-crypto": (
index 0365e31..9054b75 100644 (file)
@@ -48,8 +48,10 @@ def AddGroup(opts, args):
 
   """
   (group_name,) = args
+  diskparams = dict(opts.diskparams)
   op = opcodes.OpGroupAdd(group_name=group_name, ndparams=opts.ndparams,
-                          alloc_policy=opts.alloc_policy)
+                          alloc_policy=opts.alloc_policy,
+                          diskparams=diskparams)
   SubmitOpCode(op, opts=opts)
 
 
@@ -133,13 +135,16 @@ def SetGroupParams(opts, args):
   @return: the desired exit code
 
   """
-  if opts.ndparams is None and opts.alloc_policy is None:
+  if (opts.ndparams is None and opts.alloc_policy is None
+      and not opts.diskparams):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
+  diskparams = dict(opts.diskparams)
   op = opcodes.OpGroupSetParams(group_name=args[0],
                                 ndparams=opts.ndparams,
-                                alloc_policy=opts.alloc_policy)
+                                alloc_policy=opts.alloc_policy,
+                                diskparams=diskparams)
   result = SubmitOrSend(op, opts)
 
   if result:
@@ -214,7 +219,8 @@ def EvacuateGroup(opts, args):
 
 commands = {
   "add": (
-    AddGroup, ARGS_ONE_GROUP, [DRY_RUN_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT],
+    AddGroup, ARGS_ONE_GROUP,
+    [DRY_RUN_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT, DISK_PARAMS_OPT],
     "<group_name>", "Add a new node group to the cluster"),
   "assign-nodes": (
     AssignNodes, ARGS_ONE_GROUP + ARGS_MANY_NODES, [DRY_RUN_OPT, FORCE_OPT],
@@ -231,7 +237,8 @@ commands = {
     "Lists all available fields for node groups"),
   "modify": (
     SetGroupParams, ARGS_ONE_GROUP,
-    [DRY_RUN_OPT, SUBMIT_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT],
+    [DRY_RUN_OPT, SUBMIT_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT,
+     DISK_PARAMS_OPT],
     "<group_name>", "Alters the parameters of a node group"),
   "remove": (
     RemoveGroup, ARGS_ONE_GROUP, [DRY_RUN_OPT],
index 6cd361f..5f257d3 100644 (file)
@@ -3496,6 +3496,10 @@ class LUClusterSetParams(LogicalUnit):
     if self.op.master_netmask is not None:
       _ValidateNetmask(self.cfg, self.op.master_netmask)
 
+    if self.op.diskparams:
+      for dt_params in self.op.diskparams.values():
+        utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
+
   def ExpandNames(self):
     # FIXME: in the future maybe other cluster params won't require checking on
     # all nodes to be modified.
@@ -3628,6 +3632,15 @@ class LUClusterSetParams(LogicalUnit):
         else:
           self.new_hvparams[hv_name].update(hv_dict)
 
+    # disk template parameters
+    self.new_diskparams = objects.FillDict(cluster.diskparams, {})
+    if self.op.diskparams:
+      for dt_name, dt_params in self.op.diskparams.items():
+        if dt_name not in self.op.diskparams:
+          self.new_diskparams[dt_name] = dt_params
+        else:
+          self.new_diskparams[dt_name].update(dt_params)
+
     # os hypervisor parameters
     self.new_os_hvp = objects.FillDict(cluster.os_hvp, {})
     if self.op.os_hvp:
@@ -3746,6 +3759,8 @@ class LUClusterSetParams(LogicalUnit):
       self.cluster.osparams = self.new_osp
     if self.op.ndparams:
       self.cluster.ndparams = self.new_ndparams
+    if self.op.diskparams:
+      self.cluster.diskparams = self.new_diskparams
 
     if self.op.candidate_pool_size is not None:
       self.cluster.candidate_pool_size = self.op.candidate_pool_size
@@ -12463,6 +12478,14 @@ class LUGroupAdd(LogicalUnit):
     if self.op.ndparams:
       utils.ForceDictType(self.op.ndparams, constants.NDS_PARAMETER_TYPES)
 
+    if self.op.diskparams:
+      for templ in constants.DISK_TEMPLATES:
+        if templ not in self.op.diskparams:
+          self.op.diskparams[templ] = {}
+        utils.ForceDictType(self.op.diskparams[templ], constants.DISK_DT_TYPES)
+    else:
+      self.op.diskparams = self.cfg.GetClusterInfo().diskparams
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -12485,7 +12508,8 @@ class LUGroupAdd(LogicalUnit):
     group_obj = objects.NodeGroup(name=self.op.group_name, members=[],
                                   uuid=self.group_uuid,
                                   alloc_policy=self.op.alloc_policy,
-                                  ndparams=self.op.ndparams)
+                                  ndparams=self.op.ndparams,
+                                  diskparams=self.op.diskparams)
 
     self.cfg.AddNodeGroup(group_obj, self.proc.GetECId(), check_uuid=False)
     del self.remove_locks[locking.LEVEL_NODEGROUP]
@@ -12732,6 +12756,7 @@ class LUGroupSetParams(LogicalUnit):
   def CheckArguments(self):
     all_changes = [
       self.op.ndparams,
+      self.op.diskparams,
       self.op.alloc_policy,
       ]
 
@@ -12762,6 +12787,16 @@ class LUGroupSetParams(LogicalUnit):
       utils.ForceDictType(self.op.ndparams, constants.NDS_PARAMETER_TYPES)
       self.new_ndparams = new_ndparams
 
+    if self.op.diskparams:
+      self.new_diskparams = dict()
+      for templ in constants.DISK_TEMPLATES:
+        if templ not in self.op.diskparams:
+          self.op.diskparams[templ] = {}
+        new_templ_params = _GetUpdatedParams(self.group.diskparams[templ],
+                                             self.op.diskparams[templ])
+        utils.ForceDictType(new_templ_params, constants.DISK_DT_TYPES)
+        self.new_diskparams[templ] = new_templ_params
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -12788,6 +12823,10 @@ class LUGroupSetParams(LogicalUnit):
       self.group.ndparams = self.new_ndparams
       result.append(("ndparams", str(self.group.ndparams)))
 
+    if self.op.diskparams:
+      self.group.diskparams = self.new_diskparams
+      result.append(("diskparams", str(self.group.diskparams)))
+
     if self.op.alloc_policy:
       self.group.alloc_policy = self.op.alloc_policy
 
index 47d2a0d..827db0a 100644 (file)
@@ -460,6 +460,13 @@ LD_LV = "lvm"
 LD_DRBD8 = "drbd8"
 LD_FILE = "file"
 LD_BLOCKDEV = "blockdev"
+LOGICAL_DISK_TYPES = frozenset([
+  LD_LV,
+  LD_DRBD8,
+  LD_FILE,
+  LD_BLOCKDEV,
+  ])
+
 LDS_BLOCK = frozenset([LD_LV, LD_DRBD8, LD_BLOCKDEV])
 
 # drbd constants
@@ -889,6 +896,17 @@ NDS_PARAMETER_TYPES = {
 
 NDS_PARAMETERS = frozenset(NDS_PARAMETER_TYPES.keys())
 
+# Logical Disks parameters
+DISK_LD_TYPES = {
+  }
+DISK_LD_PARAMETERS = frozenset(DISK_LD_TYPES.keys())
+
+# Disk template parameters
+DISK_DT_TYPES = {
+  }
+
+DISK_DT_PARAMETERS = frozenset(DISK_DT_TYPES.keys())
+
 # OOB supported commands
 OOB_POWER_ON = "power-on"
 OOB_POWER_OFF = "power-off"
@@ -1651,6 +1669,32 @@ NDC_DEFAULTS = {
   ND_OOB_PROGRAM: None,
   }
 
+DISK_LD_DEFAULTS = {
+  LD_DRBD8: {
+    },
+  LD_LV: {
+    },
+  LD_FILE: {
+    },
+  LD_BLOCKDEV: {
+    },
+  }
+
+DISK_DT_DEFAULTS = {
+  DT_PLAIN: {
+    },
+  DT_DRBD8: {
+    },
+  DT_DISKLESS: {
+    },
+  DT_FILE: {
+    },
+  DT_SHARED_FILE: {
+    },
+  DT_BLOCK: {
+    },
+  }
+
 NICC_DEFAULTS = {
   NIC_MODE: NIC_MODE_BRIDGED,
   NIC_LINK: DEFAULT_BRIDGE,
index 8211f31..32f6497 100644 (file)
@@ -1176,9 +1176,11 @@ class ExportInstanceHelper:
                             " result '%s'", idx, src_node, result.payload)
       else:
         disk_id = tuple(result.payload)
+        disk_params = constants.DISK_LD_DEFAULTS[constants.LD_LV].copy()
         new_dev = objects.Disk(dev_type=constants.LD_LV, size=disk.size,
                                logical_id=disk_id, physical_id=disk_id,
-                               iv_name=disk.iv_name)
+                               iv_name=disk.iv_name,
+                               params=disk_params)
 
       self._snap_disks.append(new_dev)
 
index 0a15e98..d8c6e07 100644 (file)
@@ -110,6 +110,32 @@ def UpgradeBeParams(target):
     del target[constants.BE_MEMORY]
 
 
+def UpgradeDiskParams(diskparams):
+  """Upgrade the disk parameters.
+
+  @type diskparams: dict
+  @param diskparams: disk parameters to upgrade
+  @rtype: dict
+  @return: the upgraded disk parameters dit
+
+  """
+  result = dict()
+  if diskparams is None:
+    result = constants.DISK_DT_DEFAULTS.copy()
+  else:
+    # Update the disk parameter values for each disk template.
+    # The code iterates over constants.DISK_TEMPLATES because new templates
+    # might have been added.
+    for template in constants.DISK_TEMPLATES:
+      if template not in diskparams:
+        result[template] = constants.DISK_DT_DEFAULTS[template].copy()
+      else:
+        result[template] = FillDict(constants.DISK_DT_DEFAULTS[template],
+                                    diskparams[template])
+
+  return result
+
+
 class ConfigObject(object):
   """A generic config object.
 
@@ -451,7 +477,7 @@ class NIC(ConfigObject):
 class Disk(ConfigObject):
   """Config object representing a block device."""
   __slots__ = ["dev_type", "logical_id", "physical_id",
-               "children", "iv_name", "size", "mode"]
+               "children", "iv_name", "size", "mode", "params"]
 
   def CreateOnSecondary(self):
     """Test if this device needs to be created on a secondary node."""
@@ -745,6 +771,12 @@ class Disk(ConfigObject):
     if self.children:
       for child in self.children:
         child.UpgradeConfig()
+
+    if not self.params:
+      self.params = constants.DISK_LD_DEFAULTS[self.dev_type].copy()
+    else:
+      self.params = FillDict(constants.DISK_LD_DEFAULTS[self.dev_type],
+                             self.params)
     # add here config upgrade for this disk
 
 
@@ -1111,6 +1143,7 @@ class NodeGroup(TaggableObject):
     "name",
     "members",
     "ndparams",
+    "diskparams",
     "serial_no",
     "alloc_policy",
     ] + _TIMESTAMPS + _UUID
@@ -1155,6 +1188,8 @@ class NodeGroup(TaggableObject):
     if self.mtime is None:
       self.mtime = time.time()
 
+    self.diskparams = UpgradeDiskParams(self.diskparams)
+
   def FillND(self, node):
     """Return filled out ndparams for L{objects.Node}
 
@@ -1206,6 +1241,7 @@ class Cluster(TaggableObject):
     "osparams",
     "nicparams",
     "ndparams",
+    "diskparams",
     "candidate_pool_size",
     "modify_etc_hosts",
     "modify_ssh_setup",
@@ -1312,6 +1348,8 @@ class Cluster(TaggableObject):
     if self.use_external_mip_script is None:
       self.use_external_mip_script = False
 
+    self.diskparams = UpgradeDiskParams(self.diskparams)
+
   def ToDict(self):
     """Custom function for cluster.
 
index 64850ec..73c9564 100644 (file)
@@ -141,6 +141,13 @@ _PIgnoreErrors = ("ignore_errors", ht.EmptyList,
                   ht.TListOf(ht.TElemOf(constants.CV_ALL_ECODES_STRINGS)),
                   "List of error codes that should be treated as warnings")
 
+# Disk parameters
+_PDiskParams = ("diskparams", None,
+                ht.TOr(
+                  ht.TDictOf(ht.TElemOf(constants.DISK_TEMPLATES), ht.TDict),
+                  ht.TNone),
+                "Disk templates' parameter defaults")
+
 #: OP_ID conversion regular expression
 _OPID_RE = re.compile("([a-z])([A-Z])")
 
@@ -762,6 +769,7 @@ class OpClusterSetParams(OpCode):
     ("osparams", None, ht.TOr(ht.TDictOf(ht.TNonEmptyString, ht.TDict),
                               ht.TNone),
      "Cluster-wide OS parameter defaults"),
+    _PDiskParams,
     ("candidate_pool_size", None, ht.TOr(ht.TStrictPositiveInt, ht.TNone),
      "Master candidate pool size"),
     ("uid_pool", None, ht.NoType,
@@ -1394,6 +1402,7 @@ class OpGroupAdd(OpCode):
     _PGroupName,
     _PNodeGroupAllocPolicy,
     _PGroupNodeParams,
+    _PDiskParams,
     ]
 
 
@@ -1424,6 +1433,7 @@ class OpGroupSetParams(OpCode):
     _PGroupName,
     _PNodeGroupAllocPolicy,
     _PGroupNodeParams,
+    _PDiskParams,
     ]
   OP_RESULT = _TSetParamsResult
 
index acde75b..0b7736c 100755 (executable)
@@ -79,6 +79,12 @@ class TestConstants(unittest.TestCase):
     self.failUnless(constants.OP_PRIO_NORMAL > constants.OP_PRIO_HIGH)
     self.failUnless(constants.OP_PRIO_HIGH > constants.OP_PRIO_HIGHEST)
 
+  def testDiskDefaults(self):
+    self.failUnless(set(constants.DISK_LD_DEFAULTS.keys()) ==
+                    constants.LOGICAL_DISK_TYPES)
+    self.failUnless(set(constants.DISK_DT_DEFAULTS.keys()) ==
+                    constants.DISK_TEMPLATES)
+
 
 class TestExportedNames(unittest.TestCase):
   _VALID_NAME_RE = re.compile(r"^[A-Z][A-Z0-9_]+$")