Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[master] Port #49891 (salt_version module) #55195

Merged
merged 4 commits into from
Dec 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/topics/releases/version_numbers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Code Names
To distinguish future releases from the current release, code names are used.
The periodic table is used to derive the next codename. The first release in
the date based system was code named ``Hydrogen``, each subsequent release will
go to the next `atomic number <https://en.wikipedia.org/wiki/List_of_elements>`.
go to the next `atomic number <https://en.wikipedia.org/wiki/List_of_elements>`_.

Assigned codenames:

Expand Down
176 changes: 176 additions & 0 deletions salt/modules/salt_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
'''
Access Salt's elemental release code-names.

.. versionadded:: Neon

Salt's feature release schedule is based on the Periodic Table, as described
in the :ref:`Version Numbers <version-numbers>` documentation.

When a feature was added (or removed) in a specific release, it can be
difficult to build out future-proof functionality that is dependent on
a naming scheme that moves.

For example, a state syntax needs to change to support an option that will be
removed in the future, but there are many Minion versions in use across an
infrastructure. It would be handy to use some Jinja syntax to check for these
instances to perform one state syntax over another.

A simple example might be something like the following:

.. code-block:: jinja

{# a boolean check #}
{% set option_deprecated = salt['salt_version.less_than']("Sodium") %}

{% if option_deprecated %}
<use old syntax>
{% else %}
<use new syntax>
{% endif %}

'''

# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging

# Import Salt libs
from salt.ext import six
import salt.version
import salt.utils.versions


log = logging.getLogger(__name__)

__virtualname__ = 'salt_version'


def __virtual__():
'''
Only work on POSIX-like systems
'''
return __virtualname__


def get_release_number(name):
'''
Returns the release number of a given release code name in a
``MAJOR.PATCH`` format.

If the release name has not been given an assigned release number, the
function returns a string. If the release cannot be found, it returns
``None``.

name
The release code name for which to find a release number.

CLI Example:

.. code-block:: bash

salt '*' salt_version.get_release_number 'Oxygen'
'''
name = name.lower()
version_map = salt.version.SaltStackVersion.LNAMES
version = version_map.get(name)
if version is None:
log.info('Version {} not found.'.format(name))
return None

if version[1] == 0:
log.info('Version {} found, but no release number has been assigned '
'yet.'.format(name))
return 'No version assigned.'

return '.'.join(str(item) for item in version)


def equal(name):
'''
Returns a boolean (True) if the minion's current version
code name matches the named version.

name
The release code name to check the version against.

CLI Example:

.. code-block:: bash

salt '*' salt_version.equal 'Oxygen'
'''
if _check_release_cmp(name) == 0:
log.info(
'The minion\'s version code name matches \'{}\'.'.format(name)
)
return True

return False


def greater_than(name):
'''
Returns a boolean (True) if the minion's current
version code name is greater than the named version.

name
The release code name to check the version against.

CLI Example:

.. code-block:: bash

salt '*' salt_version.greater_than 'Sodium'
'''
if _check_release_cmp(name) == 1:
log.info(
'The minion\'s version code name is greater than \'{}\'.'.format(name)
)
return True

return False


def less_than(name):
'''
Returns a boolean (True) if the minion's current
version code name is less than the named version.

name
The release code name to check the version against.

CLI Example:

.. code-block:: bash

salt '*' salt_version.less_than 'Sodium'
'''
if _check_release_cmp(name) == -1:
log.info(
'The minion\'s version code name is less than \'{}\'.'.format(name)
)
return True

return False


def _check_release_cmp(name):
'''
Helper function to compare the minion's current
Salt version to release code name versions.

If release code name isn't found, the function returns None. Otherwise, it
returns the results of the version comparison as documented by the
``versions_cmp`` function in ``salt.utils.versions.py``.
'''
map_version = get_release_number(name)
if map_version is None:
log.info('Release code name {} was not found.'.format(name))
return None

current_version = six.text_type(salt.version.SaltStackVersion(
*salt.version.__version_info__))
current_version = current_version.rsplit('.', 1)[0]
version_cmp = salt.utils.versions.version_cmp(current_version, map_version)
return version_cmp
180 changes: 180 additions & 0 deletions tests/unit/modules/test_salt_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
'''
Unit tests for salt/modules/salt_version.py
'''

# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals

# Import Salt Testing libs
from tests.support.unit import TestCase, skipIf
from tests.support.mock import (
MagicMock,
patch,
NO_MOCK,
NO_MOCK_REASON
)

# Import Salt libs
from salt.ext import six
import salt.modules.salt_version as salt_version
import salt.version


@skipIf(NO_MOCK, NO_MOCK_REASON)
class SaltVersionTestCase(TestCase):
'''
Test cases for salt.modules.salt_version
'''

def test_mocked_objects(self):
'''
Test that the mocked objects actually have what we expect.

For example, earlier tests incorrectly mocked the
salt.version.SaltStackVersion.LNAMES dict using upper-case indexes
'''
assert isinstance(salt.version.SaltStackVersion.LNAMES, dict)
for k, v in salt.version.SaltStackVersion.LNAMES.items():
assert k == k.lower()
assert isinstance(v, tuple)
assert len(v) == 2

sv = salt.version.SaltStackVersion(*salt.version.__version_info__).__str__()
assert isinstance(sv, six.string_types)

with patch('salt.version.SaltStackVersion.LNAMES', {'neon': (2019, 8)}):
sv = salt.version.SaltStackVersion.from_name('Neon')
self.assertEqual(sv.string, '2019.8.0')

# get_release_number tests: 3

def test_get_release_number_no_codename(self):
'''
Test that None is returned when the codename isn't found.
'''
assert salt_version.get_release_number('foo') is None

@patch('salt.version.SaltStackVersion.LNAMES', {'foo': (12345, 0)})
def test_get_release_number_unassigned(self):
'''
Test that a string is returned when a version is found, but unassigned.
'''
mock_str = 'No version assigned.'
assert salt_version.get_release_number('foo') == mock_str

def test_get_release_number_success(self):
'''
Test that a version is returned for a released codename
'''
assert salt_version.get_release_number('Oxygen') == '2018.3'

# equal tests: 3

@patch('salt.version.SaltStackVersion.LNAMES', {'foo': (1900, 5)})
@patch('salt.version.SaltStackVersion', MagicMock(return_value='1900.5.0'))
def test_equal_success(self):
'''
Test that the current version is equal to the codename
'''
assert salt_version.equal('foo') is True

@patch('salt.version.SaltStackVersion.LNAMES', {'oxygen': (2018, 3),
'nitrogen': (2017, 7)})
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_equal_older_codename(self):
'''
Test that when an older codename is passed in, the function returns False.
'''
assert salt_version.equal('Nitrogen') is False

@patch('salt.version.SaltStackVersion.LNAMES', {'fluorine': (salt.version.MAX_SIZE - 100, 0)})
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_equal_newer_codename(self):
'''
Test that when a newer codename is passed in, the function returns False
'''
assert salt_version.equal('Fluorine') is False

# greater_than tests: 4

@patch('salt.modules.salt_version.get_release_number', MagicMock(return_value='2017.7'))
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_greater_than_success(self):
'''
Test that the current version is newer than the codename
'''
assert salt_version.greater_than('Nitrogen') is True

@patch('salt.version.SaltStackVersion.LNAMES', {'oxygen': (2018, 3)})
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_greater_than_with_equal_codename(self):
'''
Test that when an equal codename is passed in, the function returns False.
'''
assert salt_version.greater_than('Oxygen') is False

@patch('salt.version.SaltStackVersion.LNAMES', {'fluorine': (2019, 2),
'oxygen': (2018, 3)})
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_greater_than_with_newer_codename(self):
'''
Test that when a newer codename is passed in, the function returns False.
'''
assert salt_version.greater_than('Fluorine') is False

@patch('salt.modules.salt_version.get_release_number', MagicMock(return_value='No version assigned.'))
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_greater_than_unassigned(self):
'''
Test that the unassigned codename is greater than the current version
'''
assert salt_version.greater_than('Fluorine') is False

# less_than tests: 4

@patch('salt.modules.salt_version.get_release_number', MagicMock(return_value='2019.2'))
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_less_than_success(self):
'''
Test that when a newer codename is passed in, the function returns True.
'''
assert salt_version.less_than('Fluorine') is True

@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
@patch('salt.version.SaltStackVersion.LNAMES', {'oxygen': (2018, 3)})
def test_less_than_with_equal_codename(self):
'''
Test that when an equal codename is passed in, the function returns False.
'''
assert salt_version.less_than('Oxygen') is False

@patch('salt.modules.salt_version.get_release_number', MagicMock(return_value='2017.7'))
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_less_than_with_older_codename(self):
'''
Test that the current version is less than the codename.
'''
assert salt_version.less_than('Nitrogen') is False

@patch('salt.modules.salt_version.get_release_number', MagicMock(return_value='No version assigned.'))
@patch('salt.version.SaltStackVersion', MagicMock(return_value='2018.3.2'))
def test_less_than_with_unassigned_codename(self):
'''
Test that when an unassigned codename greater than the current version.
'''
assert salt_version.less_than('Fluorine') is True

# _check_release_cmp tests: 2

def test_check_release_cmp_no_codename(self):
'''
Test that None is returned when the codename isn't found.
'''
assert salt_version._check_release_cmp('foo') is None

def test_check_release_cmp_success(self):
'''
Test that an int is returned from the version compare
'''
assert isinstance(salt_version._check_release_cmp('Oxygen'), int)