Merge upstream version

Related-PROD: PROD-28199

Change-Id: I5d9dbde1c3ac577fb30fa5d6b1ff18bcee28a0d7
diff --git a/cfg_checker/modules/packages/versions.py b/cfg_checker/modules/packages/versions.py
new file mode 100644
index 0000000..10f65dc
--- /dev/null
+++ b/cfg_checker/modules/packages/versions.py
@@ -0,0 +1,345 @@
+import csv
+import os
+
+from cfg_checker.common import config, logger, logger_cli, pkg_dir, const
+
+
+class PkgVersions(object):
+    _labels = []
+    _list = {}
+
+    dummy_desc = {
+        "component": "unlisted",
+        "app": "-",
+        "repo": "-",
+        "versions": {}
+    }
+
+    def __init__(self):
+        # preload csv file
+        logger_cli.info("# Preloading MCP release versions")
+        with open(os.path.join(pkg_dir, 'etc', config.pkg_versions_map)) as f:
+            _reader = csv.reader(f, delimiter=',')
+            # load packages
+            for row in _reader:
+                # load release version labels
+                if _reader.line_num == 1:
+                    self._labels = [v for v in row[5:]]
+                    continue
+                # package_name,component,application_or_service,repo,openstack_release,2018.4.0,2018.11.0,2019.2.0,2019.2.1,2019.2.2
+                # reassign for code readability
+                _pkg = row[0]
+                _component = row[1]
+                _app = row[2]
+                _repo = row[3]
+                # if release cell empty - use keyword 'any'
+                _os_release = row[4] if len(row[4]) > 0 else 'any' 
+
+                # prepare versions dict
+                _l = self._labels
+                _versions = {_l[i]:row[5+i] for i in range(0, len(row[5:]))}
+                
+                if _pkg in self._list:
+                    if _os_release in self._list[_pkg]["versions"]:
+                        # all pkg/os_releases should be uniq. If found, latest one used
+                        logger_cli.info(
+                            "-> WARNING: Duplicate package info found "
+                            "'{}' (line {})".format(
+                                _pkg,
+                                _reader.line_num
+                            )
+                        )
+                else:
+                    # update pkg data in list
+                    self._list.update({
+                        _pkg: {
+                            "component": _component,
+                            "app": _app,
+                            "repo": _repo,
+                            "versions": {}
+                        }
+                    })
+                
+                # and finally, update the versions for this release
+                self._list[_pkg]["versions"].update({
+                    _os_release: _versions
+                })
+    
+    def __getitem__(self, pkg_name):
+        if pkg_name in self._list:        
+            return self._list[pkg_name]
+        else:
+            #return self._dummy_desc
+            return None
+
+
+class DebianVersion(object):
+    epoch = None
+    epoch_status = const.VERSION_NA
+    upstream = None
+    upstream_rev = None
+    upstream_status = const.VERSION_NA
+    debian = None
+    debian_rev = None
+    debian_status = const.VERSION_NA
+
+    status = ""
+    version = ""
+
+    @staticmethod
+    def split_revision(version_fragment):
+        # The symbols are -, +, ~
+        _symbols = ['-', '+', '~']
+        # nums, coz it is faster then regex
+        _chars = [46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57]
+        _ord_map = [ord(ch) not in _chars for ch in version_fragment]
+        # if there is nothing to extract, return at once
+        if not any([_s in version_fragment for _s in _symbols]) \
+            and not any(_ord_map):
+            # no revisions
+            return version_fragment, ""
+        else:
+            _main = _rev = ""
+            # get indices
+            _indices = []
+            for _s in _symbols:
+                if _s in version_fragment:
+                    _indices.append(version_fragment.index(_s))
+            for _s in version_fragment:
+                if ord(_s) not in _chars:
+                    _indices.append(version_fragment.index(_s))
+            # sort indices
+            _indices.sort()
+            # extract starting from the lowest one
+            _main = version_fragment[:_indices[0]]
+            _rev = version_fragment[_indices[0]:]
+            return _main, _rev
+    
+    def __init__(self, version_string):
+        # save
+        if len(version_string) < 1:
+            self.epoch = None
+            self.upstream = None
+            self.debian = None
+            self.version = 'n/a'
+            return
+        else:
+            # do parse the main versions
+            _v = version_string
+            # colon presence, means epoch present
+            _e = _v.split(':', 1)[0] if ':' in _v else ''
+            # if epoch was there, upstream should be cut
+            _m = _v if ':' not in _v else _v.split(':', 1)[1]
+            # dash presence, means debian present
+            _d = _m.rsplit('-', 1)[1] if '-' in _m else ''
+            # if debian was there, upstream version should be cut
+            _m = _m if '-' not in _m else _m.rsplit('-', 1)[0]
+
+            self.epoch = _e
+            self.upstream, self.upstream_rev = self.split_revision(_m)
+            self.debian, self.debian_rev = self.split_revision(_d)
+            self.version = version_string
+    
+    # Following functions is a freestyle python mimic of apt's upstream, enjoy
+    # https://github.com/chaos/apt/blob/master/apt/apt-pkg/deb/debversion.cc#L42
+    # mimic produced in order not to pull any packages or call external code
+    @staticmethod
+    def _cmp_fragment(lhf, rhf):
+        # search for difference
+        # indices
+        _li = _ri = 0
+        # pre-calc len
+        _lL = len(lhf)
+        _rL = len(rhf)
+        # bool for compare found
+        _diff = False
+        while _li < _lL and _ri < _rL:
+            # iterate lists
+            _num = lhf[_li] - rhf[_ri]
+            if _num:
+                return _num
+            _li += 1
+            _ri += 1
+        
+        # diff found? lens equal?
+        if not _diff and _lL != _rL:
+            # lens not equal? Longer - later
+            return _lL - _rL
+        else:
+            # equal
+            return 0
+    
+    def _cmp_num(self, lf, rf):
+        # split fragments into lists
+        _lhf = lf.split('.') if '.' in lf else list(lf)
+        _rhf = rf.split('.') if '.' in rf else list(rf)
+        # cast them to ints, delete empty strs
+        _lhf = [int(n) for n in _lhf if len(n)]
+        _rhf = [int(n) for n in _rhf if len(n)]
+
+        return self._cmp_fragment(_lhf, _rhf)
+    
+    def _cmp_lex(self, lf, rf):
+        # cast each item into its ORD value
+        _lhf = [ord(n) for n in lf]
+        _rhf = [ord(n) for n in rf]
+
+        return self._cmp_fragment(_lhf, _rhf)        
+   # end of cmps
+
+    # main part compared using splitted numbers
+    # if equal, revision is compared using lexical comparizon
+    def __lt__(self, v):
+        if self._cmp_num(self.epoch, v.epoch) < 0:
+            return True
+        elif self._cmp_num(self.upstream, v.upstream) < 0:
+            return True
+        elif self._cmp_lex(self.upstream_rev, v.upstream_rev) < 0:
+            return True
+        else:
+            return False
+
+    def __eq__(self, v):
+        # compare all portions
+        _result = []
+        _result.append(self._cmp_num(self.epoch, v.epoch))
+        _result.append(self._cmp_num(self.upstream, v.upstream))
+        _result.append(self._cmp_lex(self.upstream_rev, v.upstream_rev))
+        # if there is any non-zero, its not equal
+        return not any(_result)
+
+    def __gt__(self, v):
+        if self._cmp_num(self.epoch, v.epoch) > 0:
+            return True
+        elif self._cmp_num(self.upstream, v.upstream) > 0:
+            return True
+        elif self._cmp_lex(self.upstream_rev, v.upstream_rev) > 0:
+            return True
+        else:
+            return False
+    
+    def update_parts(self, target, status):
+        # updating parts of version statuses
+        if self._cmp_num(self.epoch, target.epoch) != 0:
+            self.epoch_status = status
+        else:
+            self.epoch_status = const.VERSION_OK
+
+        if self._cmp_num(self.upstream, target.upstream) != 0 \
+            or self._cmp_lex(self.upstream_rev, target.upstream_rev) != 0:
+            self.upstream_status = status
+        else:
+            self.upstream_status = const.VERSION_OK
+
+        if self._cmp_lex(self.debian, target.debian) != 0 \
+            or self._cmp_lex(self.debian_rev, target.debian_rev) != 0:
+            self.debian_status = status
+        else:
+            self.debian_status = const.VERSION_OK
+
+
+class VersionCmpResult(object):
+    status = ""
+    action = ""
+
+    source = None
+    target = None
+
+
+    def __init__(self, i, c, r):
+        # compare three versions and write a result
+        self.source = i
+        self.status = const.VERSION_NA
+        self.action = const.ACT_NA
+        
+        # Check if there is a release version present
+        if r and len(r.version) > 0 and r.version != 'n/a':
+            # I < C, installed version is older
+            if i < c:
+                self.target = c
+                if i == r:
+                    # installed version is equal vs release version
+                    self.status = const.VERSION_OK
+                    self.action = const.ACT_UPGRADE
+                elif i > r:
+                    # installed version is newer vs release version
+                    self.status = const.VERSION_UP
+                    self.action = const.ACT_UPGRADE
+                elif i < r and r < c:
+                    # installed version is older vs release version
+                    self.status = const.VERSION_ERR
+                    self.action = const.ACT_NEED_UP
+                    self.target = r
+                elif i < r and c == r:
+                    # installed version is older vs release version
+                    self.status = const.VERSION_ERR
+                    self.action = const.ACT_NEED_UP
+                    self.target = c
+                elif c < r:
+                    # installed and repo versions older vs release version
+                    self.status = const.VERSION_ERR
+                    self.action = const.ACT_REPO
+            # I > C
+            # installed version is newer
+            elif i > c:
+                self.target = c
+                if c == r:
+                    # some unknown version installed
+                    self.status = const.VERSION_ERR
+                    self.action = const.ACT_NEED_DOWN
+                elif c > r:
+                    # installed and repo versions newer than release
+                    self.status = const.VERSION_UP
+                    self.action = const.ACT_NEED_DOWN
+                elif c < r and r < i:
+                    # repo is older vs release and both older vs installed
+                    self.status = const.VERSION_UP
+                    self.action = const.ACT_REPO
+                elif c < r and r == i:
+                    # repo is older vs release, but release version installed
+                    self.status = const.VERSION_OK
+                    self.action = const.ACT_REPO
+                elif i < r:
+                    # both repo and installed older vs release, new target
+                    self.status = const.VERSION_DOWN
+                    self.action = const.ACT_REPO
+                    self.target = r
+            # I = C
+            # installed and linked repo is inline,
+            elif i == c:
+                self.target = c
+                if i < r:
+                    # both are old, new target
+                    self.status = const.VERSION_ERR
+                    self.action = const.ACT_REPO
+                    self.target = r
+                elif i > r:
+                    # both are newer, same target
+                    self.status = const.VERSION_UP
+                    self.action = const.ACT_NA
+                elif i == r:
+                    # all is ok
+                    self.status = const.VERSION_OK
+                    self.action = const.ACT_NA
+        else:
+            # no release version present
+            self.target = c
+            if i < c:
+                self.status = const.VERSION_OK
+                self.action = const.ACT_UPGRADE
+            elif i > c:
+                self.status = const.VERSION_UP
+                self.action = const.ACT_NEED_DOWN
+            elif i == c:
+                self.status = const.VERSION_OK
+                self.action = const.ACT_NA
+        
+        # and we need to update per-part status
+        self.source.update_parts(self.target, self.status)
+
+    @staticmethod
+    def deb_lower(_s, _t):
+        if _t.debian and _t.debian > _s.debian:
+            return True
+        else:
+            return false