diff options
author | Michal Minar <miminar@redhat.com> | 2013-03-20 12:59:49 +0100 |
---|---|---|
committer | Michal Minar <miminar@redhat.com> | 2013-03-20 12:59:49 +0100 |
commit | eed3cc9b4b2a78e238ece6dab185e7c76ab13142 (patch) | |
tree | 8df1d6b3a3a60e42a27882e3d8003de8cddfa92c | |
parent | 19ab3372fe708be3e937df5d1ed6945ac813b51c (diff) | |
download | openlmi-providers-eed3cc9b4b2a78e238ece6dab185e7c76ab13142.tar.gz openlmi-providers-eed3cc9b4b2a78e238ece6dab185e7c76ab13142.tar.xz openlmi-providers-eed3cc9b4b2a78e238ece6dab185e7c76ab13142.zip |
added new providers, allowed for asynchronous jobs
new providers:
LMI_SoftwareInstallationJob
LMI_SoftwareInstallationService
27 files changed, 5036 insertions, 367 deletions
diff --git a/mof/LMI_Software.mof b/mof/LMI_Software.mof index 35f2d79..8a2ca10 100644 --- a/mof/LMI_Software.mof +++ b/mof/LMI_Software.mof @@ -294,3 +294,329 @@ class LMI_HostedSoftwareIdentityResource : CIM_HostedAccessPoint { }; +class LMI_SoftwareInstallationService : CIM_SoftwareInstallationService { + + [Implemented(true), Override("CreationClassName")] + string CreationClassName; + + [Implemented(true), Override("Name")] + string Name; + + [Implemented(true), Override("SystemCreationClassName")] + string SystemCreationClassName; + + [Implemented(true), Override("SystemName")] + string SystemName; + + [Implemented(true), Override("Caption")] + string Caption; + + [Implemented(true), Override("CommunicationStatus")] + uint16 CommunicationStatus; + + [Implemented(true), Override("Description")] + string Description; + + [Implemented(true), Override("DetailedStatus")] + uint16 DetailedStatus; + + [Implemented(true), Override("EnabledDefault")] + uint16 EnabledDefault; + + [Implemented(true), Override("EnabledState")] + uint16 EnabledState; + + [Implemented(true), Override("HealthState")] + uint16 HealthState; + + [Implemented(true), Override("InstanceID")] + string InstanceID; + + [Implemented(true), Override("OperatingStatus")] + uint16 OperatingStatus; + + [Implemented(true), Override("OperationalStatus")] + uint16 OperationalStatus[]; + + [Implemented(true), Override("PrimaryStatus")] + uint16 PrimaryStatus; + + [Implemented(true), Override("PrimaryStatus")] + uint16 RequestedState; + + [Implemented(true), Override("Started")] + boolean Started; + + [Implemented(true), Override("TransitioningToState")] + uint16 TransitioningToState; + + [Implemented(True), Override("CheckSoftwareIdentity")] + uint32 CheckSoftwareIdentity( + [IN, Description ( + "Reference to the SoftwareIdentity to be checked." )] + LMI_SoftwareIdentity REF Source, + [IN, Description ( + "Reference to the ManagedElement that the Software " + "Identity is going to be installed in (or updated)." )] + CIM_ManagedElement REF Target, + [IN, Description ( + "Reference to the Collection to which the Software " + "Identity will be added." )] + LMI_SystemSoftwareCollection REF Collection, + [IN ( false ), OUT, Description ( + "The parameter describes the characteristics of the " + "installation/update that will take place if the " + "Source Software Identity is installed: \n" + "Target automatic reset: The target element will " + "automatically reset once the installation is " + "complete. \n" + "System automatic reset: The containing system of " + "the target ManagedElement (normally a logical " + "device or the system itself) will automatically " + "reset/reboot once the installation is complete. \n" + "Separate target reset required: " + "EnabledLogicalElement.RequestStateChange MUST be " + "used to reset the target element after the " + "SoftwareIdentity is installed. \n" + "Separate system reset required: " + "EnabledLogicalElement.RequestStateChange MUST be " + "used to reset/reboot the containing system of the " + "target ManagedElement after the SoftwareIdentity " + "is installed. \n" + "Manual Reboot Required: The system MUST be " + "manually rebooted by the user. \n" + "No reboot required : No reboot is required after " + "installation. \n" + "User Intervention Recomended : It is recommended " + "that a user confirm installation of this " + "SoftwareIdentity. Inappropriate application MAY " + "have serious consequences. \n" + "MAY be added to specified collection : The " + "SoftwareIndentity MAY be added to specified " + "Collection." ), + ValueMap { "2", "3", "4", "5", "6", "7", "8", "9", + "..", "0x7FFF..0xFFFF" }, + Values { "Target automatic reset", + "System automatic reset", + "Separate target reset Required", + "Separate system reset Required", + "Manual Reboot Required", "No Reboot Required", + "User Intervention recommended", + "MAY be added to specified Collection", + "DMTF Reserved", "Vendor Specific" }] + uint16 InstallCharacteristics[]); + + [Implemented(True), Override("InstallFromSoftwareIdentity")] + uint32 InstallFromSoftwareIdentity( + [IN ( false ), OUT, Description ( + "Reference to the job (may be null if job completed)." + )] + LMI_SoftwareInstallationJob REF Job, + [IN, Description ( + "Options to control the install process.\n" + "Defer target/system reset : do not automatically " + "reset the target/system.\n" + "Force installation : Force the installation of the " + "same or an older SoftwareIdentity. Install: " + "Perform an installation of this software on the " + "managed element.\n" + "Update: Perform an update of this software on the " + "managed element.\n" + "Repair: Perform a repair of the installation of " + "this software on the managed element by forcing " + "all the files required for installing the software " + "to be reinstalled.\n" + "Reboot: Reboot or reset the system immediately " + "after the install or update of this software, if " + "the install or the update requires a reboot or reset.\n" + "Password: Password will be specified as clear text " + "without any encryption for performing the install " + "or update.\n" + "Uninstall: Uninstall the software on the managed element.\n" + "Log: Create a log for the install or update of the software.\n" + "SilentMode: Perform the install or update without " + "displaying any user interface.\n" + "AdministrativeMode: Perform the install or update " + "of the software in the administrative mode. " + "ScheduleInstallAt: Indicates the time at which " + "theinstall or update of the software will occur." ), + ValueMap { "2", "3", "4", "5", "6", "7", "8", "9", + "10", "11", "12", "13", "..", "32768..65535" }, + Values { "Defer target/system reset", + "Force installation", "Install", "Update", "Repair", + "Reboot", "Password", "Uninstall", "Log", + "SilentMode", "AdministrativeMode", + "ScheduleInstallAt", "DMTF Reserved", + "Vendor Specific" }, + ArrayType ( "Indexed" ), + ModelCorrespondence { + "CIM_SoftwareInstallationService.InstallOptionsValues[]", + "CIM_SoftwareInstallationServiceCapabilities.SupportedInstallOptions[]" }] + uint16 InstallOptions[], + [IN, Description ( + "InstallOptionsValues is an array of strings " + "providing additional information to InstallOptions " + "for the method to install the software. Each entry " + "of this array is related to the entry in " + "InstallOptions that is located at the same index " + "providing additional information for " + "InstallOptions. \n" + "If the index in InstallOptions has the value " + "\"Password \" then a value at the corresponding " + "index of InstallOptionValues shall not be NULL. \n" + "If the index in InstallOptions has the value " + "\"ScheduleInstallAt\" then the value at the " + "corresponding index of InstallOptionValues shall " + "not be NULL and shall be in the datetime type " + "format. \n" + "If the index in InstallOptions has the value \"Log " + "\" then a value at the corresponding index of " + "InstallOptionValues may be NULL. \n" + "If the index in InstallOptions has the value " + "\"Defer target/system reset\", \"Force " + "installation\",\"Install\", \"Update\", \"Repair\" " + "or \"Reboot\" then a value at the corresponding " + "index of InstallOptionValues shall be NULL." ), + ArrayType ( "Indexed" ), + ModelCorrespondence { + "CIM_SoftwareInstallationService.InstallOptions[]" }] + string InstallOptionsValues[], + [IN, Description ( + "Reference to the source of the install." )] + LMI_SoftwareIdentity REF Source, + [IN, Description ( + "The installation target. If NULL then the " + "SOftwareIdentity will be added to Collection only. " + "The underlying implementation is expected to be " + "able to obtain any necessary metadata from the " + "Software Identity." )] + CIM_ManagedElement REF Target, + [IN, Description ( + "Reference to the Collection to which the Software " + "Identity SHALL be added. If NULL then the Software " + "Identity will not be added to a Collection." )] + LMI_SystemSoftwareCollection REF Collection); + + [Implemented(True), Override("InstallFromURI")] + uint32 InstallFromURI( + [IN ( false ), OUT, Description ( + "Reference to the job (may be null if job completed)." + )] + LMI_SoftwareInstallationJob REF Job, + [IN, Description ( + "A URI for the software based on RFC 2079." )] + string URI, + [IN, Description ( "The installation target." )] + CIM_ManagedElement REF Target, + [IN, Description ( + "Options to control the install process. \n" + "See the InstallOptions parameter of the " + "SoftwareInstallationService.InstallFromSoftwareIdentity " + "method for the description of these values." ), + ValueMap { "2", "3", "4", "5", "6", "7", "8", "9", + "10", "11", "12", "13", "..", "32768..65535" }, + Values { "Defer target/system reset", + "Force installation", "Install", "Update", "Repair", + "Reboot", "Password", "Uninstall", "Log", + "SilentMode", "AdministrativeMode", + "ScheduleInstallAt", "DMTF Reserved", + "Vendor Specific" }, + ArrayType ( "Indexed" ), + ModelCorrespondence { + "CIM_SoftwareInstallationService.InstallFromURI.InstallOptionsValues[]", + "CIM_SoftwareInstallationServiceCapabilities.SupportedInstallOptions[]" }] + uint16 InstallOptions[], + [IN, Description ( + "InstallOptionsValues is an array of strings " + "providing additionalinformation to InstallOptions " + "for the method to install the software. Each entry " + "of this array is related to the entry in " + "InstallOptions that is located at the same index " + "providing additional information for " + "InstallOptions. \n" + "For further information on the use of " + "InstallOptionsValues parameter, see the " + "description of the InstallOptionsValues parameter " + "of the " + "SoftwareInstallationService.InstallFromSoftwareIdentity " + "method." ), + ArrayType ( "Indexed" ), + ModelCorrespondence { + "CIM_SoftwareInstallationService.InstallFromByteStream.InstallOptions[]" }] + string InstallOptionsValues[]); + +}; + +class LMI_SoftwareInstallationJob : CIM_ConcreteJob { + + [Implemented(true), Override("InstanceID")] + string InstanceID; + + [Implemented(true), Override("Caption")] + string Caption; + + [Implemented(true), Override("CommunicationStatus")] + uint16 CommunicationStatus; + + [Implemented(true), Override("DeleteOnCompletion")] + boolean DeleteOnCompletion; + + [Implemented(true), Override("Description")] + string Description; + + [Implemented(true), Override("ElapsedTime")] + datetime ElapsedTime; + + [Implemented(true), Override("ElementName")] + string ElementName; + + [Implemented(true), Override("ErrorCode")] + uint16 ErrorCode; + + [Implemented(true), Override("ErrorDescription")] + string ErrorDescription; + + [Implemented(true), Override("JobState")] + uint16 JobState; + + [Implemented(true), Override("JobStatus")] + string JobStatus; + + [Implemented(true), Override("MethodName")] + string MethodName; + + [Implemented(true), Override("Name")] + string Name; + + [Implemented(true), Override("OperationalStatus")] + uint16 OperationalStatus[]; + + [Implemented(true), Override("Owner")] + string Owner; + + [Implemented(true), Override("PercentComplete")] + uint16 PercentComplete; + + [Implemented(true), Override("Priority")] + uint32 Priority; + + [Implemented(true), Override("RecoveryAction")] + uint16 RecoveryAction; + + [Implemented(true), Override("StartTime")] + datetime StartTime; + + [Implemented(true), Override("StatusDescriptions")] + string StatusDescriptions[]; + + [Implemented(true), Override("TimeBeforeRemoval")] + datetime TimeBeforeRemoval; + + [Implemented(true), Override("TimeOfLastStateChange")] + datetime TimeOfLastStateChange; + + [Implemented(true), Override("TimeSubmitted")] + datetime TimeSubmitted; + +}; + diff --git a/mof/LMI_Software.reg b/mof/LMI_Software.reg index 2383e44..66cd50f 100644 --- a/mof/LMI_Software.reg +++ b/mof/LMI_Software.reg @@ -46,3 +46,14 @@ type: instance association namespace: root/cimv2 +[LMI_SoftwareInstallationService] + provider: /usr/lib/python2.7/site-packages/openlmi/software/cimom_entry.py + location: pyCmpiProvider + type: instance method + namespace: root/cimv2 + +[LMI_SoftwareInstallationJob] + provider: /usr/lib/python2.7/site-packages/openlmi/software/cimom_entry.py + location: pyCmpiProvider + type: instance method + namespace: root/cimv2 diff --git a/src/software/openlmi/software/LMI_HostedSoftwareCollection.py b/src/software/openlmi/software/LMI_HostedSoftwareCollection.py index ad8b6bf..2783d95 100644 --- a/src/software/openlmi/software/LMI_HostedSoftwareCollection.py +++ b/src/software/openlmi/software/LMI_HostedSoftwareCollection.py @@ -25,7 +25,7 @@ import pywbem from pywbem.cim_provider2 import CIMProvider2 from openlmi.common import cmpi_logging -from openlmi.software.core import ComputerSystem, SystemSoftwareCollection +from openlmi.software.core import ComputerSystem, SystemCollection class LMI_HostedSoftwareCollection(CIMProvider2): """Instrument the CIM class LMI_HostedSoftwareCollection @@ -69,12 +69,12 @@ class LMI_HostedSoftwareCollection(CIMProvider2): raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, "Missing Antecedent key property!") ComputerSystem.check_path_property(env, model, "Antecedent") - SystemSoftwareCollection.check_path_property(env, model, "Dependent") + SystemCollection.check_path_property(env, model, "Dependent") model.path.update({"Antecedent":None, "Dependent":None}) model["Antecedent"] = ComputerSystem.get_path() - model["Dependent"] = SystemSoftwareCollection.get_path() + model["Dependent"] = SystemCollection.get_path() return model @@ -107,7 +107,7 @@ class LMI_HostedSoftwareCollection(CIMProvider2): model.path.update({'Dependent': None, 'Antecedent': None}) model["Antecedent"] = ComputerSystem.get_path() - model["Dependent"] = SystemSoftwareCollection.get_path() + model["Dependent"] = SystemCollection.get_path() yield model diff --git a/src/software/openlmi/software/LMI_HostedSoftwareIdentityResource.py b/src/software/openlmi/software/LMI_HostedSoftwareIdentityResource.py index 09ab5d3..05f7725 100644 --- a/src/software/openlmi/software/LMI_HostedSoftwareIdentityResource.py +++ b/src/software/openlmi/software/LMI_HostedSoftwareIdentityResource.py @@ -31,7 +31,7 @@ from pywbem.cim_provider2 import CIMProvider2 from openlmi.common import cmpi_logging from openlmi.software.core import generate_references from openlmi.software.core import ComputerSystem -from openlmi.software.core import SoftwareIdentityResource +from openlmi.software.core import IdentityResource from openlmi.software.yumdb import YumDB @cmpi_logging.trace_function @@ -46,8 +46,7 @@ def generate_system_referents(env, object_name, model, _keys_only): host=model.path.host) model["Antecedent"] = ComputerSystem.get_path() for repo in YumDB.get_instance().get_repository_list('all'): - model["Dependent"] = SoftwareIdentityResource.repo2model( - repo, model=repomodel) + model["Dependent"] = IdentityResource.repo2model(repo, model=repomodel) yield model @cmpi_logging.trace_function @@ -55,10 +54,10 @@ def generate_repository_referents(env, object_name, model, _keys_only): """ Handler for referents enumeration request. """ - repo = SoftwareIdentityResource.object_path2repo( + repo = IdentityResource.object_path2repo( env, object_name, kind='all') model["Antecedent"] = ComputerSystem.get_path() - model["Dependent"] = SoftwareIdentityResource.repo2model(repo) + model["Dependent"] = IdentityResource.repo2model(repo) yield model class LMI_HostedSoftwareIdentityResource(CIMProvider2): @@ -113,7 +112,7 @@ class LMI_HostedSoftwareIdentityResource(CIMProvider2): raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, 'Unknown repository "%s".' % repoid) model["Antecedent"] = ComputerSystem.get_path() - model["Dependent"] = SoftwareIdentityResource.repo2model(repos[0]) + model["Dependent"] = IdentityResource.repo2model(repos[0]) return model @@ -152,7 +151,7 @@ class LMI_HostedSoftwareIdentityResource(CIMProvider2): classname='LMI_SoftwareIdentityResource', namespace='root/cimv2') for repo in repolist: - model["Dependent"] = SoftwareIdentityResource.repo2model( + model["Dependent"] = IdentityResource.repo2model( repo, model=repomodel) yield model diff --git a/src/software/openlmi/software/LMI_InstalledSoftwareIdentity.py b/src/software/openlmi/software/LMI_InstalledSoftwareIdentity.py index b4c1b6f..b349afd 100644 --- a/src/software/openlmi/software/LMI_InstalledSoftwareIdentity.py +++ b/src/software/openlmi/software/LMI_InstalledSoftwareIdentity.py @@ -31,7 +31,7 @@ from pywbem.cim_provider2 import CIMProvider2 from openlmi.common import cmpi_logging from openlmi.software.core import generate_references from openlmi.software.core import ComputerSystem -from openlmi.software.core import SoftwareIdentity +from openlmi.software.core import Identity from openlmi.software.yumdb import YumDB @cmpi_logging.trace_function @@ -48,7 +48,7 @@ def generate_system_referents(env, object_name, model, _keys_only): model["System"] = ComputerSystem.get_path() with YumDB.get_instance() as ydb: for pkg_info in ydb.get_package_list('installed', sort=True): - model["InstalledSoftware"] = SoftwareIdentity.pkg2model( + model["InstalledSoftware"] = Identity.pkg2model( pkg_info, model=pkg_model) yield model @@ -57,8 +57,8 @@ def generate_package_referents(_env, object_name, model, _keys_only): """ Handler for referents enumeration request. """ - pkg_info = SoftwareIdentity.object_path2pkg(object_name, kind="installed") - model['InstalledSoftware'] = SoftwareIdentity.pkg2model(pkg_info) + pkg_info = Identity.object_path2pkg(object_name, kind="installed") + model['InstalledSoftware'] = Identity.pkg2model(pkg_info) model["System"] = ComputerSystem.get_path() yield model @@ -108,9 +108,9 @@ class LMI_InstalledSoftwareIdentity(CIMProvider2): model["System"] = model.path["System"] = ComputerSystem.get_path() with YumDB.get_instance(): - pkg_info = SoftwareIdentity.object_path2pkg( + pkg_info = Identity.object_path2pkg( model['InstalledSoftware'], kind='installed') - model['InstalledSoftware'] = SoftwareIdentity.pkg2model(pkg_info) + model['InstalledSoftware'] = Identity.pkg2model(pkg_info) return model @cmpi_logging.trace_method @@ -147,7 +147,7 @@ class LMI_InstalledSoftwareIdentity(CIMProvider2): with YumDB.get_instance() as yb: pl = yb.get_package_list('installed', sort=True) for pkg in pl: - model['InstalledSoftware'] = SoftwareIdentity.pkg2model( + model['InstalledSoftware'] = Identity.pkg2model( pkg, model=inst_model) yield model @@ -191,7 +191,7 @@ class LMI_InstalledSoftwareIdentity(CIMProvider2): ComputerSystem.check_path_property(env, instance, 'System') with YumDB.get_instance() as ydb: - pkg_info = SoftwareIdentity.object_path2pkg( + pkg_info = Identity.object_path2pkg( instance["InstalledSoftware"], kind="all") if pkg_info.installed: raise pywbem.CIMError(pywbem.CIM_ERR_ALREADY_EXISTS, @@ -199,7 +199,7 @@ class LMI_InstalledSoftwareIdentity(CIMProvider2): cmpi_logging.logger.info('installing package "%s"' % pkg_info) installed = ydb.install_package(pkg_info) cmpi_logging.logger.info('package "%s" installed' % pkg_info) - instance["InstalledSoftware"] = SoftwareIdentity.pkg2model(installed) + instance["InstalledSoftware"] = Identity.pkg2model(installed) return instance @cmpi_logging.trace_method @@ -232,7 +232,7 @@ class LMI_InstalledSoftwareIdentity(CIMProvider2): ComputerSystem.check_path_property(env, instance_name, 'System') with YumDB.get_instance() as ydb: - pkg_info = SoftwareIdentity.object_path2pkg( + pkg_info = Identity.object_path2pkg( instance_name["InstalledSoftware"], kind="installed") cmpi_logging.logger.info('removing package "%s"' % pkg_info) ydb.remove_package(pkg_info) diff --git a/src/software/openlmi/software/LMI_MemberOfSoftwareCollection.py b/src/software/openlmi/software/LMI_MemberOfSoftwareCollection.py index e9f73b5..80a10fe 100644 --- a/src/software/openlmi/software/LMI_MemberOfSoftwareCollection.py +++ b/src/software/openlmi/software/LMI_MemberOfSoftwareCollection.py @@ -26,8 +26,8 @@ from pywbem.cim_provider2 import CIMProvider2 from openlmi.common import cmpi_logging from openlmi.software.core import generate_references -from openlmi.software.core import SystemSoftwareCollection -from openlmi.software.core import SoftwareIdentity +from openlmi.software.core import SystemCollection +from openlmi.software.core import Identity from openlmi.software.yumdb import YumDB @cmpi_logging.trace_function @@ -35,17 +35,16 @@ def generate_collection_referents(env, object_name, model, _keys_only): """ Handler for referents enumeration request. """ - SystemSoftwareCollection.check_path(env, object_name, "collection") + SystemCollection.check_path(env, object_name, "collection") pkg_model = pywbem.CIMInstanceName( classname='LMI_SoftwareIdentity', namespace="root/cimv2", host=model.path.host) - model["Collection"] = SystemSoftwareCollection.get_path() + model["Collection"] = SystemCollection.get_path() with YumDB.get_instance() as ydb: for pkg_info in ydb.get_package_list('available', allow_duplicates=True, sort=True): - model["Member"] = SoftwareIdentity.pkg2model( - pkg_info, model=pkg_model) + model["Member"] = Identity.pkg2model(pkg_info, model=pkg_model) yield model @cmpi_logging.trace_function @@ -54,15 +53,14 @@ def generate_member_referents(_env, object_name, model, _keys_only): Handler for referents enumeration request. """ try: - pkg_info = SoftwareIdentity.object_path2pkg( - object_name, kind="available") - model['Member'] = SoftwareIdentity.pkg2model(pkg_info) - model["Collection"] = SystemSoftwareCollection.get_path() + pkg_info = Identity.object_path2pkg(object_name, kind="available") + model['Member'] = Identity.pkg2model(pkg_info) + model["Collection"] = SystemCollection.get_path() except pywbem.CIMError as exc: if exc.args[0] == pywbem.CIM_ERR_NOT_FOUND: msg = "Could not find requested package%s." if "InstanceID" in object_name: - msg = msg % (' with InstanceID="%s"' %object_name["InstanceID"]) + msg = msg % (' with InstanceID="%s"'%object_name["InstanceID"]) else: msg = msg % "" raise pywbem.CIMError(pywbem.CIM_ERR_FAILED, msg) @@ -73,9 +71,8 @@ def generate_member_referents(_env, object_name, model, _keys_only): class LMI_MemberOfSoftwareCollection(CIMProvider2): """Instrument the CIM class LMI_MemberOfSoftwareCollection - LMI_MemberOfSoftwareCollection is an aggregation used to establish membership - of ManagedElements in a Collection. - + LMI_MemberOfSoftwareCollection is an aggregation used to establish + membership of ManagedElements in a Collection. """ def __init__(self, _env): @@ -105,7 +102,7 @@ class LMI_MemberOfSoftwareCollection(CIMProvider2): CIM_ERR_FAILED (some other unspecified error occurred) """ - SystemSoftwareCollection.check_path_property(env, model, "Collection") + SystemCollection.check_path_property(env, model, "Collection") if not 'Member' in model: raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, @@ -114,11 +111,10 @@ class LMI_MemberOfSoftwareCollection(CIMProvider2): raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, "Expected object path for Member!") - model['Collection'] = SystemSoftwareCollection.get_path() + model['Collection'] = SystemCollection.get_path() with YumDB.get_instance(): - pkg_info = SoftwareIdentity.object_path2pkg( - model['Member'], 'available') - model['Member'] = SoftwareIdentity.pkg2model(pkg_info) + pkg_info = Identity.object_path2pkg(model['Member'], 'available') + model['Member'] = Identity.pkg2model(pkg_info) return model @cmpi_logging.trace_method @@ -148,7 +144,7 @@ class LMI_MemberOfSoftwareCollection(CIMProvider2): # we set property values on the model. model.path.update({'Member': None, 'Collection': None}) - model['Collection'] = SystemSoftwareCollection.get_path() + model['Collection'] = SystemCollection.get_path() member_model = pywbem.CIMInstanceName( classname="LMI_SoftwareIdentity", namespace="root/cimv2") @@ -157,8 +153,7 @@ class LMI_MemberOfSoftwareCollection(CIMProvider2): allow_duplicates=True, sort=True) for pkg in pl: - model['Member'] = SoftwareIdentity.pkg2model( - pkg, model=member_model) + model['Member'] = Identity.pkg2model(pkg, model=member_model) yield model @cmpi_logging.trace_method diff --git a/src/software/openlmi/software/LMI_ResourceForSoftwareIdentity.py b/src/software/openlmi/software/LMI_ResourceForSoftwareIdentity.py new file mode 100644 index 0000000..3eedc23 --- /dev/null +++ b/src/software/openlmi/software/LMI_ResourceForSoftwareIdentity.py @@ -0,0 +1,317 @@ +# -*- encoding: utf-8 -*- +# Software Management Providers +# +# Copyright (C) 2012 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Python Provider for LMI_ResourceForSoftwareIdentity + +Instruments the CIM class LMI_ResourceForSoftwareIdentity +""" + +import pywbem +from pywbem.cim_provider2 import CIMProvider2 + +from openlmi.common import cmpi_logging +from openlmi.software.core import generate_references +from openlmi.software.core import Identity +from openlmi.software.core import IdentityResource +from openlmi.software.yumdb import YumDB + +@cmpi_logging.trace_function +def generate_package_referents(_env, object_name, model, _keys_only): + """ + Generates models of repositories holding package represented + by object_name. + """ + with YumDB.get_instance() as ydb: + pkg_infos = Identity.object_path2pkg( + object_name, 'available', return_all=True) + for pkg_info in pkg_infos: + repos = ydb.filter_repositories('enabled', repoid=pkg_info.repoid) + model['ManagedElement'] = Identity.pkg2model(pkg_info) + for repo in repos: + model["AvailableSAP"] = \ + IdentityResource.repo2model(repo) + yield model + +@cmpi_logging.trace_function +def generate_repository_referents(env, object_name, model, _keys_only): + """ + Generates models of software identities contained in repository + represented by object_name. + """ + with YumDB.get_instance() as ydb: + repo = IdentityResource.object_path2repo( + env, object_name, 'all') + pkglist = ydb.get_package_list('available', + allow_duplicates=True, sort=True, + include_repos=repo.repoid, + exclude_repos='*') + model['AvailableSAP'] = IdentityResource.repo2model(repo) + pkg_model = pywbem.CIMInstanceName( + classname="LMI_SoftwareIdentity", + namespace="root/cimv2") + for pkg_info in pkglist: + model["ManagedElement"] = Identity.pkg2model( + pkg_info, model=pkg_model) + yield model + +class LMI_ResourceForSoftwareIdentity(CIMProvider2): + """Instrument the CIM class LMI_ResourceForSoftwareIdentity + + CIM_SAPAvailableForElement conveys the semantics of a Service Access + Point that is available for a ManagedElement. When + CIM_SAPAvailableForElement is not instantiated, then the SAP is + assumed to be generally available. If instantiated, the SAP is + available only for the associated ManagedElements. For example, a + device might provide management access through a URL. This association + allows the URL to be advertised for the device. + + """ + + def __init__ (self, _env): + cmpi_logging.logger.debug('Initializing provider %s from %s' \ + % (self.__class__.__name__, __file__)) + + @cmpi_logging.trace_method + def get_instance(self, env, model): + """Return an instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + model -- A template of the pywbem.CIMInstance to be returned. The + key properties are set on this instance to correspond to the + instanceName that was requested. The properties of the model + are already filtered according to the PropertyList from the + request. Only properties present in the model need to be + given values. If you prefer, you can set all of the + values, and the instance will be filtered for you. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the CIM Class does exist, but the requested CIM + Instance does not exist in the specified namespace) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + if not isinstance(model['ManagedElement'], pywbem.CIMInstanceName): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "Expected object path for ManagedElement!") + if not isinstance(model['AvailableSAP'], pywbem.CIMInstanceName): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "Expected object path for AvailableSAP!") + if not "Name" in model['AvailableSAP']: + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + 'Missing key property "Name" in AvailableSAP!') + + with YumDB.get_instance() as ydb: + repoid = model["AvailableSAP"]["Name"] + pkg_info = Identity.object_path2pkg( + model["ManagedElement"], + kind='available', + include_repos=repoid, + exclude_repos='*', + repoid=repoid) + repos = ydb.filter_repositories('all', repoid=repoid) + if len(repos) < 1: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + 'Unknown repository "%s".' % repoid) + repo = repos[0] + repo = IdentityResource.object_path2repo( + env, model["AvailableSAP"], 'all') + + model["AvailableSAP"] = IdentityResource.repo2model(repo) + model["ManagedElement"] = Identity.pkg2model(pkg_info) + + return model + + @cmpi_logging.trace_method + def enum_instances(self, env, model, keys_only): + """Enumerate instances. + + The WBEM operations EnumerateInstances and EnumerateInstanceNames + are both mapped to this method. + This method is a python generator + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + model -- A template of the pywbem.CIMInstances to be generated. + The properties of the model are already filtered according to + the PropertyList from the request. Only properties present in + the model need to be given values. If you prefer, you can + always set all of the values, and the instance will be filtered + for you. + keys_only -- A boolean. True if only the key properties should be + set on the generated instances. + + Possible Errors: + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + # Prime model.path with knowledge of the keys, so key values on + # the CIMInstanceName (model.path) will automatically be set when + # we set property values on the model. + model.path.update({'ManagedElement': None, 'AvailableSAP': None}) + + elem_model = pywbem.CIMInstanceName( + classname="LMI_SoftwareIdentity", + namespace="root/cimv2") + sap_model = pywbem.CIMInstanceName( + classname="LMI_SoftwareIdentityResource", + namespace="root/cimv2") + # maps repoid to instance name + with YumDB.get_instance() as ydb: + pl = ydb.get_package_list('available', + allow_duplicates=True, + sort=True) + repos = dict( (r.repoid, r) + for r in ydb.get_repository_list('enabled')) + for pkg in pl: + try: + repo = repos[pkg.repo] + except KeyError: + cmpi_logging.logger.error('unknown or disabled repository' + ' "%s" for package "%s"' % (pkg, pkg.repo)) + model["AvailableSAP"] = IdentityResource.repo2model( + repo, model=sap_model) + model["ManagedElement"] = Identity.pkg2model( + pkg, model=elem_model) + yield model + + @cmpi_logging.trace_method + def set_instance(self, env, instance, modify_existing): + """Return a newly created or modified instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + instance -- The new pywbem.CIMInstance. If modifying an existing + instance, the properties on this instance have been filtered by + the PropertyList from the request. + modify_existing -- True if ModifyInstance, False if CreateInstance + + Return the new instance. The keys must be set on the new instance. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_NOT_SUPPORTED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_ALREADY_EXISTS (the CIM Instance already exists -- only + valid if modify_existing is False, indicating that the operation + was CreateInstance) + CIM_ERR_NOT_FOUND (the CIM Instance does not exist -- only valid + if modify_existing is True, indicating that the operation + was ModifyInstance) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_SUPPORTED) + + @cmpi_logging.trace_method + def delete_instance(self, env, instance_name): + """Delete an instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + instance_name -- A pywbem.CIMInstanceName specifying the instance + to delete. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_NOT_SUPPORTED + CIM_ERR_INVALID_NAMESPACE + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_INVALID_CLASS (the CIM Class does not exist in the specified + namespace) + CIM_ERR_NOT_FOUND (the CIM Class does exist, but the requested CIM + Instance does not exist in the specified namespace) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_SUPPORTED) + + @cmpi_logging.trace_method + def references(self, env, object_name, model, result_class_name, role, + result_role, keys_only): + """Instrument Associations. + + All four association-related operations (Associators, AssociatorNames, + References, ReferenceNames) are mapped to this method. + This method is a python generator + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName that defines the source + CIM Object whose associated Objects are to be returned. + model -- A template pywbem.CIMInstance to serve as a model + of the objects to be returned. Only properties present on this + model need to be set. + result_class_name -- If not empty, this string acts as a filter on + the returned set of Instances by mandating that each returned + Instances MUST represent an association between object_name + and an Instance of a Class whose name matches this parameter + or a subclass. + role -- If not empty, MUST be a valid Property name. It acts as a + filter on the returned set of Instances by mandating that each + returned Instance MUST refer to object_name via a Property + whose name matches the value of this parameter. + result_role -- If not empty, MUST be a valid Property name. It acts + as a filter on the returned set of Instances by mandating that + each returned Instance MUST represent associations of + object_name to other Instances, where the other Instances play + the specified result_role in the association (i.e. the + name of the Property in the Association Class that refers to + the Object related to object_name MUST match the value of this + parameter). + keys_only -- A boolean. True if only the key properties should be + set on the generated instances. + + The following diagram may be helpful in understanding the role, + result_role, and result_class_name parameters. + +------------------------+ +-------------------+ + | object_name.classname | | result_class_name | + | ~~~~~~~~~~~~~~~~~~~~~ | | ~~~~~~~~~~~~~~~~~ | + +------------------------+ +-------------------+ + | +-----------------------------------+ | + | | [Association] model.classname | | + | object_name | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | | + +--------------+ object_name.classname REF role | | + (CIMInstanceName) | result_class_name REF result_role +------+ + | |(CIMInstanceName) + +-----------------------------------+ + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_NOT_SUPPORTED + CIM_ERR_INVALID_NAMESPACE + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + handlers = [ + ("AvailableSAP", "LMI_SoftwareIdentityResource", + generate_repository_referents), + ("ManagedElement", "LMI_SoftwareIdentity", + generate_package_referents) + ] + + for ref in generate_references(env, object_name, model, + result_class_name, role, result_role, keys_only, handlers): + yield ref + diff --git a/src/software/openlmi/software/LMI_SoftwareIdentity.py b/src/software/openlmi/software/LMI_SoftwareIdentity.py index b97ca25..066ef8f 100644 --- a/src/software/openlmi/software/LMI_SoftwareIdentity.py +++ b/src/software/openlmi/software/LMI_SoftwareIdentity.py @@ -26,7 +26,7 @@ import pywbem from pywbem.cim_provider2 import CIMProvider2 from openlmi.common import cmpi_logging -from openlmi.software.core import SoftwareIdentity +from openlmi.software.core import Identity from openlmi.software.yumdb import YumDB class LMI_SoftwareIdentity(CIMProvider2): @@ -96,9 +96,8 @@ class LMI_SoftwareIdentity(CIMProvider2): """ with YumDB.get_instance(): - pkg_info = SoftwareIdentity.object_path2pkg(model.path, 'all') - return SoftwareIdentity.pkg2model( - pkg_info, keys_only=False, model=model) + pkg_info = Identity.object_path2pkg(model.path, 'all') + return Identity.pkg2model(pkg_info, keys_only=False, model=model) @cmpi_logging.trace_method def enum_instances(self, env, model, keys_only): @@ -132,8 +131,7 @@ class LMI_SoftwareIdentity(CIMProvider2): pkglist = ydb.get_package_list( 'all', allow_duplicates=True, sort=True) for pkg_info in pkglist: - yield SoftwareIdentity.pkg2model( - pkg_info, keys_only=keys_only, model=model) + yield Identity.pkg2model(pkg_info, keys_only=keys_only, model=model) @cmpi_logging.trace_method def set_instance(self, env, instance, modify_existing): diff --git a/src/software/openlmi/software/LMI_SoftwareIdentityResource.py b/src/software/openlmi/software/LMI_SoftwareIdentityResource.py index 6acec42..7797367 100644 --- a/src/software/openlmi/software/LMI_SoftwareIdentityResource.py +++ b/src/software/openlmi/software/LMI_SoftwareIdentityResource.py @@ -29,7 +29,7 @@ import pywbem from pywbem.cim_provider2 import CIMProvider2 from openlmi.common import cmpi_logging -from openlmi.software.core import SoftwareIdentityResource +from openlmi.software.core import IdentityResource from openlmi.software.yumdb import YumDB, errors class LMI_SoftwareIdentityResource(CIMProvider2): @@ -77,9 +77,9 @@ class LMI_SoftwareIdentityResource(CIMProvider2): """ - repo = SoftwareIdentityResource.object_path2repo( + repo = IdentityResource.object_path2repo( env, model.path, kind='all') - return SoftwareIdentityResource.repo2model( + return IdentityResource.repo2model( repo, keys_only=False, model=model) @cmpi_logging.trace_method @@ -113,7 +113,7 @@ class LMI_SoftwareIdentityResource(CIMProvider2): repolist = YumDB.get_instance().get_repository_list('all') for repo in repolist: - yield SoftwareIdentityResource.repo2model( + yield IdentityResource.repo2model( repo, keys_only=keys_only, model=model) @cmpi_logging.trace_method @@ -240,22 +240,22 @@ class LMI_SoftwareIdentityResource(CIMProvider2): """ out_params = [] if param_timeoutperiod is not None: - return ( SoftwareIdentityResource.Values.RequestStateChange. \ + return ( IdentityResource.Values.RequestStateChange. \ Use_of_Timeout_Parameter_Not_Supported , out_params) if param_requestedstate not in { - SoftwareIdentityResource.Values.RequestStateChange. \ + IdentityResource.Values.RequestStateChange. \ RequestedState.Enabled, - SoftwareIdentityResource.Values.RequestStateChange. \ + IdentityResource.Values.RequestStateChange. \ RequestedState.Disabled }: - return ( SoftwareIdentityResource.Values.RequestStateChange. \ + return ( IdentityResource.Values.RequestStateChange. \ Invalid_State_Transition , out_params) with YumDB.get_instance() as ydb: - repo = SoftwareIdentityResource.object_path2repo(env, + repo = IdentityResource.object_path2repo(env, object_name, 'all') - enable = param_requestedstate == SoftwareIdentityResource.Values. \ + enable = param_requestedstate == IdentityResource.Values. \ RequestStateChange.RequestedState.Enabled cmpi_logging.logger.info("%s repository %s" % ("enabling" if enable else "disabling", repo)) @@ -271,7 +271,7 @@ class LMI_SoftwareIdentityResource(CIMProvider2): cmpi_logging.logger.info(msg % (repo, "enabled" if enable else "disabled")) - rval = SoftwareIdentityResource.Values.RequestStateChange. \ + rval = IdentityResource.Values.RequestStateChange. \ Completed_with_No_Error return (rval, out_params) diff --git a/src/software/openlmi/software/LMI_SoftwareInstallationJob.py b/src/software/openlmi/software/LMI_SoftwareInstallationJob.py new file mode 100644 index 0000000..df62f0d --- /dev/null +++ b/src/software/openlmi/software/LMI_SoftwareInstallationJob.py @@ -0,0 +1,375 @@ +"""Python Provider for LMI_SoftwareInstallationJob + +Instruments the CIM class LMI_SoftwareInstallationJob + +""" + +import inspect +import pywbem +from pywbem.cim_provider2 import CIMProvider2 + +from openlmi.common import cmpi_logging +from openlmi.software.core import Error, InstallationJob +from openlmi.software.yumdb import errors, YumDB + +class LMI_SoftwareInstallationJob(CIMProvider2): + """Instrument the CIM class LMI_SoftwareInstallationJob + + A concrete version of Job. This class represents a generic and + instantiable unit of work, such as a batch or a print job. + + """ + + def __init__ (self, _env): + cmpi_logging.logger.debug('Initializing provider %s from %s' \ + % (self.__class__.__name__, __file__)) + self.values = InstallationJob.Values + + @cmpi_logging.trace_method + def get_instance(self, env, model): + """Return an instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + model -- A template of the pywbem.CIMInstance to be returned. The + key properties are set on this instance to correspond to the + instanceName that was requested. The properties of the model + are already filtered according to the PropertyList from the + request. Only properties present in the model need to be + given values. If you prefer, you can set all of the + values, and the instance will be filtered for you. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the CIM Class does exist, but the requested CIM + Instance does not exist in the specified namespace) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + job = InstallationJob.object_path2job(model.path) + return InstallationJob.job2model( + job, keys_only=False, model=model) + + @cmpi_logging.trace_method + def enum_instances(self, env, model, keys_only): + """Enumerate instances. + + The WBEM operations EnumerateInstances and EnumerateInstanceNames + are both mapped to this method. + This method is a python generator + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + model -- A template of the pywbem.CIMInstances to be generated. + The properties of the model are already filtered according to + the PropertyList from the request. Only properties present in + the model need to be given values. If you prefer, you can + always set all of the values, and the instance will be filtered + for you. + keys_only -- A boolean. True if only the key properties should be + set on the generated instances. + + Possible Errors: + CIM_ERR_FAILED (some other unspecified error occurred) + """ + # Prime model.path with knowledge of the keys, so key values on + # the CIMInstanceName (model.path) will automatically be set when + # we set property values on the model. + model.path.update({'InstanceID': None}) + + for job in YumDB.get_instance().get_job_list(): + yield InstallationJob.job2model(job, keys_only, model) + + @cmpi_logging.trace_method + def set_instance(self, env, instance, modify_existing): + """Return a newly created or modified instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + instance -- The new pywbem.CIMInstance. If modifying an existing + instance, the properties on this instance have been filtered by + the PropertyList from the request. + modify_existing -- True if ModifyInstance, False if CreateInstance + + Return the new instance. The keys must be set on the new instance. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_NOT_SUPPORTED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_ALREADY_EXISTS (the CIM Instance already exists -- only + valid if modify_existing is False, indicating that the operation + was CreateInstance) + CIM_ERR_NOT_FOUND (the CIM Instance does not exist -- only valid + if modify_existing is True, indicating that the operation + was ModifyInstance) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + if not modify_existing: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_SUPPORTED, + "Can not create new instance.") + return InstallationJob.modify_instance(instance) + + @cmpi_logging.trace_method + def delete_instance(self, env, instance_name): + """Delete an instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + instance_name -- A pywbem.CIMInstanceName specifying the instance + to delete. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_NOT_SUPPORTED + CIM_ERR_INVALID_NAMESPACE + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_INVALID_CLASS (the CIM Class does not exist in the specified + namespace) + CIM_ERR_NOT_FOUND (the CIM Class does exist, but the requested CIM + Instance does not exist in the specified namespace) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + with YumDB.get_instance() as ydb: + job = InstallationJob.object_path2job(instance_name) + try: + cmpi_logging.logger.info('deleting job "%s"' % job) + ydb.delete_job(job.jobid) + cmpi_logging.logger.info('job "%s" removed' % job) + except errors.JobNotFound as exc: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + getattr(exc, 'message', str(exc))) + except errors.JobControlError as exc: + raise pywbem.CIMError(pywbem.CIM_ERR_FAILED, + getattr(exc, 'message', str(exc))) + + @cmpi_logging.trace_method + def cim_method_requeststatechange(self, env, object_name, + param_requestedstate=None, + param_timeoutperiod=None): + """Implements LMI_SoftwareInstallationJob.RequestStateChange() + + Requests that the state of the job be changed to the value + specified in the RequestedState parameter. Invoking the + RequestStateChange method multiple times could result in earlier + requests being overwritten or lost. If 0 is returned, then the + task completed successfully. Any other return code indicates an + error condition. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method RequestStateChange() + should be invoked. + param_requestedstate -- The input parameter RequestedState ( + type pywbem.Uint16 + self.Values.RequestStateChange.RequestedState) + RequestStateChange changes the state of a job. The possible + values are as follows: Start (2) changes the state to + 'Running'. Suspend (3) stops the job temporarily. The + intention is to subsequently restart the job with 'Start'. It + might be possible to enter the 'Service' state while + suspended. (This is job-specific.) Terminate (4) stops the + job cleanly, saving data, preserving the state, and shutting + down all underlying processes in an orderly manner. Kill (5) + terminates the job immediately with no requirement to save + data or preserve the state. Service (6) puts the job into a + vendor-specific service state. It might be possible to restart + the job. + + param_timeoutperiod -- The input parameter TimeoutPeriod ( + type pywbem.CIMDateTime) + A timeout period that specifies the maximum amount of time that + the client expects the transition to the new state to take. + The interval format must be used to specify the TimeoutPeriod. + A value of 0 or a null parameter indicates that the client has + no time requirements for the transition. If this property + does not contain 0 or null and the implementation does not + support this parameter, a return code of 'Use Of Timeout + Parameter Not Supported' must be returned. + + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.RequestStateChange) + and a list of CIMParameter objects representing the output parameters + + Output parameters: none + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + job = InstallationJob.object_path2job(object_name) + try: + if param_requestedstate not in { + self.values.RequestStateChange.RequestedState.Terminate }: + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "Valid RequestedState can by only Terminate (%d)" % + self.values.RequestStateChange.RequestedState.Terminate) + if param_timeoutperiod: + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "Timeout period is not supported.") + YumDB.get_instance().terminate_job(job.jobid) + except errors.JobNotFound as exc: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + getattr(exc, 'message', str(exc))) + except errors.JobControlError as exc: + raise pywbem.CIMError(pywbem.CIM_ERR_FAILED, + getattr(exc, 'message', str(exc))) + return (self.values.GetErrors.Success, []) + + @cmpi_logging.trace_method + def cim_method_geterrors(self, env, object_name): + """Implements LMI_SoftwareInstallationJob.GetErrors() + + If JobState is "Completed" and Operational Status is "Completed" + then no instance of CIM_Error is returned. If JobState is + "Exception" then GetErrors may return intances of CIM_Error + related to the execution of the procedure or method invoked by the + job. If Operatational Status is not "OK" or "Completed"then + GetErrors may return CIM_Error instances related to the running of + the job. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method GetErrors() + should be invoked. + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.GetErrors) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + Errors -- (type pywbem.CIMInstance(classname='CIM_Error', ...)) + If the OperationalStatus on the Job is not "OK", then this + method will return one or more CIM Error instance(s). + Otherwise, when the Job is "OK", null is returned. + + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + job = InstallationJob.object_path2job(object_name) + out_params = [] + if job.state == job.EXCEPTION: + errortup = job.result_data + callargs = inspect.getcallargs(Error.make_instance) + if issubclass(errortup[0], + (errors.RepositoryNotFound, errors.PackageNotFound)): + callargs['status_code'] = Error.Values. \ + CIMStatusCode.CIM_ERR_NOT_FOUND + if issubclass(errortup[0], errors.PackageNotFound): + callargs['status_code_description'] = \ + "Package not found" + else: + callargs['status_code_description'] = \ + "Repository not found" + elif issubclass(errortup[0], errors.PackageAlreadyInstalled): + callargs['status_code'] = Error.Values. \ + CIMStatusCode.CIM_ERR_ALREADY_EXISTS + callargs['message'] = getattr(errortup[1], 'message', + str(errortup[1])) + value = [Error.make_instance(**callargs)] + param = pywbem.CIMParameter('Errors', type='instance', + is_array=True, array_size=1, value=value) + out_params.append(param) + return (self.values.GetErrors.Success, out_params) + + @cmpi_logging.trace_method + def cim_method_killjob(self, env, object_name, + param_deleteonkill=None): + """Implements LMI_SoftwareInstallationJob.KillJob() + + KillJob is being deprecated because there is no distinction made + between an orderly shutdown and an immediate kill. + CIM_ConcreteJob.RequestStateChange() provides 'Terminate' and + 'Kill' options to allow this distinction. A method to kill this + job and any underlying processes, and to remove any 'dangling' + associations. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method KillJob() + should be invoked. + param_deleteonkill -- The input parameter DeleteOnKill (type bool) + Indicates whether or not the Job should be automatically + deleted upon termination. This parameter takes precedence over + the property, DeleteOnCompletion. + + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.KillJob) + and a list of CIMParameter objects representing the output parameters + + Output parameters: none + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + raise pywbem.CIMError(pywbem.CIM_ERR_METHOD_NOT_AVAILABLE) + + @cmpi_logging.trace_method + def cim_method_geterror(self, env, object_name): + """Implements LMI_SoftwareInstallationJob.GetError() + + GetError is deprecated because Error should be an array,not a + scalar. When the job is executing or has terminated without error, + then this method returns no CIM_Error instance. However, if the + job has failed because of some internal problem or because the job + has been terminated by a client, then a CIM_Error instance is + returned. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method GetError() + should be invoked. + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.GetError) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + Error -- (type pywbem.CIMInstance(classname='CIM_Error', ...)) + If the OperationalStatus on the Job is not "OK", then this + method will return a CIM Error instance. Otherwise, when the + Job is "OK", null is returned. + + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + raise pywbem.CIMError(pywbem.CIM_ERR_METHOD_NOT_AVAILABLE) diff --git a/src/software/openlmi/software/LMI_SoftwareInstallationService.py b/src/software/openlmi/software/LMI_SoftwareInstallationService.py new file mode 100644 index 0000000..224e923 --- /dev/null +++ b/src/software/openlmi/software/LMI_SoftwareInstallationService.py @@ -0,0 +1,783 @@ +# Software Management Providers +# +# Copyright (C) 2012 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Python Provider for LMI_SoftwareInstallationService + +Instruments the CIM class LMI_SoftwareInstallationService + +""" + +import pywbem +from pywbem.cim_provider2 import CIMProvider2 + +from openlmi.common import cmpi_logging +from openlmi.software.core import InstallationJob +from openlmi.software.core import InstallationService + +class LMI_SoftwareInstallationService(CIMProvider2): + """Instrument the CIM class LMI_SoftwareInstallationService + + A subclass of service which provides methods to install (or update) + Software Identities in ManagedElements. + + """ + + def __init__ (self, _env): + cmpi_logging.logger.debug('Initializing provider %s from %s' \ + % (self.__class__.__name__, __file__)) + self.values = InstallationService.Values + + @cmpi_logging.trace_method + def get_instance(self, env, model): + """Return an instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + model -- A template of the pywbem.CIMInstance to be returned. The + key properties are set on this instance to correspond to the + instanceName that was requested. The properties of the model + are already filtered according to the PropertyList from the + request. Only properties present in the model need to be + given values. If you prefer, you can set all of the + values, and the instance will be filtered for you. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the CIM Class does exist, but the requested CIM + Instance does not exist in the specified namespace) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + InstallationService.check_path(env, model.path, "path") + + objpath = InstallationService.get_path() + + for key, value in objpath.keybindings.items(): + model[key] = value + + model['Caption'] = 'Software installation service for this system.' + model['CommunicationStatus'] = self.values. \ + CommunicationStatus.Not_Available + model['Description'] = \ + 'Software installation service using YUM packake manager.' + model['DetailedStatus'] = self.values.DetailedStatus.Not_Available + model['EnabledDefault'] = self.values.EnabledDefault.Not_Applicable + model['EnabledState'] = self.values.EnabledState.Not_Applicable + model['HealthState'] = self.values.HealthState.OK + model['InstanceID'] = 'LMI:InstallationService' + model['OperatingStatus'] = self.values.OperatingStatus.Servicing + model['OperationalStatus'] = [self.values.OperationalStatus.OK] + model['PrimaryStatus'] = self.values.PrimaryStatus.OK + model['RequestedState'] = self.values.RequestedState.Not_Applicable + model['Started'] = True + model['TransitioningToState'] = self.values. \ + TransitioningToState.Not_Applicable + return model + + @cmpi_logging.trace_method + def enum_instances(self, env, model, keys_only): + """Enumerate instances. + + The WBEM operations EnumerateInstances and EnumerateInstanceNames + are both mapped to this method. + This method is a python generator + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + model -- A template of the pywbem.CIMInstances to be generated. + The properties of the model are already filtered according to + the PropertyList from the request. Only properties present in + the model need to be given values. If you prefer, you can + always set all of the values, and the instance will be filtered + for you. + keys_only -- A boolean. True if only the key properties should be + set on the generated instances. + + Possible Errors: + CIM_ERR_FAILED (some other unspecified error occurred) + """ + model.path.update({'CreationClassName': None, 'SystemName': None, + 'Name': None, 'SystemCreationClassName': None}) + + objpath = InstallationService.get_path() + for key, value in objpath.keybindings.items(): + model[key] = value + if not keys_only: + model = self.get_instance(env, model) + yield model + + @cmpi_logging.trace_method + def set_instance(self, env, instance, modify_existing): + """Return a newly created or modified instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + instance -- The new pywbem.CIMInstance. If modifying an existing + instance, the properties on this instance have been filtered by + the PropertyList from the request. + modify_existing -- True if ModifyInstance, False if CreateInstance + + Return the new instance. The keys must be set on the new instance. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_NOT_SUPPORTED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_ALREADY_EXISTS (the CIM Instance already exists -- only + valid if modify_existing is False, indicating that the operation + was CreateInstance) + CIM_ERR_NOT_FOUND (the CIM Instance does not exist -- only valid + if modify_existing is True, indicating that the operation + was ModifyInstance) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_SUPPORTED) + + @cmpi_logging.trace_method + def delete_instance(self, env, instance_name): + """Delete an instance. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + instance_name -- A pywbem.CIMInstanceName specifying the instance + to delete. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_NOT_SUPPORTED + CIM_ERR_INVALID_NAMESPACE + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, unrecognized + or otherwise incorrect parameters) + CIM_ERR_INVALID_CLASS (the CIM Class does not exist in the specified + namespace) + CIM_ERR_NOT_FOUND (the CIM Class does exist, but the requested CIM + Instance does not exist in the specified namespace) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_SUPPORTED) + + @cmpi_logging.trace_method + def cim_method_requeststatechange(self, env, object_name, + param_requestedstate=None, + param_timeoutperiod=None): + """Implements LMI_SoftwareInstallationService.RequestStateChange() + + Requests that the state of the element be changed to the value + specified in the RequestedState parameter. When the requested + state change takes place, the EnabledState and RequestedState of + the element will be the same. Invoking the RequestStateChange + method multiple times could result in earlier requests being + overwritten or lost. A return code of 0 shall indicate the state + change was successfully initiated. A return code of 3 shall + indicate that the state transition cannot complete within the + interval specified by the TimeoutPeriod parameter. A return code + of 4096 (0x1000) shall indicate the state change was successfully + initiated, a ConcreteJob has been created, and its reference + returned in the output parameter Job. Any other return code + indicates an error condition. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method RequestStateChange() + should be invoked. + param_requestedstate -- The input parameter RequestedState ( + type pywbem.Uint16 self.Values.RequestStateChange.RequestedState) + The state requested for the element. This information will be + placed into the RequestedState property of the instance if the + return code of the RequestStateChange method is 0 ('Completed + with No Error'), or 4096 (0x1000) ('Job Started'). Refer to + the description of the EnabledState and RequestedState + properties for the detailed explanations of the RequestedState + values. + + param_timeoutperiod -- The input parameter TimeoutPeriod ( + type pywbem.CIMDateTime) + A timeout period that specifies the maximum amount of time that + the client expects the transition to the new state to take. + The interval format must be used to specify the TimeoutPeriod. + A value of 0 or a null parameter indicates that the client has + no time requirements for the transition. If this property + does not contain 0 or null and the implementation does not + support this parameter, a return code of 'Use Of Timeout + Parameter Not Supported' shall be returned. + + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.RequestStateChange) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + Job -- (type REF (pywbem.CIMInstanceName( + classname='CIM_ConcreteJob', ...)) + May contain a reference to the ConcreteJob created to track the + state transition initiated by the method invocation. + + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + out_params = [] + out_params += [pywbem.CIMParameter('Job', type='reference', value=None)] + return ( self.values.RequestStateChange.Not_Supported + , out_params) + + @cmpi_logging.trace_method + def cim_method_stopservice(self, env, object_name): + """Implements LMI_SoftwareInstallationService.StopService() + + The StopService method places the Service in the stopped state. + Note that the function of this method overlaps with the + RequestedState property. RequestedState was added to the model to + maintain a record (such as a persisted value) of the last state + request. Invoking the StopService method should set the + RequestedState property appropriately. The method returns an + integer value of 0 if the Service was successfully stopped, 1 if + the request is not supported, and any other number to indicate an + error. In a subclass, the set of possible return codes could be + specified using a ValueMap qualifier on the method. The strings to + which the ValueMap contents are translated can also be specified + in the subclass as a Values array qualifier. Note: The semantics + of this method overlap with the RequestStateChange method that is + inherited from EnabledLogicalElement. This method is maintained + because it has been widely implemented, and its simple "stop" + semantics are convenient to use. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method StopService() + should be invoked. + + Returns a two-tuple containing the return value (type pywbem.Uint32) + and a list of CIMParameter objects representing the output parameters + + Output parameters: none + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + rval = pywbem.Uint32(1) # Not Supported + return (rval, []) + + @cmpi_logging.trace_method + def cim_method_installfromuri(self, env, object_name, + param_installoptionsvalues=None, + param_uri=None, + param_installoptions=None, + param_target=None): + """Implements LMI_SoftwareInstallationService.InstallFromURI() + + Start a job to install software from a specific URI in a + ManagedElement. Note that this method is provided to support + existing, alternative download mechanisms (such as used for + firmware download). The 'normal' mechanism will be to use the + InstallFromSoftwareIdentity method. If 0 is returned, the function + completed successfully and no ConcreteJob instance was required. + If 4096/0x1000 is returned, a ConcreteJob will be started to to + perform the install. The Job's reference will be returned in the + output parameter Job. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method InstallFromURI() + should be invoked. + param_installoptionsvalues -- The input parameter \ + InstallOptionsValues (type [unicode,]) + InstallOptionsValues is an array of strings providing + additionalinformation to InstallOptions for the method to + install the software. Each entry of this array is related to + the entry in InstallOptions that is located at the same index + providing additional information for InstallOptions. For + further information on the use of InstallOptionsValues + parameter, see the description of the InstallOptionsValues + parameter of the + InstallationService.InstallFromSoftwareIdentity + method. + + param_uri -- The input parameter URI (type unicode) + A URI for the software based on RFC 2079. + + param_installoptions -- The input parameter InstallOptions ( + type [pywbem.Uint16,] self.Values.InstallFromURI.InstallOptions) + Options to control the install process. See the InstallOptions + parameter of the + InstallationService.InstallFromSoftwareIdentity method + for the description of these values. + + param_target -- The input parameter Target (type REF ( + pywbem.CIMInstanceName(classname='CIM_ManagedElement', ...)) + The installation target. + + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.InstallFromURI) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + Job -- (type REF (pywbem.CIMInstanceName( + classname='CIM_ConcreteJob', ...)) + Reference to the job (may be null if job completed). + + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + InstallationService.check_path(env, object_name, "object_name") + out_params = [pywbem.CIMParameter('Job', type='reference', value=None)] + try: + jobid = InstallationService.install_or_remove_package( + env, "uri", param_uri, + param_target, None, param_installoptions, + param_installoptionsvalues) + rval = self.values.InstallFromURI.Job_Completed_with_No_Error + out_params[0].value = InstallationJob.job2model(jobid) + except InstallationService.InstallationError as exc: + cmpi_logging.logger.error( + "installation failed: %s", exc.description) + rval = exc.return_code + return (rval, out_params) + + @cmpi_logging.trace_method + def cim_method_checksoftwareidentity(self, env, object_name, + param_source=None, + param_target=None, + param_collection=None): + """Implements LMI_SoftwareInstallationService.CheckSoftwareIdentity() + + This method allows a client application to determine whether a + specific SoftwareIdentity can be installed (or updated) on a + ManagedElement. It also allows other characteristics to be + determined such as whether install will require a reboot. In + addition a client can check whether the SoftwareIdentity can be + added simulataneously to a specified SofwareIndentityCollection. A + client MAY specify either or both of the Collection and Target + parameters. The Collection parameter is only supported if + SoftwareInstallationServiceCapabilities.CanAddToCollection is + TRUE. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method CheckSoftwareIdentity() + should be invoked. + param_source -- The input parameter Source (type REF ( + pywbem.CIMInstanceName(classname='CIM_SoftwareIdentity', ...)) + Reference to the SoftwareIdentity to be checked. + + param_target -- The input parameter Target (type REF ( + pywbem.CIMInstanceName(classname='CIM_ManagedElement', ...)) + Reference to the ManagedElement that the Software Identity is + going to be installed in (or updated). + + param_collection -- The input parameter Collection (type REF ( + pywbem.CIMInstanceName(classname='CIM_Collection', ...)) + Reference to the Collection to which the Software Identity will + be added. + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.CheckSoftwareIdentity) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + InstallCharacteristics -- (type [pywbem.Uint16,] + self.Values.CheckSoftwareIdentity.InstallCharacteristics) + The parameter describes the characteristics of the + installation/update that will take place if the Source + Software Identity is installed: Target automatic reset: The + target element will automatically reset once the installation + is complete. System automatic reset: The containing system of + the target ManagedElement (normally a logical device or the + system itself) will automatically reset/reboot once the + installation is complete. Separate target reset required: + EnabledLogicalElement.RequestStateChange MUST be used to reset + the target element after the SoftwareIdentity is installed. + Separate system reset required: + EnabledLogicalElement.RequestStateChange MUST be used to + reset/reboot the containing system of the target + ManagedElement after the SoftwareIdentity is installed. + Manual Reboot Required: The system MUST be manually rebooted + by the user. No reboot required : No reboot is required after + installation. User Intervention Recomended : It is + recommended that a user confirm installation of this + SoftwareIdentity. Inappropriate application MAY have serious + consequences. MAY be added to specified collection : The + SoftwareIndentity MAY be added to specified Collection. + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + raise pywbem.CIMError(pywbem.CIM_ERR_METHOD_NOT_AVAILABLE) + + @cmpi_logging.trace_method + def cim_method_changeaffectedelementsassignedsequence(self, + env, object_name, + param_managedelements, + param_assignedsequence): + """Implements LMI_SoftwareInstallationService. \ + ChangeAffectedElementsAssignedSequence() + + This method is called to change relative sequence in which order + the ManagedElements associated to the Service through + CIM_ServiceAffectsElement association are affected. In the case + when the Service represents an interface for client to execute + extrinsic methods and when it is used for grouping of the managed + elements that could be affected, the ordering represents the + relevant priority of the affected managed elements with respect to + each other. An ordered array of ManagedElement instances is + passed to this method, where each ManagedElement instance shall be + already be associated with this Service instance via + CIM_ServiceAffectsElement association. If one of the + ManagedElements is not associated to the Service through + CIM_ServiceAffectsElement association, the implementation shall + return a value of 2 ("Error Occured"). Upon successful execution + of this method, if the AssignedSequence parameter is NULL, the + value of the AssignedSequence property on each instance of + CIM_ServiceAffectsElement shall be updated such that the values of + AssignedSequence properties shall be monotonically increasing in + correlation with the position of the referenced ManagedElement + instance in the ManagedElements input parameter. That is, the + first position in the array shall have the lowest value for + AssignedSequence. The second position shall have the second lowest + value, and so on. Upon successful execution, if the + AssignedSequence parameter is not NULL, the value of the + AssignedSequence property of each instance of + CIM_ServiceAffectsElement referencing the ManagedElement instance + in the ManagedElements array shall be assigned the value of the + corresponding index of the AssignedSequence parameter array. For + ManagedElements instances which are associated with the Service + instance via CIM_ServiceAffectsElement and are not present in the + ManagedElements parameter array, the AssignedSequence property on + the CIM_ServiceAffects association shall be assigned a value of 0. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method + ChangeAffectedElementsAssignedSequence() should be invoked. + param_managedelements -- The input parameter ManagedElements ( + type REF (pywbem.CIMInstanceName( + classname='CIM_ManagedElement', ...)) (Required) + An array of ManagedElements. + + param_assignedsequence -- The input parameter AssignedSequence ( + type [pywbem.Uint16,]) (Required) + An array of integers representing AssignedSequence for the + ManagedElement in the corresponding index of the + ManagedElements parameter. + + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values. \ + ChangeAffectedElementsAssignedSequence) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + Job -- (type REF (pywbem.CIMInstanceName( + classname='CIM_ConcreteJob', ...)) + Reference to the job spawned if the operation continues after + the method returns. (May be null if the task is completed). + + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + raise pywbem.CIMError(pywbem.CIM_ERR_METHOD_NOT_AVAILABLE) + + @cmpi_logging.trace_method + def cim_method_installfromsoftwareidentity( + self, env, object_name, + param_installoptions=None, + param_target=None, + param_collection=None, + param_source=None, + param_installoptionsvalues=None): + """Implements LMI_SoftwareInstallationService. \ + InstallFromSoftwareIdentity() + + Start a job to install or update a SoftwareIdentity (Source) on a + ManagedElement (Target). In addition the method can be used to + add the SoftwareIdentity simulataneously to a specified + SofwareIndentityCollection. A client MAY specify either or both of + the Collection and Target parameters. The Collection parameter is + only supported if InstallationService.CanAddToCollection + is TRUE. If 0 is returned, the function completed successfully + and no ConcreteJob instance was required. If 4096/0x1000 is + returned, a ConcreteJob will be started to perform the install. + The Job's reference will be returned in the output parameter Job. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method + InstallFromSoftwareIdentity() + should be invoked. + param_installoptions -- The input parameter InstallOptions ( + type [pywbem.Uint16,] self.Values.InstallFromSoftwareIdentity.\ + InstallOptions) + Options to control the install process. Defer target/system + reset : do not automatically reset the target/system. Force + installation : Force the installation of the same or an older + SoftwareIdentity. Install: Perform an installation of this + software on the managed element. Update: Perform an update of + this software on the managed element. Repair: Perform a repair + of the installation of this software on the managed element by + forcing all the files required for installing the software to + be reinstalled. Reboot: Reboot or reset the system immediately + after the install or update of this software, if the install + or the update requires a reboot or reset. Password: Password + will be specified as clear text without any encryption for + performing the install or update. Uninstall: Uninstall the + software on the managed element. Log: Create a log for the + install or update of the software. SilentMode: Perform the + install or update without displaying any user interface. + AdministrativeMode: Perform the install or update of the + software in the administrative mode. ScheduleInstallAt: + Indicates the time at which theinstall or update of the + software will occur. + + param_target -- The input parameter Target (type REF ( + pywbem.CIMInstanceName(classname='CIM_ManagedElement', ...)) + The installation target. If NULL then the SOftwareIdentity will + be added to Collection only. The underlying implementation is + expected to be able to obtain any necessary metadata from the + Software Identity. + + param_collection -- The input parameter Collection (type REF ( + pywbem.CIMInstanceName(classname='CIM_Collection', ...)) + Reference to the Collection to which the Software Identity + SHALL be added. If NULL then the SOftware Identity will not be + added to a Collection. + + param_source -- The input parameter Source (type REF ( + pywbem.CIMInstanceName(classname='CIM_SoftwareIdentity', ...)) + Reference to the source of the install. + + param_installoptionsvalues -- The input parameter + InstallOptionsValues (type [unicode,]) + InstallOptionsValues is an array of strings providing + additional information to InstallOptions for the method to + install the software. Each entry of this array is related to + the entry in InstallOptions that is located at the same index + providing additional information for InstallOptions. If the + index in InstallOptions has the value "Password " then a value + at the corresponding index of InstallOptionValues shall not be + NULL. If the index in InstallOptions has the value + "ScheduleInstallAt" then the value at the corresponding index + of InstallOptionValues shall not be NULL and shall be in the + datetime type format. If the index in InstallOptions has the + value "Log " then a value at the corresponding index of + InstallOptionValues may be NULL. If the index in + InstallOptions has the value "Defer target/system reset", + "Force installation","Install", "Update", "Repair" or "Reboot" + then a value at the corresponding index of InstallOptionValues + shall be NULL. + + + Returns a two-tuple containing the return value ( + type pywbem.Uint32 self.Values.InstallFromSoftwareIdentity) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + Job -- (type REF (pywbem.CIMInstanceName( + classname='CIM_ConcreteJob', ...)) + Reference to the job (may be null if job completed). + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + InstallationService.check_path(env, object_name, "object_name") + out_params = [pywbem.CIMParameter('Job', type='reference', value=None)] + try: + jobid = InstallationService.install_or_remove_package( + env, "identity", param_source, + param_target, param_collection, param_installoptions, + param_installoptionsvalues) + rval = self.values.InstallFromSoftwareIdentity. \ + Job_Completed_with_No_Error + out_params[0].value = InstallationJob.job2model(jobid) + except InstallationService.InstallationError as exc: + cmpi_logging.logger.error( + "installation failed: %s", exc.description) + rval = exc.return_code + return (rval, out_params) + + @cmpi_logging.trace_method + def cim_method_startservice(self, env, object_name): + """Implements LMI_SoftwareInstallationService.StartService() + + The StartService method places the Service in the started state. + Note that the function of this method overlaps with the + RequestedState property. RequestedState was added to the model to + maintain a record (such as a persisted value) of the last state + request. Invoking the StartService method should set the + RequestedState property appropriately. The method returns an + integer value of 0 if the Service was successfully started, 1 if + the request is not supported, and any other number to indicate an + error. In a subclass, the set of possible return codes could be + specified using a ValueMap qualifier on the method. The strings to + which the ValueMap contents are translated can also be specified + in the subclass as a Values array qualifier. Note: The semantics + of this method overlap with the RequestStateChange method that is + inherited from EnabledLogicalElement. This method is maintained + because it has been widely implemented, and its simple "start" + semantics are convenient to use. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method StartService() + should be invoked. + + Returns a two-tuple containing the return value (type pywbem.Uint32) + and a list of CIMParameter objects representing the output parameters + + Output parameters: none + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + """ + rval = pywbem.Uint32(1) # Not Supported + return (rval, []) + + @cmpi_logging.trace_method + def cim_method_installfrombytestream(self, env, object_name, + param_installoptionsvalues=None, + param_image=None, + param_installoptions=None, + param_target=None): + """Implements LMI_SoftwareInstallationService.InstallFromByteStream() + + Start a job to download a series of bytes containing a software + image to a ManagedElement. Note that this method is provided to + support existing, alternative download mechanisms (such as used + for firmware download). The 'normal' mechanism will be to use the + InstallFromSoftwareIdentity method. If 0 is returned, the + function completed successfully and no ConcreteJob instance was + required. If 4096/0x1000 is returned, a ConcreteJob will be + started to to perform the install. The Job's reference will be + returned in the output parameter Job. + + Keyword arguments: + env -- Provider Environment (pycimmb.ProviderEnvironment) + object_name -- A pywbem.CIMInstanceName or pywbem.CIMCLassName + specifying the object on which the method InstallFromByteStream() + should be invoked. + param_installoptionsvalues -- The input parameter InstallOptionsValues (type [unicode,]) + InstallOptionsValues is an array of strings providing + additional information to InstallOptions for the method to + install the software. Each entry of this array is related to + the entry in InstallOptions that is located at the same index + providing additional information for InstallOptions. For + further information on the use of InstallOptionsValues + parameter, see the description of the InstallOptionsValues + parameter of the + InstallationService.InstallFromSoftwareIdentity + method. + + param_image -- The input parameter Image (type [pywbem.Uint8,]) + A array of bytes containing the install image. + + param_installoptions -- The input parameter InstallOptions (type [pywbem.Uint16,] self.Values.InstallFromByteStream.InstallOptions) + Options to control the install process. See the InstallOptions + parameter of the + InstallationService.InstallFromSoftwareIdentity method + for the description of these values. + + param_target -- The input parameter Target (type REF (pywbem.CIMInstanceName(classname='CIM_ManagedElement', ...)) + The installation target. + + + Returns a two-tuple containing the return value (type pywbem.Uint32 self.Values.InstallFromByteStream) + and a list of CIMParameter objects representing the output parameters + + Output parameters: + Job -- (type REF (pywbem.CIMInstanceName(classname='CIM_ConcreteJob', ...)) + Reference to the job (may be null if job completed). + + + Possible Errors: + CIM_ERR_ACCESS_DENIED + CIM_ERR_INVALID_PARAMETER (including missing, duplicate, + unrecognized or otherwise incorrect parameters) + CIM_ERR_NOT_FOUND (the target CIM Class or instance does not + exist in the specified namespace) + CIM_ERR_METHOD_NOT_AVAILABLE (the CIM Server is unable to honor + the invocation request) + CIM_ERR_FAILED (some other unspecified error occurred) + + """ + out_params = [pywbem.CIMParameter('Job', type='reference', value=None)] + return ( self.values.InstallFromByteStream.Not_Supported + , out_params) diff --git a/src/software/openlmi/software/LMI_SystemSoftwareCollection.py b/src/software/openlmi/software/LMI_SystemSoftwareCollection.py index ec2382b..f1a5123 100644 --- a/src/software/openlmi/software/LMI_SystemSoftwareCollection.py +++ b/src/software/openlmi/software/LMI_SystemSoftwareCollection.py @@ -24,7 +24,7 @@ import pywbem from pywbem.cim_provider2 import CIMProvider2 from openlmi.common import cmpi_logging -from openlmi.software.core import SystemSoftwareCollection +from openlmi.software.core import SystemCollection class LMI_SystemSoftwareCollection(CIMProvider2): """Instrument the CIM class LMI_SystemSoftwareCollection @@ -75,8 +75,7 @@ class LMI_SystemSoftwareCollection(CIMProvider2): if not 'InstanceID' in model: raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, "Missing InstanceID key property") - if model['InstanceID'] != \ - SystemSoftwareCollection.get_path()['InstanceID']: + if model['InstanceID'] != SystemCollection.get_path()['InstanceID']: raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, "No such instance.") @@ -113,7 +112,7 @@ class LMI_SystemSoftwareCollection(CIMProvider2): # we set property values on the model. model.path.update({'InstanceID': None}) - model['InstanceID'] = SystemSoftwareCollection.get_path()["InstanceID"] + model['InstanceID'] = SystemCollection.get_path()["InstanceID"] if keys_only is False: yield self.get_instance(env, model) else: diff --git a/src/software/openlmi/software/cimom_entry.py b/src/software/openlmi/software/cimom_entry.py index a598fb1..640b86e 100644 --- a/src/software/openlmi/software/cimom_entry.py +++ b/src/software/openlmi/software/cimom_entry.py @@ -40,6 +40,10 @@ from openlmi.software.LMI_ResourceForSoftwareIdentity import \ LMI_ResourceForSoftwareIdentity from openlmi.software.LMI_HostedSoftwareIdentityResource import \ LMI_HostedSoftwareIdentityResource +from openlmi.software.LMI_SoftwareInstallationService import \ + LMI_SoftwareInstallationService +from openlmi.software.LMI_SoftwareInstallationJob import \ + LMI_SoftwareInstallationJob from openlmi.software.yumdb import YumDB def get_providers(env): @@ -55,9 +59,13 @@ def get_providers(env): "LMI_MemberOfSoftwareCollection" : LMI_MemberOfSoftwareCollection(env), "LMI_InstalledSoftwareIdentity" : LMI_InstalledSoftwareIdentity(env), "LMI_SoftwareIdentityResource" : LMI_SoftwareIdentityResource(env), - "LMI_ResourceForSoftwareIdentity" : LMI_ResourceForSoftwareIdentity(env), + "LMI_ResourceForSoftwareIdentity" : + LMI_ResourceForSoftwareIdentity(env), "LMI_HostedSoftwareIdentityResource" : - LMI_HostedSoftwareIdentityResource(env) + LMI_HostedSoftwareIdentityResource(env), + "LMI_SoftwareInstallationService" : \ + LMI_SoftwareInstallationService(env), + "LMI_SoftwareInstallationJob" : LMI_SoftwareInstallationJob(env) } return providers diff --git a/src/software/openlmi/software/core/Error.py b/src/software/openlmi/software/core/Error.py new file mode 100644 index 0000000..03653b2 --- /dev/null +++ b/src/software/openlmi/software/core/Error.py @@ -0,0 +1,455 @@ +# -*- encoding: utf-8 -*- +# Software Management Providers +# +# Copyright (C) 2012 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Just a common functionality related to class CIM_Error. +""" + +import pywbem + +from openlmi.software.core import InstallationService + +class Values(object): + class ErrorSourceFormat(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + CIMObjectPath = pywbem.Uint16(2) + # DMTF_Reserved = .. + _reverse_map = { + 0 : 'Unknown', + 1 : 'Other', + 2 : 'CIMObjectPath' + } + + class ErrorType(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + Communications_Error = pywbem.Uint16(2) + Quality_of_Service_Error = pywbem.Uint16(3) + Software_Error = pywbem.Uint16(4) + Hardware_Error = pywbem.Uint16(5) + Environmental_Error = pywbem.Uint16(6) + Security_Error = pywbem.Uint16(7) + Oversubscription_Error = pywbem.Uint16(8) + Unavailable_Resource_Error = pywbem.Uint16(9) + Unsupported_Operation_Error = pywbem.Uint16(10) + # DMTF_Reserved = .. + _reverse_map = { + 0 : 'Unknown', + 1 : 'Other', + 2 : 'Communications Error', + 3 : 'Quality of Service Error', + 4 : 'Software Error', + 5 : 'Hardware Error', + 6 : 'Environmental Error', + 7 : 'Security Error', + 8 : 'Oversubscription Error', + 9 : 'Unavailable Resource Error', + 10 : 'Unsupported Operation Error' + } + + class CIMStatusCode(object): + CIM_ERR_FAILED = pywbem.Uint32(1) + CIM_ERR_ACCESS_DENIED = pywbem.Uint32(2) + CIM_ERR_INVALID_NAMESPACE = pywbem.Uint32(3) + CIM_ERR_INVALID_PARAMETER = pywbem.Uint32(4) + CIM_ERR_INVALID_CLASS = pywbem.Uint32(5) + CIM_ERR_NOT_FOUND = pywbem.Uint32(6) + CIM_ERR_NOT_SUPPORTED = pywbem.Uint32(7) + CIM_ERR_CLASS_HAS_CHILDREN = pywbem.Uint32(8) + CIM_ERR_CLASS_HAS_INSTANCES = pywbem.Uint32(9) + CIM_ERR_INVALID_SUPERCLASS = pywbem.Uint32(10) + CIM_ERR_ALREADY_EXISTS = pywbem.Uint32(11) + CIM_ERR_NO_SUCH_PROPERTY = pywbem.Uint32(12) + CIM_ERR_TYPE_MISMATCH = pywbem.Uint32(13) + CIM_ERR_QUERY_LANGUAGE_NOT_SUPPORTED = pywbem.Uint32(14) + CIM_ERR_INVALID_QUERY = pywbem.Uint32(15) + CIM_ERR_METHOD_NOT_AVAILABLE = pywbem.Uint32(16) + CIM_ERR_METHOD_NOT_FOUND = pywbem.Uint32(17) + CIM_ERR_UNEXPECTED_RESPONSE = pywbem.Uint32(18) + CIM_ERR_INVALID_RESPONSE_DESTINATION = pywbem.Uint32(19) + CIM_ERR_NAMESPACE_NOT_EMPTY = pywbem.Uint32(20) + CIM_ERR_INVALID_ENUMERATION_CONTEXT = pywbem.Uint32(21) + CIM_ERR_INVALID_OPERATION_TIMEOUT = pywbem.Uint32(22) + CIM_ERR_PULL_HAS_BEEN_ABANDONED = pywbem.Uint32(23) + CIM_ERR_PULL_CANNOT_BE_ABANDONED = pywbem.Uint32(24) + CIM_ERR_FILTERED_ENUMERATION_NOT_SUPPORTED = pywbem.Uint32(25) + CIM_ERR_CONTINUATION_ON_ERROR_NOT_SUPPORTED = pywbem.Uint32(26) + CIM_ERR_SERVER_LIMITS_EXCEEDED = pywbem.Uint32(27) + CIM_ERR_SERVER_IS_SHUTTING_DOWN = pywbem.Uint32(28) + CIM_ERR_QUERY_FEATURE_NOT_SUPPORTED = pywbem.Uint32(29) + # DMTF_Reserved = .. + _reverse_map = { + 1 : 'CIM_ERR_FAILED', + 2 : 'CIM_ERR_ACCESS_DENIED', + 3 : 'CIM_ERR_INVALID_NAMESPACE', + 4 : 'CIM_ERR_INVALID_PARAMETER', + 5 : 'CIM_ERR_INVALID_CLASS', + 6 : 'CIM_ERR_NOT_FOUND', + 7 : 'CIM_ERR_NOT_SUPPORTED', + 8 : 'CIM_ERR_CLASS_HAS_CHILDREN', + 9 : 'CIM_ERR_CLASS_HAS_INSTANCES', + 10 : 'CIM_ERR_INVALID_SUPERCLASS', + 11 : 'CIM_ERR_ALREADY_EXISTS', + 12 : 'CIM_ERR_NO_SUCH_PROPERTY', + 13 : 'CIM_ERR_TYPE_MISMATCH', + 14 : 'CIM_ERR_QUERY_LANGUAGE_NOT_SUPPORTED', + 15 : 'CIM_ERR_INVALID_QUERY', + 16 : 'CIM_ERR_METHOD_NOT_AVAILABLE', + 17 : 'CIM_ERR_METHOD_NOT_FOUND', + 18 : 'CIM_ERR_UNEXPECTED_RESPONSE', + 19 : 'CIM_ERR_INVALID_RESPONSE_DESTINATION', + 20 : 'CIM_ERR_NAMESPACE_NOT_EMPTY', + 21 : 'CIM_ERR_INVALID_ENUMERATION_CONTEXT', + 22 : 'CIM_ERR_INVALID_OPERATION_TIMEOUT', + 23 : 'CIM_ERR_PULL_HAS_BEEN_ABANDONED', + 24 : 'CIM_ERR_PULL_CANNOT_BE_ABANDONED', + 25 : 'CIM_ERR_FILTERED_ENUMERATION_NOT_SUPPORTED', + 26 : 'CIM_ERR_CONTINUATION_ON_ERROR_NOT_SUPPORTED', + 27 : 'CIM_ERR_SERVER_LIMITS_EXCEEDED', + 28 : 'CIM_ERR_SERVER_IS_SHUTTING_DOWN', + 29 : 'CIM_ERR_QUERY_FEATURE_NOT_SUPPORTED' + } + + class PerceivedSeverity(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + Information = pywbem.Uint16(2) + Degraded_Warning = pywbem.Uint16(3) + Minor = pywbem.Uint16(4) + Major = pywbem.Uint16(5) + Critical = pywbem.Uint16(6) + Fatal_NonRecoverable = pywbem.Uint16(7) + # DMTF_Reserved = .. + _reverse_map = { + 0 : 'Unknown', + 1 : 'Other', + 2 : 'Information', + 3 : 'Degraded/Warning', + 4 : 'Minor', + 5 : 'Major', + 6 : 'Critical', + 7 : 'Fatal/NonRecoverable' + } + + class ProbableCause(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + Adapter_Card_Error = pywbem.Uint16(2) + Application_Subsystem_Failure = pywbem.Uint16(3) + Bandwidth_Reduced = pywbem.Uint16(4) + Connection_Establishment_Error = pywbem.Uint16(5) + Communications_Protocol_Error = pywbem.Uint16(6) + Communications_Subsystem_Failure = pywbem.Uint16(7) + Configuration_Customization_Error = pywbem.Uint16(8) + Congestion = pywbem.Uint16(9) + Corrupt_Data = pywbem.Uint16(10) + CPU_Cycles_Limit_Exceeded = pywbem.Uint16(11) + Dataset_Modem_Error = pywbem.Uint16(12) + Degraded_Signal = pywbem.Uint16(13) + DTE_DCE_Interface_Error = pywbem.Uint16(14) + Enclosure_Door_Open = pywbem.Uint16(15) + Equipment_Malfunction = pywbem.Uint16(16) + Excessive_Vibration = pywbem.Uint16(17) + File_Format_Error = pywbem.Uint16(18) + Fire_Detected = pywbem.Uint16(19) + Flood_Detected = pywbem.Uint16(20) + Framing_Error = pywbem.Uint16(21) + HVAC_Problem = pywbem.Uint16(22) + Humidity_Unacceptable = pywbem.Uint16(23) + I_O_Device_Error = pywbem.Uint16(24) + Input_Device_Error = pywbem.Uint16(25) + LAN_Error = pywbem.Uint16(26) + Non_Toxic_Leak_Detected = pywbem.Uint16(27) + Local_Node_Transmission_Error = pywbem.Uint16(28) + Loss_of_Frame = pywbem.Uint16(29) + Loss_of_Signal = pywbem.Uint16(30) + Material_Supply_Exhausted = pywbem.Uint16(31) + Multiplexer_Problem = pywbem.Uint16(32) + Out_of_Memory = pywbem.Uint16(33) + Output_Device_Error = pywbem.Uint16(34) + Performance_Degraded = pywbem.Uint16(35) + Power_Problem = pywbem.Uint16(36) + Pressure_Unacceptable = pywbem.Uint16(37) + Processor_Problem__Internal_Machine_Error_ = pywbem.Uint16(38) + Pump_Failure = pywbem.Uint16(39) + Queue_Size_Exceeded = pywbem.Uint16(40) + Receive_Failure = pywbem.Uint16(41) + Receiver_Failure = pywbem.Uint16(42) + Remote_Node_Transmission_Error = pywbem.Uint16(43) + Resource_at_or_Nearing_Capacity = pywbem.Uint16(44) + Response_Time_Excessive = pywbem.Uint16(45) + Retransmission_Rate_Excessive = pywbem.Uint16(46) + Software_Error = pywbem.Uint16(47) + Software_Program_Abnormally_Terminated = pywbem.Uint16(48) + Software_Program_Error__Incorrect_Results_ = pywbem.Uint16(49) + Storage_Capacity_Problem = pywbem.Uint16(50) + Temperature_Unacceptable = pywbem.Uint16(51) + Threshold_Crossed = pywbem.Uint16(52) + Timing_Problem = pywbem.Uint16(53) + Toxic_Leak_Detected = pywbem.Uint16(54) + Transmit_Failure = pywbem.Uint16(55) + Transmitter_Failure = pywbem.Uint16(56) + Underlying_Resource_Unavailable = pywbem.Uint16(57) + Version_Mismatch = pywbem.Uint16(58) + Previous_Alert_Cleared = pywbem.Uint16(59) + Login_Attempts_Failed = pywbem.Uint16(60) + Software_Virus_Detected = pywbem.Uint16(61) + Hardware_Security_Breached = pywbem.Uint16(62) + Denial_of_Service_Detected = pywbem.Uint16(63) + Security_Credential_Mismatch = pywbem.Uint16(64) + Unauthorized_Access = pywbem.Uint16(65) + Alarm_Received = pywbem.Uint16(66) + Loss_of_Pointer = pywbem.Uint16(67) + Payload_Mismatch = pywbem.Uint16(68) + Transmission_Error = pywbem.Uint16(69) + Excessive_Error_Rate = pywbem.Uint16(70) + Trace_Problem = pywbem.Uint16(71) + Element_Unavailable = pywbem.Uint16(72) + Element_Missing = pywbem.Uint16(73) + Loss_of_Multi_Frame = pywbem.Uint16(74) + Broadcast_Channel_Failure = pywbem.Uint16(75) + Invalid_Message_Received = pywbem.Uint16(76) + Routing_Failure = pywbem.Uint16(77) + Backplane_Failure = pywbem.Uint16(78) + Identifier_Duplication = pywbem.Uint16(79) + Protection_Path_Failure = pywbem.Uint16(80) + Sync_Loss_or_Mismatch = pywbem.Uint16(81) + Terminal_Problem = pywbem.Uint16(82) + Real_Time_Clock_Failure = pywbem.Uint16(83) + Antenna_Failure = pywbem.Uint16(84) + Battery_Charging_Failure = pywbem.Uint16(85) + Disk_Failure = pywbem.Uint16(86) + Frequency_Hopping_Failure = pywbem.Uint16(87) + Loss_of_Redundancy = pywbem.Uint16(88) + Power_Supply_Failure = pywbem.Uint16(89) + Signal_Quality_Problem = pywbem.Uint16(90) + Battery_Discharging = pywbem.Uint16(91) + Battery_Failure = pywbem.Uint16(92) + Commercial_Power_Problem = pywbem.Uint16(93) + Fan_Failure = pywbem.Uint16(94) + Engine_Failure = pywbem.Uint16(95) + Sensor_Failure = pywbem.Uint16(96) + Fuse_Failure = pywbem.Uint16(97) + Generator_Failure = pywbem.Uint16(98) + Low_Battery = pywbem.Uint16(99) + Low_Fuel = pywbem.Uint16(100) + Low_Water = pywbem.Uint16(101) + Explosive_Gas = pywbem.Uint16(102) + High_Winds = pywbem.Uint16(103) + Ice_Buildup = pywbem.Uint16(104) + Smoke = pywbem.Uint16(105) + Memory_Mismatch = pywbem.Uint16(106) + Out_of_CPU_Cycles = pywbem.Uint16(107) + Software_Environment_Problem = pywbem.Uint16(108) + Software_Download_Failure = pywbem.Uint16(109) + Element_Reinitialized = pywbem.Uint16(110) + Timeout = pywbem.Uint16(111) + Logging_Problems = pywbem.Uint16(112) + Leak_Detected = pywbem.Uint16(113) + Protection_Mechanism_Failure = pywbem.Uint16(114) + Protecting_Resource_Failure = pywbem.Uint16(115) + Database_Inconsistency = pywbem.Uint16(116) + Authentication_Failure = pywbem.Uint16(117) + Breach_of_Confidentiality = pywbem.Uint16(118) + Cable_Tamper = pywbem.Uint16(119) + Delayed_Information = pywbem.Uint16(120) + Duplicate_Information = pywbem.Uint16(121) + Information_Missing = pywbem.Uint16(122) + Information_Modification = pywbem.Uint16(123) + Information_Out_of_Sequence = pywbem.Uint16(124) + Key_Expired = pywbem.Uint16(125) + Non_Repudiation_Failure = pywbem.Uint16(126) + Out_of_Hours_Activity = pywbem.Uint16(127) + Out_of_Service = pywbem.Uint16(128) + Procedural_Error = pywbem.Uint16(129) + Unexpected_Information = pywbem.Uint16(130) + # DMTF_Reserved = .. + _reverse_map = { + 0 : 'Unknown', + 1 : 'Other', + 2 : 'Adapter/Card Error', + 3 : 'Application Subsystem Failure', + 4 : 'Bandwidth Reduced', + 5 : 'Connection Establishment Error', + 6 : 'Communications Protocol Error', + 7 : 'Communications Subsystem Failure', + 8 : 'Configuration/Customization Error', + 9 : 'Congestion', + 10 : 'Corrupt Data', + 11 : 'CPU Cycles Limit Exceeded', + 12 : 'Dataset/Modem Error', + 13 : 'Degraded Signal', + 14 : 'DTE-DCE Interface Error', + 15 : 'Enclosure Door Open', + 16 : 'Equipment Malfunction', + 17 : 'Excessive Vibration', + 18 : 'File Format Error', + 19 : 'Fire Detected', + 20 : 'Flood Detected', + 21 : 'Framing Error', + 22 : 'HVAC Problem', + 23 : 'Humidity Unacceptable', + 24 : 'I/O Device Error', + 25 : 'Input Device Error', + 26 : 'LAN Error', + 27 : 'Non-Toxic Leak Detected', + 28 : 'Local Node Transmission Error', + 29 : 'Loss of Frame', + 30 : 'Loss of Signal', + 31 : 'Material Supply Exhausted', + 32 : 'Multiplexer Problem', + 33 : 'Out of Memory', + 34 : 'Output Device Error', + 35 : 'Performance Degraded', + 36 : 'Power Problem', + 37 : 'Pressure Unacceptable', + 38 : 'Processor Problem (Internal Machine Error)', + 39 : 'Pump Failure', + 40 : 'Queue Size Exceeded', + 41 : 'Receive Failure', + 42 : 'Receiver Failure', + 43 : 'Remote Node Transmission Error', + 44 : 'Resource at or Nearing Capacity', + 45 : 'Response Time Excessive', + 46 : 'Retransmission Rate Excessive', + 47 : 'Software Error', + 48 : 'Software Program Abnormally Terminated', + 49 : 'Software Program Error (Incorrect Results)', + 50 : 'Storage Capacity Problem', + 51 : 'Temperature Unacceptable', + 52 : 'Threshold Crossed', + 53 : 'Timing Problem', + 54 : 'Toxic Leak Detected', + 55 : 'Transmit Failure', + 56 : 'Transmitter Failure', + 57 : 'Underlying Resource Unavailable', + 58 : 'Version Mismatch', + 59 : 'Previous Alert Cleared', + 60 : 'Login Attempts Failed', + 61 : 'Software Virus Detected', + 62 : 'Hardware Security Breached', + 63 : 'Denial of Service Detected', + 64 : 'Security Credential Mismatch', + 65 : 'Unauthorized Access', + 66 : 'Alarm Received', + 67 : 'Loss of Pointer', + 68 : 'Payload Mismatch', + 69 : 'Transmission Error', + 70 : 'Excessive Error Rate', + 71 : 'Trace Problem', + 72 : 'Element Unavailable', + 73 : 'Element Missing', + 74 : 'Loss of Multi Frame', + 75 : 'Broadcast Channel Failure', + 76 : 'Invalid Message Received', + 77 : 'Routing Failure', + 78 : 'Backplane Failure', + 79 : 'Identifier Duplication', + 80 : 'Protection Path Failure', + 81 : 'Sync Loss or Mismatch', + 82 : 'Terminal Problem', + 83 : 'Real Time Clock Failure', + 84 : 'Antenna Failure', + 85 : 'Battery Charging Failure', + 86 : 'Disk Failure', + 87 : 'Frequency Hopping Failure', + 88 : 'Loss of Redundancy', + 89 : 'Power Supply Failure', + 90 : 'Signal Quality Problem', + 91 : 'Battery Discharging', + 92 : 'Battery Failure', + 93 : 'Commercial Power Problem', + 94 : 'Fan Failure', + 95 : 'Engine Failure', + 96 : 'Sensor Failure', + 97 : 'Fuse Failure', + 98 : 'Generator Failure', + 99 : 'Low Battery', + 100 : 'Low Fuel', + 101 : 'Low Water', + 102 : 'Explosive Gas', + 103 : 'High Winds', + 104 : 'Ice Buildup', + 105 : 'Smoke', + 106 : 'Memory Mismatch', + 107 : 'Out of CPU Cycles', + 108 : 'Software Environment Problem', + 109 : 'Software Download Failure', + 110 : 'Element Reinitialized', + 111 : 'Timeout', + 112 : 'Logging Problems', + 113 : 'Leak Detected', + 114 : 'Protection Mechanism Failure', + 115 : 'Protecting Resource Failure', + 116 : 'Database Inconsistency', + 117 : 'Authentication Failure', + 118 : 'Breach of Confidentiality', + 119 : 'Cable Tamper', + 120 : 'Delayed Information', + 121 : 'Duplicate Information', + 122 : 'Information Missing', + 123 : 'Information Modification', + 124 : 'Information Out of Sequence', + 125 : 'Key Expired', + 126 : 'Non-Repudiation Failure', + 127 : 'Out of Hours Activity', + 128 : 'Out of Service', + 129 : 'Procedural Error', + 130 : 'Unexpected Information' + } + +def make_instance( + status_code=Values.CIMStatusCode.CIM_ERR_FAILED, + error_type=Values.ErrorType.Software_Error, + probable_cause=Values.ErrorType.Unknown, + error_source=None, + status_code_description=None, + message=None, + message_arguments=None, + probable_cause_description=None, + recommended_actions=None): + for param in ('status_code', 'probable_cause', 'error_type'): + if not isinstance(locals()[param], (int, long)): + raise TypeError('%s must be integer'%param) + if error_source is None: + error_source = InstallationService.get_path() + if not isinstance(error_source, pywbem.CIMInstanceName): + raise TypeError('error_source must be a CIMInstanceName') + + inst = pywbem.CIMInstance(classname="CIM_Error", + path=pywbem.CIMInstanceName(classname="CIM_Error", + namespace="root/cimv2")) + inst['CIMStatusCode'] = pywbem.Uint32(status_code) + if status_code_description is not None: + inst['CIMStatusCodeDescription'] = status_code_description + inst['ErrorType'] = pywbem.Uint16(error_type) + inst['ErrorSource'] = str(error_source) + inst['ErrorSourceFormat'] = Values.ErrorSourceFormat.CIMObjectPath + if message is not None: + inst['Message'] = message + if message_arguments is not None: + inst['MessageArguments'] = message_arguments + inst['ProbableCause'] = pywbem.Uint16(probable_cause) + #inst['PerceivedSeverity'] = pywbem.Uint16(perceived_severity) + if probable_cause_description is not None: + inst['ProbableCauseDescription'] = probable_cause_description + if recommended_actions is not None: + inst['RecommendedActions'] = recommended_actions + return inst + diff --git a/src/software/openlmi/software/core/SoftwareIdentity.py b/src/software/openlmi/software/core/Identity.py index edf1268..1668586 100644 --- a/src/software/openlmi/software/core/SoftwareIdentity.py +++ b/src/software/openlmi/software/core/Identity.py @@ -159,6 +159,36 @@ class Values(object): # DMTF_Reserved = .. # Vendor_Reserved = 0x8000.. + +@cmpi_logging.trace_function +def object_path2nevra(op, with_epoch='NOT_ZERO'): + if not isinstance(op, pywbem.CIMInstanceName): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "op must be an instance of CIMInstanceName") + + if (not "InstanceID" in op or not op['InstanceID']): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, "Wrong keys.") + instid = op['InstanceID'] + if not instid.lower().startswith("lmi:pkg:"): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "InstanceID must start with LMI:PKG: prefix.") + instid = instid[len("LMI:PKG:"):] + match = util.RE_NEVRA_OPT_EPOCH.match(instid) + if not match: + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "Wrong InstanceID. Expected valid nevra" + ' (name-[epoch:]version-release.arch): "%s".' % + instid) + epoch = match.group('epoch') + if not epoch: + epoch = "0" + return util.make_nevra( + match.group('name'), + epoch, + match.group('ver'), + match.group('rel'), + match.group('arch'), with_epoch=with_epoch) + @cmpi_logging.trace_function def object_path2pkg(op, kind='installed', @@ -178,37 +208,20 @@ def object_path2pkg(op, """ if not isinstance(kind, basestring): raise TypeError("kind must be a string") - if not isinstance(op, pywbem.CIMInstanceName): - raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, - "op must be an instance of CIMInstanceName") - - if (not "InstanceID" in op or not op['InstanceID']): - raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, "Wrong keys.") - instid = op['InstanceID'] - if not instid.lower().startswith("lmi:pkg:"): - raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, - "InstanceID must start with LMI:PKG: prefix.") - instid = instid[len("LMI:PKG:"):] - match = util.RE_NEVRA_OPT_EPOCH.match(instid) - if not match: - raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, - "Wrong InstanceID. Expected valid nevra" - ' (name-[epoch:]version-release.arch): "%s".' % - instid) pkglist = YumDB.get_instance().filter_packages(kind, allow_duplicates=kind not in ('installed', 'avail_reinst'), include_repos=include_repos, exclude_repos=exclude_repos, repoid=repoid, - **util.nevra2filter(match)) + nevra=object_path2nevra(op, 'ALWAYS')) if return_all is True: return pkglist if len(pkglist) > 0: return pkglist[0] raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, - 'No matching package found for package nevra=\"%s\".' % - instid) + 'No matching package found for InstanceID=\"%s\".' % + op["InstanceID"]) @cmpi_logging.trace_function def pkg2model(pkg_info, keys_only=True, model=None): diff --git a/src/software/openlmi/software/core/SoftwareIdentityResource.py b/src/software/openlmi/software/core/IdentityResource.py index 722c29b..722c29b 100644 --- a/src/software/openlmi/software/core/SoftwareIdentityResource.py +++ b/src/software/openlmi/software/core/IdentityResource.py diff --git a/src/software/openlmi/software/core/InstallationJob.py b/src/software/openlmi/software/core/InstallationJob.py new file mode 100644 index 0000000..b082331 --- /dev/null +++ b/src/software/openlmi/software/core/InstallationJob.py @@ -0,0 +1,579 @@ +# -*- encoding: utf-8 -*- +# Software Management Providers +# +# Copyright (C) 2012 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Just a common functionality related to SoftwareInstallationJob. +""" + +from datetime import datetime, timedelta +import pywbem +import time + +from openlmi.common import cmpi_logging +from openlmi.software.yumdb import errors, jobs +from openlmi.software.yumdb import YumDB + +JOB_DESCRIPTIONS = { + jobs.YumInstallPackage : + 'Software package installation job %(jobid)d for "%(pkg)s".', + jobs.YumRemovePackage : + 'Software package removal job %(jobid)d for "%(pkg)s".', + jobs.YumUpdateToPackage : + 'Software package update job %(jobid)d for "%(pkg)s".', + jobs.YumUpdatePackage : + 'Software package update job %(jobid)d for "%(pkg)s".', + jobs.YumCheckPackage : + 'Software package check job %(jobid)d for "%(pkg)s".', + jobs.YumInstallPackageFromURI : + 'Software package installation job %(jobid)d from uri: "%(uri)s".', +} + +class Values(object): + class DetailedStatus(object): + Not_Available = pywbem.Uint16(0) + No_Additional_Information = pywbem.Uint16(1) + Stressed = pywbem.Uint16(2) + Predictive_Failure = pywbem.Uint16(3) + Non_Recoverable_Error = pywbem.Uint16(4) + Supporting_Entity_in_Error = pywbem.Uint16(5) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Not Available', + 1: 'No Additional Information', + 2: 'Stressed', + 3: 'Predictive Failure', + 4: 'Non-Recoverable Error', + 5: 'Supporting Entity in Error' + } + + class Status(object): + OK = 'OK' + Error = 'Error' + Degraded = 'Degraded' + Unknown = 'Unknown' + Pred_Fail = 'Pred Fail' + Starting = 'Starting' + Stopping = 'Stopping' + Service = 'Service' + Stressed = 'Stressed' + NonRecover = 'NonRecover' + No_Contact = 'No Contact' + Lost_Comm = 'Lost Comm' + Stopped = 'Stopped' + + class HealthState(object): + Unknown = pywbem.Uint16(0) + OK = pywbem.Uint16(5) + Degraded_Warning = pywbem.Uint16(10) + Minor_failure = pywbem.Uint16(15) + Major_failure = pywbem.Uint16(20) + Critical_failure = pywbem.Uint16(25) + Non_recoverable_error = pywbem.Uint16(30) + # DMTF_Reserved = .. + # Vendor_Specific = 32768..65535 + _reverse_map = { + 0: 'Unknown', + 5: 'OK', + 10: 'Degraded/Warning', + 15: 'Minor failure', + 20: 'Major failure', + 25: 'Critical failure', + 30: 'Non-recoverable error' + } + + class JobState(object): + New = pywbem.Uint16(2) + Starting = pywbem.Uint16(3) + Running = pywbem.Uint16(4) + Suspended = pywbem.Uint16(5) + Shutting_Down = pywbem.Uint16(6) + Completed = pywbem.Uint16(7) + Terminated = pywbem.Uint16(8) + Killed = pywbem.Uint16(9) + Exception = pywbem.Uint16(10) + Service = pywbem.Uint16(11) + Query_Pending = pywbem.Uint16(12) + # DMTF_Reserved = 13..32767 + # Vendor_Reserved = 32768..65535 + _reverse_map = { + 2: 'New', + 3: 'Starting', + 4: 'Running', + 5: 'Suspended', + 6: 'Shutting Down', + 7: 'Completed', + 8: 'Terminated', + 9: 'Killed', + 10: 'Exception', + 11: 'Service', + 12: 'Query Pending' + } + + class GetError(object): + Success = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unspecified_Error = pywbem.Uint32(2) + Timeout = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + Access_Denied = pywbem.Uint32(6) + # DMTF_Reserved = .. + # Vendor_Specific = 32768..65535 + + class KillJob(object): + Success = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unknown = pywbem.Uint32(2) + Timeout = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Access_Denied = pywbem.Uint32(6) + Not_Found = pywbem.Uint32(7) + # DMTF_Reserved = .. + # Vendor_Specific = 32768..65535 + + class RecoveryAction(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + Do_Not_Continue = pywbem.Uint16(2) + Continue_With_Next_Job = pywbem.Uint16(3) + Re_run_Job = pywbem.Uint16(4) + Run_Recovery_Job = pywbem.Uint16(5) + _reverse_map = { + 0: 'Unknown', + 1: 'Other', + 2: 'Do Not Continue', + 3: 'Continue With Next Job', + 4: 'Re-run Job', + 5: 'Run Recovery Job' + } + + class RunDayOfWeek(object): + _Saturday = pywbem.Sint8(-7) + _Friday = pywbem.Sint8(-6) + _Thursday = pywbem.Sint8(-5) + _Wednesday = pywbem.Sint8(-4) + _Tuesday = pywbem.Sint8(-3) + _Monday = pywbem.Sint8(-2) + _Sunday = pywbem.Sint8(-1) + ExactDayOfMonth = pywbem.Sint8(0) + Sunday = pywbem.Sint8(1) + Monday = pywbem.Sint8(2) + Tuesday = pywbem.Sint8(3) + Wednesday = pywbem.Sint8(4) + Thursday = pywbem.Sint8(5) + Friday = pywbem.Sint8(6) + Saturday = pywbem.Sint8(7) + _reverse_map = { + 0: 'ExactDayOfMonth', + 1: 'Sunday', + 2: 'Monday', + 3: 'Tuesday', + 4: 'Wednesday', + 5: 'Thursday', + 6: 'Friday', + 7: 'Saturday', + -1: '-Sunday', + -7: '-Saturday', + -6: '-Friday', + -5: '-Thursday', + -4: '-Wednesday', + -3: '-Tuesday', + -2: '-Monday' + } + + class RunMonth(object): + January = pywbem.Uint8(0) + February = pywbem.Uint8(1) + March = pywbem.Uint8(2) + April = pywbem.Uint8(3) + May = pywbem.Uint8(4) + June = pywbem.Uint8(5) + July = pywbem.Uint8(6) + August = pywbem.Uint8(7) + September = pywbem.Uint8(8) + October = pywbem.Uint8(9) + November = pywbem.Uint8(10) + December = pywbem.Uint8(11) + _reverse_map = { + 0: 'January', + 1: 'February', + 2: 'March', + 3: 'April', + 4: 'May', + 5: 'June', + 6: 'July', + 7: 'August', + 8: 'September', + 9: 'October', + 10: 'November', + 11: 'December' + } + + class GetErrors(object): + Success = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unspecified_Error = pywbem.Uint32(2) + Timeout = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + Access_Denied = pywbem.Uint32(6) + # DMTF_Reserved = .. + # Vendor_Specific = 32768..65535 + + class CommunicationStatus(object): + Unknown = pywbem.Uint16(0) + Not_Available = pywbem.Uint16(1) + Communication_OK = pywbem.Uint16(2) + Lost_Communication = pywbem.Uint16(3) + No_Contact = pywbem.Uint16(4) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'Not Available', + 2: 'Communication OK', + 3: 'Lost Communication', + 4: 'No Contact' + } + + class OperationalStatus(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + OK = pywbem.Uint16(2) + Degraded = pywbem.Uint16(3) + Stressed = pywbem.Uint16(4) + Predictive_Failure = pywbem.Uint16(5) + Error = pywbem.Uint16(6) + Non_Recoverable_Error = pywbem.Uint16(7) + Starting = pywbem.Uint16(8) + Stopping = pywbem.Uint16(9) + Stopped = pywbem.Uint16(10) + In_Service = pywbem.Uint16(11) + No_Contact = pywbem.Uint16(12) + Lost_Communication = pywbem.Uint16(13) + Aborted = pywbem.Uint16(14) + Dormant = pywbem.Uint16(15) + Supporting_Entity_in_Error = pywbem.Uint16(16) + Completed = pywbem.Uint16(17) + Power_Mode = pywbem.Uint16(18) + Relocating = pywbem.Uint16(19) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'Other', + 2: 'OK', + 3: 'Degraded', + 4: 'Stressed', + 5: 'Predictive Failure', + 6: 'Error', + 7: 'Non-Recoverable Error', + 8: 'Starting', + 9: 'Stopping', + 10: 'Stopped', + 11: 'In Service', + 12: 'No Contact', + 13: 'Lost Communication', + 14: 'Aborted', + 15: 'Dormant', + 16: 'Supporting Entity in Error', + 17: 'Completed', + 18: 'Power Mode', + 19: 'Relocating' + } + + class OperatingStatus(object): + Unknown = pywbem.Uint16(0) + Not_Available = pywbem.Uint16(1) + Servicing = pywbem.Uint16(2) + Starting = pywbem.Uint16(3) + Stopping = pywbem.Uint16(4) + Stopped = pywbem.Uint16(5) + Aborted = pywbem.Uint16(6) + Dormant = pywbem.Uint16(7) + Completed = pywbem.Uint16(8) + Migrating = pywbem.Uint16(9) + Emigrating = pywbem.Uint16(10) + Immigrating = pywbem.Uint16(11) + Snapshotting = pywbem.Uint16(12) + Shutting_Down = pywbem.Uint16(13) + In_Test = pywbem.Uint16(14) + Transitioning = pywbem.Uint16(15) + In_Service = pywbem.Uint16(16) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'Not Available', + 2: 'Servicing', + 3: 'Starting', + 4: 'Stopping', + 5: 'Stopped', + 6: 'Aborted', + 7: 'Dormant', + 8: 'Completed', + 9: 'Migrating', + 10: 'Emigrating', + 11: 'Immigrating', + 12: 'Snapshotting', + 13: 'Shutting Down', + 14: 'In Test', + 15: 'Transitioning', + 16: 'In Service' + } + + class LocalOrUtcTime(object): + Local_Time = pywbem.Uint16(1) + UTC_Time = pywbem.Uint16(2) + _reverse_map = { + 1: 'Local Time', + 2: 'UTC Time' + } + + class RequestStateChange(object): + Completed_with_No_Error = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unknown_Unspecified_Error = pywbem.Uint32(2) + Can_NOT_complete_within_Timeout_Period = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + In_Use = pywbem.Uint32(6) + # DMTF_Reserved = .. + Method_Parameters_Checked___Transition_Started = pywbem.Uint32(4096) + Invalid_State_Transition = pywbem.Uint32(4097) + Use_of_Timeout_Parameter_Not_Supported = pywbem.Uint32(4098) + Busy = pywbem.Uint32(4099) + # Method_Reserved = 4100..32767 + # Vendor_Specific = 32768..65535 + class RequestedState(object): + Start = pywbem.Uint16(2) + Suspend = pywbem.Uint16(3) + Terminate = pywbem.Uint16(4) + Kill = pywbem.Uint16(5) + Service = pywbem.Uint16(6) + # DMTF_Reserved = 7..32767 + # Vendor_Reserved = 32768..65535 + + class PrimaryStatus(object): + Unknown = pywbem.Uint16(0) + OK = pywbem.Uint16(1) + Degraded = pywbem.Uint16(2) + Error = pywbem.Uint16(3) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'OK', + 2: 'Degraded', + 3: 'Error' + } + +def _fill_nonkeys(job, model): + """ + Fills into the model of instance all non-key properties. + """ + model['Caption'] = 'Software installation job with id=%d' % job.jobid + model['CommunicationStatus'] = Values.CommunicationStatus.Not_Available + model['DeleteOnCompletion'] = job.delete_on_completion + try: + description = JOB_DESCRIPTIONS[job.__class__] + kwargs = job.job_kwargs + kwargs['jobid'] = job.jobid + model['Description'] = description % kwargs + except KeyError: + cmpi_logging.logger.error( + 'no description string found for job class %s' % + job.__class__.__name__) + model['Description'] = pywbem.CIMProperty('Description', + type='string', value=None) + if job.started: + if job.finished: + elapsed = job.finished - job.started + else: + elapsed = time.time() - job.started + model['ElapsedTime'] = pywbem.CIMDateTime(timedelta(seconds=elapsed)) + else: + model["ElapsedTime"] = pywbem.CIMProperty('ElapsedTime', + type='datetime', value=None) + model['ErrorCode'] = pywbem.Uint16(0 if job.state != job.EXCEPTION else 1) + try: + model['JobState'], model['OperationalStatus'], model['JobStatus'] = { + jobs.YumJob.NEW : (Values.JobState.New, + [Values.OperationalStatus.Dormant], 'Enqueued'), + jobs.YumJob.RUNNING : (Values.JobState.Running, + [Values.OperationalStatus.OK], 'Running'), + jobs.YumJob.TERMINATED : (Values.JobState.Terminated, + [Values.OperationalStatus.Stopped], 'Terminated'), + jobs.YumJob.EXCEPTION : (Values.JobState.Exception + , [Values.OperationalStatus.Error] + , 'Failed'), + jobs.YumJob.COMPLETED : (Values.JobState.Completed + , [ Values.OperationalStatus.OK + , Values.OperationalStatus.Completed] + , 'Finished successfully') + }[job.state] + except KeyError: + cmpi_logging.logger.error('unknown job state: %s' % job.state) + model['JobState'] = pywbem.CIMProperty('JobState', + type='uint16', value=None) + model['OperationalStatus'] = [Values.OperationalStatus.Unknown] + model['JobStatus'] = 'Unknown' + if 'method_name' in job.metadata: + model['MethodName'] = job.metadata['method_name'] + else: + model["MethodName"] = pywbem.CIMProperty('MethodName', + type='string', value=None) + model['Name'] = job.metadata['name'] + model['PercentComplete'] = pywbem.Uint16( + 100 if job.state == job.COMPLETED else ( + 50 if job.state == job.RUNNING else + 0)) + model['Priority'] = pywbem.Uint32(job.priority) + if job.started: + model['StartTime'] = pywbem.CIMDateTime(datetime.fromtimestamp( + job.started)) + model['TimeBeforeRemoval'] = pywbem.CIMDateTime(timedelta( + seconds=job.time_before_removal)) + model['TimeOfLastStateChange'] = pywbem.CIMDateTime(datetime.fromtimestamp( + job.last_change)) + model['TimeSubmitted'] = pywbem.CIMDateTime(datetime.fromtimestamp( + job.created)) + +@cmpi_logging.trace_function +def job2model(job, keys_only=True, model=None): + """ + @param job can either be jobs.YumAsyncJob or integer + @param model if not None, will be filled with data, otherwise + a new instance of CIMInstance or CIMObjectPath is created + """ + if not isinstance(job, (int, long, jobs.YumAsyncJob)): + raise TypeError("job must be an instance of YumAsyncJob") + if isinstance(job, jobs.YumAsyncJob) and not job.async: + raise ValueError("job must be asynchronous") + if not keys_only and isinstance(job, (int, long)): + raise TypeError("job must be an instance of YumAsyncJob" + " filling non-key properties") + + if model is None: + model = pywbem.CIMInstanceName("LMI_SoftwareInstallationJob", + namespace="root/cimv2") + if not keys_only: + model = pywbem.CIMInstance("LMI_SoftwareInstallationJob", + path=model) + + jobid = job.jobid if isinstance(job, jobs.YumAsyncJob) else job + model['InstanceID'] = 'LMI:SoftwareInstallationJob:%d' % jobid + if isinstance(model, pywbem.CIMInstance): + model.path['InstanceID'] = model['InstanceID'] #pylint: disable=E1103 + if not keys_only: + _fill_nonkeys(job, model) + return model + +@cmpi_logging.trace_function +def object_path2job(op): + """ + @param op must contain precise InstanceID of job + """ + if not isinstance(op, pywbem.CIMInstanceName): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "op must be an instance of CIMInstanceName") + + if (not "InstanceID" in op or not op['InstanceID']): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, "Wrong keys.") + instid = op['InstanceID'] + if not instid.lower().startswith("lmi:softwareinstallationjob:"): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "InstanceID must start with LMI:SoftwareInstallationJob: prefix.") + try: + instid = int(instid[len("LMI:SoftwareInstallationJob:"):]) + except ValueError: + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + 'Invalid InstanceID "%s"' % instid) + try: + return YumDB.get_instance().get_job(instid) + except errors.JobNotFound: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + 'Not such job "%s".' % op['InstanceID']) + +@cmpi_logging.trace_function +def modify_instance(instance): + """ + This call modifies the job's parameters according to given instance. + """ + job = object_path2job(instance.path) + ydb = YumDB.get_instance() + update_kwargs = {} + reschedule_kwargs = {} + # all modifiable properties + prop_name_map = { + "name" : "name", + "priority" : "priority", + "deleteoncompletion" : "delete_on_completion", + "timebeforeremoval" : "time_before_removal" + } + metadata_props = {"name"} + reschedule_props = {"delete_on_completion", "time_before_removal"} + for name, prop in instance.properties.items(): + if prop is None: + cmpi_logging.logger.warn('property "%s" is None') + continue + name = name.lower() + try: + pname = prop_name_map[name] + if pname == "priority" and job.priority != prop.value: + cmpi_logging.logger.info( + 'changing priority of job %s to %d', job, prop.value) + job = ydb.set_job_priority(job.jobid, prop.value) + elif pname in reschedule_props: + if getattr(job, pname) == prop.value: + continue + if pname == "time_before_removal": + value = prop.value.timedelta.total_seconds() + else: + value = prop.value + reschedule_kwargs[pname] = value + else: + if pname in metadata_props: + if not 'metadata' in update_kwargs: + update_kwargs['metadata'] = {} + update_kwargs['metadata'][pname] = prop.value + else: + update_kwargs[pname] = prop.value + except KeyError: + if name == 'instanceid': + continue + cmpi_logging.logger.warn("skipping property %s: %s", name, prop) + + if reschedule_kwargs: + for prop in ('delete_on_completion', 'time_before_removal'): + if prop not in reschedule_kwargs: + reschedule_kwargs[prop] = getattr(job, prop) + cmpi_logging.logger.info('rescheduling job %s to: %s', + job, ", ".join("%s=%s"%(k, v) for k, v in + reschedule_kwargs.items())) + job = ydb.reschedule_job(job.jobid, **reschedule_kwargs) + + if update_kwargs: + cmpi_logging.logger.info('changing atributes of job %s to: %s', + job, ", ".join("%s=%s"%(k, v) for k, v in + update_kwargs.items())) + job = ydb.update_job(job.jobid, **update_kwargs) + + return job2model(job, keys_only=False, model=instance) + diff --git a/src/software/openlmi/software/core/InstallationService.py b/src/software/openlmi/software/core/InstallationService.py new file mode 100644 index 0000000..a2f197a --- /dev/null +++ b/src/software/openlmi/software/core/InstallationService.py @@ -0,0 +1,729 @@ +# -*- encoding: utf-8 -*- +# Software Management Providers +# +# Copyright (C) 2012 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Just a common functionality related to LMI_SoftwareInstallationService +provider. +""" + +import pywbem + +from openlmi.common import cmpi_logging +from openlmi.software.core import ComputerSystem +from openlmi.software.core import Identity +from openlmi.software.core import SystemCollection +from openlmi.software.yumdb import errors +from openlmi.software.yumdb import YumDB + +#UNSUPPORTED_INSTALL_OPTIONS = { +# { "Defer_target_system_reset", "Force_installation", +# "Reboot", "Password", "Log", "Silent_Mode", "Administrative_Mode", +# "Schedule_Install_At", "DMTF_Reserved" } + +class InstallationError(Exception): + """This exception shall be raised upon any error within + install_or_remove_package() function. + """ + def __init__(self, return_code, description): + Exception.__init__(self, return_code, description) + @property + def return_code(self): + """@return return code of CIM method""" + return self.args[0] + @property + def description(self): + """@return description of error""" + return self.args[1] + +class Values(object): + class DetailedStatus(object): + Not_Available = pywbem.Uint16(0) + No_Additional_Information = pywbem.Uint16(1) + Stressed = pywbem.Uint16(2) + Predictive_Failure = pywbem.Uint16(3) + Non_Recoverable_Error = pywbem.Uint16(4) + Supporting_Entity_in_Error = pywbem.Uint16(5) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Not Available', + 1: 'No Additional Information', + 2: 'Stressed', + 3: 'Predictive Failure', + 4: 'Non-Recoverable Error', + 5: 'Supporting Entity in Error' + } + + class RequestedState(object): + Unknown = pywbem.Uint16(0) + Enabled = pywbem.Uint16(2) + Disabled = pywbem.Uint16(3) + Shut_Down = pywbem.Uint16(4) + No_Change = pywbem.Uint16(5) + Offline = pywbem.Uint16(6) + Test = pywbem.Uint16(7) + Deferred = pywbem.Uint16(8) + Quiesce = pywbem.Uint16(9) + Reboot = pywbem.Uint16(10) + Reset = pywbem.Uint16(11) + Not_Applicable = pywbem.Uint16(12) + # DMTF_Reserved = .. + # Vendor_Reserved = 32768..65535 + _reverse_map = { + 0: 'Unknown', + 2: 'Enabled', + 3: 'Disabled', + 4: 'Shut Down', + 5: 'No Change', + 6: 'Offline', + 7: 'Test', + 8: 'Deferred', + 9: 'Quiesce', + 10: 'Reboot', + 11: 'Reset', + 12: 'Not Applicable' + } + + class HealthState(object): + Unknown = pywbem.Uint16(0) + OK = pywbem.Uint16(5) + Degraded_Warning = pywbem.Uint16(10) + Minor_failure = pywbem.Uint16(15) + Major_failure = pywbem.Uint16(20) + Critical_failure = pywbem.Uint16(25) + Non_recoverable_error = pywbem.Uint16(30) + # DMTF_Reserved = .. + # Vendor_Specific = 32768..65535 + _reverse_map = { + 0: 'Unknown', + 5: 'OK', + 10: 'Degraded/Warning', + 15: 'Minor failure', + 20: 'Major failure', + 25: 'Critical failure', + 30: 'Non-recoverable error' + } + + class InstallFromURI(object): + Job_Completed_with_No_Error = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unspecified_Error = pywbem.Uint32(2) + Timeout = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + Target_In_Use = pywbem.Uint32(6) + # DMTF_Reserved = .. + Method_Parameters_Checked___Job_Started = pywbem.Uint32(4096) + Unsupported_TargetType = pywbem.Uint32(4097) + Unattended_silent_installation_not_supported = pywbem.Uint32(4098) + Downgrade_reinstall_not_supported = pywbem.Uint32(4099) + Not_enough_memory = pywbem.Uint32(4100) + Not_enough_swap_space = pywbem.Uint32(4101) + Unsupported_version_transition = pywbem.Uint32(4102) + Not_enough_disk_space = pywbem.Uint32(4103) + Software_and_target_operating_system_mismatch = pywbem.Uint32(4104) + Missing_dependencies = pywbem.Uint32(4105) + Not_applicable_to_target = pywbem.Uint32(4106) + URI_not_accessible = pywbem.Uint32(4107) + # Method_Reserved = 4108..32767 + # Vendor_Specific = 32768..65535 + class InstallOptions(object): + Defer_target_system_reset = pywbem.Uint16(2) + Force_installation = pywbem.Uint16(3) + Install = pywbem.Uint16(4) + Update = pywbem.Uint16(5) + Repair = pywbem.Uint16(6) + Reboot = pywbem.Uint16(7) + Password = pywbem.Uint16(8) + Uninstall = pywbem.Uint16(9) + Log = pywbem.Uint16(10) + SilentMode = pywbem.Uint16(11) + AdministrativeMode = pywbem.Uint16(12) + ScheduleInstallAt = pywbem.Uint16(13) + # DMTF_Reserved = .. + # Vendor_Specific = 32768..65535 + _reverse_map = { + 2 : "Defer Target/System Reset", + 3 : "Force Installation", + 4 : "Install", + 5 : "Update", + 6 : "Repair", + 7 : "Reboot", + 8 : "Password", + 9 : "Uninstall", + 10 : "Log", + 11 : "SilentMode", + 12 : "AdministrativeMode", + 13 : "ScheduleInstallAt", + } + + InstallOptions.supported = { + InstallOptions.Install, + InstallOptions.Update, + InstallOptions.Uninstall, + InstallOptions.Force_installation, + InstallOptions.Repair, + } + + + class CheckSoftwareIdentity(object): + Job_Completed_with_No_Error = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unspecified_Error = pywbem.Uint32(2) + Timeout = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + Target_In_Use = pywbem.Uint32(6) + # DMTF_Reserved = .. + Method_Reserved = pywbem.Uint32(4096) + Unsupported_TargetType = pywbem.Uint32(4097) + Unattended_silent_installation_not_supported = pywbem.Uint32(4098) + Downgrade_reinstall_not_supported = pywbem.Uint32(4099) + Not_enough_memory = pywbem.Uint32(4100) + Not_enough_swap_space = pywbem.Uint32(4101) + Unsupported_version_transition = pywbem.Uint32(4102) + Not_enough_disk_space = pywbem.Uint32(4103) + Software_and_target_operating_system_mismatch = pywbem.Uint32(4104) + Missing_dependencies = pywbem.Uint32(4105) + Not_applicable_to_target = pywbem.Uint32(4106) + No_supported_path_to_image = pywbem.Uint32(4107) + Cannot_add_to_Collection = pywbem.Uint32(4108) + Asynchronous_Job_already_in_progress = pywbem.Uint32(4109) + # Method_Reserved = 4110..32767 + # Vendor_Specific = 32768..65535 + class InstallCharacteristics(object): + Target_automatic_reset = pywbem.Uint16(2) + System_automatic_reset = pywbem.Uint16(3) + Separate_target_reset_Required = pywbem.Uint16(4) + Separate_system_reset_Required = pywbem.Uint16(5) + Manual_Reboot_Required = pywbem.Uint16(6) + No_Reboot_Required = pywbem.Uint16(7) + User_Intervention_recommended = pywbem.Uint16(8) + MAY_be_added_to_specified_Collection = pywbem.Uint16(9) + # DMTF_Reserved = .. + # Vendor_Specific = 0x7FFF..0xFFFF + + class ChangeAffectedElementsAssignedSequence(object): + Completed_with_No_Error = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Error_Occured = pywbem.Uint32(2) + Busy = pywbem.Uint32(3) + Invalid_Reference = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + Access_Denied = pywbem.Uint32(6) + # DMTF_Reserved = 7..32767 + # Vendor_Specified = 32768..65535 + + class TransitioningToState(object): + Unknown = pywbem.Uint16(0) + Enabled = pywbem.Uint16(2) + Disabled = pywbem.Uint16(3) + Shut_Down = pywbem.Uint16(4) + No_Change = pywbem.Uint16(5) + Offline = pywbem.Uint16(6) + Test = pywbem.Uint16(7) + Defer = pywbem.Uint16(8) + Quiesce = pywbem.Uint16(9) + Reboot = pywbem.Uint16(10) + Reset = pywbem.Uint16(11) + Not_Applicable = pywbem.Uint16(12) + # DMTF_Reserved = .. + _reverse_map = { + 0: 'Unknown', + 2: 'Enabled', + 3: 'Disabled', + 4: 'Shut Down', + 5: 'No Change', + 6: 'Offline', + 7: 'Test', + 8: 'Defer', + 9: 'Quiesce', + 10: 'Reboot', + 11: 'Reset', + 12: 'Not Applicable' + } + + class EnabledDefault(object): + Enabled = pywbem.Uint16(2) + Disabled = pywbem.Uint16(3) + Not_Applicable = pywbem.Uint16(5) + Enabled_but_Offline = pywbem.Uint16(6) + No_Default = pywbem.Uint16(7) + Quiesce = pywbem.Uint16(9) + # DMTF_Reserved = .. + # Vendor_Reserved = 32768..65535 + _reverse_map = { + 2: 'Enabled', + 3: 'Disabled', + 5: 'Not Applicable', + 6: 'Enabled but Offline', + 7: 'No Default', + 9: 'Quiesce' + } + + class EnabledState(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + Enabled = pywbem.Uint16(2) + Disabled = pywbem.Uint16(3) + Shutting_Down = pywbem.Uint16(4) + Not_Applicable = pywbem.Uint16(5) + Enabled_but_Offline = pywbem.Uint16(6) + In_Test = pywbem.Uint16(7) + Deferred = pywbem.Uint16(8) + Quiesce = pywbem.Uint16(9) + Starting = pywbem.Uint16(10) + # DMTF_Reserved = 11..32767 + # Vendor_Reserved = 32768..65535 + _reverse_map = { + 0: 'Unknown', + 1: 'Other', + 2: 'Enabled', + 3: 'Disabled', + 4: 'Shutting Down', + 5: 'Not Applicable', + 6: 'Enabled but Offline', + 7: 'In Test', + 8: 'Deferred', + 9: 'Quiesce', + 10: 'Starting' + } + + class AvailableRequestedStates(object): + Enabled = pywbem.Uint16(2) + Disabled = pywbem.Uint16(3) + Shut_Down = pywbem.Uint16(4) + Offline = pywbem.Uint16(6) + Test = pywbem.Uint16(7) + Defer = pywbem.Uint16(8) + Quiesce = pywbem.Uint16(9) + Reboot = pywbem.Uint16(10) + Reset = pywbem.Uint16(11) + # DMTF_Reserved = .. + _reverse_map = { + 2: 'Enabled', + 3: 'Disabled', + 4: 'Shut Down', + 6: 'Offline', + 7: 'Test', + 8: 'Defer', + 9: 'Quiesce', + 10: 'Reboot', + 11: 'Reset' + } + + class Status(object): + OK = 'OK' + Error = 'Error' + Degraded = 'Degraded' + Unknown = 'Unknown' + Pred_Fail = 'Pred Fail' + Starting = 'Starting' + Stopping = 'Stopping' + Service = 'Service' + Stressed = 'Stressed' + NonRecover = 'NonRecover' + No_Contact = 'No Contact' + Lost_Comm = 'Lost Comm' + Stopped = 'Stopped' + + class CommunicationStatus(object): + Unknown = pywbem.Uint16(0) + Not_Available = pywbem.Uint16(1) + Communication_OK = pywbem.Uint16(2) + Lost_Communication = pywbem.Uint16(3) + No_Contact = pywbem.Uint16(4) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'Not Available', + 2: 'Communication OK', + 3: 'Lost Communication', + 4: 'No Contact' + } + + class OperationalStatus(object): + Unknown = pywbem.Uint16(0) + Other = pywbem.Uint16(1) + OK = pywbem.Uint16(2) + Degraded = pywbem.Uint16(3) + Stressed = pywbem.Uint16(4) + Predictive_Failure = pywbem.Uint16(5) + Error = pywbem.Uint16(6) + Non_Recoverable_Error = pywbem.Uint16(7) + Starting = pywbem.Uint16(8) + Stopping = pywbem.Uint16(9) + Stopped = pywbem.Uint16(10) + In_Service = pywbem.Uint16(11) + No_Contact = pywbem.Uint16(12) + Lost_Communication = pywbem.Uint16(13) + Aborted = pywbem.Uint16(14) + Dormant = pywbem.Uint16(15) + Supporting_Entity_in_Error = pywbem.Uint16(16) + Completed = pywbem.Uint16(17) + Power_Mode = pywbem.Uint16(18) + Relocating = pywbem.Uint16(19) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'Other', + 2: 'OK', + 3: 'Degraded', + 4: 'Stressed', + 5: 'Predictive Failure', + 6: 'Error', + 7: 'Non-Recoverable Error', + 8: 'Starting', + 9: 'Stopping', + 10: 'Stopped', + 11: 'In Service', + 12: 'No Contact', + 13: 'Lost Communication', + 14: 'Aborted', + 15: 'Dormant', + 16: 'Supporting Entity in Error', + 17: 'Completed', + 18: 'Power Mode', + 19: 'Relocating' + } + + class OperatingStatus(object): + Unknown = pywbem.Uint16(0) + Not_Available = pywbem.Uint16(1) + Servicing = pywbem.Uint16(2) + Starting = pywbem.Uint16(3) + Stopping = pywbem.Uint16(4) + Stopped = pywbem.Uint16(5) + Aborted = pywbem.Uint16(6) + Dormant = pywbem.Uint16(7) + Completed = pywbem.Uint16(8) + Migrating = pywbem.Uint16(9) + Emigrating = pywbem.Uint16(10) + Immigrating = pywbem.Uint16(11) + Snapshotting = pywbem.Uint16(12) + Shutting_Down = pywbem.Uint16(13) + In_Test = pywbem.Uint16(14) + Transitioning = pywbem.Uint16(15) + In_Service = pywbem.Uint16(16) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'Not Available', + 2: 'Servicing', + 3: 'Starting', + 4: 'Stopping', + 5: 'Stopped', + 6: 'Aborted', + 7: 'Dormant', + 8: 'Completed', + 9: 'Migrating', + 10: 'Emigrating', + 11: 'Immigrating', + 12: 'Snapshotting', + 13: 'Shutting Down', + 14: 'In Test', + 15: 'Transitioning', + 16: 'In Service' + } + + class RequestStateChange(object): + Completed_with_No_Error = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unknown_or_Unspecified_Error = pywbem.Uint32(2) + Cannot_complete_within_Timeout_Period = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + In_Use = pywbem.Uint32(6) + # DMTF_Reserved = .. + Method_Parameters_Checked___Job_Started = pywbem.Uint32(4096) + Invalid_State_Transition = pywbem.Uint32(4097) + Use_of_Timeout_Parameter_Not_Supported = pywbem.Uint32(4098) + Busy = pywbem.Uint32(4099) + # Method_Reserved = 4100..32767 + # Vendor_Specific = 32768..65535 + class RequestedState(object): + Enabled = pywbem.Uint16(2) + Disabled = pywbem.Uint16(3) + Shut_Down = pywbem.Uint16(4) + Offline = pywbem.Uint16(6) + Test = pywbem.Uint16(7) + Defer = pywbem.Uint16(8) + Quiesce = pywbem.Uint16(9) + Reboot = pywbem.Uint16(10) + Reset = pywbem.Uint16(11) + # DMTF_Reserved = .. + # Vendor_Reserved = 32768..65535 + + class InstallFromByteStream(object): + Job_Completed_with_No_Error = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unspecified_Error = pywbem.Uint32(2) + Timeout = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + Target_In_Use = pywbem.Uint32(6) + # DMTF_Reserved = .. + Method_Parameters_Checked___Job_Started = pywbem.Uint32(4096) + Unsupported_TargetType = pywbem.Uint32(4097) + Unattended_silent_installation_not_supported = pywbem.Uint32(4098) + Downgrade_reinstall_not_supported = pywbem.Uint32(4099) + Not_enough_memory = pywbem.Uint32(4100) + Not_enough_swap_space = pywbem.Uint32(4101) + Unsupported_version_transition = pywbem.Uint32(4102) + Not_enough_disk_space = pywbem.Uint32(4103) + Software_and_target_operating_system_mismatch = pywbem.Uint32(4104) + Missing_dependencies = pywbem.Uint32(4105) + Not_applicable_to_target = pywbem.Uint32(4106) + No_supported_path_to_image = pywbem.Uint32(4107) + # Method_Reserved = 4108..32767 + # Vendor_Specific = 32768..65535 + + InstallFromByteStream.InstallOptions = InstallFromURI.InstallOptions + + class InstallFromSoftwareIdentity(object): + Job_Completed_with_No_Error = pywbem.Uint32(0) + Not_Supported = pywbem.Uint32(1) + Unspecified_Error = pywbem.Uint32(2) + Timeout = pywbem.Uint32(3) + Failed = pywbem.Uint32(4) + Invalid_Parameter = pywbem.Uint32(5) + Target_In_Use = pywbem.Uint32(6) + # DMTF_Reserved = .. + Method_Parameters_Checked___Job_Started = pywbem.Uint32(4096) + Unsupported_TargetType = pywbem.Uint32(4097) + Unattended_silent_installation_not_supported = pywbem.Uint32(4098) + Downgrade_reinstall_not_supported = pywbem.Uint32(4099) + Not_enough_memory = pywbem.Uint32(4100) + Not_enough_swap_space = pywbem.Uint32(4101) + Unsupported_version_transition = pywbem.Uint32(4102) + Not_enough_disk_space = pywbem.Uint32(4103) + Software_and_target_operating_system_mismatch = pywbem.Uint32(4104) + Missing_dependencies = pywbem.Uint32(4105) + Not_applicable_to_target = pywbem.Uint32(4106) + No_supported_path_to_image = pywbem.Uint32(4107) + Cannot_add_to_Collection = pywbem.Uint32(4108) + # Method_Reserved = 4109..32767 + # Vendor_Specific = 32768..65535 + + InstallFromSoftwareIdentity.InstallOptions = InstallFromURI.InstallOptions + + class StartMode(object): + Automatic = 'Automatic' + Manual = 'Manual' + + class PrimaryStatus(object): + Unknown = pywbem.Uint16(0) + OK = pywbem.Uint16(1) + Degraded = pywbem.Uint16(2) + Error = pywbem.Uint16(3) + # DMTF_Reserved = .. + # Vendor_Reserved = 0x8000.. + _reverse_map = { + 0: 'Unknown', + 1: 'OK', + 2: 'Degraded', + 3: 'Error' + } + +def get_path(): + """@return instance name with prefilled properties""" + op = pywbem.CIMInstanceName( + classname="LMI_SoftwareInstallationService", + namespace="root/cimv2") + op['CreationClassName'] = op.classname + systemop = ComputerSystem.get_path() + op["SystemCreationClassName"] = systemop.classname + op['SystemName'] = systemop["Name"] + op["Name"] = "LMI:SoftwareInstallationService" + return op + +def check_path(env, service, prop_name): + """ + Checks instance name of SoftwareInstallationService. + @param prop_name name of object name; used for error descriptions + """ + if not isinstance(service, pywbem.CIMInstanceName): + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "\"%s\" must be a CIMInstanceName" % prop_name) + our_service = get_path() + ch = env.get_cimom_handle() + if service.namespace != our_service.namespace: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + 'Namespace of "%s" does not match "%s"' % ( + prop_name, our_service.namespace)) + if not ch.is_subclass(our_service.namespace, + sub=service.classname, + super=our_service.classname): + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + "Class of \"%s\" must be a sublass of %s" % ( + prop_name, our_service.classname)) + for key in our_service.keybindings.keys(): + if not key in service: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + "\"%s\" is missing %s key property" % ( prop_name, key)) + if service[key] != our_service[key]: + raise pywbem.CIMError(pywbem.CIM_ERR_NOT_FOUND, + "\"%s\" key property %s(%s) does not match \"%s\"" %( + prop_name, key, service[key], our_service[key])) + return True + +def check_path_property(env, op, prop_name): + """ + Checks, whether prop_name property of op object path is correct. + If not, an exception will be raised. + """ + if not prop_name in op: + raise pywbem.CIMError(pywbem.CIM_ERR_INVALID_PARAMETER, + "Missing %s key property!" % prop_name) + return check_path(env, op[prop_name], prop_name) + +def _check_target_and_collection(env, src_type, target, collection): + """ + Checks Target and Collection parameters of provider's installation + methods. + """ + values = Values.InstallFromSoftwareIdentity + if target: + try: + ComputerSystem.check_path(env, target, "Target") + except pywbem.CIMError as exc: + raise InstallationError(values.Unspecified_Error, + "Target must be either NULL or match managed" + " computer system: %s", str(exc)) + if collection: + try: + SystemCollection.check_path(env, collection, "Collection") + except pywbem.CIMError as exc: + raise InstallationError(values.Unspecified_Error, + "Collection does not match system software collection: %s" % + str(exc)) + if target and collection: + raise InstallationError(values.Unspecified_Error, + "Only one of Target and Collection parameters can be specified" + " at the same time.") + if not target and not collection: + raise InstallationError(values.Unspecified_Error, + "Either Target or Collection parameter must be specified." + if src_type == "identity" else + "Missing Target parameter.") + +def _install_or_remove_check_params( + env, src_type, source, target, collection, + install_options, + install_options_values): + """ + Checks parameters of provider's installation methods. + Upon any invalid option an InstallationError will be raised. + @return tuple (action, force, repair) + where action is one of Values.InstallFromSoftwareIdentity properties + """ + values = Values.InstallFromSoftwareIdentity + supported_options = values.InstallOptions.supported.copy() + if src_type == "uri": + supported_options.remove(values.InstallOptions.Uninstall) + + if not src_type in {"uri", "identity"}: + raise ValueError('uri must be one of {"uri", "identity"}') + if not source: + raise InstallationError(values.Unspecified_Error, + "Missing %s parameter." % ( + "URI" if src_type == "uri" else "Source")) + if not install_options: + install_options = [] + elif not isinstance(install_options, list): + raise InstallationError(values.Unspecified_Error, + "InstallOptions must be a list of uint16 values.") + options = {p for p in install_options} + + if options - supported_options: + raise InstallationError(values.Unspecified_Error, + "unsupported install options: {%s}" % + ", ".join([ values.InstallOptions._reverse_map[p] + for p in options - supported_options])) + if install_options_values and len(options) != len(install_options_values): + raise InstallationError(values.Unspecified_Error, + "InstallOptions array must have the same" + " length as InstallOptionsValues: %d != %d" % ( + len(options), len(install_options_values))) + if install_options_values: + for opt, val in zip(install_options, install_options_values): + if val: + raise InstallationError(values.Unspecified_Error, + "install option \"%s\" can not have any" + " associated value: %s" % (opt, val)) + _check_target_and_collection(env, src_type, target, collection) + exclusive = [opt for opt in options if opt in { + values.InstallOptions.Install, + values.InstallOptions.Update, + values.InstallOptions.Uninstall }] + if len(exclusive) > 1: + raise InstallationError(values.Unspecified_Error, + "specified more than one mutually exclusive option at once: {%s}" % + ", ".join([ values.InstallOptions._reverse_map[p] + for p in exclusive])) + if not exclusive: + exclusive.append(values.InstallOptions.Install) + return ( exclusive[0] + , values.InstallOptions.Force_installation in options + , values.InstallOptions.Repair in options) + +def install_or_remove_package(env, src_type, source, target, collection, + install_options, + install_options_values): + """ + @param src_type is one of {"uri", "identity"} + """ + values = Values.InstallFromSoftwareIdentity + (action, force, repair) = _install_or_remove_check_params( + env, src_type, source, target, collection, + install_options, install_options_values) + try: + ydb = YumDB.get_instance() + if action == values.InstallOptions.Uninstall: + nevra = Identity.object_path2nevra( + source, with_epoch='ALWAYS') + cmpi_logging.logger.info('removing package %s', nevra) + jobid = ydb.remove_package(nevra, async=True) + else: + update = action == values.InstallOptions.Update + if src_type == "uri": + cmpi_logging.logger.info('%s package "%s"', + 'updating' if update else 'installing', source) + jobid = ydb.install_package_from_uri( + source, update_only=update, force=force or repair, + async=True) + else: # software identity + nevra = Identity.object_path2nevra( + source, with_epoch='ALWAYS') + if update: + jobid = ydb.update_package(nevra, + force=force or repair, async=True) + else: + jobid = ydb.install_package( + nevra, force=force or repair, async=True) + cmpi_logging.logger.info('installation job %s for pkg "%s"' + ' enqueued', jobid, nevra) + return jobid + except (pywbem.CIMError, errors.InvalidURI) as exc: + cmpi_logging.logger.error('failed to install/remove package "%s"' + ' from %s: %s', source, src_type, str(exc)) + raise InstallationError(values.Unspecified_Error, str(exc)) + diff --git a/src/software/openlmi/software/core/SystemSoftwareCollection.py b/src/software/openlmi/software/core/SystemCollection.py index 0f59f3b..0f59f3b 100644 --- a/src/software/openlmi/software/core/SystemSoftwareCollection.py +++ b/src/software/openlmi/software/core/SystemCollection.py diff --git a/src/software/openlmi/software/yumdb/__init__.py b/src/software/openlmi/software/yumdb/__init__.py index 512602a..c9fc0c5 100644 --- a/src/software/openlmi/software/yumdb/__init__.py +++ b/src/software/openlmi/software/yumdb/__init__.py @@ -33,10 +33,12 @@ only accessor to yum api. """ import errno +import inspect import os import re import time -from multiprocessing import Process, JoinableQueue, Queue #pylint: disable=W0404 +from multiprocessing import Process, Queue #pylint: disable=W0404 +from pywbem.cim_provider2 import CIMProvider2 import Queue as TQueue # T as threaded import threading import yum @@ -49,21 +51,29 @@ from openlmi.software.yumdb.packagecheck import PackageFile from openlmi.software.yumdb.packagecheck import PackageCheck from openlmi.software.yumdb.process import YumWorker from openlmi.software.yumdb.repository import Repository +from openlmi.software.yumdb.util import DispatchingFormatter from openlmi.software.util import get_signal_name, singletonmixin # Maximum time in seconds to wait for a job to accomplish. # If timeout expires, spawned process is checked (it might # be possibly killed) and is respawned in case it's dead. -MAX_JOB_WAIT_TIME = 120 +MAX_JOB_WAIT_TIME = 30 # this may be used as an argument to YumWorker YUM_WORKER_DEBUG_LOGGING_CONFIG = { "version" : 1, "formatters": { + # this is a message format for logging function/method calls + # it's manually set up in YumWorker's init method "default": { - "format" : "%(asctime)s %(levelname)s:%(module)s:" - "%(funcName)s:%(lineno)d - %(message)s" - } + "()": DispatchingFormatter, + "formatters" : { + "openlmi.software.yumdb.util.trace_function": + "%(asctime)s %(levelname)s:%(message)s" + }, + "default" : "%(asctime)s %(levelname)s:%(module)s:" + "%(funcName)s:%(lineno)d - %(message)s" + }, }, "handlers": { "file" : { @@ -71,30 +81,121 @@ YUM_WORKER_DEBUG_LOGGING_CONFIG = { "filename" : "/var/tmp/YumWorker.log", "level" : "DEBUG", "formatter": "default", - } + }, }, - "root": { - "level": "DEBUG", - "handlers" : ["file"] - } + "loggers" : { + "root": { + "level": "ERROR", + "handlers" : ["file"] + }, + "openlmi.software.yumdb": { + "level" : "DEBUG", + "handlers" : ["file"], + "propagate" : False, + }, } +} +# ***************************************************************************** +# Utilities +# ***************************************************************************** def log_reply_error(job, reply): """ Raises an exception in case of error occured in worker process while processing job. """ - if isinstance(reply, tuple): + if isinstance(reply, (int, long)): + # asynchronous job + return + if not isinstance(reply, jobs.YumJob): + raise TypeError('expected instance of jobs.YumJob for reply, not "%s"' % + reply.__class__.__name__) + if reply.result == jobs.YumJob.RESULT_ERROR: cmpi_logging.logger.error( - "YumDB: job %s(id=%s) failed with error %s: %s", - job.__class__.__name__, job.jobid, - reply[0].__name__, str(reply[1])) + "YumDB: %s failed with error %s: %s", + job, reply.result_data[0].__name__, str(reply.result_data[1])) cmpi_logging.logger.trace_warn( - "YumDB: job %s(id=%s) exception traceback:\n%s%s: %s", - job.__class__.__name__, job.jobid, "".join(reply[2]), - reply[0].__name__, str(reply[1])) - reply[1].tb_printed = True - raise reply[1] + "YumDB: %s exception traceback:\n%s%s: %s", + job, "".join(reply.result_data[2]), + reply.result_data[0].__name__, str(reply.result_data[1])) + reply.result_data[1].tb_printed = True + raise reply.result_data[1] + elif reply.result == jobs.YumJob.RESULT_TERMINATED: + cmpi_logging.logger.warn('YumDB: %s terminated', job) + else: + cmpi_logging.logger.debug('YumDB: %s completed with success', job) + +def _make_async_job(jobcls, *args, **kwargs): + """Creates asynchronous job, filling it wih some metadata.""" + if not issubclass(jobcls, jobs.YumAsyncJob): + raise TypeError("jobcls must be a subclass of YumAsyncJob") + job = jobcls(*args, **kwargs) + if job.metadata is None: + job.metadata = {} + job.metadata['name'] = \ + type(job).__name__[len('Yum'):] + ('-%d' % job.jobid) + frm = inspect.currentframe() + method_name = None + while ( frm is not None + and ( not 'self' in frm.f_locals + or not isinstance(frm.f_locals['self'], CIMProvider2))): + frm = frm.f_back + if frm is not None: + prov = frm.f_locals['self'] + method_name = frm.f_code.co_name.lower() + if method_name.startswith('cim_method_'): + method_name = method_name[len('cim_method_'):] + if hasattr(prov, 'values'): + lowertocorrectcase = { + k.lower(): k for k in prov.values.__dict__ } + try: + method_name = lowertocorrectcase[method_name] + except KeyError: + pass + if method_name is not None: + job.metadata['method_name'] = method_name + return job + +# ***************************************************************************** +# Decorators +# ***************************************************************************** +def job_request(async=False): + """ + Decorator factory for job entry points. They are YumDB methods. + All of them must return either job objects or jobid for asynchronous calls. + Job objects are processed by this decorator for caller to obtain only the + information he needs. + + It wrapps them with logger wrapper and in case of asynchronous jobs, + it returns just the jobid. + """ + def _decorator(method): + """ + Decorator that just logs the method's call and returns job's result. + """ + logged = cmpi_logging.trace_method(method) + def _new_func(self, *args, **kwargs): + """Wrapper for YumDB's method.""" + return logged(self, *args, **kwargs).result_data + return _new_func + + def _decorator_async(method): + """ + Decorator for methods accepting async argument. In case of async=True, + the method returns jobid. Job's result is returned otherwise. + """ + logged = cmpi_logging.trace_method(method) + def _new_func(self, *args, **kwargs): + """Wrapper for YumDB's method.""" + callargs = inspect.getcallargs(method, self, *args, **kwargs) + result = logged(self, *args, **kwargs) + if callargs.get('async', False): + return result + else: + return result.result_data + return _new_func + + return _decorator_async if async else _decorator class YumDB(singletonmixin.Singleton): """ @@ -122,12 +223,15 @@ class YumDB(singletonmixin.Singleton): # this is to inform Singleton, that __init__ should be called only once ignoreSubsequent = True - def __init__(self, *args, **kwargs): #pylint: disable=W0231 + @cmpi_logging.trace_method + def __init__(self, **kwargs): #pylint: disable=W0231 """ All arguments are passed to yum.YumBase constructor. """ self._process = None - self._yum_args = (args, kwargs) + if kwargs is None: + kwargs = {} + self._yum_kwargs = kwargs self._session_lock = threading.RLock() self._session_level = 0 @@ -194,21 +298,21 @@ class YumDB(singletonmixin.Singleton): cmpi_logging.logger.debug("[jobid=%d] blocking on downlink queue", job.jobid) try: - jobid, reply = self._worker.downlink.get( + jobout = self._worker.downlink.get( block=True, timeout=MAX_JOB_WAIT_TIME) - if jobid == job.jobid: + if jobout.jobid == job.jobid: with self._reply_lock: cmpi_logging.logger.debug( "[jobid=%d] received desired reply", job.jobid) self._expected.remove(job.jobid) if len(self._expected): self._reply_cond.notify() - return reply + return jobout else: # this should not happen cmpi_logging.logger.error("[jobid=%d] received reply" " for another thread (jobid=%d)", - job.jobid, jobid) + job.jobid, jobout.jobid) except TQueue.Empty: cmpi_logging.logger.warn("[jobid=%d] wait for job reply timeout" "(%d seconds) occured", job.jobid, MAX_JOB_WAIT_TIME) @@ -231,6 +335,8 @@ class YumDB(singletonmixin.Singleton): """ with self._reply_lock: self._worker.uplink.put(job) + if getattr(job, 'async', False) is True: + return job.jobid self._expected.append(job.jobid) while True: if job.jobid not in self._expected: @@ -259,12 +365,10 @@ class YumDB(singletonmixin.Singleton): (exception_type, exception_value, formated_traceback_as_string) @return reply """ - cmpi_logging.logger.trace_verbose("YumDB: doing %s(id=%s) job", - job.__class__.__name__, job.jobid) + cmpi_logging.logger.trace_verbose("YumDB: doing %s", job) reply = self._send_and_receive(job) log_reply_error(job, reply) - cmpi_logging.logger.trace_verbose("YumDB: job %s(id=%s) done", - job.__class__.__name__, job.jobid) + cmpi_logging.logger.trace_verbose("YumDB: job %s done", job.jobid) return reply @property @@ -274,11 +378,11 @@ class YumDB(singletonmixin.Singleton): """ if self._process is None: cmpi_logging.logger.trace_info("YumDB: starting YumWorker") - uplink = JoinableQueue() + uplink = Queue() downlink = Queue() self._process = YumWorker(uplink, downlink, - yum_args=self._yum_args[0], yum_kwargs=self._yum_args[1]) - #logging_config=YUM_WORKER_DEBUG_LOGGING_CONFIG) + yum_kwargs=self._yum_kwargs, + logging_config=YUM_WORKER_DEBUG_LOGGING_CONFIG) self._process.start() cmpi_logging.logger.trace_info( "YumDB: YumWorker started with pid=%s", self._process.pid) @@ -327,7 +431,6 @@ class YumDB(singletonmixin.Singleton): if self._process is not None: cmpi_logging.logger.info('YumDB: terminating YumWorker') self._process.uplink.put(None) # terminating command - self._process.uplink.join() self._process.join() cmpi_logging.logger.info('YumDB: YumWorker terminated') self._process = None @@ -335,7 +438,10 @@ class YumDB(singletonmixin.Singleton): cmpi_logging.logger.warn("YunDB: clean_up called, when process" " not initialized!") - @cmpi_logging.trace_method + # ************************************************************************* + # Jobs with simple results + # ************************************************************************* + @job_request() def get_package_list(self, kind, allow_duplicates=False, sort=False, @@ -351,7 +457,7 @@ class YumDB(singletonmixin.Singleton): kind, allow_duplicates=allow_duplicates, sort=sort, include_repos=include_repos, exclude_repos=exclude_repos)) - @cmpi_logging.trace_method + @job_request() def filter_packages(self, kind, allow_duplicates=False, sort=False, @@ -367,75 +473,132 @@ class YumDB(singletonmixin.Singleton): include_repos=include_repos, exclude_repos=exclude_repos, **filters)) - @cmpi_logging.trace_method - def install_package(self, pkg): + @job_request() + def get_repository_list(self, kind): + """ + @param kind is one of: jobs.YumGetRepositoryList.SUPPORTED_KINDS + @param allow_duplicates says, whether to list all found versions + of single package + @return [pkg1, pkg2, ...], pkgi is instance of yumdb.Repository + """ + return self._do_job(jobs.YumGetRepositoryList(kind)) + + @job_request() + def filter_repositories(self, kind, **filters): + """ + Similar to get_repository_list(), but applies filter on packages. + @see yumdb.jobs.YumFilterRepositories job for supported filter keys + """ + return self._do_job(jobs.YumFilterRepositories(kind, **filters)) + + @job_request() + def set_repository_enabled(self, repoid, enable): + """ + Enable or disable repository. + @param enable is a boolean + """ + return self._do_job(jobs.YumSetRepositoryEnabled(repoid, enable)) + + # ************************************************************************* + # Asynchronous jobs + # ************************************************************************* + @job_request(async=True) + def install_package(self, pkg, async=False, force=False): """ Install package. @param pkg is an instance of PackageInfo obtained with - get_package_list() or filter_packages(), which must be not installed + get_package_list() or filter_packages() or a valid nevra as string. + Package must not be installed if force is False. """ - return self._do_job(jobs.YumInstallPackage(pkg)) + return self._do_job(_make_async_job(jobs.YumInstallPackage, + pkg, force=force, async=async)) - @cmpi_logging.trace_method - def remove_package(self, pkg): + @job_request(async=True) + def remove_package(self, pkg, async=False): """ @param pkg is an instance of PackageInfo obtained with get_package_list() or filter_packages(), which must be installed """ - return self._do_job(jobs.YumRemovePackage(pkg)) + return self._do_job(_make_async_job(jobs.YumRemovePackage, + pkg, async=async)) - @cmpi_logging.trace_method - def update_to_package(self, desired_pkg): + @job_request(async=True) + def update_to_package(self, desired_pkg, async=False): """ @param desired_pkg is an instance of PackageInfo, which must be available """ - return self._do_job(jobs.YumUpdateToPackage(desired_pkg)) + return self._do_job(_make_async_job(jobs.YumUpdateToPackage, + desired_pkg, async=async)) - @cmpi_logging.trace_method + @job_request(async=True) def update_package(self, pkg, + async=False, to_epoch=None, to_version=None, - to_release=None): + to_release=None, + force=False): """ @param pkg is an instance of PackageInfo, which must be installed The other parameters filter candidate available packages for update. """ - return self._do_job(jobs.YumUpdatePackage( - pkg, to_epoch, to_version, to_release)) + return self._do_job(_make_async_job(jobs.YumUpdatePackage, + pkg, async, to_epoch, to_version, to_release, force=force)) - @cmpi_logging.trace_method - def check_package(self, pkg): + @job_request(async=True) + def check_package(self, pkg, async=False): """ @param pkg is an instance of PackageInfo representing installed package @return instance of yumdb.PackageCheck """ - return self._do_job(jobs.YumCheckPackage(pkg)) + return self._do_job(_make_async_job(jobs.YumCheckPackage, + pkg, async=async)) - @cmpi_logging.trace_method - def get_repository_list(self, kind): + @job_request(async=True) + def install_package_from_uri(self, uri, + async=False, update_only=False, force=False): """ - @param kind is one of: jobs.YumGetRepositoryList.SUPPORTED_KINDS - @param allow_duplicates says, whether to list all found versions - of single package - @return [pkg1, pkg2, ...], pkgi is instance of yumdb.Repository + Install package from uri. + @param uri is either remote url or local path. """ - return self._do_job(jobs.YumGetRepositoryList(kind)) + return self._do_job(jobs.YumInstallPackageFromURI( + uri, async, update_only, force=force)) - @cmpi_logging.trace_method - def filter_repositories(self, kind, **filters): - """ - Similar to get_repository_list(), but applies filter on packages. - @see yumdb.jobs.YumFilterRepositories job for supported filter keys - """ - return self._do_job(jobs.YumFilterRepositories(kind, **filters)) - - @cmpi_logging.trace_method - def set_repository_enabled(self, repoid, enable): - """ - Enable or disable repository. - @param enable is a boolean - """ - return self._do_job(jobs.YumSetRepositoryEnabled(repoid, enable)) + # ************************************************************************* + # Control of asynchronous jobs + # ************************************************************************* + @job_request() + def get_job(self, jobid): + return self._do_job(jobs.YumJobGet(jobid)) + + @job_request() + def get_job_list(self): + return self._do_job(jobs.YumJobGetList()) + + @job_request() + def get_job_by_name(self, name): + return self._do_job(jobs.YumJobGetByName(name)) + + @job_request() + def set_job_priority(self, jobid, priority): + return self._do_job(jobs.YumJobSetPriority(jobid, priority)) + + @job_request() + def update_job(self, jobid, **kwargs): + return self._do_job(jobs.YumJobUpdate(jobid, **kwargs)) + + @job_request() + def reschedule_job(self, jobid, + delete_on_completion, time_before_removal): + return self._do_job(jobs.YumJobReschedule(jobid, + delete_on_completion, time_before_removal)) + + @job_request() + def delete_job(self, jobid): + return self._do_job(jobs.YumJobDelete(jobid)) + + @job_request() + def terminate_job(self, jobid): + return self._do_job(jobs.YumJobTerminate(jobid)) diff --git a/src/software/openlmi/software/yumdb/errors.py b/src/software/openlmi/software/yumdb/errors.py index 35dad14..2d9f0e5 100644 --- a/src/software/openlmi/software/yumdb/errors.py +++ b/src/software/openlmi/software/yumdb/errors.py @@ -23,29 +23,69 @@ Exceptions raisable by YumWorker. """ -class DatabaseLockError(Exception): +class YumDBError(Exception): + """Base class for all errors under yumdb package.""" + +class DatabaseLockError(YumDBError): """Raised, when the yum database can not be locked.""" pass -class TransactionError(Exception): + +class TransactionError(YumDBError): """Base exception representing yum transaction processing error.""" pass class TransactionBuildFailed(TransactionError): """Raised, when transaction building fails.""" pass +class PackageAlreadyInstalled(TransactionError): + """Raised, when trying to install already installed package.""" + def __init__(self, pkg): + TransactionError.__init__(self, + 'Package "%s" is already installed.' % pkg) +class PackageOpenError(TransactionError): + """Raised, when trying to open package obtained from URI.""" + def __init__(self, pkg, msg): + TransactionError.__init__(self, + 'Failed to open package "%s": %s' % (pkg, msg)) class TransactionExecutionFailed(TransactionError): """Raised, when YumBase.doTransaction() method fails.""" pass -class UnknownJob(Exception): - """Raised, when no handler is available for given job on worker.""" + +class PackageError(YumDBError): pass -class PackageNotFound(Exception): +class PackageNotFound(PackageError): """Raised, when requested package could not be found.""" pass -class RepositoryNotFound(Exception): +class PackageNotInstalled(PackageError): + """Raised, when requested package is not installed for desired action.""" + def __init__(self, pkg): + PackageError.__init__(self, 'Package "%s" is not installed.' % pkg) + +class RepositoryError(YumDBError): + pass +class RepositoryNotFound(RepositoryError): """Raised, when requested repository cound not be found.""" def __init__(self, repoid): - Exception.__init__(self, "No such repository: %s" % repoid) -class RepositoryChangeError(Exception): + RepositoryError.__init__(self, "No such repository: %s" % repoid) +class RepositoryChangeError(RepositoryError): """Raised, when modification of repository failed.""" pass +class JobError(YumDBError): + pass +class UnknownJob(JobError): + """Raised, when no handler is available for given job on worker.""" + pass +class InvalidURI(JobError): + """Raised, when passed uri is not a valid one.""" + def __init__(self, uri): + JobError.__init__(self, "Invalid uri: \"%s\"" % uri) +class InvalidNevra(JobError): + pass +class JobControlError(JobError): + pass +class JobNotFound(JobControlError): + def __init__(self, target): + JobControlError.__init__(self, "job %s could not be found" % target) +class InvalidJobState(JobControlError): + pass + diff --git a/src/software/openlmi/software/yumdb/jobmanager.py b/src/software/openlmi/software/yumdb/jobmanager.py new file mode 100644 index 0000000..515bb44 --- /dev/null +++ b/src/software/openlmi/software/yumdb/jobmanager.py @@ -0,0 +1,413 @@ +# Software Management Providers +# +# Copyright (C) 2012 Red Hat, Inc. All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Authors: Michal Minar <miminar@redhat.com> +# +""" +This is a module for JobManager which is a separate thread of +YumWorker process. It keeps a cache of asynchronous jobs and handles +input and output queues. +""" +import heapq +import inspect +import logging +import Queue +import sys +import threading +import time +import traceback + +from openlmi.software.yumdb import errors, jobs +from openlmi.software.yumdb.util import trace_function + +# Minimum time to keep asynchronous job in cache after completion. In seconds. +MINIMUM_TIME_BEFORE_REMOVAL = 10 + +LOG = None + +# ***************************************************************************** +# Decorators +# ***************************************************************************** +def job_handler(job_from_target=True): + """ + Decorator for JobManager methods serving as handlers for control jobs. + + Decorator locks the job_lock of manager's instance. + """ + def _wrapper_jft(method): + """ + It consumes "target" keyword argument (which is job's id) and makes + it an instance of YumJob. The method is then called with "job" argument + instead of "target". + """ + logged = trace_function(method) + + def _new_func(self, *args, **kwargs): + """Wrapper around method.""" + if 'target' in kwargs: + kwargs['job'] = kwargs.pop('target') + callargs = inspect.getcallargs(method, self, *args, **kwargs) + target = callargs.pop('job') + with self._job_lock: #pylint: disable=W0212 + if not target in self._async_jobs: #pylint: disable=W0212 + raise errors.JobNotFound(target) + job = self._async_jobs[target] #pylint: disable=W0212 + callargs['job'] = job + return logged(**callargs) + return _new_func + + def _simple_wrapper(method): + """Just locks the job lock.""" + def _new_func(self, *args, **kwargs): + """Wrapper around method.""" + with self._job_lock: #pylint: disable=W0212 + return method(self, *args, **kwargs) + return _new_func + + if job_from_target: + return _wrapper_jft + else: + return _simple_wrapper + +class JobManager(threading.Thread): + """ + Separate thread for managing queue of jobs requested by client. + There are three kinds of jobs, that are handled differently: + * asynchronous - kept in _async_jobs dictionary until job is + deleted by request or it expires; + no reply is sent to client upon job's completion + * synchronous - reply is sent to client after job's completion; + no reference to the job is kept afterwards + * job control - they are not enqueued in _job_queue for YumWorker + to process, but are handled directly and in the FIFO order + + Both asynchronous and synchronous jobs are enqueued in _job_queue + for YumWorker to obtain them. It's a priority queue sorting jobs by their + priority. + """ + # enumeration of actions, that may be enqueued in calendar + ACTION_REMOVE = 0 + + ACTION_NAMES = ['remove'] + + def __init__(self, queue_in, queue_out): + threading.Thread.__init__(self, name="JobManager") + self._queue_in = queue_in + self._queue_out = queue_out + self._terminate = False + + # (time, jobid, action) + self._calendar = [] + # {jobid : job} + self._async_jobs = {} + + # lock for critical access to _calendar, _async_jobs and _job_queue + self._job_lock = threading.RLock() + # priority queue of jobs that are processed by YumWorker + self._job_queue = [] + # condition for YumWorker waiting on empty _job_queue + self._job_enqueued = threading.Condition(self._job_lock) + + # ************************************************************************* + # Private methods + # ************************************************************************* + @trace_function + def _control_job(self, job): + """ + Function dispatching job to handler for particular YumJob subclass. + """ + try: + handler = { + # these are from YumDB client + jobs.YumJobGetList : self._handle_get_list, + jobs.YumJobGet : self._handle_get, + jobs.YumJobGetByName : self._handle_get_by_name, + jobs.YumJobSetPriority : self._handle_set_priority, + jobs.YumJobReschedule : self._handle_reschedule, + jobs.YumJobUpdate : self._handle_update, + jobs.YumJobDelete : self._handle_delete, + jobs.YumJobTerminate : self._handle_terminate, + }[job.__class__] + LOG.info("processing control job %s", str(job)) + except KeyError: + raise errors.UnknownJob("No handler for job \"%s\"." % + job.__class__.__name__) + return handler(**job.job_kwargs) + + @trace_function + def _enqueue_job(self, job): + """ + Insert incoming job into _job_queue. + """ + if isinstance(job, jobs.YumJobControl): + result = job.RESULT_SUCCESS + job.start() + try: + data = self._control_job(job) + except Exception: #pylint: disable=W0703 + result = job.RESULT_ERROR + data = sys.exc_info() + data = (data[0], data[1], traceback.format_tb(data[2])) + LOG.exception("control job %s failed", job) + job.finish(result, data) + LOG.debug("sending reply for %s: (%s, %s)", job, + job.ResultNames[job.result], job.result_data) + self._queue_out.put(job) + else: + if job is None: + LOG.debug('received terminating command') + self._terminate = True + LOG.debug('job %s enqued for YumWorker to handle', job) + heapq.heappush(self._job_queue, job) + if getattr(job, 'async', False) is True: + self._async_jobs[job.jobid] = job + self._job_enqueued.notify() + + @trace_function + def _schedule_event(self, after, jobid, action): + """ + Enqueue event into calendar. Event consists of time, jobid and + action. + """ + schedule_at = time.time() + after + for (sched, jid, act) in self._calendar: + if jid == jobid and act == action: + if sched <= schedule_at: # same event already scheduled + return + # schedule it for early time + LOG.debug('rescheduling action %s on job %d to take place' + ' after %d seconds (instead of %d)', + self.ACTION_NAMES[action], jid, after, + sched - schedule_at + after) + self._calendar.remove((sched, jid, act)) + self._calendar.append((schedule_at, jid, act)) + heapq.heapify(self._calendar) + return + LOG.debug('scheduling action %s on job %d to take place after ' + ' %d seconds', self.ACTION_NAMES[action], jobid, after) + heapq.heappush(self._calendar, (schedule_at, jobid, action)) + + @trace_function + def _run_event(self, jobid, action): + """ + Process event from calendar. + """ + if action == self.ACTION_REMOVE: + with self._job_lock: + del self._async_jobs[jobid] + else: + msg = "unsupported action: %s" % action + raise ValueError(msg) + + # ************************************************************************* + # Job handlers + # ************************************************************************* + @job_handler() + def _handle_get(self, job): #pylint: disable=R0201 + """@return job object""" + return job + + @job_handler(False) + def _handle_get_list(self): + """@return list of all asynchronous jobs""" + with self._job_lock: + return sorted(self._async_jobs.values()) + + @job_handler(False) + def _handle_get_by_name(self, target): + """@return job object filtered by name""" + for job in self._async_jobs.values(): + if 'name' in job.metadata and target == job.metadata['name']: + return job + raise errors.JobNotFound(target) + + @job_handler() + def _handle_set_priority(self, job, new_priority): + """ + Modify job's priority and updates its position in queue. + @return modified job object + """ + if not isinstance(new_priority, (int, long)): + raise TypeError('priority must be an integer') + if job.priority != new_priority: + job.update(priority=new_priority) + if job in self._job_queue: + heapq.heapify(self._job_queue) + return job + + @job_handler() + def _handle_reschedule(self, job, + delete_on_completion, + time_before_removal): + """ + Changes job's schedule for its deletion. + """ + if ( job.delete_on_completion == delete_on_completion + and job.time_before_removal == time_before_removal): + return + if job.finished and job.delete_on_completion: + for i, event in enumerate(self._calendar): + if event[1] == job.jobid and event[2] == self.ACTION_REMOVE: + del self._calendar[i] + heapq.heapify(self._calendar) + break + if delete_on_completion: + schedule_at = time_before_removal + if job.finished: + schedule_at = job.finished + schedule_at - time.time() + self._schedule_event(schedule_at, job.jobid, self.ACTION_REMOVE) + job.delete_on_completion = delete_on_completion + job.time_before_removal = time_before_removal + return job + + @job_handler() + def _handle_update(self, job, data): #pylint: disable=R0201 + """ + Updates any job metadata. + """ + job.update(**data) + return job + + @job_handler() + def _handle_delete(self, job): + """ + Deletes finished asynchronous job. + """ + if not job.finished: + raise errors.InvalidJobState( + 'can not delete unfinished job "%s"' % job) + try: + self._job_queue.remove(job) + heapq.heapify(self._job_queue) + LOG.debug('job "%s" removed from queue', job) + except ValueError: + LOG.debug('job "%s" not started and not enqueued', job) + del self._async_jobs[job.jobid] + return job + + @job_handler() + def _handle_terminate(self, job): + """ + Terminates not started job. + """ + if job.started and not job.finished: + raise errors.InvalidJobState('can not kill running job "%s"' % job) + if job.finished: + raise errors.InvalidJobState('job "%s" already finished' % job) + self._job_queue.remove(job) + heapq.heapify(self._job_queue) + job.finish(result=job.RESULT_TERMINATED) + LOG.info('terminated not started job "%s"', job) + return job + + # ************************************************************************* + # Public properties + # ************************************************************************* + @property + def queue_in(self): + """Incoming queue for YumJob instances.""" + return self._queue_in + + @property + def queue_out(self): + """Output queue for results.""" + return self._queue_out + + # ************************************************************************* + # Public methods + # ************************************************************************* + @trace_function + def finish_job(self, job, result, result_data): + """ + This should be called for any job by YumWorker after the job is + processed. + + If the job is synchronous, reply is send at once. Otherwise the result + is stored for later client's query in the job itself. + """ + with self._job_lock: + if job.state != job.RUNNING: + raise errors.InvalidJobState( + 'can not finish not started job "%s"' % job) + job.finish(result, result_data) + if getattr(job, 'async', False): + if job.delete_on_completion: + schedule_at = max( job.time_before_removal + , MINIMUM_TIME_BEFORE_REMOVAL) + self._schedule_event(schedule_at, job.jobid, + self.ACTION_REMOVE) + else: + LOG.debug("sending reply for %s: (%s, %s)", job, + job.ResultNames[job.result], job.result_data) + self._queue_out.put(job) + return job + + @trace_function + def get_job(self, block=True, timeout=None): + """ + Method supposed to be used only by YumWorker. It pops the first job + from _job_queue, starts it and returns it. + """ + start = time.time() + with self._job_lock: + if len(self._job_queue) == 0 and not block: + raise Queue.Empty + while len(self._job_queue) == 0: + if timeout: + LOG.debug('waiting for job for %s seconds' % timeout) + self._job_enqueued.wait(timeout) + if len(self._job_queue) == 0: + now = time.time() + if timeout > now - start: + raise Queue.Empty + job = heapq.heappop(self._job_queue) + if job is not None: + job.start() + return job + + def run(self): + """The entry point of thread.""" + global LOG #pylint: disable=W0603 + LOG = logging.getLogger(__name__) + LOG.info("%s thread started", self.name) + + while self._terminate is False: + try: + timeout = None + with self._job_lock: + if len(self._calendar) > 0: + timeout = self._calendar[0][0] - time.time() + LOG.debug('waiting on input queue for job%s', + (' with timeout %s' % timeout) if timeout else '') + job = self._queue_in.get(timeout=timeout) + with self._job_lock: + self._enqueue_job(job) + while not self._queue_in.empty(): + # this won't throw + self._enqueue_job(self._queue_in.get_nowait()) + + except Queue.Empty: + with self._job_lock: + while ( len(self._calendar) + and self._calendar[0][0] < time.time()): + _, jobid, action = heapq.heappop(self._calendar) + LOG.info('running action %s on job(id=%d)', + self.ACTION_NAMES[action], jobid) + self._run_event(jobid, action) + LOG.info('%s thread terminating', self.name) + diff --git a/src/software/openlmi/software/yumdb/jobs.py b/src/software/openlmi/software/yumdb/jobs.py index 437f123..cb36f43 100644 --- a/src/software/openlmi/software/yumdb/jobs.py +++ b/src/software/openlmi/software/yumdb/jobs.py @@ -23,25 +23,45 @@ Define job classes representing kinds of jobs of worker process. """ +import os import threading +import time +import yum from openlmi.software import util +from openlmi.software.yumdb import errors from openlmi.software.yumdb.packageinfo import PackageInfo from openlmi.software.yumdb.repository import Repository +DEFAULT_JOB_PRIORITY = 10 +# in seconds +DEFAULT_TIME_BEFORE_REMOVAL = 60 * 5 + class YumJob(object): #pylint: disable=R0903 """ Base class for any job, that is processable by YumWorker process. - It contains at least a jobid attribute, that must be unique for + It contains jobid attribute, that must be unique for each job, it's counted from zero a incremented after each creation. + + metadata attribute typically contain: + name - name of job, that is modifiable by user + method_name - name of provider's method, that lead to creation of job """ - __slots__ = ('jobid', ) + __slots__ = ( 'jobid', 'created', 'started', 'finished', 'last_change' + , 'priority', 'result', 'result_data') # jobs can be created concurrently from multiple threads, that's # why we need to make its creation thread safe _JOB_ID_LOCK = threading.Lock() _JOB_ID = 0 + # job state enumeration + NEW, RUNNING, COMPLETED, TERMINATED, EXCEPTION = range(5) + # job result enumeration + RESULT_SUCCESS, RESULT_TERMINATED, RESULT_ERROR = range(3) + + ResultNames = ("success", "terminated", "error") + @staticmethod def _get_job_id(): """ @@ -54,35 +74,261 @@ class YumJob(object): #pylint: disable=R0903 YumJob._JOB_ID += 1 return val - def __init__(self): + @classmethod + def handle_ignore_job_props(cls): + """ + @return set of job properties, that does not count as job's handler + arguments - job handler does not care fore metadata, jobid, priority, + etc... + """ + return set(YumJob.__slots__) + + def __init__(self, priority=10): + if not isinstance(priority, (int, long)): + raise TypeError("priority must be integer") self.jobid = self._get_job_id() + self.started = None + self.finished = None + self.priority = priority + self.created = time.time() + self.last_change = self.created + self.result = None + self.result_data = None + + @property + def state(self): + """ + @return integer representing job's state + """ + if not self.started: + return self.NEW + if not self.finished: + return self.RUNNING + if self.result == self.RESULT_ERROR: + return self.EXCEPTION + if self.result == self.RESULT_TERMINATED: + return self.TERMINATED + return self.COMPLETED @property def job_kwargs(self): """ - Jobs are in worker represented be methods with arguments. - Those can be obtained from job by calling this property. + Jobs are in worker handled in handlers specific for each subclass. + These handlers are methods of worker. They accepts concrete arguments + that can be obtained from job by invoking this property. @return dictionary of keyword arguments of job """ kwargs = {} cls = self.__class__ while not cls in (YumJob, object): for slot in cls.__slots__: - if not slot in kwargs: + if ( not slot in kwargs + and not slot in cls.handle_ignore_job_props()): kwargs[slot] = getattr(self, slot) cls = cls.__bases__[0] - kwargs.pop('jobid', None) + for prop in YumJob.__slots__: + kwargs.pop(prop, None) return kwargs + def start(self): + """Modify the state of job to RUNNING.""" + if self.started: + raise errors.InvalidJobState("can not start already started job") + self.started = time.time() + self.last_change = self.started + + def finish(self, result, data=None): + """ + Modify the state of job to one of {COMPLETED, EXCEPTION, TERMINATED}. + Depending on result parameter. + """ + if not self.started and result != self.RESULT_TERMINATED: + raise errors.InvalidJobState("can not finish not started job") + self.finished = time.time() + if result == self.RESULT_TERMINATED: + self.started = self.finished + self.result = result + self.result_data = data + self.last_change = self.finished + + def update(self, **kwargs): + """Change job's properties.""" + change = False + for key, value in kwargs.items(): + if getattr(self, key) != value: + setattr(self, key, value) + change = True + if change is True: + self.last_change = time.time() + + def __eq__(self, other): + return self.__class__ is other.__class__ and self.jobid == other.jobid + + def __ne__(self, other): + return ( self.__class__ is not other.__class__ + or self.jobid != other.jobid) + + def __lt__(self, other): + """ + JobControl jobs have the highest priority. + """ + return ( ( isinstance(self, YumJobControl) + and not isinstance(other, YumJobControl)) + or ( self.priority < other.priority + or ( self.priority == other.priority + and ( self.jobid < other.jobid + or ( self.jobid == other.jobid + and (self.created < other.created)))))) + + def __cmp__(self, other): + if ( isinstance(self, YumJobControl) + and not isinstance(other, YumJobControl)): + return -1 + if ( not isinstance(self, YumJobControl) + and isinstance(other, YumJobControl)): + return 1 + if self.priority < other.priority: + return -1 + if self.priority > other.priority: + return 1 + if self.jobid < other.jobid: + return -1 + if self.jobid > other.jobid: + return 1 + if self.created < other.created: + return -1 + if self.created > other.created: + return 1 + return 0 + + def __str__(self): + return "%s(id=%d,p=%d)" % ( + self.__class__.__name__, self.jobid, self.priority) + def __getstate__(self): ret = self.job_kwargs - ret.update(jobid=self.jobid) + for prop in self.handle_ignore_job_props(): + ret[prop] = getattr(self, prop) return ret def __setstate__(self, state): for k, value in state.items(): setattr(self, k, value) +class YumAsyncJob(YumJob): #pylint: disable=R0903 + """ + Base class for jobs, that support asynchronnous execution. + No reply is sent upon job completition or error. The results are + kept on server. + """ + __slots__ = ( 'async' + , 'delete_on_completion' + , 'time_before_removal' + , 'metadata') + + @classmethod + def handle_ignore_job_props(cls): + return YumJob.handle_ignore_job_props().union(YumAsyncJob.__slots__) + + def __init__(self, priority=10, async=False, metadata=None): + YumJob.__init__(self, priority) + self.async = bool(async) + self.delete_on_completion = True + self.time_before_removal = DEFAULT_TIME_BEFORE_REMOVAL + if metadata is None and self.async is True: + metadata = {} + self.metadata = metadata + + def __str__(self): + return "%s(id=%d,p=%d%s%s)" % ( + self.__class__.__name__, self.jobid, + self.priority, + ',async' if self.async else '', + (',name="%s"'%self.metadata['name']) + if self.metadata and 'name' in self.metadata else '') + + def update(self, **kwargs): + if 'metadata' in kwargs: + self.metadata.update(kwargs.pop('metadata')) + return YumJob.update(self, **kwargs) + +# ***************************************************************************** +# Job control funtions +# ***************************************************************************** +class YumJobControl(YumJob): #pylint: disable=R0903 + """Base class for any job used for asynchronous jobs management.""" + pass + +class YumJobGetList(YumJobControl): #pylint: disable=R0903 + """Request for obtaining list of all asynchronous jobs.""" + pass + +class YumJobOnJob(YumJobControl): + """ + Base class for any control job acting upon particular asynchronous job. + """ + __slots__ = ('target', ) + def __init__(self, target): + YumJobControl.__init__(self) + if not isinstance(target, (int, long)): + raise TypeError("target must be an integer") + self.target = target + +class YumJobGet(YumJobOnJob): #pylint: disable=R0903 + """Get job object by its id.""" + pass + +class YumJobGetByName(YumJobOnJob): #pylint: disable=R0903 + """Get job object by its name property.""" + def __init__(self, name): + YumJobOnJob.__init__(self, -1) + self.target = name + +class YumJobSetPriority(YumJobOnJob): #pylint: disable=R0903 + """Change priority of job.""" + __slots__ = ('new_priority', ) + + def __init__(self, target, priority): + YumJobOnJob.__init__(self, target) + self.new_priority = priority + +class YumJobUpdate(YumJobOnJob): #pylint: disable=R0903 + """Update job's metadata.""" + __slots__ = ('data', ) + FORBIDDEN_PROPERTIES = ( + 'async', 'jobid', 'created', 'started', 'priority', 'finished', + 'delete_on_completion', 'time_before_removal', 'last_change') + + def __init__(self, target, **kwargs): + YumJobOnJob.__init__(self, target) + assert not set.intersection( + set(YumJobUpdate.FORBIDDEN_PROPERTIES), set(kwargs)) + self.data = kwargs + +class YumJobReschedule(YumJobOnJob): #pylint: disable=R0903 + """Change the schedule of job's deletion.""" + __slots__ = ('delete_on_completion', 'time_before_removal') + def __init__(self, target, delete_on_completion, time_before_removal): + YumJobOnJob.__init__(self, target) + if not isinstance(time_before_removal, (int, long, float)): + raise TypeError("time_before_removal must be float") + self.delete_on_completion = bool(delete_on_completion) + self.time_before_removal = time_before_removal + +class YumJobDelete(YumJobOnJob): #pylint: disable=R0903 + """Delete job - can only be called on finished job.""" + pass + +class YumJobTerminate(YumJobOnJob): #pylint: disable=R0903 + """ + Can only be called on not yet started job. + Running job can not be terminated. + """ + pass + +# ***************************************************************************** +# Yum API functions +# ***************************************************************************** class YumBeginSession(YumJob): #pylint: disable=R0903 """ Begin session on YumWorker which ensures that yum database is locked @@ -195,27 +441,42 @@ class YumFilterPackages(YumGetPackageList): #pylint: disable=R0903 self.envra = envra self.repoid = repoid -class YumSpecificPackageJob(YumJob): #pylint: disable=R0903 +class YumSpecificPackageJob(YumAsyncJob): #pylint: disable=R0903 """ - Abstract job taking instance of yumdb.PackageInfo as argument. + Abstract job taking instance of yumdb.PackageInfo as argument or + package's nevra. Arguments: - pkg - plays different role depending on job subclass + pkg - plays different role depending on job subclass; + can also be a nevra """ __slots__ = ('pkg', ) - def __init__(self, pkg): - if not isinstance(pkg, PackageInfo): - raise TypeError("pkg must be instance of PackageInfo") - YumJob.__init__(self) + def __init__(self, pkg, async=False, metadata=None): + if isinstance(pkg, basestring): + if not util.RE_NEVRA_OPT_EPOCH.match(pkg): + raise errors.InvalidNevra('not a valid nevra "%s"' % pkg) + elif not isinstance(pkg, PackageInfo): + raise TypeError("pkg must be either string or instance" + " of PackageInfo") + YumAsyncJob.__init__(self, async=async, metadata=metadata) self.pkg = pkg class YumInstallPackage(YumSpecificPackageJob): #pylint: disable=R0903 """ Job requesting installation of specific package. pkg argument should be available. + Arguments: + pkg - same as in YumSpecificPackageJob + force is a boolean saying: + True -> reinstall the package if it's already installed + False -> fail if the package is already installed Worker replies with new instance of package. """ - pass + __slots__ = ('force', ) + def __init__(self, pkg, async=False, force=False, metadata=None): + YumSpecificPackageJob.__init__( + self, pkg, async=async, metadata=metadata) + self.force = bool(force) class YumRemovePackage(YumSpecificPackageJob): #pylint: disable=R0903 """ @@ -240,21 +501,27 @@ class YumUpdatePackage(YumSpecificPackageJob): #pylint: disable=R0903 candidate packages to ones with specific evr. Arguments: to_epoch, to_version, to_release + force is a boolean, that has meaning only when update_only is False: + True -> reinstall the package if it's already installed + False -> fail if the package is already installed The arguments more given, the more complete filter of candidates. Worker replies with new instance of package. """ - __slots__ = ('to_epoch', 'to_version', 'to_release') + __slots__ = ('to_epoch', 'to_version', 'to_release', 'force') - def __init__(self, pkg, - to_epoch=None, to_version=None, to_release=None): + def __init__(self, pkg, async=False, + to_epoch=None, to_version=None, to_release=None, force=False, + metadata=None): if not isinstance(pkg, PackageInfo): raise TypeError("pkg must be instance of yumdb.PackageInfo") - YumSpecificPackageJob.__init__(self, pkg) + YumSpecificPackageJob.__init__( + self, pkg, async=async, metadata=metadata) self.to_epoch = to_epoch self.to_version = to_version self.to_release = to_release + self.force = bool(force) class YumCheckPackage(YumSpecificPackageJob): #pylint: disable=R0903 """ @@ -262,11 +529,41 @@ class YumCheckPackage(YumSpecificPackageJob): #pylint: disable=R0903 Worker replies with new instance of yumdb.PackageCheck. """ - def __init__(self, pkg): - YumSpecificPackageJob.__init__(self, pkg) + def __init__(self, pkg, async=False, metadata=None): + YumSpecificPackageJob.__init__(self, pkg, async=async, + metadata=metadata) if not pkg.installed: raise ValueError("package must be installed to check it") +class YumInstallPackageFromURI(YumAsyncJob): #pylint: disable=R0903 + """ + Job requesting installation of specific package from URI. + Arguments: + uri is either a path to rpm package on local filesystem or url + of rpm stored on remote host + update_only is a boolean: + True -> install the package only if the older version is installed + False -> install the package if it's not already installed + force is a boolean, that has meaning only when update_only is False: + True -> reinstall the package if it's already installed + False -> fail if the package is already installed + + Worker replies with new instance of package. + """ + __slots__ = ('uri', 'update_only', "force") + def __init__(self, uri, async=False, update_only=False, force=False, + metadata=None): + if not isinstance(uri, basestring): + raise TypeError("uri must be a string") + if uri.startswith('file://'): + uri = uri[len('file://'):] + if not yum.misc.re_remote_url(uri) and not os.path.exists(uri): + raise errors.InvalidURI(uri) + YumAsyncJob.__init__(self, async=async, metadata=metadata) + self.uri = uri + self.update_only = bool(update_only) + self.force = bool(force) + class YumGetRepositoryList(YumJob): #pylint: disable=R0903 """ Job requesing a list of repositories. @@ -288,7 +585,7 @@ class YumGetRepositoryList(YumJob): #pylint: disable=R0903 ", ".join(self.SUPPORTED_KINDS)) self.kind = kind -class YumFilterRepositories(YumGetRepositoryList): +class YumFilterRepositories(YumGetRepositoryList): #pylint: disable=R0903 """ Job similar to YumGetRepositoryList, but allowing to specify filter on packages. diff --git a/src/software/openlmi/software/yumdb/process.py b/src/software/openlmi/software/yumdb/process.py index 96c471e..e77b360 100644 --- a/src/software/openlmi/software/yumdb/process.py +++ b/src/software/openlmi/software/yumdb/process.py @@ -24,7 +24,6 @@ Module holding the code of separate process accessing the YUM API. """ import errno -import inspect from itertools import chain import logging from multiprocessing import Process @@ -33,6 +32,7 @@ import Queue as TQueue # T as threaded import sys import time import traceback +from urlgrabber.grabber import default_grabber import weakref import yum @@ -42,6 +42,8 @@ from openlmi.software.yumdb import jobs from openlmi.software.yumdb import packageinfo from openlmi.software.yumdb import packagecheck from openlmi.software.yumdb import repository +from openlmi.software.yumdb.jobmanager import JobManager +from openlmi.software.yumdb.util import trace_function, setup_logging # ***************************************************************************** # Constants @@ -50,19 +52,11 @@ from openlmi.software.yumdb import repository FREE_DATABASE_TIMEOUT = 60 LOCK_WAIT_INTERVAL = 0.5 RPMDB_PATH = '/var/lib/rpm/Packages' +LOG = None # ***************************************************************************** # Utilities # **************************************************************************** -def _logger(): - """ - Returns logger for this module, when first needed. - @return logger specific for this process - """ - if not hasattr(_logger, "logger"): - _logger.logger = logging.getLogger(__name__) - return _logger.logger - def _get_package_filter_function(filters): """ @param filters is a dictionary, where keys are package property @@ -130,14 +124,14 @@ class RepoFilterSetter(object): repos = set() if exclude: repos.update(self._yum_base.repos.disableRepo(exclude)) - _logger().info('disabling repositories: [%s]', ", ".join(repos)) + LOG.info('disabling repositories: [%s]', ", ".join(repos)) if isinstance(self._include, (list, tuple, set)): include = ",".join(self._include) else: include = self._include if include: affected = self._yum_base.repos.enableRepo(include) - _logger().info('enabling repositories: [%s]', ", ".join(affected)) + LOG.info('enabling repositories: [%s]', ", ".join(affected)) repos.update(affected) for repoid, prev_enabled in self._prev_states.items(): if ( repoid not in repos @@ -154,7 +148,7 @@ class RepoFilterSetter(object): def __exit__(self, exc_type, exc_value, exc_tb): # restore previous repository states if len(self._prev_states): - _logger().info('restoring repositories: [%s]', + LOG.info('restoring repositories: [%s]', ", ".join(self._prev_states.keys())) for repoid, enabled in self._prev_states.items(): repo = self._yum_base.repos.getRepo(repoid) @@ -169,51 +163,13 @@ class RepoFilterSetter(object): # ***************************************************************************** # Decorators # ***************************************************************************** -def _trace_function(func): - """ - Decorator for logging entries and exits of function or method. - """ - if not inspect.ismethod(func) and not inspect.isfunction(func): - raise TypeError("func must be a function") - - def _print_value(val): - """ - Used here for printing function arguments. Shortens the output - string, if that would be too long. - """ - if isinstance(val, list): - if len(val) < 2: - return str(val) - else: - return "[%s, ...]" % _print_value(val[0]) - return str(val) - - def _wrapper(self, *args, **kwargs): - """ - Wrapper for function or method, that does the logging. - """ - ftype = "method" if inspect.ismethod(func) else "function" - parent = ( func.im_class.__name__ + "." - if inspect.ismethod(func) else "") - - _logger().debug("entering %s %s%s with args=(%s)", - ftype, parent, func.__name__, - ", ".join(chain( - (_print_value(a) for a in args), - ( "%s=%s"%(k, _print_value(v)) - for k, v in kwargs.items())))) - result = func(self, *args, **kwargs) - _logger().debug("exiting %s %s%s", ftype, parent, func.__name__) - return result - - return _wrapper - def _needs_database(method): """ Decorator for YumWorker job handlers, that need to access the yum database. It ensures, that database is initialized and locks it in case, that no session is active. """ + logged = trace_function(method) def _wrapper(self, *args, **kwargs): """ Wrapper for the job handler method. @@ -222,18 +178,14 @@ def _needs_database(method): if self._session_level == 0: #pylint: disable=W0212 self._lock_database() #pylint: disable=W0212 try: - _logger().debug("calling job handler %s with args=(%s)", + LOG.debug("calling job handler %s with args=(%s)", method.__name__, ", ".join(chain( (str(a) for a in args), ("%s=%s"%(k, str(v)) for k, v in kwargs.items())))) - result = method(self, *args, **kwargs) - _logger().debug("job handler %s finished", method.__name__) + result = logged(self, *args, **kwargs) + LOG.debug("job handler %s finished", method.__name__) return result - except: - _logger().debug("job handler %s terminated with exception: %s", - method.__name__, traceback.format_exc()) - raise finally: if self._session_level == 0: #pylint: disable=W0212 self._unlock_database() #pylint: disable=W0212 @@ -248,26 +200,25 @@ class YumWorker(Process): for input jobs and second for results. Jobs are dispatched by their class names to particular handler method. + + It spawns a second thread for managing asynchronous jobs and queue + of incoming jobs. It's an instance of JobManager. """ def __init__(self, queue_in, queue_out, - yum_args=None, yum_kwargs=None, logging_config=None): Process.__init__(self, name="YumWorker") - self._queue_in = queue_in - self._queue_out = queue_out + self._jobmgr = JobManager(queue_in, queue_out) self._session_level = 0 self._session_ended = False - if yum_args is None: - yum_args = tuple() if yum_kwargs is None: yum_kwargs = {} - self._yum_args = (yum_args, yum_kwargs) + self._yum_kwargs = yum_kwargs self._yum_base = None self._pkg_cache = None @@ -279,31 +230,28 @@ class YumWorker(Process): # ************************************************************************* # Private methods # ************************************************************************* - @_trace_function + @trace_function def _init_database(self): """ Initializes yum base object, when it does no exists. And updates the cache (when out of date). """ if self._yum_base is None: - _logger().info("creating YumBase with args=(%s)", - ", ".join(chain( - (str(a) for a in self._yum_args[0]), - ( "%s=%s"%(k, str(v)) - for k, v in self._yum_args[1].items())))) - self._yum_base = yum.YumBase( - *self._yum_args[0], **self._yum_args[1]) - - @_trace_function + LOG.info("creating YumBase with kwargs=(%s)", + ", ".join(( "%s=%s"%(k, str(v)) + for k, v in self._yum_kwargs.items()))) + self._yum_base = yum.YumBase(**self._yum_kwargs) + + @trace_function def _free_database(self): """ Release the yum base object to safe memory. """ - _logger().info("freing database") + LOG.info("freing database") self._pkg_cache.clear() self._yum_base = None - @_trace_function + @trace_function def _lock_database(self): """ Only one process is allowed to work with package database at given time. @@ -313,31 +261,47 @@ class YumWorker(Process): """ while True: try: - _logger().info("trying to lock database - session level %d", + LOG.info("trying to lock database - session level %d", self._session_level) self._yum_base.doLock() - _logger().info("successfully locked up") + LOG.info("successfully locked up") break except yum.Errors.LockError as exc: - _logger().warn("failed to lock") + LOG.warn("failed to lock") if exc.errno in (errno.EPERM, errno.EACCES, errno.ENOSPC): - _logger().error("can't create lock file") + LOG.error("can't create lock file") raise errors.DatabaseLockError("Can't create lock file.") - _logger().info("trying to lock again after %.1f seconds", + LOG.info("trying to lock again after %.1f seconds", LOCK_WAIT_INTERVAL) time.sleep(LOCK_WAIT_INTERVAL) - @_trace_function + @trace_function def _unlock_database(self): """ The opposite to _lock_database() method. """ if self._yum_base is not None: - _logger().info("unlocking database") + LOG.info("unlocking database") self._yum_base.closeRpmDB() self._yum_base.doUnlock() - @_trace_function + @trace_function + def _get_job(self): + """ + Get job from JobManager thread. + If no job comes for long time, free database to save memory. + """ + while True: + if self._session_ended and self._session_level == 0: + try: + return self._jobmgr.get_job(timeout=FREE_DATABASE_TIMEOUT) + except TQueue.Empty: + self._free_database() + self._session_ended = False + else: + return self._jobmgr.get_job() + + @trace_function def _transform_packages(self, packages, cache_packages=True, flush_cache=True): @@ -350,7 +314,7 @@ class YumWorker(Process): packages; makes sense only with cachee_packages=True """ if cache_packages is True and flush_cache is True: - _logger().debug("flushing package cache") + LOG.debug("flushing package cache") self._pkg_cache.clear() res = [] for orig in packages: @@ -360,7 +324,7 @@ class YumWorker(Process): res.append(pkg) return res - @_trace_function + @trace_function def _cache_packages(self, packages, flush_cache=True, transform=False): """ Store packages in cache and return them. @@ -372,13 +336,13 @@ class YumWorker(Process): if transform is True: return self._transform_packages(packages, flush_cache=flush_cache) if flush_cache is True: - _logger().debug("flushing package cache") + LOG.debug("flushing package cache") self._pkg_cache.clear() for pkg in packages: self._pkg_cache[id(pkg)] = pkg return packages - @_trace_function + @trace_function def _lookup_package(self, pkg): """ Lookup the original package in cache. @@ -387,13 +351,13 @@ class YumWorker(Process): """ if not isinstance(pkg, packageinfo.PackageInfo): raise TypeError("pkg must be instance of PackageInfo") - _logger().debug("looking up yum package %s with id=%d", + LOG.debug("looking up yum package %s with id=%d", pkg, pkg.objid) try: result = self._pkg_cache[pkg.objid] - _logger().debug("lookup successful") + LOG.debug("lookup successful") except KeyError: - _logger().warn("lookup of package %s with id=%d failed, trying" + LOG.warn("lookup of package %s with id=%d failed, trying" " to query database", pkg, pkg.objid) result = self._handle_filter_packages( 'installed' if pkg.installed else 'available', @@ -402,13 +366,13 @@ class YumWorker(Process): transform=False, **pkg.key_props) if len(result) < 1: - _logger().warn("package %s not found", pkg) + LOG.warn("package %s not found", pkg) raise errors.PackageNotFound( "package %s could not be found" % pkg) result = result[0] return result - @_trace_function + @trace_function def _clear_repository_cache(self): """ Clears the repository cache and their configuration directory @@ -421,7 +385,7 @@ class YumWorker(Process): del self._yum_base.pkgSack self._repodir_mtimes.clear() - @_trace_function + @trace_function def _check_repository_configs(self): """ Checks whether repository information is up to date with configuration @@ -435,7 +399,7 @@ class YumWorker(Process): and ( not repodir in self._repodir_mtimes or ( os.stat(repodir).st_mtime > self._repodir_mtimes[repodir]))): - _logger().info("repository config dir %s changed", repodir) + LOG.info("repository config dir %s changed", repodir) dirty = True break if not dirty: @@ -444,18 +408,18 @@ class YumWorker(Process): if ( not os.path.exists(filename) or ( int(os.stat(filename).st_mtime) > repo.repo_config_age)): - _logger().info('config file of repository "%s" changed', + LOG.info('config file of repository "%s" changed', repo.id) dirty = True break if dirty is True: - _logger().info("repository cache is dirty, cleaning up ...") + LOG.info("repository cache is dirty, cleaning up ...") self._clear_repository_cache() self._yum_base.getReposFromConfig() if dirty is True or not self._repodir_mtimes: self._update_repodir_mtimes() - @_trace_function + @trace_function def _update_repodir_mtimes(self): """ Updates the last modification times of repo configuration directories. @@ -465,7 +429,7 @@ class YumWorker(Process): if os.path.exists(repodir): self._repodir_mtimes[repodir] = os.stat(repodir).st_mtime - @_trace_function + @trace_function def _do_work(self, job): """ Dispatcher of incoming jobs. Job is passed to the right handler @@ -484,55 +448,86 @@ class YumWorker(Process): jobs.YumBeginSession : self._handle_begin_session, jobs.YumEndSession : self._handle_end_session, jobs.YumCheckPackage : self._handle_check_package, + jobs.YumInstallPackageFromURI : \ + self._handle_install_package_from_uri, jobs.YumGetRepositoryList : \ self._handle_get_repository_list, jobs.YumFilterRepositories : self._handle_filter_repositories, jobs.YumSetRepositoryEnabled : \ self._handle_set_repository_enabled }[job.__class__] - _logger().info("processing job %s(id=%d)", + LOG.info("processing job %s(id=%d)", job.__class__.__name__, job.jobid) except KeyError: - _logger().info("No handler for job \"%s\"", job.__class__.__name__) + LOG.error("No handler for job \"%s\"", job.__class__.__name__) raise errors.UnknownJob("No handler for job \"%s\"." % job.__class__.__name__) return handler(**job.job_kwargs) - @_trace_function + @trace_function def _run_transaction(self, name): """ Builds and runs the yum transaction and checks for errors. @param name of transaction used only in error description on failure """ - _logger().info("building transaction %s", name) + LOG.info("building transaction %s", name) (code, msgs) = self._yum_base.buildTransaction() if code == 1: - _logger().error("building transaction %s failed: %s", + LOG.error("building transaction %s failed: %s", name, "\n".join(msgs)) raise errors.TransactionBuildFailed( "Failed to build \"%s\" transaction: %s" % ( name, "\n".join(msgs))) - _logger().info("processing transaction %s", name) + LOG.info("processing transaction %s", name) self._yum_base.processTransaction() self._yum_base.closeRpmDB() - @_trace_function + @trace_function + def _main_loop(self): + """ + This is a main loop called from run(). Jobs are handled here. + It accepts a job from input queue, handles it, + sends the result to output queue and marks the job as done. + + It is terminated, when None is received from input queue. + """ + while True: + job = self._get_job() + if job is not None: # not a terminate command + result = jobs.YumJob.RESULT_SUCCESS + try: + data = self._do_work(job) + except Exception: #pylint: disable=W0703 + result = jobs.YumJob.RESULT_ERROR + # (type, value, traceback) + data = sys.exc_info() + # traceback is not pickable - replace it with formatted + # text + data = (data[0], data[1], traceback.format_tb(data[2])) + LOG.exception("job %s failed", job) + self._jobmgr.finish_job(job, result, data) + if job is None: + LOG.info("waiting for %s to finish", self._jobmgr.name) + self._jobmgr.join() + break + + @trace_function def _handle_begin_session(self): """ Handler for session begin job. """ self._session_level += 1 - _logger().info("beginning session level %s", self._session_level) + LOG.info("beginning session level %s", self._session_level) if self._session_level == 1: self._init_database() self._lock_database() - @_trace_function + @trace_function def _handle_end_session(self): """ Handler for session end job. """ - _logger().info("ending session level %d", self._session_level) + LOG.info("ending session level %d", self._session_level) self._session_level = max(self._session_level - 1, 0) if self._session_level == 0: self._unlock_database() @@ -556,7 +551,7 @@ class YumWorker(Process): else: what = kind with RepoFilterSetter(self._yum_base, include_repos, exclude_repos): - _logger().debug("calling YumBase.doPackageLists(%s, showdups=%s)", + LOG.debug("calling YumBase.doPackageLists(%s, showdups=%s)", what, allow_duplicates) pkglist = self._yum_base.doPackageLists(what, showdups=allow_duplicates) @@ -570,7 +565,7 @@ class YumWorker(Process): result = getattr(pkglist, what) if sort is True: result.sort() - _logger().debug("returning %s packages", len(result)) + LOG.debug("returning %s packages", len(result)) return self._cache_packages(result, transform=transform) @_needs_database @@ -588,21 +583,40 @@ class YumWorker(Process): result = [p for p in pkglist if matches(p)] if sort is True: result.sort() - _logger().debug("%d packages matching", len(result)) + LOG.debug("%d packages matching", len(result)) if transform is True: # caching has been already done by _handle_get_package_list() result = self._transform_packages(result, cache_packages=False) return result @_needs_database - def _handle_install_package(self, pkg): + def _handle_install_package(self, pkg, force=False): """ Handler for package installation job. @return installed package instance """ - pkg_desired = self._lookup_package(pkg) - self._yum_base.install(pkg_desired) - self._run_transaction("install") + if isinstance(pkg, basestring): + pkgs = self._handle_filter_packages( + 'available' if force else 'avail_notinst', + allow_duplicates=False, sort=True, + transform=False, nevra=pkg) + if len(pkgs) < 1: + raise errors.PackageNotFound('No available package matches' + ' nevra "%s".' % pkg) + elif len(pkgs) > 1: + LOG.warn('multiple packages matches nevra "%s": [%s]', + pkg, ", ".join(p.nevra for p in pkgs)) + pkg_desired = pkgs[-1] + else: + pkg_desired = self._lookup_package(pkg) + if isinstance(pkg_desired, yum.rpmsack.RPMInstalledPackage): + if force is False: + raise errors.PackageAlreadyInstalled(pkg) + action = "reinstall" + else: + action = "install" + getattr(self._yum_base, action)(pkg_desired) + self._run_transaction(action) installed = self._handle_filter_packages("installed", False, False, nevra=util.pkg2nevra(pkg_desired, with_epoch="ALWAYS")) if len(installed) < 1: @@ -615,7 +629,16 @@ class YumWorker(Process): """ Handler for package removal job. """ - pkg = self._lookup_package(pkg) + if isinstance(pkg, basestring): + pkgs = self._handle_filter_packages('installed', + allow_duplicates=False, sort=False, + transform=False, nevra=pkg) + if len(pkgs) < 1: + raise errors.PackageNotFound('No available package matches' + ' nevra "%s".' % pkg) + pkg = pkgs[-1] + else: + pkg = self._lookup_package(pkg) self._yum_base.remove(pkg) self._run_transaction("remove") @@ -625,7 +648,16 @@ class YumWorker(Process): Handler for specific package update job. @return package corresponding to pkg after update """ - pkg_desired = self._lookup_package(pkg) + if isinstance(pkg, basestring): + pkgs = self._handle_filter_packages('available', + allow_duplicates=False, sort=False, + transform=False, nevra=pkg) + if len(pkgs) < 1: + raise errors.PackageNotFound('No available package matches' + ' nevra "%s".' % pkg) + pkg_desired = pkgs[-1] + else: + pkg_desired = self._lookup_package(pkg) self._yum_base.update(update_to=True, name=pkg_desired.name, epoch=pkg_desired.epoch, @@ -641,12 +673,24 @@ class YumWorker(Process): return installed[0] @_needs_database - def _handle_update_package(self, pkg, to_epoch, to_version, to_release): + def _handle_update_package(self, pkg, to_epoch, to_version, to_release, + _force=False): """ Handler for package update job. @return updated package instance """ - pkg = self._lookup_package(pkg) + if isinstance(pkg, basestring): + pkgs = self._handle_filter_packages('installed', + allow_duplicates=False, sort=False, + transform=False, nevra=pkg) + if len(pkgs) < 1: + raise errors.PackageNotFound('No available package matches' + ' nevra "%s".' % pkg) + pkg = pkgs[-1] + else: + pkg = self._lookup_package(pkg) + if not isinstance(pkg, yum.rpmsack.RPMInstalledPackage): + raise errors.PackageNotInstalled(pkg) kwargs = { "name" : pkg.name, "arch" : pkg.arch } if any(v is not None for v in (to_epoch, to_version, to_release)): kwargs["update_to"] = True @@ -672,12 +716,54 @@ class YumWorker(Process): """ @return PackageFile instance for requested package """ - pkg = self._lookup_package(pkg) + if isinstance(pkg, basestring): + pkgs = self._handle_filter_packages('installed', + allow_duplicates=False, sort=False, + transform=False, nevra=pkg) + if len(pkgs) < 1: + raise errors.PackageNotFound('No available package matches' + ' nevra "%s".' % pkg) + pkg = pkgs[-1] + else: + pkg = self._lookup_package(pkg) + if not isinstance(pkg, yum.rpmsack.RPMInstalledPackage): + raise errors.PackageNotInstalled(pkg) vpkg = yum.packages._RPMVerifyPackage(pkg, pkg.hdr.fiFromHeader(), packagecheck.pkg_checksum_type(pkg), [], True) return packagecheck.make_package_check_from_db(vpkg) @_needs_database + def _handle_install_package_from_uri(self, uri, + update_only=False, force=False): + """ + @return installed PackageInfo instance + """ + try: + pkg = yum.packages.YumUrlPackage(self, + ts=self._yum_base.rpmdb.readOnlyTS(), url=uri, + ua=default_grabber) + except yum.Errors.MiscError as exc: + raise errors.PackageOpenError(uri, str(exc)) + installed = self._handle_filter_packages("installed", False, False, + nevra=util.pkg2nevra(pkg, with_epoch="ALWAYS")) + if installed and force is False: + raise errors.PackageAlreadyInstalled(pkg) + kwargs = { 'po' : pkg } + if installed: + action = 'reinstallLocal' + else: + action = 'installLocal' + kwargs = { 'updateonly' : update_only } + getattr(self._yum_base, action)(uri, **kwargs) + self._run_transaction('installLocal') + installed = self._handle_filter_packages("installed", False, False, + nevra=util.pkg2nevra(pkg, with_epoch="ALWAYS")) + if len(installed) < 1: + raise errors.TransactionExecutionFailed( + "Failed to install desired package %s." % pkg) + return installed[0] + + @_needs_database def _handle_get_repository_list(self, kind, transform=True): """ @return list of yumdb.Repository instances @@ -692,7 +778,7 @@ class YumWorker(Process): repos.sort() if transform: repos = [repository.make_repository_from_db(r) for r in repos] - _logger().debug("returning %d repositories from %s", + LOG.debug("returning %d repositories from %s", len(repos), kind) return repos @@ -709,15 +795,15 @@ class YumWorker(Process): self._yum_base.repos.getRepo(filters["repoid"])) if ( (kind == "enabled" and not repo.enabled) or (kind == "disabled" and repo.enabled)): - _logger().warn( + LOG.warn( 'no such repository with id="%s"matching filters', filters['repoid']) return [] - _logger().debug( + LOG.debug( "exactly one repository matching filters found") return [repo] except (KeyError, yum.Errors.RepoError): - _logger().warn('repository with id="%s" could not be found', + LOG.warn('repository with id="%s" could not be found', filters['repoid']) raise errors.RepositoryNotFound(filters['repoid']) repos = self._handle_get_repository_list(kind, transform=False) @@ -730,7 +816,7 @@ class YumWorker(Process): break else: # all properties passed result.append(repository.make_repository_from_db(repo)) - _logger().debug("found %d repositories matching", len(result)) + LOG.debug("found %d repositories matching", len(result)) return result @_needs_database @@ -746,10 +832,10 @@ class YumWorker(Process): try: if enable ^ res: if enable is True: - _logger().info("enabling repository %s" % repo) + LOG.info("enabling repository %s" % repo) repo.enable() else: - _logger().info("disabling repository %s" % repo) + LOG.info("disabling repository %s" % repo) repo.disable() try: yum.config.writeRawRepoFile(repo, only=["enabled"]) @@ -758,7 +844,7 @@ class YumWorker(Process): 'failed to modify repository "%s": %s - %s' % ( repo, exc.__class__.__name__, str(exc))) else: - _logger().info("no change for repo %s", repo) + LOG.info("no change for repo %s", repo) except yum.Errors.RepoError as exc: raise errors.RepositoryChangeError( 'failed to modify repository "%s": %s' % (repo, str(exc))) @@ -772,59 +858,32 @@ class YumWorker(Process): """ @return input queue for jobs """ - return self._queue_in + return self._jobmgr.queue_in @property def downlink(self): """ @return output queue for job results """ - return self._queue_out + return self._jobmgr.queue_out # ************************************************************************* # Public methods # ************************************************************************* def run(self): """ - Main loop of process. It accepts a job from input queue, handles it, - sends the result to output queue and marks the job as done. - - It is terminated, when None is received from input queue. + Thread's entry point. After initial setup it calls _main_loop(). """ if self._logging_config is not None: - try: - logging.config.dictConfig(self._logging_config) - except Exception: #pylint: disable=W0703 - # logging is not set up but client expects us to work - pass - _logger().info("running as pid=%d", self.pid) - _logger().info("starting %s main loop", self.__class__.__name__) + setup_logging(self._logging_config) + global LOG + LOG = logging.getLogger(__name__) + LOG.info("running as pid=%d", self.pid) + self._jobmgr.start() + LOG.info("started %s as thread %s", + self._jobmgr.name, self._jobmgr.ident) self._pkg_cache = weakref.WeakValueDictionary() - while True: - if self._session_ended and self._session_level == 0: - try: - job = self._queue_in.get(True, FREE_DATABASE_TIMEOUT) - except TQueue.Empty: - self._free_database() - self._session_ended = False - continue - else: - job = self._queue_in.get() - if job is not None: # not a terminate command - try: - result = self._do_work(job) - except Exception: #pylint: disable=W0703 - # (type, value, traceback) - result = sys.exc_info() - # traceback is not pickable - replace it with formatted - # text - result = ( result[0], result[1] - , traceback.format_tb(result[2])) - _logger().error("job %s(id=%d) failed: %s", - job.__class__.__name__, job.jobid, - traceback.format_exc()) - self._queue_out.put((job.jobid, result)) - self._queue_in.task_done() - if job is None: - break + + self._main_loop() + LOG.info("terminating") diff --git a/src/software/openlmi/software/yumdb/util.py b/src/software/openlmi/software/yumdb/util.py new file mode 100644 index 0000000..f2af151 --- /dev/null +++ b/src/software/openlmi/software/yumdb/util.py @@ -0,0 +1,97 @@ +from itertools import chain +import inspect +import logging +import os + +from openlmi.software.yumdb import errors + +class DispatchingFormatter: + def __init__(self, formatters, default): + for k, formatter in formatters.items(): + if isinstance(formatter, basestring): + formatters[k] = logging.Formatter(formatter) + self._formatters = formatters + if isinstance(default, basestring): + default = logging.Formatter(default) + self._default_formatter = default + + def format(self, record): + formatter = self._formatters.get(record.name, self._default_formatter) + return formatter.format(record) + +# ***************************************************************************** +# Decorators +# ***************************************************************************** +def trace_function(func): + """ + Decorator for logging entries and exits of function or method. + """ + if not inspect.ismethod(func) and not inspect.isfunction(func): + raise TypeError("func must be a function") + + def _print_value(val): + """ + Used here for printing function arguments. Shortens the output + string, if that would be too long. + """ + if isinstance(val, list): + if len(val) < 2: + return str(val) + else: + return "[%s, ...]" % _print_value(val[0]) + return str(val) + + logger = logging.getLogger(__name__+'.trace_function') + module = func.__module__.split('.')[-1] + lineno = inspect.currentframe().f_back.f_lineno + + def _wrapper(self, *args, **kwargs): + """ + Wrapper for function or method, that does the logging. + """ + if logger.isEnabledFor(logging.DEBUG): + frm = inspect.currentframe() + logargs = { + "caller_file" : os.path.basename(os.path.splitext( + frm.f_back.f_code.co_filename)[0]), + "caller_lineno" : frm.f_back.f_lineno, + "module" : module, + "func" : func.__name__, + "lineno" : lineno, + "action" : "entering", + "args" : ", ".join(chain( + (_print_value(a) for a in args), + ( "%s=%s"%(k, _print_value(v)) + for k, v in kwargs.items()))) + } + + if not logargs["args"]: + logargs["args"] = "" + else: + logargs["args"] = " with args=(%s)" % logargs["args"] + logger.debug("%(caller_file)s:%(caller_lineno)d - %(action)s" + " %(module)s:%(func)s:%(lineno)d%(args)s" , logargs) + try: + result = func(self, *args, **kwargs) + if logger.isEnabledFor(logging.DEBUG): + logargs["action"] = "exiting" + logger.debug("%(caller_file)s:%(caller_lineno)d - %(action)s" + " %(module)s:%(func)s:%(lineno)d", logargs) + except Exception as exc: + logargs['action'] = 'exiting' + logargs['error'] = str(exc) + logger.debug("%(caller_file)s:%(caller_lineno)d - %(action)s" + " %(module)s:%(func)s:%(lineno)d with error: %(error)s", + logargs) + raise + return result + + return _wrapper + +def setup_logging(config): + try: + logging.config.dictConfig(config) + except Exception: #pylint: disable=W0703 + # logging is not set up but client expects us to work + pass + diff --git a/src/software/test/package.py b/src/software/test/package.py index 997ee34..3cd0008 100644 --- a/src/software/test/package.py +++ b/src/software/test/package.py @@ -21,6 +21,7 @@ Abstraction for RPM package for test purposes. """ +import json import util class Package(object): #pylint: disable=R0902 @@ -115,3 +116,15 @@ class Package(object): #pylint: disable=R0902 attrs.append('arch') return util.make_evra(*[getattr(self, '_'+a) for a in attrs]) +class PackageEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Package): + return obj.__dict__ + return json.JSONEncoder.default(self, obj) + +def from_json(json_object): + if '_arch' in json_object: + kwargs = {k[1:]: v for k, v in json_object.items()} + return Package(**kwargs) + return json_object + diff --git a/src/software/test/rpmcache.py b/src/software/test/rpmcache.py index c2dde24..6dc45a0 100644 --- a/src/software/test/rpmcache.py +++ b/src/software/test/rpmcache.py @@ -24,15 +24,14 @@ Creation and manipulation utilities with rpm cache for software tests. """ import copy -import datetime import os -import pickle +import json import random import re from collections import defaultdict from subprocess import call, check_output, CalledProcessError -from package import Package +from package import Package, PackageEncoder, from_json import util DB_BACKUP_FILE = 'lmi_software_test_cache' @@ -425,8 +424,9 @@ def write_pkgdb(safe_pkgs, dangerous_pkgs, cache_dir=''): Writes package database into a file named DB_BACKUP_FILE. """ with open(os.path.join(cache_dir, DB_BACKUP_FILE), 'w') as db_file: - data = (datetime.datetime.now(), safe_pkgs, dangerous_pkgs) - pickle.dump(data, db_file) + data = (safe_pkgs, dangerous_pkgs) + json.dump(data, db_file, cls=PackageEncoder, + sort_keys=True, indent=4, separators=(',', ': ')) def load_pkgdb(cache_dir=''): """ @@ -434,7 +434,7 @@ def load_pkgdb(cache_dir=''): @return (safe, dangerous) package lists loaded from file """ with open(os.path.join(cache_dir, DB_BACKUP_FILE), 'r') as db_file: - _, safe, dangerous = pickle.load(db_file) + safe, dangerous = json.load(db_file, object_hook=from_json) #print "Loaded package database from: %s" % date_time return safe, dangerous |