diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a6afa9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +*.swp +.testrepository +.tox diff --git a/.project b/.project new file mode 100644 index 0000000..42a97f9 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + interface-ceph-client + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..06ef928 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,8 @@ + + + +/interface-ceph-client + +python 2.7 +Default + diff --git a/README.md b/README.md new file mode 100644 index 0000000..18076bd --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Overview + +This interface layer handles the communication between the Ceph Monitor +cluster and a client that requires an access key and a pool to use. + +# Usage + +## Requires + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.available` The ceph client has been related to a provider. + +The following accessors will be available: + + - key - The cephx access key + - auth - Whether or not strict auth is supported + - mon_hosts - The public addresses list of the monitor cluster + +Client example: + +```python +@when('ceph-client.connected') +def ceph_connected(ceph_client): + ceph_client.create_pool('newpool') + +@when('ceph-client.available') +def ceph_ready(ceph_client): + charm_ceph_conf= os.path.join(os.sep, 'etc', 'ceph', 'ceph.conf') + cephx_key = os.path.join(os.sep, 'etc', 'ceph', 'ceph.client.charm.keyring') + + ceph_context = { + 'auth_supported': ceph_client.auth, + 'mon_hosts': ceph_client.mon_hosts, + } + + with open(charm_ceph_conf, 'w') as cephconf: + cephconf.write(render_template('ceph.conf', ceph_context)) + + # Write out the cephx_key also + with open(cephx_key, 'w') as cephconf: + cephconf.write(ceph_client.key) +``` diff --git a/requires.py b/requires.py new file mode 100644 index 0000000..e3b592d --- /dev/null +++ b/requires.py @@ -0,0 +1,119 @@ +# Copyright 2017 Canonical Ltd +# +# 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. + +import json + +from charms.reactive import hook +from charms.reactive import RelationBase +from charms.reactive import scopes +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import log +from charmhelpers.contrib.network.ip import format_ipv6_addr + +from charmhelpers.contrib.storage.linux.ceph import ( + CephBrokerRq, + is_request_complete, + send_request_if_needed, +) + + +class CephClientRequires(RelationBase): + scope = scopes.GLOBAL + + auto_accessors = ['auth', 'key'] + + @hook('{requires:ceph-client}-relation-{joined}') + def joined(self): + self.set_state('{relation_name}.connected') + + @hook('{requires:ceph-client}-relation-{changed,departed}') + def changed(self): + data = { + 'key': self.key(), + 'auth': self.auth(), + 'mon_hosts': self.mon_hosts() + } + if all(data.values()): + self.set_state('{relation_name}.available') + + json_rq = self.get_local(key='broker_req') + if json_rq: + rq = CephBrokerRq() + j = json.loads(json_rq) + rq.ops = j['ops'] + log("changed broker_req: {}".format(rq.ops)) + + if rq and is_request_complete(rq, + relation=self.relation_name): + log("Setting ceph-client.pools.available") + self.set_state('{relation_name}.pools.available') + else: + log("incomplete request. broker_req not found") + + @hook('{requires:ceph-client}-relation-{broken}') + def broken(self): + self.remove_state('{relation_name}.available') + self.remove_state('{relation_name}.connected') + self.remove_state('{relation_name}.pools.available') + + def create_pool(self, name, replicas=3): + """ + Request pool setup + + @param name: name of pool to create + @param replicas: number of replicas for supporting pools + """ + # json.dumps of the CephBrokerRq() + json_rq = self.get_local(key='broker_req') + + if not json_rq: + rq = CephBrokerRq() + rq.add_op_create_pool(name="{}".format(name), + replica_count=replicas, + weight=None) + self.set_local(key='broker_req', value=rq.request) + send_request_if_needed(rq, relation=self.relation_name) + else: + rq = CephBrokerRq() + try: + j = json.loads(json_rq) + log("Json request: {}".format(json_rq)) + rq.ops = j['ops'] + send_request_if_needed(rq, relation=self.relation_name) + except ValueError as err: + log("Unable to decode broker_req: {}. Error: {}".format( + json_rq, err)) + + def get_remote_all(self, key, default=None): + """Return a list of all values presented by remote units for key""" + # TODO: might be a nicer way todo this - written a while back! + values = [] + for conversation in self.conversations(): + for relation_id in conversation.relation_ids: + for unit in hookenv.related_units(relation_id): + value = hookenv.relation_get(key, + unit, + relation_id) or default + if value: + values.append(value) + return list(set(values)) + + def mon_hosts(self): + """List of all monitor host public addresses""" + hosts = [] + addrs = self.get_remote_all('ceph-public-address') + for addr in addrs: + hosts.append('{}:6789'.format(format_ipv6_addr(addr) or addr)) + hosts.sort() + return hosts diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..1bae0c7 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +flake8>=2.2.4,<=2.4.1 +os-testr>=0.4.1 \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0facda6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,43 @@ +[tox] +envlist = pep8,py27,py34,py35 +skipsdist = True +skip_missing_interpreters = True + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 +install_command = + pip install --allow-unverified python-apt {opts} {packages} +commands = ostestr {posargs} + +[testenv:py27] +basepython = python2.7 +deps = -r{toxinidir}/test-requirements.txt +# TODO: Need to write unit tests then remove the following command. +# https://github.com/juju/charm-tools/issues/249 +commands = /bin/true + +[testenv:py34] +basepython = python3.4 +deps = -r{toxinidir}/test-requirements.txt +# TODO: Need to write unit tests then remove the following command. +# https://github.com/juju/charm-tools/issues/249 +commands = /bin/true + +[testenv:py35] +basepython = python3.5 +deps = -r{toxinidir}/test-requirements.txt +# TODO: Need to write unit tests then remove the following command. +# https://github.com/juju/charm-tools/issues/249 +commands = /bin/true + +[testenv:pep8] +basepython = python2.7 +deps = -r{toxinidir}/test-requirements.txt +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[flake8] +ignore = E402,E226