######################################################################## # Copyright (C) 2019-2020 VMware, Inc. # # All Rights Reserved # ######################################################################## ''' This module defines the data structure of addon and implements the functionality such as construction, serialization to json format, and deserialization from json. ''' import json import re from .ComponentScanner import ComponentScanProblem from .Errors import AddonValidationError from .ReleaseUnit import (ATTR_REL_ID, checkNameSpec, deepcopy, ESX_COMP_NAME, ReleaseUnit) try: from .Utils.JsonSchema import ValidateAddon HAVE_VALIDATE_ADDON = True except Exception: HAVE_VALIDATE_ADDON = False ERROR_REMOVE_ESX = 'The component ESXi cannot be removed from base image' # Attribute name constant ATTR_NAME_SPEC = 'nameSpec' ATTR_REM_COMPS = 'removedComponents' ATTR_SUPP_BIVERS = 'supportedBaseImageVersions' # Validation constants SUP_BIVER_REG_EXP = \ r'^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+){0,2}(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)*$' def GenerateReleaseID(name, version): return name + ':' + version class Addon(ReleaseUnit): ''' An add-on is a release unit that: 1. Only has one name spec, one version spec. 2. Directly composed from components 3. Has no component 'esx' ''' extraAttributes = [ATTR_NAME_SPEC, ATTR_REM_COMPS, ATTR_SUPP_BIVERS] extraDefault = [None, [], []] extraMap = dict(zip(extraAttributes, extraDefault)) mandatoryAttr = list(ReleaseUnit.mandatoryAttr) mandatoryAttr.append(ATTR_NAME_SPEC) mandatoryAttr.append(ATTR_SUPP_BIVERS) # The schema version. SCHEMA_VERSION = "1.0" # Valid schema veriosn map to release version. Need to populate when bump # schema version. SCHEMA_VERSION_MAP = {"1.0": '7.0.0'} # The common software spec type for all instances of this class. releaseType = 'addon' def _validateSupportedBIVersions(self, baseImageVers): '''The function validates the addon's supported baseimage versions. Each version string in the list must match the regular expression SUP_BIVER_REG_EXP. TODO: The list of baseImageVer should be non-overlapping. In future, add the required check. Parameter: * baseImageVers: List of base image versions. Exception: * Return False If the list of BI versions is empty, overlapping, or each version string violated version pattern SUP_BIVER_REG_EXP else True ''' if not isinstance(baseImageVers, list) or \ not baseImageVers: return False for ver in baseImageVers: if not re.match(SUP_BIVER_REG_EXP, ver): return False return True @checkNameSpec def SetNameSpec(self, name): self._nameSpec = name self._GenerateReleaseID() def AddRemovedComponent(self, name): if name == ESX_COMP_NAME: raise ValueError(ERROR_REMOVE_ESX) if name not in self._removedComponents: self._removedComponents.append(name) def RemoveRemovedComponent(self, name): try: self._removedComponents.remove(name) except ValueError: raise ValueError('%s is not in removed component list.' % name) def SetRemovedComponents(self, compNameList): if compNameList and ESX_COMP_NAME in compNameList: raise ValueError(ERROR_REMOVE_ESX) self._removedComponents = compNameList def SetSupportedBaseImageVersions(self, supBaseImageVers): if not self._validateSupportedBIVersions(supBaseImageVers): raise ValueError('The supported base image versions is a non-empty ' 'list. The versions in the list must be of form ' '[x(.x){0,2}(-x(.x)*)*], where x is alphanumeric.') self._supportedBaseImageVersions = supBaseImageVers nameSpec = property(lambda self: self._nameSpec, SetNameSpec) removedComponents = property(lambda self: self._removedComponents, SetRemovedComponents) supportedBaseImageVersions = property(lambda self: self._supportedBaseImageVersions, SetSupportedBaseImageVersions) @classmethod def FromJSON(cls, jsonString, validation=False): # Schema Validation if validation and HAVE_VALIDATE_ADDON: valid, errMsg = ValidateAddon(jsonString) if not valid: try: addon = json.loads(jsonString) except Exception: # failed to load addon from jsonstring # hence return empty releaseID raise AddonValidationError('', errMsg) releaseId = addon[ATTR_REL_ID] if ATTR_REL_ID in addon else '' raise AddonValidationError(releaseId, errMsg) addOn = Addon(spec=jsonString) if validation: addOn.Validate() return addOn def Validate(self, components=None, addonVibs=None): """Validates the addon. Addon should have at least one component and there should be no conflict/obsolete problems within the components. Parameters: * components - ComponentCollection object having all addon components * addonVibs - VibCollection object with VIBs that correspond to all components in addon. """ if not self.components and not self.removedComponents: raise AddonValidationError(self.releaseID, 'AddOn should have at ' 'least one component or at least remove' ' one component.') if components and addonVibs: compProblems = self._getCompProblemMsgs(components, addonVibs) if compProblems: raise AddonValidationError(self.releaseID, 'Failed to validate components in addon %s: %s' % (self.nameSpec.name, ', '.join(compProblems))) def _getCompProblemMsgs(self, components, vibs): """Validate component relations and return messages of problems. """ problems = components.Validate(vibs) # Collect all problems. Addon cannot contain components that # conflict/obsolete themselves or each other. return [p.msg for p in problems.values() if p.reltype != ComponentScanProblem.TYPE_DEPENDS] def ToJSONDict(self): releaseObj = super(Addon, self).ToJSONDict() # encode NameSpec to dict releaseObj[ATTR_NAME_SPEC] = deepcopy(self.nameSpec.ToJSONDict()) releaseObj[ATTR_REM_COMPS] = deepcopy(self.removedComponents) return releaseObj def ToJSON(self): self.Validate() jsonString = super(Addon, self).ToJSON() # Schema Validation if HAVE_VALIDATE_ADDON: valid, errMsg = ValidateAddon(jsonString) if not valid: raise AddonValidationError(self.releaseID, errMsg) return jsonString def Copy(self): addOn = Addon() addonDict = deepcopy(self.ToJSONDict()) addOn.FromJSONDict(addonDict) return addOn def _GenerateReleaseID(self): name = self._nameSpec.name if self._nameSpec else '' version = self._versionSpec.version.versionstring \ if self._versionSpec else '' self._releaseID = GenerateReleaseID(name, version)