From 2beac025f72a55796d7880ec8deec9e8fd09f9d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:59:35 +0200 Subject: [PATCH] removed deadline addon --- .../deadline/client/ayon_deadline/__init__.py | 8 - .../ayon_deadline/abstract_submit_deadline.py | 617 ------------ .../deadline/client/ayon_deadline/addon.py | 81 -- .../deadline/client/ayon_deadline/lib.py | 10 - .../collect_deadline_server_from_instance.py | 115 --- .../collect_default_deadline_server.py | 48 - .../plugins/publish/collect_pools.py | 91 -- .../publish/collect_user_credentials.py | 98 -- .../help/validate_deadline_connection.xml | 17 - .../publish/help/validate_deadline_pools.xml | 31 - .../publish/submit_aftereffects_deadline.py | 143 --- .../publish/submit_blender_deadline.py | 225 ----- .../publish/submit_celaction_deadline.py | 271 ----- .../plugins/publish/submit_fusion_deadline.py | 253 ----- .../publish/submit_harmony_deadline.py | 420 -------- .../publish/submit_houdini_cache_deadline.py | 181 ---- .../publish/submit_houdini_render_deadline.py | 403 -------- .../plugins/publish/submit_max_deadline.py | 431 -------- .../plugins/publish/submit_maya_deadline.py | 935 ------------------ .../plugins/publish/submit_nuke_deadline.py | 558 ----------- .../publish/submit_publish_cache_job.py | 463 --------- .../plugins/publish/submit_publish_job.py | 585 ----------- .../publish/validate_deadline_connection.py | 52 - .../publish/validate_deadline_pools.py | 84 -- .../validate_expected_and_rendered_files.py | 256 ----- .../repository/custom/plugins/Ayon/Ayon.ico | Bin 7679 -> 0 bytes .../custom/plugins/Ayon/Ayon.options | 9 - .../repository/custom/plugins/Ayon/Ayon.param | 35 - .../repository/custom/plugins/Ayon/Ayon.py | 159 --- .../custom/plugins/CelAction/CelAction.ico | Bin 103192 -> 0 bytes .../custom/plugins/CelAction/CelAction.param | 38 - .../custom/plugins/CelAction/CelAction.py | 122 --- .../custom/plugins/GlobalJobPreLoad.py | 662 ------------- .../plugins/HarmonyAYON/HarmonyAYON.ico | Bin 1150 -> 0 bytes .../plugins/HarmonyAYON/HarmonyAYON.options | 532 ---------- .../plugins/HarmonyAYON/HarmonyAYON.param | 98 -- .../custom/plugins/HarmonyAYON/HarmonyAYON.py | 151 --- .../OpenPypeTileAssembler.ico | Bin 126987 -> 0 bytes .../OpenPypeTileAssembler.options | 35 - .../OpenPypeTileAssembler.param | 17 - .../OpenPypeTileAssembler.py | 457 --------- .../client/ayon_deadline/repository/readme.md | 29 - .../deadline/client/ayon_deadline/version.py | 3 - server_addon/deadline/package.py | 10 - server_addon/deadline/server/__init__.py | 15 - .../deadline/server/settings/__init__.py | 12 - server_addon/deadline/server/settings/main.py | 100 -- .../server/settings/publish_plugins.py | 578 ----------- .../deadline/server/settings/site_settings.py | 28 - 49 files changed, 9466 deletions(-) delete mode 100644 server_addon/deadline/client/ayon_deadline/__init__.py delete mode 100644 server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/addon.py delete mode 100644 server_addon/deadline/client/ayon_deadline/lib.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py delete mode 100644 server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.options delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py delete mode 100644 server_addon/deadline/client/ayon_deadline/repository/readme.md delete mode 100644 server_addon/deadline/client/ayon_deadline/version.py delete mode 100644 server_addon/deadline/package.py delete mode 100644 server_addon/deadline/server/__init__.py delete mode 100644 server_addon/deadline/server/settings/__init__.py delete mode 100644 server_addon/deadline/server/settings/main.py delete mode 100644 server_addon/deadline/server/settings/publish_plugins.py delete mode 100644 server_addon/deadline/server/settings/site_settings.py diff --git a/server_addon/deadline/client/ayon_deadline/__init__.py b/server_addon/deadline/client/ayon_deadline/__init__.py deleted file mode 100644 index 6fec1006e6..0000000000 --- a/server_addon/deadline/client/ayon_deadline/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .addon import DeadlineAddon -from .version import __version__ - - -__all__ = ( - "DeadlineAddon", - "__version__" -) diff --git a/server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py b/server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py deleted file mode 100644 index ba50aaccf7..0000000000 --- a/server_addon/deadline/client/ayon_deadline/abstract_submit_deadline.py +++ /dev/null @@ -1,617 +0,0 @@ -# -*- coding: utf-8 -*- -"""Abstract package for submitting jobs to Deadline. - -It provides Deadline JobInfo data class. - -""" -import json.decoder -import os -from abc import abstractmethod -import platform -import getpass -from functools import partial -from collections import OrderedDict - -import six -import attr -import requests - -import pyblish.api -from ayon_core.pipeline.publish import ( - AbstractMetaInstancePlugin, - KnownPublishError, - AYONPyblishPluginMixin -) -from ayon_core.pipeline.publish.lib import ( - replace_with_published_scene_path -) - -JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) - - -def requests_post(*args, **kwargs): - """Wrap request post method. - - Disabling SSL certificate validation if ``verify`` kwarg is set to False. - This is useful when Deadline server is - running with self-signed certificates and its certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing, and it is not recommended. - - """ - auth = kwargs.get("auth") - if auth: - kwargs["auth"] = tuple(auth) # explicit cast to tuple - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.post(*args, **kwargs) - - -def requests_get(*args, **kwargs): - """Wrap request get method. - - Disabling SSL certificate validation if ``verify`` kwarg is set to False. - This is useful when Deadline server is - running with self-signed certificates and its certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing, and it is not recommended. - - """ - auth = kwargs.get("auth") - if auth: - kwargs["auth"] = tuple(auth) - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.get(*args, **kwargs) - - -class DeadlineKeyValueVar(dict): - """ - - Serializes dictionary key values as "{key}={value}" like Deadline uses - for EnvironmentKeyValue. - - As an example: - EnvironmentKeyValue0="A_KEY=VALUE_A" - EnvironmentKeyValue1="OTHER_KEY=VALUE_B" - - The keys are serialized in alphabetical order (sorted). - - Example: - >>> var = DeadlineKeyValueVar("EnvironmentKeyValue") - >>> var["my_var"] = "hello" - >>> var["my_other_var"] = "hello2" - >>> var.serialize() - - - """ - def __init__(self, key): - super(DeadlineKeyValueVar, self).__init__() - self.__key = key - - def serialize(self): - key = self.__key - - # Allow custom location for index in serialized string - if "{}" not in key: - key = key + "{}" - - return { - key.format(index): "{}={}".format(var_key, var_value) - for index, (var_key, var_value) in enumerate(sorted(self.items())) - } - - -class DeadlineIndexedVar(dict): - """ - - Allows to set and query values by integer indices: - Query: var[1] or var.get(1) - Set: var[1] = "my_value" - Append: var += "value" - - Note: Iterating the instance is not guarantueed to be the order of the - indices. To do so iterate with `sorted()` - - """ - def __init__(self, key): - super(DeadlineIndexedVar, self).__init__() - self.__key = key - - def serialize(self): - key = self.__key - - # Allow custom location for index in serialized string - if "{}" not in key: - key = key + "{}" - - return { - key.format(index): value for index, value in sorted(self.items()) - } - - def next_available_index(self): - # Add as first unused entry - i = 0 - while i in self.keys(): - i += 1 - return i - - def update(self, data): - # Force the integer key check - for key, value in data.items(): - self.__setitem__(key, value) - - def __iadd__(self, other): - index = self.next_available_index() - self[index] = other - return self - - def __setitem__(self, key, value): - if not isinstance(key, int): - raise TypeError("Key must be an integer: {}".format(key)) - - if key < 0: - raise ValueError("Negative index can't be set: {}".format(key)) - dict.__setitem__(self, key, value) - - -@attr.s -class DeadlineJobInfo(object): - """Mapping of all Deadline *JobInfo* attributes. - - This contains all JobInfo attributes plus their default values. - Those attributes set to `None` shouldn't be posted to Deadline as - the only required one is `Plugin`. Their default values used by Deadline - are stated in - comments. - - ..seealso: - https://docs.thinkboxsoftware.com/products/deadline/10.1/1_User%20Manual/manual/manual-submission.html - - """ - - # Required - # ---------------------------------------------- - Plugin = attr.ib() - - # General - Frames = attr.ib(default=None) # default: 0 - Name = attr.ib(default="Untitled") - Comment = attr.ib(default=None) # default: empty - Department = attr.ib(default=None) # default: empty - BatchName = attr.ib(default=None) # default: empty - UserName = attr.ib(default=getpass.getuser()) - MachineName = attr.ib(default=platform.node()) - Pool = attr.ib(default=None) # default: "none" - SecondaryPool = attr.ib(default=None) - Group = attr.ib(default=None) # default: "none" - Priority = attr.ib(default=50) - ChunkSize = attr.ib(default=1) - ConcurrentTasks = attr.ib(default=1) - LimitConcurrentTasksToNumberOfCpus = attr.ib( - default=None) # default: "true" - OnJobComplete = attr.ib(default="Nothing") - SynchronizeAllAuxiliaryFiles = attr.ib(default=None) # default: false - ForceReloadPlugin = attr.ib(default=None) # default: false - Sequential = attr.ib(default=None) # default: false - SuppressEvents = attr.ib(default=None) # default: false - Protected = attr.ib(default=None) # default: false - InitialStatus = attr.ib(default="Active") - NetworkRoot = attr.ib(default=None) - - # Timeouts - # ---------------------------------------------- - MinRenderTimeSeconds = attr.ib(default=None) # Default: 0 - MinRenderTimeMinutes = attr.ib(default=None) # Default: 0 - TaskTimeoutSeconds = attr.ib(default=None) # Default: 0 - TaskTimeoutMinutes = attr.ib(default=None) # Default: 0 - StartJobTimeoutSeconds = attr.ib(default=None) # Default: 0 - StartJobTimeoutMinutes = attr.ib(default=None) # Default: 0 - InitializePluginTimeoutSeconds = attr.ib(default=None) # Default: 0 - # can be one of - OnTaskTimeout = attr.ib(default=None) # Default: Error - EnableTimeoutsForScriptTasks = attr.ib(default=None) # Default: false - EnableFrameTimeouts = attr.ib(default=None) # Default: false - EnableAutoTimeout = attr.ib(default=None) # Default: false - - # Interruptible - # ---------------------------------------------- - Interruptible = attr.ib(default=None) # Default: false - InterruptiblePercentage = attr.ib(default=None) - RemTimeThreshold = attr.ib(default=None) - - # Notifications - # ---------------------------------------------- - # can be comma separated list of users - NotificationTargets = attr.ib(default=None) # Default: blank - ClearNotificationTargets = attr.ib(default=None) # Default: false - # A comma separated list of additional email addresses - NotificationEmails = attr.ib(default=None) # Default: blank - OverrideNotificationMethod = attr.ib(default=None) # Default: false - EmailNotification = attr.ib(default=None) # Default: false - PopupNotification = attr.ib(default=None) # Default: false - # String with `[EOL]` used for end of line - NotificationNote = attr.ib(default=None) # Default: blank - - # Machine Limit - # ---------------------------------------------- - MachineLimit = attr.ib(default=None) # Default: 0 - MachineLimitProgress = attr.ib(default=None) # Default: -1.0 - Whitelist = attr.ib(default=None) # Default: blank - Blacklist = attr.ib(default=None) # Default: blank - - # Limits - # ---------------------------------------------- - # comma separated list of limit groups - LimitGroups = attr.ib(default=None) # Default: blank - - # Dependencies - # ---------------------------------------------- - # comma separated list of job IDs - JobDependencies = attr.ib(default=None) # Default: blank - JobDependencyPercentage = attr.ib(default=None) # Default: -1 - IsFrameDependent = attr.ib(default=None) # Default: false - FrameDependencyOffsetStart = attr.ib(default=None) # Default: 0 - FrameDependencyOffsetEnd = attr.ib(default=None) # Default: 0 - ResumeOnCompleteDependencies = attr.ib(default=None) # Default: true - ResumeOnDeletedDependencies = attr.ib(default=None) # Default: false - ResumeOnFailedDependencies = attr.ib(default=None) # Default: false - # comma separated list of asset paths - RequiredAssets = attr.ib(default=None) # Default: blank - # comma separated list of script paths - ScriptDependencies = attr.ib(default=None) # Default: blank - - # Failure Detection - # ---------------------------------------------- - OverrideJobFailureDetection = attr.ib(default=None) # Default: false - FailureDetectionJobErrors = attr.ib(default=None) # 0..x - OverrideTaskFailureDetection = attr.ib(default=None) # Default: false - FailureDetectionTaskErrors = attr.ib(default=None) # 0..x - IgnoreBadJobDetection = attr.ib(default=None) # Default: false - SendJobErrorWarning = attr.ib(default=None) # Default: false - - # Cleanup - # ---------------------------------------------- - DeleteOnComplete = attr.ib(default=None) # Default: false - ArchiveOnComplete = attr.ib(default=None) # Default: false - OverrideAutoJobCleanup = attr.ib(default=None) # Default: false - OverrideJobCleanup = attr.ib(default=None) - JobCleanupDays = attr.ib(default=None) # Default: false - # - OverrideJobCleanupType = attr.ib(default=None) - - # Scheduling - # ---------------------------------------------- - # - ScheduledType = attr.ib(default=None) # Default: None - #
- ScheduledStartDateTime = attr.ib(default=None) - ScheduledDays = attr.ib(default=None) # Default: 1 - # - JobDelay = attr.ib(default=None) - # Time= - Scheduled = attr.ib(default=None) - - # Scripts - # ---------------------------------------------- - # all accept path to script - PreJobScript = attr.ib(default=None) # Default: blank - PostJobScript = attr.ib(default=None) # Default: blank - PreTaskScript = attr.ib(default=None) # Default: blank - PostTaskScript = attr.ib(default=None) # Default: blank - - # Event Opt-Ins - # ---------------------------------------------- - # comma separated list of plugins - EventOptIns = attr.ib(default=None) # Default: blank - - # Environment - # ---------------------------------------------- - EnvironmentKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar, - "EnvironmentKeyValue")) - - IncludeEnvironment = attr.ib(default=None) # Default: false - UseJobEnvironmentOnly = attr.ib(default=None) # Default: false - CustomPluginDirectory = attr.ib(default=None) # Default: blank - - # Job Extra Info - # ---------------------------------------------- - ExtraInfo = attr.ib(factory=partial(DeadlineIndexedVar, "ExtraInfo")) - ExtraInfoKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar, - "ExtraInfoKeyValue")) - - # Task Extra Info Names - # ---------------------------------------------- - OverrideTaskExtraInfoNames = attr.ib(default=None) # Default: false - TaskExtraInfoName = attr.ib(factory=partial(DeadlineIndexedVar, - "TaskExtraInfoName")) - - # Output - # ---------------------------------------------- - OutputFilename = attr.ib(factory=partial(DeadlineIndexedVar, - "OutputFilename")) - OutputFilenameTile = attr.ib(factory=partial(DeadlineIndexedVar, - "OutputFilename{}Tile")) - OutputDirectory = attr.ib(factory=partial(DeadlineIndexedVar, - "OutputDirectory")) - - # Asset Dependency - # ---------------------------------------------- - AssetDependency = attr.ib(factory=partial(DeadlineIndexedVar, - "AssetDependency")) - - # Tile Job - # ---------------------------------------------- - TileJob = attr.ib(default=None) # Default: false - TileJobFrame = attr.ib(default=None) # Default: 0 - TileJobTilesInX = attr.ib(default=None) # Default: 0 - TileJobTilesInY = attr.ib(default=None) # Default: 0 - TileJobTileCount = attr.ib(default=None) # Default: 0 - - # Maintenance Job - # ---------------------------------------------- - MaintenanceJob = attr.ib(default=None) # Default: false - MaintenanceJobStartFrame = attr.ib(default=None) # Default: 0 - MaintenanceJobEndFrame = attr.ib(default=None) # Default: 0 - - def serialize(self): - """Return all data serialized as dictionary. - - Returns: - OrderedDict: all serialized data. - - """ - def filter_data(a, v): - if isinstance(v, (DeadlineIndexedVar, DeadlineKeyValueVar)): - return False - if v is None: - return False - return True - - serialized = attr.asdict( - self, dict_factory=OrderedDict, filter=filter_data) - - # Custom serialize these attributes - for attribute in [ - self.EnvironmentKeyValue, - self.ExtraInfo, - self.ExtraInfoKeyValue, - self.TaskExtraInfoName, - self.OutputFilename, - self.OutputFilenameTile, - self.OutputDirectory, - self.AssetDependency - ]: - serialized.update(attribute.serialize()) - - return serialized - - def update(self, data): - """Update instance with data dict""" - for key, value in data.items(): - setattr(self, key, value) - - def add_render_job_env_var(self): - """Check if in OP or AYON mode and use appropriate env var.""" - self.EnvironmentKeyValue["AYON_RENDER_JOB"] = "1" - self.EnvironmentKeyValue["AYON_BUNDLE_NAME"] = ( - os.environ["AYON_BUNDLE_NAME"]) - - -@six.add_metaclass(AbstractMetaInstancePlugin) -class AbstractSubmitDeadline(pyblish.api.InstancePlugin, - AYONPyblishPluginMixin): - """Class abstracting access to Deadline.""" - - label = "Submit to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - - import_reference = False - use_published = True - asset_dependencies = False - default_priority = 50 - - def __init__(self, *args, **kwargs): - super(AbstractSubmitDeadline, self).__init__(*args, **kwargs) - self._instance = None - self._deadline_url = None - self.scene_path = None - self.job_info = None - self.plugin_info = None - self.aux_files = None - - def process(self, instance): - """Plugin entry point.""" - self._instance = instance - context = instance.context - self._deadline_url = instance.data["deadline"]["url"] - - assert self._deadline_url, "Requires Deadline Webservice URL" - - file_path = None - if self.use_published: - if not self.import_reference: - file_path = self.from_published_scene() - else: - self.log.info("use the scene with imported reference for rendering") # noqa - file_path = context.data["currentFile"] - - # fallback if nothing was set - if not file_path: - self.log.warning("Falling back to workfile") - file_path = context.data["currentFile"] - - self.scene_path = file_path - self.log.info("Using {} for render/export.".format(file_path)) - - self.job_info = self.get_job_info() - self.plugin_info = self.get_plugin_info() - self.aux_files = self.get_aux_files() - - job_id = self.process_submission() - self.log.info("Submitted job to Deadline: {}.".format(job_id)) - - # TODO: Find a way that's more generic and not render type specific - if instance.data.get("splitRender"): - self.log.info("Splitting export and render in two jobs") - self.log.info("Export job id: %s", job_id) - render_job_info = self.get_job_info(dependency_job_ids=[job_id]) - render_plugin_info = self.get_plugin_info(job_type="render") - payload = self.assemble_payload( - job_info=render_job_info, - plugin_info=render_plugin_info - ) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - render_job_id = self.submit(payload, auth, verify) - self.log.info("Render job id: %s", render_job_id) - - def process_submission(self): - """Process data for submission. - - This takes Deadline JobInfo, PluginInfo, AuxFile, creates payload - from them and submit it do Deadline. - - Returns: - str: Deadline job ID - - """ - payload = self.assemble_payload() - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - return self.submit(payload, auth, verify) - - @abstractmethod - def get_job_info(self): - """Return filled Deadline JobInfo. - - This is host/plugin specific implementation of how to fill data in. - - See: - :class:`DeadlineJobInfo` - - Returns: - :class:`DeadlineJobInfo`: Filled Deadline JobInfo. - - """ - pass - - @abstractmethod - def get_plugin_info(self): - """Return filled Deadline PluginInfo. - - This is host/plugin specific implementation of how to fill data in. - - See: - :class:`DeadlineJobInfo` - - Returns: - dict: Filled Deadline JobInfo. - - """ - pass - - def get_aux_files(self): - """Return list of auxiliary files for Deadline job. - - If needed this should be overridden, otherwise return empty list as - that field even empty must be present on Deadline submission. - - Returns: - list: List of files. - - """ - return [] - - def from_published_scene(self, replace_in_path=True): - """Switch work scene for published scene. - - If rendering/exporting from published scenes is enabled, this will - replace paths from working scene to published scene. - - Args: - replace_in_path (bool): if True, it will try to find - old scene name in path of expected files and replace it - with name of published scene. - - Returns: - str: Published scene path. - None: if no published scene is found. - - Note: - Published scene path is actually determined from project Anatomy - as at the time this plugin is running scene can still no be - published. - - """ - return replace_with_published_scene_path( - self._instance, replace_in_path=replace_in_path) - - def assemble_payload( - self, job_info=None, plugin_info=None, aux_files=None): - """Assemble payload data from its various parts. - - Args: - job_info (DeadlineJobInfo): Deadline JobInfo. You can use - :class:`DeadlineJobInfo` for it. - plugin_info (dict): Deadline PluginInfo. Plugin specific options. - aux_files (list, optional): List of auxiliary file to submit with - the job. - - Returns: - dict: Deadline Payload. - - """ - job = job_info or self.job_info - return { - "JobInfo": job.serialize(), - "PluginInfo": plugin_info or self.plugin_info, - "AuxFiles": aux_files or self.aux_files - } - - def submit(self, payload, auth, verify): - """Submit payload to Deadline API end-point. - - This takes payload in the form of JSON file and POST it to - Deadline jobs end-point. - - Args: - payload (dict): dict to become json in deadline submission. - auth (tuple): (username, password) - verify (bool): verify SSL certificate if present - - Returns: - str: resulting Deadline job id. - - Throws: - KnownPublishError: if submission fails. - - """ - url = "{}/api/jobs".format(self._deadline_url) - response = requests_post( - url, json=payload, auth=auth, verify=verify) - if not response.ok: - self.log.error("Submission failed!") - self.log.error(response.status_code) - self.log.error(response.content) - self.log.debug(payload) - raise KnownPublishError(response.text) - - try: - result = response.json() - except JSONDecodeError: - msg = "Broken response {}. ".format(response) - msg += "Try restarting the Deadline Webservice." - self.log.warning(msg, exc_info=True) - raise KnownPublishError("Broken response from DL") - - # for submit publish job - self._instance.data["deadlineSubmissionJob"] = result - - return result["_id"] diff --git a/server_addon/deadline/client/ayon_deadline/addon.py b/server_addon/deadline/client/ayon_deadline/addon.py deleted file mode 100644 index 87fc2ad665..0000000000 --- a/server_addon/deadline/client/ayon_deadline/addon.py +++ /dev/null @@ -1,81 +0,0 @@ -import os -import sys - -import requests -import six - -from ayon_core.lib import Logger -from ayon_core.addon import AYONAddon, IPluginPaths - -from .version import __version__ - - -class DeadlineWebserviceError(Exception): - """ - Exception to throw when connection to Deadline server fails. - """ - - -class DeadlineAddon(AYONAddon, IPluginPaths): - name = "deadline" - version = __version__ - - def initialize(self, studio_settings): - deadline_settings = studio_settings[self.name] - deadline_servers_info = { - url_item["name"]: url_item - for url_item in deadline_settings["deadline_urls"] - } - - if not deadline_servers_info: - self.enabled = False - self.log.warning(( - "Deadline Webservice URLs are not specified. Disabling addon." - )) - - self.deadline_servers_info = deadline_servers_info - - def get_plugin_paths(self): - """Deadline plugin paths.""" - current_dir = os.path.dirname(os.path.abspath(__file__)) - return { - "publish": [os.path.join(current_dir, "plugins", "publish")] - } - - @staticmethod - def get_deadline_pools(webservice, auth=None, log=None): - """Get pools from Deadline. - Args: - webservice (str): Server url. - auth (Optional[Tuple[str, str]]): Tuple containing username, - password - log (Optional[Logger]): Logger to log errors to, if provided. - Returns: - List[str]: Pools. - Throws: - RuntimeError: If deadline webservice is unreachable. - - """ - from .abstract_submit_deadline import requests_get - - if not log: - log = Logger.get_logger(__name__) - - argument = "{}/api/pools?NamesOnly=true".format(webservice) - try: - kwargs = {} - if auth: - kwargs["auth"] = auth - response = requests_get(argument, **kwargs) - except requests.exceptions.ConnectionError as exc: - msg = 'Cannot connect to DL web service {}'.format(webservice) - log.error(msg) - six.reraise( - DeadlineWebserviceError, - DeadlineWebserviceError('{} - {}'.format(msg, exc)), - sys.exc_info()[2]) - if not response.ok: - log.warning("No pools retrieved") - return [] - - return response.json() diff --git a/server_addon/deadline/client/ayon_deadline/lib.py b/server_addon/deadline/client/ayon_deadline/lib.py deleted file mode 100644 index 7f07c350ec..0000000000 --- a/server_addon/deadline/client/ayon_deadline/lib.py +++ /dev/null @@ -1,10 +0,0 @@ -# describes list of product typed used for plugin filtering for farm publishing -FARM_FAMILIES = [ - "render", "render.farm", "render.frames_farm", - "prerender", "prerender.farm", "prerender.frames_farm", - "renderlayer", "imagesequence", "image", - "vrayscene", "maxrender", - "arnold_rop", "mantra_rop", - "karma_rop", "vray_rop", "redshift_rop", - "renderFarm", "usdrender", "publish.hou" -] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py deleted file mode 100644 index 2c8cbd1620..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_deadline_server_from_instance.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect Deadline servers from instance. - -This is resolving index of server lists stored in `deadlineServers` instance -attribute or using default server if that attribute doesn't exists. - -""" -import pyblish.api -from ayon_core.pipeline.publish import KnownPublishError - -from ayon_deadline.lib import FARM_FAMILIES - - -class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): - """Collect Deadline Webservice URL from instance.""" - - # Run before collect_render. - order = pyblish.api.CollectorOrder + 0.225 - label = "Deadline Webservice from the Instance" - targets = ["local"] - - families = FARM_FAMILIES - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Should not be processed on farm, skipping.") - return - - if not instance.data.get("deadline"): - instance.data["deadline"] = {} - - # todo: separate logic should be removed, all hosts should have same - host_name = instance.context.data["hostName"] - if host_name == "maya": - deadline_url = self._collect_deadline_url(instance) - else: - deadline_url = (instance.data.get("deadlineUrl") or # backwards - instance.data.get("deadline", {}).get("url")) - if deadline_url: - instance.data["deadline"]["url"] = deadline_url.strip().rstrip("/") - else: - instance.data["deadline"]["url"] = instance.context.data["deadline"]["defaultUrl"] # noqa - self.log.debug( - "Using {} for submission".format(instance.data["deadline"]["url"])) - - def _collect_deadline_url(self, render_instance): - # type: (pyblish.api.Instance) -> str - """Get Deadline Webservice URL from render instance. - - This will get all configured Deadline Webservice URLs and create - subset of them based upon project configuration. It will then take - `deadlineServers` from render instance that is now basically `int` - index of that list. - - Args: - render_instance (pyblish.api.Instance): Render instance created - by Creator in Maya. - - Returns: - str: Selected Deadline Webservice URL. - - """ - # Not all hosts can import this module. - from maya import cmds - deadline_settings = ( - render_instance.context.data - ["project_settings"] - ["deadline"] - ) - default_server_url = (render_instance.context.data["deadline"] - ["defaultUrl"]) - # QUESTION How and where is this is set? Should be removed? - instance_server = render_instance.data.get("deadlineServers") - if not instance_server: - self.log.debug("Using default server.") - return default_server_url - - # Get instance server as sting. - if isinstance(instance_server, int): - instance_server = cmds.getAttr( - "{}.deadlineServers".format(render_instance.data["objset"]), - asString=True - ) - - default_servers = { - url_item["name"]: url_item["value"] - for url_item in deadline_settings["deadline_servers_info"] - } - project_servers = ( - render_instance.context.data - ["project_settings"] - ["deadline"] - ["deadline_servers"] - ) - if not project_servers: - self.log.debug("Not project servers found. Using default servers.") - return default_servers[instance_server] - - project_enabled_servers = { - k: default_servers[k] - for k in project_servers - if k in default_servers - } - - if instance_server not in project_enabled_servers: - msg = ( - "\"{}\" server on instance is not enabled in project settings." - " Enabled project servers:\n{}".format( - instance_server, project_enabled_servers - ) - ) - raise KnownPublishError(msg) - - self.log.debug("Using project approved server.") - return project_enabled_servers[instance_server] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py deleted file mode 100644 index 77d03c713f..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_default_deadline_server.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect default Deadline server.""" -import pyblish.api - - -class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): - """Collect default Deadline Webservice URL. - - DL webservice addresses must be configured first in System Settings for - project settings enum to work. - - Default webservice could be overridden by - `project_settings/deadline/deadline_servers`. Currently only single url - is expected. - - This url could be overridden by some hosts directly on instances with - `CollectDeadlineServerFromInstance`. - """ - - # Run before collect_deadline_server_instance. - order = pyblish.api.CollectorOrder + 0.200 - label = "Default Deadline Webservice" - targets = ["local"] - - def process(self, context): - try: - deadline_addon = context.data["ayonAddonsManager"]["deadline"] - except AttributeError: - self.log.error("Cannot get AYON Deadline addon.") - raise AssertionError("AYON Deadline addon not found.") - - deadline_settings = context.data["project_settings"]["deadline"] - deadline_server_name = deadline_settings["deadline_server"] - - dl_server_info = None - if deadline_server_name: - dl_server_info = deadline_addon.deadline_servers_info.get( - deadline_server_name) - - if dl_server_info: - deadline_url = dl_server_info["value"] - else: - default_dl_server_info = deadline_addon.deadline_servers_info[0] - deadline_url = default_dl_server_info["value"] - - context.data["deadline"] = {} - context.data["deadline"]["defaultUrl"] = ( - deadline_url.strip().rstrip("/")) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py deleted file mode 100644 index b2b6bc60d4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_pools.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -from ayon_core.lib import TextDef -from ayon_core.pipeline.publish import AYONPyblishPluginMixin - -from ayon_deadline.lib import FARM_FAMILIES - - -class CollectDeadlinePools(pyblish.api.InstancePlugin, - AYONPyblishPluginMixin): - """Collect pools from instance or Publisher attributes, from Setting - otherwise. - - Pools are used to control which DL workers could render the job. - - Pools might be set: - - directly on the instance (set directly in DCC) - - from Publisher attributes - - from defaults from Settings. - - Publisher attributes could be shown even for instances that should be - rendered locally as visibility is driven by product type of the instance - (which will be `render` most likely). - (Might be resolved in the future and class attribute 'families' should - be cleaned up.) - - """ - - order = pyblish.api.CollectorOrder + 0.420 - label = "Collect Deadline Pools" - hosts = [ - "aftereffects", - "fusion", - "harmony", - "maya", - "max", - "houdini", - "nuke", - ] - - families = FARM_FAMILIES - - primary_pool = None - secondary_pool = None - - @classmethod - def apply_settings(cls, project_settings): - # deadline.publish.CollectDeadlinePools - settings = project_settings["deadline"]["publish"]["CollectDeadlinePools"] # noqa - cls.primary_pool = settings.get("primary_pool", None) - cls.secondary_pool = settings.get("secondary_pool", None) - - def process(self, instance): - attr_values = self.get_attr_values_from_data(instance.data) - if not instance.data.get("primaryPool"): - instance.data["primaryPool"] = ( - attr_values.get("primaryPool") or self.primary_pool or "none" - ) - if instance.data["primaryPool"] == "-": - instance.data["primaryPool"] = None - - if not instance.data.get("secondaryPool"): - instance.data["secondaryPool"] = ( - attr_values.get("secondaryPool") or self.secondary_pool or "none" # noqa - ) - - if instance.data["secondaryPool"] == "-": - instance.data["secondaryPool"] = None - - @classmethod - def get_attribute_defs(cls): - # TODO: Preferably this would be an enum for the user - # but the Deadline server URL can be dynamic and - # can be set per render instance. Since get_attribute_defs - # can't be dynamic unfortunately EnumDef isn't possible (yet?) - # pool_names = self.deadline_addon.get_deadline_pools(deadline_url, - # self.log) - # secondary_pool_names = ["-"] + pool_names - - return [ - TextDef("primaryPool", - label="Primary Pool", - default=cls.primary_pool, - tooltip="Deadline primary pool, " - "applicable for farm rendering"), - TextDef("secondaryPool", - label="Secondary Pool", - default=cls.secondary_pool, - tooltip="Deadline secondary pool, " - "applicable for farm rendering") - ] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py deleted file mode 100644 index 1c59c178d3..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/collect_user_credentials.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect user credentials - -Requires: - context -> project_settings - instance.data["deadline"]["url"] - -Provides: - instance.data["deadline"] -> require_authentication (bool) - instance.data["deadline"] -> auth (tuple (str, str)) - - (username, password) or None -""" -import pyblish.api - -from ayon_api import get_server_api_connection - -from ayon_deadline.lib import FARM_FAMILIES - - -class CollectDeadlineUserCredentials(pyblish.api.InstancePlugin): - """Collects user name and password for artist if DL requires authentication - - If Deadline server is marked to require authentication, it looks first for - default values in 'Studio Settings', which could be overriden by artist - dependent values from 'Site settings`. - """ - order = pyblish.api.CollectorOrder + 0.250 - label = "Collect Deadline User Credentials" - - targets = ["local"] - hosts = ["aftereffects", - "blender", - "fusion", - "harmony", - "nuke", - "maya", - "max", - "houdini"] - - families = FARM_FAMILIES - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Should not be processed on farm, skipping.") - return - - collected_deadline_url = instance.data["deadline"]["url"] - if not collected_deadline_url: - raise ValueError("Instance doesn't have '[deadline][url]'.") - context_data = instance.context.data - deadline_settings = context_data["project_settings"]["deadline"] - - deadline_server_name = None - # deadline url might be set directly from instance, need to find - # metadata for it - for deadline_info in deadline_settings["deadline_urls"]: - dl_settings_url = deadline_info["value"].strip().rstrip("/") - if dl_settings_url == collected_deadline_url: - deadline_server_name = deadline_info["name"] - break - - if not deadline_server_name: - raise ValueError(f"Collected {collected_deadline_url} doesn't " - "match any site configured in Studio Settings") - - instance.data["deadline"]["require_authentication"] = ( - deadline_info["require_authentication"] - ) - instance.data["deadline"]["auth"] = None - - instance.data["deadline"]["verify"] = ( - not deadline_info["not_verify_ssl"]) - - if not deadline_info["require_authentication"]: - return - - addons_manager = instance.context.data["ayonAddonsManager"] - deadline_addon = addons_manager["deadline"] - - default_username = deadline_info["default_username"] - default_password = deadline_info["default_password"] - if default_username and default_password: - self.log.debug("Setting credentials from defaults") - instance.data["deadline"]["auth"] = (default_username, - default_password) - - # TODO import 'get_addon_site_settings' when available - # in public 'ayon_api' - local_settings = get_server_api_connection().get_addon_site_settings( - deadline_addon.name, deadline_addon.version) - local_settings = local_settings["local_settings"] - for server_info in local_settings: - if deadline_server_name == server_info["server_name"]: - if server_info["username"] and server_info["password"]: - self.log.debug("Setting credentials from Site Settings") - instance.data["deadline"]["auth"] = \ - (server_info["username"], server_info["password"]) - break diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml b/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml deleted file mode 100644 index eec05df08a..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_connection.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - Deadline Authentication - -## Deadline authentication is required - -This project has set in Settings that Deadline requires authentication. - -### How to repair? - -Please go to Ayon Server > Site Settings and provide your Deadline username and password. -In some cases the password may be empty if Deadline is configured to allow that. Ask your administrator. - - - - \ No newline at end of file diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml b/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml deleted file mode 100644 index 879adcee97..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/help/validate_deadline_pools.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - Deadline Pools - -## Invalid Deadline pools found - -Configured pools don't match available pools in Deadline. - -### How to repair? - -If your instance had deadline pools set on creation, remove or -change them. - -In other cases inform admin to change them in Settings. - -Available deadline pools: - -{pools_str} - - - -### __Detailed Info__ - -This error is shown when a configured pool is not available on Deadline. It -can happen when publishing old workfiles which were created with previous -deadline pools, or someone changed the available pools in Deadline, -but didn't modify AYON Settings to match the changes. - - - \ No newline at end of file diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py deleted file mode 100644 index 45d907cbba..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_aftereffects_deadline.py +++ /dev/null @@ -1,143 +0,0 @@ -import os -import attr -import getpass -import pyblish.api -from datetime import datetime - -from ayon_core.lib import ( - env_value_to_bool, - collect_frames, - is_in_tests, -) -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class DeadlinePluginInfo(): - Comp = attr.ib(default=None) - SceneFile = attr.ib(default=None) - OutputFilePath = attr.ib(default=None) - Output = attr.ib(default=None) - StartupDirectory = attr.ib(default=None) - Arguments = attr.ib(default=None) - ProjectPath = attr.ib(default=None) - AWSAssetFile0 = attr.ib(default=None) - Version = attr.ib(default=None) - MultiProcess = attr.ib(default=None) - - -class AfterEffectsSubmitDeadline( - abstract_submit_deadline.AbstractSubmitDeadline -): - - label = "Submit AE to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["aftereffects"] - families = ["render.farm"] # cannot be "render' as that is integrated - use_published = True - targets = ["local"] - - priority = 50 - chunk_size = 1000000 - group = None - department = None - multiprocess = True - - def get_job_info(self): - dln_job_info = DeadlineJobInfo(Plugin="AfterEffects") - - context = self._instance.context - - batch_name = os.path.basename(self._instance.data["source"]) - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - dln_job_info.Name = self._instance.data["name"] - dln_job_info.BatchName = batch_name - dln_job_info.Plugin = "AfterEffects" - dln_job_info.UserName = context.data.get( - "deadlineUser", getpass.getuser()) - # Deadline requires integers in frame range - frame_range = "{}-{}".format( - int(round(self._instance.data["frameStart"])), - int(round(self._instance.data["frameEnd"]))) - dln_job_info.Frames = frame_range - - dln_job_info.Priority = self.priority - dln_job_info.Pool = self._instance.data.get("primaryPool") - dln_job_info.SecondaryPool = self._instance.data.get("secondaryPool") - dln_job_info.Group = self.group - dln_job_info.Department = self.department - dln_job_info.ChunkSize = self.chunk_size - dln_job_info.OutputFilename += \ - os.path.basename(self._instance.data["expectedFiles"][0]) - dln_job_info.OutputDirectory += \ - os.path.dirname(self._instance.data["expectedFiles"][0]) - dln_job_info.JobDelay = "00:00:00" - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - for key in keys: - value = environment.get(key) - if value: - dln_job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - dln_job_info.add_render_job_env_var() - - return dln_job_info - - def get_plugin_info(self): - deadline_plugin_info = DeadlinePluginInfo() - - render_path = self._instance.data["expectedFiles"][0] - - file_name, frame = list(collect_frames([render_path]).items())[0] - if frame: - # replace frame ('000001') with Deadline's required '[#######]' - # expects filename in format project_folder_product_version.FRAME.ext - render_dir = os.path.dirname(render_path) - file_name = os.path.basename(render_path) - hashed = '[{}]'.format(len(frame) * "#") - file_name = file_name.replace(frame, hashed) - render_path = os.path.join(render_dir, file_name) - - deadline_plugin_info.Comp = self._instance.data["comp_name"] - deadline_plugin_info.Version = self._instance.data["app_version"] - # must be here because of DL AE plugin - # added override of multiprocess by env var, if shouldn't be used for - # some app variant use MULTIPROCESS:false in Settings, default is True - env_multi = env_value_to_bool("MULTIPROCESS", default=True) - deadline_plugin_info.MultiProcess = env_multi and self.multiprocess - deadline_plugin_info.SceneFile = self.scene_path - deadline_plugin_info.Output = render_path.replace("\\", "/") - - return attr.asdict(deadline_plugin_info) - - def from_published_scene(self): - """ Do not overwrite expected files. - - Use published is set to True, so rendering will be triggered - from published scene (in 'publish' folder). Default implementation - of abstract class renames expected (eg. rendered) files accordingly - which is not needed here. - """ - return super().from_published_scene(False) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py deleted file mode 100644 index 073de909b4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_blender_deadline.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submitting render job to Deadline.""" - -import os -import getpass -import attr -from datetime import datetime - -from ayon_core.lib import ( - BoolDef, - NumberDef, - TextDef, - is_in_tests, -) -from ayon_core.pipeline.publish import AYONPyblishPluginMixin -from ayon_core.pipeline.farm.tools import iter_expected_files - -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class BlenderPluginInfo(): - SceneFile = attr.ib(default=None) # Input - Version = attr.ib(default=None) # Mandatory for Deadline - SaveFile = attr.ib(default=True) - - -class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin): - label = "Submit Render to Deadline" - hosts = ["blender"] - families = ["render"] - settings_category = "deadline" - - use_published = True - priority = 50 - chunk_size = 1 - jobInfo = {} - pluginInfo = {} - group = None - job_delay = "00:00:00:00" - - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="Blender") - - job_info.update(self.jobInfo) - - instance = self._instance - context = instance.context - - # Always use the original work file name for the Job name even when - # rendering is done from the published Work File. The original work - # file name is clearer because it can also have subversion strings, - # etc. which are stripped for the published file. - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - - if is_in_tests(): - src_filename += datetime.now().strftime("%d%m%Y%H%M%S") - - job_info.Name = f"{src_filename} - {instance.name}" - job_info.BatchName = src_filename - instance.data.get("blenderRenderPlugin", "Blender") - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - - # Deadline requires integers in frame range - frames = "{start}-{end}x{step}".format( - start=int(instance.data["frameStartHandle"]), - end=int(instance.data["frameEndHandle"]), - step=int(instance.data["byFrameStep"]), - ) - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.Comment = instance.data.get("comment") - - if self.group != "none" and self.group: - job_info.Group = self.group - - attr_values = self.get_attr_values_from_data(instance.data) - render_globals = instance.data.setdefault("renderGlobals", {}) - machine_list = attr_values.get("machineList", "") - if machine_list: - if attr_values.get("whitelist", True): - machine_list_key = "Whitelist" - else: - machine_list_key = "Blacklist" - render_globals[machine_list_key] = machine_list - - job_info.ChunkSize = attr_values.get("chunkSize", self.chunk_size) - job_info.Priority = attr_values.get("priority", self.priority) - job_info.ScheduledType = "Once" - job_info.JobDelay = attr_values.get("job_delay", self.job_delay) - - # Add options from RenderGlobals - render_globals = instance.data.get("renderGlobals", {}) - job_info.update(render_globals) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - - # to recognize job from PYPE for turning Event On/Off - job_info.add_render_job_env_var() - job_info.EnvironmentKeyValue["AYON_LOG_NO_COLORS"] = "1" - - # Adding file dependencies. - if self.asset_dependencies: - dependencies = instance.context.data["fileDependencies"] - for dependency in dependencies: - job_info.AssetDependency += dependency - - # Add list of expected files to job - # --------------------------------- - exp = instance.data.get("expectedFiles") - for filepath in iter_expected_files(exp): - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - - def get_plugin_info(self): - # Not all hosts can import this module. - import bpy - - plugin_info = BlenderPluginInfo( - SceneFile=self.scene_path, - Version=bpy.app.version_string, - SaveFile=True, - ) - - plugin_payload = attr.asdict(plugin_info) - - # Patching with pluginInfo from settings - for key, value in self.pluginInfo.items(): - plugin_payload[key] = value - - return plugin_payload - - def process_submission(self, auth=None): - instance = self._instance - - expected_files = instance.data["expectedFiles"] - if not expected_files: - raise RuntimeError("No Render Elements found!") - - first_file = next(iter_expected_files(expected_files)) - output_dir = os.path.dirname(first_file) - instance.data["outputDir"] = output_dir - instance.data["toBeRenderedOn"] = "deadline" - - payload = self.assemble_payload() - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - return self.submit(payload, auth=auth, verify=verify) - - def from_published_scene(self): - """ - This is needed to set the correct path for the json metadata. Because - the rendering path is set in the blend file during the collection, - and the path is adjusted to use the published scene, this ensures that - the metadata and the rendered files are in the same location. - """ - return super().from_published_scene(False) - - @classmethod - def get_attribute_defs(cls): - defs = super(BlenderSubmitDeadline, cls).get_attribute_defs() - defs.extend([ - BoolDef("use_published", - default=cls.use_published, - label="Use Published Scene"), - - NumberDef("priority", - minimum=1, - maximum=250, - decimals=0, - default=cls.priority, - label="Priority"), - - NumberDef("chunkSize", - minimum=1, - maximum=50, - decimals=0, - default=cls.chunk_size, - label="Frame Per Task"), - - TextDef("group", - default=cls.group, - label="Group Name"), - - TextDef("job_delay", - default=cls.job_delay, - label="Job Delay", - placeholder="dd:hh:mm:ss", - tooltip="Delay the job by the specified amount of time. " - "Timecode: dd:hh:mm:ss."), - ]) - - return defs diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py deleted file mode 100644 index e9313e3f2f..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_celaction_deadline.py +++ /dev/null @@ -1,271 +0,0 @@ -import os -import re -import json -import getpass -import pyblish.api - -from ayon_deadline.abstract_submit_deadline import requests_post - - -class CelactionSubmitDeadline(pyblish.api.InstancePlugin): - """Submit CelAction2D scene to Deadline - - Renders are submitted to a Deadline Web Service. - - """ - - label = "Submit CelAction to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["celaction"] - families = ["render.farm"] - settings_category = "deadline" - - deadline_department = "" - deadline_priority = 50 - deadline_pool = "" - deadline_pool_secondary = "" - deadline_group = "" - deadline_chunk_size = 1 - deadline_job_delay = "00:00:08:00" - - def process(self, instance): - - context = instance.context - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - self.deadline_url = "{}/api/jobs".format(deadline_url) - self._comment = instance.data["comment"] - self._deadline_user = context.data.get( - "deadlineUser", getpass.getuser()) - self._frame_start = int(instance.data["frameStart"]) - self._frame_end = int(instance.data["frameEnd"]) - - # get output path - render_path = instance.data['path'] - script_path = context.data["currentFile"] - - response = self.payload_submit(instance, - script_path, - render_path - ) - # Store output dir for unified publisher (filesequence) - instance.data["deadlineSubmissionJob"] = response.json() - - instance.data["outputDir"] = os.path.dirname( - render_path).replace("\\", "/") - - instance.data["publishJobState"] = "Suspended" - - # adding 2d render specific family for version identification in Loader - instance.data["families"] = ["render2d"] - - def payload_submit(self, - instance, - script_path, - render_path - ): - resolution_width = instance.data["resolutionWidth"] - resolution_height = instance.data["resolutionHeight"] - render_dir = os.path.normpath(os.path.dirname(render_path)) - render_path = os.path.normpath(render_path) - script_name = os.path.basename(script_path) - - anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template_item( - "publish", "default", "path" - ) - for item in instance.context: - if "workfile" in item.data["productType"]: - msg = "Workfile (scene) must be published along" - assert item.data["publish"] is True, msg - - template_data = item.data.get("anatomyData") - rep = item.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - template_filled = publish_template.format_strict( - template_data - ) - script_path = os.path.normpath(template_filled) - - self.log.info( - "Using published scene for render {}".format(script_path) - ) - - jobname = "%s - %s" % (script_name, instance.name) - - output_filename_0 = self.preview_fname(render_path) - - try: - # Ensure render folder exists - os.makedirs(render_dir) - except OSError: - pass - - # define chunk and priority - chunk_size = instance.context.data.get("chunk") - if not chunk_size: - chunk_size = self.deadline_chunk_size - - # search for %02d pattern in name, and padding number - search_results = re.search(r"(%0)(\d)(d)[._]", render_path).groups() - split_patern = "".join(search_results) - padding_number = int(search_results[1]) - - args = [ - f"{script_path}", - "-a", - "-16", - "-s ", - "-e ", - f"-d {render_dir}", - f"-x {resolution_width}", - f"-y {resolution_height}", - f"-r {render_path.replace(split_patern, '')}", - f"-= AbsoluteFrameNumber=on -= PadDigits={padding_number}", - "-= ClearAttachment=on", - ] - - payload = { - "JobInfo": { - # Job name, as seen in Monitor - "Name": jobname, - - # plugin definition - "Plugin": "CelAction", - - # Top-level group name - "BatchName": script_name, - - # Arbitrary username, for visualisation in Monitor - "UserName": self._deadline_user, - - "Department": self.deadline_department, - "Priority": self.deadline_priority, - - "Group": self.deadline_group, - "Pool": self.deadline_pool, - "SecondaryPool": self.deadline_pool_secondary, - "ChunkSize": chunk_size, - - "Frames": f"{self._frame_start}-{self._frame_end}", - "Comment": self._comment, - - # Optional, enable double-click to preview rendered - # frames from Deadline Monitor - "OutputFilename0": output_filename_0.replace("\\", "/"), - - # # Asset dependency to wait for at least - # the scene file to sync. - # "AssetDependency0": script_path - "ScheduledType": "Once", - "JobDelay": self.deadline_job_delay - }, - "PluginInfo": { - # Input - "SceneFile": script_path, - - # Output directory - "OutputFilePath": render_dir.replace("\\", "/"), - - # Plugin attributes - "StartupDirectory": "", - "Arguments": " ".join(args), - - # Resolve relative references - "ProjectPath": script_path, - "AWSAssetFile0": render_path, - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - plugin = payload["JobInfo"]["Plugin"] - self.log.debug("using render plugin : {}".format(plugin)) - - self.log.debug("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # adding expectied files to instance.data - self.expected_files(instance, render_path) - self.log.debug("__ expectedFiles: `{}`".format( - instance.data["expectedFiles"])) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post(self.deadline_url, json=payload, - auth=auth, - verify=verify) - - if not response.ok: - self.log.error( - "Submission failed! [{}] {}".format( - response.status_code, response.content)) - self.log.debug(payload) - raise SystemExit(response.text) - - return response - - def preflight_check(self, instance): - """Ensure the startFrame, endFrame and byFrameStep are integers""" - - for key in ("frameStart", "frameEnd"): - value = instance.data[key] - - if int(value) == value: - continue - - self.log.warning( - "%f=%d was rounded off to nearest integer" - % (value, int(value)) - ) - - def preview_fname(self, path): - """Return output file path with #### for padding. - - Deadline requires the path to be formatted with # in place of numbers. - For example `/path/to/render.####.png` - - Args: - path (str): path to rendered images - - Returns: - str - - """ - self.log.debug("_ path: `{}`".format(path)) - if "%" in path: - search_results = re.search(r"[._](%0)(\d)(d)[._]", path).groups() - split_patern = "".join(search_results) - split_path = path.split(split_patern) - hashes = "#" * int(search_results[1]) - return "".join([split_path[0], hashes, split_path[-1]]) - - self.log.debug("_ path: `{}`".format(path)) - return path - - def expected_files(self, instance, filepath): - """ Create expected files in instance data - """ - if not instance.data.get("expectedFiles"): - instance.data["expectedFiles"] = [] - - dirpath = os.path.dirname(filepath) - filename = os.path.basename(filepath) - - if "#" in filename: - pparts = filename.split("#") - padding = "%0{}d".format(len(pparts) - 1) - filename = pparts[0] + padding + pparts[-1] - - if "%" not in filename: - instance.data["expectedFiles"].append(filepath) - return - - for i in range(self._frame_start, (self._frame_end + 1)): - instance.data["expectedFiles"].append( - os.path.join(dirpath, (filename % i)).replace("\\", "/") - ) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py deleted file mode 100644 index bf9df40edc..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_fusion_deadline.py +++ /dev/null @@ -1,253 +0,0 @@ -import os -import json -import getpass - -import pyblish.api - -from ayon_deadline.abstract_submit_deadline import requests_post -from ayon_core.pipeline.publish import ( - AYONPyblishPluginMixin -) -from ayon_core.lib import NumberDef - - -class FusionSubmitDeadline( - pyblish.api.InstancePlugin, - AYONPyblishPluginMixin -): - """Submit current Comp to Deadline - - Renders are submitted to a Deadline Web Service as - supplied via settings key "DEADLINE_REST_URL". - - """ - - label = "Submit Fusion to Deadline" - order = pyblish.api.IntegratorOrder - hosts = ["fusion"] - families = ["render"] - targets = ["local"] - settings_category = "deadline" - - # presets - plugin = None - - priority = 50 - chunk_size = 1 - concurrent_tasks = 1 - group = "" - - @classmethod - def get_attribute_defs(cls): - return [ - NumberDef( - "priority", - label="Priority", - default=cls.priority, - decimals=0 - ), - NumberDef( - "chunk", - label="Frames Per Task", - default=cls.chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - NumberDef( - "concurrency", - label="Concurrency", - default=cls.concurrent_tasks, - decimals=0, - minimum=1, - maximum=10 - ) - ] - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - attribute_values = self.get_attr_values_from_data( - instance.data) - - context = instance.context - - key = "__hasRun{}".format(self.__class__.__name__) - if context.data.get(key, False): - return - else: - context.data[key] = True - - from ayon_fusion.api.lib import get_frame_path - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - # Collect all saver instances in context that are to be rendered - saver_instances = [] - for inst in context: - if inst.data["productType"] != "render": - # Allow only saver family instances - continue - - if not inst.data.get("publish", True): - # Skip inactive instances - continue - - self.log.debug(inst.data["name"]) - saver_instances.append(inst) - - if not saver_instances: - raise RuntimeError("No instances found for Deadline submission") - - comment = instance.data.get("comment", "") - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - - script_path = context.data["currentFile"] - - anatomy = instance.context.data["anatomy"] - publish_template = anatomy.get_template_item( - "publish", "default", "path" - ) - for item in context: - if "workfile" in item.data["families"]: - msg = "Workfile (scene) must be published along" - assert item.data["publish"] is True, msg - - template_data = item.data.get("anatomyData") - rep = item.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - template_filled = publish_template.format_strict( - template_data - ) - script_path = os.path.normpath(template_filled) - - self.log.info( - "Using published scene for render {}".format(script_path) - ) - - filename = os.path.basename(script_path) - - # Documentation for keys available at: - # https://docs.thinkboxsoftware.com - # /products/deadline/8.0/1_User%20Manual/manual - # /manual-submission.html#job-info-file-options - payload = { - "JobInfo": { - # Top-level group name - "BatchName": filename, - - # Asset dependency to wait for at least the scene file to sync. - "AssetDependency0": script_path, - - # Job name, as seen in Monitor - "Name": filename, - - "Priority": attribute_values.get( - "priority", self.priority), - "ChunkSize": attribute_values.get( - "chunk", self.chunk_size), - "ConcurrentTasks": attribute_values.get( - "concurrency", - self.concurrent_tasks - ), - - # User, as seen in Monitor - "UserName": deadline_user, - - "Pool": instance.data.get("primaryPool"), - "SecondaryPool": instance.data.get("secondaryPool"), - "Group": self.group, - - "Plugin": self.plugin, - "Frames": "{start}-{end}".format( - start=int(instance.data["frameStartHandle"]), - end=int(instance.data["frameEndHandle"]) - ), - - "Comment": comment, - }, - "PluginInfo": { - # Input - "FlowFile": script_path, - - # Mandatory for Deadline - "Version": str(instance.data["app_version"]), - - # Render in high quality - "HighQuality": True, - - # Whether saver output should be checked after rendering - # is complete - "CheckOutput": True, - - # Proxy: higher numbers smaller images for faster test renders - # 1 = no proxy quality - "Proxy": 1 - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Enable going to rendered frames from Deadline Monitor - for index, instance in enumerate(saver_instances): - head, padding, tail = get_frame_path( - instance.data["expectedFiles"][0] - ) - path = "{}{}{}".format(head, "#" * padding, tail) - folder, filename = os.path.split(path) - payload["JobInfo"]["OutputDirectory%d" % index] = folder - payload["JobInfo"]["OutputFilename%d" % index] = filename - - # Include critical variables with submission - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - "AYON_IN_TESTS", - "AYON_BUNDLE_NAME", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - # to recognize render jobs - environment["AYON_RENDER_JOB"] = "1" - - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - self.log.debug("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(deadline_url) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post(url, json=payload, auth=auth, verify=verify) - if not response.ok: - raise Exception(response.text) - - # Store the response for dependent job submission plug-ins - for instance in saver_instances: - instance.data["deadlineSubmissionJob"] = response.json() diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py deleted file mode 100644 index bc91483c4f..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_harmony_deadline.py +++ /dev/null @@ -1,420 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submitting render job to Deadline.""" -import os -from pathlib import Path -from collections import OrderedDict -from zipfile import ZipFile, is_zipfile -import re -from datetime import datetime - -import attr -import pyblish.api - -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo -from ayon_core.lib import is_in_tests - - -class _ZipFile(ZipFile): - """Extended check for windows invalid characters.""" - - # this is extending default zipfile table for few invalid characters - # that can come from Mac - _windows_illegal_characters = ":<>|\"?*\r\n\x00" - _windows_illegal_name_trans_table = str.maketrans( - _windows_illegal_characters, - "_" * len(_windows_illegal_characters) - ) - - -@attr.s -class PluginInfo(object): - """Plugin info structure for Harmony Deadline plugin.""" - - SceneFile = attr.ib() - # Harmony version - Version = attr.ib() - - Camera = attr.ib(default="") - FieldOfView = attr.ib(default=41.11) - IsDatabase = attr.ib(default=False) - ResolutionX = attr.ib(default=1920) - ResolutionY = attr.ib(default=1080) - - # Resolution name preset, default - UsingResPreset = attr.ib(default=False) - ResolutionName = attr.ib(default="HDTV_1080p24") - - PreRenderInlineScript = attr.ib(default=None) - - # -------------------------------------------------- - _outputNode = attr.ib(factory=list) - - @property - def OutputNode(self): # noqa: N802 - """Return all output nodes formatted for Deadline. - - Returns: - dict: as `{'Output0Node', 'Top/renderFarmDefault'}` - - """ - out = {} - for index, v in enumerate(self._outputNode): - out["Output{}Node".format(index)] = v - return out - - @OutputNode.setter - def OutputNode(self, val): # noqa: N802 - self._outputNode.append(val) - - # -------------------------------------------------- - _outputType = attr.ib(factory=list) - - @property - def OutputType(self): # noqa: N802 - """Return output nodes type formatted for Deadline. - - Returns: - dict: as `{'Output0Type', 'Image'}` - - """ - out = {} - for index, v in enumerate(self._outputType): - out["Output{}Type".format(index)] = v - return out - - @OutputType.setter - def OutputType(self, val): # noqa: N802 - self._outputType.append(val) - - # -------------------------------------------------- - _outputLeadingZero = attr.ib(factory=list) - - @property - def OutputLeadingZero(self): # noqa: N802 - """Return output nodes type formatted for Deadline. - - Returns: - dict: as `{'Output0LeadingZero', '3'}` - - """ - out = {} - for index, v in enumerate(self._outputLeadingZero): - out["Output{}LeadingZero".format(index)] = v - return out - - @OutputLeadingZero.setter - def OutputLeadingZero(self, val): # noqa: N802 - self._outputLeadingZero.append(val) - - # -------------------------------------------------- - _outputFormat = attr.ib(factory=list) - - @property - def OutputFormat(self): # noqa: N802 - """Return output nodes format formatted for Deadline. - - Returns: - dict: as `{'Output0Type', 'PNG4'}` - - """ - out = {} - for index, v in enumerate(self._outputFormat): - out["Output{}Format".format(index)] = v - return out - - @OutputFormat.setter - def OutputFormat(self, val): # noqa: N802 - self._outputFormat.append(val) - - # -------------------------------------------------- - _outputStartFrame = attr.ib(factory=list) - - @property - def OutputStartFrame(self): # noqa: N802 - """Return start frame for output nodes formatted for Deadline. - - Returns: - dict: as `{'Output0StartFrame', '1'}` - - """ - out = {} - for index, v in enumerate(self._outputStartFrame): - out["Output{}StartFrame".format(index)] = v - return out - - @OutputStartFrame.setter - def OutputStartFrame(self, val): # noqa: N802 - self._outputStartFrame.append(val) - - # -------------------------------------------------- - _outputPath = attr.ib(factory=list) - - @property - def OutputPath(self): # noqa: N802 - """Return output paths for nodes formatted for Deadline. - - Returns: - dict: as `{'Output0Path', '/output/path'}` - - """ - out = {} - for index, v in enumerate(self._outputPath): - out["Output{}Path".format(index)] = v - return out - - @OutputPath.setter - def OutputPath(self, val): # noqa: N802 - self._outputPath.append(val) - - def set_output(self, node, image_format, output, - output_type="Image", zeros=3, start_frame=1): - """Helper to set output. - - This should be used instead of setting properties individually - as so index remain consistent. - - Args: - node (str): harmony write node name - image_format (str): format of output (PNG4, TIF, ...) - output (str): output path - output_type (str, optional): "Image" or "Movie" (not supported). - zeros (int, optional): Leading zeros (for 0001 = 3) - start_frame (int, optional): Sequence offset. - - """ - - self.OutputNode = node - self.OutputFormat = image_format - self.OutputPath = output - self.OutputType = output_type - self.OutputLeadingZero = zeros - self.OutputStartFrame = start_frame - - def serialize(self): - """Return all data serialized as dictionary. - - Returns: - OrderedDict: all serialized data. - - """ - def filter_data(a, v): - if a.name.startswith("_"): - return False - if v is None: - return False - return True - - serialized = attr.asdict( - self, dict_factory=OrderedDict, filter=filter_data) - serialized.update(self.OutputNode) - serialized.update(self.OutputFormat) - serialized.update(self.OutputPath) - serialized.update(self.OutputType) - serialized.update(self.OutputLeadingZero) - serialized.update(self.OutputStartFrame) - - return serialized - - -class HarmonySubmitDeadline( - abstract_submit_deadline.AbstractSubmitDeadline -): - """Submit render write of Harmony scene to Deadline. - - Renders are submitted to a Deadline Web Service as - supplied via the environment variable ``DEADLINE_REST_URL``. - - Note: - If Deadline configuration is not detected, this plugin will - be disabled. - - Attributes: - use_published (bool): Use published scene to render instead of the - one in work area. - - """ - - label = "Submit to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["harmony"] - families = ["render.farm"] - targets = ["local"] - settings_category = "deadline" - - optional = True - use_published = False - priority = 50 - chunk_size = 1000000 - group = "none" - department = "" - - def get_job_info(self): - job_info = DeadlineJobInfo("Harmony") - job_info.Name = self._instance.data["name"] - job_info.Plugin = "HarmonyAYON" - job_info.Frames = "{}-{}".format( - self._instance.data["frameStartHandle"], - self._instance.data["frameEndHandle"] - ) - # for now, get those from presets. Later on it should be - # configurable in Harmony UI directly. - job_info.Priority = self.priority - job_info.Pool = self._instance.data.get("primaryPool") - job_info.SecondaryPool = self._instance.data.get("secondaryPool") - job_info.ChunkSize = self.chunk_size - batch_name = os.path.basename(self._instance.data["source"]) - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - job_info.BatchName = batch_name - job_info.Department = self.department - job_info.Group = self.group - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS" - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - for key in keys: - value = environment.get(key) - if value: - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - - return job_info - - def _unzip_scene_file(self, published_scene: Path) -> Path: - """Unzip scene zip file to its directory. - - Unzip scene file (if it is zip file) to its current directory and - return path to xstage file there. Xstage file is determined by its - name. - - Args: - published_scene (Path): path to zip file. - - Returns: - Path: The path to unzipped xstage. - """ - # if not zip, bail out. - if "zip" not in published_scene.suffix or not is_zipfile( - published_scene.as_posix() - ): - self.log.error("Published scene is not in zip.") - self.log.error(published_scene) - raise AssertionError("invalid scene format") - - xstage_path = ( - published_scene.parent - / published_scene.stem - / f"{published_scene.stem}.xstage" - ) - unzip_dir = (published_scene.parent / published_scene.stem) - with _ZipFile(published_scene, "r") as zip_ref: - # UNC path (//?/) added to minimalize risk with extracting - # to large file paths - zip_ref.extractall("//?/" + str(unzip_dir.as_posix())) - - # find any xstage files in directory, prefer the one with the same name - # as directory (plus extension) - xstage_files = [] - for scene in unzip_dir.iterdir(): - if scene.suffix == ".xstage": - xstage_files.append(scene) - - # there must be at least one (but maybe not more?) xstage file - if not xstage_files: - self.log.error("No xstage files found in zip") - raise AssertionError("Invalid scene archive") - - ideal_scene = False - # find the one with the same name as zip. In case there can be more - # then one xtage file. - for scene in xstage_files: - # if /foo/bar/baz.zip == /foo/bar/baz/baz.xstage - # ^^^ ^^^ - if scene.stem == published_scene.stem: - xstage_path = scene - ideal_scene = True - - # but sometimes xstage file has different name then zip - in that case - # use that one. - if not ideal_scene: - xstage_path = xstage_files[0] - return xstage_path - - def get_plugin_info(self): - # this is path to published scene workfile _ZIP_. Before - # rendering, we need to unzip it. - published_scene = Path( - self.from_published_scene(False)) - self.log.debug(f"Processing {published_scene.as_posix()}") - xstage_path = self._unzip_scene_file(published_scene) - render_path = xstage_path.parent / "renders" - - # for submit_publish job to create .json file in - self._instance.data["outputDir"] = render_path - new_expected_files = [] - render_path_str = str(render_path.as_posix()) - for file in self._instance.data["expectedFiles"]: - _file = str(Path(file).as_posix()) - expected_dir_str = os.path.dirname(_file) - new_expected_files.append( - _file.replace(expected_dir_str, render_path_str) - ) - audio_file = self._instance.data.get("audioFile") - if audio_file: - abs_path = xstage_path.parent / audio_file - self._instance.context.data["audioFile"] = str(abs_path) - - self._instance.data["source"] = str(published_scene.as_posix()) - self._instance.data["expectedFiles"] = new_expected_files - harmony_plugin_info = PluginInfo( - SceneFile=xstage_path.as_posix(), - Version=( - self._instance.context.data["harmonyVersion"].split(".")[0]), - FieldOfView=self._instance.context.data["FOV"], - ResolutionX=self._instance.data["resolutionWidth"], - ResolutionY=self._instance.data["resolutionHeight"] - ) - - pattern = '[0]{' + str(self._instance.data["leadingZeros"]) + \ - '}1\.[a-zA-Z]{3}' - render_prefix = re.sub(pattern, '', - self._instance.data["expectedFiles"][0]) - harmony_plugin_info.set_output( - self._instance.data["setMembers"][0], - self._instance.data["outputFormat"], - render_prefix, - self._instance.data["outputType"], - self._instance.data["leadingZeros"], - self._instance.data["outputStartFrame"] - ) - - all_write_nodes = self._instance.context.data["all_write_nodes"] - disable_nodes = [] - for node in all_write_nodes: - # disable all other write nodes - if node != self._instance.data["setMembers"][0]: - disable_nodes.append("node.setEnable('{}', false)" - .format(node)) - harmony_plugin_info.PreRenderInlineScript = ';'.join(disable_nodes) - - return harmony_plugin_info.serialize() diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py deleted file mode 100644 index ac9ad570c3..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_cache_deadline.py +++ /dev/null @@ -1,181 +0,0 @@ -import os -import getpass -from datetime import datetime - -import attr -import pyblish.api -from ayon_core.lib import ( - TextDef, - NumberDef, - is_in_tests, -) -from ayon_core.pipeline import ( - AYONPyblishPluginMixin -) -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class HoudiniPluginInfo(object): - Build = attr.ib(default=None) - IgnoreInputs = attr.ib(default=True) - ScriptJob = attr.ib(default=True) - SceneFile = attr.ib(default=None) # Input - SaveFile = attr.ib(default=True) - ScriptFilename = attr.ib(default=None) - OutputDriver = attr.ib(default=None) - Version = attr.ib(default=None) # Mandatory for Deadline - ProjectPath = attr.ib(default=None) - - -class HoudiniCacheSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # noqa - AYONPyblishPluginMixin): - """Submit Houdini scene to perform a local publish in Deadline. - - Publishing in Deadline can be helpful for scenes that publish very slow. - This way it can process in the background on another machine without the - Artist having to wait for the publish to finish on their local machine. - """ - - label = "Submit Scene to Deadline" - order = pyblish.api.IntegratorOrder - hosts = ["houdini"] - families = ["publish.hou"] - targets = ["local"] - settings_category = "deadline" - - priority = 50 - chunk_size = 999999 - group = None - jobInfo = {} - pluginInfo = {} - - - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="Houdini") - - job_info.update(self.jobInfo) - instance = self._instance - context = instance.context - assert all( - result["success"] for result in context.data["results"] - ), "Errors found, aborting integration.." - - project_name = instance.context.data["projectName"] - filepath = context.data["currentFile"] - scenename = os.path.basename(filepath) - job_name = "{scene} - {instance} [PUBLISH]".format( - scene=scenename, instance=instance.name) - batch_name = "{code} - {scene}".format(code=project_name, - scene=scenename) - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - - job_info.Name = job_name - job_info.BatchName = batch_name - job_info.Plugin = instance.data["plugin"] - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - rop_node = self.get_rop_node(instance) - if rop_node.type().name() != "alembic": - frames = "{start}-{end}x{step}".format( - start=int(instance.data["frameStart"]), - end=int(instance.data["frameEnd"]), - step=int(instance.data["byFrameStep"]), - ) - - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - - attr_values = self.get_attr_values_from_data(instance.data) - - job_info.ChunkSize = instance.data.get("chunk_size", self.chunk_size) - job_info.Comment = context.data.get("comment") - job_info.Priority = attr_values.get("priority", self.priority) - job_info.Group = attr_values.get("group", self.group) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - # to recognize render jobs - job_info.add_render_job_env_var() - - return job_info - - def get_plugin_info(self): - # Not all hosts can import this module. - import hou - - instance = self._instance - version = hou.applicationVersionString() - version = ".".join(version.split(".")[:2]) - rop = self.get_rop_node(instance) - plugin_info = HoudiniPluginInfo( - Build=None, - IgnoreInputs=True, - ScriptJob=True, - SceneFile=self.scene_path, - SaveFile=True, - OutputDriver=rop.path(), - Version=version, - ProjectPath=os.path.dirname(self.scene_path) - ) - - plugin_payload = attr.asdict(plugin_info) - - return plugin_payload - - def process(self, instance): - super(HoudiniCacheSubmitDeadline, self).process(instance) - output_dir = os.path.dirname(instance.data["files"][0]) - instance.data["outputDir"] = output_dir - instance.data["toBeRenderedOn"] = "deadline" - - def get_rop_node(self, instance): - # Not all hosts can import this module. - import hou - - rop = instance.data.get("instance_node") - rop_node = hou.node(rop) - - return rop_node - - @classmethod - def get_attribute_defs(cls): - defs = super(HoudiniCacheSubmitDeadline, cls).get_attribute_defs() - defs.extend([ - NumberDef("priority", - minimum=1, - maximum=250, - decimals=0, - default=cls.priority, - label="Priority"), - TextDef("group", - default=cls.group, - label="Group Name"), - ]) - - return defs diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py deleted file mode 100644 index 7956108e77..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_houdini_render_deadline.py +++ /dev/null @@ -1,403 +0,0 @@ -import os -import attr -import getpass -from datetime import datetime - -import pyblish.api - -from ayon_core.pipeline import AYONPyblishPluginMixin -from ayon_core.lib import ( - is_in_tests, - TextDef, - NumberDef -) -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class DeadlinePluginInfo(): - SceneFile = attr.ib(default=None) - OutputDriver = attr.ib(default=None) - Version = attr.ib(default=None) - IgnoreInputs = attr.ib(default=True) - - -@attr.s -class ArnoldRenderDeadlinePluginInfo(): - InputFile = attr.ib(default=None) - Verbose = attr.ib(default=4) - - -@attr.s -class MantraRenderDeadlinePluginInfo(): - SceneFile = attr.ib(default=None) - Version = attr.ib(default=None) - - -@attr.s -class VrayRenderPluginInfo(): - InputFilename = attr.ib(default=None) - SeparateFilesPerFrame = attr.ib(default=True) - - -@attr.s -class RedshiftRenderPluginInfo(): - SceneFile = attr.ib(default=None) - # Use "1" as the default Redshift version just because it - # default fallback version in Deadline's Redshift plugin - # if no version was specified - Version = attr.ib(default="1") - - -@attr.s -class HuskStandalonePluginInfo(): - """Requires Deadline Husk Standalone Plugin. - See Deadline Plug-in: - https://github.com/BigRoy/HuskStandaloneSubmitter - Also see Husk options here: - https://www.sidefx.com/docs/houdini/ref/utils/husk.html - """ - SceneFile = attr.ib() - # TODO: Below parameters are only supported by custom version of the plugin - Renderer = attr.ib(default=None) - RenderSettings = attr.ib(default="/Render/rendersettings") - Purpose = attr.ib(default="geometry,render") - Complexity = attr.ib(default="veryhigh") - Snapshot = attr.ib(default=-1) - LogLevel = attr.ib(default="2") - PreRender = attr.ib(default="") - PreFrame = attr.ib(default="") - PostFrame = attr.ib(default="") - PostRender = attr.ib(default="") - RestartDelegate = attr.ib(default="") - Version = attr.ib(default="") - - -class HoudiniSubmitDeadline( - abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin -): - """Submit Render ROPs to Deadline. - - Renders are submitted to a Deadline Web Service as - supplied via the environment variable AVALON_DEADLINE. - - Target "local": - Even though this does *not* render locally this is seen as - a 'local' submission as it is the regular way of submitting - a Houdini render locally. - - """ - - label = "Submit Render to Deadline" - order = pyblish.api.IntegratorOrder - hosts = ["houdini"] - families = ["redshift_rop", - "arnold_rop", - "mantra_rop", - "karma_rop", - "vray_rop"] - targets = ["local"] - settings_category = "deadline" - use_published = True - - # presets - export_priority = 50 - export_chunk_size = 10 - export_group = "" - priority = 50 - chunk_size = 1 - group = "" - - @classmethod - def get_attribute_defs(cls): - return [ - NumberDef( - "priority", - label="Priority", - default=cls.priority, - decimals=0 - ), - NumberDef( - "chunk", - label="Frames Per Task", - default=cls.chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - TextDef( - "group", - default=cls.group, - label="Group Name" - ), - NumberDef( - "export_priority", - label="Export Priority", - default=cls.export_priority, - decimals=0 - ), - NumberDef( - "export_chunk", - label="Export Frames Per Task", - default=cls.export_chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - TextDef( - "export_group", - default=cls.export_group, - label="Export Group Name" - ), - ] - - def get_job_info(self, dependency_job_ids=None): - - instance = self._instance - context = instance.context - - attribute_values = self.get_attr_values_from_data(instance.data) - - # Whether Deadline render submission is being split in two - # (extract + render) - split_render_job = instance.data.get("splitRender") - - # If there's some dependency job ids we can assume this is a render job - # and not an export job - is_export_job = True - if dependency_job_ids: - is_export_job = False - - job_type = "[RENDER]" - if split_render_job and not is_export_job: - product_type = instance.data["productType"] - plugin = { - "usdrender": "HuskStandalone", - }.get(product_type) - if not plugin: - # Convert from product type to Deadline plugin name - # i.e., arnold_rop -> Arnold - plugin = product_type.replace("_rop", "").capitalize() - else: - plugin = "Houdini" - if split_render_job: - job_type = "[EXPORT IFD]" - - job_info = DeadlineJobInfo(Plugin=plugin) - - filepath = context.data["currentFile"] - filename = os.path.basename(filepath) - job_info.Name = "{} - {} {}".format(filename, instance.name, job_type) - job_info.BatchName = filename - - job_info.UserName = context.data.get( - "deadlineUser", getpass.getuser()) - - if is_in_tests(): - job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") - - # Deadline requires integers in frame range - start = instance.data["frameStartHandle"] - end = instance.data["frameEndHandle"] - frames = "{start}-{end}x{step}".format( - start=int(start), - end=int(end), - step=int(instance.data["byFrameStep"]), - ) - job_info.Frames = frames - - # Make sure we make job frame dependent so render tasks pick up a soon - # as export tasks are done - if split_render_job and not is_export_job: - job_info.IsFrameDependent = bool(instance.data.get( - "splitRenderFrameDependent", True)) - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - - if split_render_job and is_export_job: - job_info.Priority = attribute_values.get( - "export_priority", self.export_priority - ) - job_info.ChunkSize = attribute_values.get( - "export_chunk", self.export_chunk_size - ) - job_info.Group = self.export_group - else: - job_info.Priority = attribute_values.get( - "priority", self.priority - ) - job_info.ChunkSize = attribute_values.get( - "chunk", self.chunk_size - ) - job_info.Group = self.group - - # Apply render globals, like e.g. data from collect machine list - render_globals = instance.data.get("renderGlobals", {}) - if render_globals: - self.log.debug("Applying 'renderGlobals' to job info: %s", - render_globals) - job_info.update(render_globals) - - job_info.Comment = context.data.get("comment") - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if value: - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - - for i, filepath in enumerate(instance.data["files"]): - dirname = os.path.dirname(filepath) - fname = os.path.basename(filepath) - job_info.OutputDirectory += dirname.replace("\\", "/") - job_info.OutputFilename += fname - - # Add dependencies if given - if dependency_job_ids: - job_info.JobDependencies = ",".join(dependency_job_ids) - - return job_info - - def get_plugin_info(self, job_type=None): - # Not all hosts can import this module. - import hou - - instance = self._instance - context = instance.context - - hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] - - # Output driver to render - if job_type == "render": - product_type = instance.data.get("productType") - if product_type == "arnold_rop": - plugin_info = ArnoldRenderDeadlinePluginInfo( - InputFile=instance.data["ifdFile"] - ) - elif product_type == "mantra_rop": - plugin_info = MantraRenderDeadlinePluginInfo( - SceneFile=instance.data["ifdFile"], - Version=hou_major_minor, - ) - elif product_type == "vray_rop": - plugin_info = VrayRenderPluginInfo( - InputFilename=instance.data["ifdFile"], - ) - elif product_type == "redshift_rop": - plugin_info = RedshiftRenderPluginInfo( - SceneFile=instance.data["ifdFile"] - ) - # Note: To use different versions of Redshift on Deadline - # set the `REDSHIFT_VERSION` env variable in the Tools - # settings in the AYON Application plugin. You will also - # need to set that version in `Redshift.param` file - # of the Redshift Deadline plugin: - # [Redshift_Executable_*] - # where * is the version number. - if os.getenv("REDSHIFT_VERSION"): - plugin_info.Version = os.getenv("REDSHIFT_VERSION") - else: - self.log.warning(( - "REDSHIFT_VERSION env variable is not set" - " - using version configured in Deadline" - )) - - elif product_type == "usdrender": - plugin_info = self._get_husk_standalone_plugin_info( - instance, hou_major_minor) - - else: - self.log.error( - "Product type '%s' not supported yet to split render job", - product_type - ) - return - else: - driver = hou.node(instance.data["instance_node"]) - plugin_info = DeadlinePluginInfo( - SceneFile=context.data["currentFile"], - OutputDriver=driver.path(), - Version=hou_major_minor, - IgnoreInputs=True - ) - - return attr.asdict(plugin_info) - - def process(self, instance): - if not instance.data["farm"]: - self.log.debug("Render on farm is disabled. " - "Skipping deadline submission.") - return - - super(HoudiniSubmitDeadline, self).process(instance) - - # TODO: Avoid the need for this logic here, needed for submit publish - # Store output dir for unified publisher (filesequence) - output_dir = os.path.dirname(instance.data["files"][0]) - instance.data["outputDir"] = output_dir - - def _get_husk_standalone_plugin_info(self, instance, hou_major_minor): - # Not all hosts can import this module. - import hou - - # Supply additional parameters from the USD Render ROP - # to the Husk Standalone Render Plug-in - rop_node = hou.node(instance.data["instance_node"]) - snapshot_interval = -1 - if rop_node.evalParm("dosnapshot"): - snapshot_interval = rop_node.evalParm("snapshotinterval") - - restart_delegate = 0 - if rop_node.evalParm("husk_restartdelegate"): - restart_delegate = rop_node.evalParm("husk_restartdelegateframes") - - rendersettings = ( - rop_node.evalParm("rendersettings") - or "/Render/rendersettings" - ) - return HuskStandalonePluginInfo( - SceneFile=instance.data["ifdFile"], - Renderer=rop_node.evalParm("renderer"), - RenderSettings=rendersettings, - Purpose=rop_node.evalParm("husk_purpose"), - Complexity=rop_node.evalParm("husk_complexity"), - Snapshot=snapshot_interval, - PreRender=rop_node.evalParm("husk_prerender"), - PreFrame=rop_node.evalParm("husk_preframe"), - PostFrame=rop_node.evalParm("husk_postframe"), - PostRender=rop_node.evalParm("husk_postrender"), - RestartDelegate=restart_delegate, - Version=hou_major_minor - ) - - -class HoudiniSubmitDeadlineUsdRender(HoudiniSubmitDeadline): - # Do not use published workfile paths for USD Render ROP because the - # Export Job doesn't seem to occur using the published path either, so - # output paths then do not match the actual rendered paths - use_published = False - families = ["usdrender"] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py deleted file mode 100644 index 6a369eb001..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_max_deadline.py +++ /dev/null @@ -1,431 +0,0 @@ -import os -import getpass -import copy -import attr - -from ayon_core.lib import ( - TextDef, - BoolDef, - NumberDef, -) -from ayon_core.pipeline import ( - AYONPyblishPluginMixin -) -from ayon_core.pipeline.publish.lib import ( - replace_with_published_scene_path -) -from ayon_core.pipeline.publish import KnownPublishError -from ayon_max.api.lib import ( - get_current_renderer, - get_multipass_setting -) -from ayon_max.api.lib_rendersettings import RenderSettings -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -@attr.s -class MaxPluginInfo(object): - SceneFile = attr.ib(default=None) # Input - Version = attr.ib(default=None) # Mandatory for Deadline - SaveFile = attr.ib(default=True) - IgnoreInputs = attr.ib(default=True) - - -class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin): - - label = "Submit Render to Deadline" - hosts = ["max"] - families = ["maxrender"] - targets = ["local"] - settings_category = "deadline" - - use_published = True - priority = 50 - chunk_size = 1 - jobInfo = {} - pluginInfo = {} - group = None - - @classmethod - def apply_settings(cls, project_settings): - settings = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa - - # Take some defaults from settings - cls.use_published = settings.get("use_published", - cls.use_published) - cls.priority = settings.get("priority", - cls.priority) - cls.chuck_size = settings.get("chunk_size", cls.chunk_size) - cls.group = settings.get("group", cls.group) - # TODO: multiple camera instance, separate job infos - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="3dsmax") - - # todo: test whether this works for existing production cases - # where custom jobInfo was stored in the project settings - job_info.update(self.jobInfo) - - instance = self._instance - context = instance.context - # Always use the original work file name for the Job name even when - # rendering is done from the published Work File. The original work - # file name is clearer because it can also have subversion strings, - # etc. which are stripped for the published file. - - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - job_info.Name = "%s - %s" % (src_filename, instance.name) - job_info.BatchName = src_filename - job_info.Plugin = instance.data["plugin"] - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - job_info.EnableAutoTimeout = True - # Deadline requires integers in frame range - frames = "{start}-{end}".format( - start=int(instance.data["frameStart"]), - end=int(instance.data["frameEnd"]) - ) - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - - attr_values = self.get_attr_values_from_data(instance.data) - - job_info.ChunkSize = attr_values.get("chunkSize", 1) - job_info.Comment = context.data.get("comment") - job_info.Priority = attr_values.get("priority", self.priority) - job_info.Group = attr_values.get("group", self.group) - - # Add options from RenderGlobals - render_globals = instance.data.get("renderGlobals", {}) - job_info.update(render_globals) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_IN_TESTS", - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - job_info.EnvironmentKeyValue["AYON_LOG_NO_COLORS"] = "1" - - # Add list of expected files to job - # --------------------------------- - if not instance.data.get("multiCamera"): - exp = instance.data.get("expectedFiles") - for filepath in self._iter_expected_files(exp): - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - - def get_plugin_info(self): - instance = self._instance - - plugin_info = MaxPluginInfo( - SceneFile=self.scene_path, - Version=instance.data["maxversion"], - SaveFile=True, - IgnoreInputs=True - ) - - plugin_payload = attr.asdict(plugin_info) - - # Patching with pluginInfo from settings - for key, value in self.pluginInfo.items(): - plugin_payload[key] = value - - return plugin_payload - - def process_submission(self): - - instance = self._instance - filepath = instance.context.data["currentFile"] - - files = instance.data["expectedFiles"] - if not files: - raise KnownPublishError("No Render Elements found!") - first_file = next(self._iter_expected_files(files)) - output_dir = os.path.dirname(first_file) - instance.data["outputDir"] = output_dir - - filename = os.path.basename(filepath) - - payload_data = { - "filename": filename, - "dirname": output_dir - } - - self.log.debug("Submitting 3dsMax render..") - project_settings = instance.context.data["project_settings"] - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - if instance.data.get("multiCamera"): - self.log.debug("Submitting jobs for multiple cameras..") - payload = self._use_published_name_for_multiples( - payload_data, project_settings) - job_infos, plugin_infos = payload - for job_info, plugin_info in zip(job_infos, plugin_infos): - self.submit( - self.assemble_payload(job_info, plugin_info), - auth=auth, - verify=verify - ) - else: - payload = self._use_published_name(payload_data, project_settings) - job_info, plugin_info = payload - self.submit( - self.assemble_payload(job_info, plugin_info), - auth=auth, - verify=verify - ) - - def _use_published_name(self, data, project_settings): - # Not all hosts can import these modules. - from ayon_max.api.lib import ( - get_current_renderer, - get_multipass_setting - ) - from ayon_max.api.lib_rendersettings import RenderSettings - - instance = self._instance - job_info = copy.deepcopy(self.job_info) - plugin_info = copy.deepcopy(self.plugin_info) - plugin_data = {} - - multipass = get_multipass_setting(project_settings) - if multipass: - plugin_data["DisableMultipass"] = 0 - else: - plugin_data["DisableMultipass"] = 1 - - files = instance.data.get("expectedFiles") - if not files: - raise KnownPublishError("No render elements found") - first_file = next(self._iter_expected_files(files)) - old_output_dir = os.path.dirname(first_file) - output_beauty = RenderSettings().get_render_output(instance.name, - old_output_dir) - rgb_bname = os.path.basename(output_beauty) - dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" - beauty_name = beauty_name.replace("\\", "/") - plugin_data["RenderOutput"] = beauty_name - # as 3dsmax has version with different languages - plugin_data["Language"] = "ENU" - - renderer_class = get_current_renderer() - - renderer = str(renderer_class).split(":")[0] - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = RenderSettings().get_render_element() - for i, element in enumerate(render_elem_list): - elem_bname = os.path.basename(element) - new_elem = f"{dir}/{elem_bname}" - new_elem = new_elem.replace("/", "\\") - plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa - - if renderer == "Redshift_Renderer": - plugin_data["redshift_SeparateAovFiles"] = instance.data.get( - "separateAovFiles") - if instance.data["cameras"]: - camera = instance.data["cameras"][0] - plugin_info["Camera0"] = camera - plugin_info["Camera"] = camera - plugin_info["Camera1"] = camera - self.log.debug("plugin data:{}".format(plugin_data)) - plugin_info.update(plugin_data) - - return job_info, plugin_info - - def get_job_info_through_camera(self, camera): - """Get the job parameters for deadline submission when - multi-camera is enabled. - Args: - infos(dict): a dictionary with job info. - """ - instance = self._instance - context = instance.context - job_info = copy.deepcopy(self.job_info) - exp = instance.data.get("expectedFiles") - - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - job_info.Name = "%s - %s - %s" % ( - src_filename, instance.name, camera) - for filepath in self._iter_expected_files(exp): - if camera not in filepath: - continue - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - # set the output filepath with the relative camera - - def get_plugin_info_through_camera(self, camera): - """Get the plugin parameters for deadline submission when - multi-camera is enabled. - Args: - infos(dict): a dictionary with plugin info. - """ - instance = self._instance - # set the target camera - plugin_info = copy.deepcopy(self.plugin_info) - - plugin_data = {} - # set the output filepath with the relative camera - if instance.data.get("multiCamera"): - scene_filepath = instance.context.data["currentFile"] - scene_filename = os.path.basename(scene_filepath) - scene_directory = os.path.dirname(scene_filepath) - current_filename, ext = os.path.splitext(scene_filename) - camera_scene_name = f"{current_filename}_{camera}{ext}" - camera_scene_filepath = os.path.join( - scene_directory, f"_{current_filename}", camera_scene_name) - plugin_data["SceneFile"] = camera_scene_filepath - - files = instance.data.get("expectedFiles") - if not files: - raise KnownPublishError("No render elements found") - first_file = next(self._iter_expected_files(files)) - old_output_dir = os.path.dirname(first_file) - rgb_output = RenderSettings().get_batch_render_output(camera) # noqa - rgb_bname = os.path.basename(rgb_output) - dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" - beauty_name = beauty_name.replace("\\", "/") - plugin_info["RenderOutput"] = beauty_name - renderer_class = get_current_renderer() - - renderer = str(renderer_class).split(":")[0] - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = RenderSettings().get_batch_render_elements( - instance.name, old_output_dir, camera - ) - for i, element in enumerate(render_elem_list): - if camera in element: - elem_bname = os.path.basename(element) - new_elem = f"{dir}/{elem_bname}" - new_elem = new_elem.replace("/", "\\") - plugin_info["RenderElementOutputFilename%d" % i] = new_elem # noqa - - if camera: - # set the default camera and target camera - # (weird parameters from max) - plugin_data["Camera"] = camera - plugin_data["Camera1"] = camera - plugin_data["Camera0"] = None - - plugin_info.update(plugin_data) - return plugin_info - - def _use_published_name_for_multiples(self, data, project_settings): - """Process the parameters submission for deadline when - user enables multi-cameras option. - Args: - job_info_list (list): A list of multiple job infos - plugin_info_list (list): A list of multiple plugin infos - """ - job_info_list = [] - plugin_info_list = [] - instance = self._instance - cameras = instance.data.get("cameras", []) - plugin_data = {} - multipass = get_multipass_setting(project_settings) - if multipass: - plugin_data["DisableMultipass"] = 0 - else: - plugin_data["DisableMultipass"] = 1 - for cam in cameras: - job_info = self.get_job_info_through_camera(cam) - plugin_info = self.get_plugin_info_through_camera(cam) - plugin_info.update(plugin_data) - job_info_list.append(job_info) - plugin_info_list.append(plugin_info) - - return job_info_list, plugin_info_list - - def from_published_scene(self, replace_in_path=True): - instance = self._instance - if instance.data["renderer"] == "Redshift_Renderer": - self.log.debug("Using Redshift...published scene wont be used..") - replace_in_path = False - return replace_with_published_scene_path( - instance, replace_in_path) - - @staticmethod - def _iter_expected_files(exp): - if isinstance(exp[0], dict): - for _aov, files in exp[0].items(): - for file in files: - yield file - else: - for file in exp: - yield file - - @classmethod - def get_attribute_defs(cls): - defs = super(MaxSubmitDeadline, cls).get_attribute_defs() - defs.extend([ - BoolDef("use_published", - default=cls.use_published, - label="Use Published Scene"), - - NumberDef("priority", - minimum=1, - maximum=250, - decimals=0, - default=cls.priority, - label="Priority"), - - NumberDef("chunkSize", - minimum=1, - maximum=50, - decimals=0, - default=cls.chunk_size, - label="Frame Per Task"), - - TextDef("group", - default=cls.group, - label="Group Name"), - ]) - - return defs diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py deleted file mode 100644 index d50b0147d9..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_maya_deadline.py +++ /dev/null @@ -1,935 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submitting render job to Deadline. - -This module is taking care of submitting job from Maya to Deadline. It -creates job and set correct environments. Its behavior is controlled by -``DEADLINE_REST_URL`` environment variable - pointing to Deadline Web Service -and :data:`MayaSubmitDeadline.use_published` property telling Deadline to -use published scene workfile or not. - -If ``vrscene`` or ``assscene`` are detected in families, it will first -submit job to export these files and then dependent job to render them. - -Attributes: - payload_skeleton (dict): Skeleton payload data sent as job to Deadline. - Default values are for ``MayaBatch`` plugin. - -""" - -from __future__ import print_function -import os -import json -import getpass -import copy -import re -import hashlib -from datetime import datetime -import itertools -from collections import OrderedDict - -import attr - -from ayon_core.pipeline import ( - AYONPyblishPluginMixin -) -from ayon_core.lib import ( - BoolDef, - NumberDef, - TextDef, - EnumDef, - is_in_tests, -) -from ayon_maya.api.lib_rendersettings import RenderSettings -from ayon_maya.api.lib import get_attr_in_layer - -from ayon_core.pipeline.farm.tools import iter_expected_files - -from ayon_deadline import abstract_submit_deadline -from ayon_deadline.abstract_submit_deadline import DeadlineJobInfo - - -def _validate_deadline_bool_value(instance, attribute, value): - if not isinstance(value, (str, bool)): - raise TypeError( - "Attribute {} must be str or bool.".format(attribute)) - if value not in {"1", "0", True, False}: - raise ValueError( - ("Value of {} must be one of " - "'0', '1', True, False").format(attribute) - ) - - -@attr.s -class MayaPluginInfo(object): - SceneFile = attr.ib(default=None) # Input - OutputFilePath = attr.ib(default=None) # Output directory and filename - OutputFilePrefix = attr.ib(default=None) - Version = attr.ib(default=None) # Mandatory for Deadline - UsingRenderLayers = attr.ib(default=True) - RenderLayer = attr.ib(default=None) # Render only this layer - Renderer = attr.ib(default=None) - ProjectPath = attr.ib(default=None) # Resolve relative references - # Include all lights flag - RenderSetupIncludeLights = attr.ib( - default="1", validator=_validate_deadline_bool_value) - StrictErrorChecking = attr.ib(default=True) - - -@attr.s -class PythonPluginInfo(object): - ScriptFile = attr.ib() - Version = attr.ib(default="3.6") - Arguments = attr.ib(default=None) - SingleFrameOnly = attr.ib(default=None) - - -@attr.s -class VRayPluginInfo(object): - InputFilename = attr.ib(default=None) # Input - SeparateFilesPerFrame = attr.ib(default=None) - VRayEngine = attr.ib(default="V-Ray") - Width = attr.ib(default=None) - Height = attr.ib(default=None) # Mandatory for Deadline - OutputFilePath = attr.ib(default=True) - OutputFileName = attr.ib(default=None) # Render only this layer - - -@attr.s -class ArnoldPluginInfo(object): - ArnoldFile = attr.ib(default=None) - - -class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, - AYONPyblishPluginMixin): - - label = "Submit Render to Deadline" - hosts = ["maya"] - families = ["renderlayer"] - targets = ["local"] - settings_category = "deadline" - - tile_assembler_plugin = "OpenPypeTileAssembler" - priority = 50 - tile_priority = 50 - limit = [] # limit groups - jobInfo = {} - pluginInfo = {} - group = "none" - strict_error_checking = True - - @classmethod - def apply_settings(cls, project_settings): - settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa - - # Take some defaults from settings - cls.asset_dependencies = settings.get("asset_dependencies", - cls.asset_dependencies) - cls.import_reference = settings.get("import_reference", - cls.import_reference) - cls.use_published = settings.get("use_published", cls.use_published) - cls.priority = settings.get("priority", cls.priority) - cls.tile_priority = settings.get("tile_priority", cls.tile_priority) - cls.limit = settings.get("limit", cls.limit) - cls.group = settings.get("group", cls.group) - cls.strict_error_checking = settings.get("strict_error_checking", - cls.strict_error_checking) - job_info = settings.get("jobInfo") - if job_info: - job_info = json.loads(job_info) - plugin_info = settings.get("pluginInfo") - if plugin_info: - plugin_info = json.loads(plugin_info) - - cls.jobInfo = job_info or cls.jobInfo - cls.pluginInfo = plugin_info or cls.pluginInfo - - def get_job_info(self): - job_info = DeadlineJobInfo(Plugin="MayaBatch") - - # todo: test whether this works for existing production cases - # where custom jobInfo was stored in the project settings - job_info.update(self.jobInfo) - - instance = self._instance - context = instance.context - - # Always use the original work file name for the Job name even when - # rendering is done from the published Work File. The original work - # file name is clearer because it can also have subversion strings, - # etc. which are stripped for the published file. - src_filepath = context.data["currentFile"] - src_filename = os.path.basename(src_filepath) - - if is_in_tests(): - src_filename += datetime.now().strftime("%d%m%Y%H%M%S") - - job_info.Name = "%s - %s" % (src_filename, instance.name) - job_info.BatchName = src_filename - job_info.Plugin = instance.data.get("mayaRenderPlugin", "MayaBatch") - job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - - # Deadline requires integers in frame range - frames = "{start}-{end}x{step}".format( - start=int(instance.data["frameStartHandle"]), - end=int(instance.data["frameEndHandle"]), - step=int(instance.data["byFrameStep"]), - ) - job_info.Frames = frames - - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.Comment = context.data.get("comment") - job_info.Priority = instance.data.get("priority", self.priority) - - if self.group != "none" and self.group: - job_info.Group = self.group - - if self.limit: - job_info.LimitGroups = ",".join(self.limit) - - attr_values = self.get_attr_values_from_data(instance.data) - render_globals = instance.data.setdefault("renderGlobals", dict()) - machine_list = attr_values.get("machineList", "") - if machine_list: - if attr_values.get("whitelist", True): - machine_list_key = "Whitelist" - else: - machine_list_key = "Blacklist" - render_globals[machine_list_key] = machine_list - - job_info.Priority = attr_values.get("priority") - job_info.ChunkSize = attr_values.get("chunkSize") - - # Add options from RenderGlobals - render_globals = instance.data.get("renderGlobals", {}) - job_info.update(render_globals) - - keys = [ - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "OPENPYPE_SG_USER", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_IN_TESTS" - ] - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - for key in keys: - value = environment.get(key) - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - - # to recognize render jobs - job_info.add_render_job_env_var() - job_info.EnvironmentKeyValue["AYON_LOG_NO_COLORS"] = "1" - - # Adding file dependencies. - if not is_in_tests() and self.asset_dependencies: - dependencies = instance.context.data["fileDependencies"] - for dependency in dependencies: - job_info.AssetDependency += dependency - - # Add list of expected files to job - # --------------------------------- - exp = instance.data.get("expectedFiles") - for filepath in iter_expected_files(exp): - job_info.OutputDirectory += os.path.dirname(filepath) - job_info.OutputFilename += os.path.basename(filepath) - - return job_info - - def get_plugin_info(self): - # Not all hosts can import this module. - from maya import cmds - - instance = self._instance - context = instance.context - - # Set it to default Maya behaviour if it cannot be determined - # from instance (but it should be, by the Collector). - - default_rs_include_lights = ( - instance.context.data['project_settings'] - ['maya'] - ['render_settings'] - ['enable_all_lights'] - ) - - rs_include_lights = instance.data.get( - "renderSetupIncludeLights", default_rs_include_lights) - if rs_include_lights not in {"1", "0", True, False}: - rs_include_lights = default_rs_include_lights - - attr_values = self.get_attr_values_from_data(instance.data) - strict_error_checking = attr_values.get("strict_error_checking", - self.strict_error_checking) - plugin_info = MayaPluginInfo( - SceneFile=self.scene_path, - Version=cmds.about(version=True), - RenderLayer=instance.data['setMembers'], - Renderer=instance.data["renderer"], - RenderSetupIncludeLights=rs_include_lights, # noqa - ProjectPath=context.data["workspaceDir"], - UsingRenderLayers=True, - StrictErrorChecking=strict_error_checking - ) - - plugin_payload = attr.asdict(plugin_info) - - # Patching with pluginInfo from settings - for key, value in self.pluginInfo.items(): - plugin_payload[key] = value - - return plugin_payload - - def process_submission(self): - from maya import cmds - instance = self._instance - - filepath = self.scene_path # publish if `use_publish` else workfile - - # TODO: Avoid the need for this logic here, needed for submit publish - # Store output dir for unified publisher (filesequence) - expected_files = instance.data["expectedFiles"] - first_file = next(iter_expected_files(expected_files)) - output_dir = os.path.dirname(first_file) - instance.data["outputDir"] = output_dir - - # Patch workfile (only when use_published is enabled) - if self.use_published: - self._patch_workfile() - - # Gather needed data ------------------------------------------------ - filename = os.path.basename(filepath) - dirname = os.path.join( - cmds.workspace(query=True, rootDirectory=True), - cmds.workspace(fileRuleEntry="images") - ) - - # Fill in common data to payload ------------------------------------ - # TODO: Replace these with collected data from CollectRender - payload_data = { - "filename": filename, - "dirname": dirname, - } - - # Submit preceding export jobs ------------------------------------- - export_job = None - assert not all(x in instance.data["families"] - for x in ['vrayscene', 'assscene']), ( - "Vray Scene and Ass Scene options are mutually exclusive") - - auth = self._instance.data["deadline"]["auth"] - verify = self._instance.data["deadline"]["verify"] - if "vrayscene" in instance.data["families"]: - self.log.debug("Submitting V-Ray scene render..") - vray_export_payload = self._get_vray_export_payload(payload_data) - export_job = self.submit(vray_export_payload, - auth=auth, - verify=verify) - - payload = self._get_vray_render_payload(payload_data) - - else: - self.log.debug("Submitting MayaBatch render..") - payload = self._get_maya_payload(payload_data) - - # Add export job as dependency -------------------------------------- - if export_job: - job_info, _ = payload - job_info.JobDependencies = export_job - - if instance.data.get("tileRendering"): - # Prepare tiles data - self._tile_render(payload) - else: - # Submit main render job - job_info, plugin_info = payload - self.submit(self.assemble_payload(job_info, plugin_info), - auth=auth, - verify=verify) - - def _tile_render(self, payload): - """Submit as tile render per frame with dependent assembly jobs.""" - - # As collected by super process() - instance = self._instance - - payload_job_info, payload_plugin_info = payload - job_info = copy.deepcopy(payload_job_info) - plugin_info = copy.deepcopy(payload_plugin_info) - - # Force plugin reload for vray cause the region does not get flushed - # between tile renders. - if plugin_info["Renderer"] == "vray": - job_info.ForceReloadPlugin = True - - # if we have sequence of files, we need to create tile job for - # every frame - job_info.TileJob = True - job_info.TileJobTilesInX = instance.data.get("tilesX") - job_info.TileJobTilesInY = instance.data.get("tilesY") - - tiles_count = job_info.TileJobTilesInX * job_info.TileJobTilesInY - - plugin_info["ImageHeight"] = instance.data.get("resolutionHeight") - plugin_info["ImageWidth"] = instance.data.get("resolutionWidth") - plugin_info["RegionRendering"] = True - - R_FRAME_NUMBER = re.compile( - r".+\.(?P[0-9]+)\..+") # noqa: N806, E501 - REPL_FRAME_NUMBER = re.compile( - r"(.+\.)([0-9]+)(\..+)") # noqa: N806, E501 - - exp = instance.data["expectedFiles"] - if isinstance(exp[0], dict): - # we have aovs and we need to iterate over them - # get files from `beauty` - files = exp[0].get("beauty") - # assembly files are used for assembly jobs as we need to put - # together all AOVs - assembly_files = list( - itertools.chain.from_iterable( - [f for _, f in exp[0].items()])) - if not files: - # if beauty doesn't exist, use first aov we found - files = exp[0].get(list(exp[0].keys())[0]) - else: - files = exp - assembly_files = files - - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - - # Define frame tile jobs - frame_file_hash = {} - frame_payloads = {} - file_index = 1 - for file in files: - frame = re.search(R_FRAME_NUMBER, file).group("frame") - - new_job_info = copy.deepcopy(job_info) - new_job_info.Name += " (Frame {} - {} tiles)".format(frame, - tiles_count) - new_job_info.TileJobFrame = frame - - new_plugin_info = copy.deepcopy(plugin_info) - - # Add tile data into job info and plugin info - tiles_data = _format_tiles( - file, 0, - instance.data.get("tilesX"), - instance.data.get("tilesY"), - instance.data.get("resolutionWidth"), - instance.data.get("resolutionHeight"), - payload_plugin_info["OutputFilePrefix"] - )[0] - - new_job_info.update(tiles_data["JobInfo"]) - new_plugin_info.update(tiles_data["PluginInfo"]) - - self.log.debug("hashing {} - {}".format(file_index, file)) - job_hash = hashlib.sha256( - ("{}_{}".format(file_index, file)).encode("utf-8")) - - file_hash = job_hash.hexdigest() - frame_file_hash[frame] = file_hash - - new_job_info.ExtraInfo[0] = file_hash - new_job_info.ExtraInfo[1] = file - - frame_payloads[frame] = self.assemble_payload( - job_info=new_job_info, - plugin_info=new_plugin_info - ) - file_index += 1 - - self.log.debug( - "Submitting tile job(s) [{}] ...".format(len(frame_payloads))) - - # Submit frame tile jobs - frame_tile_job_id = {} - for frame, tile_job_payload in frame_payloads.items(): - job_id = self.submit( - tile_job_payload, auth, verify) - frame_tile_job_id[frame] = job_id - - # Define assembly payloads - assembly_job_info = copy.deepcopy(job_info) - assembly_job_info.Plugin = self.tile_assembler_plugin - assembly_job_info.Name += " - Tile Assembly Job" - assembly_job_info.Frames = 1 - assembly_job_info.MachineLimit = 1 - - attr_values = self.get_attr_values_from_data(instance.data) - assembly_job_info.Priority = attr_values.get("tile_priority", - self.tile_priority) - assembly_job_info.TileJob = False - - # TODO: This should be a new publisher attribute definition - pool = instance.context.data["project_settings"]["deadline"] - pool = pool["publish"]["ProcessSubmittedJobOnFarm"]["deadline_pool"] - assembly_job_info.Pool = pool or instance.data.get("primaryPool", "") - - assembly_plugin_info = { - "CleanupTiles": 1, - "ErrorOnMissing": True, - "Renderer": self._instance.data["renderer"] - } - - assembly_payloads = [] - output_dir = self.job_info.OutputDirectory[0] - config_files = [] - for file in assembly_files: - frame = re.search(R_FRAME_NUMBER, file).group("frame") - - frame_assembly_job_info = copy.deepcopy(assembly_job_info) - frame_assembly_job_info.Name += " (Frame {})".format(frame) - frame_assembly_job_info.OutputFilename[0] = re.sub( - REPL_FRAME_NUMBER, - "\\1{}\\3".format("#" * len(frame)), file) - - file_hash = frame_file_hash[frame] - tile_job_id = frame_tile_job_id[frame] - - frame_assembly_job_info.ExtraInfo[0] = file_hash - frame_assembly_job_info.ExtraInfo[1] = file - frame_assembly_job_info.JobDependencies = tile_job_id - frame_assembly_job_info.Frames = frame - - # write assembly job config files - config_file = os.path.join( - output_dir, - "{}_config_{}.txt".format( - os.path.splitext(file)[0], - datetime.now().strftime("%Y_%m_%d_%H_%M_%S") - ) - ) - config_files.append(config_file) - try: - if not os.path.isdir(output_dir): - os.makedirs(output_dir) - except OSError: - # directory is not available - self.log.warning("Path is unreachable: " - "`{}`".format(output_dir)) - - with open(config_file, "w") as cf: - print("TileCount={}".format(tiles_count), file=cf) - print("ImageFileName={}".format(file), file=cf) - print("ImageWidth={}".format( - instance.data.get("resolutionWidth")), file=cf) - print("ImageHeight={}".format( - instance.data.get("resolutionHeight")), file=cf) - - reversed_y = False - if plugin_info["Renderer"] == "arnold": - reversed_y = True - - with open(config_file, "a") as cf: - # Need to reverse the order of the y tiles, because image - # coordinates are calculated from bottom left corner. - tiles = _format_tiles( - file, 0, - instance.data.get("tilesX"), - instance.data.get("tilesY"), - instance.data.get("resolutionWidth"), - instance.data.get("resolutionHeight"), - payload_plugin_info["OutputFilePrefix"], - reversed_y=reversed_y - )[1] - for k, v in sorted(tiles.items()): - print("{}={}".format(k, v), file=cf) - - assembly_payloads.append( - self.assemble_payload( - job_info=frame_assembly_job_info, - plugin_info=assembly_plugin_info.copy(), - # This would fail if the client machine and webserice are - # using different storage paths. - aux_files=[config_file] - ) - ) - - # Submit assembly jobs - assembly_job_ids = [] - num_assemblies = len(assembly_payloads) - for i, payload in enumerate(assembly_payloads): - self.log.debug( - "submitting assembly job {} of {}".format(i + 1, - num_assemblies) - ) - assembly_job_id = self.submit( - payload, - auth=auth, - verify=verify - ) - assembly_job_ids.append(assembly_job_id) - - instance.data["assemblySubmissionJobs"] = assembly_job_ids - - # Remove config files to avoid confusion about where data is coming - # from in Deadline. - for config_file in config_files: - os.remove(config_file) - - def _get_maya_payload(self, data): - - job_info = copy.deepcopy(self.job_info) - - if not is_in_tests() and self.asset_dependencies: - # Asset dependency to wait for at least the scene file to sync. - job_info.AssetDependency += self.scene_path - - # Get layer prefix - renderlayer = self._instance.data["setMembers"] - renderer = self._instance.data["renderer"] - layer_prefix_attr = RenderSettings.get_image_prefix_attr(renderer) - layer_prefix = get_attr_in_layer(layer_prefix_attr, layer=renderlayer) - - plugin_info = copy.deepcopy(self.plugin_info) - plugin_info.update({ - # Output directory and filename - "OutputFilePath": data["dirname"].replace("\\", "/"), - "OutputFilePrefix": layer_prefix, - }) - - # This hack is here because of how Deadline handles Renderman version. - # it considers everything with `renderman` set as version older than - # Renderman 22, and so if we are using renderman > 21 we need to set - # renderer string on the job to `renderman22`. We will have to change - # this when Deadline releases new version handling this. - renderer = self._instance.data["renderer"] - if renderer == "renderman": - try: - from rfm2.config import cfg # noqa - except ImportError: - raise Exception("Cannot determine renderman version") - - rman_version = cfg().build_info.version() # type: str - if int(rman_version.split(".")[0]) > 22: - renderer = "renderman22" - - plugin_info["Renderer"] = renderer - - # this is needed because renderman plugin in Deadline - # handles directory and file prefixes separately - plugin_info["OutputFilePath"] = job_info.OutputDirectory[0] - - return job_info, plugin_info - - def _get_vray_export_payload(self, data): - - job_info = copy.deepcopy(self.job_info) - job_info.Name = self._job_info_label("Export") - - # Get V-Ray settings info to compute output path - vray_scene = self.format_vray_output_filename() - - plugin_info = { - "Renderer": "vray", - "SkipExistingFrames": True, - "UseLegacyRenderLayers": True, - "OutputFilePath": os.path.dirname(vray_scene) - } - - return job_info, attr.asdict(plugin_info) - - def _get_vray_render_payload(self, data): - - # Job Info - job_info = copy.deepcopy(self.job_info) - job_info.Name = self._job_info_label("Render") - job_info.Plugin = "Vray" - job_info.OverrideTaskExtraInfoNames = False - - # Plugin Info - plugin_info = VRayPluginInfo( - InputFilename=self.format_vray_output_filename(), - SeparateFilesPerFrame=False, - VRayEngine="V-Ray", - Width=self._instance.data["resolutionWidth"], - Height=self._instance.data["resolutionHeight"], - OutputFilePath=job_info.OutputDirectory[0], - OutputFileName=job_info.OutputFilename[0] - ) - - return job_info, attr.asdict(plugin_info) - - def _get_arnold_render_payload(self, data): - # Job Info - job_info = copy.deepcopy(self.job_info) - job_info.Name = self._job_info_label("Render") - job_info.Plugin = "Arnold" - job_info.OverrideTaskExtraInfoNames = False - - # Plugin Info - ass_file, _ = os.path.splitext(data["output_filename_0"]) - ass_filepath = ass_file + ".ass" - - plugin_info = ArnoldPluginInfo( - ArnoldFile=ass_filepath - ) - - return job_info, attr.asdict(plugin_info) - - def format_vray_output_filename(self): - """Format the expected output file of the Export job. - - Example: - /_/ - "shot010_v006/shot010_v006_CHARS/CHARS_0001.vrscene" - Returns: - str - - """ - from maya import cmds - # "vrayscene//_/" - vray_settings = cmds.ls(type="VRaySettingsNode") - node = vray_settings[0] - template = cmds.getAttr("{}.vrscene_filename".format(node)) - scene, _ = os.path.splitext(self.scene_path) - - def smart_replace(string, key_values): - new_string = string - for key, value in key_values.items(): - new_string = new_string.replace(key, value) - return new_string - - # Get workfile scene path without extension to format vrscene_filename - scene_filename = os.path.basename(self.scene_path) - scene_filename_no_ext, _ = os.path.splitext(scene_filename) - - layer = self._instance.data['setMembers'] - - # Reformat without tokens - output_path = smart_replace( - template, - {"": scene_filename_no_ext, - "": layer}) - - start_frame = int(self._instance.data["frameStartHandle"]) - workspace = self._instance.context.data["workspace"] - filename_zero = "{}_{:04d}.vrscene".format(output_path, start_frame) - filepath_zero = os.path.join(workspace, filename_zero) - - return filepath_zero.replace("\\", "/") - - def _patch_workfile(self): - """Patch Maya scene. - - This will take list of patches (lines to add) and apply them to - *published* Maya scene file (that is used later for rendering). - - Patches are dict with following structure:: - { - "name": "Name of patch", - "regex": "regex of line before patch", - "line": "line to insert" - } - - """ - project_settings = self._instance.context.data["project_settings"] - patches = ( - project_settings.get( - "deadline", {}).get( - "publish", {}).get( - "MayaSubmitDeadline", {}).get( - "scene_patches", {}) - ) - if not patches: - return - - if not os.path.splitext(self.scene_path)[1].lower() != ".ma": - self.log.debug("Skipping workfile patch since workfile is not " - ".ma file") - return - - compiled_regex = [re.compile(p["regex"]) for p in patches] - with open(self.scene_path, "r+") as pf: - scene_data = pf.readlines() - for ln, line in enumerate(scene_data): - for i, r in enumerate(compiled_regex): - if re.match(r, line): - scene_data.insert(ln + 1, patches[i]["line"]) - pf.seek(0) - pf.writelines(scene_data) - pf.truncate() - self.log.info("Applied {} patch to scene.".format( - patches[i]["name"] - )) - - def _job_info_label(self, label): - return "{label} {job.Name} [{start}-{end}]".format( - label=label, - job=self.job_info, - start=int(self._instance.data["frameStartHandle"]), - end=int(self._instance.data["frameEndHandle"]), - ) - - @classmethod - def get_attribute_defs(cls): - defs = super(MayaSubmitDeadline, cls).get_attribute_defs() - - defs.extend([ - NumberDef("priority", - label="Priority", - default=cls.default_priority, - decimals=0), - NumberDef("chunkSize", - label="Frames Per Task", - default=1, - decimals=0, - minimum=1, - maximum=1000), - TextDef("machineList", - label="Machine List", - default="", - placeholder="machine1,machine2"), - EnumDef("whitelist", - label="Machine List (Allow/Deny)", - items={ - True: "Allow List", - False: "Deny List", - }, - default=False), - NumberDef("tile_priority", - label="Tile Assembler Priority", - decimals=0, - default=cls.tile_priority), - BoolDef("strict_error_checking", - label="Strict Error Checking", - default=cls.strict_error_checking), - - ]) - - return defs - -def _format_tiles( - filename, - index, - tiles_x, - tiles_y, - width, - height, - prefix, - reversed_y=False -): - """Generate tile entries for Deadline tile job. - - Returns two dictionaries - one that can be directly used in Deadline - job, second that can be used for Deadline Assembly job configuration - file. - - This will format tile names: - - Example:: - { - "OutputFilename0Tile0": "_tile_1x1_4x4_Main_beauty.1001.exr", - "OutputFilename0Tile1": "_tile_2x1_4x4_Main_beauty.1001.exr" - } - - And add tile prefixes like: - - Example:: - Image prefix is: - `//_` - - Result for tile 0 for 4x4 will be: - `//_tile_1x1_4x4__` - - Calculating coordinates is tricky as in Job they are defined as top, - left, bottom, right with zero being in top-left corner. But Assembler - configuration file takes tile coordinates as X, Y, Width and Height and - zero is bottom left corner. - - Args: - filename (str): Filename to process as tiles. - index (int): Index of that file if it is sequence. - tiles_x (int): Number of tiles in X. - tiles_y (int): Number of tiles in Y. - width (int): Width resolution of final image. - height (int): Height resolution of final image. - prefix (str): Image prefix. - reversed_y (bool): Reverses the order of the y tiles. - - Returns: - (dict, dict): Tuple of two dictionaries - first can be used to - extend JobInfo, second has tiles x, y, width and height - used for assembler configuration. - - """ - # Math used requires integers for correct output - as such - # we ensure our inputs are correct. - assert isinstance(tiles_x, int), "tiles_x must be an integer" - assert isinstance(tiles_y, int), "tiles_y must be an integer" - assert isinstance(width, int), "width must be an integer" - assert isinstance(height, int), "height must be an integer" - - out = {"JobInfo": {}, "PluginInfo": {}} - cfg = OrderedDict() - w_space = width // tiles_x - h_space = height // tiles_y - - cfg["TilesCropped"] = "False" - - tile = 0 - range_y = range(1, tiles_y + 1) - reversed_y_range = list(reversed(range_y)) - for tile_x in range(1, tiles_x + 1): - for i, tile_y in enumerate(range_y): - tile_y_index = tile_y - if reversed_y: - tile_y_index = reversed_y_range[i] - - tile_prefix = "_tile_{}x{}_{}x{}_".format( - tile_x, tile_y_index, tiles_x, tiles_y - ) - - new_filename = "{}/{}{}".format( - os.path.dirname(filename), - tile_prefix, - os.path.basename(filename) - ) - - top = height - (tile_y * h_space) - bottom = height - ((tile_y - 1) * h_space) - 1 - left = (tile_x - 1) * w_space - right = (tile_x * w_space) - 1 - - # Job info - key = "OutputFilename{}".format(index) - out["JobInfo"][key] = new_filename - - # Plugin Info - key = "RegionPrefix{}".format(str(tile)) - out["PluginInfo"][key] = "/{}".format( - tile_prefix - ).join(prefix.rsplit("/", 1)) - out["PluginInfo"]["RegionTop{}".format(tile)] = top - out["PluginInfo"]["RegionBottom{}".format(tile)] = bottom - out["PluginInfo"]["RegionLeft{}".format(tile)] = left - out["PluginInfo"]["RegionRight{}".format(tile)] = right - - # Tile config - cfg["Tile{}FileName".format(tile)] = new_filename - cfg["Tile{}X".format(tile)] = left - cfg["Tile{}Y".format(tile)] = top - cfg["Tile{}Width".format(tile)] = w_space - cfg["Tile{}Height".format(tile)] = h_space - - tile += 1 - - return out, cfg diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py deleted file mode 100644 index 7ead5142cf..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_nuke_deadline.py +++ /dev/null @@ -1,558 +0,0 @@ -import os -import re -import json -import getpass -from datetime import datetime - -import pyblish.api - -from ayon_core.pipeline.publish import ( - AYONPyblishPluginMixin -) -from ayon_core.lib import ( - is_in_tests, - BoolDef, - NumberDef -) -from ayon_deadline.abstract_submit_deadline import requests_post - - -class NukeSubmitDeadline(pyblish.api.InstancePlugin, - AYONPyblishPluginMixin): - """Submit write to Deadline - - Renders are submitted to a Deadline Web Service as - supplied via settings key "DEADLINE_REST_URL". - - """ - - label = "Submit Nuke to Deadline" - order = pyblish.api.IntegratorOrder + 0.1 - hosts = ["nuke"] - families = ["render", "prerender"] - optional = True - targets = ["local"] - settings_category = "deadline" - - # presets - priority = 50 - chunk_size = 1 - concurrent_tasks = 1 - group = "" - department = "" - limit_groups = [] - use_gpu = False - env_allowed_keys = [] - env_search_replace_values = [] - workfile_dependency = True - use_published_workfile = True - - @classmethod - def get_attribute_defs(cls): - return [ - NumberDef( - "priority", - label="Priority", - default=cls.priority, - decimals=0 - ), - NumberDef( - "chunk", - label="Frames Per Task", - default=cls.chunk_size, - decimals=0, - minimum=1, - maximum=1000 - ), - NumberDef( - "concurrency", - label="Concurrency", - default=cls.concurrent_tasks, - decimals=0, - minimum=1, - maximum=10 - ), - BoolDef( - "use_gpu", - default=cls.use_gpu, - label="Use GPU" - ), - BoolDef( - "workfile_dependency", - default=cls.workfile_dependency, - label="Workfile Dependency" - ), - BoolDef( - "use_published_workfile", - default=cls.use_published_workfile, - label="Use Published Workfile" - ) - ] - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - instance.data["attributeValues"] = self.get_attr_values_from_data( - instance.data) - - families = instance.data["families"] - - node = instance.data["transientData"]["node"] - context = instance.context - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - self.deadline_url = "{}/api/jobs".format(deadline_url) - self._comment = context.data.get("comment", "") - self._ver = re.search(r"\d+\.\d+", context.data.get("hostVersion")) - self._deadline_user = context.data.get( - "deadlineUser", getpass.getuser()) - submit_frame_start = int(instance.data["frameStartHandle"]) - submit_frame_end = int(instance.data["frameEndHandle"]) - - # get output path - render_path = instance.data['path'] - script_path = context.data["currentFile"] - - use_published_workfile = instance.data["attributeValues"].get( - "use_published_workfile", self.use_published_workfile - ) - if use_published_workfile: - script_path = self._get_published_workfile_path(context) - - # only add main rendering job if target is not frames_farm - r_job_response_json = None - if instance.data["render_target"] != "frames_farm": - r_job_response = self.payload_submit( - instance, - script_path, - render_path, - node.name(), - submit_frame_start, - submit_frame_end - ) - r_job_response_json = r_job_response.json() - instance.data["deadlineSubmissionJob"] = r_job_response_json - - # Store output dir for unified publisher (filesequence) - instance.data["outputDir"] = os.path.dirname( - render_path).replace("\\", "/") - instance.data["publishJobState"] = "Suspended" - - if instance.data.get("bakingNukeScripts"): - for baking_script in instance.data["bakingNukeScripts"]: - render_path = baking_script["bakeRenderPath"] - script_path = baking_script["bakeScriptPath"] - exe_node_name = baking_script["bakeWriteNodeName"] - - b_job_response = self.payload_submit( - instance, - script_path, - render_path, - exe_node_name, - submit_frame_start, - submit_frame_end, - r_job_response_json, - baking_submission=True - ) - - # Store output dir for unified publisher (filesequence) - instance.data["deadlineSubmissionJob"] = b_job_response.json() - - instance.data["publishJobState"] = "Suspended" - - # add to list of job Id - if not instance.data.get("bakingSubmissionJobs"): - instance.data["bakingSubmissionJobs"] = [] - - instance.data["bakingSubmissionJobs"].append( - b_job_response.json()["_id"]) - - # redefinition of families - if "render" in instance.data["productType"]: - instance.data["family"] = "write" - instance.data["productType"] = "write" - families.insert(0, "render2d") - elif "prerender" in instance.data["productType"]: - instance.data["family"] = "write" - instance.data["productType"] = "write" - families.insert(0, "prerender") - instance.data["families"] = families - - def _get_published_workfile_path(self, context): - """This method is temporary while the class is not inherited from - AbstractSubmitDeadline""" - anatomy = context.data["anatomy"] - # WARNING Hardcoded template name 'default' > may not be used - publish_template = anatomy.get_template_item( - "publish", "default", "path" - ) - for instance in context: - if ( - instance.data["productType"] != "workfile" - # Disabled instances won't be integrated - or instance.data("publish") is False - ): - continue - template_data = instance.data["anatomyData"] - # Expect workfile instance has only one representation - representation = instance.data["representations"][0] - # Get workfile extension - repre_file = representation["files"] - self.log.info(repre_file) - ext = os.path.splitext(repre_file)[1].lstrip(".") - - # Fill template data - template_data["representation"] = representation["name"] - template_data["ext"] = ext - template_data["comment"] = None - - template_filled = publish_template.format(template_data) - script_path = os.path.normpath(template_filled) - self.log.info( - "Using published scene for render {}".format( - script_path - ) - ) - return script_path - - return None - - def payload_submit( - self, - instance, - script_path, - render_path, - exe_node_name, - start_frame, - end_frame, - response_data=None, - baking_submission=False, - ): - """Submit payload to Deadline - - Args: - instance (pyblish.api.Instance): pyblish instance - script_path (str): path to nuke script - render_path (str): path to rendered images - exe_node_name (str): name of the node to render - start_frame (int): start frame - end_frame (int): end frame - response_data Optional[dict]: response data from - previous submission - baking_submission Optional[bool]: if it's baking submission - - Returns: - requests.Response - """ - render_dir = os.path.normpath(os.path.dirname(render_path)) - - # batch name - src_filepath = instance.context.data["currentFile"] - batch_name = os.path.basename(src_filepath) - job_name = os.path.basename(render_path) - - if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - - output_filename_0 = self.preview_fname(render_path) - - if not response_data: - response_data = {} - - try: - # Ensure render folder exists - os.makedirs(render_dir) - except OSError: - pass - - # resolve any limit groups - limit_groups = self.get_limit_groups() - self.log.debug("Limit groups: `{}`".format(limit_groups)) - - payload = { - "JobInfo": { - # Top-level group name - "BatchName": batch_name, - - # Job name, as seen in Monitor - "Name": job_name, - - # Arbitrary username, for visualisation in Monitor - "UserName": self._deadline_user, - - "Priority": instance.data["attributeValues"].get( - "priority", self.priority), - "ChunkSize": instance.data["attributeValues"].get( - "chunk", self.chunk_size), - "ConcurrentTasks": instance.data["attributeValues"].get( - "concurrency", - self.concurrent_tasks - ), - - "Department": self.department, - - "Pool": instance.data.get("primaryPool"), - "SecondaryPool": instance.data.get("secondaryPool"), - "Group": self.group, - - "Plugin": "Nuke", - "Frames": "{start}-{end}".format( - start=start_frame, - end=end_frame - ), - "Comment": self._comment, - - # Optional, enable double-click to preview rendered - # frames from Deadline Monitor - "OutputFilename0": output_filename_0.replace("\\", "/"), - - # limiting groups - "LimitGroups": ",".join(limit_groups) - - }, - "PluginInfo": { - # Input - "SceneFile": script_path, - - # Output directory and filename - "OutputFilePath": render_dir.replace("\\", "/"), - # "OutputFilePrefix": render_variables["filename_prefix"], - - # Mandatory for Deadline - "Version": self._ver.group(), - - # Resolve relative references - "ProjectPath": script_path, - "AWSAssetFile0": render_path, - - # using GPU by default - "UseGpu": instance.data["attributeValues"].get( - "use_gpu", self.use_gpu), - - # Only the specific write node is rendered. - "WriteNode": exe_node_name - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Add workfile dependency. - workfile_dependency = instance.data["attributeValues"].get( - "workfile_dependency", self.workfile_dependency - ) - if workfile_dependency: - payload["JobInfo"].update({"AssetDependency0": script_path}) - - # TODO: rewrite for baking with sequences - if baking_submission: - payload["JobInfo"].update({ - "JobType": "Normal", - "ChunkSize": 99999999 - }) - - if response_data.get("_id"): - payload["JobInfo"].update({ - "BatchName": response_data["Props"]["Batch"], - "JobDependency0": response_data["_id"], - }) - - # Include critical environment variables with submission - keys = [ - "PYTHONPATH", - "PATH", - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_APP_NAME", - "FTRACK_API_KEY", - "FTRACK_API_USER", - "FTRACK_SERVER", - "PYBLISHPLUGINPATH", - "NUKE_PATH", - "TOOL_ENV", - "FOUNDRY_LICENSE", - "OPENPYPE_SG_USER", - ] - - # add allowed keys from preset if any - if self.env_allowed_keys: - keys += self.env_allowed_keys - - environment = { - key: os.environ[key] - for key in keys - if key in os.environ - } - - # to recognize render jobs - environment["AYON_RENDER_JOB"] = "1" - - # finally search replace in values of any key - if self.env_search_replace_values: - for key, value in environment.items(): - for item in self.env_search_replace_values: - environment[key] = value.replace( - item["name"], item["value"] - ) - - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - plugin = payload["JobInfo"]["Plugin"] - self.log.debug("using render plugin : {}".format(plugin)) - - self.log.debug("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # adding expected files to instance.data - self.expected_files( - instance, - render_path, - start_frame, - end_frame - ) - - self.log.debug("__ expectedFiles: `{}`".format( - instance.data["expectedFiles"])) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post(self.deadline_url, - json=payload, - timeout=10, - auth=auth, - verify=verify) - - if not response.ok: - raise Exception(response.text) - - return response - - def preflight_check(self, instance): - """Ensure the startFrame, endFrame and byFrameStep are integers""" - - for key in ("frameStart", "frameEnd"): - value = instance.data[key] - - if int(value) == value: - continue - - self.log.warning( - "%f=%d was rounded off to nearest integer" - % (value, int(value)) - ) - - def preview_fname(self, path): - """Return output file path with #### for padding. - - Deadline requires the path to be formatted with # in place of numbers. - For example `/path/to/render.####.png` - - Args: - path (str): path to rendered images - - Returns: - str - - """ - self.log.debug("_ path: `{}`".format(path)) - if "%" in path: - search_results = re.search(r"(%0)(\d)(d.)", path).groups() - self.log.debug("_ search_results: `{}`".format(search_results)) - return int(search_results[1]) - if "#" in path: - self.log.debug("_ path: `{}`".format(path)) - return path - - def expected_files( - self, - instance, - filepath, - start_frame, - end_frame - ): - """ Create expected files in instance data - """ - if not instance.data.get("expectedFiles"): - instance.data["expectedFiles"] = [] - - dirname = os.path.dirname(filepath) - file = os.path.basename(filepath) - - # since some files might be already tagged as publish_on_farm - # we need to avoid adding them to expected files since those would be - # duplicated into metadata.json file - representations = instance.data.get("representations", []) - # check if file is not in representations with publish_on_farm tag - for repre in representations: - # Skip if 'publish_on_farm' not available - if "publish_on_farm" not in repre.get("tags", []): - continue - - # in case where single file (video, image) is already in - # representation file. Will be added to expected files via - # submit_publish_job.py - if file in repre.get("files", []): - self.log.debug( - "Skipping expected file: {}".format(filepath)) - return - - # in case path is hashed sequence expression - # (e.g. /path/to/file.####.png) - if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] - - # in case input path was single file (video or image) - if "%" not in file: - instance.data["expectedFiles"].append(filepath) - return - - # shift start frame by 1 if slate is present - if instance.data.get("slate"): - start_frame -= 1 - - # add sequence files to expected files - for i in range(start_frame, (end_frame + 1)): - instance.data["expectedFiles"].append( - os.path.join(dirname, (file % i)).replace("\\", "/")) - - def get_limit_groups(self): - """Search for limit group nodes and return group name. - Limit groups will be defined as pairs in Nuke deadline submitter - presents where the key will be name of limit group and value will be - a list of plugin's node class names. Thus, when a plugin uses more - than one node, these will be captured and the triggered process - will add the appropriate limit group to the payload jobinfo attributes. - Returning: - list: captured groups list - """ - # Not all hosts can import this module. - import nuke - - captured_groups = [] - for limit_group in self.limit_groups: - lg_name = limit_group["name"] - - for node_class in limit_group["value"]: - for node in nuke.allNodes(recurseGroups=True): - # ignore all nodes not member of defined class - if node.Class() not in node_class: - continue - # ignore all disabled nodes - if node["disable"].value(): - continue - # add group name if not already added - if lg_name not in captured_groups: - captured_groups.append(lg_name) - return captured_groups diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py deleted file mode 100644 index d93592a6a3..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_cache_job.py +++ /dev/null @@ -1,463 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submit publishing job to farm.""" -import os -import json -import re -from copy import deepcopy - -import ayon_api -import pyblish.api - -from ayon_core.pipeline import publish -from ayon_core.lib import EnumDef, is_in_tests -from ayon_core.pipeline.version_start import get_versioning_start -from ayon_core.pipeline.farm.pyblish_functions import ( - create_skeleton_instance_cache, - create_instances_for_cache, - attach_instances_to_product, - prepare_cache_representations, - create_metadata_path -) -from ayon_deadline.abstract_submit_deadline import requests_post - - -class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin): - """Process Cache Job submitted on farm - This is replicated version of submit publish job - specifically for cache(s). - - These jobs are dependent on a deadline job - submission prior to this plug-in. - - - In case of Deadline, it creates dependent job on farm publishing - rendered image sequence. - - Options in instance.data: - - deadlineSubmissionJob (dict, Required): The returned .json - data from the job submission to deadline. - - - outputDir (str, Required): The output directory where the metadata - file should be generated. It's assumed that this will also be - final folder containing the output files. - - - ext (str, Optional): The extension (including `.`) that is required - in the output filename to be picked up for image sequence - publishing. - - - expectedFiles (list or dict): explained below - - """ - - label = "Submit cache jobs to Deadline" - order = pyblish.api.IntegratorOrder + 0.2 - icon = "tractor" - settings_category = "deadline" - - targets = ["local"] - - hosts = ["houdini"] - - families = ["publish.hou"] - - environ_keys = [ - "FTRACK_API_USER", - "FTRACK_API_KEY", - "FTRACK_SERVER", - "AYON_APP_NAME", - "AYON_USERNAME", - "AYON_SG_USERNAME", - "KITSU_LOGIN", - "KITSU_PWD" - ] - - # custom deadline attributes - deadline_department = "" - deadline_pool = "" - deadline_pool_secondary = "" - deadline_group = "" - deadline_chunk_size = 1 - deadline_priority = None - - # regex for finding frame number in string - R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') - - plugin_pype_version = "3.0" - - # script path for publish_filesequence.py - publishing_script = None - - def _submit_deadline_post_job(self, instance, job): - """Submit publish job to Deadline. - - Returns: - (str): deadline_publish_job_id - """ - data = instance.data.copy() - product_name = data["productName"] - job_name = "Publish - {}".format(product_name) - - anatomy = instance.context.data['anatomy'] - - # instance.data.get("productName") != instances[0]["productName"] - # 'Main' vs 'renderMain' - override_version = None - instance_version = instance.data.get("version") # take this if exists - if instance_version != 1: - override_version = instance_version - - output_dir = self._get_publish_folder( - anatomy, - deepcopy(instance.data["anatomyData"]), - instance.data.get("folderEntity"), - instance.data["productName"], - instance.context, - instance.data["productType"], - override_version - ) - - # Transfer the environment from the original job to this dependent - # job so they use the same environment - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - environment = { - "AYON_PROJECT_NAME": instance.context.data["projectName"], - "AYON_FOLDER_PATH": instance.context.data["folderPath"], - "AYON_TASK_NAME": instance.context.data["task"], - "AYON_USERNAME": instance.context.data["user"], - "AYON_LOG_NO_COLORS": "1", - "AYON_IN_TESTS": str(int(is_in_tests())), - "AYON_PUBLISH_JOB": "1", - "AYON_RENDER_JOB": "0", - "AYON_REMOTE_PUBLISH": "0", - "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], - "AYON_DEFAULT_SETTINGS_VARIANT": ( - os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] - ), - } - - # add environments from self.environ_keys - for env_key in self.environ_keys: - if os.getenv(env_key): - environment[env_key] = os.environ[env_key] - - priority = self.deadline_priority or instance.data.get("priority", 50) - - instance_settings = self.get_attr_values_from_data(instance.data) - initial_status = instance_settings.get("publishJobState", "Active") - - args = [ - "--headless", - 'publish', - '"{}"'.format(rootless_metadata_path), - "--targets", "deadline", - "--targets", "farm" - ] - - # Generate the payload for Deadline submission - secondary_pool = ( - self.deadline_pool_secondary or instance.data.get("secondaryPool") - ) - payload = { - "JobInfo": { - "Plugin": "Ayon", - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "UserName": job["Props"]["User"], - "Comment": instance.context.data.get("comment", ""), - - "Department": self.deadline_department, - "ChunkSize": self.deadline_chunk_size, - "Priority": priority, - "InitialStatus": initial_status, - - "Group": self.deadline_group, - "Pool": self.deadline_pool or instance.data.get("primaryPool"), - "SecondaryPool": secondary_pool, - # ensure the outputdirectory with correct slashes - "OutputDirectory0": output_dir.replace("\\", "/") - }, - "PluginInfo": { - "Version": self.plugin_pype_version, - "Arguments": " ".join(args), - "SingleFrameOnly": "True", - }, - # Mandatory for Deadline, may be empty - "AuxFiles": [], - } - - if job.get("_id"): - payload["JobInfo"]["JobDependency0"] = job["_id"] - - for index, (key_, value_) in enumerate(environment.items()): - payload["JobInfo"].update( - { - "EnvironmentKeyValue%d" - % index: "{key}={value}".format( - key=key_, value=value_ - ) - } - ) - # remove secondary pool - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.debug("Submitting Deadline publish job ...") - - url = "{}/api/jobs".format(self.deadline_url) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post( - url, json=payload, timeout=10, auth=auth, verify=verify) - if not response.ok: - raise Exception(response.text) - - deadline_publish_job_id = response.json()["_id"] - - return deadline_publish_job_id - - def process(self, instance): - # type: (pyblish.api.Instance) -> None - """Process plugin. - - Detect type of render farm submission and create and post dependent - job in case of Deadline. It creates json file with metadata needed for - publishing in directory of render. - - Args: - instance (pyblish.api.Instance): Instance data. - - """ - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - anatomy = instance.context.data["anatomy"] - - instance_skeleton_data = create_skeleton_instance_cache(instance) - """ - if content of `expectedFiles` list are dictionaries, we will handle - it as list of AOVs, creating instance for every one of them. - - Example: - -------- - - expectedFiles = [ - { - "beauty": [ - "foo_v01.0001.exr", - "foo_v01.0002.exr" - ], - - "Z": [ - "boo_v01.0001.exr", - "boo_v01.0002.exr" - ] - } - ] - - This will create instances for `beauty` and `Z` product - adding those files to their respective representations. - - If we have only list of files, we collect all file sequences. - More then one doesn't probably make sense, but we'll handle it - like creating one instance with multiple representations. - - Example: - -------- - - expectedFiles = [ - "foo_v01.0001.exr", - "foo_v01.0002.exr", - "xxx_v01.0001.exr", - "xxx_v01.0002.exr" - ] - - This will result in one instance with two representations: - `foo` and `xxx` - """ - - if isinstance(instance.data.get("expectedFiles")[0], dict): - instances = create_instances_for_cache( - instance, instance_skeleton_data) - else: - representations = prepare_cache_representations( - instance_skeleton_data, - instance.data.get("expectedFiles"), - anatomy - ) - - if "representations" not in instance_skeleton_data.keys(): - instance_skeleton_data["representations"] = [] - - # add representation - instance_skeleton_data["representations"] += representations - instances = [instance_skeleton_data] - - # attach instances to product - if instance.data.get("attachTo"): - instances = attach_instances_to_product( - instance.data.get("attachTo"), instances - ) - - r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 - ____ - ' ' .---. .---. .--. .---. .--..--..--..--. .---. - | | --= \ | . \/ _|/ \| . \ || || \ |/ _| - | JOB | --= / | | || __| .. | | | |;_ || \ || __| - | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| - ._____. - - ''' - - render_job = None - submission_type = "" - if instance.data.get("toBeRenderedOn") == "deadline": - render_job = instance.data.pop("deadlineSubmissionJob", None) - submission_type = "deadline" - - if not render_job: - import getpass - - render_job = {} - self.log.debug("Faking job data ...") - render_job["Props"] = {} - # Render job doesn't exist because we do not have prior submission. - # We still use data from it so lets fake it. - # - # Batch name reflect original scene name - - if instance.data.get("assemblySubmissionJobs"): - render_job["Props"]["Batch"] = instance.data.get( - "jobBatchName") - else: - batch = os.path.splitext(os.path.basename( - instance.context.data.get("currentFile")))[0] - render_job["Props"]["Batch"] = batch - # User is deadline user - render_job["Props"]["User"] = instance.context.data.get( - "deadlineUser", getpass.getuser()) - - deadline_publish_job_id = None - if submission_type == "deadline": - self.deadline_url = instance.data["deadline"]["url"] - assert self.deadline_url, "Requires Deadline Webservice URL" - - deadline_publish_job_id = \ - self._submit_deadline_post_job(instance, render_job) - - # Inject deadline url to instances. - for inst in instances: - if "deadline" not in inst: - inst["deadline"] = {} - inst["deadline"] = instance.data["deadline"] - - # publish job file - publish_job = { - "folderPath": instance_skeleton_data["folderPath"], - "frameStart": instance_skeleton_data["frameStart"], - "frameEnd": instance_skeleton_data["frameEnd"], - "fps": instance_skeleton_data["fps"], - "source": instance_skeleton_data["source"], - "user": instance.context.data["user"], - "version": instance.context.data["version"], # workfile version - "intent": instance.context.data.get("intent"), - "comment": instance.context.data.get("comment"), - "job": render_job or None, - "instances": instances - } - - if deadline_publish_job_id: - publish_job["deadline_publish_job_id"] = deadline_publish_job_id - - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - with open(metadata_path, "w") as f: - json.dump(publish_job, f, indent=4, sort_keys=True) - - def _get_publish_folder(self, anatomy, template_data, - folder_entity, product_name, context, - product_type, version=None): - """ - Extracted logic to pre-calculate real publish folder, which is - calculated in IntegrateNew inside of Deadline process. - This should match logic in: - 'collect_anatomy_instance_data' - to - get correct anatomy, family, version for product and - 'collect_resources_path' - get publish_path - - Args: - anatomy (ayon_core.pipeline.anatomy.Anatomy): - template_data (dict): pre-calculated collected data for process - folder_entity (dict[str, Any]): Folder entity. - product_name (str): Product name (actually group name of product). - product_type (str): for current deadline process it's always - 'render' - TODO - for generic use family needs to be dynamically - calculated like IntegrateNew does - version (int): override version from instance if exists - - Returns: - (string): publish folder where rendered and published files will - be stored - based on 'publish' template - """ - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - if not version: - version_entity = None - if folder_entity: - version_entity = ayon_api.get_last_version_by_product_name( - project_name, - product_name, - folder_entity["id"] - ) - - if version_entity: - version = int(version_entity["version"]) + 1 - else: - version = get_versioning_start( - project_name, - host_name, - task_name=template_data["task"]["name"], - task_type=template_data["task"]["type"], - product_type="render", - product_name=product_name, - project_settings=context.data["project_settings"] - ) - - task_info = template_data.get("task") or {} - - template_name = publish.get_publish_template_name( - project_name, - host_name, - product_type, - task_info.get("name"), - task_info.get("type"), - ) - - template_data["subset"] = product_name - template_data["family"] = product_type - template_data["version"] = version - template_data["product"] = { - "name": product_name, - "type": product_type, - } - - render_dir_template = anatomy.get_template_item( - "publish", template_name, "directory" - ) - return render_dir_template.format_strict(template_data) - - @classmethod - def get_attribute_defs(cls): - return [ - EnumDef("publishJobState", - label="Publish Job State", - items=["Active", "Suspended"], - default="Active") - ] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py deleted file mode 100644 index 643dcc1c46..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/submit_publish_job.py +++ /dev/null @@ -1,585 +0,0 @@ -# -*- coding: utf-8 -*- -"""Submit publishing job to farm.""" -import os -import json -import re -from copy import deepcopy - -import clique -import ayon_api -import pyblish.api - -from ayon_core.pipeline import publish -from ayon_core.lib import EnumDef, is_in_tests -from ayon_core.pipeline.version_start import get_versioning_start - -from ayon_core.pipeline.farm.pyblish_functions import ( - create_skeleton_instance, - create_instances_for_aov, - attach_instances_to_product, - prepare_representations, - create_metadata_path -) -from ayon_deadline.abstract_submit_deadline import requests_post - - -def get_resource_files(resources, frame_range=None): - """Get resource files at given path. - - If `frame_range` is specified those outside will be removed. - - Arguments: - resources (list): List of resources - frame_range (list): Frame range to apply override - - Returns: - list of str: list of collected resources - - """ - res_collections, _ = clique.assemble(resources) - assert len(res_collections) == 1, "Multiple collections found" - res_collection = res_collections[0] - - # Remove any frames - if frame_range is not None: - for frame in frame_range: - if frame not in res_collection.indexes: - continue - res_collection.indexes.remove(frame) - - return list(res_collection) - - -class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, - publish.AYONPyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin): - """Process Job submitted on farm. - - These jobs are dependent on a deadline job - submission prior to this plug-in. - - It creates dependent job on farm publishing rendered image sequence. - - Options in instance.data: - - deadlineSubmissionJob (dict, Required): The returned .json - data from the job submission to deadline. - - - outputDir (str, Required): The output directory where the metadata - file should be generated. It's assumed that this will also be - final folder containing the output files. - - - ext (str, Optional): The extension (including `.`) that is required - in the output filename to be picked up for image sequence - publishing. - - - publishJobState (str, Optional): "Active" or "Suspended" - This defaults to "Suspended" - - - expectedFiles (list or dict): explained below - - """ - - label = "Submit Image Publishing job to Deadline" - order = pyblish.api.IntegratorOrder + 0.2 - icon = "tractor" - - targets = ["local"] - - hosts = ["fusion", "max", "maya", "nuke", "houdini", - "celaction", "aftereffects", "harmony", "blender"] - - families = ["render", "render.farm", "render.frames_farm", - "prerender", "prerender.farm", "prerender.frames_farm", - "renderlayer", "imagesequence", "image", - "vrayscene", "maxrender", - "arnold_rop", "mantra_rop", - "karma_rop", "vray_rop", - "redshift_rop", "usdrender"] - settings_category = "deadline" - - aov_filter = [ - { - "name": "maya", - "value": [r".*([Bb]eauty).*"] - }, - { - "name": "blender", - "value": [r".*([Bb]eauty).*"] - }, - { - # for everything from AE - "name": "aftereffects", - "value": [r".*"] - }, - { - "name": "harmony", - "value": [r".*"] - }, - { - "name": "celaction", - "value": [r".*"] - }, - { - "name": "max", - "value": [r".*"] - }, - ] - - environ_keys = [ - "FTRACK_API_USER", - "FTRACK_API_KEY", - "FTRACK_SERVER", - "AYON_APP_NAME", - "AYON_USERNAME", - "AYON_SG_USERNAME", - "KITSU_LOGIN", - "KITSU_PWD" - ] - - # custom deadline attributes - deadline_department = "" - deadline_pool = "" - deadline_pool_secondary = "" - deadline_group = "" - deadline_chunk_size = 1 - deadline_priority = None - - # regex for finding frame number in string - R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') - - # mapping of instance properties to be transferred to new instance - # for every specified family - instance_transfer = { - "slate": ["slateFrames", "slate"], - "review": ["lutPath"], - "render2d": ["bakingNukeScripts", "version"], - "renderlayer": ["convertToScanline"] - } - - # list of family names to transfer to new family if present - families_transfer = ["render3d", "render2d", "ftrack", "slate"] - plugin_pype_version = "3.0" - - # script path for publish_filesequence.py - publishing_script = None - - # poor man exclusion - skip_integration_repre_list = [] - - def _submit_deadline_post_job(self, instance, job, instances): - """Submit publish job to Deadline. - - Returns: - (str): deadline_publish_job_id - """ - data = instance.data.copy() - product_name = data["productName"] - job_name = "Publish - {}".format(product_name) - - anatomy = instance.context.data['anatomy'] - - # instance.data.get("productName") != instances[0]["productName"] - # 'Main' vs 'renderMain' - override_version = None - instance_version = instance.data.get("version") # take this if exists - if instance_version != 1: - override_version = instance_version - - output_dir = self._get_publish_folder( - anatomy, - deepcopy(instance.data["anatomyData"]), - instance.data.get("folderEntity"), - instances[0]["productName"], - instance.context, - instances[0]["productType"], - override_version - ) - - # Transfer the environment from the original job to this dependent - # job so they use the same environment - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - environment = { - "AYON_PROJECT_NAME": instance.context.data["projectName"], - "AYON_FOLDER_PATH": instance.context.data["folderPath"], - "AYON_TASK_NAME": instance.context.data["task"], - "AYON_USERNAME": instance.context.data["user"], - "AYON_LOG_NO_COLORS": "1", - "AYON_IN_TESTS": str(int(is_in_tests())), - "AYON_PUBLISH_JOB": "1", - "AYON_RENDER_JOB": "0", - "AYON_REMOTE_PUBLISH": "0", - "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], - "AYON_DEFAULT_SETTINGS_VARIANT": ( - os.environ["AYON_DEFAULT_SETTINGS_VARIANT"] - ), - } - - # add environments from self.environ_keys - for env_key in self.environ_keys: - if os.getenv(env_key): - environment[env_key] = os.environ[env_key] - - priority = self.deadline_priority or instance.data.get("priority", 50) - - instance_settings = self.get_attr_values_from_data(instance.data) - initial_status = instance_settings.get("publishJobState", "Active") - - args = [ - "--headless", - 'publish', - '"{}"'.format(rootless_metadata_path), - "--targets", "deadline", - "--targets", "farm" - ] - - # Generate the payload for Deadline submission - secondary_pool = ( - self.deadline_pool_secondary or instance.data.get("secondaryPool") - ) - payload = { - "JobInfo": { - "Plugin": "Ayon", - "BatchName": job["Props"]["Batch"], - "Name": job_name, - "UserName": job["Props"]["User"], - "Comment": instance.context.data.get("comment", ""), - - "Department": self.deadline_department, - "ChunkSize": self.deadline_chunk_size, - "Priority": priority, - "InitialStatus": initial_status, - - "Group": self.deadline_group, - "Pool": self.deadline_pool or instance.data.get("primaryPool"), - "SecondaryPool": secondary_pool, - # ensure the outputdirectory with correct slashes - "OutputDirectory0": output_dir.replace("\\", "/") - }, - "PluginInfo": { - "Version": self.plugin_pype_version, - "Arguments": " ".join(args), - "SingleFrameOnly": "True", - }, - # Mandatory for Deadline, may be empty - "AuxFiles": [], - } - - # add assembly jobs as dependencies - if instance.data.get("tileRendering"): - self.log.info("Adding tile assembly jobs as dependencies...") - job_index = 0 - for assembly_id in instance.data.get("assemblySubmissionJobs"): - payload["JobInfo"]["JobDependency{}".format( - job_index)] = assembly_id # noqa: E501 - job_index += 1 - elif instance.data.get("bakingSubmissionJobs"): - self.log.info( - "Adding baking submission jobs as dependencies..." - ) - job_index = 0 - for assembly_id in instance.data["bakingSubmissionJobs"]: - payload["JobInfo"]["JobDependency{}".format( - job_index)] = assembly_id # noqa: E501 - job_index += 1 - elif job.get("_id"): - payload["JobInfo"]["JobDependency0"] = job["_id"] - - for index, (key_, value_) in enumerate(environment.items()): - payload["JobInfo"].update( - { - "EnvironmentKeyValue%d" - % index: "{key}={value}".format( - key=key_, value=value_ - ) - } - ) - # remove secondary pool - payload["JobInfo"].pop("SecondaryPool", None) - - self.log.debug("Submitting Deadline publish job ...") - - url = "{}/api/jobs".format(self.deadline_url) - auth = instance.data["deadline"]["auth"] - verify = instance.data["deadline"]["verify"] - response = requests_post( - url, json=payload, timeout=10, auth=auth, verify=verify) - if not response.ok: - raise Exception(response.text) - - deadline_publish_job_id = response.json()["_id"] - - return deadline_publish_job_id - - def process(self, instance): - # type: (pyblish.api.Instance) -> None - """Process plugin. - - Detect type of render farm submission and create and post dependent - job in case of Deadline. It creates json file with metadata needed for - publishing in directory of render. - - Args: - instance (pyblish.api.Instance): Instance data. - - """ - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - anatomy = instance.context.data["anatomy"] - - instance_skeleton_data = create_skeleton_instance( - instance, families_transfer=self.families_transfer, - instance_transfer=self.instance_transfer) - """ - if content of `expectedFiles` list are dictionaries, we will handle - it as list of AOVs, creating instance for every one of them. - - Example: - -------- - - expectedFiles = [ - { - "beauty": [ - "foo_v01.0001.exr", - "foo_v01.0002.exr" - ], - - "Z": [ - "boo_v01.0001.exr", - "boo_v01.0002.exr" - ] - } - ] - - This will create instances for `beauty` and `Z` product - adding those files to their respective representations. - - If we have only list of files, we collect all file sequences. - More then one doesn't probably make sense, but we'll handle it - like creating one instance with multiple representations. - - Example: - -------- - - expectedFiles = [ - "foo_v01.0001.exr", - "foo_v01.0002.exr", - "xxx_v01.0001.exr", - "xxx_v01.0002.exr" - ] - - This will result in one instance with two representations: - `foo` and `xxx` - """ - do_not_add_review = False - if instance.data.get("review") is False: - self.log.debug("Instance has review explicitly disabled.") - do_not_add_review = True - - aov_filter = { - item["name"]: item["value"] - for item in self.aov_filter - } - if isinstance(instance.data.get("expectedFiles")[0], dict): - instances = create_instances_for_aov( - instance, instance_skeleton_data, - aov_filter, - self.skip_integration_repre_list, - do_not_add_review - ) - else: - representations = prepare_representations( - instance_skeleton_data, - instance.data.get("expectedFiles"), - anatomy, - aov_filter, - self.skip_integration_repre_list, - do_not_add_review, - instance.context, - self - ) - - if "representations" not in instance_skeleton_data.keys(): - instance_skeleton_data["representations"] = [] - - # add representation - instance_skeleton_data["representations"] += representations - instances = [instance_skeleton_data] - - # attach instances to product - if instance.data.get("attachTo"): - instances = attach_instances_to_product( - instance.data.get("attachTo"), instances - ) - - r''' SUBMiT PUBLiSH JOB 2 D34DLiN3 - ____ - ' ' .---. .---. .--. .---. .--..--..--..--. .---. - | | --= \ | . \/ _|/ \| . \ || || \ |/ _| - | JOB | --= / | | || __| .. | | | |;_ || \ || __| - | | |____./ \.__|._||_.|___./|_____|||__|\__|\.___| - ._____. - - ''' - - render_job = instance.data.pop("deadlineSubmissionJob", None) - if not render_job and instance.data.get("tileRendering") is False: - raise AssertionError(("Cannot continue without valid " - "Deadline submission.")) - if not render_job: - import getpass - - render_job = {} - self.log.debug("Faking job data ...") - render_job["Props"] = {} - # Render job doesn't exist because we do not have prior submission. - # We still use data from it so lets fake it. - # - # Batch name reflect original scene name - - if instance.data.get("assemblySubmissionJobs"): - render_job["Props"]["Batch"] = instance.data.get( - "jobBatchName") - else: - batch = os.path.splitext(os.path.basename( - instance.context.data.get("currentFile")))[0] - render_job["Props"]["Batch"] = batch - # User is deadline user - render_job["Props"]["User"] = instance.context.data.get( - "deadlineUser", getpass.getuser()) - - render_job["Props"]["Env"] = { - "FTRACK_API_USER": os.environ.get("FTRACK_API_USER"), - "FTRACK_API_KEY": os.environ.get("FTRACK_API_KEY"), - "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), - } - - # get default deadline webservice url from deadline module - self.deadline_url = instance.data["deadline"]["url"] - assert self.deadline_url, "Requires Deadline Webservice URL" - - deadline_publish_job_id = \ - self._submit_deadline_post_job(instance, render_job, instances) - - # Inject deadline url to instances to query DL for job id for overrides - for inst in instances: - inst["deadline"] = instance.data["deadline"] - - # publish job file - publish_job = { - "folderPath": instance_skeleton_data["folderPath"], - "frameStart": instance_skeleton_data["frameStart"], - "frameEnd": instance_skeleton_data["frameEnd"], - "fps": instance_skeleton_data["fps"], - "source": instance_skeleton_data["source"], - "user": instance.context.data["user"], - "version": instance.context.data["version"], # workfile version - "intent": instance.context.data.get("intent"), - "comment": instance.context.data.get("comment"), - "job": render_job or None, - "instances": instances - } - - if deadline_publish_job_id: - publish_job["deadline_publish_job_id"] = deadline_publish_job_id - - # add audio to metadata file if available - audio_file = instance.context.data.get("audioFile") - if audio_file and os.path.isfile(audio_file): - publish_job.update({"audio": audio_file}) - - metadata_path, rootless_metadata_path = \ - create_metadata_path(instance, anatomy) - - with open(metadata_path, "w") as f: - json.dump(publish_job, f, indent=4, sort_keys=True) - - def _get_publish_folder(self, anatomy, template_data, - folder_entity, product_name, context, - product_type, version=None): - """ - Extracted logic to pre-calculate real publish folder, which is - calculated in IntegrateNew inside of Deadline process. - This should match logic in: - 'collect_anatomy_instance_data' - to - get correct anatomy, family, version for product name and - 'collect_resources_path' - get publish_path - - Args: - anatomy (ayon_core.pipeline.anatomy.Anatomy): - template_data (dict): pre-calculated collected data for process - folder_entity (dict[str, Any]): Folder entity. - product_name (string): Product name (actually group name - of product) - product_type (string): for current deadline process it's always - 'render' - TODO - for generic use family needs to be dynamically - calculated like IntegrateNew does - version (int): override version from instance if exists - - Returns: - (string): publish folder where rendered and published files will - be stored - based on 'publish' template - """ - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - if not version: - version_entity = None - if folder_entity: - version_entity = ayon_api.get_last_version_by_product_name( - project_name, - product_name, - folder_entity["id"] - ) - - if version_entity: - version = int(version_entity["version"]) + 1 - else: - version = get_versioning_start( - project_name, - host_name, - task_name=template_data["task"]["name"], - task_type=template_data["task"]["type"], - product_type="render", - product_name=product_name, - project_settings=context.data["project_settings"] - ) - - host_name = context.data["hostName"] - task_info = template_data.get("task") or {} - - template_name = publish.get_publish_template_name( - project_name, - host_name, - product_type, - task_info.get("name"), - task_info.get("type"), - ) - - template_data["version"] = version - template_data["subset"] = product_name - template_data["family"] = product_type - template_data["product"] = { - "name": product_name, - "type": product_type, - } - - render_dir_template = anatomy.get_template_item( - "publish", template_name, "directory" - ) - return render_dir_template.format_strict(template_data) - - @classmethod - def get_attribute_defs(cls): - return [ - EnumDef("publishJobState", - label="Publish Job State", - items=["Active", "Suspended"], - default="Active") - ] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py deleted file mode 100644 index fd89e3a2a7..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_connection.py +++ /dev/null @@ -1,52 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import PublishXmlValidationError - -from ayon_deadline.abstract_submit_deadline import requests_get - - -class ValidateDeadlineConnection(pyblish.api.InstancePlugin): - """Validate Deadline Web Service is running""" - - label = "Validate Deadline Web Service" - order = pyblish.api.ValidatorOrder - hosts = ["maya", "nuke", "aftereffects", "harmony", "fusion"] - families = ["renderlayer", "render", "render.farm"] - - # cache - responses = {} - - def process(self, instance): - if not instance.data.get("farm"): - self.log.debug("Should not be processed on farm, skipping.") - return - - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - kwargs = {} - if instance.data["deadline"]["require_authentication"]: - auth = instance.data["deadline"]["auth"] - kwargs["auth"] = auth - - if not auth[0]: - raise PublishXmlValidationError( - self, - "Deadline requires authentication. " - "At least username is required to be set in " - "Site Settings.") - - if deadline_url not in self.responses: - self.responses[deadline_url] = requests_get(deadline_url, **kwargs) - - response = self.responses[deadline_url] - if response.status_code == 401: - raise PublishXmlValidationError( - self, - "Deadline requires authentication. " - "Provided credentials are not working. " - "Please change them in Site Settings") - assert response.ok, "Response must be ok" - assert response.text.startswith("Deadline Web Service "), ( - "Web service did not respond with 'Deadline Web Service'" - ) diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py deleted file mode 100644 index c7445465c4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_deadline_pools.py +++ /dev/null @@ -1,84 +0,0 @@ -import pyblish.api - -from ayon_core.pipeline import ( - PublishXmlValidationError, - OptionalPyblishPluginMixin -) - - -class ValidateDeadlinePools(OptionalPyblishPluginMixin, - pyblish.api.InstancePlugin): - """Validate primaryPool and secondaryPool on instance. - - Values are on instance based on value insertion when Creating instance or - by Settings in CollectDeadlinePools. - """ - - label = "Validate Deadline Pools" - order = pyblish.api.ValidatorOrder - families = ["rendering", - "render.farm", - "render.frames_farm", - "renderFarm", - "renderlayer", - "maxrender", - "publish.hou"] - optional = True - - # cache - pools_per_url = {} - - def process(self, instance): - if not self.is_active(instance.data): - return - - if not instance.data.get("farm"): - self.log.debug("Skipping local instance.") - return - - deadline_url = instance.data["deadline"]["url"] - addons_manager = instance.context.data["ayonAddonsManager"] - deadline_addon = addons_manager["deadline"] - pools = self.get_pools( - deadline_addon, - deadline_url, - instance.data["deadline"].get("auth") - ) - - invalid_pools = {} - primary_pool = instance.data.get("primaryPool") - if primary_pool and primary_pool not in pools: - invalid_pools["primary"] = primary_pool - - secondary_pool = instance.data.get("secondaryPool") - if secondary_pool and secondary_pool not in pools: - invalid_pools["secondary"] = secondary_pool - - if invalid_pools: - message = "\n".join( - "{} pool '{}' not available on Deadline".format(key.title(), - pool) - for key, pool in invalid_pools.items() - ) - raise PublishXmlValidationError( - plugin=self, - message=message, - formatting_data={"pools_str": ", ".join(pools)} - ) - - def get_pools(self, deadline_addon, deadline_url, auth): - if deadline_url not in self.pools_per_url: - self.log.debug( - "Querying available pools for Deadline url: {}".format( - deadline_url) - ) - pools = deadline_addon.get_deadline_pools( - deadline_url, auth=auth, log=self.log - ) - # some DL return "none" as a pool name - if "none" not in pools: - pools.append("none") - self.log.info("Available pools: {}".format(pools)) - self.pools_per_url[deadline_url] = pools - - return self.pools_per_url[deadline_url] diff --git a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py b/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py deleted file mode 100644 index 3fd13cfa10..0000000000 --- a/server_addon/deadline/client/ayon_deadline/plugins/publish/validate_expected_and_rendered_files.py +++ /dev/null @@ -1,256 +0,0 @@ -import os -import requests - -import pyblish.api - -from ayon_core.lib import collect_frames -from ayon_deadline.abstract_submit_deadline import requests_get - - -class ValidateExpectedFiles(pyblish.api.InstancePlugin): - """Compare rendered and expected files""" - - label = "Validate rendered files from Deadline" - order = pyblish.api.ValidatorOrder - families = ["render"] - targets = ["deadline"] - - # check if actual frame range on render job wasn't different - # case when artists wants to render only subset of frames - allow_user_override = True - - def process(self, instance): - """Process all the nodes in the instance""" - - # get dependency jobs ids for retrieving frame list - dependent_job_ids = self._get_dependent_job_ids(instance) - - if not dependent_job_ids: - self.log.warning("No dependent jobs found for instance: {}" - "".format(instance)) - return - - # get list of frames from dependent jobs - frame_list = self._get_dependent_jobs_frames( - instance, dependent_job_ids) - - for repre in instance.data["representations"]: - expected_files = self._get_expected_files(repre) - - staging_dir = repre["stagingDir"] - existing_files = self._get_existing_files(staging_dir) - - if self.allow_user_override: - # We always check for user override because the user might have - # also overridden the Job frame list to be longer than the - # originally submitted frame range - # todo: We should first check if Job frame range was overridden - # at all so we don't unnecessarily override anything - file_name_template, frame_placeholder = \ - self._get_file_name_template_and_placeholder( - expected_files) - - if not file_name_template: - raise RuntimeError("Unable to retrieve file_name template" - "from files: {}".format(expected_files)) - - job_expected_files = self._get_job_expected_files( - file_name_template, - frame_placeholder, - frame_list) - - job_files_diff = job_expected_files.difference(expected_files) - if job_files_diff: - self.log.debug( - "Detected difference in expected output files from " - "Deadline job. Assuming an updated frame list by the " - "user. Difference: {}".format(sorted(job_files_diff)) - ) - - # Update the representation expected files - self.log.info("Update range from actual job range " - "to frame list: {}".format(frame_list)) - # single item files must be string not list - repre["files"] = (sorted(job_expected_files) - if len(job_expected_files) > 1 else - list(job_expected_files)[0]) - - # Update the expected files - expected_files = job_expected_files - - # We don't use set.difference because we do allow other existing - # files to be in the folder that we might not want to use. - missing = expected_files - existing_files - if missing: - raise RuntimeError( - "Missing expected files: {}\n" - "Expected files: {}\n" - "Existing files: {}".format( - sorted(missing), - sorted(expected_files), - sorted(existing_files) - ) - ) - - def _get_dependent_job_ids(self, instance): - """Returns list of dependent job ids from instance metadata.json - - Args: - instance (pyblish.api.Instance): pyblish instance - - Returns: - (list): list of dependent job ids - - """ - dependent_job_ids = [] - - # job_id collected from metadata.json - original_job_id = instance.data["render_job_id"] - - dependent_job_ids_env = os.environ.get("RENDER_JOB_IDS") - if dependent_job_ids_env: - dependent_job_ids = dependent_job_ids_env.split(',') - elif original_job_id: - dependent_job_ids = [original_job_id] - - return dependent_job_ids - - def _get_dependent_jobs_frames(self, instance, dependent_job_ids): - """Returns list of frame ranges from all render job. - - Render job might be re-submitted so job_id in metadata.json could be - invalid. GlobalJobPreload injects current job id to RENDER_JOB_IDS. - - Args: - instance (pyblish.api.Instance): pyblish instance - dependent_job_ids (list): list of dependent job ids - Returns: - (list) - """ - all_frame_lists = [] - - for job_id in dependent_job_ids: - job_info = self._get_job_info(instance, job_id) - frame_list = job_info["Props"].get("Frames") - if frame_list: - all_frame_lists.extend(frame_list.split(',')) - - return all_frame_lists - - def _get_job_expected_files(self, - file_name_template, - frame_placeholder, - frame_list): - """Calculates list of names of expected rendered files. - - Might be different from expected files from submission if user - explicitly and manually changed the frame list on the Deadline job. - - """ - # no frames in file name at all, eg 'renderCompositingMain.withLut.mov' - if not frame_placeholder: - return {file_name_template} - - real_expected_rendered = set() - src_padding_exp = "%0{}d".format(len(frame_placeholder)) - for frames in frame_list: - if '-' not in frames: # single frame - frames = "{}-{}".format(frames, frames) - - start, end = frames.split('-') - for frame in range(int(start), int(end) + 1): - ren_name = file_name_template.replace( - frame_placeholder, src_padding_exp % frame) - real_expected_rendered.add(ren_name) - - return real_expected_rendered - - def _get_file_name_template_and_placeholder(self, files): - """Returns file name with frame replaced with # and this placeholder""" - sources_and_frames = collect_frames(files) - - file_name_template = frame_placeholder = None - for file_name, frame in sources_and_frames.items(): - - # There might be cases where clique was unable to collect - # collections in `collect_frames` - thus we capture that case - if frame is not None: - frame_placeholder = "#" * len(frame) - - file_name_template = os.path.basename( - file_name.replace(frame, frame_placeholder)) - else: - file_name_template = file_name - break - - return file_name_template, frame_placeholder - - def _get_job_info(self, instance, job_id): - """Calls DL for actual job info for 'job_id' - - Might be different than job info saved in metadata.json if user - manually changes job pre/during rendering. - - Args: - instance (pyblish.api.Instance): pyblish instance - job_id (str): Deadline job id - - Returns: - (dict): Job info from Deadline - - """ - deadline_url = instance.data["deadline"]["url"] - assert deadline_url, "Requires Deadline Webservice URL" - - url = "{}/api/jobs?JobID={}".format(deadline_url, job_id) - try: - kwargs = {} - auth = instance.data["deadline"]["auth"] - if auth: - kwargs["auth"] = auth - response = requests_get(url, **kwargs) - except requests.exceptions.ConnectionError: - self.log.error("Deadline is not accessible at " - "{}".format(deadline_url)) - return {} - - if not response.ok: - self.log.error("Submission failed!") - self.log.error(response.status_code) - self.log.error(response.content) - raise RuntimeError(response.text) - - json_content = response.json() - if json_content: - return json_content.pop() - return {} - - def _get_existing_files(self, staging_dir): - """Returns set of existing file names from 'staging_dir'""" - existing_files = set() - for file_name in os.listdir(staging_dir): - existing_files.add(file_name) - return existing_files - - def _get_expected_files(self, repre): - """Returns set of file names in representation['files'] - - The representations are collected from `CollectRenderedFiles` using - the metadata.json file submitted along with the render job. - - Args: - repre (dict): The representation containing 'files' - - Returns: - set: Set of expected file_names in the staging directory. - - """ - expected_files = set() - - files = repre["files"] - if not isinstance(files, list): - files = [files] - - for file_name in files: - expected_files.add(file_name) - return expected_files diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/Ayon/Ayon.ico deleted file mode 100644 index aea977a1251232d0f3d78ea6cb124994f4d31eb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7679 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 dg=}F#T++JjmkP-0+ukjp@", str(self.GetStartFrame()), - arguments) - arguments = re.sub(r"<(?i)ENDFRAME>", str(self.GetEndFrame()), - arguments) - arguments = re.sub(r"<(?i)QUOTE>", "\"", arguments) - - arguments = self.ReplacePaddedFrame(arguments, - "<(?i)STARTFRAME%([0-9]+)>", - self.GetStartFrame()) - arguments = self.ReplacePaddedFrame(arguments, - "<(?i)ENDFRAME%([0-9]+)>", - self.GetEndFrame()) - - count = 0 - for filename in self.GetAuxiliaryFilenames(): - localAuxFile = Path.Combine(self.GetJobsDataDirectory(), filename) - arguments = re.sub(r"<(?i)AUXFILE" + str(count) + r">", - localAuxFile.replace("\\", "/"), arguments) - count += 1 - - return arguments - - def ReplacePaddedFrame(self, arguments, pattern, frame): - frameRegex = Regex(pattern) - while True: - frameMatch = frameRegex.Match(arguments) - if not frameMatch.Success: - break - paddingSize = int(frameMatch.Groups[1].Value) - if paddingSize > 0: - padding = StringUtils.ToZeroPaddedString( - frame, paddingSize, False) - else: - padding = str(frame) - arguments = arguments.replace( - frameMatch.Groups[0].Value, padding) - - return arguments - - def HandleProgress(self): - progress = float(self.GetRegexMatch(1)) - self.SetProgress(progress) diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.ico deleted file mode 100644 index 39d61592fe1addb07ed3ef93de362370485a23b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103192 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 JVj#NS{{bY%R3rcZ diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param deleted file mode 100644 index 24c59d2005..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.param +++ /dev/null @@ -1,38 +0,0 @@ -[About] -Type=label -Label=About -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=Celaction Plugin for Deadline -Description=Not configurable - -[ConcurrentTasks] -Type=label -Label=ConcurrentTasks -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=True -Description=Not configurable - -[Executable] -Type=filename -Label=Executable -Category=Config -CategoryOrder=0 -CategoryIndex=0 -Description=The command executable to run -Required=false -DisableIfBlank=true - -[RenderNameSeparator] -Type=string -Label=RenderNameSeparator -Category=Config -CategoryOrder=0 -CategoryIndex=1 -Description=The separator to use for naming -Required=false -DisableIfBlank=true -Default=. diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py deleted file mode 100644 index 2d0edd3dca..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/CelAction.py +++ /dev/null @@ -1,122 +0,0 @@ -from System.Text.RegularExpressions import * - -from Deadline.Plugins import * -from Deadline.Scripting import * - -import _winreg - -###################################################################### -# This is the function that Deadline calls to get an instance of the -# main DeadlinePlugin class. -###################################################################### - - -def GetDeadlinePlugin(): - return CelActionPlugin() - - -def CleanupDeadlinePlugin(deadlinePlugin): - deadlinePlugin.Cleanup() - -###################################################################### -# This is the main DeadlinePlugin class for the CelAction plugin. -###################################################################### - - -class CelActionPlugin(DeadlinePlugin): - - def __init__(self): - self.InitializeProcessCallback += self.InitializeProcess - self.RenderExecutableCallback += self.RenderExecutable - self.RenderArgumentCallback += self.RenderArgument - self.StartupDirectoryCallback += self.StartupDirectory - - def Cleanup(self): - for stdoutHandler in self.StdoutHandlers: - del stdoutHandler.HandleCallback - - del self.InitializeProcessCallback - del self.RenderExecutableCallback - del self.RenderArgumentCallback - del self.StartupDirectoryCallback - - def GetCelActionRegistryKey(self): - # Modify registry for frame separation - path = r'Software\CelAction\CelAction2D\User Settings' - _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, path) - regKey = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, path, 0, - _winreg.KEY_ALL_ACCESS) - return regKey - - def GetSeparatorValue(self, regKey): - useSeparator, _ = _winreg.QueryValueEx( - regKey, 'RenderNameUseSeparator') - separator, _ = _winreg.QueryValueEx(regKey, 'RenderNameSeparator') - - return useSeparator, separator - - def SetSeparatorValue(self, regKey, useSeparator, separator): - _winreg.SetValueEx(regKey, 'RenderNameUseSeparator', - 0, _winreg.REG_DWORD, useSeparator) - _winreg.SetValueEx(regKey, 'RenderNameSeparator', - 0, _winreg.REG_SZ, separator) - - def InitializeProcess(self): - # Set the plugin specific settings. - self.SingleFramesOnly = False - - # Set the process specific settings. - self.StdoutHandling = True - self.PopupHandling = True - - # Ignore 'celaction' Pop-up dialog - self.AddPopupIgnorer(".*Rendering.*") - self.AddPopupIgnorer(".*AutoRender.*") - - # Ignore 'celaction' Pop-up dialog - self.AddPopupIgnorer(".*Wait.*") - - # Ignore 'celaction' Pop-up dialog - self.AddPopupIgnorer(".*Timeline Scrub.*") - - celActionRegKey = self.GetCelActionRegistryKey() - - self.SetSeparatorValue(celActionRegKey, 1, self.GetConfigEntryWithDefault( - "RenderNameSeparator", ".").strip()) - - def RenderExecutable(self): - return RepositoryUtils.CheckPathMapping(self.GetConfigEntry("Executable").strip()) - - def RenderArgument(self): - arguments = RepositoryUtils.CheckPathMapping( - self.GetPluginInfoEntry("Arguments").strip()) - arguments = arguments.replace( - "", str(self.GetStartFrame())) - arguments = arguments.replace("", str(self.GetEndFrame())) - arguments = self.ReplacePaddedFrame( - arguments, "", self.GetStartFrame()) - arguments = self.ReplacePaddedFrame( - arguments, "", self.GetEndFrame()) - arguments = arguments.replace("", "\"") - return arguments - - def StartupDirectory(self): - return self.GetPluginInfoEntryWithDefault("StartupDirectory", "").strip() - - def ReplacePaddedFrame(self, arguments, pattern, frame): - frameRegex = Regex(pattern) - while True: - frameMatch = frameRegex.Match(arguments) - if frameMatch.Success: - paddingSize = int(frameMatch.Groups[1].Value) - if paddingSize > 0: - padding = StringUtils.ToZeroPaddedString( - frame, paddingSize, False) - else: - padding = str(frame) - arguments = arguments.replace( - frameMatch.Groups[0].Value, padding) - else: - break - - return arguments diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py deleted file mode 100644 index dbd1798608..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ /dev/null @@ -1,662 +0,0 @@ -# /usr/bin/env python3 -# -*- coding: utf-8 -*- -import os -import tempfile -from datetime import datetime -import subprocess -import json -import platform -import uuid -import re -from Deadline.Scripting import ( - RepositoryUtils, - FileUtils, - DirectoryUtils, -) -__version__ = "1.1.1" -VERSION_REGEX = re.compile( - r"(?P0|[1-9]\d*)" - r"\.(?P0|[1-9]\d*)" - r"\.(?P0|[1-9]\d*)" - r"(?:-(?P[a-zA-Z\d\-.]*))?" - r"(?:\+(?P[a-zA-Z\d\-.]*))?" -) - - -class OpenPypeVersion: - """Fake semver version class for OpenPype version purposes. - - The version - """ - def __init__(self, major, minor, patch, prerelease, origin=None): - self.major = major - self.minor = minor - self.patch = patch - self.prerelease = prerelease - - is_valid = True - if major is None or minor is None or patch is None: - is_valid = False - self.is_valid = is_valid - - if origin is None: - base = "{}.{}.{}".format(str(major), str(minor), str(patch)) - if not prerelease: - origin = base - else: - origin = "{}-{}".format(base, str(prerelease)) - - self.origin = origin - - @classmethod - def from_string(cls, version): - """Create an object of version from string. - - Args: - version (str): Version as a string. - - Returns: - Union[OpenPypeVersion, None]: Version object if input is nonempty - string otherwise None. - """ - - if not version: - return None - valid_parts = VERSION_REGEX.findall(version) - if len(valid_parts) != 1: - # Return invalid version with filled 'origin' attribute - return cls(None, None, None, None, origin=str(version)) - - # Unpack found version - major, minor, patch, pre, post = valid_parts[0] - prerelease = pre - # Post release is not important anymore and should be considered as - # part of prerelease - # - comparison is implemented to find suitable build and builds should - # never contain prerelease part so "not proper" parsing is - # acceptable for this use case. - if post: - prerelease = "{}+{}".format(pre, post) - - return cls( - int(major), int(minor), int(patch), prerelease, origin=version - ) - - def has_compatible_release(self, other): - """Version has compatible release as other version. - - Both major and minor versions must be exactly the same. In that case - a build can be considered as release compatible with any version. - - Args: - other (OpenPypeVersion): Other version. - - Returns: - bool: Version is release compatible with other version. - """ - - if self.is_valid and other.is_valid: - return self.major == other.major and self.minor == other.minor - return False - - def __bool__(self): - return self.is_valid - - def __repr__(self): - return "<{} {}>".format(self.__class__.__name__, self.origin) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return self.origin == other - return self.origin == other.origin - - def __lt__(self, other): - if not isinstance(other, self.__class__): - return None - - if not self.is_valid: - return True - - if not other.is_valid: - return False - - if self.origin == other.origin: - return None - - same_major = self.major == other.major - if not same_major: - return self.major < other.major - - same_minor = self.minor == other.minor - if not same_minor: - return self.minor < other.minor - - same_patch = self.patch == other.patch - if not same_patch: - return self.patch < other.patch - - if not self.prerelease: - return False - - if not other.prerelease: - return True - - pres = [self.prerelease, other.prerelease] - pres.sort() - return pres[0] == self.prerelease - - -def get_openpype_version_from_path(path, build=True): - """Get OpenPype version from provided path. - path (str): Path to scan. - build (bool, optional): Get only builds, not sources - - Returns: - Union[OpenPypeVersion, None]: version of OpenPype if found. - """ - - # fix path for application bundle on macos - if platform.system().lower() == "darwin": - path = os.path.join(path, "MacOS") - - version_file = os.path.join(path, "openpype", "version.py") - if not os.path.isfile(version_file): - return None - - # skip if the version is not build - exe = os.path.join(path, "openpype_console.exe") - if platform.system().lower() in ["linux", "darwin"]: - exe = os.path.join(path, "openpype_console") - - # if only builds are requested - if build and not os.path.isfile(exe): # noqa: E501 - print(" ! path is not a build: {}".format(path)) - return None - - version = {} - with open(version_file, "r") as vf: - exec(vf.read(), version) - - version_str = version.get("__version__") - if version_str: - return OpenPypeVersion.from_string(version_str) - return None - - -def get_openpype_executable(): - """Return OpenPype Executable from Event Plug-in Settings""" - config = RepositoryUtils.GetPluginConfig("OpenPype") - exe_list = config.GetConfigEntryWithDefault("OpenPypeExecutable", "") - dir_list = config.GetConfigEntryWithDefault( - "OpenPypeInstallationDirs", "") - - # clean '\ ' for MacOS pasting - if platform.system().lower() == "darwin": - exe_list = exe_list.replace("\\ ", " ") - dir_list = dir_list.replace("\\ ", " ") - return exe_list, dir_list - - -def get_openpype_versions(dir_list): - print(">>> Getting OpenPype executable ...") - openpype_versions = [] - - # special case of multiple install dirs - for dir_list in dir_list.split(","): - install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if install_dir: - print("--- Looking for OpenPype at: {}".format(install_dir)) - sub_dirs = [ - f.path for f in os.scandir(install_dir) - if f.is_dir() - ] - for subdir in sub_dirs: - version = get_openpype_version_from_path(subdir) - if not version: - continue - print(" - found: {} - {}".format(version, subdir)) - openpype_versions.append((version, subdir)) - return openpype_versions - - -def get_requested_openpype_executable( - exe, dir_list, requested_version -): - requested_version_obj = OpenPypeVersion.from_string(requested_version) - if not requested_version_obj: - print(( - ">>> Requested version '{}' does not match version regex '{}'" - ).format(requested_version, VERSION_REGEX)) - return None - - print(( - ">>> Scanning for compatible requested version {}" - ).format(requested_version)) - openpype_versions = get_openpype_versions(dir_list) - if not openpype_versions: - return None - - # if looking for requested compatible version, - # add the implicitly specified to the list too. - if exe: - exe_dir = os.path.dirname(exe) - print("Looking for OpenPype at: {}".format(exe_dir)) - version = get_openpype_version_from_path(exe_dir) - if version: - print(" - found: {} - {}".format(version, exe_dir)) - openpype_versions.append((version, exe_dir)) - - matching_item = None - compatible_versions = [] - for version_item in openpype_versions: - version, version_dir = version_item - if requested_version_obj.has_compatible_release(version): - compatible_versions.append(version_item) - if version == requested_version_obj: - # Store version item if version match exactly - # - break if is found matching version - matching_item = version_item - break - - if not compatible_versions: - return None - - compatible_versions.sort(key=lambda item: item[0]) - if matching_item: - version, version_dir = matching_item - print(( - "*** Found exact match build version {} in {}" - ).format(version_dir, version)) - - else: - version, version_dir = compatible_versions[-1] - - print(( - "*** Latest compatible version found is {} in {}" - ).format(version_dir, version)) - - # create list of executables for different platform and let - # Deadline decide. - exe_list = [ - os.path.join(version_dir, "openpype_console.exe"), - os.path.join(version_dir, "openpype_console"), - os.path.join(version_dir, "MacOS", "openpype_console") - ] - return FileUtils.SearchFileList(";".join(exe_list)) - - -def inject_openpype_environment(deadlinePlugin): - """ Pull env vars from OpenPype and push them to rendering process. - - Used for correct paths, configuration from OpenPype etc. - """ - job = deadlinePlugin.GetJob() - - print(">>> Injecting OpenPype environments ...") - try: - exe_list, dir_list = get_openpype_executable() - exe = FileUtils.SearchFileList(exe_list) - - requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") - if requested_version: - exe = get_requested_openpype_executable( - exe, dir_list, requested_version - ) - if exe is None: - raise RuntimeError(( - "Cannot find compatible version available for version {}" - " requested by the job. Please add it through plugin" - " configuration in Deadline or install it to configured" - " directory." - ).format(requested_version)) - - if not exe: - raise RuntimeError(( - "OpenPype executable was not found in the semicolon " - "separated list \"{}\"." - "The path to the render executable can be configured" - " from the Plugin Configuration in the Deadline Monitor." - ).format(";".join(exe_list))) - - print("--- OpenPype executable: {}".format(exe)) - - # tempfile.TemporaryFile cannot be used because of locking - temp_file_name = "{}_{}.json".format( - datetime.utcnow().strftime("%Y%m%d%H%M%S%f"), - str(uuid.uuid1()) - ) - export_url = os.path.join(tempfile.gettempdir(), temp_file_name) - print(">>> Temporary path: {}".format(export_url)) - - args = [ - "--headless", - "extractenvironments", - export_url - ] - - add_kwargs = { - "project": job.GetJobEnvironmentKeyValue("AVALON_PROJECT"), - "asset": job.GetJobEnvironmentKeyValue("AVALON_ASSET"), - "task": job.GetJobEnvironmentKeyValue("AVALON_TASK"), - "app": job.GetJobEnvironmentKeyValue("AVALON_APP_NAME"), - "envgroup": "farm" - } - - # use legacy IS_TEST env var to mark automatic tests for OP - if job.GetJobEnvironmentKeyValue("IS_TEST"): - args.append("--automatic-tests") - - if all(add_kwargs.values()): - for key, value in add_kwargs.items(): - args.extend(["--{}".format(key), value]) - else: - raise RuntimeError(( - "Missing required env vars: AVALON_PROJECT, AVALON_ASSET," - " AVALON_TASK, AVALON_APP_NAME" - )) - - openpype_mongo = job.GetJobEnvironmentKeyValue("OPENPYPE_MONGO") - if openpype_mongo: - # inject env var for OP extractenvironments - # SetEnvironmentVariable is important, not SetProcessEnv... - deadlinePlugin.SetEnvironmentVariable("OPENPYPE_MONGO", - openpype_mongo) - - if not os.environ.get("OPENPYPE_MONGO"): - print(">>> Missing OPENPYPE_MONGO env var, process won't work") - - os.environ["AVALON_TIMEOUT"] = "5000" - - args_str = subprocess.list2cmdline(args) - print(">>> Executing: {} {}".format(exe, args_str)) - process_exitcode = deadlinePlugin.RunProcess( - exe, args_str, os.path.dirname(exe), -1 - ) - - if process_exitcode != 0: - raise RuntimeError( - "Failed to run OpenPype process to extract environments." - ) - - print(">>> Loading file ...") - with open(export_url) as fp: - contents = json.load(fp) - - for key, value in contents.items(): - deadlinePlugin.SetProcessEnvironmentVariable(key, value) - - if "PATH" in contents: - # Set os.environ[PATH] so studio settings' path entries - # can be used to define search path for executables. - print(f">>> Setting 'PATH' Environment to: {contents['PATH']}") - os.environ["PATH"] = contents["PATH"] - - script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") - if script_url: - script_url = script_url.format(**contents).replace("\\", "/") - print(">>> Setting script path {}".format(script_url)) - job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) - - print(">>> Removing temporary file") - os.remove(export_url) - - print(">> Injection end.") - except Exception as e: - if hasattr(e, "output"): - print(">>> Exception {}".format(e.output)) - import traceback - print(traceback.format_exc()) - print("!!! Injection failed.") - RepositoryUtils.FailJob(job) - raise - - -def inject_ayon_environment(deadlinePlugin): - """ Pull env vars from AYON and push them to rendering process. - - Used for correct paths, configuration from AYON etc. - """ - job = deadlinePlugin.GetJob() - - print(">>> Injecting AYON environments ...") - try: - exe_list = get_ayon_executable() - exe = FileUtils.SearchFileList(exe_list) - - if not exe: - raise RuntimeError(( - "Ayon executable was not found in the semicolon " - "separated list \"{}\"." - "The path to the render executable can be configured" - " from the Plugin Configuration in the Deadline Monitor." - ).format(exe_list)) - - print("--- Ayon executable: {}".format(exe)) - - ayon_bundle_name = job.GetJobEnvironmentKeyValue("AYON_BUNDLE_NAME") - if not ayon_bundle_name: - raise RuntimeError( - "Missing env var in job properties AYON_BUNDLE_NAME" - ) - - config = RepositoryUtils.GetPluginConfig("Ayon") - ayon_server_url = ( - job.GetJobEnvironmentKeyValue("AYON_SERVER_URL") or - config.GetConfigEntryWithDefault("AyonServerUrl", "") - ) - ayon_api_key = ( - job.GetJobEnvironmentKeyValue("AYON_API_KEY") or - config.GetConfigEntryWithDefault("AyonApiKey", "") - ) - - if not all([ayon_server_url, ayon_api_key]): - raise RuntimeError(( - "Missing required values for server url and api key. " - "Please fill in Ayon Deadline plugin or provide by " - "AYON_SERVER_URL and AYON_API_KEY" - )) - - # tempfile.TemporaryFile cannot be used because of locking - temp_file_name = "{}_{}.json".format( - datetime.utcnow().strftime("%Y%m%d%H%M%S%f"), - str(uuid.uuid1()) - ) - export_url = os.path.join(tempfile.gettempdir(), temp_file_name) - print(">>> Temporary path: {}".format(export_url)) - - add_kwargs = { - "envgroup": "farm", - } - # Support backwards compatible keys - for key, env_keys in ( - ("project", ["AYON_PROJECT_NAME", "AVALON_PROJECT"]), - ("folder", ["AYON_FOLDER_PATH", "AVALON_ASSET"]), - ("task", ["AYON_TASK_NAME", "AVALON_TASK"]), - ("app", ["AYON_APP_NAME", "AVALON_APP_NAME"]), - ): - value = "" - for env_key in env_keys: - value = job.GetJobEnvironmentKeyValue(env_key) - if value: - break - add_kwargs[key] = value - - if not all(add_kwargs.values()): - raise RuntimeError(( - "Missing required env vars: AYON_PROJECT_NAME," - " AYON_FOLDER_PATH, AYON_TASK_NAME, AYON_APP_NAME" - )) - - # Use applications addon arguments - # TODO validate if applications addon should be used - args = [ - "--headless", - "addon", - "applications", - "extractenvironments", - export_url - ] - # Backwards compatibility for older versions - legacy_args = [ - "--headless", - "extractenvironments", - export_url - ] - - for key, value in add_kwargs.items(): - args.extend(["--{}".format(key), value]) - # Legacy arguments expect '--asset' instead of '--folder' - if key == "folder": - key = "asset" - legacy_args.extend(["--{}".format(key), value]) - - environment = { - "AYON_SERVER_URL": ayon_server_url, - "AYON_API_KEY": ayon_api_key, - "AYON_BUNDLE_NAME": ayon_bundle_name, - } - - automatic_tests = job.GetJobEnvironmentKeyValue("AYON_IN_TESTS") - if automatic_tests: - environment["AYON_IN_TESTS"] = automatic_tests - for env, val in environment.items(): - # Add the env var for the Render Plugin that is about to render - deadlinePlugin.SetEnvironmentVariable(env, val) - # Add the env var for current calls to `DeadlinePlugin.RunProcess` - deadlinePlugin.SetProcessEnvironmentVariable(env, val) - - args_str = subprocess.list2cmdline(args) - print(">>> Executing: {} {}".format(exe, args_str)) - process_exitcode = deadlinePlugin.RunProcess( - exe, args_str, os.path.dirname(exe), -1 - ) - - if process_exitcode != 0: - print( - "Failed to run AYON process to extract environments. Trying" - " to use legacy arguments." - ) - legacy_args_str = subprocess.list2cmdline(legacy_args) - process_exitcode = deadlinePlugin.RunProcess( - exe, legacy_args_str, os.path.dirname(exe), -1 - ) - if process_exitcode != 0: - raise RuntimeError( - "Failed to run AYON process to extract environments." - ) - - print(">>> Loading file ...") - with open(export_url) as fp: - contents = json.load(fp) - - for key, value in contents.items(): - deadlinePlugin.SetProcessEnvironmentVariable(key, value) - - if "PATH" in contents: - # Set os.environ[PATH] so studio settings' path entries - # can be used to define search path for executables. - print(f">>> Setting 'PATH' Environment to: {contents['PATH']}") - os.environ["PATH"] = contents["PATH"] - - script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") - if script_url: - script_url = script_url.format(**contents).replace("\\", "/") - print(">>> Setting script path {}".format(script_url)) - job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) - - print(">>> Removing temporary file") - os.remove(export_url) - - print(">> Injection end.") - except Exception as e: - if hasattr(e, "output"): - print(">>> Exception {}".format(e.output)) - import traceback - print(traceback.format_exc()) - print("!!! Injection failed.") - RepositoryUtils.FailJob(job) - raise - - -def get_ayon_executable(): - """Return AYON Executable from Event Plug-in Settings - - Returns: - list[str]: AYON executable paths. - - Raises: - RuntimeError: When no path configured at all. - - """ - config = RepositoryUtils.GetPluginConfig("Ayon") - exe_list = config.GetConfigEntryWithDefault("AyonExecutable", "") - - if not exe_list: - raise RuntimeError( - "Path to AYON executable not configured." - "Please set it in Ayon Deadline Plugin." - ) - - # clean '\ ' for MacOS pasting - if platform.system().lower() == "darwin": - exe_list = exe_list.replace("\\ ", " ") - - # Expand user paths - expanded_paths = [] - for path in exe_list.split(";"): - if path.startswith("~"): - path = os.path.expanduser(path) - expanded_paths.append(path) - return ";".join(expanded_paths) - - -def inject_render_job_id(deadlinePlugin): - """Inject dependency ids to publish process as env var for validation.""" - print(">>> Injecting render job id ...") - job = deadlinePlugin.GetJob() - - dependency_ids = job.JobDependencyIDs - print(">>> Dependency IDs: {}".format(dependency_ids)) - render_job_ids = ",".join(dependency_ids) - - deadlinePlugin.SetProcessEnvironmentVariable( - "RENDER_JOB_IDS", render_job_ids - ) - print(">>> Injection end.") - - -def __main__(deadlinePlugin): - print("*** GlobalJobPreload {} start ...".format(__version__)) - print(">>> Getting job ...") - job = deadlinePlugin.GetJob() - - openpype_render_job = job.GetJobEnvironmentKeyValue( - "OPENPYPE_RENDER_JOB") - openpype_publish_job = job.GetJobEnvironmentKeyValue( - "OPENPYPE_PUBLISH_JOB") - openpype_remote_job = job.GetJobEnvironmentKeyValue( - "OPENPYPE_REMOTE_PUBLISH") - - if openpype_publish_job == "1" and openpype_render_job == "1": - raise RuntimeError( - "Misconfiguration. Job couldn't be both render and publish." - ) - - if openpype_publish_job == "1": - inject_render_job_id(deadlinePlugin) - if openpype_render_job == "1" or openpype_remote_job == "1": - inject_openpype_environment(deadlinePlugin) - - ayon_render_job = job.GetJobEnvironmentKeyValue("AYON_RENDER_JOB") - ayon_publish_job = job.GetJobEnvironmentKeyValue("AYON_PUBLISH_JOB") - ayon_remote_job = job.GetJobEnvironmentKeyValue("AYON_REMOTE_PUBLISH") - - if ayon_publish_job == "1" and ayon_render_job == "1": - raise RuntimeError( - "Misconfiguration. Job couldn't be both render and publish." - ) - - if ayon_publish_job == "1": - inject_render_job_id(deadlinePlugin) - if ayon_render_job == "1" or ayon_remote_job == "1": - inject_ayon_environment(deadlinePlugin) diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.ico deleted file mode 100644 index cf6f6bfcfa7cb19da3d76a576814a1473afc0d7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmbtUze@sP7=FaS;f8c5h?a(iT3Q++Y-k8U`U6@j=?A8Cb)lZ8|A7tFRzpK%KZc7k zm42wHK}56!&Q1-XjndR0*ZUzKd|jsy-tpY~zR&wSAMf{ZcYuv=@%+FN!N9I?M?6NgnA_@od)18#nCLsU~rTuPbmmz z6`Ykc7*;51XdW+PlFfb-!bv$!egO|LH1>TMFUIgC@;oQc5#RUjA_i%OKf3EJtY<@r za}STSW@B$21KiJIMuEorEEY7pFL)q%hvCRaQ5Ab>1*+&}xu|<2zb<2rYc6DXa}hk> zNAPqPMw)4PPmFpHv&>7nGCSAQCN@N5s18oSC3?*HZ5Y`DAKZl~KE#<>PBY}pdUNd= zgG6V$ZT8ZceIKr3)U3Cv$-iUvGtNqD#U=c%-hzr6UxW7YnV3PZm9`ysC1Z*EFJ-K> n)3xL2JG19iyyR58vb>$H6QA^B;(V?3j(5^^(=-1M{#*Y5EFN;S diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options deleted file mode 100644 index efd44b4f94..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.options +++ /dev/null @@ -1,532 +0,0 @@ -[SceneFile] -Type=filename -Label=Scene Filename -Category=Global Settings -CategoryOrder=0 -Index=0 -Description=The scene filename as it exists on the network. -Required=false -DisableIfBlank=true - -[Environment] -Type=filename -Label=Scene Environment -Category=Global Settings -CategoryOrder=0 -Index=1 -Description=The Environment for the scene. -Required=false -DisableIfBlank=true - -[Job] -Type=filename -Label=Scene Job -Category=Global Settings -CategoryOrder=0 -Index=2 -Description=The Job that the scene belongs to. -Required=false -DisableIfBlank=true - -[SceneName] -Type=filename -Label=Scene Name -Category=Global Settings -CategoryOrder=0 -Index=3 -Description=The name of the scene to render -Required=false -DisableIfBlank=true - -[SceneVersion] -Type=filename -Label=Scene Version -Category=Global Settings -CategoryOrder=0 -Index=4 -Description=The version of the scene to render. -Required=false -DisableIfBlank=true - -[Version] -Type=enum -Values=10;11;12 -Label=Harmony Version -Category=Global Settings -CategoryOrder=0 -Index=5 -Description=The version of Harmony to use. -Required=false -DisableIfBlank=true - -[IsDatabase] -Type=Boolean -Label=Is Database Scene -Category=Global Settings -CategoryOrder=0 -Index=6 -Description=Whether or not the scene is in the database or not -Required=false -DisableIfBlank=true - -[Camera] -Type=string -Label=Camera -Category=Render Settings -CategoryOrder=1 -Index=0 -Description=Specifies the camera to use for rendering images. If Blank, the scene will be rendered with the current Camera. -Required=false -DisableIfBlank=true - -[UsingResPreset] -Type=Boolean -Label=Use Resolution Preset -Category=Render Settings -CategoryOrder=1 -Index=1 -Description=Whether or not you are using a resolution preset. -Required=false -DisableIfBlank=true - -[ResolutionName] -Type=enum -Values=HDTV_1080p24;HDTV_1080p25;HDTV_720p24;4K_UHD;8K_UHD;DCI_2K;DCI_4K;film-2K;film-4K;film-1.33_H;film-1.66_H;film-1.66_V;Cineon;NTSC;PAL;2160p;1440p;1080p;720p;480p;360p;240p;low;Web_Video;Game_512;Game_512_Ortho;WebCC_Preview;Custom -Label=Resolution Preset -Category=Render Settings -CategoryOrder=1 -Index=2 -Description=The resolution preset to use. -Required=true -Default=HDTV_1080p24 - -[PresetName] -Type=string -Label=Preset Name -Category=Render Settings -CategoryOrder=1 -Index=3 -Description=Specify the custom resolution name. -Required=true -Default= - -[ResolutionX] -Type=integer -Label=Resolution X -Minimum=0 -Maximum=1000000 -Category=Render Settings -CategoryOrder=1 -Index=4 -Description=Specifies the width of the rendered images. If 0, then the current resolution and Field of view will be used. -Required=true -Default=1920 - -[ResolutionY] -Type=integer -Label=Resolution Y -Minimum=0 -Maximum=1000000 -Category=Render Settings -CategoryOrder=1 -Index=5 -Description=Specifies the height of the rendered images. If 0, then the current resolution and Field of view will be used. -Required=true -Default=1080 - -[FieldOfView] -Type=float -Label=Field Of View -Minimum=0 -Maximum=89 -DecimalPlaces=2 -Category=Render Settings -CategoryOrder=1 -Index=6 -Description=Specifies the field of view of the rendered images. If 0, then the current resolution and Field of view will be used. -Required=true -Default=41.11 - -[Output0Node] -Type=string -Label=Render Node 0 Name -Category=Output Settings -CategoryOrder=2 -Index=0 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output0Type] -Type=enum -Values=Image;Movie -Label=Render Node 0 Type -Category=Output Settings -CategoryOrder=2 -Index=1 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output0Path] -Type=string -Label=Render Node 0 Path -Category=Output Settings -CategoryOrder=2 -Index=2 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output0LeadingZero] -Type=integer -Label=Render Node 0 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=3 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output0Format] -Type=string -Label=Render Node 0 Format -Category=Output Settings -CategoryOrder=2 -Index=4 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output0StartFrame] -Type=integer -Label=Render Node 0 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=5 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output1Node] -Type=string -Label=Render Node 1 Name -Category=Output Settings -CategoryOrder=2 -Index=6 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output1Type] -Type=enum -Values=Image;Movie -Label=Render Node 1 Type -Category=Output Settings -CategoryOrder=2 -Index=7 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output1Path] -Type=string -Label=Render Node 1 Path -Category=Output Settings -CategoryOrder=2 -Index=8 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output1LeadingZero] -Type=integer -Label=Render Node 1 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=9 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output1Format] -Type=string -Label=Render Node 1 Format -Category=Output Settings -CategoryOrder=2 -Index=10 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output1StartFrame] -Type=integer -Label=Render Node 1 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=11 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output2Node] -Type=string -Label=Render Node 2 Name -Category=Output Settings -CategoryOrder=2 -Index=12 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output2Type] -Type=enum -Values=Image;Movie -Label=Render Node 2 Type -Category=Output Settings -CategoryOrder=2 -Index=13 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output2Path] -Type=string -Label=Render Node 2 Path -Category=Output Settings -CategoryOrder=2 -Index=14 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output2LeadingZero] -Type=integer -Label=Render Node 2 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=15 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output2Format] -Type=string -Label=Render Node 2 Format -Category=Output Settings -CategoryOrder=2 -Index=16 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output2StartFrame] -Type=integer -Label=Render Node 2 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=17 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output3Node] -Type=string -Label=Render Node 3 Name -Category=Output Settings -CategoryOrder=2 -Index=18 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output3Type] -Type=enum -Values=Image;Movie -Label=Render Node 3 Type -Category=Output Settings -CategoryOrder=2 -Index=19 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output3Path] -Type=string -Label=Render Node 3 Path -Category=Output Settings -CategoryOrder=2 -Index=20 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output3LeadingZero] -Type=integer -Label=Render Node 3 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=21 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output3Format] -Type=string -Label=Render Node 3 Format -Category=Output Settings -CategoryOrder=2 -Index=22 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output3StartFrame] -Type=integer -Label=Render Node 3 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=23 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output4Node] -Type=string -Label=Render Node 4 Name -Category=Output Settings -CategoryOrder=2 -Index=24 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output4Type] -Type=enum -Values=Image;Movie -Label=Render Node 4 Type -Category=Output Settings -CategoryOrder=2 -Index=25 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output4Path] -Type=string -Label=Render Node 4 Path -Category=Output Settings -CategoryOrder=2 -Index=26 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output4LeadingZero] -Type=integer -Label=Render Node 4 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=27 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output4Format] -Type=string -Label=Render Node 4 Format -Category=Output Settings -CategoryOrder=2 -Index=28 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output4StartFrame] -Type=integer -Label=Render Node 4 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=29 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true - -[Output5Node] -Type=string -Label=Render Node 5 Name -Category=Output Settings -CategoryOrder=2 -Index=30 -Description=The name of the render node. -Required=false -DisableIfBlank=true - -[Output5Type] -Type=enum -Values=Image;Movie -Label=Render Node 5 Type -Category=Output Settings -CategoryOrder=2 -Index=31 -Description=The type of output that the render node is producing. -Required=false -DisableIfBlank=true - -[Output5Path] -Type=string -Label=Render Node 5 Path -Category=Output Settings -CategoryOrder=2 -Index=32 -Description=The output path and file name of the output files. -Required=false -DisableIfBlank=true - -[Output5LeadingZero] -Type=integer -Label=Render Node 5 Leading Zeroes -Category=Output Settings -CategoryOrder=2 -Minimum=0 -Maximum=5 -Index=33 -Description=The number of leading zeroes for a 1 digit frame number. (1 less then the full padded length) -Required=false -DisableIfBlank=true - -[Output5Format] -Type=string -Label=Render Node 5 Format -Category=Output Settings -CategoryOrder=2 -Index=34 -Description=The format for the rendered output images. -Required=false -DisableIfBlank=true - -[Output5StartFrame] -Type=integer -Label=Render Node 5 Start Frame -Category=Output Settings -CategoryOrder=2 -Minimum=1 -Index=35 -Description=The frame that will correspond to frame one when numbering. If this value is not 1 then the monitor's job output features will not work properly. -Required=false -DisableIfBlank=true \ No newline at end of file diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param deleted file mode 100644 index 43a54a464e..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.param +++ /dev/null @@ -1,98 +0,0 @@ -[About] -Type=label -Label=About -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=Harmony Render Plugin for Deadline -Description=Not configurable - -[ConcurrentTasks] -Type=label -Label=ConcurrentTasks -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=True -Description=Not configurable - -[Harmony_RenderExecutable_10] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=0 -Label=Harmony 10 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 10.0\win64\bin\Stage.exe - -[Harmony_RenderExecutable_11] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=1 -Label=Harmony 11 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 11.0\win64\bin\Stage.exe - -[Harmony_RenderExecutable_12] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=2 -Label=Harmony 12 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 12.0 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 12.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_12/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_14] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=3 -Label=Harmony 14 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 14.0 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 14.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_14/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_15] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 15 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=C:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 15.0 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 15.0 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_15.0/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_17] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 17 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 17 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_17/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_20] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 20 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 20 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_20/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_21] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 21 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 21 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_21/lnx86_64/bin/HarmonyPremium - -[Harmony_RenderExecutable_22] -Type=multilinemultifilename -Category=Render Executables -CategoryOrder=0 -Index=4 -Label=Harmony 22 Render Executable -Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 22 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 22 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_22/lnx86_64/bin/HarmonyPremium diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py deleted file mode 100644 index d9fd0b49ef..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/HarmonyAYON.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -from System import * -from System.Diagnostics import * -from System.IO import * -from System.Text import * - -from Deadline.Plugins import * -from Deadline.Scripting import * - -def GetDeadlinePlugin(): - return HarmonyAYONPlugin() - -def CleanupDeadlinePlugin(deadlinePlugin): - deadlinePlugin.Cleanup() - -class HarmonyAYONPlugin(DeadlinePlugin): - - def __init__( self ): - super().__init__() - self.InitializeProcessCallback += self.InitializeProcess - self.RenderExecutableCallback += self.RenderExecutable - self.RenderArgumentCallback += self.RenderArgument - self.CheckExitCodeCallback += self.CheckExitCode - - def Cleanup( self ): - print("Cleanup") - for stdoutHandler in self.StdoutHandlers: - del stdoutHandler.HandleCallback - - del self.InitializeProcessCallback - del self.RenderExecutableCallback - del self.RenderArgumentCallback - - def CheckExitCode( self, exitCode ): - print("check code") - if exitCode != 0: - if exitCode == 100: - self.LogInfo( "Renderer reported an error with error code 100. This will be ignored, since the option to ignore it is specified in the Job Properties." ) - else: - self.FailRender( "Renderer returned non-zero error code %d. Check the renderer's output." % exitCode ) - - def InitializeProcess( self ): - self.PluginType = PluginType.Simple - self.StdoutHandling = True - self.PopupHandling = True - - self.AddStdoutHandlerCallback( "Rendered frame ([0-9]+)" ).HandleCallback += self.HandleStdoutProgress - - def HandleStdoutProgress( self ): - startFrame = self.GetStartFrame() - endFrame = self.GetEndFrame() - if( endFrame - startFrame + 1 != 0 ): - self.SetProgress( 100 * ( int(self.GetRegexMatch(1)) - startFrame + 1 ) / ( endFrame - startFrame + 1 ) ) - - def RenderExecutable( self ): - version = int( self.GetPluginInfoEntry( "Version" ) ) - exe = "" - exeList = self.GetConfigEntry( "Harmony_RenderExecutable_" + str(version) ) - exe = FileUtils.SearchFileList( exeList ) - if( exe == "" ): - self.FailRender( "Harmony render executable was not found in the configured separated list \"" + exeList + "\". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor." ) - return exe - - def RenderArgument( self ): - renderArguments = "-batch" - - if self.GetBooleanPluginInfoEntryWithDefault( "UsingResPreset", False ): - resName = self.GetPluginInfoEntryWithDefault( "ResolutionName", "HDTV_1080p24" ) - if resName == "Custom": - renderArguments += " -res " + self.GetPluginInfoEntryWithDefault( "PresetName", "HDTV_1080p24" ) - else: - renderArguments += " -res " + resName - else: - resolutionX = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionX", -1 ) - resolutionY = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionY", -1 ) - fov = self.GetFloatPluginInfoEntryWithDefault( "FieldOfView", -1 ) - - if resolutionX > 0 and resolutionY > 0 and fov > 0: - renderArguments += " -res " + str( resolutionX ) + " " + str( resolutionY ) + " " + str( fov ) - - camera = self.GetPluginInfoEntryWithDefault( "Camera", "" ) - - if not camera == "": - renderArguments += " -camera " + camera - - startFrame = str( self.GetStartFrame() ) - endFrame = str( self.GetEndFrame() ) - - renderArguments += " -frames " + startFrame + " " + endFrame - - if not self.GetBooleanPluginInfoEntryWithDefault( "IsDatabase", False ): - sceneFilename = self.GetPluginInfoEntryWithDefault( "SceneFile", self.GetDataFilename() ) - sceneFilename = RepositoryUtils.CheckPathMapping( sceneFilename ) - renderArguments += " \"" + sceneFilename + "\"" - else: - environment = self.GetPluginInfoEntryWithDefault( "Environment", "" ) - renderArguments += " -env " + environment - job = self.GetPluginInfoEntryWithDefault( "Job", "" ) - renderArguments += " -job " + job - scene = self.GetPluginInfoEntryWithDefault( "SceneName", "" ) - renderArguments += " -scene " + scene - version = self.GetPluginInfoEntryWithDefault( "SceneVersion", "" ) - renderArguments += " -version " + version - - #tempSceneDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) ) - #preRenderScript = - rendernodeNum = 0 - scriptBuilder = StringBuilder() - - while True: - nodeName = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Node", "" ) - if nodeName == "": - break - nodeType = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Type", "Image" ) - if nodeType == "Image": - nodePath = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Path", "" ) - nodeLeadingZero = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "LeadingZero", "" ) - nodeFormat = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Format", "" ) - nodeStartFrame = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "StartFrame", "" ) - - if not nodePath == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingName\", 1, \"" + nodePath + "\" );") - - if not nodeLeadingZero == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"leadingZeros\", 1, \"" + nodeLeadingZero + "\" );") - - if not nodeFormat == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingType\", 1, \"" + nodeFormat + "\" );") - - if not nodeStartFrame == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"start\", 1, \"" + nodeStartFrame + "\" );") - - if nodeType == "Movie": - nodePath = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Path", "" ) - if not nodePath == "": - scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"moviePath\", 1, \"" + nodePath + "\" );") - - rendernodeNum += 1 - - tempDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) ) - preRenderScriptName = Path.Combine( tempDirectory, "preRenderScript.txt" ) - - File.WriteAllText( preRenderScriptName, scriptBuilder.ToString() ) - - preRenderInlineScript = self.GetPluginInfoEntryWithDefault( "PreRenderInlineScript", "" ) - if preRenderInlineScript: - renderArguments += " -preRenderInlineScript \"" + preRenderInlineScript +"\"" - - renderArguments += " -preRenderScript \"" + preRenderScriptName +"\"" - - return renderArguments diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico deleted file mode 100644 index de860673c4f25ed881e975ea4b17e8673cefb6c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126987 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 CpbIwu diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options deleted file mode 100644 index 0b631ba66d..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options +++ /dev/null @@ -1,35 +0,0 @@ -[OIIOToolPath] -Type=filename -Label=OIIO Tool location -Category=OIIO -Index=0 -Description=OIIO Tool executable to use. -Required=false -DisableIfBlank=true - -[OutputFile] -Type=filenamesave -Label=Output File -Category=Output -Index=0 -Description=The scene filename as it exists on the network -Required=false -DisableIfBlank=true - -[CleanupTiles] -Type=boolean -Category=Options -Index=0 -Label=Cleanup Tiles -Required=false -DisableIfBlank=true -Description=If enabled, the OpenPype Tile Assembler will cleanup all tiles after assembly. - -[Renderer] -Type=string -Label=Renderer -Category=Quicktime Info -Index=0 -Description=Renderer name -Required=false -DisableIfBlank=true diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param deleted file mode 100644 index 66a3342e38..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param +++ /dev/null @@ -1,17 +0,0 @@ -[About] -Type=label -Label=About -Category=About Plugin -CategoryOrder=-1 -Index=0 -Default=OpenPype Tile Assembler Plugin for Deadline -Description=Not configurable - -[OIIOTool_RenderExecutable] -Type=multilinemultifilename -Label=OIIO Tool Executable -Category=Render Executables -CategoryOrder=0 -Default=C:\Program Files\OIIO\bin\oiiotool.exe;/usr/bin/oiiotool -Description=The path to the Open Image IO Tool executable file used for rendering. Enter alternative paths on separate lines. -W diff --git a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py b/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py deleted file mode 100644 index f146aef7b4..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py +++ /dev/null @@ -1,457 +0,0 @@ -# -*- coding: utf-8 -*- -"""Tile Assembler Plugin using Open Image IO tool. - -Todo: - Currently we support only EXRs with their data window set. -""" -import os -import re -import subprocess -import xml.etree.ElementTree - -from System.IO import Path - -from Deadline.Plugins import DeadlinePlugin -from Deadline.Scripting import ( - FileUtils, RepositoryUtils, SystemUtils) - - -version_major = 1 -version_minor = 0 -version_patch = 0 -version_string = "{}.{}.{}".format(version_major, version_minor, version_patch) -STRING_TAGS = { - "format" -} -INT_TAGS = { - "x", "y", "z", - "width", "height", "depth", - "full_x", "full_y", "full_z", - "full_width", "full_height", "full_depth", - "tile_width", "tile_height", "tile_depth", - "nchannels", - "alpha_channel", - "z_channel", - "deep", - "subimages", -} - - -XML_CHAR_REF_REGEX_HEX = re.compile(r"&#x?[0-9a-fA-F]+;") - -# Regex to parse array attributes -ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$") - - -def convert_value_by_type_name(value_type, value): - """Convert value to proper type based on type name. - - In some cases value types have custom python class. - """ - - # Simple types - if value_type == "string": - return value - - if value_type == "int": - return int(value) - - if value_type == "float": - return float(value) - - # Vectors will probably have more types - if value_type in ("vec2f", "float2"): - return [float(item) for item in value.split(",")] - - # Matrix should be always have square size of element 3x3, 4x4 - # - are returned as list of lists - if value_type == "matrix": - output = [] - current_index = -1 - parts = value.split(",") - parts_len = len(parts) - if parts_len == 1: - divisor = 1 - elif parts_len == 4: - divisor = 2 - elif parts_len == 9: - divisor = 3 - elif parts_len == 16: - divisor = 4 - else: - print("Unknown matrix resolution {}. Value: \"{}\"".format( - parts_len, value - )) - for part in parts: - output.append(float(part)) - return output - - for idx, item in enumerate(parts): - list_index = idx % divisor - if list_index > current_index: - current_index = list_index - output.append([]) - output[list_index].append(float(item)) - return output - - if value_type == "rational2i": - parts = value.split("/") - top = float(parts[0]) - bottom = 1.0 - if len(parts) != 1: - bottom = float(parts[1]) - return float(top) / float(bottom) - - if value_type == "vector": - parts = [part.strip() for part in value.split(",")] - output = [] - for part in parts: - if part == "-nan": - output.append(None) - continue - try: - part = float(part) - except ValueError: - pass - output.append(part) - return output - - if value_type == "timecode": - return value - - # Array of other types is converted to list - re_result = ARRAY_TYPE_REGEX.findall(value_type) - if re_result: - array_type = re_result[0] - output = [] - for item in value.split(","): - output.append( - convert_value_by_type_name(array_type, item) - ) - return output - - print(( - "Dev note (missing implementation):" - " Unknown attrib type \"{}\". Value: {}" - ).format(value_type, value)) - return value - - -def parse_oiio_xml_output(xml_string): - """Parse xml output from OIIO info command.""" - output = {} - if not xml_string: - return output - - # Fix values with ampresand (lazy fix) - # - oiiotool exports invalid xml which ElementTree can't handle - # e.g. "" - # WARNING: this will affect even valid character entities. If you need - # those values correctly, this must take care of valid character ranges. - # See https://github.com/pypeclub/OpenPype/pull/2729 - matches = XML_CHAR_REF_REGEX_HEX.findall(xml_string) - for match in matches: - new_value = match.replace("&", "&") - xml_string = xml_string.replace(match, new_value) - - tree = xml.etree.ElementTree.fromstring(xml_string) - attribs = {} - output["attribs"] = attribs - for child in tree: - tag_name = child.tag - if tag_name == "attrib": - attrib_def = child.attrib - value = convert_value_by_type_name( - attrib_def["type"], child.text - ) - - attribs[attrib_def["name"]] = value - continue - - # Channels are stored as tex on each child - if tag_name == "channelnames": - value = [] - for channel in child: - value.append(channel.text) - - # Convert known integer type tags to int - elif tag_name in INT_TAGS: - value = int(child.text) - - # Keep value of known string tags - elif tag_name in STRING_TAGS: - value = child.text - - # Keep value as text for unknown tags - # - feel free to add more tags - else: - value = child.text - print(( - "Dev note (missing implementation):" - " Unknown tag \"{}\". Value \"{}\"" - ).format(tag_name, value)) - - output[child.tag] = value - - return output - - -def info_about_input(oiiotool_path, filepath): - args = [ - oiiotool_path, - "--info", - "-v", - "-i:infoformat=xml", - filepath - ] - popen = subprocess.Popen(args, stdout=subprocess.PIPE) - _stdout, _stderr = popen.communicate() - output = "" - if _stdout: - output += _stdout.decode("utf-8", errors="backslashreplace") - - if _stderr: - output += _stderr.decode("utf-8", errors="backslashreplace") - - output = output.replace("\r\n", "\n") - xml_started = False - lines = [] - for line in output.split("\n"): - if not xml_started: - if not line.startswith("<"): - continue - xml_started = True - if xml_started: - lines.append(line) - - if not xml_started: - raise ValueError( - "Failed to read input file \"{}\".\nOutput:\n{}".format( - filepath, output - ) - ) - xml_text = "\n".join(lines) - return parse_oiio_xml_output(xml_text) - - -def GetDeadlinePlugin(): # noqa: N802 - """Helper.""" - return OpenPypeTileAssembler() - - -def CleanupDeadlinePlugin(deadlinePlugin): # noqa: N802, N803 - """Helper.""" - deadlinePlugin.cleanup() - - -class OpenPypeTileAssembler(DeadlinePlugin): - """Deadline plugin for assembling tiles using OIIO.""" - - def __init__(self): - """Init.""" - super().__init__() - self.InitializeProcessCallback += self.initialize_process - self.RenderExecutableCallback += self.render_executable - self.RenderArgumentCallback += self.render_argument - self.PreRenderTasksCallback += self.pre_render_tasks - self.PostRenderTasksCallback += self.post_render_tasks - - def cleanup(self): - """Cleanup function.""" - for stdoutHandler in self.StdoutHandlers: - del stdoutHandler.HandleCallback - - del self.InitializeProcessCallback - del self.RenderExecutableCallback - del self.RenderArgumentCallback - del self.PreRenderTasksCallback - del self.PostRenderTasksCallback - - def initialize_process(self): - """Initialization.""" - self.LogInfo("Plugin version: {}".format(version_string)) - self.SingleFramesOnly = True - self.StdoutHandling = True - self.renderer = self.GetPluginInfoEntryWithDefault( - "Renderer", "undefined") - self.AddStdoutHandlerCallback( - ".*Error.*").HandleCallback += self.handle_stdout_error - - def render_executable(self): - """Get render executable name. - - Get paths from plugin configuration, find executable and return it. - - Returns: - (str): Render executable. - - """ - oiiotool_exe_list = self.GetConfigEntry("OIIOTool_RenderExecutable") - oiiotool_exe = FileUtils.SearchFileList(oiiotool_exe_list) - - if oiiotool_exe == "": - self.FailRender(("No file found in the semicolon separated " - "list \"{}\". The path to the render executable " - "can be configured from the Plugin Configuration " - "in the Deadline Monitor.").format( - oiiotool_exe_list)) - - return oiiotool_exe - - def render_argument(self): - """Generate command line arguments for render executable. - - Returns: - (str): arguments to add to render executable. - - """ - # Read tile config file. This file is in compatible format with - # Draft Tile Assembler - data = {} - with open(self.config_file, "rU") as f: - for text in f: - # Parsing key-value pair and removing white-space - # around the entries - info = [x.strip() for x in text.split("=", 1)] - - if len(info) > 1: - try: - data[str(info[0])] = info[1] - except Exception as e: - # should never be called - self.FailRender( - "Cannot parse config file: {}".format(e)) - - # Get output file. We support only EXRs now. - output_file = data["ImageFileName"] - output_file = RepositoryUtils.CheckPathMapping(output_file) - output_file = self.process_path(output_file) - - tile_info = [] - for tile in range(int(data["TileCount"])): - tile_info.append({ - "filepath": data["Tile{}".format(tile)], - "pos_x": int(data["Tile{}X".format(tile)]), - "pos_y": int(data["Tile{}Y".format(tile)]), - "height": int(data["Tile{}Height".format(tile)]), - "width": int(data["Tile{}Width".format(tile)]) - }) - - arguments = self.tile_oiio_args( - int(data["ImageWidth"]), int(data["ImageHeight"]), - tile_info, output_file) - self.LogInfo( - "Using arguments: {}".format(" ".join(arguments))) - self.tiles = tile_info - return " ".join(arguments) - - def process_path(self, filepath): - """Handle slashes in file paths.""" - if SystemUtils.IsRunningOnWindows(): - filepath = filepath.replace("/", "\\") - if filepath.startswith("\\") and not filepath.startswith("\\\\"): - filepath = "\\" + filepath - else: - filepath = filepath.replace("\\", "/") - return filepath - - def pre_render_tasks(self): - """Load config file and do remapping.""" - self.LogInfo("OpenPype Tile Assembler starting...") - config_file = self.GetPluginInfoEntry("ConfigFile") - - temp_scene_directory = self.CreateTempDirectory( - "thread" + str(self.GetThreadNumber())) - temp_scene_filename = Path.GetFileName(config_file) - self.config_file = Path.Combine( - temp_scene_directory, temp_scene_filename) - - if SystemUtils.IsRunningOnWindows(): - RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator( - config_file, self.config_file, "/", "\\") - else: - RepositoryUtils.CheckPathMappingInFileAndReplaceSeparator( - config_file, self.config_file, "\\", "/") - os.chmod(self.config_file, os.stat(self.config_file).st_mode) - - def post_render_tasks(self): - """Cleanup tiles if required.""" - if self.GetBooleanPluginInfoEntryWithDefault("CleanupTiles", False): - self.LogInfo("Cleaning up Tiles...") - for tile in self.tiles: - try: - self.LogInfo("Deleting: {}".format(tile["filepath"])) - os.remove(tile["filepath"]) - # By this time we would have errored out - # if error on missing was enabled - except KeyError: - pass - except OSError: - self.LogInfo("Failed to delete: {}".format( - tile["filepath"])) - pass - - self.LogInfo("OpenPype Tile Assembler Job finished.") - - def handle_stdout_error(self): - """Handle errors in stdout.""" - self.FailRender(self.GetRegexMatch(0)) - - def tile_oiio_args( - self, output_width, output_height, tile_info, output_path): - """Generate oiio tool arguments for tile assembly. - - Args: - output_width (int): Width of output image. - output_height (int): Height of output image. - tile_info (list): List of tile items, each item must be - dictionary with `filepath`, `pos_x` and `pos_y` keys - representing path to file and x, y coordinates on output - image where top-left point of tile item should start. - output_path (str): Path to file where should be output stored. - - Returns: - (list): oiio tools arguments. - - """ - args = [] - - # Create new image with output resolution, and with same type and - # channels as input - oiiotool_path = self.render_executable() - first_tile_path = tile_info[0]["filepath"] - first_tile_info = info_about_input(oiiotool_path, first_tile_path) - create_arg_template = "--create{} {}x{} {}" - - image_type = "" - image_format = first_tile_info.get("format") - if image_format: - image_type = ":type={}".format(image_format) - - create_arg = create_arg_template.format( - image_type, output_width, - output_height, first_tile_info["nchannels"] - ) - args.append(create_arg) - - for tile in tile_info: - path = tile["filepath"] - pos_x = tile["pos_x"] - tile_height = info_about_input(oiiotool_path, path)["height"] - if self.renderer == "vray": - pos_y = tile["pos_y"] - else: - pos_y = output_height - tile["pos_y"] - tile_height - - # Add input path and make sure inputs origin is 0, 0 - args.append(path) - args.append("--origin +0+0") - # Swap to have input as foreground - args.append("--swap") - # Paste foreground to background - args.append("--paste {x:+d}{y:+d}".format(x=pos_x, y=pos_y)) - - args.append("-o") - args.append(output_path) - - return args diff --git a/server_addon/deadline/client/ayon_deadline/repository/readme.md b/server_addon/deadline/client/ayon_deadline/repository/readme.md deleted file mode 100644 index 31ffffd0b7..0000000000 --- a/server_addon/deadline/client/ayon_deadline/repository/readme.md +++ /dev/null @@ -1,29 +0,0 @@ -## OpenPype Deadline repository overlay - - This directory is an overlay for Deadline repository. - It means that you can copy the whole hierarchy to Deadline repository and it - should work. - - Logic: - ----- - GlobalJobPreLoad - ----- - -The `GlobalJobPreLoad` will retrieve the OpenPype executable path from the -`OpenPype` Deadline Plug-in's settings. Then it will call the executable to -retrieve the environment variables needed for the Deadline Job. -These environment variables are injected into rendering process. - -Deadline triggers the `GlobalJobPreLoad.py` for each Worker as it starts the -Job. - -*Note*: It also contains backward compatible logic to preserve functionality -for old Pype2 and non-OpenPype triggered jobs. - - Plugin - ------ - For each render and publishing job the `OpenPype` Deadline Plug-in is checked - for the configured location of the OpenPype executable (needs to be configured - in `Deadline's Configure Plugins > OpenPype`) through `GlobalJobPreLoad`. - - diff --git a/server_addon/deadline/client/ayon_deadline/version.py b/server_addon/deadline/client/ayon_deadline/version.py deleted file mode 100644 index 96262d7186..0000000000 --- a/server_addon/deadline/client/ayon_deadline/version.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- -"""Package declaring AYON addon 'deadline' version.""" -__version__ = "0.2.3" diff --git a/server_addon/deadline/package.py b/server_addon/deadline/package.py deleted file mode 100644 index 8fcc007850..0000000000 --- a/server_addon/deadline/package.py +++ /dev/null @@ -1,10 +0,0 @@ -name = "deadline" -title = "Deadline" -version = "0.2.3" - -client_dir = "ayon_deadline" - -ayon_required_addons = { - "core": ">0.3.2", -} -ayon_compatible_addons = {} diff --git a/server_addon/deadline/server/__init__.py b/server_addon/deadline/server/__init__.py deleted file mode 100644 index 8d2dc152cd..0000000000 --- a/server_addon/deadline/server/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Type - -from ayon_server.addons import BaseServerAddon - -from .settings import DeadlineSettings, DEFAULT_VALUES, DeadlineSiteSettings - - -class Deadline(BaseServerAddon): - settings_model: Type[DeadlineSettings] = DeadlineSettings - site_settings_model: Type[DeadlineSiteSettings] = DeadlineSiteSettings - - - async def get_default_settings(self): - settings_model_cls = self.get_settings_model() - return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/deadline/server/settings/__init__.py b/server_addon/deadline/server/settings/__init__.py deleted file mode 100644 index d25c0fb330..0000000000 --- a/server_addon/deadline/server/settings/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .main import ( - DeadlineSettings, - DEFAULT_VALUES, -) -from .site_settings import DeadlineSiteSettings - - -__all__ = ( - "DeadlineSettings", - "DeadlineSiteSettings", - "DEFAULT_VALUES", -) diff --git a/server_addon/deadline/server/settings/main.py b/server_addon/deadline/server/settings/main.py deleted file mode 100644 index edb8a16e35..0000000000 --- a/server_addon/deadline/server/settings/main.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import TYPE_CHECKING -from pydantic import validator - -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, - ensure_unique_names, -) -if TYPE_CHECKING: - from ayon_server.addons import BaseServerAddon - -from .publish_plugins import ( - PublishPluginsModel, - DEFAULT_DEADLINE_PLUGINS_SETTINGS -) - - -async def defined_deadline_ws_name_enum_resolver( - addon: "BaseServerAddon", - settings_variant: str = "production", - project_name: str | None = None, -) -> list[str]: - """Provides list of names of configured Deadline webservice urls.""" - if addon is None: - return [] - - settings = await addon.get_studio_settings(variant=settings_variant) - - ws_server_name = [] - for deadline_url_item in settings.deadline_urls: - ws_server_name.append(deadline_url_item.name) - - return ws_server_name - -class ServerItemSubmodel(BaseSettingsModel): - """Connection info about configured DL servers.""" - _layout = "expanded" - name: str = SettingsField(title="Name") - value: str = SettingsField(title="Url") - require_authentication: bool = SettingsField( - False, title="Require authentication") - not_verify_ssl: bool = SettingsField( - False, title="Don't verify SSL") - default_username: str = SettingsField( - "", - title="Default user name", - description="Webservice username, 'Require authentication' must be " - "enabled." - ) - default_password: str = SettingsField( - "", - title="Default password", - description="Webservice password, 'Require authentication' must be " - "enabled." - ) - - -class DeadlineSettings(BaseSettingsModel): - # configured DL servers - deadline_urls: list[ServerItemSubmodel] = SettingsField( - default_factory=list, - title="System Deadline Webservice Info", - scope=["studio"], - ) - - # name(key) of selected server for project - deadline_server: str = SettingsField( - title="Project Deadline server name", - section="---", - scope=["project"], - enum_resolver=defined_deadline_ws_name_enum_resolver - ) - - publish: PublishPluginsModel = SettingsField( - default_factory=PublishPluginsModel, - title="Publish Plugins", - ) - - @validator("deadline_urls") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - - -DEFAULT_VALUES = { - "deadline_urls": [ - { - "name": "default", - "value": "http://127.0.0.1:8082", - "require_authentication": False, - "not_verify_ssl": False, - "default_username": "", - "default_password": "" - - } - ], - "deadline_server": "default", - "publish": DEFAULT_DEADLINE_PLUGINS_SETTINGS -} diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py deleted file mode 100644 index 1cf699db23..0000000000 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ /dev/null @@ -1,578 +0,0 @@ -from pydantic import validator - -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, - ensure_unique_names, -) - - -class CollectDeadlinePoolsModel(BaseSettingsModel): - """Settings Deadline default pools.""" - - primary_pool: str = SettingsField(title="Primary Pool") - - secondary_pool: str = SettingsField(title="Secondary Pool") - - -class ValidateExpectedFilesModel(BaseSettingsModel): - enabled: bool = SettingsField(True, title="Enabled") - active: bool = SettingsField(True, title="Active") - allow_user_override: bool = SettingsField( - True, title="Allow user change frame range" - ) - families: list[str] = SettingsField( - default_factory=list, title="Trigger on families" - ) - targets: list[str] = SettingsField( - default_factory=list, title="Trigger for plugins" - ) - - -def tile_assembler_enum(): - """Return a list of value/label dicts for the enumerator. - - Returning a list of dicts is used to allow for a custom label to be - displayed in the UI. - """ - return [ - { - "value": "DraftTileAssembler", - "label": "Draft Tile Assembler" - }, - { - "value": "OpenPypeTileAssembler", - "label": "Open Image IO" - } - ] - - -class ScenePatchesSubmodel(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField(title="Patch name") - regex: str = SettingsField(title="Patch regex") - line: str = SettingsField(title="Patch line") - - -class MayaSubmitDeadlineModel(BaseSettingsModel): - """Maya deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - import_reference: bool = SettingsField( - title="Use Scene with Imported Reference" - ) - asset_dependencies: bool = SettingsField(title="Use Asset dependencies") - priority: int = SettingsField(title="Priority") - tile_priority: int = SettingsField(title="Tile Priority") - group: str = SettingsField(title="Group") - limit: list[str] = SettingsField( - default_factory=list, - title="Limit Groups" - ) - tile_assembler_plugin: str = SettingsField( - title="Tile Assembler Plugin", - enum_resolver=tile_assembler_enum, - ) - jobInfo: str = SettingsField( - title="Additional JobInfo data", - widget="textarea", - ) - pluginInfo: str = SettingsField( - title="Additional PluginInfo data", - widget="textarea", - ) - - scene_patches: list[ScenePatchesSubmodel] = SettingsField( - default_factory=list, - title="Scene patches", - ) - strict_error_checking: bool = SettingsField( - title="Disable Strict Error Check profiles" - ) - - @validator("scene_patches") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - -class MaxSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True) - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Frame per Task") - group: str = SettingsField("", title="Group Name") - - -class EnvSearchReplaceSubmodel(BaseSettingsModel): - _layout = "compact" - name: str = SettingsField(title="Name") - value: str = SettingsField(title="Value") - - -class LimitGroupsSubmodel(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField(title="Name") - value: list[str] = SettingsField( - default_factory=list, - title="Limit Groups" - ) - - -def fusion_deadline_plugin_enum(): - """Return a list of value/label dicts for the enumerator. - - Returning a list of dicts is used to allow for a custom label to be - displayed in the UI. - """ - return [ - { - "value": "Fusion", - "label": "Fusion" - }, - { - "value": "FusionCmd", - "label": "FusionCmd" - } - ] - - -class FusionSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True, title="Enabled") - optional: bool = SettingsField(False, title="Optional") - active: bool = SettingsField(True, title="Active") - priority: int = SettingsField(50, title="Priority") - chunk_size: int = SettingsField(10, title="Frame per Task") - concurrent_tasks: int = SettingsField( - 1, title="Number of concurrent tasks" - ) - group: str = SettingsField("", title="Group Name") - plugin: str = SettingsField("Fusion", - enum_resolver=fusion_deadline_plugin_enum, - title="Deadline Plugin") - - -class NukeSubmitDeadlineModel(BaseSettingsModel): - """Nuke deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - concurrent_tasks: int = SettingsField(title="Number of concurrent tasks") - group: str = SettingsField(title="Group") - department: str = SettingsField(title="Department") - use_gpu: bool = SettingsField(title="Use GPU") - workfile_dependency: bool = SettingsField(title="Workfile Dependency") - use_published_workfile: bool = SettingsField( - title="Use Published Workfile" - ) - - env_allowed_keys: list[str] = SettingsField( - default_factory=list, - title="Allowed environment keys" - ) - - env_search_replace_values: list[EnvSearchReplaceSubmodel] = SettingsField( - default_factory=list, - title="Search & replace in environment values", - ) - - limit_groups: list[LimitGroupsSubmodel] = SettingsField( - default_factory=list, - title="Limit Groups", - ) - - @validator( - "limit_groups", - "env_search_replace_values") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - -class HarmonySubmitDeadlineModel(BaseSettingsModel): - """Harmony deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - department: str = SettingsField(title="Department") - - -class HoudiniSubmitDeadlineModel(BaseSettingsModel): - """Houdini deadline render submitter settings.""" - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - - export_priority: int = SettingsField(title="Export Priority") - export_chunk_size: int = SettingsField(title="Export Chunk Size") - export_group: str = SettingsField(title="Export Group") - - -class HoudiniCacheSubmitDeadlineModel(BaseSettingsModel): - """Houdini deadline cache submitter settings.""" - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - - -class AfterEffectsSubmitDeadlineModel(BaseSettingsModel): - """After Effects deadline submitter settings.""" - - enabled: bool = SettingsField(title="Enabled") - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Chunk Size") - group: str = SettingsField(title="Group") - department: str = SettingsField(title="Department") - multiprocess: bool = SettingsField(title="Optional") - - -class CelactionSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True, title="Enabled") - deadline_department: str = SettingsField("", title="Deadline apartment") - deadline_priority: int = SettingsField(50, title="Deadline priority") - deadline_pool: str = SettingsField("", title="Deadline pool") - deadline_pool_secondary: str = SettingsField( - "", title="Deadline pool (secondary)" - ) - deadline_group: str = SettingsField("", title="Deadline Group") - deadline_chunk_size: int = SettingsField(10, title="Deadline Chunk size") - deadline_job_delay: str = SettingsField( - "", title="Delay job (timecode dd:hh:mm:ss)" - ) - - -class BlenderSubmitDeadlineModel(BaseSettingsModel): - enabled: bool = SettingsField(True) - optional: bool = SettingsField(title="Optional") - active: bool = SettingsField(title="Active") - use_published: bool = SettingsField(title="Use Published scene") - asset_dependencies: bool = SettingsField(title="Use Asset dependencies") - priority: int = SettingsField(title="Priority") - chunk_size: int = SettingsField(title="Frame per Task") - group: str = SettingsField("", title="Group Name") - job_delay: str = SettingsField( - "", title="Delay job (timecode dd:hh:mm:ss)" - ) - - -class AOVFilterSubmodel(BaseSettingsModel): - _layout = "expanded" - name: str = SettingsField(title="Host") - value: list[str] = SettingsField( - default_factory=list, - title="AOV regex" - ) - - -class ProcessCacheJobFarmModel(BaseSettingsModel): - """Process submitted job on farm.""" - - enabled: bool = SettingsField(title="Enabled") - deadline_department: str = SettingsField(title="Department") - deadline_pool: str = SettingsField(title="Pool") - deadline_group: str = SettingsField(title="Group") - deadline_chunk_size: int = SettingsField(title="Chunk Size") - deadline_priority: int = SettingsField(title="Priority") - - -class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): - """Process submitted job on farm.""" - - enabled: bool = SettingsField(title="Enabled") - deadline_department: str = SettingsField(title="Department") - deadline_pool: str = SettingsField(title="Pool") - deadline_group: str = SettingsField(title="Group") - deadline_chunk_size: int = SettingsField(title="Chunk Size") - deadline_priority: int = SettingsField(title="Priority") - publishing_script: str = SettingsField(title="Publishing script path") - skip_integration_repre_list: list[str] = SettingsField( - default_factory=list, - title="Skip integration of representation with ext" - ) - families_transfer: list[str] = SettingsField( - default_factory=list, - title=( - "List of family names to transfer\n" - "to generated instances (AOVs for example)." - ) - ) - aov_filter: list[AOVFilterSubmodel] = SettingsField( - default_factory=list, - title="Reviewable products filter", - ) - - @validator("aov_filter") - def validate_unique_names(cls, value): - ensure_unique_names(value) - return value - - -class PublishPluginsModel(BaseSettingsModel): - CollectDeadlinePools: CollectDeadlinePoolsModel = SettingsField( - default_factory=CollectDeadlinePoolsModel, - title="Default Pools") - ValidateExpectedFiles: ValidateExpectedFilesModel = SettingsField( - default_factory=ValidateExpectedFilesModel, - title="Validate Expected Files" - ) - AfterEffectsSubmitDeadline: AfterEffectsSubmitDeadlineModel = ( - SettingsField( - default_factory=AfterEffectsSubmitDeadlineModel, - title="After Effects to deadline", - section="Hosts" - ) - ) - BlenderSubmitDeadline: BlenderSubmitDeadlineModel = SettingsField( - default_factory=BlenderSubmitDeadlineModel, - title="Blender Submit Deadline") - CelactionSubmitDeadline: CelactionSubmitDeadlineModel = SettingsField( - default_factory=CelactionSubmitDeadlineModel, - title="Celaction Submit Deadline") - FusionSubmitDeadline: FusionSubmitDeadlineModel = SettingsField( - default_factory=FusionSubmitDeadlineModel, - title="Fusion submit to Deadline") - HarmonySubmitDeadline: HarmonySubmitDeadlineModel = SettingsField( - default_factory=HarmonySubmitDeadlineModel, - title="Harmony Submit to deadline") - HoudiniCacheSubmitDeadline: HoudiniCacheSubmitDeadlineModel = SettingsField( - default_factory=HoudiniCacheSubmitDeadlineModel, - title="Houdini Submit cache to deadline") - HoudiniSubmitDeadline: HoudiniSubmitDeadlineModel = SettingsField( - default_factory=HoudiniSubmitDeadlineModel, - title="Houdini Submit render to deadline") - MaxSubmitDeadline: MaxSubmitDeadlineModel = SettingsField( - default_factory=MaxSubmitDeadlineModel, - title="Max Submit to deadline") - MayaSubmitDeadline: MayaSubmitDeadlineModel = SettingsField( - default_factory=MayaSubmitDeadlineModel, - title="Maya Submit to deadline") - NukeSubmitDeadline: NukeSubmitDeadlineModel = SettingsField( - default_factory=NukeSubmitDeadlineModel, - title="Nuke Submit to deadline") - ProcessSubmittedCacheJobOnFarm: ProcessCacheJobFarmModel = SettingsField( - default_factory=ProcessCacheJobFarmModel, - title="Process submitted cache Job on farm", - section="Publish Jobs") - ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = SettingsField( - default_factory=ProcessSubmittedJobOnFarmModel, - title="Process submitted job on farm") - - -DEFAULT_DEADLINE_PLUGINS_SETTINGS = { - "CollectDeadlinePools": { - "primary_pool": "", - "secondary_pool": "" - }, - "ValidateExpectedFiles": { - "enabled": True, - "active": True, - "allow_user_override": True, - "families": [ - "render" - ], - "targets": [ - "deadline" - ] - }, - "AfterEffectsSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "priority": 50, - "chunk_size": 10000, - "group": "", - "department": "", - "multiprocess": True - }, - "BlenderSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "asset_dependencies": True, - "priority": 50, - "chunk_size": 10, - "group": "none", - "job_delay": "00:00:00:00" - }, - "CelactionSubmitDeadline": { - "enabled": True, - "deadline_department": "", - "deadline_priority": 50, - "deadline_pool": "", - "deadline_pool_secondary": "", - "deadline_group": "", - "deadline_chunk_size": 10, - "deadline_job_delay": "00:00:00:00" - }, - "FusionSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 10, - "concurrent_tasks": 1, - "group": "" - }, - "HarmonySubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "priority": 50, - "chunk_size": 10000, - "group": "", - "department": "" - }, - "HoudiniCacheSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 999999, - "group": "" - }, - "HoudiniSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 1, - "group": "", - "export_priority": 50, - "export_chunk_size": 10, - "export_group": "" - }, - "MaxSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "use_published": True, - "priority": 50, - "chunk_size": 10, - "group": "none" - }, - "MayaSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "tile_assembler_plugin": "DraftTileAssembler", - "use_published": True, - "import_reference": False, - "asset_dependencies": True, - "strict_error_checking": True, - "priority": 50, - "tile_priority": 50, - "group": "none", - "limit": [], - # this used to be empty dict - "jobInfo": "", - # this used to be empty dict - "pluginInfo": "", - "scene_patches": [] - }, - "NukeSubmitDeadline": { - "enabled": True, - "optional": False, - "active": True, - "priority": 50, - "chunk_size": 10, - "concurrent_tasks": 1, - "group": "", - "department": "", - "use_gpu": True, - "workfile_dependency": True, - "use_published_workfile": True, - "env_allowed_keys": [], - "env_search_replace_values": [], - "limit_groups": [] - }, - "ProcessSubmittedCacheJobOnFarm": { - "enabled": True, - "deadline_department": "", - "deadline_pool": "", - "deadline_group": "", - "deadline_chunk_size": 1, - "deadline_priority": 50 - }, - "ProcessSubmittedJobOnFarm": { - "enabled": True, - "deadline_department": "", - "deadline_pool": "", - "deadline_group": "", - "deadline_chunk_size": 1, - "deadline_priority": 50, - "publishing_script": "", - "skip_integration_repre_list": [], - "families_transfer": ["render3d", "render2d", "ftrack", "slate"], - "aov_filter": [ - { - "name": "maya", - "value": [ - ".*([Bb]eauty).*" - ] - }, - { - "name": "blender", - "value": [ - ".*([Bb]eauty).*" - ] - }, - { - "name": "aftereffects", - "value": [ - ".*" - ] - }, - { - "name": "celaction", - "value": [ - ".*" - ] - }, - { - "name": "harmony", - "value": [ - ".*" - ] - }, - { - "name": "max", - "value": [ - ".*" - ] - }, - { - "name": "fusion", - "value": [ - ".*" - ] - } - ] - } -} diff --git a/server_addon/deadline/server/settings/site_settings.py b/server_addon/deadline/server/settings/site_settings.py deleted file mode 100644 index 92c092324e..0000000000 --- a/server_addon/deadline/server/settings/site_settings.py +++ /dev/null @@ -1,28 +0,0 @@ -from ayon_server.settings import ( - BaseSettingsModel, - SettingsField, -) - -from .main import defined_deadline_ws_name_enum_resolver - - -class CredentialPerServerModel(BaseSettingsModel): - """Provide credentials for configured DL servers""" - _layout = "expanded" - server_name: str = SettingsField( - "", - title="DL server name", - enum_resolver=defined_deadline_ws_name_enum_resolver - ) - username: str = SettingsField("", title="Username") - password: str = SettingsField("", title="Password") - - -class DeadlineSiteSettings(BaseSettingsModel): - local_settings: list[CredentialPerServerModel] = SettingsField( - default_factory=list, - title="Local setting", - description=( - "Please provide credentials for configured Deadline servers" - ), - )