diff --git a/README.md b/README.md index 04600a6..87932a2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -infinidat Storage Backend for Cinder -------------------------------- +Infinidat Storage Backend for Manila +------------------------------------ Overview ======== -This charm provides a infinidat storage backend for use with the Cinder +This charm provides Infinidat storage backend for use with the Manila charm. To use: - juju deploy cinder + juju deploy manila juju deploy manila-infinidat - juju add-relation manila-infinidat cinder + juju add-relation manila-infinidat manila Configuration ============= diff --git a/config.yaml b/config.yaml index d931dfe..77d8523 100644 --- a/config.yaml +++ b/config.yaml @@ -11,16 +11,19 @@ options: description: > The status of service-affecting packages will be set to this value in the dpkg database. Valid values are "install" and "hold". - source: - description: > - List of extra apt sources, per charm-helpers standard - format (a yaml list of strings encoded as a string). Each source - may be either a line that can be added directly to - sources.list(5), or in the form ppa:/ for adding - Personal Package Archives, or a distribution component to enable. + install_sources: + description: | + Optional configuration to support use of additional sources such as: + - ppa:myteam/ppa + - cloud:trusty-proposed/kilo + - http://my.archive.com/ubuntu main + The last option should be used in conjunction with the key configuration + option. See https://repo.infinidat.com/home/main-stable for details. + The charm also supports templating of the distribution codename via + automatic expansion of {distrib_codename} depending on the host system + default: "deb https://repo.infinidat.com/packages/main-stable/apt/linux-ubuntu {distrib_codename} main" type: string - default: "" - key: + install_keys: description: > List of signing keys for install_sources package sources, per charmhelpers standard format (a yaml list of strings encoded as @@ -32,7 +35,25 @@ options: sources where the package signing key is securely retrieved from Launchpad. type: string - default: "" + default: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + Version: GnuPG v1.4.11 (GNU/Linux) + + mQENBFESDRIBCADMR7MQMbH4GdCQqfrOMt35MhBwwH4wv9kb1WRSTxa0CmuzYaBB + 1nJ0nLaMAwHsEr9CytPWDpMngm/3nt+4F2hJcsOEkQkqeJ31gScJewM+AOUV3DEl + qOeXXYLcP+jUY6pPjlZpOw0p7moUQPXHn+7amVrk7cXGQ8O3B+5a5wjN86LT2hlX + DlBlV5bX/DYluiPUbvQLOknmwO53KpaeDeZc4a8iIOCYWu2ntuAMddBkTps0El5n + JJZMTf6os2ZzngWMZRMDiVJgqVRi2b+8SgFQlQy0cAmne/mpgPrRq0ZMX3DokGG5 + hnIg1mF82laTxd+9qtiOxupzJqf8mncQHdaTABEBAAG0IWFwcF9yZXBvIChDb21t + ZW50KSA8bm9AZW1haWwuY29tPokBOAQTAQIAIgUCURINEgIbLwYLCQgHAwIGFQgC + CQoLBBYCAwECHgECF4AACgkQem2D/j05RYSrcggAsCc4KppV/SZX5XI/CWFXIAXw + +HaNsh2EwYKf9DhtoGbTOuwePvrPGcgFYM3Tu+m+rziPnnFl0bs0xwQyNEVQ9yDw + t465pSgmXwEHbBkoISV1e4WYtZAsnTNne9ieJ49Ob/WY4w3AkdPRK/41UP5Ct6lR + HHRXrSWJYHVq5Rh6BakRuMJyJLz/KvcJAaPkA4U6VrPD7PFtSecMTaONPjGCcomq + b7q84G5ZfeJWb742PWBTS8fJdC+Jd4y5fFdJS9fQwIo52Ff9In2QBpJt5Wdc02SI + fvQnuh37D2P8OcIfMxMfoFXpAMWjrMYc5veyQY1GXD/EOkfjjLne6qWPLfNojA== + =w5Os + -----END PGP PUBLIC KEY BLOCK----- debug: default: !!bool false type: boolean @@ -65,8 +86,18 @@ options: thin-provision: type: boolean description: Choose whether to thin provision - default: !!bool true + default: !!bool "true" share-backend-name: type: string description: Manila backend name default: __app__ + use-ssl: + type: boolean + description: > + Configures SSL support for Infinidat management API + default: !!bool "false" + suppress-ssl-warnings: + type: boolean + description: > + Configures SSL warnings suppression + default: !!bool "false" diff --git a/src/charm.py b/src/charm.py index 180925c..8efd231 100755 --- a/src/charm.py +++ b/src/charm.py @@ -15,9 +15,7 @@ # limitations under the License. from typing import OrderedDict -import ops_openstack.adapters -from ops_openstack.plugins.classes import CinderStoragePluginCharm -from ops_openstack.core import charm_class, get_charm_class_for_release, OSBaseCharm +from ops_openstack.core import OSBaseCharm from ops.main import main from ops.model import ( @@ -45,19 +43,25 @@ import logging RELATION_NAME = 'manila-plugin' + class ManilaInfinidatPluginCharm(OSBaseCharm): - PACKAGES = ['infinisdk', 'infinishell'] + PACKAGES = ['python3-infinisdk', 'infinishell'] REQUIRED_RELATIONS = [RELATION_NAME] - SHARE_DRIVER = 'manila.share.drivers.infinidat.infinibox.InfiniboxShareDriver' + SHARE_DRIVER = \ + 'manila.share.drivers.infinidat.infinibox.InfiniboxShareDriver' - active_active = True + MANDATORY_CONFIG = ['infinibox-ip', 'infinibox-login', + 'infinibox-password', 'pool-name'] + + DEFAULT_REPO_BASEURL = \ + 'https://repo.infinidat.com/packages/main-stable/apt/linux-ubuntu' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self.framework.observe( self.on.config_changed, self.on_config) @@ -69,14 +73,16 @@ class ManilaInfinidatPluginCharm(OSBaseCharm): def on_config(self, event): self.on_manila_plugin(event) self.set_started(started=True) + if self.framework.model.relations.get(RELATION_NAME): + self.send_backend_config() self.unit.status = ActiveStatus('Unit is ready') - def on_manila_plugin(self, event): - logging.error("Relation changed") + def send_backend_config(self): config = dict(self.framework.model.config) manila_backends = self.manila_configuration(config) for relation in self.framework.model.relations.get(RELATION_NAME): - relation.data[self.unit]['_name'] = ','.join(manila_backends.keys()) + relation.data[self.unit]['_name'] = \ + ','.join(manila_backends.keys()) rendered_config = render( source="parts/backends", template_loader=os_templating.get_loader( @@ -90,12 +96,15 @@ class ManilaInfinidatPluginCharm(OSBaseCharm): } }) + def on_manila_plugin(self, event): + self.send_backend_config() + def manila_configuration(self, config): """ See https://docs.openstack.org/manila/latest/configuration/shared-file-systems/drivers/infinidat-share-driver.html # noqa: E501 """ # Return the configuration to be set by the principal. - + backends = OrderedDict() backend_name = config.get('share-backend-name') @@ -103,6 +112,7 @@ class ManilaInfinidatPluginCharm(OSBaseCharm): # backend name selection logic if config.get('nas-network-space-name'): backend_names = filter( + None, map( str.strip, config.get('nas-network-space-name').split(',') @@ -115,66 +125,60 @@ class ManilaInfinidatPluginCharm(OSBaseCharm): for backend_name in backend_names: - backends[backend_name] = ( + backends[backend_name] = [ ('share_driver', self.SHARE_DRIVER), ('share_backend_name', backend_name), - - ('infinibox_hostname', config.get('infinibox-ip')), - ('infinibox_login', config.get('infinibox-login')), - ('infinibox_password', config.get('infinibox-password')), - ('infinidat_nas_network_space_name', backend_name), #TODO: mapping to nas-network-space-name + ('driver_handles_share_servers', 'false'), + + ('infinidat_nas_network_space_name', backend_name), - ('infinidat_pool_name', config.get('pool-name')), ('infinidat_thin_provision', config.get('thin-provision')), - ) + + ('infinidat_use_ssl', config.get('use-ssl', False)), + + ('infinidat_suppress_ssl_warnings', + config.get('suppress-ssl-warnings', True)), + ] + + if config.get('infinibox-ip') and config.get('infinibox-login') \ + and config.get('infinibox-password'): + + backends[backend_name].extend([ + ('infinibox_hostname', config.get('infinibox-ip')), + ('infinibox_login', config.get('infinibox-login')), + ('infinibox_password', config.get('infinibox-password')), + ]) + + if config.get('pool-name'): + backends[backend_name].append( + ('infinidat_pool_name', config.get('pool-name')) + ), if not backends: self.unit.status = BlockedStatus("No backends configured") return backends - @property - def stateless(self): - """Indicate whether the charm is stateless. - - For more information, see: https://cinderlib.readthedocs.io/en/v0.2.1/topics/serialization.html - - :returns: A boolean value indicating statefulness. - :rtype: bool - """ # noqa - return False - - @property - def active_active(self): - """Indicate active-active support in the charm. - - For more information, see: https://specs.openstack.org/openstack/cinder-specs/specs/mitaka/cinder-volume-active-active-support.html - - :returns: A boolean indicating active-active support. - :rtype: bool - """ # noqa - return False - def install_pkgs(self): logging.info("Installing packages") - # value of 'source' param is defined like this: - # deb https://repo.infinidat.com/packages/main-stable/apt/linux-ubuntu $codename main - if self.model.config.get('source'): + # we implement $codename expansion here + # see the default value for 'source' in config.yaml + if self.model.config.get('install_sources'): + distrib_codename = lsb_release()['DISTRIB_CODENAME'].lower() add_source( - self.model.config['source'] \ - .replace('$codename', - lsb_release()['DISTRIB_CODENAME'].lower()), - self.model.config.get('key')) + self.model.config['install_sources'] + .format(distrib_codename=distrib_codename), + self.model.config.get('install_keys')) apt_update(fatal=True) apt_install(self.PACKAGES, fatal=True) self.update_status() def on_install(self, event): self.install_pkgs() - self.update_status() + if __name__ == '__main__': main(ManilaInfinidatPluginCharm) diff --git a/templates/parts/backends b/templates/parts/backends index 482215a..6468d2f 100644 --- a/templates/parts/backends +++ b/templates/parts/backends @@ -1,6 +1,6 @@ {% for backend_name in backends %} [{{ backend_name }}] -{% for key, value in backends[backend_name] -%} +{% for key, value in backends[backend_name] %} {{ key }} = {{ value }} {%- endfor %} {% endfor %} \ No newline at end of file diff --git a/unit_tests/test_manila_infinidat_charm.py b/unit_tests/test_manila_infinidat_charm.py index 37c9995..801fb24 100644 --- a/unit_tests/test_manila_infinidat_charm.py +++ b/unit_tests/test_manila_infinidat_charm.py @@ -13,17 +13,136 @@ # limitations under the License. import unittest -from src.charm import CinderCharmBase -from ops.model import ActiveStatus +from src.charm import ManilaInfinidatPluginCharm from ops.testing import Harness +from unittest import mock -class TestManilainfinidatCharm(unittest.TestCase): +from charmhelpers.core.host_factory.ubuntu import UBUNTU_RELEASES + +SOURCE = "deb https://repo.infinidat.com/packages/main-stable/apt/linux-ubuntu focal main" # noqa: E501 +KEY = """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.11 (GNU/Linux) + +mQENBFESDRIBCADMR7MQMbH4GdCQqfrOMt35MhBwwH4wv9kb1WRSTxa0CmuzYaBB +1nJ0nLaMAwHsEr9CytPWDpMngm/3nt+4F2hJcsOEkQkqeJ31gScJewM+AOUV3DEl +qOeXXYLcP+jUY6pPjlZpOw0p7moUQPXHn+7amVrk7cXGQ8O3B+5a5wjN86LT2hlX +DlBlV5bX/DYluiPUbvQLOknmwO53KpaeDeZc4a8iIOCYWu2ntuAMddBkTps0El5n +JJZMTf6os2ZzngWMZRMDiVJgqVRi2b+8SgFQlQy0cAmne/mpgPrRq0ZMX3DokGG5 +hnIg1mF82laTxd+9qtiOxupzJqf8mncQHdaTABEBAAG0IWFwcF9yZXBvIChDb21t +ZW50KSA8bm9AZW1haWwuY29tPokBOAQTAQIAIgUCURINEgIbLwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AACgkQem2D/j05RYSrcggAsCc4KppV/SZX5XI/CWFXIAXw ++HaNsh2EwYKf9DhtoGbTOuwePvrPGcgFYM3Tu+m+rziPnnFl0bs0xwQyNEVQ9yDw +t465pSgmXwEHbBkoISV1e4WYtZAsnTNne9ieJ49Ob/WY4w3AkdPRK/41UP5Ct6lR +HHRXrSWJYHVq5Rh6BakRuMJyJLz/KvcJAaPkA4U6VrPD7PFtSecMTaONPjGCcomq +b7q84G5ZfeJWb742PWBTS8fJdC+Jd4y5fFdJS9fQwIo52Ff9In2QBpJt5Wdc02SI +fvQnuh37D2P8OcIfMxMfoFXpAMWjrMYc5veyQY1GXD/EOkfjjLne6qWPLfNojA== +=w5Os +-----END PGP PUBLIC KEY BLOCK----- +""" + + +class TestManilaInfinidatCharm(unittest.TestCase): def setUp(self): - self.harness = Harness(CinderCharmBase) + self.harness = Harness(ManilaInfinidatPluginCharm) self.addCleanup(self.harness.cleanup) self.harness.begin() self.harness.set_leader(True) backend = self.harness.add_relation('manila-plugin', 'manila') self.harness.add_relation_unit(backend, 'manila/0') + + def _get_partial_config_sample(self): + """ + A config with all mandatory params set + """ + return { + 'infinibox-ip': '123.123.123.123', + 'infinibox-login': 'login', + 'infinibox-password': 'password', + 'pool-name': 'test', + } + + def _get_valid_config_sample(self): + """ + A minimal config that would transition the charm to ActiveState + """ + partial_config = self._get_partial_config_sample() + partial_config.update({ + 'nas-network-space-name': 'A,B', + }) + return partial_config + + def _get_source(self, codename, pocket, baseurl=None): + if baseurl is None: + baseurl = self.harness.charm.DEFAULT_REPO_BASEURL + return ' '.join(( + 'deb', + baseurl, + codename, + pocket)) + + def test_update_config(self): + self.harness.update_config({ + "infinibox-ip": "172.27.12.151", + "infinibox-login": "admin", + "infinibox-password": "123456", + "nas-network-space-name": "iscsi", + "package_status": "install", + "pool-name": "manila", + "share-backend-name": "__app__", + "suppress-ssl-warnings": True, + "thin-provision": True, + "use-ssl": True, + "verbose": False, + }) + + @mock.patch('src.charm.add_source') + @mock.patch('src.charm.apt_update') + @mock.patch('src.charm.apt_install') + @mock.patch('src.charm.lsb_release') + def test_repo_management(self, lsb_release, + apt_install, apt_update, add_source): + + add_source.return_value = None + apt_install.return_value = None + apt_update.return_value = None + + # we'll need the config the charm considers valid + # in order to test repo management: + cfg = self._get_valid_config_sample() + + dynamic_source = self._get_source('{distrib_codename}', 'main') + + # generate test data for both 'source' values that need substituion + # and for the static ones + + test_data = [] + + for release in UBUNTU_RELEASES: + static_source = self._get_source(release, 'main') + test_data.append( + (dynamic_source, release, + self._get_source(release, 'main')), + ) + test_data.append( + (static_source, release, static_source), + ) + + for i in test_data: + # distro codename the charm runs on + lsb_release.return_value = {'DISTRIB_CODENAME': i[1]} + + # configure to use specific repo version + cfg['install_sources'] = i[0] + cfg['install_keys'] = KEY + + # on_config calls package installation + self.harness.update_config(cfg) + self.harness.charm.on.install.emit() + + # make sure the repo management calls were correct + add_source.assert_called_with(i[2], KEY) + apt_install.assert_called_with(self.harness.charm.PACKAGES, + fatal=True)