def render_config(*args): """Render the configuration for charm when all the interfaces are available. """ with charm.provide_charm_instance() as charm_class: charm_class.upgrade_if_available(args) with charm.provide_charm_instance() as charm_class: charm_class.render_with_interfaces(args) charm_class.assess_status() reactive.set_state("config.rendered")
def setup_amqp_req(amqp): """Use the amqp interface to request access to the amqp broker using our local configuration. """ amqp.request_access(username='******', vhost='openstack') with charm.provide_charm_instance() as instance: instance.assess_status()
def rejoin_instance(args): """Rejoin a given instance to the cluster. In the event an instance is removed from the cluster or fails to automatically rejoin, an instance can be rejoined to the cluster by an existing cluster member. Note: This action must be run on an instance that is already a member of the cluster. The action parameter, address, is the addresss of the instance that is being joined to the cluster. :param args: sys.argv :type args: sys.argv :side effect: Calls instance.rejoin_instance :returns: This function is called for its side effect :rtype: None :action param address: String address of the instance to be joined :action return: Dictionary with command output """ address = ch_core.hookenv.action_get("address") try: with charm.provide_charm_instance() as instance: output = instance.rejoin_instance(address) ch_core.hookenv.action_set({"output": output, "outcome": "Success"}) except subprocess.CalledProcessError as e: ch_core.hookenv.action_set({ "output": e.stderr.decode("UTF-8"), "return-code": e.returncode, "traceback": traceback.format_exc() }) ch_core.hookenv.action_fail("Rejoin instance failed")
def configure_resources(*args): """Create/discover resources for management of load balancer instances.""" if not reactive.is_flag_set('leadership.is_leader'): return ch_core.hookenv.action_fail('action must be run on the leader ' 'unit.') if not reactive.all_flags_set( 'identity-service.available', 'neutron-api.available', 'sdn-subordinate.available', 'amqp.available'): return ch_core.hookenv.action_fail('all required relations not ' 'available, please defer action' 'until deployment is complete.') identity_service = reactive.endpoint_from_flag( 'identity-service.available') try: (network, secgrp) = api_crud.get_mgmt_network( identity_service, create=reactive.is_flag_set('config.default.create-mgmt-network'), ) except api_crud.APIUnavailable as e: ch_core.hookenv.action_fail( 'Neutron API not available yet, deferring ' 'network creation/discovery. ("{}")'.format(e)) return if network and secgrp: leadership.leader_set({ 'amp-boot-network-list': network['id'], 'amp-secgroup-list': secgrp['id'] }) if reactive.is_flag_set('config.default.custom-amp-flavor-id'): # NOTE(fnordahl): custom flavor provided through configuration is # handled in the charm class configuration property. try: flavor = api_crud.get_nova_flavor(identity_service) except api_crud.APIUnavailable as e: ch_core.hookenv.action_fail('Nova API not available yet, ' 'deferring flavor ' 'creation. ("{}")'.format(e)) return else: leadership.leader_set({'amp-flavor-id': flavor.id}) amp_key_name = ch_core.hookenv.config('amp-ssh-key-name') if amp_key_name: identity_service = reactive.endpoint_from_flag( 'identity-service.available') api_crud.create_nova_keypair(identity_service, amp_key_name) # Set qutotas to unlimited try: api_crud.set_service_quotas_unlimited(identity_service) except api_crud.APIUnavailable as e: ch_core.hookenv.action_fail( 'Unbable to set quotas to unlimited: {}'.format(e)) # execute port setup for leader, the followers will execute theirs on # `leader-settings-changed` hook with charm.provide_charm_instance() as octavia_charm: api_crud.setup_hm_port(identity_service, octavia_charm) octavia_charm.render_all_configs() octavia_charm._assess_status()
def init_db(): """Run initial DB migrations when config is rendered.""" with charm.provide_charm_instance() as octavia_charm: octavia_charm.db_sync() octavia_charm.restart_all() reactive.set_state('db.synced') octavia_charm.assess_status()
def default_setup_endpoint_available(tls): """When the identity-service interface is available, this default handler switches on the SSL support. """ with charm.provide_charm_instance() as instance: instance.configure_ssl(tls) instance.assess_status()
def test_remove_config(self, unlink, exists, config): exists.return_value = True self.patch_object(keystone_ldap.ch_host, 'file_hash') reply = { 'ldap-server': 'myserver', 'ldap-user': '******', 'ldap-password': '******', 'ldap-suffix': 'suffix', 'domain-name': 'userdomain', } def mock_config(key=None): if key: return reply.get(key) return reply config.side_effect = mock_config with provide_charm_instance() as kldap_charm: # Ensure a basic level of function from render_config cf = keystone_ldap.DOMAIN_CONF.format(reply['domain-name']) kldap_charm.remove_config() exists.assert_called_once_with(cf) unlink.assert_called_once_with(cf)
def configure_pools(): local = reactive.endpoint_from_flag('ceph-local.available') remote = reactive.endpoint_from_flag('ceph-remote.available') with charm.provide_charm_instance() as charm_instance: for pool, attrs in charm_instance.eligible_pools(local.pools).items(): if not (charm_instance.mirror_pool_enabled(pool) and charm_instance.mirror_pool_has_peers(pool)): charm_instance.mirror_pool_enable(pool) pg_num = attrs['parameters'].get('pg_num', None) max_bytes = attrs['quota'].get('max_bytes', None) max_objects = attrs['quota'].get('max_objects', None) if 'erasure_code_profile' in attrs['parameters']: ec_profile = attrs['parameters'].get('erasure_code_profile', None) remote.create_erasure_pool(pool, erasure_profile=ec_profile, pg_num=pg_num, app_name='rbd', max_bytes=max_bytes, max_objects=max_objects) else: size = attrs['parameters'].get('size', None) remote.create_replicated_pool(pool, replicas=size, pg_num=pg_num, app_name='rbd', max_bytes=max_bytes, max_objects=max_objects)
def render(*args): hookenv.log("about to call the render_configs with {}".format(args)) with charm.provide_charm_instance() as ironic_charm: ironic_charm.render_with_interfaces(charm.optional_interfaces(args)) ironic_charm.configure_tls() ironic_charm.assess_status() reactive.set_state('config.complete')
def configure_neutron(): neutron = reactive.endpoint_from_flag('neutron-plugin.connected') ovsdb = reactive.endpoint_from_flag('ovsdb-cms.available') ch_core.hookenv.log('DEBUG: neutron_config_data="{}"'.format( neutron.neutron_config_data)) service_plugins = neutron.neutron_config_data.get('service_plugins', '').split(',') service_plugins = [svc for svc in service_plugins if svc not in ['router']] service_plugins.append('networking_ovn.l3.l3_ovn.OVNL3RouterPlugin') tenant_network_types = neutron.neutron_config_data.get( 'tenant_network_types', '').split(',') tenant_network_types.insert(0, 'geneve') def _split_if_str(s): _s = s or '' return _s.split() with charm.provide_charm_instance() as instance: options = instance.adapters_instance.options sections = { 'ovn': [ ('ovn_nb_connection', ','.join(ovsdb.db_nb_connection_strs)), ('ovn_nb_private_key', options.ovn_key), ('ovn_nb_certificate', options.ovn_cert), ('ovn_nb_ca_cert', options.ovn_ca_cert), ('ovn_sb_connection', ','.join(ovsdb.db_sb_connection_strs)), ('ovn_sb_private_key', options.ovn_key), ('ovn_sb_certificate', options.ovn_cert), ('ovn_sb_ca_cert', options.ovn_ca_cert), ('ovn_l3_scheduler', options.ovn_l3_scheduler), ('ovn_metadata_enabled', options.ovn_metadata_enabled), ('enable_distributed_floating_ip', options.enable_distributed_floating_ip), ('dns_servers', ','.join(_split_if_str(options.dns_servers))), ('dhcp_default_lease_time', options.dhcp_default_lease_time), ('ovn_dhcp4_global_options', ','.join(_split_if_str(options.ovn_dhcp4_global_options))), ('ovn_dhcp6_global_options', ','.join(_split_if_str(options.ovn_dhcp6_global_options))), ], 'ml2_type_geneve': [ ('vni_ranges', ','.join(_split_if_str(options.geneve_vni_ranges))), ('max_header_size', '38'), ], } neutron.configure_plugin( 'ovn', service_plugins=','.join(service_plugins), mechanism_drivers='ovn', tenant_network_types=','.join(tenant_network_types), subordinate_configuration={ 'neutron-api': { '/etc/neutron/plugins/ml2/ml2_conf.ini': { 'sections': sections, }, }, }, ) instance.assess_status()
def db_monitor_respond(): """Response to db-monitor relation changed.""" ch_core.hookenv.log("db-monitor connected", ch_core.hookenv.DEBUG) db_monitor = reactive.endpoint_from_flag("db-monitor.connected") # get related application name = user username = related_app = ch_core.hookenv.remote_service_name() # get or create db-monitor user password db_monitor_stored_passwd_key = "db-monitor.{}.passwd".format(related_app) password = leadership.leader_get(db_monitor_stored_passwd_key) if not password: password = ch_core.host.pwgen() leadership.leader_set({db_monitor_stored_passwd_key: password}) # provide relation data with charm.provide_charm_instance() as instance: # NOTE (rgildein): Create a custom user with administrator privileges, # but read-only access. if not instance.create_cluster_user(db_monitor.relation_ip, username, password, True): ch_core.hookenv.log("db-monitor user was not created.", ch_core.hookenv.WARNING) return db_monitor.provide_access( port=instance.cluster_port, user=username, password=password, ) instance.assess_status()
def plugin_info_barbican_publish(): barbican = reactive.endpoint_from_flag('endpoint.secrets.joined') secrets_storage = reactive.endpoint_from_flag('secrets-storage.available') with charm.provide_charm_instance() as barbican_vault_charm: if secrets_storage.vault_ca: ch_core.hookenv.log('Installing vault CA certificate') barbican_vault_charm.install_ca_cert(secrets_storage.vault_ca) ch_core.hookenv.log('Retrieving secret-id from vault ({})'.format( secrets_storage.vault_url), level=ch_core.hookenv.INFO) secret_id = vault_utils.retrieve_secret_id(secrets_storage.vault_url, secrets_storage.unit_token) vault_data = { 'approle_role_id': secrets_storage.unit_role_id, 'approle_secret_id': secret_id, 'vault_url': secrets_storage.vault_url, 'kv_mountpoint': barbican_vault_charm.secret_backend_name, } if barbican_vault_charm.installed_ca_name: vault_data.update( {'ssl_ca_crt_file': barbican_vault_charm.installed_ca_name}) ch_core.hookenv.log('Publishing vault plugin info to barbican', level=ch_core.hookenv.INFO) barbican.publish_plugin_info('vault', vault_data) reactive.clear_flag('endpoint.secrets-storage.changed')
def configure_ceph(ceph): with charm.provide_charm_instance() as charm_instance: key = ceph.key if key and isinstance(key, str): charm_instance.configure_ceph_keyring(key) else: hookenv.log("No ceph keyring data is available")
def enable_metadata(): nova_compute = reactive.endpoint_from_flag('nova-compute.connected') nova_compute.publish_shared_secret() with charm.provide_charm_instance() as charm_instance: ch_core.hookenv.log('DEBUG: {} {} {} {}'.format( charm_instance, charm_instance.packages, charm_instance.services, charm_instance.restart_map), level=ch_core.hookenv.INFO) charm_instance.enable_metadata() with charm.provide_charm_instance() as charm_instance: ch_core.hookenv.log('DEBUG: {} {} {} {}'.format( charm_instance, charm_instance.packages, charm_instance.services, charm_instance.restart_map), level=ch_core.hookenv.INFO) charm_instance.install() charm_instance.assess_status()
def render_config_with_certs(amqp, keystone, shared_db, certs): with charm.provide_charm_instance() as magnum_charm: magnum_charm.configure_tls(certs) magnum_charm.render_with_interfaces( [amqp, keystone, shared_db, certs]) magnum_charm.assess_status() reactive.set_state('config.complete')
def enable_openstack(): reactive.set_flag('charm.ovn-chassis.enable-openstack') nova_compute = reactive.endpoint_from_flag('nova-compute.connected') nova_compute.publish_shared_secret() with charm.provide_charm_instance() as charm_instance: charm_instance.install() charm_instance.assess_status()
def config_changed(): with charm.provide_charm_instance() as charm_instance: charm_instance.upgrade_if_available([ reactive.endpoint_from_flag('ceph-local.available'), reactive.endpoint_from_flag('ceph-remote.available'), ]) charm_instance.assess_status()
def maybe_enable_ovn_driver(): ovsdb = reactive.endpoint_from_flag('ovsdb-subordinate.available') if ovsdb.ovn_configured: reactive.set_flag('charm.octavia.enable-ovn-driver') with charm.provide_charm_instance() as charm_instance: charm_instance.install() charm_instance.assess_status()
def render(): ovsdb_peer = reactive.endpoint_from_flag('ovsdb-peer.available') with charm.provide_charm_instance() as ovn_charm: ovn_charm.render_with_interfaces([ovsdb_peer]) # NOTE: The upstream ctl scripts currently do not support passing # multiple connection strings to the ``ovsdb-tool join-cluster`` # command. # # This makes it harder to bootstrap a cluster in the event # one of the units are not available. Thus the charm performs the # ``join-cluster`` command expliclty before handing off to the # upstream scripts. # # Replace this with functionality in ``ovn-ctl`` when support has been # added upstream. ovn_charm.join_cluster( '/var/lib/openvswitch/ovnnb_db.db', 'OVN_Northbound', ovsdb_peer.db_connection_strs((ovsdb_peer.cluster_local_addr, ), ovsdb_peer.db_nb_cluster_port), ovsdb_peer.db_connection_strs(ovsdb_peer.cluster_remote_addrs, ovsdb_peer.db_nb_cluster_port)) ovn_charm.join_cluster( '/var/lib/openvswitch/ovnsb_db.db', 'OVN_Southbound', ovsdb_peer.db_connection_strs((ovsdb_peer.cluster_local_addr, ), ovsdb_peer.db_sb_cluster_port), ovsdb_peer.db_connection_strs(ovsdb_peer.cluster_remote_addrs, ovsdb_peer.db_sb_cluster_port)) if ovn_charm.enable_services(): reactive.set_flag('config.rendered') ovn_charm.assess_status()
def keystone_departed(): """ Service restart should be handled on the keystone side in this case. """ with charm.provide_charm_instance() as charm_instance: charm_instance.remove_config()
def add_instance(args): """Add an instance to the cluster. If a new instance is not able to be joined to the cluster, this action will configure and add the unit to the cluster. :param args: sys.argv :type args: sys.argv :side effect: Calls instance.configure_and_add_instance :returns: This function is called for its side effect :rtype: None :action param address: String address of the instance to be joined :action return: Dictionary with command output """ # Note: Due to issues/# reactive does not initiate Endpoints during an # action execution. This is here to work around that until the issue is # resolved. reactive.Endpoint._startup() address = ch_core.hookenv.action_get("address") try: with charm.provide_charm_instance() as instance: output = instance.configure_and_add_instance(address) ch_core.hookenv.action_set({"output": output, "outcome": "Success"}) except subprocess.CalledProcessError as e: ch_core.hookenv.action_set({ "output": e.stderr.decode("UTF-8"), "return-code": e.returncode, "traceback": traceback.format_exc() }) ch_core.hookenv.action_fail("Add instance failed")
def configure_instances_for_clustering(cluster): """Configure cluster peers for clustering. Prepare peers to be added to the cluster. :param cluster: Cluster interface :type cluster: MySQLInnoDBClusterPeers object """ ch_core.hookenv.log("Configuring instances for clustering.", "DEBUG") with charm.provide_charm_instance() as instance: for unit in cluster.all_joined_units: if unit.received['unit-configure-ready']: instance.configure_instance( unit.received['cluster-address']) instance.add_instance_to_cluster( unit.received['cluster-address']) # Verify all are configured for unit in cluster.all_joined_units: if not reactive.is_flag_set( "leadership.set.cluster-instance-configured-{}" .format(unit.received['cluster-address'])): return # All have been configured leadership.leader_set( {"cluster-instances-configured": True}) instance.assess_status()
def set_cluster_option(args): """Set cluster option. Set an option on the InnoDB cluster. Action parameter key is the name of the option and action parameter value is the value to be set. :param args: sys.argv :type args: sys.argv :side effect: Calls instance.set_cluster_option :returns: This function is called for its side effect :rtype: None :action param key: String option name :action param value: String option value :action return: Dictionary with command output """ key = ch_core.hookenv.action_get("key") value = ch_core.hookenv.action_get("value") try: with charm.provide_charm_instance() as instance: output = instance.set_cluster_option(key, value) ch_core.hookenv.action_set({"output": output, "outcome": "Success"}) except subprocess.CalledProcessError as e: ch_core.hookenv.action_set({ "output": e.stderr.decode("UTF-8"), "return-code": e.returncode, "traceback": traceback.format_exc() }) ch_core.hookenv.action_fail("Set cluster option failed")
def remove_instance(args): """Remove an instance from the cluster. This action cleanly removes an instance from the cluster. If an instance has died and is unrecoverable it shows up in metadata as MISSING. This action will remove an instance from the metadata using the force option even if it is unreachable. :param args: sys.argv :type args: sys.argv :side effect: Calls instance.remove_instance :returns: This function is called for its side effect :rtype: None :action param address: String address of the instance to be removed :action param force: Boolean force removal of missing instance :action return: Dictionary with command output """ address = ch_core.hookenv.action_get("address") force = ch_core.hookenv.action_get("force") try: with charm.provide_charm_instance() as instance: output = instance.remove_instance(address, force=force) ch_core.hookenv.action_set({"output": output, "outcome": "Success"}) except subprocess.CalledProcessError as e: ch_core.hookenv.action_set({ "output": e.stderr.decode("UTF-8"), "return-code": e.returncode, "traceback": traceback.format_exc() }) ch_core.hookenv.action_fail("Remove instance failed")
def restore_mysqldump(args): """Restore a mysqldump backup. Execute mysqldump of the database(s). The mysqldump action will take in the databases action parameter. If the databases parameter is unset all databases will be dumped, otherwise only the named databases will be dumped. The action will use the basedir action parameter to dump the database into the base directory. A successful mysqldump backup will set the action results key, mysqldump-file, with the full path to the dump file. :param args: sys.argv :type args: sys.argv :side effect: Calls instance.restore_mysqldump :returns: This function is called for its side effect :rtype: None :action param dump-file: Path to mysqldump file to restore. :action return: """ dump_file = ch_core.hookenv.action_get("dump-file") try: with charm.provide_charm_instance() as instance: instance.restore_mysqldump(dump_file) ch_core.hookenv.action_set({"outcome": "Success"}) except subprocess.CalledProcessError as e: ch_core.hookenv.action_set({ "output": e.stderr.decode("UTF-8"), "return-code": e.returncode, "traceback": traceback.format_exc() }) ch_core.hookenv.action_fail( "Restore mysqldump of {} failed".format(dump_file))
def setup_hm_port(): """Create a per unit Neutron and OVS port for Octavia Health Manager. This is used to plug the unit into the overlay network for direct communication with the octavia managed load balancer instances running within the deployed cloud. """ neutron_ovs = reactive.endpoint_from_flag('neutron-openvswitch.connected') ovsdb = reactive.endpoint_from_flag('ovsdb-subordinate.available') host_id = neutron_ovs.host() if neutron_ovs else ovsdb.chassis_name with charm.provide_charm_instance() as octavia_charm: identity_service = reactive.endpoint_from_flag( 'identity-service.available') try: if api_crud.setup_hm_port( identity_service, octavia_charm, host_id=host_id): # trigger config render to make systemd-networkd bring up # automatic IP configuration of the new port right now. reactive.set_flag('config.changed') if reactive.is_flag_set('charm.octavia.action_setup_hm_port'): reactive.clear_flag('charm.octavia.action_setup_hm_port') except api_crud.APIUnavailable as e: ch_core.hookenv.log('Neutron API not available yet, deferring ' 'port discovery. ("{}")' .format(e), level=ch_core.hookenv.DEBUG) return
def configure_websso(websso_fid_sp): with charm.provide_charm_instance() as charm_instance: if charm_instance.configuration_complete(): # publish config options for all remote units of a given rel options = charm_instance.options websso_fid_sp.publish(options.protocol_name, options.idp_name, options.user_facing_name)
def default_config_changed(): """Default handler for config.changed state from reactive. Just see if our status has changed. This is just to clear any errors that may have got stuck due to missing async handlers, etc. """ with charm.provide_charm_instance() as instance: instance.config_changed() instance.assess_status()
def create_ea_definitions(): principal_neutron = \ endpoint_from_flag('neutron.available') if principal_neutron.principal_charm_state() == "True": with provide_charm_instance() as charm_class: charm_class.create_ea_definitions() status_set('active', 'Unit is ready') set_flag('create-ea-definitions.done')
def configure_neutron(principle): with provide_charm_instance() as charm_class: config = charm_class.get_neutron_conf() principal_neutron = \ endpoint_from_flag('neutron.available') principal_neutron.configure_principal(config) clear_flag('create-ea-definitions.done') set_flag('neutron.configured')
def config_changed(): if reactive.is_flag_set('leadership.is_leader'): with charm.provide_charm_instance() as instance: instance.render_all_configs() instance.wait_until_cluster_available() if reactive.is_flag_set('config.changed.auto-rejoin-tries'): instance.set_cluster_option("autoRejoinTries", instance.options.auto_rejoin_tries) else: with charm.provide_charm_instance() as instance: try: instance.wait_until_cluster_available() except Exception: ch_core.hookenv.log("Cluster was not availble as expected.", "WARNING") ch_core.hookenv.log("Non-leader requst to restart.", "DEBUG") coordinator.acquire('config-changed-restart')
def update_unit_acls(args): """Update IP allow list on each node in cluster. Update IP allow list on each node in cluster. At present this can only be done when replication is stopped. """ with charm.provide_charm_instance() as instance: instance.update_acls()
def default_setup_certificates(tls): """When the identity-service interface is available, this default handler switches on the SSL support. """ with charm.provide_charm_instance() as instance: for cn, req in instance.get_certificate_requests().items(): tls.add_request_server_cert(cn, req['sans']) tls.request_server_certs() instance.assess_status()
def default_amqp_connection(amqp): """Handle the default amqp connection. This requires that the charm implements get_amqp_credentials() to provide a tuple of the (user, vhost) for the amqp server """ with charm.provide_charm_instance() as instance: user, vhost = instance.get_amqp_credentials() amqp.request_access(username=user, vhost=vhost) instance.assess_status()
def render_stuff(*args): """Render the configuration for Barbican when all the interfaces are available. Note that the HSM interface is optional and thus is only used if it is available. """ hookenv.log("about to call the render_configs with {}".format(args)) with charm.provide_charm_instance() as barbican_charm: barbican_charm.render_with_interfaces( charm.optional_interfaces(args, 'hsm.available')) barbican_charm.assess_status()
def default_setup_database(database): """Handle the default database connection setup This requires that the charm implements get_database_setup() to provide a list of dictionaries; [{'database': ..., 'username': ..., 'hostname': ..., 'prefix': ...}] The prefix can be missing: it defaults to None. """ with charm.provide_charm_instance() as instance: for db in instance.get_database_setup(): database.configure(**db) instance.assess_status()
def default_setup_endpoint_connection(keystone): """When the keystone interface connects, register this unit into the catalog. This is the default handler, and calls on the charm class to provide the endpoint information. If multiple endpoints are needed, then a custom endpoint handler will be needed. """ with charm.provide_charm_instance() as instance: keystone.register_endpoints(instance.service_type, instance.region, instance.public_url, instance.internal_url, instance.admin_url) instance.assess_status()
def default_install(): """Provide a default install handler The instance automagically becomes the derived OpenStackCharm instance. The kv() key charmers.openstack-release-version' is used to cache the release being used for this charm. It is determined by the default_select_release() function below, unless this is overridden by the charm author """ unitdata.kv().unset(defaults.OPENSTACK_RELEASE_KEY) with charm.provide_charm_instance() as instance: instance.install() reactive.set_state('charm.installed')
def cluster_connected(hacluster): """Configure HA resources in corosync.""" with charm.provide_charm_instance() as barbican_charm: barbican_charm.configure_ha_resources(hacluster) barbican_charm.assess_status()
def run_default_upgrade_charm(): with charm.provide_charm_instance() as instance: instance.upgrade_charm() reactive.remove_state('run-default-upgrade-charm')
def run_default_update_status(): with charm.provide_charm_instance() as instance: instance.assess_status() reactive.remove_state('run-default-update-status')
def run_storage_backend(): with charm.provide_charm_instance() as instance: instance.send_storage_backend_data()
def default_pre_series_upgrade(): """Default handler for pre-series-upgrade. """ with charm.provide_charm_instance() as instance: instance.series_upgrade_prepare()
def default_post_series_upgrade(): """Default handler for post-series-upgrade. """ with charm.provide_charm_instance() as instance: instance.series_upgrade_complete()