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
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)
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"))
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")
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 __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 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)
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()
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()
def setUp(self): self.config = ConfigOptions()
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(), "")
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()
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(), "")