mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/use-settings-field-in-ayon-settings
# Conflicts: # server_addon/muster/server/settings.py
This commit is contained in:
commit
7de54c6fe5
35 changed files with 100 additions and 1397 deletions
|
|
@ -8,55 +8,6 @@ from openpype.hosts.fusion.api.action import SelectInvalidAction
|
|||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
|
||||
def get_tool_resolution(tool, frame):
|
||||
"""Return the 2D input resolution to a Fusion tool
|
||||
|
||||
If the current tool hasn't been rendered its input resolution
|
||||
hasn't been saved. To combat this, add an expression in
|
||||
the comments field to read the resolution
|
||||
|
||||
Args
|
||||
tool (Fusion Tool): The tool to query input resolution
|
||||
frame (int): The frame to query the resolution on.
|
||||
|
||||
Returns:
|
||||
tuple: width, height as 2-tuple of integers
|
||||
|
||||
"""
|
||||
comp = tool.Composition
|
||||
|
||||
# False undo removes the undo-stack from the undo list
|
||||
with comp_lock_and_undo_chunk(comp, "Read resolution", False):
|
||||
# Save old comment
|
||||
old_comment = ""
|
||||
has_expression = False
|
||||
if tool["Comments"][frame] != "":
|
||||
if tool["Comments"].GetExpression() is not None:
|
||||
has_expression = True
|
||||
old_comment = tool["Comments"].GetExpression()
|
||||
tool["Comments"].SetExpression(None)
|
||||
else:
|
||||
old_comment = tool["Comments"][frame]
|
||||
tool["Comments"][frame] = ""
|
||||
|
||||
# Get input width
|
||||
tool["Comments"].SetExpression("self.Input.OriginalWidth")
|
||||
width = int(tool["Comments"][frame])
|
||||
|
||||
# Get input height
|
||||
tool["Comments"].SetExpression("self.Input.OriginalHeight")
|
||||
height = int(tool["Comments"][frame])
|
||||
|
||||
# Reset old comment
|
||||
tool["Comments"].SetExpression(None)
|
||||
if has_expression:
|
||||
tool["Comments"].SetExpression(old_comment)
|
||||
else:
|
||||
tool["Comments"][frame] = old_comment
|
||||
|
||||
return width, height
|
||||
|
||||
|
||||
class ValidateSaverResolution(
|
||||
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
|
||||
):
|
||||
|
|
@ -87,19 +38,79 @@ class ValidateSaverResolution(
|
|||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
resolution = cls.get_resolution(instance)
|
||||
saver = instance.data["tool"]
|
||||
try:
|
||||
resolution = cls.get_resolution(instance)
|
||||
except PublishValidationError:
|
||||
resolution = None
|
||||
expected_resolution = cls.get_expected_resolution(instance)
|
||||
if resolution != expected_resolution:
|
||||
saver = instance.data["tool"]
|
||||
return [saver]
|
||||
|
||||
@classmethod
|
||||
def get_resolution(cls, instance):
|
||||
saver = instance.data["tool"]
|
||||
first_frame = instance.data["frameStartHandle"]
|
||||
return get_tool_resolution(saver, frame=first_frame)
|
||||
return cls.get_tool_resolution(saver, frame=first_frame)
|
||||
|
||||
@classmethod
|
||||
def get_expected_resolution(cls, instance):
|
||||
data = instance.data["assetEntity"]["data"]
|
||||
return data["resolutionWidth"], data["resolutionHeight"]
|
||||
|
||||
@classmethod
|
||||
def get_tool_resolution(cls, tool, frame):
|
||||
"""Return the 2D input resolution to a Fusion tool
|
||||
|
||||
If the current tool hasn't been rendered its input resolution
|
||||
hasn't been saved. To combat this, add an expression in
|
||||
the comments field to read the resolution
|
||||
|
||||
Args
|
||||
tool (Fusion Tool): The tool to query input resolution
|
||||
frame (int): The frame to query the resolution on.
|
||||
|
||||
Returns:
|
||||
tuple: width, height as 2-tuple of integers
|
||||
|
||||
"""
|
||||
comp = tool.Composition
|
||||
|
||||
# False undo removes the undo-stack from the undo list
|
||||
with comp_lock_and_undo_chunk(comp, "Read resolution", False):
|
||||
# Save old comment
|
||||
old_comment = ""
|
||||
has_expression = False
|
||||
|
||||
if tool["Comments"][frame] not in ["", None]:
|
||||
if tool["Comments"].GetExpression() is not None:
|
||||
has_expression = True
|
||||
old_comment = tool["Comments"].GetExpression()
|
||||
tool["Comments"].SetExpression(None)
|
||||
else:
|
||||
old_comment = tool["Comments"][frame]
|
||||
tool["Comments"][frame] = ""
|
||||
# Get input width
|
||||
tool["Comments"].SetExpression("self.Input.OriginalWidth")
|
||||
if tool["Comments"][frame] is None:
|
||||
raise PublishValidationError(
|
||||
"Cannot get resolution info for frame '{}'.\n\n "
|
||||
"Please check that saver has connected input.".format(
|
||||
frame
|
||||
)
|
||||
)
|
||||
|
||||
width = int(tool["Comments"][frame])
|
||||
|
||||
# Get input height
|
||||
tool["Comments"].SetExpression("self.Input.OriginalHeight")
|
||||
height = int(tool["Comments"][frame])
|
||||
|
||||
# Reset old comment
|
||||
tool["Comments"].SetExpression(None)
|
||||
if has_expression:
|
||||
tool["Comments"].SetExpression(old_comment)
|
||||
else:
|
||||
tool["Comments"][frame] = old_comment
|
||||
|
||||
return width, height
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ def requests_post(*args, **kwargs):
|
|||
"""Wrap request post method.
|
||||
|
||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
||||
variable is found. This is useful when Deadline or Muster server are
|
||||
running with self-signed certificates and their certificate is not
|
||||
variable is found. 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.
|
||||
of defense SSL is providing, and it is not recommended.
|
||||
|
||||
"""
|
||||
if "verify" not in kwargs:
|
||||
|
|
@ -24,13 +24,13 @@ def requests_get(*args, **kwargs):
|
|||
"""Wrap request get method.
|
||||
|
||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
||||
variable is found. This is useful when Deadline or Muster server are
|
||||
running with self-signed certificates and their certificate is not
|
||||
variable is found. 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.
|
||||
of defense SSL is providing, and it is not recommended.
|
||||
|
||||
"""
|
||||
if "verify" not in kwargs:
|
||||
|
|
|
|||
|
|
@ -1333,7 +1333,6 @@ class TrayModulesManager(ModulesManager):
|
|||
"user",
|
||||
"ftrack",
|
||||
"kitsu",
|
||||
"muster",
|
||||
"launcher_tool",
|
||||
"avalon",
|
||||
"clockify",
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ def requests_post(*args, **kwargs):
|
|||
"""Wrap request post method.
|
||||
|
||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
||||
variable is found. This is useful when Deadline or Muster server are
|
||||
running with self-signed certificates and their certificate is not
|
||||
variable is found. 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:
|
||||
|
|
@ -55,8 +55,8 @@ def requests_get(*args, **kwargs):
|
|||
"""Wrap request get method.
|
||||
|
||||
Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
|
||||
variable is found. This is useful when Deadline or Muster server are
|
||||
running with self-signed certificates and their certificate is not
|
||||
variable is found. 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:
|
||||
|
|
|
|||
|
|
@ -99,10 +99,6 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin,
|
|||
def _submit_deadline_post_job(self, instance, job):
|
||||
"""Submit publish job to Deadline.
|
||||
|
||||
Deadline specific code separated from :meth:`process` for sake of
|
||||
more universal code. Muster post job is sent directly by Muster
|
||||
submitter, so this type of code isn't necessary for it.
|
||||
|
||||
Returns:
|
||||
(str): deadline_publish_job_id
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -59,21 +59,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
publish.ColormanagedPyblishPluginMixin):
|
||||
"""Process Job submitted on farm.
|
||||
|
||||
These jobs are dependent on a deadline or muster job
|
||||
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.
|
||||
|
||||
- In case of Muster, there is no need for such thing as dependent job,
|
||||
post action will be executed and rendered sequence will be published.
|
||||
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.
|
||||
|
||||
- musterSubmissionJob (dict, Required): same as 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.
|
||||
|
|
@ -161,10 +155,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
def _submit_deadline_post_job(self, instance, job, instances):
|
||||
"""Submit publish job to Deadline.
|
||||
|
||||
Deadline specific code separated from :meth:`process` for sake of
|
||||
more universal code. Muster post job is sent directly by Muster
|
||||
submitter, so this type of code isn't necessary for it.
|
||||
|
||||
Returns:
|
||||
(str): deadline_publish_job_id
|
||||
"""
|
||||
|
|
@ -586,9 +576,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
|
||||
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 "
|
||||
"or Muster submission."))
|
||||
|
||||
raise AssertionError(("Cannot continue without valid "
|
||||
"Deadline submission."))
|
||||
if not render_job:
|
||||
import getpass
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
from .muster import MusterModule
|
||||
|
||||
|
||||
__all__ = (
|
||||
"MusterModule",
|
||||
)
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import appdirs
|
||||
import requests
|
||||
|
||||
from openpype.modules import OpenPypeModule, ITrayModule
|
||||
|
||||
|
||||
class MusterModule(OpenPypeModule, ITrayModule):
|
||||
"""
|
||||
Module handling Muster Render credentials. This will display dialog
|
||||
asking for user credentials for Muster if not already specified.
|
||||
"""
|
||||
cred_folder_path = os.path.normpath(
|
||||
appdirs.user_data_dir('pype-app', 'pype')
|
||||
)
|
||||
cred_filename = 'muster_cred.json'
|
||||
|
||||
name = "muster"
|
||||
|
||||
def initialize(self, modules_settings):
|
||||
muster_settings = modules_settings[self.name]
|
||||
self.enabled = muster_settings["enabled"]
|
||||
self.muster_url = muster_settings["MUSTER_REST_URL"]
|
||||
|
||||
self.cred_path = os.path.join(
|
||||
self.cred_folder_path, self.cred_filename
|
||||
)
|
||||
# Tray attributes
|
||||
self.widget_login = None
|
||||
self.action_show_login = None
|
||||
self.rest_api_obj = None
|
||||
|
||||
def get_global_environments(self):
|
||||
return {
|
||||
"MUSTER_REST_URL": self.muster_url
|
||||
}
|
||||
|
||||
def tray_init(self):
|
||||
from .widget_login import MusterLogin
|
||||
self.widget_login = MusterLogin(self)
|
||||
|
||||
def tray_start(self):
|
||||
"""Show login dialog if credentials not found."""
|
||||
# This should be start of module in tray
|
||||
cred = self.load_credentials()
|
||||
if not cred:
|
||||
self.show_login()
|
||||
|
||||
def tray_exit(self):
|
||||
"""Nothing special for Muster."""
|
||||
return
|
||||
|
||||
# Definition of Tray menu
|
||||
def tray_menu(self, parent):
|
||||
"""Add **change credentials** option to tray menu."""
|
||||
from qtpy import QtWidgets
|
||||
|
||||
# Menu for Tray App
|
||||
menu = QtWidgets.QMenu('Muster', parent)
|
||||
menu.setProperty('submenu', 'on')
|
||||
|
||||
# Actions
|
||||
self.action_show_login = QtWidgets.QAction(
|
||||
"Change login", menu
|
||||
)
|
||||
|
||||
menu.addAction(self.action_show_login)
|
||||
self.action_show_login.triggered.connect(self.show_login)
|
||||
|
||||
parent.addMenu(menu)
|
||||
|
||||
def load_credentials(self):
|
||||
"""
|
||||
Get credentials from JSON file
|
||||
"""
|
||||
credentials = {}
|
||||
try:
|
||||
file = open(self.cred_path, 'r')
|
||||
credentials = json.load(file)
|
||||
except Exception:
|
||||
file = open(self.cred_path, 'w+')
|
||||
file.close()
|
||||
|
||||
return credentials
|
||||
|
||||
def get_auth_token(self, username, password):
|
||||
"""
|
||||
Authenticate user with Muster and get authToken from server.
|
||||
"""
|
||||
if not self.muster_url:
|
||||
raise AttributeError("Muster REST API url not set")
|
||||
params = {
|
||||
'username': username,
|
||||
'password': password
|
||||
}
|
||||
api_entry = '/api/login'
|
||||
response = self._requests_post(
|
||||
self.muster_url + api_entry, params=params)
|
||||
if response.status_code != 200:
|
||||
self.log.error(
|
||||
'Cannot log into Muster: {}'.format(response.status_code))
|
||||
raise Exception('Cannot login into Muster.')
|
||||
|
||||
try:
|
||||
token = response.json()['ResponseData']['authToken']
|
||||
except ValueError as e:
|
||||
self.log.error('Invalid response from Muster server {}'.format(e))
|
||||
raise Exception('Invalid response from Muster while logging in.')
|
||||
|
||||
self.save_credentials(token)
|
||||
|
||||
def save_credentials(self, token):
|
||||
"""Save credentials to JSON file."""
|
||||
|
||||
with open(self.cred_path, "w") as f:
|
||||
json.dump({'token': token}, f)
|
||||
|
||||
def show_login(self):
|
||||
"""
|
||||
Show dialog to enter credentials
|
||||
"""
|
||||
if self.widget_login:
|
||||
self.widget_login.show()
|
||||
|
||||
# Webserver module implementation
|
||||
def webserver_initialization(self, server_manager):
|
||||
"""Add routes for Muster login."""
|
||||
if self.tray_initialized:
|
||||
from .rest_api import MusterModuleRestApi
|
||||
|
||||
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
|
||||
|
||||
def _requests_post(self, *args, **kwargs):
|
||||
""" Wrapper for requests, disabling SSL certificate validation if
|
||||
DONT_VERIFY_SSL environment variable is found. This is useful when
|
||||
Deadline or Muster server are running with self-signed certificates
|
||||
and their 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.
|
||||
"""
|
||||
if 'verify' not in kwargs:
|
||||
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa
|
||||
return requests.post(*args, **kwargs)
|
||||
|
|
@ -1,555 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
import getpass
|
||||
import platform
|
||||
|
||||
import appdirs
|
||||
|
||||
from maya import cmds
|
||||
|
||||
import pyblish.api
|
||||
from openpype.lib import requests_post
|
||||
from openpype.hosts.maya.api import lib
|
||||
from openpype.hosts.maya.api.lib_rendersettings import RenderSettings
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.settings import get_system_settings
|
||||
|
||||
|
||||
# mapping between Maya renderer names and Muster template ids
|
||||
def _get_template_id(renderer):
|
||||
"""
|
||||
Return muster template ID based on renderer name.
|
||||
|
||||
:param renderer: renderer name
|
||||
:type renderer: str
|
||||
:returns: muster template id
|
||||
:rtype: int
|
||||
"""
|
||||
|
||||
# TODO: Use settings from context?
|
||||
templates = get_system_settings()["modules"]["muster"]["templates_mapping"]
|
||||
if not templates:
|
||||
raise RuntimeError(("Muster template mapping missing in "
|
||||
"pype-settings"))
|
||||
try:
|
||||
template_id = templates[renderer]
|
||||
except KeyError:
|
||||
raise RuntimeError("Unmapped renderer - missing template id")
|
||||
|
||||
return template_id
|
||||
|
||||
|
||||
def _get_script():
|
||||
"""Get path to the image sequence script"""
|
||||
try:
|
||||
from openpype.scripts import publish_filesequence
|
||||
except Exception:
|
||||
raise RuntimeError("Expected module 'publish_deadline'"
|
||||
"to be available")
|
||||
|
||||
module_path = publish_filesequence.__file__
|
||||
if module_path.endswith(".pyc"):
|
||||
module_path = module_path[:-len(".pyc")] + ".py"
|
||||
|
||||
return module_path
|
||||
|
||||
|
||||
def get_renderer_variables(renderlayer=None):
|
||||
"""Retrieve the extension which has been set in the VRay settings
|
||||
|
||||
Will return None if the current renderer is not VRay
|
||||
For Maya 2016.5 and up the renderSetup creates renderSetupLayer node which
|
||||
start with `rs`. Use the actual node name, do NOT use the `nice name`
|
||||
|
||||
Args:
|
||||
renderlayer (str): the node name of the renderlayer.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
|
||||
renderer = lib.get_renderer(renderlayer or lib.get_current_renderlayer())
|
||||
|
||||
padding = cmds.getAttr(RenderSettings.get_padding_attr(renderer))
|
||||
|
||||
filename_0 = cmds.renderSettings(fullPath=True, firstImageName=True)[0]
|
||||
|
||||
if renderer == "vray":
|
||||
# Maya's renderSettings function does not return V-Ray file extension
|
||||
# so we get the extension from vraySettings
|
||||
extension = cmds.getAttr("vraySettings.imageFormatStr")
|
||||
|
||||
# When V-Ray image format has not been switched once from default .png
|
||||
# the getAttr command above returns None. As such we explicitly set
|
||||
# it to `.png`
|
||||
if extension is None:
|
||||
extension = "png"
|
||||
|
||||
filename_prefix = "<Scene>/<Scene>_<Layer>/<Layer>"
|
||||
else:
|
||||
# Get the extension, getAttr defaultRenderGlobals.imageFormat
|
||||
# returns an index number.
|
||||
filename_base = os.path.basename(filename_0)
|
||||
extension = os.path.splitext(filename_base)[-1].strip(".")
|
||||
filename_prefix = "<Scene>/<RenderLayer>/<RenderLayer>"
|
||||
|
||||
return {"ext": extension,
|
||||
"filename_prefix": filename_prefix,
|
||||
"padding": padding,
|
||||
"filename_0": filename_0}
|
||||
|
||||
|
||||
def preview_fname(folder, scene, layer, padding, ext):
|
||||
"""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:
|
||||
folder (str): The root output folder (image path)
|
||||
scene (str): The scene name
|
||||
layer (str): The layer name to be rendered
|
||||
padding (int): The padding length
|
||||
ext(str): The output file extension
|
||||
|
||||
Returns:
|
||||
str
|
||||
|
||||
"""
|
||||
|
||||
# Following hardcoded "<Scene>/<Scene>_<Layer>/<Layer>"
|
||||
output = "{scene}/{layer}/{layer}.{number}.{ext}".format(
|
||||
scene=scene,
|
||||
layer=layer,
|
||||
number="#" * padding,
|
||||
ext=ext
|
||||
)
|
||||
|
||||
return os.path.join(folder, output)
|
||||
|
||||
|
||||
class MayaSubmitMuster(pyblish.api.InstancePlugin):
|
||||
"""Submit available render layers to Muster
|
||||
|
||||
Renders are submitted to a Muster via HTTP API as
|
||||
supplied via the environment variable ``MUSTER_REST_URL``.
|
||||
|
||||
Also needed is ``MUSTER_USER`` and ``MUSTER_PASSWORD``.
|
||||
"""
|
||||
|
||||
label = "Submit to Muster"
|
||||
order = pyblish.api.IntegratorOrder + 0.1
|
||||
hosts = ["maya"]
|
||||
families = ["renderlayer"]
|
||||
icon = "satellite-dish"
|
||||
if not os.environ.get("MUSTER_REST_URL"):
|
||||
optional = False
|
||||
active = False
|
||||
else:
|
||||
optional = True
|
||||
|
||||
_token = None
|
||||
|
||||
def _load_credentials(self):
|
||||
"""
|
||||
Load Muster credentials from file and set `MUSTER_USER`,
|
||||
`MUSTER_PASSWORD`, `MUSTER_REST_URL` is loaded from settings.
|
||||
|
||||
.. todo::
|
||||
|
||||
Show login dialog if access token is invalid or missing.
|
||||
"""
|
||||
app_dir = os.path.normpath(
|
||||
appdirs.user_data_dir('pype-app', 'pype')
|
||||
)
|
||||
file_name = 'muster_cred.json'
|
||||
fpath = os.path.join(app_dir, file_name)
|
||||
file = open(fpath, 'r')
|
||||
muster_json = json.load(file)
|
||||
self._token = muster_json.get('token', None)
|
||||
if not self._token:
|
||||
raise RuntimeError("Invalid access token for Muster")
|
||||
file.close()
|
||||
self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL")
|
||||
if not self.MUSTER_REST_URL:
|
||||
raise AttributeError("Muster REST API url not set")
|
||||
|
||||
def _get_templates(self):
|
||||
"""
|
||||
Get Muster templates from server.
|
||||
"""
|
||||
params = {
|
||||
"authToken": self._token,
|
||||
"select": "name"
|
||||
}
|
||||
api_entry = '/api/templates/list'
|
||||
response = requests_post(
|
||||
self.MUSTER_REST_URL + api_entry, params=params)
|
||||
if response.status_code != 200:
|
||||
self.log.error(
|
||||
'Cannot get templates from Muster: {}'.format(
|
||||
response.status_code))
|
||||
raise Exception('Cannot get templates from Muster.')
|
||||
|
||||
try:
|
||||
response_templates = response.json()["ResponseData"]["templates"]
|
||||
except ValueError as e:
|
||||
self.log.error(
|
||||
'Muster server returned unexpected data {}'.format(e)
|
||||
)
|
||||
raise Exception('Muster server returned unexpected data')
|
||||
|
||||
templates = {}
|
||||
for t in response_templates:
|
||||
templates[t.get("name")] = t.get("id")
|
||||
|
||||
self._templates = templates
|
||||
|
||||
def _resolve_template(self, renderer):
|
||||
"""
|
||||
Returns template ID based on renderer string.
|
||||
|
||||
:param renderer: Name of renderer to match against template names
|
||||
:type renderer: str
|
||||
:returns: ID of template
|
||||
:rtype: int
|
||||
:raises: Exception if template ID isn't found
|
||||
"""
|
||||
self.log.debug("Trying to find template for [{}]".format(renderer))
|
||||
mapped = _get_template_id(renderer)
|
||||
self.log.debug("got id [{}]".format(mapped))
|
||||
return self._templates.get(mapped)
|
||||
|
||||
def _submit(self, payload):
|
||||
"""
|
||||
Submit job to Muster
|
||||
|
||||
:param payload: json with job to submit
|
||||
:type payload: str
|
||||
:returns: response
|
||||
:raises: Exception status is wrong
|
||||
"""
|
||||
params = {
|
||||
"authToken": self._token,
|
||||
"name": "submit"
|
||||
}
|
||||
api_entry = '/api/queue/actions'
|
||||
response = requests_post(
|
||||
self.MUSTER_REST_URL + api_entry, params=params, json=payload)
|
||||
|
||||
if response.status_code != 200:
|
||||
self.log.error(
|
||||
'Cannot submit job to Muster: {}'.format(response.text))
|
||||
raise Exception('Cannot submit job to Muster.')
|
||||
|
||||
return response
|
||||
|
||||
def process(self, instance):
|
||||
"""
|
||||
Authenticate with Muster, collect all data, prepare path for post
|
||||
render publish job and submit job to farm.
|
||||
"""
|
||||
# setup muster environment
|
||||
self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL")
|
||||
|
||||
if self.MUSTER_REST_URL is None:
|
||||
self.log.error(
|
||||
"\"MUSTER_REST_URL\" is not found. Skipping "
|
||||
"[{}]".format(instance)
|
||||
)
|
||||
raise RuntimeError("MUSTER_REST_URL not set")
|
||||
|
||||
self._load_credentials()
|
||||
# self._get_templates()
|
||||
|
||||
context = instance.context
|
||||
workspace = context.data["workspaceDir"]
|
||||
project_name = context.data["projectName"]
|
||||
asset_name = context.data["asset"]
|
||||
|
||||
filepath = None
|
||||
|
||||
allInstances = []
|
||||
for result in context.data["results"]:
|
||||
if ((result["instance"] is not None) and
|
||||
(result["instance"] not in allInstances)):
|
||||
allInstances.append(result["instance"])
|
||||
|
||||
for inst in allInstances:
|
||||
print(inst)
|
||||
if inst.data['family'] == 'scene':
|
||||
filepath = inst.data['destination_list'][0]
|
||||
|
||||
if not filepath:
|
||||
filepath = context.data["currentFile"]
|
||||
|
||||
self.log.debug(filepath)
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
comment = context.data.get("comment", "")
|
||||
scene = os.path.splitext(filename)[0]
|
||||
dirname = os.path.join(workspace, "renders")
|
||||
renderlayer = instance.data['renderlayer'] # rs_beauty
|
||||
renderlayer_name = instance.data['subset'] # beauty
|
||||
renderglobals = instance.data["renderGlobals"]
|
||||
# legacy_layers = renderlayer_globals["UseLegacyRenderLayers"]
|
||||
# deadline_user = context.data.get("deadlineUser", getpass.getuser())
|
||||
jobname = "%s - %s" % (filename, instance.name)
|
||||
|
||||
# Get the variables depending on the renderer
|
||||
render_variables = get_renderer_variables(renderlayer)
|
||||
output_filename_0 = preview_fname(folder=dirname,
|
||||
scene=scene,
|
||||
layer=renderlayer_name,
|
||||
padding=render_variables["padding"],
|
||||
ext=render_variables["ext"])
|
||||
|
||||
instance.data["outputDir"] = os.path.dirname(output_filename_0)
|
||||
self.log.debug("output: {}".format(filepath))
|
||||
# build path for metadata file
|
||||
metadata_filename = "{}_metadata.json".format(instance.data["subset"])
|
||||
output_dir = instance.data["outputDir"]
|
||||
metadata_path = os.path.join(output_dir, metadata_filename)
|
||||
|
||||
pype_root = os.environ["OPENPYPE_SETUP_PATH"]
|
||||
|
||||
# we must provide either full path to executable or use musters own
|
||||
# python named MPython.exe, residing directly in muster bin
|
||||
# directory.
|
||||
if platform.system().lower() == "windows":
|
||||
# for muster, those backslashes must be escaped twice
|
||||
muster_python = ("\"C:\\\\Program Files\\\\Virtual Vertex\\\\"
|
||||
"Muster 9\\\\MPython.exe\"")
|
||||
else:
|
||||
# we need to run pype as different user then Muster dispatcher
|
||||
# service is running (usually root).
|
||||
muster_python = ("/usr/sbin/runuser -u {}"
|
||||
" -- /usr/bin/python3".format(getpass.getuser()))
|
||||
|
||||
# build the path and argument. We are providing separate --pype
|
||||
# argument with network path to pype as post job actions are run
|
||||
# but dispatcher (Server) and not render clients. Render clients
|
||||
# inherit environment from publisher including PATH, so there's
|
||||
# no problem finding PYPE, but there is now way (as far as I know)
|
||||
# to set environment dynamically for dispatcher. Therefore this hack.
|
||||
args = [muster_python,
|
||||
_get_script().replace('\\', '\\\\'),
|
||||
"--paths",
|
||||
metadata_path.replace('\\', '\\\\'),
|
||||
"--pype",
|
||||
pype_root.replace('\\', '\\\\')]
|
||||
|
||||
postjob_command = " ".join(args)
|
||||
|
||||
try:
|
||||
# Ensure render folder exists
|
||||
os.makedirs(dirname)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
env = self.clean_environment()
|
||||
|
||||
payload = {
|
||||
"RequestData": {
|
||||
"platform": 0,
|
||||
"job": {
|
||||
"jobName": jobname,
|
||||
"templateId": _get_template_id(
|
||||
instance.data["renderer"]),
|
||||
"chunksInterleave": 2,
|
||||
"chunksPriority": "0",
|
||||
"chunksTimeoutValue": 320,
|
||||
"department": "",
|
||||
"dependIds": [""],
|
||||
"dependLinkMode": 0,
|
||||
"dependMode": 0,
|
||||
"emergencyQueue": False,
|
||||
"excludedPools": [""],
|
||||
"includedPools": [renderglobals["Pool"]],
|
||||
"packetSize": 4,
|
||||
"packetType": 1,
|
||||
"priority": 1,
|
||||
"jobId": -1,
|
||||
"startOn": 0,
|
||||
"parentId": -1,
|
||||
"project": project_name or scene,
|
||||
"shot": asset_name or scene,
|
||||
"camera": instance.data.get("cameras")[0],
|
||||
"dependMode": 0,
|
||||
"packetSize": 4,
|
||||
"packetType": 1,
|
||||
"priority": 1,
|
||||
"maximumInstances": 0,
|
||||
"assignedInstances": 0,
|
||||
"attributes": {
|
||||
"environmental_variables": {
|
||||
"value": ", ".join("{!s}={!r}".format(k, v)
|
||||
for (k, v) in env.items()),
|
||||
|
||||
"state": True,
|
||||
"subst": False
|
||||
},
|
||||
"memo": {
|
||||
"value": comment,
|
||||
"state": True,
|
||||
"subst": False
|
||||
},
|
||||
"frames_range": {
|
||||
"value": "{start}-{end}".format(
|
||||
start=int(instance.data["frameStart"]),
|
||||
end=int(instance.data["frameEnd"])),
|
||||
"state": True,
|
||||
"subst": False
|
||||
},
|
||||
"job_file": {
|
||||
"value": filepath,
|
||||
"state": True,
|
||||
"subst": True
|
||||
},
|
||||
"job_project": {
|
||||
"value": workspace,
|
||||
"state": True,
|
||||
"subst": True
|
||||
},
|
||||
"output_folder": {
|
||||
"value": dirname.replace("\\", "/"),
|
||||
"state": True,
|
||||
"subst": True
|
||||
},
|
||||
"post_job_action": {
|
||||
"value": postjob_command,
|
||||
"state": True,
|
||||
"subst": True
|
||||
},
|
||||
"MAYADIGITS": {
|
||||
"value": 1,
|
||||
"state": True,
|
||||
"subst": False
|
||||
},
|
||||
"ARNOLDMODE": {
|
||||
"value": "0",
|
||||
"state": True,
|
||||
"subst": False
|
||||
},
|
||||
"ABORTRENDER": {
|
||||
"value": "0",
|
||||
"state": True,
|
||||
"subst": True
|
||||
},
|
||||
"ARNOLDLICENSE": {
|
||||
"value": "0",
|
||||
"state": False,
|
||||
"subst": False
|
||||
},
|
||||
"ADD_FLAGS": {
|
||||
"value": "-rl {}".format(renderlayer),
|
||||
"state": True,
|
||||
"subst": True
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.preflight_check(instance)
|
||||
|
||||
self.log.debug("Submitting ...")
|
||||
self.log.debug(json.dumps(payload, indent=4, sort_keys=True))
|
||||
|
||||
response = self._submit(payload)
|
||||
# response = requests.post(url, json=payload)
|
||||
if not response.ok:
|
||||
raise Exception(response.text)
|
||||
|
||||
# Store output dir for unified publisher (filesequence)
|
||||
|
||||
instance.data["musterSubmissionJob"] = response.json()
|
||||
|
||||
def clean_environment(self):
|
||||
"""
|
||||
Clean and set environment variables for render job so render clients
|
||||
work in more or less same environment as publishing machine.
|
||||
|
||||
.. warning:: This is not usable for **post job action** as this is
|
||||
executed on dispatcher machine (server) and not render clients.
|
||||
"""
|
||||
keys = [
|
||||
# This will trigger `userSetup.py` on the slave
|
||||
# such that proper initialisation happens the same
|
||||
# way as it does on a local machine.
|
||||
# TODO(marcus): This won't work if the slaves don't
|
||||
# have access to these paths, such as if slaves are
|
||||
# running Linux and the submitter is on Windows.
|
||||
"PYTHONPATH",
|
||||
"PATH",
|
||||
|
||||
"MTOA_EXTENSIONS_PATH",
|
||||
"MTOA_EXTENSIONS",
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"MAYA_RENDER_DESC_PATH",
|
||||
"MAYA_MODULE_PATH",
|
||||
"ARNOLD_PLUGIN_PATH",
|
||||
"FTRACK_API_KEY",
|
||||
"FTRACK_API_USER",
|
||||
"FTRACK_SERVER",
|
||||
"PYBLISHPLUGINPATH",
|
||||
|
||||
# todo: This is a temporary fix for yeti variables
|
||||
"PEREGRINEL_LICENSE",
|
||||
"SOLIDANGLE_LICENSE",
|
||||
"ARNOLD_LICENSE"
|
||||
"MAYA_MODULE_PATH",
|
||||
"TOOL_ENV"
|
||||
]
|
||||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **legacy_io.Session)
|
||||
# self.log.debug("enviro: {}".format(pprint(environment)))
|
||||
for path in os.environ:
|
||||
if path.lower().startswith('pype_'):
|
||||
environment[path] = os.environ[path]
|
||||
|
||||
environment["PATH"] = os.environ["PATH"]
|
||||
# self.log.debug("enviro: {}".format(environment['OPENPYPE_SCRIPTS']))
|
||||
clean_environment = {}
|
||||
for key, value in environment.items():
|
||||
clean_path = ""
|
||||
self.log.debug("key: {}".format(key))
|
||||
if "://" in value:
|
||||
clean_path = value
|
||||
else:
|
||||
valid_paths = []
|
||||
for path in value.split(os.pathsep):
|
||||
if not path:
|
||||
continue
|
||||
try:
|
||||
path.decode('UTF-8', 'strict')
|
||||
valid_paths.append(os.path.normpath(path))
|
||||
except UnicodeDecodeError:
|
||||
print('path contains non UTF characters')
|
||||
|
||||
if valid_paths:
|
||||
clean_path = os.pathsep.join(valid_paths)
|
||||
|
||||
clean_environment[key] = clean_path
|
||||
|
||||
return clean_environment
|
||||
|
||||
def preflight_check(self, instance):
|
||||
"""Ensure the startFrame, endFrame and byFrameStep are integers"""
|
||||
|
||||
for key in ("frameStart", "frameEnd", "byFrameStep"):
|
||||
value = instance.data[key]
|
||||
|
||||
if int(value) == value:
|
||||
continue
|
||||
|
||||
self.log.warning(
|
||||
"%f=%d was rounded off to nearest integer"
|
||||
% (value, int(value))
|
||||
)
|
||||
|
||||
|
||||
# TODO: Remove hack to avoid this plug-in in new publisher
|
||||
# This plug-in should actually be in dedicated module
|
||||
if not os.environ.get("MUSTER_REST_URL"):
|
||||
del MayaSubmitMuster
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import appdirs
|
||||
|
||||
import pyblish.api
|
||||
from openpype.lib import requests_get
|
||||
from openpype.pipeline.publish import (
|
||||
context_plugin_should_run,
|
||||
RepairAction,
|
||||
)
|
||||
|
||||
|
||||
class ValidateMusterConnection(pyblish.api.ContextPlugin):
|
||||
"""
|
||||
Validate Muster REST API Service is running and we have valid auth token
|
||||
"""
|
||||
|
||||
label = "Validate Muster REST API Service"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
hosts = ["maya"]
|
||||
families = ["renderlayer"]
|
||||
token = None
|
||||
if not os.environ.get("MUSTER_REST_URL"):
|
||||
active = False
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
# Workaround bug pyblish-base#250
|
||||
if not context_plugin_should_run(self, context):
|
||||
return
|
||||
|
||||
# test if we have environment set (redundant as this plugin shouldn'
|
||||
# be active otherwise).
|
||||
try:
|
||||
MUSTER_REST_URL = os.environ["MUSTER_REST_URL"]
|
||||
except KeyError:
|
||||
self.log.error("Muster REST API url not found.")
|
||||
raise ValueError("Muster REST API url not found.")
|
||||
|
||||
# Load credentials
|
||||
try:
|
||||
self._load_credentials()
|
||||
except RuntimeError:
|
||||
self.log.error("invalid or missing access token")
|
||||
|
||||
assert self._token is not None, "Invalid or missing token"
|
||||
|
||||
# We have token, lets do trivial query to web api to see if we can
|
||||
# connect and access token is valid.
|
||||
params = {
|
||||
'authToken': self._token
|
||||
}
|
||||
api_entry = '/api/pools/list'
|
||||
response = requests_get(
|
||||
MUSTER_REST_URL + api_entry, params=params)
|
||||
assert response.status_code == 200, "invalid response from server"
|
||||
assert response.json()['ResponseData'], "invalid data in response"
|
||||
|
||||
def _load_credentials(self):
|
||||
"""
|
||||
Load Muster credentials from file and set `MUSTER_USER`,
|
||||
`MUSTER_PASSWORD`, `MUSTER_REST_URL` is loaded from settings.
|
||||
|
||||
.. todo::
|
||||
|
||||
Show login dialog if access token is invalid or missing.
|
||||
"""
|
||||
app_dir = os.path.normpath(
|
||||
appdirs.user_data_dir('pype-app', 'pype')
|
||||
)
|
||||
file_name = 'muster_cred.json'
|
||||
fpath = os.path.join(app_dir, file_name)
|
||||
file = open(fpath, 'r')
|
||||
muster_json = json.load(file)
|
||||
self._token = muster_json.get('token', None)
|
||||
if not self._token:
|
||||
raise RuntimeError("Invalid access token for Muster")
|
||||
file.close()
|
||||
self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL")
|
||||
if not self.MUSTER_REST_URL:
|
||||
raise AttributeError("Muster REST API url not set")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
"""
|
||||
Renew authentication token by logging into Muster
|
||||
"""
|
||||
api_url = "{}/muster/show_login".format(
|
||||
os.environ["OPENPYPE_WEBSERVER_URL"])
|
||||
cls.log.debug(api_url)
|
||||
response = requests_get(api_url, timeout=1)
|
||||
if response.status_code != 200:
|
||||
cls.log.error('Cannot show login form to Muster')
|
||||
raise Exception('Cannot show login form to Muster')
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
from aiohttp.web_response import Response
|
||||
|
||||
|
||||
class MusterModuleRestApi:
|
||||
def __init__(self, user_module, server_manager):
|
||||
self.module = user_module
|
||||
self.server_manager = server_manager
|
||||
|
||||
self.prefix = "/muster"
|
||||
|
||||
self.register()
|
||||
|
||||
def register(self):
|
||||
self.server_manager.add_route(
|
||||
"GET",
|
||||
self.prefix + "/show_login",
|
||||
self.show_login_widget
|
||||
)
|
||||
|
||||
async def show_login_widget(self, request):
|
||||
self.module.action_show_login.trigger()
|
||||
return Response(status=200)
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
from openpype import resources, style
|
||||
|
||||
|
||||
class MusterLogin(QtWidgets.QWidget):
|
||||
|
||||
SIZE_W = 300
|
||||
SIZE_H = 150
|
||||
|
||||
loginSignal = QtCore.Signal(object, object, object)
|
||||
|
||||
def __init__(self, module, parent=None):
|
||||
|
||||
super(MusterLogin, self).__init__(parent)
|
||||
|
||||
self.module = module
|
||||
|
||||
# Icon
|
||||
icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
# Font
|
||||
self.font = QtGui.QFont()
|
||||
self.font.setFamily("DejaVu Sans Condensed")
|
||||
self.font.setPointSize(9)
|
||||
self.font.setBold(True)
|
||||
self.font.setWeight(50)
|
||||
self.font.setKerning(True)
|
||||
|
||||
# Size setting
|
||||
self.resize(self.SIZE_W, self.SIZE_H)
|
||||
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
|
||||
self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
self.setLayout(self._main())
|
||||
self.setWindowTitle('Muster login')
|
||||
|
||||
def _main(self):
|
||||
self.main = QtWidgets.QVBoxLayout()
|
||||
self.main.setObjectName("main")
|
||||
|
||||
self.form = QtWidgets.QFormLayout()
|
||||
self.form.setContentsMargins(10, 15, 10, 5)
|
||||
self.form.setObjectName("form")
|
||||
|
||||
self.label_username = QtWidgets.QLabel("Username:")
|
||||
self.label_username.setFont(self.font)
|
||||
self.label_username.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||
self.label_username.setTextFormat(QtCore.Qt.RichText)
|
||||
|
||||
self.input_username = QtWidgets.QLineEdit()
|
||||
self.input_username.setEnabled(True)
|
||||
self.input_username.setFrame(True)
|
||||
self.input_username.setPlaceholderText(
|
||||
self._translate("main", "e.g. John Smith")
|
||||
)
|
||||
|
||||
self.label_password = QtWidgets.QLabel("Password:")
|
||||
self.label_password.setFont(self.font)
|
||||
self.label_password.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||
self.label_password.setTextFormat(QtCore.Qt.RichText)
|
||||
|
||||
self.input_password = QtWidgets.QLineEdit()
|
||||
self.input_password.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||
self.input_password.setEnabled(True)
|
||||
self.input_password.setFrame(True)
|
||||
self.input_password.setPlaceholderText(
|
||||
self._translate("main", "e.g. ********")
|
||||
)
|
||||
|
||||
self.error_label = QtWidgets.QLabel("")
|
||||
self.error_label.setFont(self.font)
|
||||
self.error_label.setStyleSheet('color: #FC6000')
|
||||
self.error_label.setWordWrap(True)
|
||||
self.error_label.hide()
|
||||
|
||||
self.form.addRow(self.label_username, self.input_username)
|
||||
self.form.addRow(self.label_password, self.input_password)
|
||||
self.form.addRow(self.error_label)
|
||||
|
||||
self.btn_group = QtWidgets.QHBoxLayout()
|
||||
self.btn_group.addStretch(1)
|
||||
self.btn_group.setObjectName("btn_group")
|
||||
|
||||
self.btn_ok = QtWidgets.QPushButton("Ok")
|
||||
self.btn_ok.clicked.connect(self.click_ok)
|
||||
|
||||
self.btn_cancel = QtWidgets.QPushButton("Cancel")
|
||||
QtWidgets.QShortcut(
|
||||
QtGui.QKeySequence(
|
||||
QtCore.Qt.Key_Escape), self).activated.connect(self.close)
|
||||
self.btn_cancel.clicked.connect(self.close)
|
||||
|
||||
self.btn_group.addWidget(self.btn_ok)
|
||||
self.btn_group.addWidget(self.btn_cancel)
|
||||
|
||||
self.main.addLayout(self.form)
|
||||
self.main.addLayout(self.btn_group)
|
||||
|
||||
return self.main
|
||||
|
||||
def keyPressEvent(self, key_event):
|
||||
if key_event.key() == QtCore.Qt.Key_Return:
|
||||
if self.input_username.hasFocus():
|
||||
self.input_password.setFocus()
|
||||
|
||||
elif self.input_password.hasFocus() or self.btn_ok.hasFocus():
|
||||
self.click_ok()
|
||||
|
||||
elif self.btn_cancel.hasFocus():
|
||||
self.close()
|
||||
else:
|
||||
super().keyPressEvent(key_event)
|
||||
|
||||
def setError(self, msg):
|
||||
self.error_label.setText(msg)
|
||||
self.error_label.show()
|
||||
|
||||
def invalid_input(self, entity):
|
||||
entity.setStyleSheet("border: 1px solid red;")
|
||||
|
||||
def click_ok(self):
|
||||
# all what should happen - validations and saving into appsdir
|
||||
username = self.input_username.text()
|
||||
password = self.input_password.text()
|
||||
# TODO: more robust validation. Password can be empty in muster?
|
||||
if not username:
|
||||
self.setError("Username cannot be empty")
|
||||
self.invalid_input(self.input_username)
|
||||
try:
|
||||
self.save_credentials(username, password)
|
||||
except Exception as e:
|
||||
self.setError(
|
||||
"<b>Cannot get auth token:</b>\n<code>{}</code>".format(e))
|
||||
else:
|
||||
self._close_widget()
|
||||
|
||||
def save_credentials(self, username, password):
|
||||
self.module.get_auth_token(username, password)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(MusterLogin, self).showEvent(event)
|
||||
|
||||
# Make btns same width
|
||||
max_width = max(
|
||||
self.btn_ok.sizeHint().width(),
|
||||
self.btn_cancel.sizeHint().width()
|
||||
)
|
||||
self.btn_ok.setMinimumWidth(max_width)
|
||||
self.btn_cancel.setMinimumWidth(max_width)
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
self._close_widget()
|
||||
|
||||
def _close_widget(self):
|
||||
self.hide()
|
||||
|
|
@ -1971,7 +1971,6 @@ class PlaceholderCreateMixin(object):
|
|||
if not placeholder.data.get("keep_placeholder", True):
|
||||
self.delete_placeholder(placeholder)
|
||||
|
||||
|
||||
def create_failed(self, placeholder, creator_data):
|
||||
if hasattr(placeholder, "create_failed"):
|
||||
placeholder.create_failed(creator_data)
|
||||
|
|
@ -2036,7 +2035,7 @@ class CreatePlaceholderItem(PlaceholderItem):
|
|||
self._failed_created_publish_instances = []
|
||||
|
||||
def get_errors(self):
|
||||
if not self._failed_representations:
|
||||
if not self._failed_created_publish_instances:
|
||||
return []
|
||||
message = (
|
||||
"Failed to create {} instance using Creator {}"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class CollectFarmTarget(pyblish.api.InstancePlugin):
|
|||
farm_name = ""
|
||||
op_modules = context.data.get("openPypeModules")
|
||||
|
||||
for farm_renderer in ["deadline", "royalrender", "muster"]:
|
||||
for farm_renderer in ["deadline", "royalrender"]:
|
||||
op_module = op_modules.get(farm_renderer, False)
|
||||
|
||||
if op_module and op_module.enabled:
|
||||
|
|
|
|||
|
|
@ -93,14 +93,6 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin):
|
|||
assert ctx.get("user") == data.get("user"), ctx_err % "user"
|
||||
assert ctx.get("version") == data.get("version"), ctx_err % "version"
|
||||
|
||||
# ftrack credentials are passed as environment variables by Deadline
|
||||
# to publish job, but Muster doesn't pass them.
|
||||
if data.get("ftrack") and not os.environ.get("FTRACK_API_USER"):
|
||||
ftrack = data.get("ftrack")
|
||||
os.environ["FTRACK_API_USER"] = ftrack["FTRACK_API_USER"]
|
||||
os.environ["FTRACK_API_KEY"] = ftrack["FTRACK_API_KEY"]
|
||||
os.environ["FTRACK_SERVER"] = ftrack["FTRACK_SERVER"]
|
||||
|
||||
# now we can just add instances from json file and we are done
|
||||
any_staging_dir_persistent = False
|
||||
for instance_data in data.get("instances"):
|
||||
|
|
|
|||
|
|
@ -220,22 +220,6 @@ def _convert_deadline_system_settings(
|
|||
output["modules"]["deadline"] = deadline_settings
|
||||
|
||||
|
||||
def _convert_muster_system_settings(
|
||||
ayon_settings, output, addon_versions, default_settings
|
||||
):
|
||||
enabled = addon_versions.get("muster") is not None
|
||||
muster_settings = default_settings["modules"]["muster"]
|
||||
muster_settings["enabled"] = enabled
|
||||
if enabled:
|
||||
ayon_muster = ayon_settings["muster"]
|
||||
muster_settings["MUSTER_REST_URL"] = ayon_muster["MUSTER_REST_URL"]
|
||||
muster_settings["templates_mapping"] = {
|
||||
item["name"]: item["value"]
|
||||
for item in ayon_muster["templates_mapping"]
|
||||
}
|
||||
output["modules"]["muster"] = muster_settings
|
||||
|
||||
|
||||
def _convert_royalrender_system_settings(
|
||||
ayon_settings, output, addon_versions, default_settings
|
||||
):
|
||||
|
|
@ -261,7 +245,6 @@ def _convert_modules_system(
|
|||
_convert_timers_manager_system_settings,
|
||||
_convert_clockify_system_settings,
|
||||
_convert_deadline_system_settings,
|
||||
_convert_muster_system_settings,
|
||||
_convert_royalrender_system_settings,
|
||||
):
|
||||
func(ayon_settings, output, addon_versions, default_settings)
|
||||
|
|
|
|||
|
|
@ -164,23 +164,6 @@
|
|||
"default": "http://127.0.0.1:8082"
|
||||
}
|
||||
},
|
||||
"muster": {
|
||||
"enabled": false,
|
||||
"MUSTER_REST_URL": "http://127.0.0.1:9890",
|
||||
"templates_mapping": {
|
||||
"file_layers": 7,
|
||||
"mentalray": 2,
|
||||
"mentalray_sf": 6,
|
||||
"redshift": 55,
|
||||
"renderman": 29,
|
||||
"software": 1,
|
||||
"software_sf": 5,
|
||||
"turtle": 10,
|
||||
"vector": 4,
|
||||
"vray": 37,
|
||||
"ffmpeg": 48
|
||||
}
|
||||
},
|
||||
"royalrender": {
|
||||
"enabled": false,
|
||||
"rr_paths": {
|
||||
|
|
|
|||
|
|
@ -645,7 +645,7 @@ How output of the schema could look like on save:
|
|||
},
|
||||
"is_group": true,
|
||||
"key": "templates_mapping",
|
||||
"label": "Muster - Templates mapping",
|
||||
"label": "Deadline - Templates mapping",
|
||||
"is_file": true
|
||||
}
|
||||
```
|
||||
|
|
@ -657,7 +657,7 @@ How output of the schema could look like on save:
|
|||
"object_type": "text",
|
||||
"is_group": true,
|
||||
"key": "templates_mapping",
|
||||
"label": "Muster - Templates mapping",
|
||||
"label": "Deadline - Templates mapping",
|
||||
"is_file": true
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -207,37 +207,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "muster",
|
||||
"label": "Muster",
|
||||
"require_restart": true,
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "MUSTER_REST_URL",
|
||||
"label": "Muster Rest URL"
|
||||
},
|
||||
{
|
||||
"type": "dict-modifiable",
|
||||
"object_type": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 300
|
||||
},
|
||||
"is_group": true,
|
||||
"key": "templates_mapping",
|
||||
"label": "Templates mapping"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "royalrender",
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@
|
|||
},
|
||||
"is_group": true,
|
||||
"key": "templates_mapping",
|
||||
"label": "Muster - Templates mapping",
|
||||
"label": "Deadline - Templates mapping",
|
||||
"is_file": true
|
||||
}
|
||||
```
|
||||
|
|
@ -346,7 +346,7 @@
|
|||
"object_type": "text",
|
||||
"is_group": true,
|
||||
"key": "templates_mapping",
|
||||
"label": "Muster - Templates mapping",
|
||||
"label": "Deadline - Templates mapping",
|
||||
"is_file": true
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
from typing import Type
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import KitsuSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class KitsuAddon(BaseServerAddon):
|
||||
name = "kitsu"
|
||||
title = "Kitsu"
|
||||
version = __version__
|
||||
settings_model: Type[KitsuSettings] = KitsuSettings
|
||||
frontend_scopes = {}
|
||||
services = {}
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
return settings_model_cls(**DEFAULT_VALUES)
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
from pydantic import Field
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
|
||||
|
||||
class EntityPattern(BaseSettingsModel):
|
||||
episode: str = Field(title="Episode")
|
||||
sequence: str = Field(title="Sequence")
|
||||
shot: str = Field(title="Shot")
|
||||
|
||||
|
||||
def _status_change_cond_enum():
|
||||
return [
|
||||
{"value": "equal", "label": "Equal"},
|
||||
{"value": "not_equal", "label": "Not equal"}
|
||||
]
|
||||
|
||||
|
||||
class StatusChangeCondition(BaseSettingsModel):
|
||||
condition: str = Field(
|
||||
"equal",
|
||||
enum_resolver=_status_change_cond_enum,
|
||||
title="Condition"
|
||||
)
|
||||
short_name: str = Field("", title="Short name")
|
||||
|
||||
|
||||
class StatusChangeProductTypeRequirementModel(BaseSettingsModel):
|
||||
condition: str = Field(
|
||||
"equal",
|
||||
enum_resolver=_status_change_cond_enum,
|
||||
title="Condition"
|
||||
)
|
||||
product_type: str = Field("", title="Product type")
|
||||
|
||||
|
||||
class StatusChangeConditionsModel(BaseSettingsModel):
|
||||
status_conditions: list[StatusChangeCondition] = Field(
|
||||
default_factory=list,
|
||||
title="Status conditions"
|
||||
)
|
||||
product_type_requirements: list[StatusChangeProductTypeRequirementModel] = Field(
|
||||
default_factory=list,
|
||||
title="Product type requirements")
|
||||
|
||||
|
||||
class CustomCommentTemplateModel(BaseSettingsModel):
|
||||
enabled: bool = Field(True)
|
||||
comment_template: str = Field("", title="Custom comment")
|
||||
|
||||
|
||||
class IntegrateKitsuNotes(BaseSettingsModel):
|
||||
"""Kitsu supports markdown and here you can create a custom comment template.
|
||||
|
||||
You can use data from your publishing instance's data.
|
||||
"""
|
||||
|
||||
set_status_note: bool = Field(title="Set status on note")
|
||||
note_status_shortname: str = Field(title="Note shortname")
|
||||
status_change_conditions: StatusChangeConditionsModel = Field(
|
||||
default_factory=StatusChangeConditionsModel,
|
||||
title="Status change conditions"
|
||||
)
|
||||
custom_comment_template: CustomCommentTemplateModel = Field(
|
||||
default_factory=CustomCommentTemplateModel,
|
||||
title="Custom Comment Template",
|
||||
)
|
||||
|
||||
|
||||
class PublishPlugins(BaseSettingsModel):
|
||||
IntegrateKitsuNote: IntegrateKitsuNotes = Field(
|
||||
default_factory=IntegrateKitsuNotes,
|
||||
title="Integrate Kitsu Note"
|
||||
)
|
||||
|
||||
|
||||
class KitsuSettings(BaseSettingsModel):
|
||||
server: str = Field(
|
||||
"",
|
||||
title="Kitsu Server",
|
||||
scope=["studio"],
|
||||
)
|
||||
entities_naming_pattern: EntityPattern = Field(
|
||||
default_factory=EntityPattern,
|
||||
title="Entities naming pattern",
|
||||
)
|
||||
publish: PublishPlugins = Field(
|
||||
default_factory=PublishPlugins,
|
||||
title="Publish plugins",
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"entities_naming_pattern": {
|
||||
"episode": "E##",
|
||||
"sequence": "SQ##",
|
||||
"shot": "SH##"
|
||||
},
|
||||
"publish": {
|
||||
"IntegrateKitsuNote": {
|
||||
"set_status_note": False,
|
||||
"note_status_shortname": "wfa",
|
||||
"status_change_conditions": {
|
||||
"status_conditions": [],
|
||||
"product_type_requirements": []
|
||||
},
|
||||
"custom_comment_template": {
|
||||
"enabled": False,
|
||||
"comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| product type | `{product[type]}` |\n| name | `{name}` |"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.1"
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
from typing import Type
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .version import __version__
|
||||
from .settings import MusterSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class MusterAddon(BaseServerAddon):
|
||||
name = "muster"
|
||||
version = __version__
|
||||
title = "Muster"
|
||||
settings_model: Type[MusterSettings] = MusterSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
return settings_model_cls(**DEFAULT_VALUES)
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
from ayon_server.settings import BaseSettingsModel, SettingsField
|
||||
|
||||
|
||||
class TemplatesMapping(BaseSettingsModel):
|
||||
_layout = "compact"
|
||||
name: str = SettingsField(title="Name")
|
||||
value: int = SettingsField(title="mapping")
|
||||
|
||||
|
||||
class MusterSettings(BaseSettingsModel):
|
||||
enabled: bool = True
|
||||
MUSTER_REST_URL: str = SettingsField(
|
||||
"",
|
||||
title="Muster Rest URL",
|
||||
scope=["studio"],
|
||||
)
|
||||
|
||||
templates_mapping: list[TemplatesMapping] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Templates mapping",
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"enabled": False,
|
||||
"MUSTER_REST_URL": "http://127.0.0.1:9890",
|
||||
"templates_mapping": [
|
||||
{"name": "file_layers", "value": 7},
|
||||
{"name": "mentalray", "value": 2},
|
||||
{"name": "mentalray_sf", "value": 6},
|
||||
{"name": "redshift", "value": 55},
|
||||
{"name": "renderman", "value": 29},
|
||||
{"name": "software", "value": 1},
|
||||
{"name": "software_sf", "value": 5},
|
||||
{"name": "turtle", "value": 10},
|
||||
{"name": "vector", "value": 4},
|
||||
{"name": "vray", "value": 37},
|
||||
{"name": "ffmpeg", "value": 48}
|
||||
]
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.1"
|
||||
4
start.py
4
start.py
|
|
@ -1186,10 +1186,6 @@ def get_info(use_staging=None) -> list:
|
|||
inf.append(("Using Deadline webservice at",
|
||||
os.environ.get("DEADLINE_REST_URL")))
|
||||
|
||||
if os.environ.get('MUSTER_REST_URL'):
|
||||
inf.append(("Using Muster at",
|
||||
os.environ.get("MUSTER_REST_URL")))
|
||||
|
||||
# Reinitialize
|
||||
Logger.initialize()
|
||||
|
||||
|
|
|
|||
|
|
@ -95,13 +95,6 @@ Disable/Enable Standalone Publisher option
|
|||
**`Deadline Rest URL`** - URL to deadline webservice that. This URL must be reachable from every
|
||||
workstation that should be submitting render jobs to deadline via OpenPype.
|
||||
|
||||
### Muster
|
||||
|
||||
**`Muster Rest URL`** - URL to Muster webservice that. This URL must be reachable from every
|
||||
workstation that should be submitting render jobs to muster via OpenPype.
|
||||
|
||||
**`templates mapping`** - you can customize Muster templates to match your existing setup here.
|
||||
|
||||
### Royal Render
|
||||
|
||||
**`Royal Render Root Paths`** - multi platform paths to Royal Render installation.
|
||||
|
|
|
|||
|
|
@ -386,14 +386,11 @@ models you've put into layout.
|
|||
|
||||
OpenPype in Maya can be used for submitting renders to render farm and for their
|
||||
subsequent publishing. Right now OpenPype support [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline)
|
||||
and [Virtual Vertex Muster](https://www.vvertex.com/overview/).
|
||||
and [Royal Render](https://www.royalrender.de/).
|
||||
|
||||
* For setting up Muster support see [admin section](module_muster.md)
|
||||
* For setting up Royal Render support see [admin section](module_royalrender.md)
|
||||
* For setting up Deadline support see [here](module_deadline.md)
|
||||
|
||||
:::note Muster login
|
||||
Muster is now configured so every user must log in to get authentication support. If OpenPype founds out this token is missing or expired, it will ask again for credentials.
|
||||
:::
|
||||
|
||||
### Creating basic render setup
|
||||
|
||||
|
|
@ -436,12 +433,7 @@ checked **Use selection** it will use your current Render Layers (if you have th
|
|||
if no render layers is present in scene, it will create one for you named **Main** and under it
|
||||
default collection with `*` selector.
|
||||
|
||||
No matter if you use *Deadline* or *Muster*, OpenPype will try to connect to render farm and
|
||||
fetch machine pool list.
|
||||
|
||||
:::note Muster login
|
||||
This might fail on *Muster* in the event that you have expired authentication token. In that case, you'll be presented with login window. Nothing will be created in the scene until you log in again and do create **Render** again.
|
||||
:::
|
||||
OpenPype will try to connect to render farm and fetch machine pool list.
|
||||
|
||||
So now my scene now looks like this:
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ As was mentioned schema items define output type of values, how they are stored
|
|||
- `"is_file"` - this key is used when defaults values are stored in the file. Its value matches the filename where values are stored
|
||||
- key is validated, must be unique in hierarchy otherwise it won't be possible to store default values
|
||||
- make sense to fill it only if it's value if `true`
|
||||
|
||||
|
||||
- `"is_group"` - define that all values under a key in settings hierarchy will be overridden if any value is modified
|
||||
- this key is not allowed for all inputs as they may not have technical ability to handle it
|
||||
- key is validated, must be unique in hierarchy and is automatically filled on last possible item if is not defined in schemas
|
||||
|
|
@ -710,7 +710,7 @@ How output of the schema could look like on save:
|
|||
"object_type": "text",
|
||||
"is_group": true,
|
||||
"key": "templates_mapping",
|
||||
"label": "Muster - Templates mapping",
|
||||
"label": "Deadline - Templates mapping",
|
||||
"is_file": true
|
||||
}
|
||||
```
|
||||
|
|
@ -726,7 +726,7 @@ How output of the schema could look like on save:
|
|||
},
|
||||
"is_group": true,
|
||||
"key": "templates_mapping",
|
||||
"label": "Muster - Templates mapping",
|
||||
"label": "Deadline - Templates mapping",
|
||||
"is_file": true
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -238,17 +238,11 @@ Create preview quicktimes from rendered frames
|
|||
|
||||
publish rendered outputs to Avalon and Ftrack
|
||||
|
||||
## Muster
|
||||
|
||||
Publish to deadline from
|
||||
|
||||
## Royal Render
|
||||
Publish to Royal Render from
|
||||
Maya
|
||||
|
||||
Nuke
|
||||
|
||||
Create preview quicktimes from rendered frames
|
||||
|
||||
publish rendered outputs to Avalon and Ftrack
|
||||
|
||||
## Clockify
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
id: module_muster
|
||||
title: Muster Administration
|
||||
sidebar_label: Muster
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
|
||||
|
|
@ -157,6 +157,10 @@ machine setup on farm. If there is no mix of windows/linux machines on farm, the
|
|||
|
||||
## Virtual Vertex Muster
|
||||
|
||||
:::warning
|
||||
Support for Muster was removed from OpenPype and AYON.
|
||||
:::
|
||||
|
||||
Pype supports rendering with [Muster](https://www.vvertex.com/). To enable it:
|
||||
1. Add `muster` to **init_env** to your `deploy.json`
|
||||
file:
|
||||
|
|
|
|||
|
|
@ -110,7 +110,6 @@ module.exports = {
|
|||
"module_kitsu",
|
||||
"module_site_sync",
|
||||
"module_deadline",
|
||||
"module_muster",
|
||||
"module_royalrender",
|
||||
"module_clockify",
|
||||
"module_slack"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const services = [
|
|||
title: <>Extensible</>,
|
||||
description: (
|
||||
<>
|
||||
Project needs differ, clients differ and studios differ. OpenPype is designed to fit into your workflow and bend to your will. If a feature is missing, it can most probably be added.
|
||||
Project needs differ, clients differ and studios differ. OpenPype is designed to fit into your workflow and bend to your will. If a feature is missing, it can most probably be added.
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
@ -310,7 +310,7 @@ function Home() {
|
|||
<h2>Why choose openPype?
|
||||
</h2>
|
||||
<p>
|
||||
Pipeline is the technical backbone of your production. It means, that whatever solution you use, it will cause vendor-lock to some extend.
|
||||
Pipeline is the technical backbone of your production. It means, that whatever solution you use, it will cause vendor-lock to some extend.
|
||||
You can mitigate this risk by developing purely in-house tools, however, that just shifts the problem from a software vendor to your developers. Sooner or later, you'll hit the limits of such solution. In-house tools tend to be undocumented, narrow focused and heavily dependent on a very few or even a single developer.
|
||||
</p>
|
||||
<p>
|
||||
|
|
@ -332,7 +332,7 @@ function Home() {
|
|||
<img src="/img/app_maya.png" alt="" title=""></img>
|
||||
<span className="caption">Maya</span>
|
||||
</a>
|
||||
|
||||
|
||||
<a className="link" href="https://www.autodesk.com/products/flame">
|
||||
<img src="/img/app_flame.png" alt="" title=""></img>
|
||||
<span className="caption">Flame</span>
|
||||
|
|
@ -422,7 +422,7 @@ function Home() {
|
|||
<img src="/img/app_deadline.png" alt="" title=""></img>
|
||||
<span className="caption">Deadline</span>
|
||||
</a>
|
||||
|
||||
|
||||
<a className="link" href="https://www.royalrender.de/index.php/startseite.html">
|
||||
<img src="/img/app_royalrender.png" alt="" title=""></img>
|
||||
<span className="caption">Royal Render</span>
|
||||
|
|
@ -443,17 +443,12 @@ function Home() {
|
|||
<p> <b>Planned or in development by us and OpenPype community.</b></p>
|
||||
|
||||
<div className={classnames('showcase',)}>
|
||||
|
||||
|
||||
<a className="link" href="https://fatfi.sh/aquarium/en">
|
||||
<img src="/img/app_aquarium.png" alt="" title=""></img>
|
||||
<span className="caption">Aquarium</span>
|
||||
</a>
|
||||
|
||||
<a className="link" href="https://www.vvertex.com">
|
||||
<img src="/img/app_muster.png" alt="" title=""></img>
|
||||
<span className="caption">Muster</span>
|
||||
</a>
|
||||
|
||||
<a className="link" href="https://www.hibob.com">
|
||||
<img src="/img/app_hibob.png" alt="Hi Bob" title="Hi Bob"></img>
|
||||
<span className="caption">Bob</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue