def test_equality(self): test_cases = [ ("1.0.0", "1.0.0", True), ("01.0.0", "1.0.0", True), ("10.0.0", "9.0.0", False), ("1.0.0", "1.0.1", False), ("1.0.1", "1.0.0", False), ("1.0.0", "1.1.0", False), ("1.1.0", "1.0.0", False), ("1.0.0", "2.0.0", False), ("1.2-alpha1", "1.2.0", False), ("1.2-alpha2", "1.2-alpha1", False), ("1.2-alpha2.1", "1.2-alpha2", False), ("1.2-alpha2.2", "1.2-alpha2.1", False), ("1.2-beta1", "1.2-alpha1", False), ("1.2-beta1", "1.2-alpha2.1", False), ("1.2-beta1", "1.2.0", False), ("1.2.1", "1.2.0", False), ("2.0.0", "1.0.0", False), ("2.0.0.0", "2.0.0", True), ("2.0.0.0", "2.0.0.0", True), ("2.0.0.1", "2.0.0.0", False), ("2.0.1.10", "2.0.0.0", False), ] for a, b, expected in test_cases: self.assertEqual(JujuVersion(a) == JujuVersion(b), expected) self.assertEqual(JujuVersion(a) == b, expected)
def relation_set(self, relation_id, key, value, is_app): if not isinstance(is_app, bool): raise TypeError( 'is_app parameter to relation_set must be a boolean') if is_app: version = JujuVersion.from_environ() if not version.has_app_data(): raise RuntimeError( 'setting application data is not supported on Juju version {}' .format(version)) args = [ 'relation-set', '-r', str(relation_id), '{}={}'.format(key, value) ] if is_app: args.append('--app') try: return self._run(*args) except ModelError as e: if 'relation not found' in str(e): raise RelationNotFoundError() from e raise
def __init__(self, charm_dir: Path): self._charm_dir = charm_dir self._exec_path = Path(os.environ.get('JUJU_DISPATCH_PATH', sys.argv[0])) dispatch = charm_dir / 'dispatch' if JujuVersion.from_environ().is_dispatch_aware() and _exe_path(dispatch) is not None: self._init_dispatch() else: self._init_legacy()
def test_parsing(self): test_cases = [("0.0.0", 0, 0, '', 0, 0), ("0.0.2", 0, 0, '', 2, 0), ("0.1.0", 0, 1, '', 0, 0), ("0.2.3", 0, 2, '', 3, 0), ("10.234.3456", 10, 234, '', 3456, 0), ("10.234.3456.1", 10, 234, '', 3456, 1), ("1.21-alpha12", 1, 21, 'alpha', 12, 0), ("1.21-alpha1.34", 1, 21, 'alpha', 1, 34), ("2.7", 2, 7, '', 0, 0)] for vs, major, minor, tag, patch, build in test_cases: v = JujuVersion(vs) self.assertEqual(v.major, major) self.assertEqual(v.minor, minor) self.assertEqual(v.tag, tag) self.assertEqual(v.patch, patch) self.assertEqual(v.build, build)
def test_from_environ(self): # JUJU_VERSION is not set v = JujuVersion.from_environ() self.assertEqual(v, JujuVersion('0.0.0')) os.environ['JUJU_VERSION'] = 'no' with self.assertRaisesRegex(RuntimeError, 'not a valid Juju version'): JujuVersion.from_environ() os.environ['JUJU_VERSION'] = '2.8.0' v = JujuVersion.from_environ() self.assertEqual(v, JujuVersion('2.8.0'))
def test_parsing_errors(self): invalid_versions = [ "xyz", "foo.bar", "foo.bar.baz", "dead.beef.ca.fe", "1234567890.2.1", # The major version is too long. "0.2..1", # Two periods next to each other. "1.21.alpha1", # Tag comes after period. "1.21-alpha", # No patch number but a tag is present. "1.21-alpha1beta", # Non-numeric string after the patch number. "1.21-alpha-dev", # Tag duplication. "1.21-alpha_dev3", # Underscore in a tag. "1.21-alpha123dev3", # Non-numeric string after the patch number. ] for v in invalid_versions: with self.assertRaises(RuntimeError): JujuVersion(v)
def _should_use_controller_storage(db_path: Path, meta: ops.charm.CharmMeta) -> bool: """Figure out whether we want to use controller storage or not.""" # if you've previously used local state, carry on using that if db_path.exists(): logger.debug("Using local storage: %s already exists", db_path) return False # if you're not in k8s you don't need controller storage if 'kubernetes' not in meta.series: logger.debug("Using local storage: not a kubernetes charm") return False # are we in a new enough Juju? cur_version = JujuVersion.from_environ() if cur_version.has_controller_storage(): logger.debug("Using controller storage: JUJU_VERSION=%s", cur_version) return True else: logger.debug("Using local storage: JUJU_VERSION=%s", cur_version) return False
def main(charm_class: ops.charm.CharmBase, use_juju_for_storage: bool = None): """Setup the charm and dispatch the observed event. The event name is based on the way this executable was called (argv[0]). Args: charm_class: your charm class. use_juju_for_storage: whether to use controller-side storage. If not specified then kubernetes charms that haven't previously used local storage and that are running on a new enough Juju default to controller-side storage, otherwise local storage is used. """ charm_dir = _get_charm_dir() model_backend = ops.model._ModelBackend() debug = ('JUJU_DEBUG' in os.environ) setup_root_logging(model_backend, debug=debug) logger.debug("Operator Framework %s up and running.", ops.__version__) dispatcher = _Dispatcher(charm_dir) dispatcher.run_any_legacy_hook() metadata = (charm_dir / 'metadata.yaml').read_text() actions_meta = charm_dir / 'actions.yaml' if actions_meta.exists(): actions_metadata = actions_meta.read_text() else: actions_metadata = None if not yaml.__with_libyaml__: logger.debug( 'yaml does not have libyaml extensions, using slower pure Python yaml loader' ) meta = ops.charm.CharmMeta.from_yaml(metadata, actions_metadata) model = ops.model.Model(meta, model_backend) charm_state_path = charm_dir / CHARM_STATE_FILE if use_juju_for_storage and not ops.storage.juju_backend_available(): # raise an exception; the charm is broken and needs fixing. msg = 'charm set use_juju_for_storage=True, but Juju version {} does not support it' raise RuntimeError(msg.format(JujuVersion.from_environ())) if use_juju_for_storage is None: use_juju_for_storage = _should_use_controller_storage( charm_state_path, meta) if use_juju_for_storage: if dispatcher.is_restricted_context(): # TODO: jam 2020-06-30 This unconditionally avoids running a collect metrics event # Though we eventually expect that juju will run collect-metrics in a # non-restricted context. Once we can determine that we are running collect-metrics # in a non-restricted context, we should fire the event as normal. logger.debug( '"%s" is not supported when using Juju for storage\n' 'see: https://github.com/canonical/operator/issues/348', dispatcher.event_name) # Note that we don't exit nonzero, because that would cause Juju to rerun the hook return store = ops.storage.JujuStorage() else: store = ops.storage.SQLiteStorage(charm_state_path) framework = ops.framework.Framework(store, charm_dir, meta, model) framework.set_breakpointhook() try: sig = inspect.signature(charm_class) try: sig.bind(framework) except TypeError: msg = ( "the second argument, 'key', has been deprecated and will be " "removed after the 0.7 release") warnings.warn(msg, DeprecationWarning) charm = charm_class(framework, None) else: charm = charm_class(framework) dispatcher.ensure_event_links(charm) # TODO: Remove the collect_metrics check below as soon as the relevant # Juju changes are made. Also adjust the docstring on # EventBase.defer(). # # Skip reemission of deferred events for collect-metrics events because # they do not have the full access to all hook tools. if not dispatcher.is_restricted_context(): framework.reemit() _emit_charm_event(charm, dispatcher.event_name) framework.commit() finally: framework.close()
def test_has_controller_storage(self): self.assertTrue(JujuVersion('2.8.0').has_controller_storage()) self.assertFalse(JujuVersion('2.7.9').has_controller_storage())
def test_is_dispatch_aware(self): self.assertTrue(JujuVersion('2.8.0').is_dispatch_aware()) self.assertFalse(JujuVersion('2.7.9').is_dispatch_aware())
def test_has_app_data(self): self.assertTrue(JujuVersion('2.8.0').has_app_data()) self.assertTrue(JujuVersion('2.7.0').has_app_data()) self.assertFalse(JujuVersion('2.6.9').has_app_data())
def test_comparison(self): test_cases = [ ("1.0.0", "1.0.0", False, True), ("01.0.0", "1.0.0", False, True), ("10.0.0", "9.0.0", False, False), ("1.0.0", "1.0.1", True, True), ("1.0.1", "1.0.0", False, False), ("1.0.0", "1.1.0", True, True), ("1.1.0", "1.0.0", False, False), ("1.0.0", "2.0.0", True, True), ("1.2-alpha1", "1.2.0", True, True), ("1.2-alpha2", "1.2-alpha1", False, False), ("1.2-alpha2.1", "1.2-alpha2", False, False), ("1.2-alpha2.2", "1.2-alpha2.1", False, False), ("1.2-beta1", "1.2-alpha1", False, False), ("1.2-beta1", "1.2-alpha2.1", False, False), ("1.2-beta1", "1.2.0", True, True), ("1.2.1", "1.2.0", False, False), ("2.0.0", "1.0.0", False, False), ("2.0.0.0", "2.0.0", False, True), ("2.0.0.0", "2.0.0.0", False, True), ("2.0.0.1", "2.0.0.0", False, False), ("2.0.1.10", "2.0.0.0", False, False), ("2.10.0", "2.8.0", False, False), ] for a, b, expected_strict, expected_weak in test_cases: with self.subTest(a=a, b=b): self.assertEqual( JujuVersion(a) < JujuVersion(b), expected_strict) self.assertEqual( JujuVersion(a) <= JujuVersion(b), expected_weak) self.assertEqual( JujuVersion(b) > JujuVersion(a), expected_strict) self.assertEqual( JujuVersion(b) >= JujuVersion(a), expected_weak) # Implicit conversion. self.assertEqual(JujuVersion(a) < b, expected_strict) self.assertEqual(JujuVersion(a) <= b, expected_weak) self.assertEqual(b > JujuVersion(a), expected_strict) self.assertEqual(b >= JujuVersion(a), expected_weak)