def resolve(vague_name, repository_path, default_series): """Get a Charm and associated identifying information :param str vague_name: a lazily specified charm name, suitable for use with :meth:`CharmURL.infer` :param repository_path: where on the local filesystem to find a repository (only currently meaningful when `charm_name` is specified with `"local:"`) :type repository_path: str or None :param str default_series: the Ubuntu series to insert when `charm_name` is inadequately specified. :return: a tuple of a :class:`juju.charm.url.CharmURL` and a :class:`juju.charm.base.CharmBase` subclass, which together contain both the charm's data and all information necessary to specify its source. """ url = CharmURL.infer(vague_name, default_series) if url.collection.schema == "local": repo = LocalCharmRepository(repository_path) elif url.collection.schema == "cs": repo = RemoteCharmRepository("https://store.juju.ubuntu.com") return repo, url
def test_agent_upgrade_bad_unit_state(self): """The an upgrade fails if the unit is in a bad state.""" self.agent.set_watch_enabled(False) yield self.agent.startService() # Upload a new version of the unit's charm repository = self.increment_charm(self.charm) charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) # Mark the unit for upgrade, with an invalid state. yield self.states["service"].set_charm_id(charm_state.id) yield self.states["unit"].set_upgrade_flag() yield self.agent.workflow.set_state("start_error") output = self.capture_logging("unit.upgrade", level=logging.DEBUG) # Do the upgrade upgrade = CharmUpgradeOperation(self.agent) value = yield upgrade.run() # Verify the upgrade. self.assertIdentical(value, False) self.assertIn("Unit not in an upgradeable state: start_error", output.getvalue()) self.assertIdentical( (yield self.states["unit"].get_upgrade_flag()), False)
def test_agent_force_upgrade_bad_unit_state(self): """The upgrade runs if forced and the unit is in a bad state.""" # Upload a new version of the unit's charm repository = self.increment_charm(self.charm) charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) old_charm_id = yield self.states["unit"].get_charm_id() output = self.capture_logging("juju.agents.unit", level=logging.DEBUG) self.agent.set_watch_enabled(True) yield self.agent.startService() # Mark the unit for upgrade, with an invalid state. with (yield self.agent.workflow.lock()): yield self.agent.workflow.fire_transition("error_configure") yield self.states["service"].set_charm_id(charm_state.id) yield self.states["unit"].set_upgrade_flag(force=True) # Its hard to watch something with no hooks and no state changes. yield self.sleep(0.1) self.assertIdentical( (yield self.states["unit"].get_upgrade_flag()), False) self.assertIn("Forced upgrade complete", output.getvalue()) self.assertEquals( (yield self.states["unit"].get_charm_id()), "local:series/mysql-2") self.assertEquals(old_charm_id, "local:series/dummy-1")
def test_agent_upgrade_bad_unit_state(self): """The upgrade fails if the unit is in a bad state.""" # Upload a new version of the unit's charm repository = self.increment_charm(self.charm) charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) old_charm_id = yield self.states["unit"].get_charm_id() log_written = self.wait_for_log( "juju.agents.unit", "Cannot upgrade: unit is in non-started state configure_error. " "Reissue upgrade command to try again.") self.agent.set_watch_enabled(True) yield self.agent.startService() # Mark the unit for upgrade, with an invalid state. with (yield self.agent.workflow.lock()): yield self.agent.workflow.fire_transition("error_configure") yield self.states["service"].set_charm_id(charm_state.id) yield self.states["unit"].set_upgrade_flag() yield log_written self.assertIdentical( (yield self.states["unit"].get_upgrade_flag()), False) self.assertEquals( (yield self.states["unit"].get_charm_id()), old_charm_id)
def test_charm_upgrade(self): """ 'juju charm-upgrade <service_name>' will schedule a charm for upgrade. """ repository = self.increment_charm(self.charm) mock_environment = self.mocker.patch(Environment) mock_environment.get_machine_provider() self.mocker.result(self.provider) finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() main(["upgrade-charm", "--repository", repository.path, "mysql"]) yield finished # Verify the service has a new charm reference charm_id = yield self.service_state1.get_charm_id() self.assertEqual(charm_id, "local:series/mysql-2") # Verify the provider storage has been updated charm = yield repository.find(CharmURL.parse("local:series/mysql")) storage = self.provider.get_file_storage() try: yield storage.get( "local_3a_series_2f_mysql-2_3a_%s" % charm.get_sha256()) except FileNotFound: self.fail("New charm not uploaded") # Verify the upgrade flag on the service units. upgrade_flag = yield self.service_unit1.get_upgrade_flag() self.assertTrue(upgrade_flag)
def assert_charm_upgraded(self, expect_upgraded): charm_id = yield self.states["unit"].get_charm_id() self.assertEquals(charm_id == self.expected_upgrade, expect_upgraded) if expect_upgraded: expect_revision = CharmURL.parse(self.expected_upgrade).revision charm = CharmDirectory(os.path.join(self.unit_directory, "charm")) self.assertEquals(charm.get_revision(), expect_revision)
def test_agent_upgrade_bad_unit_state(self): """The an upgrade fails if the unit is in a bad state.""" self.agent.set_watch_enabled(False) yield self.agent.startService() # Upload a new version of the unit's charm repository = self.increment_charm(self.charm) charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) # Mark the unit for upgrade, with an invalid state. yield self.states["service"].set_charm_id(charm_state.id) yield self.states["unit"].set_upgrade_flag() yield self.agent.workflow.set_state("start_error") output = self.capture_logging("unit.upgrade", level=logging.DEBUG) # Do the upgrade upgrade = CharmUpgradeOperation(self.agent) value = yield upgrade.run() # Verify the upgrade. self.assertIdentical(value, False) self.assertIn("Unit not in an upgradeable state: start_error", output.getvalue()) self.assertIdentical((yield self.states["unit"].get_upgrade_flag()), False)
def test_apply_new_charm_defaults(self): finished = self.setup_cli_reactor() self.setup_exit(0) self.mocker.replay() # Add a charm and its service. metadata = {"name": "haiku", "summary": "its short", "description": "but with cadence"} repository = self.add_charm( metadata=metadata, revision=1, config={ "options": { "foo": {"type": "string", "default": "foo-default", "description": "Foo"}, "bar": {"type": "string", "default": "bar-default", "description": "Bar"}, } }) charm_dir = yield repository.find(CharmURL.parse("local:series/haiku")) service_state = yield self.add_service_from_charm( "haiku", charm_dir=charm_dir) # Update a config value config = yield service_state.get_config() config["foo"] = "abc" yield config.write() # Upgrade the charm repository = self.add_charm( metadata=metadata, revision=2, config={ "options": { "foo": {"type": "string", "default": "foo-default", "description": "Foo"}, "bar": {"type": "string", "default": "bar-default", "description": "Bar"}, "dca": {"type": "string", "default": "default-dca", "description": "Airport"}, } }) main(["upgrade-charm", "--repository", repository.path, "haiku"]) yield finished config = yield service_state.get_config() self.assertEqual( config, {"foo": "abc", "dca": "default-dca", "bar": "bar-default"})
def assert_latest_error(self, dns_name, url_str, err_type, message): self.mocker.replay() repo = self.repo(dns_name) d = self.assertFailure(repo.latest(CharmURL.parse(url_str)), err_type) def verify(error): self.assertEquals(str(error), message) d.addCallback(verify) return d
def assert_find_uncached(self, dns_name, url_str, info_url, find_url): self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1))) self.mock_download(find_url) self.mocker.replay() repo = self.repo(dns_name) charm = yield repo.find(CharmURL.parse(url_str)) self.assertEquals(charm.get_sha256(), self.sha256) self.assertEquals(charm.path, self.cache_location(url_str, 1)) self.assertEquals(os.listdir(self.download_path), [])
def ready_upgrade(self, bad_hook): repository = self.increment_charm(self.charm) hooks_dir = os.path.join(repository.path, "series", "mysql", "hooks") self.write_exit_hook( "upgrade-charm", int(bad_hook), hooks_dir=hooks_dir) charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) yield self.states["service"].set_charm_id(charm_state.id) self.expected_upgrade = charm_state.id
def mark_charm_upgrade(self): # Create a new version of the charm repository = self.increment_charm(self.charm) # Upload the new charm version charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) # Mark the unit for upgrade yield self.states["service"].set_charm_id(charm_state.id) yield self.states["unit"].set_upgrade_flag()
def assert_find_cached(self, dns_name, url_str, info_url): os.makedirs(self.cache_path) cache_location = self.cache_location(url_str, 1) shutil.copy(self.charm.as_bundle().path, cache_location) self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1))) self.mocker.replay() repo = self.repo(dns_name) charm = yield repo.find(CharmURL.parse(url_str)) self.assertEquals(charm.get_sha256(), self.sha256) self.assertEquals(charm.path, cache_location)
def test_latest_store_warning(self): self.mock_charm_info( "https://anoth.er/charm-info?charms=cs%3Aseries/name", succeed(self.charm_info( "cs:series/name", 1, warnings=["eww", "yuck"]))) self.mocker.replay() repo = self.repo("https://anoth.er") log = self.capture_logging("juju.charm") revision = yield repo.latest(CharmURL.parse("cs:series/name-1")) self.assertIn("eww", log.getvalue()) self.assertIn("yuck", log.getvalue()) self.assertEquals(revision, 1)
def test_latest_store_warning(self): self.mock_charm_info( "https://anoth.er/charm-info?charms=cs%3Aseries/name", succeed( self.charm_info("cs:series/name", 1, warnings=["eww", "yuck"]))) self.mocker.replay() repo = self.repo("https://anoth.er") log = self.capture_logging("juju.charm") revision = yield repo.latest(CharmURL.parse("cs:series/name-1")) self.assertIn("eww", log.getvalue()) self.assertIn("yuck", log.getvalue()) self.assertEquals(revision, 1)
def test_find_info_store_warning(self): self.mock_charm_info( "https://anoth.er/charm-info?charms=cs%3Aseries/name-1", succeed(self.charm_info( "cs:series/name-1", 1, warnings=["omg", "halp"]))) self.mock_download("https://anoth.er/charm/series/name-1") self.mocker.replay() repo = self.repo("https://anoth.er") log = self.capture_logging("juju.charm") charm = yield repo.find(CharmURL.parse("cs:series/name-1")) self.assertIn("omg", log.getvalue()) self.assertIn("halp", log.getvalue()) self.assertEquals(charm.get_sha256(), self.sha256)
def test_revision(self): url1 = CharmURL.parse("cs:foo/bar") error = self.assertRaises(CharmURLError, url1.assert_revision) self.assertEquals( str(error), "Bad charm URL 'cs:foo/bar': expected a revision") url2 = url1.with_revision(0) url1.collection.schema = "local" # change url1, verify deep copied url2.assert_revision() self.assertEquals(str(url2), "cs:foo/bar-0") url3 = url2.with_revision(999) url3.assert_revision() self.assertEquals(str(url3), "cs:foo/bar-999")
def test_revision(self): url1 = CharmURL.parse("cs:foo/bar") error = self.assertRaises(CharmURLError, url1.assert_revision) self.assertEquals(str(error), "Bad charm URL 'cs:foo/bar': expected a revision") url2 = url1.with_revision(0) url1.collection.schema = "local" # change url1, verify deep copied url2.assert_revision() self.assertEquals(str(url2), "cs:foo/bar-0") url3 = url2.with_revision(999) url3.assert_revision() self.assertEquals(str(url3), "cs:foo/bar-999")
def test_find_info_store_warning(self): self.mock_charm_info( "https://anoth.er/charm-info?charms=cs%3Aseries/name-1", succeed( self.charm_info("cs:series/name-1", 1, warnings=["omg", "halp"]))) self.mock_download("https://anoth.er/charm/series/name-1") self.mocker.replay() repo = self.repo("https://anoth.er") log = self.capture_logging("juju.charm") charm = yield repo.find(CharmURL.parse("cs:series/name-1")) self.assertIn("omg", log.getvalue()) self.assertIn("halp", log.getvalue()) self.assertEquals(charm.get_sha256(), self.sha256)
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_deploy_upgrade_remote(self): """The upgrade option is invalid with a remote charm.""" repo = self.mocker.mock(RemoteCharmRepository) repo.type self.mocker.result("store") resolve = self.mocker.replace("juju.control.deploy.resolve") resolve("cs:sample", None, "series") self.mocker.result((repo, CharmURL.infer("cs:sample", "series"))) repo.find(MATCH(lambda x: isinstance(x, CharmURL))) self.mocker.result(CharmDirectory(self.sample_dir1)) self.mocker.replay() environment = self.config.get("firstenv") error = yield self.assertFailure(deploy.deploy( self.config, environment, None, "cs:sample", "myblog", logging.getLogger("deploy"), [], upgrade=True), CharmError) self.assertIn("Only local directory charms can be upgraded on deploy", str(error))
def test_agent_upgrade_hook_failure(self): """An upgrade fails if the upgrade hook errors.""" self.agent.set_watch_enabled(False) yield self.agent.startService() # Upload a new version of the unit's charm repository = self.increment_charm(self.charm) charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) # Mark the unit for upgrade yield self.states["service"].set_charm_id(charm_state.id) yield self.states["unit"].set_upgrade_flag() hook_done = self.wait_on_hook( "upgrade-charm", executor=self.agent.executor) self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") output = self.capture_logging("unit.upgrade", level=logging.DEBUG) # Do the upgrade upgrade = CharmUpgradeOperation(self.agent) value = yield upgrade.run() # Verify the failed upgrade. self.assertIdentical(value, False) self.assertIn("Invoking upgrade transition", output.getvalue()) self.assertIn("Upgrade failed.", output.getvalue()) yield hook_done # Verify state workflow_state = yield self.agent.workflow.get_state() self.assertEqual("charm_upgrade_error", workflow_state) # Verify new charm is in place new_charm = get_charm_from_path( os.path.join(self.agent.unit_directory, "charm")) self.assertEqual( self.charm.get_revision() + 1, new_charm.get_revision()) # Verify upgrade flag is cleared. self.assertFalse((yield self.states["unit"].get_upgrade_flag()))
def test_agent_upgrade_hook_failure(self): """An upgrade fails if the upgrade hook errors.""" self.agent.set_watch_enabled(False) yield self.agent.startService() # Upload a new version of the unit's charm repository = self.increment_charm(self.charm) charm = yield repository.find(CharmURL.parse("local:series/mysql")) charm, charm_state = yield self.publish_charm(charm.path) # Mark the unit for upgrade yield self.states["service"].set_charm_id(charm_state.id) yield self.states["unit"].set_upgrade_flag() hook_done = self.wait_on_hook("upgrade-charm", executor=self.agent.executor) self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1") output = self.capture_logging("unit.upgrade", level=logging.DEBUG) # Do the upgrade upgrade = CharmUpgradeOperation(self.agent) value = yield upgrade.run() # Verify the failed upgrade. self.assertIdentical(value, False) self.assertIn("Invoking upgrade transition", output.getvalue()) self.assertIn("Upgrade failed.", output.getvalue()) yield hook_done # Verify state workflow_state = yield self.agent.workflow.get_state() self.assertEqual("charm_upgrade_error", workflow_state) # Verify new charm is in place new_charm = get_charm_from_path( os.path.join(self.agent.unit_directory, "charm")) self.assertEqual(self.charm.get_revision() + 1, new_charm.get_revision()) # Verify upgrade flag is cleared. self.assertFalse((yield self.states["unit"].get_upgrade_flag()))
def test_find_inappropriate_url(self): url = CharmURL.parse("cs:foo/bar") err = self.assertRaises(AssertionError, self.repository1.find, url) self.assertEquals(str(err), "schema mismatch")
def assert_infer(self, string, schema, user, series, name, rev): url = CharmURL.infer(string, "default") self.assert_url(url, schema, user, series, name, rev)
def assert_latest(self, dns_name, url_str, revision): self.mocker.replay() repo = self.repo(dns_name) result = yield repo.latest(CharmURL.parse(url_str)) self.assertEquals(result, revision)
def assert_parse(self, string, schema, user, series, name, rev): url = CharmURL.parse(string) self.assert_url(url, schema, user, series, name, rev) self.assertEquals(str(url), string) self.assertEquals(url.path, string.split(":", 1)[1])
def charm_url(self, name): return CharmURL.parse("local:series/" + name)
def upgrade_charm( config, environment, verbose, log, repository_path, service_name, dry_run): """Upgrades a service's charm. First determines if an upgrade is available, then updates the service charm reference, and marks the units as needing upgrades. """ provider = environment.get_machine_provider() client = yield provider.connect() service_manager = ServiceStateManager(client) service_state = yield service_manager.get_service_state(service_name) old_charm_id = yield service_state.get_charm_id() old_charm_url = CharmURL.parse(old_charm_id) old_charm_url.assert_revision() repo, charm_url = resolve( str(old_charm_url.with_revision(None)), repository_path, environment.default_series) new_charm_url = charm_url.with_revision( (yield repo.latest(charm_url))) if charm_url.collection.schema == "local": if old_charm_url.revision >= new_charm_url.revision: new_revision = old_charm_url.revision + 1 charm = yield repo.find(new_charm_url) if isinstance(charm, CharmDirectory): if dry_run: log.info("%s would be set to revision %s", charm.path, new_revision) else: log.info("Setting %s to revision %s", charm.path, new_revision) charm.set_revision(new_revision) new_charm_url.revision = new_revision new_charm_id = str(new_charm_url) # Verify its newer than what's deployed if not new_charm_url.revision > old_charm_url.revision: if dry_run: log.info("Service already running latest charm %r", old_charm_id) else: raise NewerCharmNotFound(old_charm_id) elif dry_run: log.info("Service would be upgraded from charm %r to %r", old_charm_id, new_charm_id) # On dry run, stop before modifying state. if not dry_run: # Publish the new charm storage = provider.get_file_storage() publisher = CharmPublisher(client, storage) charm = yield repo.find(new_charm_url) yield publisher.add_charm(new_charm_id, charm) result = yield publisher.publish() charm_state = result[0] # Update the service charm reference yield service_state.set_charm_id(charm_state.id) # Mark the units for upgrades units = yield service_state.get_all_unit_states() for unit in units: running, state = yield is_unit_running(client, unit) if not running: log.info( "Unit %r is not in a running state (state: %r), won't upgrade", unit.unit_name, state or "uninitialized") continue if not dry_run: yield unit.set_upgrade_flag()
def cache_location(self, url_str, revision): charm_url = CharmURL.parse(url_str) cache_key = under.quote("%s.charm" % (charm_url.with_revision(revision))) return os.path.join(self.cache_path, cache_key)
def cache_location(self, url_str, revision): charm_url = CharmURL.parse(url_str) cache_key = under.quote( "%s.charm" % (charm_url.with_revision(revision))) return os.path.join(self.cache_path, cache_key)