%MJSComputeCloud Interact with an MJS cluster that was started on the cloud
%
%   parallel.cluster.MJSComputeCloud methods:
%      MJSComputeCloud        - Create a MJSComputeCloud cluster instance
%      batch                  - Run MATLAB script or function as batch job
%      createCommunicatingJob - Create a new communicating job
%      createJob              - Create a new independent job
%      findJob                - Find job objects belonging to a cluster object
%      isequal                - True if clusters have same property values
%      parpool                - Create a parallel pool of workers on a cluster and return a pool object
%      saveAsProfile          - Save modified properties to a profile
%      saveProfile            - Save modified properties to the current profile
%      shutdown               - Shut down this cluster
%      start                  - Start this cluster
%      wait                   - Wait for this cluster to change state
%
%   parallel.cluster.MJSComputeCloud properties:
%      BusyWorkers               - Workers currently running tasks
%      ClusterMatlabRoot         - Specifies the path to MATLAB that the cluster is using
%      HasSecureCommunication    - True if the cluster is using secure communication
%      Host                      - Host on which this cluster is running
%      IdleWorkers               - Workers that are free to run tasks
%      JobStorageLocation        - Location where job and task data is stored
%      Jobs                      - List of jobs contained in this cluster
%      LicenseNumber             - License number to use when running jobs with this cluster
%      MatlabVersion             - Version of MATLAB running on the workers
%      MaxNumWorkers             - Maximum number of workers for this cluster
%      Modified                  - True if any properties in this cluster have been modified
%      Name                      - Name of this cluster
%      NumBusyWorkers            - Number of workers currently running tasks
%      NumIdleWorkers            - Number of workers that are free to run tasks
%      NumMachines               - The number of machines in this cluster
%      NumThreads                - Number of computational threads for workers
%      NumWorkers                - Number of workers currently running on this cluster
%      NumWorkersRequested       - Number of workers requested for this cluster
%      OperatingSystem           - Operating system of the nodes used by the cluster
%      PreferredPoolNumWorkers   - Preferred number of workers in a parallel pool
%      Profile                   - Profile used to build this cluster
%      RequiresClientCertificate - True if the cluster requires the client to have a certificate
%      RequiresOnlineLicensing   - True if the cluster is using online licensing
%      SharedState               - The shared state of the cluster
%      ShutdownAt                - Shutdown time or event
%      State                     - Current state of the cluster
%      Type                      - Type of this cluster
%      UserData                  - Data associated with a cluster object in this client session
%      Username                  - User who is using the cluster
%
%   See also PARCLUSTER, parallel.Cluster.

%   Copyright 2011-2023 The MathWorks, Inc.

classdef ( ConstructOnLoad = true, Sealed ) MJSComputeCloud < parallel.cluster.MJSCloudCluster
    
    properties ( Dependent )
        %NumWorkers Number of workers currently running on this cluster
        %   NumWorkers indicates the total number of workers currently
        %   available to the cluster for running your jobs.
        %   (read-only)
        %
        %   See also parallel.cluster.MJSComputeCloud.BusyWorkers,
        %            parallel.cluster.MJSComputeCloud.IdleWorkers, 
        %            parallel.cluster.MJSComputeCloud.MaxNumWorkers,
        %            parallel.cluster.MJSComputeCloud.NumBusyWorkers, 
        %            parallel.cluster.MJSComputeCloud.NumIdleWorkers.
        NumWorkers

        %NumMachines The number of machines in this cluster
        %   NumMachines indicates the current number of machines in this
        %   cluster, including the headnode.
        %   (read-only)
        %
        %   See also parallel.cluster.MJSComputeCloud.MaxNumWorkers,
        %            parallel.cluster.MJSComputeCloud.NumWorkers.
        NumMachines
    end

    properties ( Dependent, SetAccess = private )
        %MatlabVersion Version of MATLAB running on the workers
        MatlabVersion
        %SharedState The shared state of the cluster
        SharedState
    end

    properties ( Dependent )
        %NumWorkersRequested Number of workers requested for this cluster
        %   NumWorkersRequested indicates the target size of the cluster.
        %   The cluster will add or remove workers as soon as possible to
        %   reach this size.
        %   (read-only)
        %
        %   See also parallel.cluster.MJSComputeCloud.MaxNumWorkers,
        %            parallel.cluster.MJSComputeCloud.NumWorkers.
        NumWorkersRequested
        
        %MaxNumWorkers Maximum number of workers for this cluster
        %   MaxNumWorkers indicates the maximum allowed size of this
        %   cluster. New workers will be added to the cluster up to and
        %   including this value, as necessary.
        %   (read-only)
        %
        %   See also parallel.cluster.MJSComputeCloud.NumWorkersRequested,
        %            parallel.cluster.MJSComputeCloud.NumWorkers.
        MaxNumWorkers
    end
    
    properties ( Transient, PCTGetSet, GetAccess = public, SetAccess = private, PCTConstraint = 'string', Hidden )
        % Certificate for authorization with Cloud Center
        Certificate = '';
    end

    properties ( Constant, Access = private )
        %MinimalMementoProperties A string array that defines the
        % properties needed for a minimal cluster memento (i.e. one that
        % allows a cluster object to be constructed that can find the
        % original cluster's jobs). A property should only be included if
        % it is required in the memento for this class and every sub-class.
        %
        % A sub-class can define additional properties to be included in
        % the memento in a constant, private property of the same name. A
        % sub-class should not include properties that are included by any
        % of its super-classes.  A super-class should only define a
        % property if it should be included in the memento of each of its
        % sub-classes.
        %
        % The full list of properties required by any class can be gathered
        % using hDetermineMementoProperties.
        MinimalMementoProperties = [ ...
            "Identifier", ...
            "Certificate"]
        
        % A string array that stores all the properties needed for a
        % minimal cluster memento for this class. It should include all
        % those defined in MinimalMementoProperties in this class and in
        % all superclasses.
        AllPropertiesForMinimalMemento = parallel.Cluster.hDetermineMementoProperties( ...
            ?parallel.cluster.MJSComputeCloud, parallel.internal.types.ClusterMementoType.Minimal )
        
        % A string array that stores all the properties needed for a full
        % cluster memento for this class. It should include all those
        % defined in MinimalMementoProperties and FullMementoProperties in
        % this class and in all superclasses.
        AllPropertiesForFullMemento = parallel.Cluster.hDetermineMementoProperties( ...
            ?parallel.cluster.MJSComputeCloud, parallel.internal.types.ClusterMementoType.Full )
    end
    
    % Public, Getters and Setters
    % -------------------------------------------------------------------------
    methods
        function val = get.NumWorkers( obj )
            try
                val = obj.getClusterProps( 'NumWorkers' );
            catch E
                dctSchedulerMessage(1, 'Failed to get NumWorkers property. Setting value to zero. Original error: %s', E.getReport());
                val = 0;
            end
        end
        function set.NumWorkers( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'NumWorkers' ) );
        end
        function val = get.NumMachines( obj )
            val = obj.getClusterProps( 'NumMachines' );
        end
        function set.NumMachines( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'NumMachines' ) );
        end
        function val = get.NumWorkersRequested( obj )
            % Only need to refresh the state for resizable clusters since
            % NumWorkersRequested can't change for non-resizable ones
            if obj.isResizable()
                obj.refreshState();
            end
            
            val = obj.hGetTotalNumWorkers();
        end
        function set.NumWorkersRequested( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'NumWorkersRequested' ) );
        end
        function val = get.MaxNumWorkers( obj )
            if obj.isResizable()
                maxCapacity = obj.getClusterProps( 'MaxCapacity' );
                val = sum( maxCapacity );
            else
                val = obj.hGetTotalNumWorkers();
            end
        end
        function set.MaxNumWorkers( obj, ~ )
            throwAsCaller( obj.buildCannotModify( 'MaxNumWorkers' ) );
        end
    end
    
    % Public, Hidden discoveryClusters method
    % -------------------------------------------------------------------------
    methods ( Static, Hidden )
        
        clusterList = hDiscoverClusters()
        
        function [startText, startCommand] = hGetStartLink(identifier)
            if feature('hotlinks')
                startMessage = message('parallel:cloud:StartClusterLinkText');
                postStartMessage = message('parallel:cloud:PostStartText');
                startCommand = sprintf('parallel.cluster.MJSComputeCloud.hStartFromId(''%s''); disp(''%s'');', ...
                    identifier, postStartMessage.getString());
                startText = [ ...
                    sprintf('<a href="matlab: %s">', startCommand), ...
                    startMessage.getString() ...
                    '</a>'];
            else
                startMessage = message('parallel:cloud:StartClusterNoLinkText');
                % The command is specified in the text of StartClusterNoLinkText message in this case.
                startCommand = '';
                startText = startMessage.getString();
            end
        end      

        function hStartFromId( identifier )
            parallel.internal.cloud.startCloudCluster( identifier );
        end

    end
    
    % Private methods
    % -------------------------------------------------------------------------
    methods ( Access = private )
        
        function delete( obj )
            % Private for g787820
            delete@handle( obj );
        end
        
        function tf = isResizable( obj )
            % We do not have to refresh the state of the cluster before
            % retrieving whether it is resizable until Cloud Center allows
            % dynamically enabling/disabling resizing on a running cluster
            % obj.refreshState();
            tf = obj.hIsResizable();
        end
        
        function errorIfInsufficientWorkers( obj, numWorkers )
            maxNumWorkers = obj.MaxNumWorkers;
            if numWorkers > maxNumWorkers
                error(message('parallel:cluster:MJSComputeCloud:RequestedMoreWorkersThanAvailable', numWorkers, maxNumWorkers));
            end
        end
        
        function waitForWorkers( obj, numRequestedWorkers, timeout, stateCheckPeriod )
            workerWaiter = parallel.internal.cloud.WorkerWaiter( stateCheckPeriod, numRequestedWorkers, obj );
            dctSchedulerMessage( 4, 'Waiting for %d workers to register with the job manager', numRequestedWorkers );
            ok = workerWaiter.wait( timeout );
            if ~ok
                numWorkers = obj.NumWorkers;
                dctSchedulerMessage( 1, ...
                    ['Workers failed to register with the job manager within the timeout: %d. ', ...
                    '%d out of %d workers successfully registered.'], ...
                    timeout, numWorkers, numRequestedWorkers );
                % We do not error here, let's just continue with the workers
                % we've already got.
            end
        end
        
        function checkCompletionState( obj )
            import parallel.internal.types.MJSComputeCloudStates
            
            % Issue a warning if a job is submitted with less than the
            % requested number of workers started.
            obj.refreshState();
            if obj.ClusterState == MJSComputeCloudStates.HeadNodeReady
                % Get workers numbers explicitly to avoid race conditions
                % where all workers start just as we reach here.
                numWorkersStarted = obj.NumWorkers;
                numWorkersRequested = obj.NumWorkersRequested;
                if numWorkersStarted < numWorkersRequested
                    parallel.internal.warningNoBackTrace(message('parallel:cluster:MJSComputeCloud:HeadNodeReadySubmit', ...
                    numWorkersStarted, numWorkersRequested));
                end
            end
        end
    end
    
    % Public hidden methods
    % -------------------------------------------------------------------------
    methods ( Access = public, Hidden )

        function hJobPreSubmitCheck( obj, job )
            hJobPreSubmitCheck@parallel.Cluster( obj, job );
            
            obj.errorIfInsufficientWorkers( job.MinNumWorkers );
            if ~obj.isResizable()
                obj.waitForWorkers( job.MinNumWorkers, obj.WorkerWaitTimeout, obj.StateCheckPeriod );
                obj.checkCompletionState();
            end
        end

        function newjob = hBuildChild( obj, variant, jobId, jobSId )
            % Build a new job object of the requested variant. Called by the
            % support which caches the resulting new job.
            
            newjob = obj.buildChild( variant, jobId, jobSId );
            
        end
        
        % Add Cluster Properties to Display
        function [clusterPropertyMap, propNames] = hGetDisplayItems(obj, diFactory)
            
            % For MJS compute cloud clusters, we want to insure that we
            % carefully use hVectorizedGet when possible for remote calls,
            % as requesting multiple properties simultaneously is much
            % more efficient, so we do not call the base class map.
            % We build our own.
            % Create an empty map to hold displayable cluster properties
            clusterPropertyMap = containers.Map('KeyType', 'char', 'ValueType', 'any');
            
            % 'Separator' is not a typical name-value object. However, it
            % needs to be added to the propNames list in between property
            % groups, so it is put into the map with the other
            % displayable items.
            clusterPropertyMap('Separator') = diFactory.createSeparator();
            
            obj.refreshState();
            
            % If we are not attached to the clusters and cannot make remote
            % calls there is a limited list of interesting properties
            if ~obj.isConnected()
                % The order the properties appear in the propNames array is the
                % order in which they will be displayed. NOTE: There are
                % different display items for  attached and non-attached
                % clusters, so there are different propNames arrays.
                propNames = {...
                    'Name', ...
                    'State', ...
                    'Profile', ...
                    };
                clusterDetails = obj.getClusterDetails();
                clusterPropertyMap('Name') = diFactory.createDefaultItem( clusterDetails.ClusterName );
                clusterPropertyMap('State') = diFactory.createDefaultItem( obj.ClusterState.Name );
                clusterPropertyMap('Profile') = diFactory.createDefaultItem( obj.Profile );
            else
                % The order the properties appear in the propNames array is the
                % order in which they will be displayed. NOTE: There are
                % different display items for  attached and non-attached
                % clusters, so there are different propNames arrays.
                propNames = {...
                    'Name', ...
                    'State', ...
                    'Profile', ...
                    'Modified', ...
                    'SharedState', ...
                    'Host', ...
                    'ShutdownAt', ...
                    'Separator', ...
                    'MaxNumWorkers', ...
                    'NumWorkersRequested', ...
                    'Separator', ...
                    'NumWorkers', ...
                    'NumThreads', ...
                    'NumBusyWorkers', ...
                    'NumIdleWorkers', ...
                    'Separator', ...
                    'JobStorageLocation', ...
                    'ClusterMatlabRoot', ...
                    'OperatingSystem', ...
                    'RequiresOnlineLicensing', ...
                    'PreferredPoolNumWorkers'};
                
                % Add cluster specific items to map for display
                names = {...
                    'Name', ...
                    'State', ...
                    'Profile', ...
                    'Modified', ...
                    'SharedState', ...
                    'Host', ...
                    'ShutdownAt', ...
                    'MaxNumWorkers', ...
                    'NumWorkersRequested', ...
                    'NumWorkers', ...
                    'NumThreads', ...
                    'NumBusyWorkers', ...
                    'NumIdleWorkers'...
                    'JobStorageLocation', ...
                    'ClusterMatlabRoot', ...
                    'OperatingSystem', ...
                    'RequiresOnlineLicensing', ...
                    'LicenseNumber', ...
                    'PreferredPoolNumWorkers'};
                
                % We are calling doVectorisedGet() here rather than hVectorisedGet() as this allows
                % us to avoid the call to refreshState() which would make an unnecessary extra call
                % to the web services (g939466).
                values = diFactory.makeMultipleItems(@createDefaultItem, ...
                    obj.hGetDisplayPropertiesNoError(names, @doVectorisedGet));
                clusterPropertyMap = [clusterPropertyMap; containers.Map(names, values)];
            end
            
            % If there is a communication error with a stashed message, add
            % it to the display and wrap it so we don't lose any information
            % in the text of the message.
            stashedErrorMessage = obj.StashedErrorMessage;
            if obj.ClusterState >= parallel.internal.types.MJSComputeCloudStates.CommunicationError && ...
                    ~isempty(stashedErrorMessage)
                clusterPropertyMap('ErrorMessage') = diFactory.createWrappedTextItem(stashedErrorMessage);
                % NOTE: Also add the error message to propNames because
                % we don't want it there unless we need it.
                propNames{end+1} = 'ErrorMessage';
            end
        end
    end
    
    % Public, user-visible methods
    % -------------------------------------------------------------------------
    methods ( Access = public )
        
        function obj = MJSComputeCloud( varargin )
            %MJSComputeCloud Create a MJSComputeCloud cluster instance
            %   parallel.cluster.MJSComputeCloud(p1, v1, p2, v2, ...) builds an
            %   MJSComputeCloud cluster with the specified property values. The
            %   properties can include any of the properties of the
            %   MJSComputeCloud cluster class, or a profile.
            %
            %   Example:
            %   % Create an MJSComputeCloud cluster using the 'myCloud' profile.
            %   myCluster = parallel.cluster.MJSComputeCloud('Profile', 'myCloud');
            %
            %   See also PARCLUSTER, parallel.Cluster.
            
            import parallel.internal.settings.ValueType
            
            % Constructor called by hDiscoverClusters
            if nargin == 1 && isa( varargin{1}, 'parallel.internal.cloud.ClusterInfo' )
                clusterInfo = varargin{1};
                
                % Extract the certificate and identifier from the
                % ClusterInfo
                certificate = clusterInfo.Certificate;
                certificateType = ValueType.User;
                identifier  = clusterInfo.ClusterID;
                identifierType = ValueType.User;
                
                % Fake up the profile and constructor args
                propsFromArgs   = containers.Map();
                cProf = '';
                
                usePeerLookup = true;
                promptForPassword = parallel.internal.cluster.defaultMJSCredentialPromptsEnabled();
            else
                % Extract profile and cluster properties
                try
                    [propsFromArgs, cProf] = ...
                        parallel.internal.cluster.ConstructorArgsHelper.interpretClusterCtorArgs( ...
                        ?parallel.cluster.MJSComputeCloud, varargin{:} );
                catch E
                    throw( E );
                end
                
                [ok, identifier, identifierType] = iMustGetPropFromMapAndRemove(propsFromArgs, 'Identifier', '');
                if ~ok
                    error(message('parallel:cluster:MJSComputeCloud:MissingIdentifier'));
                end
                [~, certificate, certificateType] = iMustGetPropFromMapAndRemove(propsFromArgs, 'Certificate', '');
                                
                % Allow override of UsePeerLookup for testing with local
                % MJS
                usePeerLookup = iGetPropFromMapAndRemoveWithDefaultValue(propsFromArgs, 'UsePeerLookup', true);
                promptForPassword = iGetPropFromMapAndRemoveWithDefaultValue(propsFromArgs, 'PromptForPassword', ...
                    parallel.internal.cluster.defaultMJSCredentialPromptsEnabled());
            end

            % Obtain the cluster details from the server.
            clusterDetailsFunc = @( x, ~ ) parallel.internal.cloud.getCloudClusterDetails( x );
            clusterDetails = iAuthorizeAndRunGetClusterDetails(identifier, certificate, clusterDetailsFunc);
            
            % Check 1: Confirm the cluster defined in Cloud Center is the
            % same version as the client.
            if ~parallel.internal.cluster.MJSComputeCloudFinder.isCompatibleCluster(clusterDetails)
                cloudVersion = clusterDetails.MatlabReleaseVersion;
                matlabRelease = parallel.internal.cluster.MJSComputeCloudFinder.CurrentRelease;
                error(message('parallel:cloud:CloudVersionNotCompatible', ...
                      cloudVersion, matlabRelease));
            end

            % Check 2: Confirm we have a cluster certificate.
            if strcmp(certificate, "")
               error(message('parallel:cluster:MJSComputeCloud:MissingClusterCertificate'));
            end

            % Construct the cluster object
            obj@parallel.cluster.MJSCloudCluster( 'MJSComputeCloud', cProf );

            sharedState = parallel.internal.types.ClusterSharedStates.fromName(clusterDetails.ClusterSharedState);
            obj.IsSharedCluster = sharedState ~= parallel.internal.types.ClusterSharedStates.Private;

            % For personal clusters, default to using all workers in
            % parpools
            if ~obj.IsSharedCluster
                obj.PropStorage.setValue('PreferredPoolNumWorkers', ...
                    parallel.internal.settings.ValueType.Factory, Inf);
            end
                        
            factoryLicenseNumber = '';
            % Put all PCTGetSet properties into PropStorage.            
            obj.PropStorage.addFactoryValue( 'Identifier',  identifier );
            obj.PropStorage.setValueType(    'Identifier',  identifierType );
            obj.PropStorage.addFactoryValue( 'Certificate', char(certificate) );
            obj.PropStorage.setValueType(    'Certificate', certificateType );
            obj.PropStorage.addFactoryValue( 'LicenseNumber', factoryLicenseNumber );

            % Use spf support if specified by experimental switch
            useSPF = ~feature("pctUseJavaTransport");
            if useSPF
                % Always uses TLS lookup
                obj.UsePeerLookup = true;

                % We must update the cluster details before creating the access
                % because the host of the cluster might have changed if we have
                % shutdown and restarted the cluster (see g1074206).
                getClusterDetailsFunc = @() clusterDetailsFunc(identifier, []);
                supportBuildFcn = @() parallel.internal.cloud.MJSComputeCloudSupportWrapperProvider.createSupport(...
                    getClusterDetailsFunc, promptForPassword, obj.Username, certificate);
            else
                % The certificate needs to be installed before the lookup is done.
                certRemovalCleanup = parallel.internal.cluster.installJmCertificate(certificate);

                obj.UsePeerLookup = usePeerLookup;

                cache = parallel.internal.cluster.TwoLevelCache();
                accessFactory = parallel.internal.cluster.MJSComputeCloudClientJmAccessFactory( ...
                    cache, obj.Username, promptForPassword, usePeerLookup, identifier, clusterDetailsFunc);
                supportBuildFcn = @() parallel.internal.cluster.MJSSupport( cache, accessFactory, certRemovalCleanup );
            end

            try
                obj.CloudSupport = parallel.internal.cloud.CloudSupport( clusterDetails, ...
                    clusterDetailsFunc, identifier, supportBuildFcn );
                obj.ClusterState = obj.CloudSupport.refreshState();
            catch E
                dctSchedulerMessage( 1, ...
                    'Failed to create cloud support object. Original error: %s', E.getReport() );
                throw( E );
            end

            % Ensure any remaining constructor args go through the correct set methods.
            obj.applyConstructorArgs( propsFromArgs );
        end        
    end

    methods ( Access = protected )
        function startCloudCluster( obj )
            parallel.internal.cloud.startCloudCluster( obj.Identifier );
        end
        
        % Get all the properties needed for the ClusterMemento
        function props = getAllMementoProperties( obj, clusterMementoType )
            import parallel.internal.types.ClusterMementoType

            switch clusterMementoType
                case ClusterMementoType.Minimal
                    props = obj.AllPropertiesForMinimalMemento;
                case ClusterMementoType.Full
                    props = obj.AllPropertiesForFullMemento;
                otherwise
                    % Default to the full memento
                    props = obj.AllPropertiesForFullMemento;
            end
        end
    end

    methods
        function val = get.MatlabVersion( obj )
            clusterDetails = obj.getClusterDetails();
            val = clusterDetails.MatlabReleaseVersion;
        end
        
        function val = get.SharedState( obj )
            clusterDetails = obj.getClusterDetails();
            enumString = parallel.internal.types.ClusterSharedStates.fromName(clusterDetails.ClusterSharedState);
            messageCatKey = enumString.MessageCatalogueUIString;
            val = getString(message(messageCatKey));
        end
    end

    % Hidden methods
    % -------------------------------------------------------------------------
    methods ( Hidden )
        function hErrorIfNumPoolWorkersInvalid( obj, numWorkers )
            clusterNumWorkers = obj.MaxNumWorkers;
            obj.CloudSupport.hErrorIfNumPoolWorkersInvalid(numWorkers, clusterNumWorkers);
        end
        
        function maxNumWorkers = hGetMaxNumWorkers( obj )
            maxNumWorkers = obj.MaxNumWorkers;
        end
        
        function maxNumPoolWorkers = hGetMaxNumPoolWorkers( obj )
            % Use the maximum of the MaxCapacity array (not the sum) since
            % pool workers must have the same operating system
            maxCapacity = obj.getClusterProps( 'MaxCapacity' );
            maxNumPoolWorkers = max( maxCapacity );
        end
    end
end

% iMustGetPropFromMapAndRemove - return the value and type from the map if possible,
% setting ok to indicate a successful lookup
function [ok, val, type] = iMustGetPropFromMapAndRemove( map, prop, default )
    if ~map.isKey(prop)
        ok = false;
        val = default;
        type = parallel.internal.settings.ValueType.Factory;
    else
        [val, type] = iGetPropValAndRemove( map, prop );
        ok     = true;
    end
end

% iGetPropFromMapAndRemoveWithDefaultValue - return the value if it's in the map,
% otherwise return the supplied default value
% Also remove the prop from the map if it exists.
function val = iGetPropFromMapAndRemoveWithDefaultValue( map, prop, default )
    if map.isKey(prop)
        val = iGetPropValAndRemove(map,prop);
    else
        val = default;
    end
end

% -------------------------------------------------------------------------
% iGetPropValAndRemove - simply return the value from the propsFromArgs map
% and remove it from the map it exists.
function [val, type] = iGetPropValAndRemove( map, prop )
    if map.isKey( prop )
        valtmp = map(prop);
        val    = valtmp{1};
        type   = valtmp{2};
        map.remove(prop);
    else
        val    = '';
    end
end

% iAuthorizeAndRunGetClusterDetails - obtain the cluster details from the
% server, ensuring first to execute an authorization handshake.
function clusterDetails = iAuthorizeAndRunGetClusterDetails(clusterID, certificate, clusterDetailsFunc)
    % Authorization is performed by obtaining a "nonce" string from Cloud
    % Center, encrypting this with the certificate we hold, before
    % sending this string back to the server.

    if strcmp(certificate, "")
        % This is potentially a 19b cluster profile since we have no
        % certificate. Announce this to Cloud Center by setting the
        % encrypted nonce to an empty string.

        % First, make a call to getNonce(). This is needed in CloudCenter
        % prior to an authorize() call.
        parallel.internal.cloud.getNonce(clusterID);
        parallel.internal.cloud.authorizeWithEncryptedNonce(clusterID, "");
    else
        % We have a certificate, so encrypt the nonce string.
        parallel.internal.cloud.authorizeWithCertificate(clusterID, certificate);
    end

    % Call getClusterDetails.
    oldClusterDetails = [];
    clusterDetails = clusterDetailsFunc(clusterID, oldClusterDetails);
end
