🔥 remove Muster

This commit is contained in:
Ondřej Samohel 2023-12-21 14:27:35 +01:00
parent e46e76254e
commit 9ff27b51b5
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
30 changed files with 49 additions and 1245 deletions

View file

@ -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:

View file

@ -1332,7 +1332,6 @@ class TrayModulesManager(ModulesManager):
"user",
"ftrack",
"kitsu",
"muster",
"launcher_tool",
"avalon",
"clockify",

View file

@ -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:

View file

@ -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
"""

View file

@ -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.
@ -89,7 +83,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
"""
label = "Submit image sequence jobs to Deadline or Muster"
label = "Submit image sequence jobs to Deadline"
order = pyblish.api.IntegratorOrder + 0.2
icon = "tractor"
@ -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
"""
@ -581,21 +571,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
._____.
'''
render_job = None
submission_type = ""
if instance.data.get("toBeRenderedOn") == "deadline":
render_job = instance.data.pop("deadlineSubmissionJob", None)
submission_type = "deadline"
if instance.data.get("toBeRenderedOn") == "muster":
render_job = instance.data.pop("musterSubmissionJob", None)
submission_type = "muster"
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
@ -624,21 +604,19 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
"FTRACK_SERVER": os.environ.get("FTRACK_SERVER"),
}
deadline_publish_job_id = None
if submission_type == "deadline":
# get default deadline webservice url from deadline module
self.deadline_url = instance.context.data["defaultDeadline"]
# if custom one is set in instance, use that
if instance.data.get("deadlineUrl"):
self.deadline_url = instance.data.get("deadlineUrl")
assert self.deadline_url, "Requires Deadline Webservice URL"
# get default deadline webservice url from deadline module
self.deadline_url = instance.context.data["defaultDeadline"]
# if custom one is set in instance, use that
if instance.data.get("deadlineUrl"):
self.deadline_url = instance.data.get("deadlineUrl")
assert self.deadline_url, "Requires Deadline Webservice URL"
deadline_publish_job_id = \
self._submit_deadline_post_job(instance, render_job, instances)
deadline_publish_job_id = \
self._submit_deadline_post_job(instance, render_job, instances)
# Inject deadline url to instances.
for inst in instances:
inst["deadlineUrl"] = self.deadline_url
# Inject deadline url to instances.
for inst in instances:
inst["deadlineUrl"] = self.deadline_url
# publish job file
publish_job = {
@ -664,15 +642,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
if audio_file and os.path.isfile(audio_file):
publish_job.update({"audio": audio_file})
# pass Ftrack credentials in case of Muster
if submission_type == "muster":
ftrack = {
"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"),
}
publish_job.update({"ftrack": ftrack})
metadata_path, rootless_metadata_path = \
create_metadata_path(instance, anatomy)

View file

@ -1,6 +0,0 @@
from .muster import MusterModule
__all__ = (
"MusterModule",
)

View file

@ -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)

View file

@ -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

View file

@ -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')

View file

@ -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)

View file

@ -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()

View file

@ -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:

View file

@ -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"):

View file

@ -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)

View file

@ -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": {

View file

@ -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
}
```

View file

@ -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",

View file

@ -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
}
```

View file

@ -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)

View file

@ -1,41 +0,0 @@
from pydantic import Field
from ayon_server.settings import BaseSettingsModel
class TemplatesMapping(BaseSettingsModel):
_layout = "compact"
name: str = Field(title="Name")
value: int = Field(title="mapping")
class MusterSettings(BaseSettingsModel):
enabled: bool = True
MUSTER_REST_URL: str = Field(
"",
title="Muster Rest URL",
scope=["studio"],
)
templates_mapping: list[TemplatesMapping] = Field(
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}
]
}

View file

@ -1 +0,0 @@
__version__ = "0.1.1"

View file

@ -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()

View file

@ -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.

View file

@ -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:

View file

@ -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
}
```

View file

@ -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

View file

@ -1,10 +0,0 @@
---
id: module_muster
title: Muster Administration
sidebar_label: Muster
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

View file

@ -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:

View file

@ -110,7 +110,6 @@ module.exports = {
"module_kitsu",
"module_site_sync",
"module_deadline",
"module_muster",
"module_royalrender",
"module_clockify",
"module_slack"

View file

@ -449,11 +449,6 @@ function Home() {
<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>