#!/usr/bin/python
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from charmhelpers.core import hookenv
from charmhelpers.core.reactive import RelationBase
from charmhelpers.core.reactive import scopes
from charmhelpers.core.reactive import hook
from charmhelpers.core.reactive import not_until
[docs]class PostgreSQL(RelationBase):
# We expect multiple, separate services to be related, but all units of a
# given service will share the same database name and connection info.
# Thus, we use SERVICE scope and will have one converstaion per service.
scope = scopes.SERVICE
@hook('{provides:pgsql}-relation-{joined,changed}')
[docs] def joined_changed(self):
"""
Handles the relation-joined and relation-changed hook.
Depending on the state of the conversation, this can trigger one
of the following states:
* ``{relation_name}.database.requested`` This state will be activated if
the remote service has requested a different database name than the
one it has been provided. This state should be resolved by calling
:meth:`provide_database`. See also :meth:`requested_databases`.
* ``{relation_name}.roles.requested`` This state will be activated if
the remote service has requested a specific set of roles for its user.
This state should be resolved by calling :meth:`ack_roles`. See also
:meth:`requrested_roles`.
"""
service = hookenv.remote_service()
conversation = self.conversation()
if self.previous_database(service) != self.requested_database(service):
conversation.set_state('{relation_name}.database.requested')
if self.previous_roles(service) != self.requested_roles(service):
conversation.set_state('{relation_name}.roles.requested')
@hook('{provides:pgsql}-relation-{broken,departed}')
def departed(self):
conversation = self.conversation()
# if these were requested but not yet fulfilled, cancel the request
conversation.remove_state('{relation_name}.database.requested')
conversation.remove_state('{relation_name}.roles.requested')
@not_until('{provides:pgsql}.database.requested')
[docs] def provide_database(self, service, host, port, database, user, password, schema_user, schema_password, state):
"""
Provide a database to a requesting service.
:param str service: The service which requested the database, as
returned by :meth:`~provides.PostgreSQL.requested_databases`.
:param str host: The host where the database can be reached (e.g.,
the charm's private or public-address).
:param int port: The port where the database can be reached.
:param str database: The name of the database being provided.
:param str user: The username to be used to access the database.
:param str password: The password to be used to access the database.
:param str schema_user: The username to be used to admin the database.
:param str schema_password: The password to be used to admin the database.
:param str state: I have no idea what this is for. TODO: Document this better
"""
conversation = self.conversation(scope=service)
conversation.set_remote(
host=host,
port=port,
database=database,
user=user,
password=password,
schema_user=schema_user,
schema_password=schema_password,
state=state,
)
conversation.set_local('database', database)
conversation.remove_state('{relation_name}.database.requested')
@not_until('{provides:pgsql}.roles.requested')
[docs] def ack_roles(self, service, roles):
"""
Acknowledge that a set of roles have been given to a service's user.
:param str service: The service which requested the roles, as
returned by :meth:`~provides.PostgreSQL.requested_roles`.
"""
conversation = self.conversation(scope=service)
conversation.set_local('roles', roles)
conversation.remove_state('{relation_name}.roles.requested')
[docs] def requested_roles(self, service=None):
"""
Return the roles requested by all or a single given service.
:param str service: The name of a service requesting roles, as
provided by either :meth:`requested_roles` (with no args) or
:meth:`requested_databases`.
:returns: If no service name is given, then a list of ``(service, roles)``
tuples are returned, mapping service names to their requested
roles. If a service name is given, a list of the roles requested
for that service is returned.
Example usage::
for service, roles in pgsql.requested_roles():
set_roles(username_from_service(service), roles)
pgsql.ack_roles(service, roles)
"""
_roles = lambda conv: filter(None, conv.get_remote('roles', '').split(','))
if service is not None:
return _roles(self.conversation(scope=service))
else:
results = []
for conversation in self.conversations():
service = conversation.scope
results.append((service, _roles(conversation)))
return results
[docs] def previous_roles(self, service):
"""
Return the roles previously requested, if different from the currently
requested roles.
"""
return self.conversation(scope=service).get_local('roles')
[docs] def requested_databases(self):
"""
Return a list of tuples mapping a service name to the database name
requested by that service. If a given service has not requested a
specific database name, an empty string is returned, indicating that
the database name should be generated.
Example usage::
for service, database in pgsql.requested_databases():
database = database or generate_dbname(service)
pgsql.provide_database(**create_database(database))
"""
for conversation in self.conversations():
service = conversation.scope
database = self.requested_database(service)
yield service, database
[docs] def requested_database(self, service):
"""
Return the database name requested by the given service. If the given
service has not requested a specific database name, an empty string is
returned, indicating that the database name should be generated.
"""
return self.conversation(scope=service).get_remote('database', '')
[docs] def previous_database(self, service):
"""
Return the roles previously requested, if different from the currently
requested roles.
"""
return self.conversation(scope=service).get_local('database')