Esempio n. 1
0
class CharmState(object):
    """State of a charm registered in an environment."""

    def __init__(self, client, charm_id, charm_data):
        self._client = client
        self._charm_url = CharmURL.parse(charm_id)
        self._charm_url.assert_revision()

        self._metadata = MetaData()
        self._metadata.parse_serialization_data(charm_data["metadata"])

        self._config = ConfigOptions()
        self._config.parse(charm_data["config"])

        # Just a health check:
        assert self._metadata.name == self.name

        self._sha256 = charm_data["sha256"]

        self._bundle_url = charm_data.get("url")

    @property
    def name(self):
        """The charm name."""
        return self._charm_url.name

    @property
    def revision(self):
        """The monotonically increasing charm revision number.
        """
        return self._charm_url.revision

    @property
    def bundle_url(self):
        """The url to the charm bundle in the provider storage."""
        return self._bundle_url

    @property
    def id(self):
        """The charm id"""
        return str(self._charm_url)

    def get_metadata(self):
        """Return deferred MetaData."""
        return succeed(self._metadata)

    def get_config(self):
        """Return deferred ConfigOptions."""
        return succeed(self._config)

    def get_sha256(self):
        """Return deferred sha256 for the charm."""
        return succeed(self._sha256)

    def is_subordinate(self):
        """Is this a subordinate charm."""
        return self._metadata.is_subordinate
Esempio n. 2
0
class CharmState(object):
    """State of a charm registered in an environment."""
    def __init__(self, client, charm_id, charm_data):
        self._client = client
        self._charm_url = CharmURL.parse(charm_id)
        self._charm_url.assert_revision()

        self._metadata = MetaData()
        self._metadata.parse_serialization_data(charm_data["metadata"])

        self._config = ConfigOptions()
        self._config.parse(charm_data["config"])

        # Just a health check:
        assert self._metadata.name == self.name

        self._sha256 = charm_data["sha256"]

        self._bundle_url = charm_data.get("url")

    @property
    def name(self):
        """The charm name."""
        return self._charm_url.name

    @property
    def revision(self):
        """The monotonically increasing charm revision number.
        """
        return self._charm_url.revision

    @property
    def bundle_url(self):
        """The url to the charm bundle in the provider storage."""
        return self._bundle_url

    @property
    def id(self):
        """The charm id"""
        return str(self._charm_url)

    def get_metadata(self):
        """Return deferred MetaData."""
        return succeed(self._metadata)

    def get_config(self):
        """Return deferred ConfigOptions."""
        return succeed(self._config)

    def get_sha256(self):
        """Return deferred sha256 for the charm."""
        return succeed(self._sha256)
Esempio n. 3
0
class CharmBundle(CharmBase):
    """ZIP-archive that contains charm directory content."""

    def __init__(self, path):
        self.path = isinstance(path, file) and path.name or path
        try:
            zf = ZipFile(path, 'r')
        except BadZipfile, exc:
            raise CharmError(path, "must be a zip file (%s)" % exc)

        if "metadata.yaml" not in zf.namelist():
            raise CharmError(
                path, "charm does not contain required file 'metadata.yaml'")
        self.metadata = MetaData()
        self.metadata.parse(zf.read("metadata.yaml"))

        try:
            revision_content = zf.read("revision")
        except KeyError:
            revision_content = None
        self._revision = get_revision(
            revision_content, self.metadata, self.path)
        if self._revision is None:
            raise CharmError(self.path, "has no revision")

        self.config = ConfigOptions()
        if "config.yaml" in zf.namelist():
            self.config.parse(zf.read("config.yaml"))
Esempio n. 4
0
    def __init__(self, client, charm_id, charm_data):
        self._client = client
        self._charm_url = CharmURL.parse(charm_id)
        self._charm_url.assert_revision()

        self._metadata = MetaData()
        self._metadata.parse_serialization_data(charm_data["metadata"])

        self._config = ConfigOptions()
        self._config.parse(charm_data["config"])

        # Just a health check:
        assert self._metadata.name == self.name

        self._sha256 = charm_data["sha256"]

        self._bundle_url = charm_data.get("url")
Esempio n. 5
0
    def test_load_file(self):
        sample_path = self.makeFile(sample_configuration)
        config = ConfigOptions()
        config.load(sample_path)

        self.assertEqual(config.get_serialization_data(),
                         sample_yaml_data)

        # and an expected exception
        # on an empty file
        empty_file = self.makeFile("")
        error = self.assertRaises(ServiceConfigError, config.load, empty_file)
        self.assertEqual(
            str(error),
            ("Error processing %r: "
             "Missing required service options metadata") % empty_file)

        # a missing filename is allowed
        config = config.load("missing_file")
Esempio n. 6
0
    def __init__(self, path):
        self.path = path
        self.metadata = MetaData(os.path.join(path, "metadata.yaml"))

        revision_content = None
        revision_path = os.path.join(self.path, "revision")
        if os.path.exists(revision_path):
            with open(revision_path) as f:
                revision_content = f.read()
        self._revision = get_revision(revision_content, self.metadata,
                                      self.path)
        if self._revision is None:
            self.set_revision(0)
        elif revision_content is None:
            self.set_revision(self._revision)

        self.config = ConfigOptions()
        self.config.load(os.path.join(path, "config.yaml"))
        self._temp_bundle = None
        self._temp_bundle_file = None
Esempio n. 7
0
    def __init__(self, client, charm_id, charm_data):
        self._client = client
        self._charm_url = CharmURL.parse(charm_id)
        self._charm_url.assert_revision()

        self._metadata = MetaData()
        self._metadata.parse_serialization_data(charm_data["metadata"])

        self._config = ConfigOptions()
        self._config.parse(charm_data["config"])

        # Just a health check:
        assert self._metadata.name == self.name

        self._sha256 = charm_data["sha256"]

        self._bundle_url = charm_data.get("url")
Esempio n. 8
0
    def __init__(self, path):
        self.path = path
        self.metadata = MetaData(os.path.join(path, "metadata.yaml"))

        revision_content = None
        revision_path = os.path.join(self.path, "revision")
        if os.path.exists(revision_path):
            with open(revision_path) as f:
                revision_content = f.read()
        self._revision = get_revision(revision_content, self.metadata, self.path)
        if self._revision is None:
            self.set_revision(0)
        elif revision_content is None:
            self.set_revision(self._revision)

        self.config = ConfigOptions()
        self.config.load(os.path.join(path, "config.yaml"))
        self._temp_bundle = None
        self._temp_bundle_file = None
Esempio n. 9
0
    def test_validate(self):
        sample_input = {"title": "Helpful Title", "outlook": "Peachy"}

        self.config.parse(sample_configuration)
        data = self.config.validate(sample_input)

        # This should include an overridden value, a default and a new value.
        self.assertEqual(data,
                         {"username": "******",
                          "outlook": "Peachy",
                          "title": "Helpful Title"})

        # now try to set a value outside the expected
        sample_input["bad"] = "value"
        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, sample_input)
        self.assertEqual(error.message,
                         "bad is not a valid configuration option.")

        # validating with an empty instance
        # the service takes no options
        config = ConfigOptions()
        self.assertRaises(
            ServiceConfigValueError, config.validate, sample_input)
Esempio n. 10
0
    def test_load_file(self):
        sample_path = self.makeFile(sample_configuration)
        config = ConfigOptions()
        config.load(sample_path)

        self.assertEqual(config.get_serialization_data(),
                         sample_yaml_data)

        # and an expected exception
        # on an empty file
        empty_file = self.makeFile("")
        error = self.assertRaises(ServiceConfigError, config.load, empty_file)
        self.assertEqual(
            str(error),
            ("Error processing %r: "
             "Missing required service options metadata") % empty_file)

        # a missing filename is allowed
        config = config.load("missing_file")
Esempio n. 11
0
class CharmDirectory(CharmBase):
    """Directory that holds charm content.

    :param path: Path to charm directory

    The directory must contain the following files::

    - ``metadata.yaml``

    """

    type = "dir"

    def __init__(self, path):
        self.path = path
        self.metadata = MetaData(os.path.join(path, "metadata.yaml"))

        revision_content = None
        revision_path = os.path.join(self.path, "revision")
        if os.path.exists(revision_path):
            with open(revision_path) as f:
                revision_content = f.read()
        self._revision = get_revision(revision_content, self.metadata, self.path)
        if self._revision is None:
            self.set_revision(0)
        elif revision_content is None:
            self.set_revision(self._revision)

        self.config = ConfigOptions()
        self.config.load(os.path.join(path, "config.yaml"))
        self._temp_bundle = None
        self._temp_bundle_file = None

    def get_revision(self):
        return self._revision

    def set_revision(self, revision):
        self._revision = revision
        with open(os.path.join(self.path, "revision"), "w") as f:
            f.write(str(revision) + "\n")

    def make_archive(self, path):
        """Create archive of directory and write to ``path``.

        :param path: Path to archive

        - build/* - This is used for packing the charm itself and any
                    similar tasks.
        - */.*    - Hidden files are all ignored for now.  This will most
                    likely be changed into a specific ignore list (.bzr, etc)
        """

        zf = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED)
        for dirpath, dirnames, filenames in os.walk(self.path):
            relative_path = dirpath[len(self.path) + 1 :]
            if relative_path and not self._ignore(relative_path):
                zf.write(dirpath, relative_path)
            for name in filenames:
                archive_name = os.path.join(relative_path, name)
                if not self._ignore(archive_name):
                    real_path = os.path.join(dirpath, name)
                    self._check_type(real_path)
                    if os.path.islink(real_path):
                        self._check_link(real_path)
                        self._write_symlink(zf, os.readlink(real_path), archive_name)
                    else:
                        zf.write(real_path, archive_name)
        zf.close()

    def _check_type(self, path):
        """Check the path
        """
        s = os.stat(path)
        if stat.S_ISDIR(s.st_mode) or stat.S_ISREG(s.st_mode):
            return path
        raise InvalidCharmFile(self.metadata.name, path, "Invalid file type for a charm")

    def _check_link(self, path):
        link_path = os.readlink(path)
        if link_path[0] == "/":
            raise InvalidCharmFile(self.metadata.name, path, "Absolute links are invalid")
        path_dir = os.path.dirname(path)
        link_path = os.path.join(path_dir, link_path)
        if not link_path.startswith(os.path.abspath(self.path)):
            raise InvalidCharmFile(self.metadata.name, path, "Only internal symlinks are allowed")

    def _write_symlink(self, zf, link_target, link_path):
        """Package symlinks with appropriate zipfile metadata."""
        info = zipfile.ZipInfo()
        info.filename = link_path
        info.create_system = 3
        # Preserve the pre-existing voodoo mode in a slightly clearer form.
        info.external_attr = (stat.S_IFLNK | 0755) << 16
        zf.writestr(info, link_target)

    def _ignore(self, path):
        if path == "build" or path.startswith("build/"):
            return True
        if path.startswith("."):
            return True

    def as_bundle(self):
        if self._temp_bundle is None:
            prefix = "%s-%d.charm." % (self.metadata.name, self.get_revision())
            temp_file = tempfile.NamedTemporaryFile(prefix=prefix)
            self.make_archive(temp_file.name)
            self._temp_bundle = CharmBundle(temp_file.name)
            # Attach the life time of temp_file to self:
            self._temp_bundle_file = temp_file
        return self._temp_bundle

    def as_directory(self):
        return self

    def compute_sha256(self):
        """
        Compute sha256, based on the bundle.
        """
        return self.as_bundle().compute_sha256()
Esempio n. 12
0
class CharmDirectory(CharmBase):
    """Directory that holds charm content.

    :param path: Path to charm directory

    The directory must contain the following files::

    - ``metadata.yaml``

    """

    def __init__(self, path):
        self.path = path
        self.metadata = MetaData(os.path.join(path, "metadata.yaml"))

        revision_content = None
        revision_path = os.path.join(self.path, "revision")
        if os.path.exists(revision_path):
            with open(revision_path) as f:
                revision_content = f.read()
        self._revision = get_revision(
            revision_content, self.metadata, self.path)
        if self._revision is None:
            self.set_revision(0)
        elif revision_content is None:
            self.set_revision(self._revision)

        self.config = ConfigOptions()
        self.config.load(os.path.join(path, "config.yaml"))
        self._temp_bundle = None
        self._temp_bundle_file = None

    def get_revision(self):
        return self._revision

    def set_revision(self, revision):
        self._revision = revision
        with open(os.path.join(self.path, "revision"), "w") as f:
            f.write(str(revision) + "\n")

    def make_archive(self, path):
        """Create archive of directory and write to ``path``.

        :param path: Path to archive

        - build/* - This is used for packing the charm itself and any
                    similar tasks.
        - */.*    - Hidden files are all ignored for now.  This will most
                    likely be changed into a specific ignore list (.bzr, etc)
        """

        zf = zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED)

        for dirpath, dirnames, filenames in os.walk(self.path):
            relative_path = dirpath[len(self.path) + 1:]
            if relative_path and not self._ignore(relative_path):
                zf.write(dirpath, relative_path)
            for name in filenames:
                archive_name = os.path.join(relative_path, name)
                if not self._ignore(archive_name):
                    real_path = os.path.join(dirpath, name)
                    zf.write(real_path, archive_name)

        zf.close()

    def _ignore(self, path):
        if path == "build" or path.startswith("build/"):
            return True
        if path.startswith('.'):
            return True

    def as_bundle(self):
        if self._temp_bundle is None:
            prefix = "%s-%d.charm." % (self.metadata.name, self.get_revision())
            temp_file = tempfile.NamedTemporaryFile(prefix=prefix)
            self.make_archive(temp_file.name)
            self._temp_bundle = CharmBundle(temp_file.name)
            # Attach the life time of temp_file to self:
            self._temp_bundle_file = temp_file
        return self._temp_bundle

    def as_directory(self):
        return self

    def compute_sha256(self):
        """
        Compute sha256, based on the bundle.
        """
        return self.as_bundle().compute_sha256()
Esempio n. 13
0
 def setUp(self):
     self.config = ConfigOptions()
Esempio n. 14
0
class ConfigOptionsTest(TestCase):

    def setUp(self):
        self.config = ConfigOptions()

    def test_load(self):
        """Validate we can load data or get expected errors."""

        # load valid data
        filename = self.makeFile(sample_configuration)
        self.config.load(filename)
        self.assertEqual(self.config.get_serialization_data(),
                         sample_yaml_data)

        # test with dict based data
        self.config.parse(sample_yaml_data)
        self.assertEqual(self.config.get_serialization_data(),
                         sample_yaml_data)

        # and with an unhandled type
        self.assertRaises(TypeError, self.config.load, 1.234)

    def test_load_file(self):
        sample_path = self.makeFile(sample_configuration)
        config = ConfigOptions()
        config.load(sample_path)

        self.assertEqual(config.get_serialization_data(),
                         sample_yaml_data)

        # and an expected exception
        # on an empty file
        empty_file = self.makeFile("")
        error = self.assertRaises(ServiceConfigError, config.load, empty_file)
        self.assertEqual(
            str(error),
            ("Error processing %r: "
             "Missing required service options metadata") % empty_file)

        # a missing filename is allowed
        config = config.load("missing_file")

    def test_defaults(self):
        self.config.parse(sample_configuration)
        defaults = self.config.get_defaults()
        self.assertEqual(defaults, sample_config_defaults)

    def test_defaults_validated(self):
        e = self.assertRaises(
            ServiceConfigValueError,
            self.config.parse,
            yaml.dump(
                {"options": {
                    "foobar": {
                        "description": "beyond what?",
                        "type": "string",
                        "default": True}}}))
        self.assertEqual(
            str(e), "Invalid value for foobar: True")

    def test_as_dict(self):
        # load valid data
        filename = self.makeFile(sample_configuration)
        self.config.load(filename)

        # Verify dictionary serialization
        schema_dict = self.config.as_dict()
        self.assertEqual(schema_dict,
                         yaml.load(sample_configuration)["options"])

        # Verify the dictionary is a copy
        # Poke at embedded objects
        schema_dict["outlook"]["default"] = 1
        schema2_dict = self.config.as_dict()
        self.assertFalse("default" in schema2_dict["outlook"])

    def test_parse(self):
        """Verify that parse checks and raises."""
        # no options dict
        self.assertRaises(
            ServiceConfigError, self.config.parse, {"foo": "bar"})

        # and with bad data expected exceptions
        error = self.assertRaises(yaml.YAMLError,
                          self.config.parse, "foo: [1, 2", "/tmp/zamboni")
        self.assertIn("/tmp/zamboni", str(error))

    def test_validate(self):
        sample_input = {"title": "Helpful Title", "outlook": "Peachy"}

        self.config.parse(sample_configuration)
        data = self.config.validate(sample_input)

        # This should include an overridden value, a default and a new value.
        self.assertEqual(data,
                         {"outlook": "Peachy",
                          "title": "Helpful Title"})

        # now try to set a value outside the expected
        sample_input["bad"] = "value"
        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, sample_input)
        self.assertEqual(error.message,
                         "bad is not a valid configuration option.")

        # validating with an empty instance
        # the service takes no options
        config = ConfigOptions()
        self.assertRaises(
            ServiceConfigValueError, config.validate, sample_input)

    def test_validate_float(self):
        self.config.parse(yaml.dump(
            {"options": {
                "score": {
                    "description": "A number indicating score.",
                    "type": "float"}}}))
        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, {"score": "82"})
        self.assertEquals(str(error), "Invalid value for score: '82'")

        data = self.config.validate({"score": 82})
        self.assertEqual(data, {"score": 82})

    def test_validate_string(self):
        self.config.parse(sample_configuration)

        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, {"title": True})
        self.assertEquals(str(error), "Invalid value for title: True")

        data = self.config.validate({"title": u"Good"})
        self.assertEqual(data, {"title": u"Good"})

    def test_validate_boolean(self):
        self.config.parse(yaml.dump(
            {"options": {
                "active": {
                    "description": "A boolean indicating activity.",
                    "type": "boolean"}}}))

        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, {"active": "True"})
        self.assertEquals(str(error), "Invalid value for active: 'True'")

        data = self.config.validate({"active": False})
        self.assertEqual(data, {"active": False})

    def test_validate_integer(self):
        self.config.parse(sample_configuration)

        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, {"skill-level": "NaN"})
        self.assertEquals(str(error), "Invalid value for skill-level: 'NaN'")

        data = self.config.validate({"skill-level": "9001"})
        # its over 9000!
        self.assertEqual(data, {"skill-level": 9001})

    def test_validate_with_obsolete_str(self):
        """
        Test the handling for the obsolete 'str' option type (it's
        'string' now). Remove support for it after a while, and take
        this test with it.
        """
        config = yaml.load(sample_configuration)
        config["options"]["title"]["type"] = "str"
        obsolete_config = yaml.dump(config)

        sio = StringIO()
        self.patch(sys, "stderr", sio)

        self.config.parse(obsolete_config)
        data = self.config.validate({"title": "Helpful Title"})
        self.assertEqual(data["title"], "Helpful Title")
        self.assertIn("obsolete 'str'", sio.getvalue())

        # Trying it again, it should not warn since we don't want
        # to pester the charm author.
        sio.truncate(0)
        self.config.parse(obsolete_config)
        data = self.config.validate({"title": "Helpful Title"})
        self.assertEqual(data["title"], "Helpful Title")
        self.assertEqual(sio.getvalue(), "")
Esempio n. 15
0
class CharmDirectory(CharmBase):
    """Directory that holds charm content.

    :param path: Path to charm directory

    The directory must contain the following files::

    - ``metadata.yaml``

    """
    def __init__(self, path):
        self.path = path
        self.metadata = MetaData(os.path.join(path, "metadata.yaml"))

        revision_content = None
        revision_path = os.path.join(self.path, "revision")
        if os.path.exists(revision_path):
            with open(revision_path) as f:
                revision_content = f.read()
        self._revision = get_revision(revision_content, self.metadata,
                                      self.path)
        if self._revision is None:
            self.set_revision(0)
        elif revision_content is None:
            self.set_revision(self._revision)

        self.config = ConfigOptions()
        self.config.load(os.path.join(path, "config.yaml"))
        self._temp_bundle = None
        self._temp_bundle_file = None

    def get_revision(self):
        return self._revision

    def set_revision(self, revision):
        self._revision = revision
        with open(os.path.join(self.path, "revision"), "w") as f:
            f.write(str(revision) + "\n")

    def make_archive(self, path):
        """Create archive of directory and write to ``path``.

        :param path: Path to archive

        - build/* - This is used for packing the charm itself and any
                    similar tasks.
        - */.*    - Hidden files are all ignored for now.  This will most
                    likely be changed into a specific ignore list (.bzr, etc)
        """

        zf = zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED)

        for dirpath, dirnames, filenames in os.walk(self.path):
            relative_path = dirpath[len(self.path) + 1:]
            if relative_path and not self._ignore(relative_path):
                zf.write(dirpath, relative_path)
            for name in filenames:
                archive_name = os.path.join(relative_path, name)
                if not self._ignore(archive_name):
                    real_path = os.path.join(dirpath, name)
                    zf.write(real_path, archive_name)

        zf.close()

    def _ignore(self, path):
        if path == "build" or path.startswith("build/"):
            return True
        if path.startswith('.'):
            return True

    def as_bundle(self):
        if self._temp_bundle is None:
            prefix = "%s-%d.charm." % (self.metadata.name, self.get_revision())
            temp_file = tempfile.NamedTemporaryFile(prefix=prefix)
            self.make_archive(temp_file.name)
            self._temp_bundle = CharmBundle(temp_file.name)
            # Attach the life time of temp_file to self:
            self._temp_bundle_file = temp_file
        return self._temp_bundle

    def as_directory(self):
        return self

    def compute_sha256(self):
        """
        Compute sha256, based on the bundle.
        """
        return self.as_bundle().compute_sha256()
Esempio n. 16
0
 def setUp(self):
     self.config = ConfigOptions()
Esempio n. 17
0
class ConfigOptionsTest(TestCase):

    def setUp(self):
        self.config = ConfigOptions()

    def test_load(self):
        """Validate we can load data or get expected errors."""

        # load valid data
        filename = self.makeFile(sample_configuration)
        self.config.load(filename)
        self.assertEqual(self.config.get_serialization_data(),
                         sample_yaml_data)

        # test with dict based data
        self.config.parse(sample_yaml_data)
        self.assertEqual(self.config.get_serialization_data(),
                         sample_yaml_data)

        # and with an unhandled type
        self.assertRaises(TypeError, self.config.load, 1.234)

    def test_load_file(self):
        sample_path = self.makeFile(sample_configuration)
        config = ConfigOptions()
        config.load(sample_path)

        self.assertEqual(config.get_serialization_data(),
                         sample_yaml_data)

        # and an expected exception
        # on an empty file
        empty_file = self.makeFile("")
        error = self.assertRaises(ServiceConfigError, config.load, empty_file)
        self.assertEqual(
            str(error),
            ("Error processing %r: "
             "Missing required service options metadata") % empty_file)

        # a missing filename is allowed
        config = config.load("missing_file")

    def test_defaults(self):
        self.config.parse(sample_configuration)
        defaults = self.config.get_defaults()
        self.assertEqual(defaults, sample_config_defaults)

    def test_as_dict(self):
        # load valid data
        filename = self.makeFile(sample_configuration)
        self.config.load(filename)

        # Verify dictionary serialization
        schema_dict = self.config.as_dict()
        self.assertEqual(schema_dict,
                         yaml.load(sample_configuration)["options"])

        # Verify the dictionary is a copy
        # Poke at embedded objects
        schema_dict["outlook"]["default"] = 1
        schema2_dict = self.config.as_dict()
        self.assertFalse("default" in schema2_dict["outlook"])

    def test_parse(self):
        """Verify that parse checks and raises."""
        # no options dict
        self.assertRaises(
            ServiceConfigError, self.config.parse, {"foo": "bar"})

        # and with bad data expected exceptions
        error = self.assertRaises(yaml.YAMLError,
                          self.config.parse, "foo: [1, 2", "/tmp/zamboni")
        self.assertIn("/tmp/zamboni", str(error))

    def test_validate(self):
        sample_input = {"title": "Helpful Title", "outlook": "Peachy"}

        self.config.parse(sample_configuration)
        data = self.config.validate(sample_input)

        # This should include an overridden value, a default and a new value.
        self.assertEqual(data,
                         {"username": "******",
                          "outlook": "Peachy",
                          "title": "Helpful Title"})

        # now try to set a value outside the expected
        sample_input["bad"] = "value"
        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, sample_input)
        self.assertEqual(error.message,
                         "bad is not a valid configuration option.")

        # validating with an empty instance
        # the service takes no options
        config = ConfigOptions()
        self.assertRaises(
            ServiceConfigValueError, config.validate, sample_input)

    def test_validate_types(self):
        self.config.parse(sample_configuration)

        error = self.assertRaises(ServiceConfigValueError,
                                  self.config.validate, {"skill-level": "NaN"})
        self.assertEquals(str(error), "Invalid value for skill-level: NaN")

        data = self.config.validate({"skill-level": "9001"})
        # its over 9000!
        self.assertEqual(data, {"skill-level": 9001,
                                "title": "My Title",
                                "username": "******"})

    def test_validate_with_obsolete_str(self):
        """
        Test the handling for the obsolete 'str' option type (it's
        'string' now). Remove support for it after a while, and take
        this test with it.
        """
        config = yaml.load(sample_configuration)
        config["options"]["title"]["type"] = "str"
        obsolete_config = yaml.dump(config)

        sio = StringIO()
        self.patch(sys, "stderr", sio)

        self.config.parse(obsolete_config)
        data = self.config.validate({"title": "Helpful Title"})
        self.assertEqual(data["title"], "Helpful Title")
        self.assertIn("obsolete 'str'", sio.getvalue())

        # Trying it again, it should not warn since we don't want
        # to pester the charm author.
        sio.truncate(0)
        self.config.parse(obsolete_config)
        data = self.config.validate({"title": "Helpful Title"})
        self.assertEqual(data["title"], "Helpful Title")
        self.assertEqual(sio.getvalue(), "")