def request_integration(): hookenv.status_set('maintenance', 'requesting cloud integration') kube_control = endpoint_from_flag('kube-control.cluster_tag.available') cluster_tag = kube_control.get_cluster_tag() if is_state('endpoint.aws.joined'): cloud = endpoint_from_flag('endpoint.aws.joined') cloud.tag_instance({ 'kubernetes.io/cluster/{}'.format(cluster_tag): 'owned', }) cloud.tag_instance_security_group({ 'kubernetes.io/cluster/{}'.format(cluster_tag): 'owned', }) cloud.tag_instance_subnet({ 'kubernetes.io/cluster/{}'.format(cluster_tag): 'owned', }) cloud.enable_object_storage_management(['kubernetes-*']) elif is_state('endpoint.gcp.joined'): cloud = endpoint_from_flag('endpoint.gcp.joined') cloud.label_instance({ 'k8s-io-cluster-name': cluster_tag, }) cloud.enable_object_storage_management() cloud.enable_instance_inspection() cloud.enable_dns_management() set_state('kubernetes-worker.cloud-request-sent') hookenv.status_set('waiting', 'waiting for cloud integration')
def _write_gcp_snap_config(component): # gcp requires additional credentials setup gcp = endpoint_from_flag('endpoint.gcp.ready') creds_path = _gcp_creds_path(component) with creds_path.open('w') as fp: os.fchmod(fp.fileno(), 0o600) fp.write(gcp.credentials) # create a cloud-config file that sets token-url to nil to make the # services use the creds env var instead of the metadata server, as # well as making the cluster multizone cloud_config_path = _cloud_config_path(component) cloud_config_path.write_text('[Global]\n' 'token-url = nil\n' 'multizone = true\n') daemon_env_path = _daemon_env_path(component) if daemon_env_path.exists(): daemon_env = daemon_env_path.read_text() if not daemon_env.endswith('\n'): daemon_env += '\n' else: daemon_env = '' if gcp_creds_env_key not in daemon_env: daemon_env += '{}={}\n'.format(gcp_creds_env_key, creds_path) daemon_env_path.parent.mkdir(parents=True, exist_ok=True) daemon_env_path.write_text(daemon_env)
def request_integration(): kube_control = endpoint_from_flag('kube-control.cluster_tag.available') hookenv.status_set('maintenance', 'requesting aws integration') aws = endpoint_from_flag('endpoint.aws.joined') cluster_tag = kube_control.get_cluster_tag() aws.tag_instance({ 'KubernetesCluster': cluster_tag, }) aws.tag_instance_security_group({ 'KubernetesCluster': cluster_tag, }) aws.tag_instance_subnet({ 'KubernetesCluster': cluster_tag, }) aws.enable_instance_inspection() aws.enable_dns_management() aws.enable_object_storage_management(['kubernetes-*']) set_state('kubernetes-worker.aws-request-sent') hookenv.status_set('waiting', 'waiting for aws integration')
def request_client_cert(common_name, sans=None, crt_path=None, key_path=None): tls = endpoint_from_flag('certificates.available') tls.request_client_cert(common_name, sans) if not crt_path and not key_path: return kv = unitdata.kv() cert_paths = kv.get('layer.tls-client.cert-paths', {}) cert_paths.setdefault('client', {})[common_name] = { 'crt': str(crt_path), 'key': str(key_path), } kv.set('layer.tls-client.cert-paths', cert_paths)
def configure(): layer.status.maintenance("Configuring ui container") try: mysql = endpoint_from_flag("mysql.available") nbi = endpoint_from_flag("nbi.ready") nbi_unit = nbi.nbis()[0] nbi_host = "{}".format(nbi_unit["host"]) spec = make_pod_spec( mysql.host(), mysql.port(), mysql.user(), mysql.password(), mysql.root_password(), nbi_host, ) log("set pod spec:\n{}".format(spec)) pod_spec_set(spec) set_flag("ui-k8s.configured") except Exception as e: layer.status.blocked("k8s spec failed to deploy: {}".format(e))
def update_status_subscribers(): endpoint = endpoint_from_flag('endpoint.helm.status-update') subs = endpoint.get_status_update_subscribers() if not subs: return previous_requests = unitdata.kv().get('live-releases', {}) needed_requests = previous_requests for unit in previous_requests: if unit not in subs: del needed_requests[unit] live_subs = update_release_info(needed_requests) endpoint.send_status(live_subs)
def request_database(): """When connection established to postgres, request a database. """ status_set('maintenance', 'Requesting PostgreSQL database') pgsql = endpoint_from_flag('pgsql.connected') pgsql.set_database('snap-db-redis') log('Database Available') status_set('active', 'pgsql.requested') set_flag('snap-db-redis.pgsql.requested')
def request_server_certificates(): '''Send the data that is required to create a server certificate for this server.''' website = endpoint_from_flag('website.available') # Use the public ip of this unit as the Common Name for the certificate. common_name = hookenv.unit_public_ip() bind_ips = kubernetes_common.get_bind_addrs(ipv4=True, ipv6=True) # Create SANs that the tls layer will add to the server cert. sans = [ # The CN field is checked as a hostname, so if it's an IP, it # won't match unless also included in the SANs as an IP field. common_name, kubernetes_common.get_ingress_address(website.endpoint_name), socket.gethostname(), socket.getfqdn(), ] + bind_ips forced_lb_ips = hookenv.config('loadbalancer-ips').split() if forced_lb_ips: sans.extend(forced_lb_ips) else: hacluster = endpoint_from_flag('ha.connected') if hacluster: vips = hookenv.config('ha-cluster-vip').split() dns_record = hookenv.config('ha-cluster-dns') if vips: sans.extend(vips) elif dns_record: sans.append(dns_record) # maybe they have extra names they want as SANs extra_sans = hookenv.config('extra_sans') if extra_sans and not extra_sans == "": sans.extend(extra_sans.split()) # Request a server cert with this information. tls_client.request_server_cert(common_name, sorted(set(sans)), crt_path=server_crt_path, key_path=server_key_path)
def configure(): cleanup() os.makedirs(CALICO_UPGRADE_DIR) # Extract calico-upgrade resource architecture = arch() if architecture == 'amd64': resource_name = 'calico-upgrade' else: resource_name = 'calico-upgrade-' + architecture archive = resource_get(resource_name) if not archive: message = 'Missing calico-upgrade resource' status_set('blocked', message) raise ResourceMissing(message) check_call(['tar', '-xvf', archive, '-C', CALICO_UPGRADE_DIR]) # Configure calico-upgrade, etcd2 (data source) etcd = endpoint_from_flag('etcd.available') etcd_endpoints = etcd.get_connection_string() etcd2_data = { 'apiVersion': 'v1', 'kind': 'calicoApiConfig', 'metadata': None, 'spec': { 'datastoreType': 'etcdv2', 'etcdEndpoints': etcd_endpoints, 'etcdKeyFile': ETCD_KEY_PATH, 'etcdCertFile': ETCD_CERT_PATH, 'etcdCACertFile': ETCD_CA_PATH } } with open(ETCD2_DATA_PATH, 'w') as f: yaml.dump(etcd2_data, f) # Configure calico-upgrade, etcd3 (data destination) etcd3_data = { 'apiVersion': 'projectcalico.org/v3', 'kind': 'CalicoAPIConfig', 'metadata': None, 'spec': { 'datastoreType': 'etcdv3', 'etcdEndpoints': etcd_endpoints, 'etcdKeyFile': ETCD_KEY_PATH, 'etcdCertFile': ETCD_CERT_PATH, 'etcdCACertFile': ETCD_CA_PATH } } with open(ETCD3_DATA_PATH, 'w') as f: yaml.dump(etcd3_data, f)
def configure_registry(): """ Add docker registry config when present. :return: None """ registry = endpoint_from_flag('endpoint.docker-registry.ready') netloc = registry.registry_netloc # handle tls data cert_subdir = netloc cert_dir = os.path.join(CERTIFICATE_DIRECTORY, cert_subdir) insecure_opt = {'insecure-registry': netloc} if registry.has_tls(): # ensure the CA that signed our registry cert is trusted install_ca_cert(registry.tls_ca, name='juju-docker-registry') # remove potential insecure docker opts related to this registry manage_docker_opts(insecure_opt, remove=True) manage_registry_certs(cert_dir, remove=False) else: manage_docker_opts(insecure_opt, remove=False) manage_registry_certs(cert_dir, remove=True) # handle auth data if registry.has_auth_basic(): hookenv.log('Logging into docker registry: {}.'.format(netloc)) cmd = [ 'docker', 'login', netloc, '-u', registry.basic_user, '-p', registry.basic_password ] try: check_output(cmd, stderr=subprocess.STDOUT) except CalledProcessError as e: if b'http response' in e.output.lower(): # non-tls login with basic auth will error like this: # Error response ... server gave HTTP response to HTTPS client msg = 'docker login requires a TLS-enabled registry' elif b'unauthorized' in e.output.lower(): # invalid creds will error like this: # Error response ... 401 Unauthorized msg = 'Incorrect credentials for docker registry' else: msg = 'docker login failed, see juju debug-log' hookenv.status_set('blocked', msg) else: hookenv.log('Disabling auth for docker registry: {}.'.format(netloc)) # NB: it's safe to logout of a registry that was never logged in check_call(['docker', 'logout', netloc]) # NB: store our netloc so we can clean up if the registry goes away DB.set('registry_netloc', netloc) set_state('docker.registry.configured')
def generate_openstack_cloud_config(): # openstack requires additional credentials setup openstack = endpoint_from_flag('endpoint.openstack.ready') lines = [ '[Global]', 'auth-url = {}'.format(openstack.auth_url), 'region = {}'.format(openstack.region), 'username = {}'.format(openstack.username), 'password = {}'.format(openstack.password), 'tenant-name = {}'.format(openstack.project_name), 'domain-name = {}'.format(openstack.user_domain_name), ] if openstack.endpoint_tls_ca: lines.append('ca-file = /etc/config/endpoint-ca.cert') lines.extend([ '', '[LoadBalancer]', ]) if openstack.has_octavia in (True, None): # Newer integrator charm will detect whether underlying OpenStack has # Octavia enabled so we can set this intelligently. If we're still # related to an older integrator, though, default to assuming Octavia # is available. lines.append('use-octavia = true') if openstack.subnet_id: lines.append('subnet-id = {}'.format(openstack.subnet_id)) if openstack.floating_network_id: lines.append('floating-network-id = {}'.format( openstack.floating_network_id)) if openstack.lb_method: lines.append('lb-method = {}'.format(openstack.lb_method)) if openstack.manage_security_groups: lines.append('manage-security-groups = {}'.format( openstack.manage_security_groups)) if any([ openstack.bs_version, openstack.trust_device_path, openstack.ignore_volume_az ]): lines.append('') lines.append('[BlockStorage]') if openstack.bs_version is not None: lines.append('bs-version = {}'.format(openstack.bs_version)) if openstack.trust_device_path is not None: lines.append('trust-device-path = {}'.format( openstack.trust_device_path)) if openstack.ignore_volume_az is not None: lines.append('ignore-volume-az = {}'.format( openstack.ignore_volume_az)) return '\n'.join(lines) + '\n'
def publish_stonith_info(): """Provide remote hacluster with info for including remote in cluster""" remote_ip = hookenv.network_get_primary_address('pacemaker-remote') remote_hostname = socket.getfqdn() if hookenv.config('enable-stonith'): stonith_hostname = remote_hostname else: stonith_hostname = None remote = reactive.endpoint_from_flag('endpoint.pacemaker-remote.joined') remote.publish_info(remote_hostname=remote_hostname, remote_ip=remote_ip, enable_resources=hookenv.config('enable-resources'), stonith_hostname=stonith_hostname)
def safely_join_cohort(): """Coordinate the rollout of snap refreshes. When cohort keys change, grab a lock so that only 1 unit in the application joins the new cohort at a time. This allows us to roll out snap refreshes without risking all units going down at once. """ kube_control = endpoint_from_flag("kube-control.cohort_keys.available") cohort_keys = kube_control.cohort_keys if is_data_changed("master-cohorts", cohort_keys): clear_flag("kubernetes-worker.cohorts.joined") charms.coordinator.acquire("cohort")
def send_config(): layer.status.maintenance("Sending RO configuration") try: ro = endpoint_from_flag("ro.joined") if ro: service_ip = get_service_ip("ro") if service_ip: ro.send_connection( service_ip, get_ro_port(), ) clear_flag("ro.joined") except Exception as e: log("Fail sending RO configuration: {}".format(e))
def create_and_config_db(): pgsql = endpoint_from_flag('postgresql.master.available') render(source='text-file.tmpl', target='/var/snap/flasksnap/common/config/text-file.txt', owner='root', perms=0o775, context={'db': pgsql.master}) set_flag('flaskapp.db_configured') db = pgsql.master # URI_STRING = "postgresql://{}:{}@localhost{}/{}".format(str(db.user), str(db.password), str(db.name)) # check_call("snap set flasksnap snap.mode='{}'".format(URI_STRING).split()) # check_call("snap set flasksnap snap.mode='apples'".split()) status_set('active', 'Ready: file rendered')
def test_xcp(dea, *_): dea.return_value = [] kubernetes_worker.db.set("credentials", defaultdict(str)) endpoint_from_flag().has_xcp = False endpoint_from_name().services.return_value = [{ "hosts": [{ "hostname": "foo", "port": "80" }] }] kubernetes_common.get_unit_number.return_value = 0 kubernetes_worker.start_worker() assert kubernetes_worker.configure_kubelet.called assert kubernetes_worker.configure_kubelet.call_args == (ANY, { "has_xcp": False }) endpoint_from_flag().has_xcp = True kubernetes_worker.start_worker() assert kubernetes_worker.configure_kubelet.call_args == (ANY, { "has_xcp": True })
def acquire_kibana_host_port_via_relation(): """Get kibana host:port from relation, set to leader. """ status_set('maintenance', 'Acquiring kibana host:port ...') endpoint = endpoint_from_flag('endpoint.kibana-host-port.available') kibana_host_port = endpoint.list_unit_data()[0] host = kibana_host_port['host'] port = kibana_host_port['port'] charms.leadership.leader_set(monitoring_kibana_host_port=f"{host}:{port}") es_active_status()
def render_pgsql_config_and_share_details(): pgsql_endpoint = endpoint_from_flag('pgsqldb.master.available') # fill dictionary db_details['technology'] = "postgresql" db_details['password'] = pgsql_endpoint.master['password'] db_details['dbname'] = pgsql_endpoint.master['dbname'] db_details['host'] = pgsql_endpoint.master['host'] db_details['user'] = pgsql_endpoint.master['user'] db_details['port'] = pgsql_endpoint.master['port'] db_details['concrete'] = True # On own apache render( 'gdb-config.j2', '/var/www/generic-database/gdb-config.html', { 'db_master': pgsql_endpoint.master, 'db_pass': pgsql_endpoint.master['password'], 'db_dbname': pgsql_endpoint.master['dbname'], 'db_host': pgsql_endpoint.master['host'], 'db_user': pgsql_endpoint.master['user'], 'db_port': pgsql_endpoint.master['port'], }) # share details to consumer-app gdb_endpoint = endpoint_from_flag( 'endpoint.generic-database.postgresql.requested') gdb_endpoint.share_details( "postgresql", pgsql_endpoint.master['host'], pgsql_endpoint.master['dbname'], pgsql_endpoint.master['user'], pgsql_endpoint.master['password'], pgsql_endpoint.master['port'], ) clear_flag('endpoint.generic-database.postgresql.requested') set_flag('endpoint.generic-database.postgresql.available') set_flag('endpoint.generic-database.concrete') set_flag('restart-app')
def config_changed(): ceph_mds = reactive.endpoint_from_flag('ceph-mds.pools.available') with charm.provide_charm_instance() as cephfs_charm: cephfs_charm.configure_ceph_keyring(ceph_mds.mds_key()) cephfs_charm.render_with_interfaces([ceph_mds]) if reactive.is_flag_set('config.changed.source'): # update system source configuration and check for upgrade cephfs_charm.install() cephfs_charm.upgrade_if_available([ceph_mds]) reactive.clear_flag('config.changed.source') reactive.set_flag('cephfs.configured') reactive.set_flag('config.rendered') cephfs_charm.assess_status()
def configure_certificates(): """When the certificates interface is available, this default handler updates on-disk certificates and switches on the TLS support. """ tls = reactive.endpoint_from_flag('certificates.available') with charm.provide_charm_instance() as instance: instance.configure_tls(tls) # make charms.openstack required relation check happy reactive.set_flag('certificates.connected') for flag in 'certificates.ca.changed', 'certificates.certs.changed': if reactive.is_flag_set(flag): reactive.clear_flag(flag) instance.assess_status()
def calicoctl(*args): cmd = ['/opt/calicoctl/calicoctl'] + list(args) etcd = endpoint_from_flag('etcd.available') env = os.environ.copy() env['ETCD_ENDPOINTS'] = etcd.get_connection_string() env['ETCD_KEY_FILE'] = ETCD_KEY_PATH env['ETCD_CERT_FILE'] = ETCD_CERT_PATH env['ETCD_CA_CERT_FILE'] = ETCD_CA_PATH try: return check_output(cmd, env=env, stderr=STDOUT) except CalledProcessError as e: log(e.output) raise
def configure(): layer.status.maintenance("Configuring pol container") try: kafka = endpoint_from_flag("kafka.ready") mongo = endpoint_from_flag("mongo.ready") if kafka and mongo: kafka_units = kafka.kafkas() kafka_unit = kafka_units[0] mongo_uri = mongo.connection_string() log("Mongo URI: {}".format(mongo_uri)) if mongo_uri and kafka_unit["host"]: spec = make_pod_spec(kafka_unit["host"], kafka_unit["port"], mongo_uri) log("set pod spec:\n{}".format(spec)) pod_spec_set(spec) set_flag("pol-k8s.configured") except Exception as e: layer.status.blocked("k8s spec failed to deploy: {}".format(e))
def provide_client_relation_data(): ''' Set client relation data. (only 'master' or 'all' type nodes should run this code) ''' status_set( 'maintenance', 'Client relation joined, sending elasticsearch cluster data to client.' ) if ES_NODE_TYPE not in ['master', 'all']: log('SOMETHING BAD IS HAPPENING - wronge nodetype for client relation') status_set( 'blocked', 'Cannot make relation to master - ' 'wrong node-typeforclient relation, please remove relation') return else: endpoint_from_flag('endpoint.client.joined').configure( ES_PUBLIC_INGRESS_ADDRESS, ES_HTTP_PORT, ES_CLUSTER_NAME) es_active_status()
def provide_application_details(): ''' re-use the nginx layer website relation to relay the hostname/port to any consuming kubernetes-workers, or other units that require the kubernetes API ''' website = endpoint_from_flag('website.available') lb_address = _get_lb_address() lb_port = _get_lb_port(prefer_private=True) if lb_address: website.configure(port=lb_port, private_address=lb_address, hostname=lb_address) else: website.configure(port=lb_port)
def create_and_configure_database(): pgsql = endpoint_from_flag('db.master.available') render( 'settings.py.j2', '/srv/hello-juju/current/settings.py', {'db': pgsql.master}, owner='www-data', perms=0o644, ) create_database_tables() set_state('hello_juju.database_configured') status_set('active', '')
def _write_openstack_snap_config(component): # openstack requires additional credentials setup openstack = endpoint_from_flag('endpoint.openstack.ready') cloud_config_path = _cloud_config_path(component) cloud_config_path.write_text('\n'.join([ '[Global]', 'auth-url = {}'.format(openstack.auth_url), 'username = {}'.format(openstack.username), 'password = {}'.format(openstack.password), 'tenant-name = {}'.format(openstack.project_name), 'domain-name = {}'.format(openstack.user_domain_name), ]))
def shared_db_respond(): """Respond to Shared DB Requests. """ ch_core.hookenv.log( "The share-db relation is DEPRECATED. " "Please use mysql-router and the db-router relation.", "WARNING") shared_db = reactive.endpoint_from_flag("shared-db.available") with charm.provide_charm_instance() as instance: if instance.create_databases_and_users(shared_db): ch_core.hookenv.log("Shared DB relation created DBs and users.", "DEBUG") reactive.clear_flag('endpoint.shared-db.changed') instance.assess_status()
def configure_ovs(): ovsdb = reactive.endpoint_from_flag('ovsdb.available') with charm.provide_charm_instance() as charm_instance: if reactive.is_flag_set('config.changed.enable-dpdk'): # Install required packages and/or run update-alternatives charm_instance.install() charm_instance.configure_ovs(','.join(ovsdb.db_sb_connection_strs)) charm_instance.render_with_interfaces( charm.optional_interfaces((ovsdb,), 'nova-compute.connected', 'amqp.connected')) reactive.set_flag('config.rendered') charm_instance.assess_status()
def write_openstack_snap_config(component): # openstack requires additional credentials setup openstack = endpoint_from_flag('endpoint.openstack.ready') lines = [ '[Global]', 'auth-url = {}'.format(openstack.auth_url), 'region = {}'.format(openstack.region), 'username = {}'.format(openstack.username), 'password = {}'.format(openstack.password), 'tenant-name = {}'.format(openstack.project_name), 'domain-name = {}'.format(openstack.user_domain_name), ] if openstack.endpoint_tls_ca: cloud_endpoint_ca_path = _cloud_endpoint_ca_path(component) cloud_endpoint_ca_path.write_text( base64.b64decode(openstack.endpoint_tls_ca).decode('utf-8')) lines.append('ca-file = {}'.format(str(cloud_endpoint_ca_path))) if any([ openstack.subnet_id, openstack.floating_network_id, openstack.lb_method, openstack.manage_security_groups ]): lines.append('') lines.append('[LoadBalancer]') if openstack.subnet_id: lines.append('subnet-id = {}'.format(openstack.subnet_id)) if openstack.floating_network_id: lines.append('floating-network-id = {}'.format( openstack.floating_network_id)) if openstack.lb_method: lines.append('lb-method = {}'.format(openstack.lb_method)) if openstack.manage_security_groups: lines.append('manage-security-groups = {}'.format( openstack.manage_security_groups)) if any([ openstack.bs_version, openstack.trust_device_path, openstack.ignore_volume_az ]): lines.append('') lines.append('[BlockStorage]') if openstack.bs_version is not None: lines.append('bs-version = {}'.format(openstack.bs_version)) if openstack.trust_device_path is not None: lines.append('trust-device-path = {}'.format( openstack.trust_device_path)) if openstack.ignore_volume_az is not None: lines.append('ignore-volume-az = {}'.format( openstack.ignore_volume_az)) comp_cloud_config_path = cloud_config_path(component) comp_cloud_config_path.write_text(''.join('{}\n'.format(l) for l in lines))
def write_azure_snap_config(component): azure = endpoint_from_flag('endpoint.azure.ready') comp_cloud_config_path = cloud_config_path(component) comp_cloud_config_path.write_text(json.dumps({ 'useInstanceMetadata': True, 'useManagedIdentityExtension': True, 'subscriptionId': azure.subscription_id, 'resourceGroup': azure.resource_group, 'location': azure.resource_group_location, 'vnetName': azure.vnet_name, 'vnetResourceGroup': azure.vnet_resource_group, 'subnetName': azure.subnet_name, 'securityGroupName': azure.security_group_name, }))
def configure_relation_data(): cfg = config() endpoint = endpoint_from_flag('endpoint.bitcoind.joined') info = network_get('bitcoind', relation_id()) log('network info {0}'.format(info)) host = info['ingress-addresses'][0] if host == "": log("no service address yet") return else: endpoint.configure(host=host, port=cfg.get('btc-rpcport'), user=cfg.get('btc-rpcuser'), password=cfg.get('btc-rpcpassword'))
def connect_to_concrete_mongodb(): gdb_endpoint = endpoint_from_flag( 'endpoint.generic-database.mongodb.requested') gdb_endpoint.share_details( db_details['technology'], db_details['host'], db_details['dbname'], db_details['user'], db_details['password'], db_details['port'], ) clear_flag('endpoint.generic-database.mongodb.requested') status_set('active', 'Shared mongodb details!')
def write_webhook_yaml(): ''' Write out the webhook yaml file for the api server to use. Everyone, including the leader, does this with leadership data set by the leader. ''' hookenv.status_set('maintenance', 'Writing apiserver webhook configuration') context = {} cert = leader_get('cert').encode('utf-8') context['cert'] = base64.b64encode(cert).decode('utf-8') context['service_ip'] = leader_get('service_ip') render('webhook.yaml', webhook_path, context) aws_iam = endpoint_from_flag('endpoint.aws-iam.available') aws_iam.set_webhook_status(True) set_flag('charm.aws-iam.written-webhook')
def set_bootstrapped(): u = reactive.endpoint_from_flag('endpoint.cluster.joined') u.set_bootstrapped(cassandra.listen_ip_address()) reactive.set_flag('cassandra.bootstrapped.published')