[autotest] Add a provision control segment to autoserv.

This adds a control segment to autoserv that understands how to look up
what test is associated with a certain label that needs to be changed,
and that invokes the test.  Note that support for autoserv actually
calling this control segment has not been added yet.

The design of this is such that the status log of a provision test will
look like
START   ----    provision
  INFO    ----    provision Can't provision label 'x'. Skipping.
  START    provision_AutoUpdate
    TEST_NA provision_AutoUpdate Host already running lumpy-release/R28-3993.0.0
  END TEST_NA     provision_AutoUpdate
END GOOD   ----    provision  hostname provisioned successfully

So that it's quite clear when looking at the status.log what provision
test were invoked, and what the result of each one was.

There's also some work snuck in here that moves some code out of
provision_AutoUpdate that will be useful to other provisioning tests,
and centralizes where to hold constants.

BUG=chromium:249437
TEST=Ran the control segment manually via
|./server/autoserv server/control_segments/provision| and passed
"cros-version:R28-3993.0.0,unknown_arg" via --args, and confirmed
The provision_AutoUpdate test was invoked and "unknown_arg" was logged
as an unprovisionable label.

Change-Id: I1f6c301d837b9553863389409475a76ff67e0828
Reviewed-on: https://gerrit.chromium.org/gerrit/58383
Commit-Queue: Alex Miller <milleral@chromium.org>
Reviewed-by: Alex Miller <milleral@chromium.org>
Tested-by: Alex Miller <milleral@chromium.org>
diff --git a/server/cros/provision.py b/server/cros/provision.py
new file mode 100644
index 0000000..0b05d38
--- /dev/null
+++ b/server/cros/provision.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+import logging
+
+import common
+from autotest_lib.frontend.afe.json_rpc import proxy
+from autotest_lib.server import frontend
+
+
+### Constants for label prefixes
+CROS_VERSION_PREFIX = 'cros-version'
+
+
+### Helpers to convert value to label
+def cros_version_to_label(image):
+    """
+    Returns the proper label name for a ChromeOS build of |image|.
+
+    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
+    @returns: A string that is the appropriate label name.
+
+    """
+    return CROS_VERSION_PREFIX + ':' + image
+
+
+# TODO(milleral): http://crbug.com/249555
+# Create some way to discover and register provisioning tests so that we don't
+# need to hand-maintain a list of all of them.
+_provision_types = {
+    CROS_VERSION_PREFIX:'provision_AutoUpdate',
+}
+
+
+def can_provision(label):
+    """
+    Returns True if the label is a label that we recognize as something we
+    know how to provision.
+
+    @param label: The label as a string.
+    @returns: True if there exists a test to provision the label.
+
+    """
+    return label.split(':')[0] in _provision_types
+
+
+def provisioner_for(name):
+    """
+    Returns the provisioning class associated with the given (string) name.
+
+    @param name: The name of the provision type being requested.
+    @returns: The subclass of Provisioner that was requested.
+    @raises KeyError: If the name was not recognized as a provision type.
+
+    """
+    return _provision_types[name]
+
+
+def filter_labels(labels):
+    """
+    Filter a list of labels into two sets: those labels that we know how to
+    change and those that we don't.  For the ones we know how to change, split
+    them apart into the name of configuration type and its value.
+
+    @param labels: A list of strings of labels.
+    @returns: A tuple where the first element is a set of unprovisionable
+              labels, and the second element is a set of the provisionable
+              labels.
+
+    >>> filter_labels(['bluetooth', 'cros-version:lumpy-release/R28-3993.0.0'])
+    (set(['bluetooth']), set(['cros-version:lumpy-release/R28-3993.0.0']))
+
+    """
+    capabilities = set()
+    configurations = set()
+
+    for label in labels:
+        if can_provision(label):
+            configurations.add(label)
+        else:
+            capabilities.add(label)
+
+    return capabilities, configurations
+
+
+def split_labels(labels):
+    """
+    Split a list of labels into a dict mapping name to value.  All labels must
+    be provisionable labels, or else a ValueError
+
+    @param labels: list of strings of label names
+    @returns: A dict of where the key is the configuration name, and the value
+              is the configuration value.
+    @raises: ValueError if a label is not a provisionable label.
+
+    >>> split_labels(['cros-version:lumpy-release/R28-3993.0.0'])
+    {'cros-version': 'lumpy-release/R28-3993.0.0'}
+    >>> split_labels(['bluetooth'])
+    Traceback (most recent call last):
+    ...
+    ValueError: Unprovisionable label bluetooth
+
+    """
+    configurations = dict()
+
+    for label in labels:
+        if can_provision(label):
+            name, value = label.split(':', 1)
+            configurations[name] = value
+        else:
+            raise ValueError('Unprovisionable label %s' % label)
+
+    return configurations
+
+
+# This has been copied out of dynamic_suite's reimager.py, which will be killed
+# off in a future CL.  I'd prefer if this would go away by doing
+# http://crbug.com/249424, so that labels are just automatically made when we
+# try to add them to a host.
+def ensure_label_exists(name):
+    """
+    Ensure that a label called |name| exists in the autotest DB.
+
+    @param name: the label to check for/create.
+    @raises ValidationError: There was an error in the response that was
+                             not because the label already existed.
+
+    """
+    afe = frontend.AFE()
+    try:
+        afe.create_label(name=name)
+    except proxy.ValidationError as ve:
+        if ('name' in ve.problem_keys and
+            'This value must be unique' in ve.problem_keys['name']):
+            logging.debug('Version label %s already exists', name)
+        else:
+            raise ve