diff --git a/charms_openstack/adapters.py b/charms_openstack/adapters.py index d8ef564..9f069f9 100644 --- a/charms_openstack/adapters.py +++ b/charms_openstack/adapters.py @@ -660,6 +660,14 @@ class APIConfigurationAdapter(ConfigurationAdapter): """ return charms.reactive.bus.get_state('ssl.enabled') + @property + def ssl(self): + """Whether SSL is being used for this service + + @return True is SSL has been enable + """ + return charms.reactive.bus.get_state('ssl.enabled') + def determine_service_port(self, port): """Calculate port service should use given external port diff --git a/charms_openstack/charm/classes.py b/charms_openstack/charm/classes.py index ae395b1..28e26e8 100644 --- a/charms_openstack/charm/classes.py +++ b/charms_openstack/charm/classes.py @@ -16,6 +16,7 @@ from charms_openstack.charm.core import ( BaseOpenStackCharm, BaseOpenStackCharmActions, BaseOpenStackCharmAssessStatus, + _get_snap_version, ) from charms_openstack.charm.utils import ( get_upstream_version, @@ -50,15 +51,28 @@ class OpenStackCharm(BaseOpenStackCharm, # first_release = this is the first release in which this charm works release = 'icehouse' + # package type - package type (deb or snap) in which this charm works + package_type = 'deb' + # The name of the charm (for printing, etc.) name = 'charmname' # List of packages to install packages = [] + # List of snaps to install + snaps = [] + + # Mode to install snaps in (jailmode/devmode/classic) + snap_mode = 'jailmode' + # Package to determine application version from # defaults to first in packages if not provided - version_package = None + version_package = release_pkg = None + + # Snap to determine application version from; + # defaults to first in snaps if not provided + version_snap = release_snap = None # Keystone endpoint type service_type = None @@ -121,15 +135,26 @@ class OpenStackCharm(BaseOpenStackCharm, @property def application_version(self): """Return the current version of the application being deployed by - the charm, as indicated by the version_package attribute + the charm, as indicated by the version_package or version_snap + attribute """ - if not self.version_package: - self.version_package = self.packages[0] - version = get_upstream_version( - self.version_package - ) - if not version: - version = os_utils.os_release(self.version_package) + if os_utils.snap_install_requested(): + if not self.version_snap: + self.version_snap = self.snaps[0] + version = _get_snap_version(self.version_snap, + fatal=False) + if not version: + version = os_utils.get_os_codename_install_source( + self.config['openstack-origin'] + ) + else: + if not self.version_package: + self.version_package = self.packages[0] + version = get_upstream_version( + self.version_package + ) + if not version: + version = os_utils.os_release(self.version_package) return version @@ -250,6 +275,14 @@ class OpenStackAPICharm(OpenStackCharm): return (super(OpenStackAPICharm, self).all_packages + self.token_cache_pkgs()) + @property + def all_snaps(self): + """List of snaps to be installed + + @return ['snap1', 'snap2', ...] + """ + return (super(OpenStackAPICharm, self).all_snaps) + @property def full_restart_map(self): """Map of services to be restarted if a file changes @@ -278,7 +311,7 @@ class HAOpenStackCharm(OpenStackAPICharm): self.set_haproxy_stat_password() @property - def apache_vhost_file(self): + def apache_ssl_vhost_file(self): """Apache vhost for SSL termination :returns: string @@ -291,8 +324,8 @@ class HAOpenStackCharm(OpenStackAPICharm): Enable Apache vhost for SSL termination if vhost exists and it is not curently enabled """ - if not os.path.exists(self.apache_vhost_file): - open(self.apache_vhost_file, 'a').close() + if not os.path.exists(self.apache_ssl_vhost_file): + open(self.apache_ssl_vhost_file, 'a').close() check_enabled = subprocess.call( ['a2query', '-s', 'openstack_https_frontend']) @@ -315,10 +348,20 @@ class HAOpenStackCharm(OpenStackAPICharm): _packages = super(HAOpenStackCharm, self).all_packages if self.haproxy_enabled(): _packages.append('haproxy') - if self.apache_enabled(): - _packages.append('apache2') + if not os_utils.snap_install_requested(): + if self.apache_enabled(): + _packages.append('apache2') return _packages + @property + def all_snaps(self): + """List of snaps to be installed + + @return ['snap1', 'snap2', ...] + """ + _snaps = super(HAOpenStackCharm, self).all_snaps + return _snaps + @property def full_restart_map(self): """Map of services to be restarted if a file changes @@ -332,16 +375,33 @@ class HAOpenStackCharm(OpenStackAPICharm): _restart_map = super(HAOpenStackCharm, self).full_restart_map if self.haproxy_enabled(): _restart_map[self.HAPROXY_CONF] = ['haproxy'] - if self.apache_enabled(): - _restart_map[self.apache_vhost_file] = ['apache2'] + if os_utils.snap_install_requested(): + # TODO(coreycb): add nginx config/service for ssl vhost + pass + else: + if self.apache_enabled(): + _restart_map[self.apache_ssl_vhost_file] = ['apache2'] return _restart_map def apache_enabled(self): """Determine if apache is being used @return True if apache is being used""" - return (self.get_state('ssl.enabled') or - self.get_state('ssl.requested')) + if os_utils.snap_install_requested(): + return False + else: + return (self.get_state('ssl.enabled') or + self.get_state('ssl.requested')) + + def nginx_ssl_enabled(self): + """Determine if nginx is being used + + @return True if nginx is being used""" + if os_utils.snap_install_requested(): + return (self.get_state('ssl.enabled') or + self.get_state('ssl.requested')) + else: + return False def haproxy_enabled(self): """Determine if haproxy is fronting the services @@ -394,6 +454,8 @@ class HAOpenStackCharm(OpenStackAPICharm): def enable_apache_modules(self): """Enable Apache modules needed for SSL termination""" + if os_utils.snap_install_requested(): + return restart = False for module in ['ssl', 'proxy', 'proxy_http']: check_enabled = subprocess.call(['a2query', '-m', module]) @@ -412,9 +474,15 @@ class HAOpenStackCharm(OpenStackAPICharm): @param key string SSL Key @param cn string Canonical name for service """ + if os_utils.snap_install_requested(): + ssl_dir = '/var/snap/{snap_name}/etc/nginx/ssl'.format( + snap_name=self.primary_snap + ) + else: + ssl_dir = os.path.join('/etc/apache2/ssl/', self.name) + if not cn: cn = os_ip.resolve_address(endpoint_type=os_ip.INTERNAL) - ssl_dir = os.path.join('/etc/apache2/ssl/', self.name) ch_host.mkdir(path=ssl_dir) if cn: cert_filename = 'cert_{}'.format(cn) @@ -535,7 +603,10 @@ class HAOpenStackCharm(OpenStackAPICharm): self.configure_cert( ssl['cert'], ssl['key'], cn=ssl['cn']) self.configure_ca(ssl['ca']) - self.configure_apache() + + if not os_utils.snap_install_requested(): + self.configure_apache() + self.remove_state('ssl.requested') self.set_state('ssl.enabled', True) else: @@ -563,6 +634,7 @@ class HAOpenStackCharm(OpenStackAPICharm): def configure_ca(self, ca_cert, update_certs=True): """Write Certificate Authority certificate""" + # TODO(jamespage): work this out for snap based installations cert_file = ( '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt') if ca_cert: diff --git a/charms_openstack/charm/core.py b/charms_openstack/charm/core.py index 6c5f8bb..c654907 100644 --- a/charms_openstack/charm/core.py +++ b/charms_openstack/charm/core.py @@ -36,6 +36,12 @@ _singleton = None # This is to enable the defining code to define which release is used. _release_selector_function = None +# `_package_type_selector_function` holds a function that optionally takes a +# package type and commutes it to another package type or just returns a +# package type. This is to enable the defining code to define which +# package type is used. +_package_type_selector_function = None + def optional_interfaces(args, *interfaces): """Return a tuple with possible optional interfaces @@ -85,7 +91,7 @@ class provide_charm_instance(object): return False -def get_charm_instance(release=None, *args, **kwargs): +def get_charm_instance(release=None, package_type='deb', *args, **kwargs): """Get an instance of the charm based on the release (or use the default if release is None). @@ -96,6 +102,7 @@ def get_charm_instance(release=None, *args, **kwargs): Note that it passes args and kwargs to the class __init__() method. :param release: lc string representing release wanted. + :param package_type: string representing the package type required :returns: BaseOpenStackCharm() derived class according to cls.releases """ if len(_releases.keys()) == 0: @@ -106,7 +113,7 @@ def get_charm_instance(release=None, *args, **kwargs): cls = None if release is None: # take the latest version of the charm if no release is passed. - cls = _releases[known_releases[-1]] + cls = _releases[known_releases[-1]][package_type] else: # check that the release is a valid release if release not in os_utils.OPENSTACK_RELEASES: @@ -122,8 +129,9 @@ def get_charm_instance(release=None, *args, **kwargs): # try to find the release that is supported. for known_release in reversed(known_releases): if (release_index >= - os_utils.OPENSTACK_RELEASES.index(known_release)): - cls = _releases[known_release] + os_utils.OPENSTACK_RELEASES.index(known_release) and + package_type in _releases[known_release]): + cls = _releases[known_release][package_type] break if cls is None: raise RuntimeError("Release {} is not supported".format(release)) @@ -154,6 +162,58 @@ def register_os_release_selector(f): return f +def register_package_type_selector(f): + """Register a function that determines what the package type is for the + invocation run. This allows the charm to define HOW the package type is + determined. + + Usage: + + @register_package_type_selector + def my_package_type_selector(): + return package_type_chooser() + + The function should return a string which is 'snap' or 'deb'. + """ + global _package_type_selector_function + if _package_type_selector_function is None: + # we can only do this once in a system invocation. + _package_type_selector_function = f + else: + raise RuntimeError( + "Only a single package_type_selector_function is supported." + " Called with {}".format(f.__name__)) + return f + + +# TODO(jamespage): move to snap charmhelper +def _get_snap_version(snap, fatal=True): + """Determine version for an installed snap. + + :param package: str Snap name to lookup (ie. in snap list) + :param fatal: bool Raise exception if snap not installed + :returns: str version of snap installed + """ + cmd = ['snap', 'list', snap] + try: + out = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + if not fatal: + return None + # the snap is unknown to snapd + e = ('Could not determine version of snap: {} as it\'s' + ' not installed'.format(snap)) + raise Exception(e) + + version_or_codename = None + lines = out.split('\n') + for line in lines: + if snap in line: + # Second item in list is version or a codename + version_or_codename = line.split()[1] + return version_or_codename + + class BaseOpenStackCharmMeta(type): """Metaclass to provide a classproperty of 'singleton' so that class methods in the derived BaseOpenStackCharm() class can simply use @@ -190,18 +250,28 @@ class BaseOpenStackCharmMeta(type): if members.get('abstract_class', False): return if 'release' in members.keys(): + package_type = members.get('package_type', 'deb') + if package_type not in ('deb', 'snap'): + raise RuntimeError( + "Package type {} is not a known type" + .format(package_type)) release = members['release'] if release not in os_utils.OPENSTACK_RELEASES: raise RuntimeError( "Release {} is not a known OpenStack release" .format(release)) - if release in _releases.keys(): + if (release in _releases.keys() and + package_type in _releases[release].keys()): raise RuntimeError( "Release {} defined more than once in classes {} and {} " " (at least)" - .format(release, _releases[release].__name__, name)) + .format(release, + _releases[release][package_type].__name__, + name)) # store the class against the release. - _releases[release] = cls + if release not in _releases: + _releases[release] = {} + _releases[release][package_type] = cls else: raise RuntimeError( "class '{}' does not define a release that it supports. " @@ -218,10 +288,14 @@ class BaseOpenStackCharmMeta(type): global _singleton if _singleton is None: release = None + package_type = None # see if a _release_selector_function has been registered. if _release_selector_function is not None: release = _release_selector_function() - _singleton = get_charm_instance(release=release) + if _package_type_selector_function is not None: + package_type = _package_type_selector_function() + _singleton = get_charm_instance(release=release, + package_type=package_type or 'deb') return _singleton @@ -347,12 +421,46 @@ class BaseOpenStackCharm(object, metaclass=BaseOpenStackCharmMeta): """proxy for charms.reactive.bus.get_state()""" return reactive.bus.get_state(state) + @staticmethod + def get_os_codename_snap(snap, codenames, fatal=True): + """Derive OpenStack release codename from an installed snap. + + :param package: str Snap name to lookup (ie. in snap list) + :param codenames: dict of OrderedDict + { + 'snap1': collections.OrderedDict([ + ('2', 'mitaka'), + ('3', 'newton'), + ('4', 'ocata'), ]), + 'snap2': collections.OrderedDict([ + ('12', 'mitaka'), + ('13', 'newton'), + ('14', 'ocata'), ]), + } + :param fatal: bool Raise exception if snap not installed + :returns: str OpenStack version name corresponding to package + """ + version_or_codename = _get_snap_version(snap, fatal) + + match = re.match('^(\d+)\.(\d+)', version_or_codename) + if match: + version = match.group(0) + # Generate a major version number for newer semantic + # versions of openstack projects + major_vers = version.split('.')[0] + if (snap in codenames and + major_vers in codenames[snap]): + return codenames[snap][major_vers] + else: + # NOTE(jamespage): fallback to codename assumption + return version_or_codename + @staticmethod def get_os_codename_package(package, codenames, fatal=True): """Derive OpenStack release codename from an installed package. - :param package: str Package name to lookup in apt cache - :param codenames: dict of OrderedDict eg + :param package: str Package name to lookup (ie. in apt cache) + :param codenames: dict of OrderedDict eg (not applicable for snap pkgs) { 'pkg1': collections.OrderedDict([ ('2', 'mitaka'), @@ -396,6 +504,28 @@ class BaseOpenStackCharm(object, metaclass=BaseOpenStackCharmMeta): major_vers in codenames[package]): return codenames[package][major_vers] + def get_os_version_snap(self, snap, fatal=True): + """Derive OpenStack version number from an installed snap. + + :param package: str Snap name to lookup in snap list + :param fatal: bool Raise exception if snap not installed + :returns: str OpenStack version number corresponding to snap + """ + # TODO(coreycb): Add support for get_os_version_snap + if os_utils.snap_install_requested(): + codenames = self.snap_codenames + codename = self.get_os_codename_snap(snap, codenames, + fatal=fatal) + if not codename: + return None + + vers_map = os_utils.OPENSTACK_CODENAMES + for version, cname in vers_map.items(): + if cname == codename: + return version + + return None + def get_os_version_package(self, package, fatal=True): """Derive OpenStack version number from an installed package. @@ -403,16 +533,19 @@ class BaseOpenStackCharm(object, metaclass=BaseOpenStackCharmMeta): :param fatal: bool Raise exception if pkg not installed :returns: str OpenStack version number corresponding to package """ - codenames = self.package_codenames or os_utils.PACKAGE_CODENAMES - codename = self.get_os_codename_package( - package, codenames, fatal=fatal) - if not codename: - return None + if not os_utils.snap_install_requested(): + codenames = self.package_codenames or os_utils.PACKAGE_CODENAMES + codename = self.get_os_codename_package( + package, codenames, fatal=fatal) + if not codename: + return None - vers_map = os_utils.OPENSTACK_CODENAMES - for version, cname in vers_map.items(): - if cname == codename: - return version + vers_map = os_utils.OPENSTACK_CODENAMES + for version, cname in vers_map.items(): + if cname == codename: + return version + + return None class BaseOpenStackCharmActions(object): @@ -440,15 +573,48 @@ class BaseOpenStackCharmActions(object): """ return self.packages + @property + def all_snaps(self): + """List of snaps to be installed + + Relies on the class variable 'snaps' + + @return ['snap1', 'snap2', ...] + """ + return self.snaps + + @property + def primary_snap(self): + """Primary snap to use for configuration + + Relies on the class variable 'snaps' + + :return string: first snap found in 'snaps' + """ + if self.snaps: + return self.snaps[0] + return None + def install(self): - """Install packages related to this charm based on - contents of self.packages attribute. + """Install packages or snaps related to this charm based on + contents of self.packages or self.snaps attribute. """ packages = fetch.filter_installed_packages( self.all_packages) if packages: hookenv.status_set('maintenance', 'Installing packages') fetch.apt_install(packages, fatal=True) + + if os_utils.snap_install_requested(): + if self.all_snaps: + hookenv.status_set('maintenance', 'Installing snaps') + os_utils.install_os_snaps( + os_utils.get_snaps_install_info_from_origin( + self.all_snaps, + self.config['openstack-origin'], + mode=self.snap_mode) + ) + # AJK: we set this as charms can use it to detect installed state self.set_state('{}-installed'.format(self.name)) self.update_api_ports() @@ -652,7 +818,7 @@ class BaseOpenStackCharmActions(object): ports.append(line) return ports - def openstack_upgrade_available(self, package=None): + def openstack_upgrade_available(self, package=None, snap=None): """Check if an OpenStack upgrade is available :param package: str Package name to use to check upgrade availability @@ -660,10 +826,16 @@ class BaseOpenStackCharmActions(object): """ if not package: package = self.release_pkg + if not snap: + snap = self.release_snap src = self.config['openstack-origin'] cur_vers = self.get_os_version_package(package) avail_vers = os_utils.get_os_version_install_source(src) + if os_utils.snap_install_requested(): + cur_vers = self.get_os_version_snap(snap) + else: + cur_vers = self.get_os_version_package(package) apt.init() return apt.version_compare(avail_vers, cur_vers) == 1 @@ -680,7 +852,7 @@ class BaseOpenStackCharmActions(object): self.do_openstack_upgrade_db_migration() def do_openstack_pkg_upgrade(self): - """Upgrade OpenStack packages + """Upgrade OpenStack packages and snaps :returns: None """ @@ -688,6 +860,14 @@ class BaseOpenStackCharmActions(object): new_os_rel = os_utils.get_os_codename_install_source(new_src) hookenv.log('Performing OpenStack upgrade to %s.' % (new_os_rel)) + if os_utils.snap_install_requested() and self.all_snaps: + os_utils.install_os_snaps( + snaps=os_utils.get_snaps_install_info_from_origin( + self.all_snaps, + self.config['openstack-origin'], + mode=self.snap_mode), + refresh=True) + os_utils.configure_installation_source(new_src) fetch.apt_update() @@ -722,6 +902,15 @@ class BaseOpenStackCharmActions(object): else: hookenv.log("Deferring DB sync to leader", level=hookenv.INFO) + # NOTE(jamespage): Not currently used - switch from c-h function for perf? + def snap_install_requested(self): + """Determine whether a snap based install is configured + via the openstack-origin configuration option + + :returns: None + """ + return self.options.openstack_origin.startswith('snap:') + class BaseOpenStackCharmAssessStatus(object): """Provides the 'Assess Status' functionality to the OpenStack charm class. diff --git a/charms_openstack/charm/defaults.py b/charms_openstack/charm/defaults.py index 57f6a2c..fd00d78 100644 --- a/charms_openstack/charm/defaults.py +++ b/charms_openstack/charm/defaults.py @@ -4,6 +4,7 @@ import charms.reactive as reactive from charms_openstack.charm.classes import OpenStackCharm from charms_openstack.charm.core import register_os_release_selector +from charms_openstack.charm.core import register_package_type_selector # The default handlers that charms.openstack provides. ALLOWED_DEFAULT_HANDLERS = [ @@ -14,6 +15,7 @@ ALLOWED_DEFAULT_HANDLERS = [ 'identity-service.available', 'config.changed', 'charm.default-select-release', + 'charm.default-select-package-type', 'update-status', 'upgrade-charm', ] @@ -23,6 +25,7 @@ _default_handler_map = {} # Used to store the discovered release version for caching between invocations OPENSTACK_RELEASE_KEY = 'charmers.openstack-release-version' +OPENSTACK_PACKAGE_TYPE_KEY = 'charmers.openstack-package-type' def use_defaults(*defaults): @@ -98,6 +101,31 @@ def make_default_select_release_handler(): return release_version +@_map_default_handler('charm.default-select-package-type') +def make_default_select_package_type_handler(): + """This handler is a bit more unusual, as it just sets the package type + selector using the @register_package_type_selector decorator + """ + + @register_package_type_selector + def default_select_package_type(): + """Determine the package type (snap or deb) based on the + openstack-origin setting. + + Note that this function caches the package type after the first + install so that it doesn't need to keep going and getting it from + the config information. + """ + package_type = unitdata.kv().get(OPENSTACK_PACKAGE_TYPE_KEY, None) + if package_type is None: + if os_utils.snap_install_requested(): + package_type = 'snap' + else: + package_type = 'deb' + unitdata.kv().set(OPENSTACK_PACKAGE_TYPE_KEY, package_type) + return package_type + + @_map_default_handler('amqp.connected') def make_default_amqp_connection_handler(): """Set the default amqp.connected state so that the default handler in diff --git a/unit_tests/charms_openstack/charm/test_classes.py b/unit_tests/charms_openstack/charm/test_classes.py index 1e48311..c1de68b 100644 --- a/unit_tests/charms_openstack/charm/test_classes.py +++ b/unit_tests/charms_openstack/charm/test_classes.py @@ -8,7 +8,8 @@ from unit_tests.charms_openstack.charm.common import MyOpenStackCharm import charms_openstack.charm.classes as chm import charms_openstack.charm.core as chm_core -TEST_CONFIG = {'config': True} +TEST_CONFIG = {'config': True, + 'openstack-origin': None} class TestOpenStackCharm__init__(BaseOpenStackCharmTest): @@ -236,6 +237,8 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest): self.patch_object(chm.os_utils, 'os_release') self.patch_object(chm, 'get_upstream_version', return_value='1.2.3') + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.target.version_package = None self.assertEqual(self.target.application_version, '1.2.3') self.get_upstream_version.assert_called_once_with('p1') @@ -244,6 +247,8 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest): self.patch_object(chm.os_utils, 'os_release') self.patch_object(chm, 'get_upstream_version', return_value='1.2.3') + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.assertEqual(self.target.application_version, '1.2.3') self.get_upstream_version.assert_called_once_with('p2') @@ -252,6 +257,8 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest): return_value='mitaka') self.patch_object(chm, 'get_upstream_version', return_value=None) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.assertEqual(self.target.application_version, 'mitaka') self.get_upstream_version.assert_called_once_with('p2') self.os_release.assert_called_once_with('p2') @@ -366,6 +373,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): self.patch_target('token_cache_pkgs', return_value=[]) self.patch_target('haproxy_enabled', return_value=False) self.patch_target('apache_enabled', return_value=False) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.assertEqual(['pkg1'], self.target.all_packages) self.token_cache_pkgs.return_value = ['memcache'] self.haproxy_enabled.return_value = True @@ -391,6 +400,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): self.patch_target('enable_memcache', return_value=True) self.patch_target('haproxy_enabled', return_value=True) self.patch_target('apache_enabled', return_value=True) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.assertEqual( self.target.full_restart_map, {'/etc/apache2/sites-available/openstack_https_frontend.conf': @@ -515,6 +526,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): self.patch_object( chm.subprocess, 'call', new=lambda x: apache_mods[x.pop()]) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.target.enable_apache_modules() self.check_call.assert_called_once_with( ['a2enmod', 'proxy_http']) @@ -523,6 +536,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): def test_configure_cert(self): self.patch_object(chm.ch_host, 'mkdir') self.patch_object(chm.ch_host, 'write_file') + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.target.configure_cert('mycert', 'mykey', cn='mycn') self.mkdir.assert_called_once_with(path='/etc/apache2/ssl/charmname') calls = [ @@ -564,6 +579,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): 'ssl_cert': base64.b64encode(b'cert'), 'ssl_ca': base64.b64encode(b'ca')} self.patch_target('config', new=config) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.assertEqual( self.target.get_certs_and_keys(), [{'key': 'key', 'cert': 'cert', 'ca': 'ca', 'cn': None}]) @@ -573,6 +590,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): 'ssl_key': base64.b64encode(b'key'), 'ssl_cert': base64.b64encode(b'cert')} self.patch_target('config', new=config) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.assertEqual( self.target.get_certs_and_keys(), [{'key': 'key', 'cert': 'cert', 'ca': None, 'cn': None}]) @@ -601,6 +620,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): self.patch_target( 'get_local_addresses', return_value=['int_addr', 'priv_addr', 'pub_addr', 'admin_addr']) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) expect = [ { 'ca': 'ca', @@ -664,6 +685,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): self.patch_object(chm_core.charmhelpers.fetch, 'apt_install', name='apt_install') + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.target.configure_ssl() cert_calls = [ mock.call('cert1', 'key1', cn='cn1'), @@ -684,6 +707,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): self.patch_object(chm.reactive.bus, 'set_state') self.patch_object(chm.reactive.RelationBase, 'from_state', return_value=None) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.target.configure_ssl() self.set_state.assert_called_once_with('ssl.enabled', False) @@ -693,6 +718,8 @@ class TestHAOpenStackCharm(BaseOpenStackCharmTest): self.patch_object(chm.reactive.bus, 'set_state') self.patch_object(chm.reactive.RelationBase, 'from_state', return_value='ssl_int') + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) self.target.configure_ssl() self.set_state.assert_called_once_with('ssl.enabled', False) self.configure_rabbit_cert.assert_called_once_with('ssl_int') diff --git a/unit_tests/charms_openstack/charm/test_core.py b/unit_tests/charms_openstack/charm/test_core.py index af2cbd0..1d7a275 100644 --- a/unit_tests/charms_openstack/charm/test_core.py +++ b/unit_tests/charms_openstack/charm/test_core.py @@ -60,10 +60,16 @@ class TestBaseOpenStackCharmMeta(BaseOpenStackCharmTest): class TestC2(chm_core.BaseOpenStackCharm): release = 'mitaka' + class TestC3(chm_core.BaseOpenStackCharm): + release = 'ocata' + package_type = 'snap' + self.assertTrue('liberty' in chm_core._releases.keys()) self.assertTrue('mitaka' in chm_core._releases.keys()) - self.assertEqual(chm_core._releases['liberty'], TestC1) - self.assertEqual(chm_core._releases['mitaka'], TestC2) + self.assertTrue('ocata' in chm_core._releases.keys()) + self.assertEqual(chm_core._releases['liberty']['deb'], TestC1) + self.assertEqual(chm_core._releases['mitaka']['deb'], TestC2) + self.assertEqual(chm_core._releases['ocata']['snap'], TestC3) def test_register_unknown_series(self): self.patch_object(chm_core, '_releases', new={}) @@ -557,6 +563,8 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest): self.apt_cache.return_value = { 'testpkg': pkg_mock} self.patch_object(chm_core.apt, 'upstream_version') + self.patch_object(chm_core.os_utils, 'snap_install_requested', + return_value=False) self.upstream_version.return_value = '3.0.0~b1' self.assertEqual( chm_core.BaseOpenStackCharm.get_os_codename_package( @@ -576,6 +584,8 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest): self.patch_target('package_codenames') self.patch_target('get_os_codename_package', return_value='my-series') + self.patch_object(chm_core.os_utils, 'snap_install_requested', + return_value=False) self.assertEqual( self.target.get_os_version_package('testpkg'), '2011.2') @@ -590,6 +600,8 @@ class TestMyOpenStackCharm(BaseOpenStackCharmTest): self.patch_object(chm_core, 'apt') self.patch_target('config', new={'openstack-origin': 'cloud:natty-folsom'}) + self.patch_object(chm_core.os_utils, 'snap_install_requested', + return_value=False) self.get_os_version_package.return_value = 2 self.get_os_version_install_source.return_value = 3 self.target.openstack_upgrade_available('testpkg') diff --git a/unit_tests/charms_openstack/charm/test_defaults.py b/unit_tests/charms_openstack/charm/test_defaults.py index 033888b..87a70b3 100644 --- a/unit_tests/charms_openstack/charm/test_defaults.py +++ b/unit_tests/charms_openstack/charm/test_defaults.py @@ -120,6 +120,49 @@ class TestDefaults(BaseOpenStackCharmTest): kv.set.assert_called_once_with(chm.OPENSTACK_RELEASE_KEY, 'two') self.os_release.assert_called_once_with('python-keystonemiddleware') + def test_default_select_package_type_handler(self): + self.assertIn('charm.default-select-package-type', + chm._default_handler_map) + self.patch_object(chm, 'register_package_type_selector') + h = self.mock_decorator_gen_simple() + self.register_package_type_selector.side_effect = h.decorator + # call the default handler installer function, and check its map. + f = chm._default_handler_map['charm.default-select-package-type'] + f() + self.assertIsNotNone(h.map['function']) + # verify that the installed function works + kv = mock.MagicMock() + self.patch_object(chm.unitdata, 'kv', new=lambda: kv) + self.patch_object(chm.os_utils, 'snap_install_requested', + return_value=False) + # set a package_type + kv.get.return_value = 'deb' + package_type = h.map['function']() + self.assertEqual(package_type, 'deb') + kv.set.assert_not_called() + kv.get.assert_called_once_with(chm.OPENSTACK_PACKAGE_TYPE_KEY, None) + + # No release set, ensure it calls snap_install_requested and + # sets package_type to 'snap' + kv.reset_mock() + kv.get.return_value = None + self.snap_install_requested.return_value = True + package_type = h.map['function']() + self.assertEqual(package_type, 'snap') + kv.set.assert_called_once_with(chm.OPENSTACK_PACKAGE_TYPE_KEY, 'snap') + self.snap_install_requested.assert_called_once_with() + + # No release set, ensure it calls snap_install_requested and + # sets package_type to 'deb' + kv.reset_mock() + kv.get.return_value = None + self.snap_install_requested.reset_mock() + self.snap_install_requested.return_value = False + package_type = h.map['function']() + self.assertEqual(package_type, 'deb') + kv.set.assert_called_once_with(chm.OPENSTACK_PACKAGE_TYPE_KEY, 'deb') + self.snap_install_requested.assert_called_once_with() + def test_default_amqp_connection_handler(self): self.assertIn('amqp.connected', chm._default_handler_map) self.patch_object(chm.reactive, 'set_state')