package be.iminds.ilabt.jfed.ui.cli;

import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetails;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetailsFactory;
import be.iminds.ilabt.jfed.call_log_output.SerializableApiCallDetailsWriter;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.Server;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.AggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.StatusDetails;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.UserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AMv2Wrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AMv3Wrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AutomaticAggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AutomaticUserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;

import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.model.impl.BasicExecuteAnsiblePlaybook;
import be.iminds.ilabt.jfed.rspec.parser.RspecParseException;
import be.iminds.ilabt.jfed.rspec.parser.RspecXmlConstants;
import be.iminds.ilabt.jfed.rspec.rspec_source.ManifestRspecMerger;
import be.iminds.ilabt.jfed.rspec.rspec_source.ManifestRspecSource;
import be.iminds.ilabt.jfed.rspec.rspec_source.RequestRspecSource;
import be.iminds.ilabt.jfed.ui.commandline.BaseCli;
import be.iminds.ilabt.jfed.util.*;
import com.google.inject.Injector;
import javanet.staxutils.IndentingXMLEventWriter;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.FileConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.LoggerFactory;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.PublicKey;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import static be.iminds.ilabt.jfed.ui.cli.ExperimenterCommandLineParser.Command.*;

/**
 * ExperimenterCli
 */
@SuppressWarnings("AccessStaticViaInstance")
public class ExperimenterCli {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(ExperimenterCli.class);

    private static final String KEY_EXPIRATION_TIME = "expiration-time";
    private static final String KEY_REQUEST_RSPEC = "request-rspec";

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        int retval = main_helper(args, System.out, System.err, System.in, new BaseCli(System.err), true);
        System.exit(retval);
    }

    public static int main_helper(String[] args,
                                  final PrintStream out, final PrintStream err, final InputStream in,
                                  BaseCli baseCli,
                                  boolean temporarilyRedirectSystemStreams) throws IOException, ClassNotFoundException {
        final PrintStream orig_out = System.out;
        final PrintStream orig_err = System.err;
        final InputStream orig_in = System.in;
        if (temporarilyRedirectSystemStreams) {
            System.setOut(out);
            System.setErr(err);
            System.setIn(in);
        }

        int retval = -1;
        try {
            retval = main_internal(args, out, err, in, baseCli);
        } finally {
            if (temporarilyRedirectSystemStreams) {
                System.setOut(orig_out);
                System.setErr(orig_err);
                System.setIn(orig_in);
            }
        }
        return retval;
    }

    public static int main_internal(String[] args,
                                    final PrintStream out, final PrintStream err, final InputStream in,
                                    BaseCli baseCli) throws IOException, ClassNotFoundException {
        final ExperimenterCommandLineParser cli;
        try {
            cli = new ExperimenterCommandLineParser(args, out, err, in, baseCli);

            baseCli.initAuthoritiesModule(cli.line);
            try {
                baseCli.initUserModule(cli.line, out, err, in);
            } catch (BaseCli.CliArgumentException e) {
                LOG.debug("Got CliArgumentException", e);
                cli.commandHelp();
                return 3;
            }
            final Injector injector = baseCli.getInjector(err);
            final TestbedInfoSource testbedInfoSource = injector.getInstance(TestbedInfoSource.class);
            final AuthorityFinder authorityFinder = injector.getInstance(AuthorityFinder.class);
            final GeniUser user = injector.getInstance(GeniUser.class);
            final JFedPreferences jFedPreferences = injector.getInstance(JFedPreferences.class);
            final Logger logger = injector.getInstance(Logger.class);

            GeniUrn bindRspecUrn = verifyBindRspecUrn(cli, out, err, authorityFinder);

            final CallLogOutput callLogOutput = initCallLogOutput(cli, out, err, logger);

            boolean successExitStatus = true;
            try { //used for finally callLogOutput.close()

                Triplet<StringRspec, ModelRspec, List<String>> rspec3 = initRspec(cli, out, err, bindRspecUrn);
                StringRspec rspec = rspec3.first;
                ModelRspec modelRspec = rspec3.second;
                List<String> rspecComponentManagers = rspec3.third;

                if (cli.stitchingArg && rspec != null) {
                    if (!rspec.isStitching(testbedInfoSource, authorityFinder)) {
                        err.println("The --stitching option was set, but the rspec does not specify any stitched links: Aborted CreateSliver.");
                        return 3;
                    }
                }

                if (!BaseCli.checkUser(user, cli.line, out, err, in))
                    return 3;

                final Server userAuth = user.getUserAuthorityServer();
                final GeniUserProvider userProvider = injector.getInstance(GeniUserProvider.class);

                if (!cli.silent) {
                }
                if (cli.debug) {
                    out.println("User " + user.getUserUrnString() + " will be used.");
                    out.println("User Authority: " + user.getUserAuthorityServer().getName());
                    out.println("User Authority URN: " + user.getUserAuthorityServer().getDefaultComponentManagerUrn());
                }

                Map sliceInfoHT = new HashMap();

                boolean usesSpeaksFor = cli.speaksForArgs != null && cli.speaksForArgs.length > 0;
                sliceInfoHT.put("usedSpeaksfor", usesSpeaksFor);

                GeniUrn speakingForUser = null;
                List<AnyCredential> speaksForCredentials = new ArrayList<>();
                if (usesSpeaksFor) {
                    LOG.debug("There are "+cli.speaksForArgs.length+" speaksfor args: "+Arrays.asList(cli.speaksForArgs));
                    for (String speaksForArg : cli.speaksForArgs) {
                        List<String> speaksForCredentialStrings;
                        try {
                            String speaksForMultiCredentialString = IOUtils.fileToString(speaksForArg);

                            speaksForCredentialStrings = extractCredentials(speaksForMultiCredentialString);
                        } catch (IOException e) {
                            LOG.error("Error reading speaksFor credential from file \"" + speaksForArg + "\"", e);
                            err.println("Error reading speaksFor credential from file \"" + speaksForArg + "\": " + e.getMessage());
                            return 3;
                        }
                        LOG.debug("File \""+speaksForArg+"\" contains "+speaksForCredentialStrings.size()+" credentials.");
                        for (String speaksForCredentialString : speaksForCredentialStrings) {
                            AnyCredential speaksForCredential;
                            GeniUrn currentSpeakingForUser = null;
                            try {
                                speaksForCredential = AnyCredential.createAutoDetect("speaksFor", speaksForCredentialString);
                            } catch (CredentialException e) {
                                LOG.error("Error parsing speaksFor credential from file \"" + speaksForArg + "\"", e);
                                err.println("Error parsing speaksFor credential from file \"" + speaksForArg + "\": " + e.getMessage());
                                return 3;
                            }
                            if (speaksForCredential == null) {
                                LOG.error("Error parsing speaksFor credential from file \"" + speaksForArg + "\"");
                                err.println("Error parsing speaksFor credential from file \"" + speaksForArg + "\"");
                                return 3;
                            }
                            if (!speaksForCredential.isSpeaksFor()) {
                                LOG.warn("WARNING: parsed speaksFor credential from file \"" + speaksForArg + "\" is NOT a speaksFor credential");
                                err.println("WARNING parsed speaksFor credential from file \"" + speaksForArg + "\" is NOT a speaksFor credential. (Will continue anyway)");
                            }
                            try {
                                boolean supportedCredType = false;
                                if (speaksForCredential instanceof AbacCredential) {
                                    supportedCredType = true;
                                    AbacCredential c = (AbacCredential) speaksForCredential;
                                    if (!c.isSpeaksFor()) {
                                        LOG.error("Provided ABAC credential is not a speaksfor credential");
                                        err.println("Provided ABAC credential is not a speaksfor credential");
                                        return 3;
                                    }
                                    currentSpeakingForUser = c.getSpokenForUrn();
                                    if (c.getSpokenForUrn() != null) {
                                        if (speakingForUser != null) {
                                            LOG.debug("Speaksfor chain search: speakingForUser="+speakingForUser);
                                            LOG.debug("Speaksfor chain search: checking "+speaksForCredentials.size()+" credentials");
                                            //there are multiple abac credentials, so it's harder to find who is spoken for
                                            //assume all credentials are ABAC, and start complex processing of chain
                                            Map<String, GeniUrn> spokenForByKeyId = new HashMap<>();
                                            List<String> speakersKeyIds = new ArrayList<>();

                                            List<AnyCredential> allCurrentlyKnownSpeaksForCredentials = new ArrayList<>(speaksForCredentials);
                                            allCurrentlyKnownSpeaksForCredentials.add(speaksForCredential);
                                            for (AnyCredential anyc : allCurrentlyKnownSpeaksForCredentials) {
                                                AbacCredential abacc = (AbacCredential) anyc;
                                                assert !spokenForByKeyId.containsKey(abacc.getHeadKeyId());
                                                assert !spokenForByKeyId.containsValue(abacc.getSpokenForUrn());
                                                assert !speakersKeyIds.contains(abacc.getTailKeyId());
                                                spokenForByKeyId.put(abacc.getHeadKeyId(), abacc.getSpokenForUrn());
                                                speakersKeyIds.add(abacc.getTailKeyId());
                                            }
                                            LOG.debug("Speaksfor chain search: spokenForByKeyId="+spokenForByKeyId);
                                            LOG.debug("Speaksfor chain search: speakersKeyIds="+speakersKeyIds);
                                            for (String speakersKeyId : speakersKeyIds)
                                                spokenForByKeyId.remove(speakersKeyId);
                                            LOG.debug("Speaksfor chain search: spokenForByKeyId="+spokenForByKeyId);
                                            if (spokenForByKeyId.size() == 1) {
                                                speakingForUser = spokenForByKeyId.values().iterator().next();
                                            } else {
                                                if (spokenForByKeyId.size() == 0) {
                                                    LOG.warn("Speaksfor chain search got 0 leftover speaking_for urns");
                                                    speakingForUser = null;
                                                } else {
                                                    //ignore that there are multiple
                                                    LOG.warn("Speaksfor chain search got "+spokenForByKeyId.size()+" leftover speaking_for urns");
                                                    speakingForUser = spokenForByKeyId.values().iterator().next();
                                                }
                                            }
                                            LOG.debug("Speaksfor chain search: speakingForUser="+speakingForUser);
                                        } else {
                                            speakingForUser = c.getSpokenForUrn();
                                        }
                                    }
                                }
                                if (speaksForCredential instanceof SfaCredential) {
                                    if (speaksForArg.length() > 1) {
                                        LOG.error("Multiple speaksfor credentials are not supported for type SfaCredential \"" + speaksForArg + "\"");
                                        err.println("Multiple speaksfor credentials are not supported for type SfaCredential \"" + speaksForArg + "\"");
                                        return 3;
                                    }
                                    supportedCredType = true;
                                    SfaCredential c = (SfaCredential) speaksForCredential;
                                    if (c.getTargetUrn() != null)
                                        currentSpeakingForUser = new GeniUrn(c.getTargetUrn());
                                    else {
                                        if (c.getTargetGid() != null) {
                                            Gid gid = new Gid(c.getTargetGid());
                                            if (gid.getSubjectUrn() != null) {
                                                currentSpeakingForUser = new GeniUrn(gid.getSubjectUrn());
                                            }
                                        }
                                    }
                                    speakingForUser = currentSpeakingForUser;
                                }
                                if (!supportedCredType) {
                                    LOG.error("Unsupported speaksfor credential type for \"" + speaksForArg + "\": "+speaksForCredential.getType());
                                    err.println("Unsupported speaksfor credential type for \"" + speaksForArg + "\": "+speaksForCredential.getType());
                                    return 3;
                                }
                            } catch (GeniUrn.GeniUrnParseException e) {
                                LOG.error("Error parsing spoken for user URN from speaksFor credential from file \"" + speaksForArg + "\"", e);
                                err.println("Error parsing spoken for user URN from speaksFor credential from file \"" + speaksForArg + "\": " + e.getMessage());
                                return 3;
                            }
//                if (!speaksForCredential.validate()) {
//                    LOG.warn("WARNING: Provided speaksfor credential does not validate");
//                    err.println("WARNING: Provided speaksfor credential does not validate");
//                }

                            boolean check;
                            try {
                                check = speaksForCredential.check(testbedInfoSource);
                            } catch (CredentialException e) {
                                LOG.warn("Error trying to check credential signature: ", e);
                                check = false;
                            }
                            if (!check) {
                                LOG.warn("WARNING: Provided speaksfor credential signature check failed. (frequently caused by: you modified the credential)");
                                err.println("WARNING: Provided speaksfor credential signature check failed. (frequently caused by: you modified the credential)");
                            }

                            if (currentSpeakingForUser == null) {
                                LOG.warn("ERROR: could not determine the spoken for user in the speaksFor credential from file \"" + speaksForArg + "\". Cannot continue.");
                                err.println("ERROR: could not determine the spoken for user in the speaksFor credential from file \"" + speaksForArg + "\". Cannot continue.");
                                return 3;
                            }
                            if (speakingForUser == null) {
                                LOG.warn("ERROR: could not determine the final spoken for user. Cannot continue.");
                                err.println("ERROR: could not determine the final spoken for user. Cannot continue.");
                                return 3;
                            }
                            if (!cli.silent)
                                out.println("Using speaksFor credential for user \"" + currentSpeakingForUser + "\"");
                            speaksForCredentials.add(speaksForCredential);
                        }
                    }

                    if (!cli.silent && cli.speaksForArgs.length > 1)
                        out.println("You are using multiple speaksfor credentials. The final user being spoken for is \"" + speakingForUser + "\"");
                }

                JFedConnectionProvider conPool = injector.getInstance(JFedConnectionProvider.class);
                AutomaticUserAndSliceApiWrapper.AutomaticUserAndSliceApiWrapperFactory
                        automaticUserAndSliceApiWrapperFactory =
                        injector.getInstance(AutomaticUserAndSliceApiWrapper.AutomaticUserAndSliceApiWrapperFactory.class);
                AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory
                        automaticAggregateManagerWrapperFactory =
                        injector.getInstance(AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory.class);

                final UserAndSliceApiWrapper sa = automaticUserAndSliceApiWrapperFactory.create();
                GeniUrn targetUserUrn = user.getUserUrn();
                sliceInfoHT.put("userUrn", targetUserUrn);

                if (usesSpeaksFor) {
                    sa.setSpeaksFor(speaksForCredentials, speakingForUser);
                    targetUserUrn = speakingForUser;
                    if (speaksForCredentials.size() == 1)
                        sliceInfoHT.put("speaksForCredential", speaksForCredentials.get(0));
                    else {
                        int i = 1;
                        for (AnyCredential speaksForCredential : speaksForCredentials)
                            sliceInfoHT.put("speaksForCredential" + (i++), speaksForCredential);
                    }
                    sliceInfoHT.put("speakingForUserUrn", speakingForUser);
                }
                String targetUserUrnString = targetUserUrn.toString();

                Set<GeniUrn> shareUserUrns;
                boolean shareSliceWithProjectUsers = false;
                if (cli.shareSliceUsersArg != null) {
                    String tla = targetUserUrn.getEncodedTopLevelAuthority();
                    shareUserUrns = new HashSet<>();
                    String[] users = cli.shareSliceUsersArg.split(",");
                    for (String opt : users) {
                        boolean validOpt = false;

                        if (Objects.equals(opt, "PROJECTUSERS")) {
                            if (!cli.silent)
                                out.println("Will share slice with all users in the slice project.");
                            shareSliceWithProjectUsers = true;
                            validOpt = true;
                        }
                        if (!validOpt && GeniUrn.valid(opt)) {
                            GeniUrn userUrn = GeniUrn.parse(opt);
                            assert opt != null;
                            shareUserUrns.add(userUrn);
                            validOpt = true;
                        }
                        if (!validOpt && opt.matches("[a-zA-Z][a-zA-Z0-9]*") && opt.length() <= 8) {
                            GeniUrn userUrn = GeniUrn.createGeniUrnFromEncodedParts(tla, "user", opt);
                            if (!cli.silent)
                                out.println("Assuming specified user \""+opt+"\" is: "+userUrn);
                            shareUserUrns.add(userUrn);
                            validOpt = true;
                        }

                        if (!validOpt) {
                            err.println("The share-slice option you have specified (" + opt + ") is not valid a valid URN or username");
                            cli.commandHelp();
                            return 3;
                        }
                    }
                } else {
                    shareUserUrns = Collections.emptySet();
                }
                assert shareUserUrns != null;

                List<AnyCredential> userCreds;
                try {
                    userCreds = sa.getLocalUserCredentials(logger);
                    sliceInfoHT.put("userCredentials", userCreds);
                } catch (JFedException e) {
                    return callFailed("Error retrieving user credential", e, err, callLogOutput);
                }
                if (cli.debug)
                    out.println(userCreds.size() + " user credentials retrieved.");
                List<GeniUrn> existingSlices;
                List<String> existingSlicesCompareStrings;
                try {
                    existingSlices = sa.getSlicesForUser(logger, targetUserUrn);
                } catch (JFedException e) {
                    return callFailed("Error retrieving slices for user", e, err, callLogOutput);
                }
                if (cli.debug)
                    out.println("User " + targetUserUrnString + " has the following slices: " + existingSlices);
                if (existingSlices != null) {
                    existingSlicesCompareStrings = new ArrayList<>();
                    for (GeniUrn sliceUrn : existingSlices) {
                        GeniUrn lowercaseSliceUrn = GeniUrn.createGeniUrnFromEncodedParts(
                                sliceUrn.getEncodedTopLevelAuthority().toLowerCase(),
                                sliceUrn.getEncodedResourceType(),
                                sliceUrn.getEncodedResourceName().toLowerCase()
                        );
                        existingSlicesCompareStrings.add(lowercaseSliceUrn.toString());
                    }
                } else
                    existingSlicesCompareStrings = null;

                if (cli.isCommand(LIST_USER_INFO)) {
                    List<String> projectNames;
                    try {
                        projectNames = sa.getSubAuthorityNames(logger, targetUserUrn);
                    } catch (JFedException e) {
                        return callFailed("Error retrieving projects for user", e, err, callLogOutput);
                    }
                    if (cli.isJson) {
                        Map<String,Object> userHt = new HashMap<>();
                        userHt.put("urn", targetUserUrnString);
                        userHt.put("slices", existingSlices);
                        userHt.put("projects", projectNames);
                        out.println(JSonHelper.jsonObjectToString(userHt));
                    }
                    if (cli.isTextOutput) {
                        out.println("User Info: ");
                        out.println("   Urn: "+targetUserUrnString);
                        out.println("   Projects: "+ projectNames.stream().collect(Collectors.joining(",")));
                        out.println("   Slices: "+ existingSlices.stream().map(GeniUrn::toString).collect(Collectors.joining(",")));
                    }

                    return 0;
                }

                List<String> projectNames = null;
                if (Objects.equals(cli.projectNameArg, cli.CHOOSE_PROJECT_AUTOMATICALLY_OPTION)) {
                    try {
                        projectNames = sa.getSubAuthorityNames(logger, targetUserUrn);

                        if (cli.debug)
                            out.println("You are a member of the following projects: " + projectNames);

                        if (projectNames.isEmpty()) {
                            err.println("WARNING: " + cli.CHOOSE_PROJECT_AUTOMATICALLY_OPTION + " was set as project, but you do not seem to be a member of any project! Will use fallback: continue without project.");

                            //fallback: no project
                            cli.projectNameArg = null;
                        } else {
                            //select last project
                            cli.projectNameArg = projectNames.get(projectNames.size() - 1);
                            if (cli.debug || !cli.silent)
                                out.println("Automatically selected project \"" + cli.projectNameArg + "\"");
                        }
                    } catch (JFedException e) {
                        //fallback: no project
                        cli.projectNameArg = null;

                        String message =
                                "WARNING: " + cli.CHOOSE_PROJECT_AUTOMATICALLY_OPTION + " was set as project, but there was an error retrieving list of user projects. Will use fallback: continue without project.";
                        if (e.getXmlRpcResult() != null)
                            err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                        else {
                            err.println(message + ":");
                            e.printStackTrace(err);
                        }
                    }
                }

                GeniUrn initialSliceUrn = cli.sliceIdArg.startsWith("urn:") ?
                        GeniUrn.parse(cli.sliceIdArg) :
                        GeniUrn.createGeniUrnFromEncodedParts(
                                userAuth.getUrnTld() + (cli.projectNameArg == null ? "" : ":" + cli.projectNameArg),
                                "slice",
                                cli.sliceIdArg
                        );

                GeniUrn lowercaseInitialSliceUrn = GeniUrn.createGeniUrnFromEncodedParts(
                        initialSliceUrn.getEncodedTopLevelAuthority().toLowerCase(),
                        initialSliceUrn.getEncodedResourceType(),
                        initialSliceUrn.getEncodedResourceName().toLowerCase()
                );
//            final boolean sliceExists = existingSlices.contains(initialSliceUrn);
                final boolean sliceExists = existingSlicesCompareStrings.contains(lowercaseInitialSliceUrn.toString());

                if (!cli.silent && !(cli.isCommand(SHOW_SLICE_INFO) && sliceExists)&& !(cli.isCommand(CREATE_SLICE_ONLY) && !sliceExists))
                    out.println("slice " + initialSliceUrn + " " + (sliceExists ? "already exists" : "does not yet exist"));

                //check if project exists
                if ((cli.isCommand(CREATE, CREATE_SLICE_ONLY)) && !sliceExists && initialSliceUrn.getEncodedSubAuthName() != null) {
                    try {
                        if (projectNames == null)
                            projectNames = sa.getSubAuthorityNames(logger, targetUserUrn);
                        assert projectNames != null;

                        List<String> projectNamesLowerCase = new ArrayList<>(projectNames.size()+1);
                        for (String projectName : projectNames)
                            projectNamesLowerCase.add(projectName.toLowerCase());

                        if (!projectNamesLowerCase.contains(initialSliceUrn.getEncodedSubAuthName().toLowerCase()))
                            err.println("WARNING: You do not seem to be a member if the project (\"" + initialSliceUrn.getEncodedSubAuthName() + "\") " +
                                    "in which you wish to create a slice. Projects you are member of: " + projectNames);
                        else {
                            if (!projectNames.contains(initialSliceUrn.getEncodedSubAuthName())) {
                                String userProvidedProjectName = initialSliceUrn.getEncodedSubAuthName();
                                for (String correctCaseProjectName : projectNames) {
                                    if (userProvidedProjectName.equalsIgnoreCase(correctCaseProjectName) && !Objects.equals(userProvidedProjectName, correctCaseProjectName)) {
                                        GeniUrn orig = initialSliceUrn;
                                        initialSliceUrn = GeniUrn.createGeniUrnFromEncodedParts(
                                                initialSliceUrn.getEncodedTopLevelAuthority_withoutSubAuth()+":"+correctCaseProjectName,
                                                initialSliceUrn.getEncodedResourceType(),
                                                initialSliceUrn.getEncodedResourceName());
                                        LOG.info("(CaseBugs): You are a member of \"" + userProvidedProjectName + "\", " +
                                                "but that project is known with a different case: \"" + correctCaseProjectName + "\"." +
                                                "jFed will change the case to avoid any SA/AM bugs. (though it shouldn't matter.). " +
                                                "Changed slice URN from " + orig + " to " +initialSliceUrn);
                                    }
                                }
                            }

                        }

                        if (cli.debug)
                            out.println("You are a member of the following projects: " + projectNames);
                    } catch (JFedException e) {
                        String message = "Error retrieving list of user projects. (this error will be ignored)";
                        if (e.getXmlRpcResult() != null)
                            err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                        else {
                            err.println(message + ":");
                            e.printStackTrace(err);
                        }
                    }
                }

                final GeniUrn sliceUrn = initialSliceUrn;

                if (!(cli.isCommand(CREATE, CREATE_SLICE_ONLY)) && !sliceExists) {
                    err.println("Error: The slice " + sliceUrn + " does not exist. Cannot continue.");
                    return 2;
                }

                if (cli.fake) {
                    out.println("Argument processing done. --cli.fake is set, so no further steps are taken.");
                    return 0;
                }

                List<AnyCredential> sliceCredentials;
                Date sliceExpirationDate = null;
                if (sliceExists) {
                    sliceInfoHT.put("sliceUrn", sliceUrn);
                    try {
                        sliceCredentials = sa.getSliceCredentials(logger, sliceUrn);
                        if (!sliceCredentials.isEmpty()) sliceExpirationDate = sliceCredentials.get(0).getExpiresDate();
                        LOG.debug("ExperimenterCli requested slice credentials, and got "+sliceCredentials.size()+" credentials");
                    } catch (JFedException e) {
                        return callFailed("Error retrieving slice credential", e, err, callLogOutput);
                    }

                    if (cli.rspecArg == null && !(cli.isCommand(SHOW_SLICE_INFO, CREATE_SLICE_ONLY))) {
                        //no RSpec provided, so get rspecComponentManagers from SLIVER_INFO (or equivalent)
                        try {
                            rspecComponentManagers = sa.getAggregatesForSlice(logger, sliceCredentials, sliceUrn).stream()
                                    .map(GeniUrn::getValue).collect(Collectors.toList());
                        } catch (JFedException e) {
                            return callFailed("Error retrieving slice credential", e, err, callLogOutput);
                        }
                        if (rspecComponentManagers.size() == 0) {
                            err.println("WARNING: No RSpec was provided using the '--rspec' option, and the SA did not know of any AM's that have resources. " +
                                    "Few commands can now do anything. Will try anyway.");
                        }
                    }

                    if (cli.isCommand(RENEW, CREATE)) {
                        Date currentSliceExpireDate = null;
                        for (AnyCredential credential : sliceCredentials)
                            if (credential.getExpiresDate() != null)
                                currentSliceExpireDate = credential.getExpiresDate();
                        if (currentSliceExpireDate != null && isBefore(currentSliceExpireDate, cli.requestedExpirationDate)) {
                            try {
                                if (cli.debug)
                                    out.println("Need to Renew slice, because the slice expires at " + RFC3339Util.dateToRFC3339String(currentSliceExpireDate, true, true, true) +
                                            " while requested expiration date is " + RFC3339Util.dateToRFC3339String(cli.requestedExpirationDate, true, true, true));
                                if (!cli.silent)
                                    out.println("Renewing slice, requested new expire date is "+RFC3339Util.dateToRFC3339String(cli.requestedExpirationDate, true, true, true)+" ...");
                                List<AnyCredential> newSliceCredentials =
                                        sa.renewSlice(logger, sliceCredentials, cli.requestedExpirationDate);
                                sliceExpirationDate = cli.requestedExpirationDate; //fallback
                                if (newSliceCredentials != null) {
                                    sliceCredentials = newSliceCredentials;
                                    if (cli.debug)
                                        out.println("ExperimenterCli renewed the slice and got "+sliceCredentials.size()+" credentials");
                                    if (!sliceCredentials.isEmpty()) {
                                        sliceExpirationDate = sliceCredentials.get(0).getExpiresDate();
                                        if (sliceExpirationDate != null && isBefore(sliceExpirationDate, cli.requestedExpirationDate)) {
                                            if (!cli.silent) {
                                                err.println("WARNING: The slice was renewed, but the requested expiration date was not set. " +
                                                        "Actual new expire date: "+RFC3339Util.dateToRFC3339String(sliceExpirationDate, true, true, true));
                                            }
                                        }
                                    }
                                }
                                else
                                    out.println("WARNING: did not receive renewed credential");
                            } catch (JFedException e) {
                                return callFailed("Error renewing slice credential", e, err, callLogOutput);
                            }
                        } else {
                            if (currentSliceExpireDate != null) {
                                if (cli.isCommand(RENEW))
                                    if (cli.debug)
                                        out.println("No need to renew slice. It already expires at "+RFC3339Util.dateToRFC3339String(currentSliceExpireDate, true, true, true));
                            } else {
                                if (!cli.silent) {
                                    if (cli.isCommand(RENEW))
                                        err.println("WARNING: expire date of slice is not known. Will not attempt renew!");
                                    else
                                        err.println("WARNING: actual expire date of created slice is not known.");
                                }
                            }
                        }
                    }

                    sliceInfoHT.put("sliceCredentials", sliceCredentials);

                    if (cli.isCommand(SHOW_SLICE_INFO, CREATE_SLICE_ONLY)) {
                        try {
                            List<GeniUrn> sliceUsers = sa.getUsersForSlice(logger, sliceUrn, sliceCredentials);
                            sliceInfoHT.put("sliceUsers", sliceUsers);
                        } catch (JFedException e) {
                            LOG.error("Error getting users for slice", e);
                            err.println("Error getting users for slice, will ignore.");
                        }

                        try {
                            List<GeniUrn> sliceAggs = sa.getAggregatesForSlice(logger, sliceCredentials, sliceUrn);
                            sliceInfoHT.put("sliceAggregates", sliceAggs);
                        } catch (JFedException e) {
                            LOG.error("Error getting aggregates for slice", e);
                            err.println("Error getting aggregates for slice, will ignore.");
                        }

                        try {
                            Date sliceFoundExpirationDate = sa.getSliceExpiration(logger, sliceCredentials, sliceUrn);
                            if (sliceFoundExpirationDate != null)
                                sliceInfoHT.put("sliceExpiration", RFC3339Util.dateToRFC3339String(sliceFoundExpirationDate));
                        } catch (JFedException e) {
                            LOG.error("Error getting expiration date for slice", e);
                            err.println("Error getting expiration date, will ignore.");
                        }
                    }
                } else {
                    assert cli.isCommand(CREATE, CREATE_SLICE_ONLY);
                    if (!cli.createSliceArg && cli.isCommand(CREATE)) {
                        err.println("Error: The slice " + sliceUrn + " does not exist, and you have not specified the --create-slice option.");
                        return 2;
                    }
                    UserAndSliceApiWrapper.SliceInfo sliceInfo;
                    try {
                        sliceInfo =
                                sa.createSlice(logger, sliceUrn.getEncodedResourceName(), cli.requestedExpirationDate, cli.projectNameArg);
                        if (!sliceInfo.getCredentials().isEmpty()) {
                            sliceExpirationDate = sliceInfo.getCredentials().get(0).getExpiresDate();
                        }
                    } catch (JFedException e) {
                        return callFailed("Error creating slice", e, err, callLogOutput);
                    }
                    sliceCredentials = sliceInfo.getCredentials();
                    LOG.debug("ExperimenterCli created a slice, and got "+sliceCredentials.size()+" credentials");

                    sliceInfoHT.put("sliceCredentials", sliceCredentials);
                    sliceInfoHT.put("sliceUrn", sliceInfo.getUrn());
                    sliceInfoHT.put("sliceExpiration", cli.requestedExpirationDate);
                }
                if (sliceCredentials == null || sliceCredentials.isEmpty()) {
                    err.println("Error: Did not receive slice credentials. (but call was successful)");
                    return 2;
                }
                LOG.debug("ExperimenterCLI has "+sliceCredentials.size()+" slice credentials");



                if (shareSliceWithProjectUsers) {
                    if (cli.projectNameArg == null) {
                        if (!cli.silent) {
                            out.println("NOTE: Slice will NOT be shared with all members of project, because no project is specified.");
                        }
                    } else {
                        try {
                            List<GeniUrn> projectUsers = sa.getUsersForSubAuthority(logger, cli.projectNameArg);
                            shareUserUrns.addAll(projectUsers);
                            if (!cli.silent) {
                                out.println("Slice will be shared with all members of project \"" + cli.projectNameArg + "\": " + projectUsers);
                            }
                        } catch (UnsupportedOperationException e) {
                            err.println("Cannot get project members of \"" + cli.projectNameArg + "\" to share slice with. -> Will not share slice with project members. " +
                                    "Reason: API does not support retrieving the project members.");
                        } catch (JFedException e) {
                            LOG.error("Failed to get project members of \"" + cli.projectNameArg + "\" to share slice with. -> Will not share slice with project members.", e);
                            err.println("Failed to get project members of \"" + cli.projectNameArg + "\" to share slice with. Will not share slice with project members. Exception: " + e.getMessage());
                            e.printStackTrace(err);
                        }
                    }
                }
                if (cli.debug) {
                    out.println("Slice will be shared with: "+shareUserUrns);
                }

                if (!shareUserUrns.isEmpty()) {
                    if (!cli.silent)
                        out.println("Will share existing slice with: "+shareUserUrns);
                    for (GeniUrn userUrn : shareUserUrns) {
                        try {
                            sa.shareSlice(logger, sliceUrn, sliceCredentials, userUrn);
                        } catch (JFedException e) {
                            LOG.error("Failed to share slice with \""+userUrn+"\"", e);
                            err.println("Failed to share slice with \"" + userUrn + "\": " + e.getMessage());
                            e.printStackTrace(err);
                        }
                    }
                }


                GeniUrn newSliceUrn = null;
                //see FEDIBBTDEV-2071: change slice URN to match slice URN in credential
                for (AnyCredential cred : sliceCredentials) {
                    if (cred instanceof SfaCredential) {
                        SfaCredential c = (SfaCredential) cred;
                        if (c.getTargetUrn() != null && c.getTargetUrn().equalsIgnoreCase(sliceUrn.toString()) && !Objects.equals(c.getTargetUrn(), sliceUrn.toString())) {
                            String message = "(CaseBugs): slice URN ("+sliceUrn+") does not match URN in credential ("+c.getTargetUrn()+"). " +
                                    "Will automatically fix this by changing slice URN. " +
                                    "(Otherwise, some AM's might give strange errors)";
//                        if (!cli.silent)
                            if (cli.debug)
                                err.println("WARNING: "+message);
                            LOG.warn(message);

                            newSliceUrn = GeniUrn.parse(c.getTargetUrn());
                            if (newSliceUrn != null) {
                                break;
                            }
                            else
                                err.println("ERROR: slice URN in credential is not a valid URN. This should not happen.");
                        }
                    }
                    if (cred instanceof AbacCredential) {
                        AbacCredential c = (AbacCredential) cred;
                        //cannot do this for ABAC
                    }
                }
                final GeniUrn usedSliceUrn = newSliceUrn == null ? sliceUrn : newSliceUrn;
                sliceInfoHT.put("sliceUrn", usedSliceUrn);

                if (cli.isCommand(CREATE_SLICE_ONLY, SHOW_SLICE_INFO)) {
                    if (cli.isCommand(CREATE_SLICE_ONLY) && sliceExists) {
                        err.println("Error: Slice already exists.");
                    }

                    if (cli.isJson) {
                        String jsonOutput = XmlRpcPrintUtil.xmlRpcObjectToString(sliceInfoHT);
                        out.println(jsonOutput);
                    } else {
                        assert cli.isTextOutput;
                        out.println("User URN: "+sliceInfoHT.get("userUrn"));
//                    out.println("User Credentials: "+sliceInfoHT.get("userCredentials"));
                        out.println("Slice URN: "+sliceInfoHT.get("sliceUrn"));
                        out.println("Slice URN: "+sliceInfoHT.get("sliceExpiration"));
                        List sliceCreds = (List) sliceInfoHT.get("sliceCredentials");
                        int i = 0;
                        if (sliceCreds != null)
                            for (Object c : sliceCreds) {
                                if (c != null && c instanceof AnyCredential) {
                                    AnyCredential ac = (AnyCredential) c;
                                    out.println("Slice Credential " + (++i) + ":\n"+ac.getCredentialXml()+"\n\nEND SLICE CREDENTIAL\n");
                                }
                            }
                    }

                    if (cli.isCommand(CREATE_SLICE_ONLY) && sliceExists)
                        return 1;
                    return 0;
                }

                if (cli.isCommand(CREATE) && cli.sliceRecoverInfoArg != null) {
                    if (cli.debug)
                        out.println("Writing slice recovery info file \"" + cli.sliceRecoverInfoArg + "\"");

                    try {
                        FileConfiguration conf = new PropertiesConfiguration();
                        conf.setEncoding("UTF-8");
                        conf.setProperty(KEY_EXPIRATION_TIME,
                                sliceExpirationDate == null ? "unknown"
                                        : ISO_OFFSET_DATE_TIME.format(ZonedDateTime.ofInstant(sliceExpirationDate.toInstant(), ZoneId.systemDefault())));
                        conf.setProperty(KEY_REQUEST_RSPEC, rspec.getXmlString());
                        conf.save(new File(cli.sliceRecoverInfoArg));
                    } catch (ConfigurationException e) {
                        LOG.error("Error writing slice recovery info to \"" + cli.sliceRecoverInfoArg + "\"", e);
                        err.println("ERROR (non fatal): Exception writing slice recovery info to \"" + cli.sliceRecoverInfoArg + "\": " + e.getMessage());
                    }
                }


                if (cli.fakeSliver) {
                    out.println("Argument and slice processing done. --cli.fake-sliver is set, so no further steps are taken.");
                    return 0;
                }

                final List<AutoShareLanInfo> lanToShare = new ArrayList<>();
                if (cli.isCommand(CREATE)) {
                    try {
                        modelRspec = getModelRspec(modelRspec, rspec);

                        for(RspecLink link : modelRspec.getLinks()) {
                            if (link.hasAutoShareLan()) {
                                for (String shareName : link.getAutoShareLanNames()) {
                                    String clientId = link.getClientId();
                                    if (clientId == null) {
                                        LOG.warn("Cannot share link that has no client_id");
                                    } else {
                                        if (link.getComponentManagerUrns().size() > 1) {
                                            LOG.warn("Cannot share link ("+clientId+") that has multiple component_managers");
                                        } else {
                                            if (link.getComponentManagerUrns().isEmpty()) {
                                                LOG.warn("Cannot share link ("+clientId+") that has no component_managers");
                                            } else {
                                                GeniUrn cm = link.getComponentManagerUrns().get(0);
                                                AutoShareLanInfo asli = new AutoShareLanInfo(cm, clientId, shareName);
                                                lanToShare.add(asli);
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    } catch (RspecParseException e) {
                        LOG.error("Problem parsing RSpec to automatically share LAN. Will not automatically share LAN.", e);
                        lanToShare.clear();
                    }
                }

                final List<AnsiblePlaybookInfo> ansiblePlaybooksToExecute = new ArrayList<>();
                if ((cli.isCommand(CREATE, MANIFEST)) && !cli.noPlaybookToExecute) {
                    if (cli.ansibleExtraPlaybooks != null) {
                        LOG.debug("Processing "+cli.ansibleExtraPlaybooks.length+" command line extra playbooks");
                        for (String ansibleExtraPlaybook : cli.ansibleExtraPlaybooks) {
                            String[] parts = ansibleExtraPlaybook.split(" ", 2);
                            String source = parts[0];
                            String outputFilename = parts.length > 1 ? parts[1] : null;
                            ExecuteAnsiblePlaybook executeAnsiblePlaybook = new BasicExecuteAnsiblePlaybook(source, outputFilename);
                            AnsiblePlaybookInfo info = new AnsiblePlaybookInfo(executeAnsiblePlaybook);
                            try {
                                info.loadContent(cli.ansibleAllowPlaybookInputfile);
                                info.checkFilename(false /*mustBeBasename*/, cli.ansibleAllowAnyPlaybookOutputFile);
                            } catch (AnsiblePlaybookInfo.AnsiblePlaybookException e) {
                                LOG.debug("Error processing ExecuteAnsiblePlaybook", e);
                                err.println("Error processing --ansible-add-playbook: " + e.getMessage());
                                return 2;
                            }
                            ansiblePlaybooksToExecute.add(info);
                        }
                    }

                    if (rspec != null && cli.ansibleExecuteRspecPlaybooks) {
                        try {
                            modelRspec = getModelRspec(modelRspec, rspec);

                            for (ExecuteAnsiblePlaybook executeAnsiblePlaybook : modelRspec.getExecuteAnsiblePlaybooks()) {
                                AnsiblePlaybookInfo info = new AnsiblePlaybookInfo(executeAnsiblePlaybook);
                                try {
                                    info.loadContent(cli.ansibleAllowPlaybookInputfile);
                                    info.checkFilename(false /*mustBeBasename*/, cli.ansibleAllowAnyPlaybookOutputFile);
                                } catch (AnsiblePlaybookInfo.AnsiblePlaybookException e) {
                                    LOG.debug("Error processing ExecuteAnsiblePlaybook", e);
                                    err.println("Error processing " + RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart() + ": " + e.getMessage());
                                    return 2;
                                }
                                ansiblePlaybooksToExecute.add(info);
                            }
                        } catch (RspecParseException e) {
                            LOG.error("Problem parsing RSpec to automatically execute ansible playbooks. Will not automatically execute ansible playbooks.", e);
                            ansiblePlaybooksToExecute.clear();
                        }
                    }

                    Set<String> sources = new HashSet<>();
                    for (Iterator<AnsiblePlaybookInfo> it = ansiblePlaybooksToExecute.iterator(); it.hasNext();) {
                        AnsiblePlaybookInfo i = it.next();
                        if (!sources.add(i.getExecuteAnsiblePlaybook().getSource())) {
                            err.println("Warning: duplicate playbook will be ignored: " + i.getExecuteAnsiblePlaybook());
                            it.remove();
                        }
                    }

                    if (cli.debug) {
                        out.println("There are "+ansiblePlaybooksToExecute.size()+" ansible playbooks to execute.");
                    }
                }


                List<UserSpec> userSpecs = new ArrayList<>();
                if (cli.isCommand(CREATE)) {
                    if (cli.sshKeysUserServerKeys) {
                        try {
                            List<String> userKeys = sa.getSshKeysForUser(logger, targetUserUrn);
                            addToUserSpecs(userSpecs, targetUserUrn, userKeys);
                            if (cli.debug)
                                out.println("Added " + userKeys.size() + " keys from server.");
                        } catch (JFedException e) {
                            return callFailed("Error retrieving ssh keys for user " + targetUserUrn, e, err, callLogOutput);
                        }
                    }
                    if (!shareUserUrns.isEmpty() && cli.sshKeysSharedUserKeys) {
                        for (GeniUrn shareUserUrn : shareUserUrns) {
                            try {
                                List<String> userKeys = sa.getSshKeysForUser(logger, shareUserUrn);
                                if (userKeys != null && !userKeys.isEmpty()) {
                                    addToUserSpecs(userSpecs, shareUserUrn, userKeys);
                                    if (cli.debug)
                                        out.println("Added " + userKeys.size() + " keys from server for user \"" + shareUserUrn + "\".");
                                } else {
                                    if (!cli.silent)
                                        err.println("WARNING: No SSH keys found for user \"" + shareUserUrn + "\".");
                                }
                            } catch (JFedException e) {
                                LOG.error("Error retrieving ssh keys for user \"" + shareUserUrn+"\"", e);
                                err.println("Error retrieving ssh keys for user \"" + shareUserUrn+"\": "+e.getMessage());
                                e.printStackTrace(err);
                            }
                        }
                    }
                    if (cli.sshKeysUserCert) {
                        if (!usesSpeaksFor) {
                            String userKey = KeyUtil.publicKeyToOpenSshAuthorizedKeysFormat(user.getPublicKey());
                            addToUserSpecs(userSpecs, targetUserUrn, userKey);
                            if (cli.debug)
                                out.println("Adding userspec from user certificate.");
                        } else {
                            PublicKey pubKey = null;
                            for (AnyCredential speaksForCredential : speaksForCredentials) {
                                assert speaksForCredential != null;
                                PublicKey curPubKey = null;
                                GeniUrn currentSpeakingForUser = null;
                                if (speaksForCredential instanceof AbacCredential) {
                                    AbacCredential c = (AbacCredential) speaksForCredential;
                                    curPubKey = c.getSpokenForPubKey();

                                    if (c.getSpokenForUrn() != null)
                                        currentSpeakingForUser = c.getSpokenForUrn();
                                }
                                try {
                                    if (speaksForCredential instanceof SfaCredential) {
                                        SfaCredential c = (SfaCredential) speaksForCredential;
                                        if (c.getTargetUrn() != null)
                                            currentSpeakingForUser = new GeniUrn(c.getTargetUrn());
                                        if (c.getTargetGid() != null) {
                                            Gid gid = new Gid(c.getTargetGid());
                                            if (gid.getCertificate() != null)
                                                curPubKey = gid.getCertificate().getPublicKey();
                                            if (currentSpeakingForUser == null && gid.getSubjectUrn() != null) {
                                                currentSpeakingForUser = new GeniUrn(gid.getSubjectUrn());
                                            }
                                        }
                                    }
                                } catch (GeniUrn.GeniUrnParseException e) {
                                    LOG.error("Ignoring speaksfor cert target URN parse exception", e);
                                }

                                if (curPubKey != null && currentSpeakingForUser != null && Objects.equals(currentSpeakingForUser, speakingForUser)) {
                                    pubKey = curPubKey;
                                }
                            }
                            if (pubKey != null) {
                                String userKey = KeyUtil.publicKeyToOpenSshAuthorizedKeysFormat(pubKey);
                                addToUserSpecs(userSpecs, targetUserUrn, userKey);
                                if (cli.debug)
                                    out.println("Adding userspec from speaksFor SfaCredential");
                            } else {
                                err.println("WARNING: Could not fetch user public key from user speaksFor certificate(s).");
                            }
                        }
                    }
                    if (cli.sshKeysRspec) {
                        try {
                            modelRspec = getModelRspec(modelRspec, rspec);
                            List<UserSpec> extraUserSpec = modelRspec.getSshKeys();
                            if (cli.debug)
                                out.println("Adding " + extraUserSpec.size() + " userspecs from RSpec");
                            for (UserSpec us : extraUserSpec) {
                                GeniUrn userUrn = GeniUrn.parse(us.getUrn());
                                addToUserSpecs(userSpecs, userUrn, us.getSshKey());
                            }
                        } catch (RspecParseException e) {
                            err.println("WARNING: Error parsing specified RSpec. The users could not be extracted from this RSpec. Note: This tool will continue, and the RSpec will still be used to create slivers.");
                        }
                    }
                    if (cli.debug) {
                        out.println("The following user specification will be added to all create sliver calls:");
                        for (UserSpec us : userSpecs) {
                            out.println("  User " + us.getUrn() + " with " + us.getSshKey().size() + " keys");
                            for (String key : us.getSshKey())
                                out.println("     " + key);
                        }
                    }
                }

                //create amWrappers
                List<AggregateManagerWrapper> amWrappers = new ArrayList<>();
                for (String rspecComponentManager : rspecComponentManagers) {
                    GeniUrn rspecComponentManagerUrn = GeniUrn.parse(rspecComponentManager);

                    Server currentAuthority =
                            authorityFinder.findByUrn(rspecComponentManager, AuthorityFinder.Purpose.CREATE_SLIVER);
                    if (currentAuthority == null) {
                        err.println("Tested authority URN is unknown: \"" + rspecComponentManager + "\"");
                        return 2;
                    }
                    assert currentAuthority != null;

                    final AggregateManagerWrapper am;
                    if (cli.amApiVersionArg == null) {
                        am = automaticAggregateManagerWrapperFactory.create(currentAuthority);
                    } else {
                        if (Objects.equals(cli.amApiVersionArg, "2"))
                            am = new AMv2Wrapper(logger, userProvider, conPool, jFedPreferences, currentAuthority);
                        else {
                            assert Objects.equals(cli.amApiVersionArg, "3");
                            am = new AMv3Wrapper(logger, userProvider, conPool, jFedPreferences, currentAuthority);
                        }
                    }
                    if (usesSpeaksFor) {
                        am.setSpeaksFor(speaksForCredentials, speakingForUser);
                    }
                    amWrappers.add(am);
                }



                if (cli.isCommand(CREATE) && cli.abortIfSliversExist) {
                    if (cli.debug) {
                        out.println("--abort-if-slivers-exist option is set: will first check if slivers already exist");
                    }
                    for (AggregateManagerWrapper am : amWrappers) {
                        Server currentAuthority = am.getAmServer();
                        String rspecComponentManager = currentAuthority.getDefaultComponentManagerUrn();
                        if (!cli.silent)
                            out.println("Contacting  " + rspecComponentManager + "...");

                        try {
                            LOG.debug("ExperimenterCLI is calling status with " + sliceCredentials.size() + " credentials");
                            StatusDetails statusDetails = am.status(usedSliceUrn, sliceCredentials);

                            if (!statusDetails.getAllSliverUrns().isEmpty()) {
                                err.println("Slivers exist at "+rspecComponentManager+" and --abort-if-slivers-exist is set: will not create any new slivers");
                                return -1;
                            }
                            if (cli.debug) {
                                out.println("No slivers at " + rspecComponentManager+"  (got empty sliver list)");
                            }
                        } catch (JFedException e) {
                            //error is expected: normally means there are no slivers
                            if (cli.debug) {
                                out.println("No known slivers at " + rspecComponentManager + "  (got error)");
                            }
                        }
                    }
                }

                ManifestRspecMerger manifestRspecMerger = new ManifestRspecMerger(ModelRspecType.BASIC);

                Map outerStatusHt = null;
                Map outerStatusAmDetailHt = null;
                Date earliestSliverExpireDate = null;
                if (cli.isCommand(STATUS)) {
                    outerStatusHt = new HashMap();
                    outerStatusHt.put("sliceUrn", sliceUrn);
                    if (sliceExpirationDate != null)
                        outerStatusHt.put("sliceExpireDate", RFC3339Util.dateToRFC3339String(sliceExpirationDate));
                    outerStatusAmDetailHt = new HashMap();
                    outerStatusHt.put("AMs", outerStatusAmDetailHt);
                }

                for (AggregateManagerWrapper am : amWrappers) {
                    Server currentAuthority = am.getAmServer();
                    String rspecComponentManager = currentAuthority.getDefaultComponentManagerUrn();
                    GeniUrn rspecComponentManagerUrn = currentAuthority.getDefaultComponentManagerAsGeniUrn();
                    if (!cli.silent)
                        out.println("Contacting  " + rspecComponentManager + "...");

                    if (cli.isCommand(CREATE)) {
                        try {
                            String requestRspec = rspec.getXmlString();

                            String manifest;
                            if (am.hasSeperateAllocateAndProvision()) {
                                AggregateManagerWrapper.AllocateReply allocateReply =
                                        am.allocate(usedSliceUrn, sliceCredentials, requestRspec, cli.requestedExpirationDate);

                                if (allocateReply != null && allocateReply.getSliverUrns() != null && !allocateReply.getSliverUrns().isEmpty())
                                    sa.registerAggregatesForSlice(logger, sliceCredentials, usedSliceUrn, am.getAmServer().getDefaultComponentManagerAsGeniUrn(), allocateReply.getSliverUrns(), cli.requestedExpirationDate);

                                manifest =
                                        am.provisionAndStart(usedSliceUrn, sliceCredentials, userSpecs, cli.requestedExpirationDate);

                                if (manifest != null) {
                                    Collection<GeniUrn> sliverUrns = ManifestSliverFinder.findSliversInManifest(manifest);
                                    if (allocateReply != null && allocateReply.getSliverUrns() != null && !allocateReply.getSliverUrns().isEmpty())
                                        sliverUrns.removeAll(allocateReply.getSliverUrns());
                                    if (!sliverUrns.isEmpty())
                                        sa.registerAggregatesForSlice(logger, sliceCredentials, usedSliceUrn, am.getAmServer().getDefaultComponentManagerAsGeniUrn(), sliverUrns, cli.requestedExpirationDate);
                                }
                            } else {
                                manifest =
                                        am.createSliver(usedSliceUrn, sliceCredentials, requestRspec, userSpecs, cli.requestedExpirationDate);

                                if (manifest != null) {
                                    Collection<GeniUrn> sliverUrns = ManifestSliverFinder.findSliversInManifest(manifest);
                                    sa.registerAggregatesForSlice(logger, sliceCredentials, usedSliceUrn, am.getAmServer().getDefaultComponentManagerAsGeniUrn(), sliverUrns, cli.requestedExpirationDate);
                                }
                            }

                            if (!cli.silent)
                                out.println("Sliver at  " + rspecComponentManager + " is created and initializing...");

                            manifestRspecMerger.setManifest(currentAuthority.getDefaultComponentManagerAsGeniUrn(), new ManifestRspecSource(manifest, ModelRspecType.BASIC));
                        } catch (AggregateManagerWrapper.NotEnoughFreeResourcesException e) {
                            if (cli.deleteOnCreateFailure)
                                deleteAll(amWrappers, cli.silent, out, err, usedSliceUrn, sliceCredentials, logger, sa);
                            return callFailed("Not enough resources available to create sliver at " + currentAuthority.getDefaultComponentManagerUrn(), e, err, callLogOutput);
                        } catch (JFedException e) {
                            if (cli.deleteOnCreateFailure)
                                deleteAll(amWrappers, cli.silent, out, err, usedSliceUrn, sliceCredentials, logger, sa);
                            return callFailed("Error creating sliver at " + currentAuthority.getDefaultComponentManagerUrn(), e, err, callLogOutput);
                        }
                    }

                    if (cli.isCommand(DELETE))
                        try {
                            am.deleteSliver(usedSliceUrn, sliceCredentials);

                            if (!cli.silent)
                                out.println("Slivers at  " + rspecComponentManager + " have been deleted.");


                            try {
                                sa.unregisterAggregatesForSlice(logger, sliceCredentials, usedSliceUrn, rspecComponentManagerUrn, null/*sliverUrns*/);

                                if (!cli.silent)
                                    out.println("Slivers at  " + rspecComponentManager + " has been unregistered.");
                            } catch (JFedException e) {
                                String message =
                                        "Error unregistering sliver at " + rspecComponentManager + ". (this error will be ignored)";
                                if (e.getXmlRpcResult() != null)
                                    err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                                else {
                                    err.println(message + ":");
                                    e.printStackTrace(err);
                                }
                            }
                        } catch (JFedException e) {
                            String message =
                                    "Error deleting sliver at " + rspecComponentManager + ". (this error will be ignored)";
                            if (e.getXmlRpcResult() != null)
                                err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                            else {
                                err.println(message + ":");
                                e.printStackTrace(err);
                            }
                        }

                    if (cli.isCommand(STATUS))
                        try {
                            LOG.debug("ExperimenterCLI is calling status with " + sliceCredentials.size() + " credentials");
                            StatusDetails statusDetails = am.status(usedSliceUrn, sliceCredentials);

                            if (!cli.silent && cli.isTextOutput)
                                out.println("Sliver at  " + rspecComponentManager + " has status " + statusDetails.getGlobalStatus());

                            if (cli.isJson || cli.isTextOutput) {
                                Date earliestAMSliverExpireDate = statusDetails.getEarliestKnownExpires();
                                if (earliestSliverExpireDate == null)
                                    earliestSliverExpireDate = earliestAMSliverExpireDate;
                                else {
                                    if (earliestAMSliverExpireDate != null && earliestAMSliverExpireDate.before(earliestSliverExpireDate))
                                        earliestSliverExpireDate = earliestAMSliverExpireDate;
                                }
                                Map outerAMHt = new HashMap();
                                outerAMHt.put("amUrn", am.getAmServer().getDefaultComponentManagerUrn());
                                outerAMHt.put("amGlobalSliverStatus", statusDetails.getGlobalStatus());
                                if (earliestAMSliverExpireDate != null)
                                    outerAMHt.put("earliestSliverExpireDate", RFC3339Util.dateToRFC3339String(earliestAMSliverExpireDate));
                                List detailStatus = new ArrayList();
                                for (Map.Entry<GeniUrn, StatusDetails.SliverStatus> detail : statusDetails.getStatusBySliverUrn().entrySet()) {
                                    Map detailHt = new HashMap();
                                    detailHt.put("urn", detail.getKey());
                                    detailHt.put("type", "sliver");
                                    detailHt.put("status", detail.getValue() + "");
                                    detailStatus.add(detailHt);
                                }
                                for (Map.Entry<GeniUrn, StatusDetails.SliverStatus> detail : statusDetails.getStatusByComponentUrn().entrySet()) {
                                    Map detailHt = new HashMap();
                                    detailHt.put("urn", detail.getKey());
                                    detailHt.put("type", "component");
                                    detailHt.put("status", detail.getValue() + "");
                                    detailStatus.add(detailHt);
                                }
                                outerAMHt.put("details", detailStatus);
                                outerStatusAmDetailHt.put(am.getAmServer().getDefaultComponentManagerUrn(), outerAMHt);
                            }
                        } catch (JFedException e) {
                            String message =
                                    "Error getting status of sliver at " + rspecComponentManager + ". (this error will be ignored)";
                            if (e.getXmlRpcResult() != null) {
                                err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                                err.println("Request was: " + "\"" + e.getXmlRpcResult().getRequestXmlRpcString() + "\"");
                            } else {
                                err.println(message + ":");
                                e.printStackTrace(err);
                            }
                        }


                    if (cli.isCommand(MANIFEST))
                        try {
                            LOG.debug("ExperimenterCLI is fetching manifest with " + sliceCredentials.size() + " credentials");
                            String manifestString = am.describe(usedSliceUrn, sliceCredentials);

                            if (manifestString != null)
                                manifestRspecMerger.setManifest(rspecComponentManagerUrn, new ManifestRspecSource(manifestString, ModelRspecType.BASIC));

                            if (!cli.silent && manifestString != null)
                                out.println("Manifest for " + rspecComponentManager + " received.");
                            if (manifestString == null)
                                err.println("Error fetching manifest for " + rspecComponentManager + "");
                        } catch (JFedException e) {
                            String message =
                                    "Error fetching manifest at " + rspecComponentManager + ". (this error will be ignored, but the final manifest might be missing information)";
                            if (e.getXmlRpcResult() != null) {
                                err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                                err.println("Request was: " + "\"" + e.getXmlRpcResult().getRequestXmlRpcString() + "\"");
                            } else {
                                err.println(message + ":");
                                e.printStackTrace(err);
                            }
                        }

                    if (cli.isCommand(POA)) {
                        if (rspecComponentManagerUrn == null ||
                                !Objects.equals(rspecComponentManagerUrn.getTopLevelAuthority_withoutSubAuth(), cli.poaTargetSliverUrn.getTopLevelAuthority_withoutSubAuth())) {
                            if (!cli.silent)
                                out.println("No PerformOperationalAction command on  " + rspecComponentManager + ".");
                            continue;
                        }
                        if (!cli.silent)
                            out.println("Executing PerformOperationalAction command on " + rspecComponentManager + "...");
                        try {
                            if (cli.poaActionArg.equalsIgnoreCase("ReloadOS")) {
                                if (!am.isReloadOSSupported()) {
                                    err.println("The AM at " + rspecComponentManager + " does not support " + cli.poaActionArg + ". Will try anyway, but the call will likely fail.");
                                }
                                LOG.debug("ExperimenterCLI is calling PerformOperationalAction with " + sliceCredentials.size() + " credentials");
                                am.reloadOS(cli.poaTargetSliverUrn, sliceCredentials);
                            }
                            if (cli.poaActionArg.equalsIgnoreCase("Restart")) {
                                if (!am.isRestartSupported()) {
                                    err.println("The AM at " + rspecComponentManager + " does not support " + cli.poaActionArg + ". Will try anyway, but the call will likely fail.");
                                }
                                LOG.debug("ExperimenterCLI is calling PerformOperationalAction with " + sliceCredentials.size() + " credentials");
                                am.restart(cli.poaTargetSliverUrn, sliceCredentials);
                            }
                            if (cli.poaActionArg.equalsIgnoreCase("ConsoleUrl")) {
                                if (!am.isOpenConsoleSupported()) {
                                    err.println("The AM at " + rspecComponentManager + " does not support " + cli.poaActionArg + ". Will try anyway, but the call will likely fail.");
                                }
                                LOG.debug("ExperimenterCLI is calling PerformOperationalAction with " + sliceCredentials.size() + " credentials");
                                URL consoleURL = am.openConsole(cli.poaTargetSliverUrn, sliceCredentials);
                                if (consoleURL != null)
                                    out.println("The console URN for " + cli.poaTargetSliverUrn + " is " + consoleURL.toExternalForm());
                                else
                                    out.println("Error: No console URN was returned for " + cli.poaTargetSliverUrn + "");
                            }
                            if (!cli.silent)
                                out.println("PerformOperationalAction call completed.");
                        } catch (JFedException e) {
                            String message =
                                    "Error in PerformOperationalAction " + cli.poaActionArg + " of " + cli.poaTargetSliverUrn + ".";
                            if (e.getXmlRpcResult() != null) {
                                err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                                err.println("Request was: " + "\"" + e.getXmlRpcResult().getRequestXmlRpcString() + "\"");
                            } else {
                                err.println(message + ":");
                                e.printStackTrace(err);
                            }
                        }
                    }

                    if (cli.isCommand(RENEW)) {
                        Date requestedSliverExpirationDate = sliceExpirationDate == null ? cli.requestedExpirationDate : sliceExpirationDate;
                        assert requestedSliverExpirationDate != null;

                        try {
                            if (!cli.silent) {
                                out.print("Renewing sliver at  " + rspecComponentManager + " to expire at " +
                                        RFC3339Util.dateToRFC3339String(requestedSliverExpirationDate, true, true, true));
                                if (sliceExpirationDate != null)
                                    out.println(" (Slice expired at " +
                                            RFC3339Util.dateToRFC3339String(sliceExpirationDate, true, true, true) + ")");
                                else
                                    out.println();
                            }

                            am.renewSlivers(usedSliceUrn, null/*no sliverUrns known*/, sliceCredentials, requestedSliverExpirationDate);

                            if (!cli.silent)
                                out.println("Sliver at  " + rspecComponentManager + " has been renewed successfully");

                        } catch (JFedException e) {
                            successExitStatus = false;
                            String message =
                                    "Error renewing sliver at " + rspecComponentManager + ". (jFed will first continue Renewing the other AMs and fail afterwards)\n" +
                                            "WARNING: Your slivers (= resources) might now expire before your slice itself expires. You can use the \"status\" command to check this.";
                            if (e.getXmlRpcResult() != null)
                                err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                            else {
                                err.println(message + ":");
                                e.printStackTrace(err);
                            }
                        }

                        if (sa.isRegisterAggregatesForSliceSupported())
                            try {
                                StatusDetails statusDetails = am.status(usedSliceUrn, sliceCredentials);

                                Collection<GeniUrn> sliverUrns = statusDetails.getAllSliverUrns();
                                Date firstExpires = statusDetails.getEarliestKnownExpires();
                                Date lastExpires = statusDetails.getLatestKnownExpires();
                                Date updateExpireDate = requestedSliverExpirationDate;
                                if (firstExpires != null && lastExpires != null) {
                                    updateExpireDate = lastExpires; //better register them too long than too short
                                    if (!Objects.equals(firstExpires, lastExpires)) {
                                        if (!cli.silent)
                                            err.println("WARNING: The slivers at "+rspecComponentManager+" do not expire at the same time. " +
                                                    "first expire time = "+firstExpires+" last expire time = "+lastExpires+"" +
                                                    " -> Will update all slivers as expiring at "+updateExpireDate);
                                    }
                                }

                                sa.updateSliverExpirationDateInfoForSlice(logger, sliceCredentials, usedSliceUrn, sliverUrns, updateExpireDate);
                            } catch (Exception e) {
                                String message =
                                        "Error registering renewed sliver(s) at " + rspecComponentManager + ". This is not critical, and will be ignored. This will not influence the exit code.";
                                if (e instanceof JFedException) {
                                    JFedException je = (JFedException) e;
                                    if (je.getXmlRpcResult() != null)
                                        err.println(message + ": " + "\"" + je.getXmlRpcResult().getResultXmlRpcString() + "\"");
                                    else {
                                        err.println(message + ":");
                                        e.printStackTrace(err);
                                    }
                                } else {
                                    err.println(message + ":");
                                    e.printStackTrace(err);
                                }
                            }
                    }
                }

                if (cli.isCommand(MANIFEST, CREATE)) {
                    if (cli.debug || (!cli.silent && cli.isCommand(MANIFEST)))
                        out.println("Writing combined manifest to file \"" + cli.manifestFilename + "\"");
                    String combinedManifestXmlString = manifestRspecMerger.getMergedManifest().getRspecXmlString();
                    IOUtils.stringToFile(new File(cli.manifestFilename), combinedManifestXmlString);

                    if (cli.ansibleDir != null) {
                        if (!cli.ansibleDir.exists()) {
                            boolean created = cli.ansibleDir.mkdir();
                            if (!cli.ansibleDir.exists()) {
                                err.println("ERROR: Could not create ansible dir: \""+cli.ansibleDirName+"\"");
                            }
                        }
                        if (cli.ansibleDir.exists()) {
                            AnsibleFileWriter writer = null;
                            if (user.getPrivateKeyFile() != null) {
                                try {
                                    String privKey = IOUtils.fileToString(user.getPrivateKeyFile());
                                    if (!KeyUtil.isPemPrivateKeyEncrypted(privKey)) {
                                        writer = AnsibleFileWriter.createWithLinkedPrivateKey(
                                                new BasicStringRspec(combinedManifestXmlString),
                                                user.getPrivateKeyFile(),
                                                user.getPublicKey(),
                                                user,
                                                null/*sshProxyInfo*/,
                                                null /*preferredUser*/);
                                    }
                                } catch (Exception e) {
                                    LOG.error("Exception trying to determine if private key file is encrypted. Safe fallback: Will assume it is.", e);
                                    writer = null;
                                }
                            }
                            if (writer == null) {
                                writer = AnsibleFileWriter.createWithCopiedPrivateKey(
                                        new BasicStringRspec(combinedManifestXmlString),
                                        user.getPrivateKey(),
                                        user.getPublicKey(),
                                        user,
                                        null, null /*preferredUser*/);
                            }
                            writer.writeFilesToDir(cli.ansibleDir);
                        }
                    }
                }

                if (cli.isCommand(STATUS)) {
                    if (earliestSliverExpireDate != null)
                        outerStatusHt.put("earliestSliverExpireDate", RFC3339Util.dateToRFC3339String(earliestSliverExpireDate));
                    if (cli.isJson && outerStatusHt != null) {
                        out.println(JSonHelper.jsonObjectToString(outerStatusHt));
                    }
                    if (cli.isTextOutput && outerStatusHt != null) {
                        out.println("Status Info:");
                        out.println("   Slice URN: "+outerStatusHt.get("sliceUrn"));
                        out.println("   Slice expire date: "+outerStatusHt.get("sliceExpireDate"));
                        if (outerStatusHt.get("earliestSliverExpireDate") != null)
                            out.println("   Earliest sliver expire date: "+outerStatusHt.get("earliestSliverExpireDate"));
                        else
                            out.println("   Earliest sliver expire date: no slivers");
                        Map amsHt = (Map) outerStatusHt.get("AMs");
                        Set<Map.Entry> entries = amsHt.entrySet();
                        for (Map.Entry e : entries) {
                            String key = (String) e.getKey();
                            Map amHt = (Map)e.getValue();

                            String amUrn = amHt.get("amUrn")+"";
                            String amGlobalSliverStatus = amHt.get("amGlobalSliverStatus")+"";
                            String earliestAMSliverExpireDateStr = amHt.get("earliestSliverExpireDate") == null ? null : amHt.get("earliestSliverExpireDate")+"";

                            out.println("   AM: "+amUrn+" "+amGlobalSliverStatus+(earliestAMSliverExpireDateStr == null ? "" : " "+earliestAMSliverExpireDateStr));

                            List details= (List) amHt.get("details");
                            for (Object detail : details) {
                                Map detailHt = (Map) detail;
                                String urn = detailHt.get("urn")+"";
                                String type = (String)detailHt.get("type");
                                String status = detailHt.get("status")+"";
                                out.println("      "+type+": "+urn+" "+status);
                            }
                        }
                    }
                }

                if (cli.isCommand(CREATE)) {
                    if (cli.waitUntilReady) {
                        if (!cli.silent)
                            if (rspecComponentManagers.size() == 1)
                                out.println("Will now wait until the sliver is ready...");
                            else
                                out.println("Will now wait until the " + rspecComponentManagers.size() + " slivers are ready...");

                        for (AggregateManagerWrapper am : amWrappers) {
                            Server currentAuthority = am.getAmServer();
                            String rspecComponentManager = currentAuthority.getDefaultComponentManagerUrn();

                            StatusDetails statusDetails = null;
                            while (statusDetails == null || !Objects.equals(statusDetails.getGlobalStatus(), StatusDetails.SliverStatus.READY)) {
                                if (cli.debug)
                                    out.println("Waiting 5 seconds before checking status...");
                                try {
                                    Thread.sleep(5 * 1000L);
                                } catch (InterruptedException e) {
                                    LOG.debug("InterruptedException while sleeping", e);
                                }

                                if (!cli.silent)
                                    out.println("Contacting " + rspecComponentManager + " to check status...");

                                try {
                                    statusDetails = am.status(usedSliceUrn, sliceCredentials);
                                } catch (JFedException e) {
                                    if (cli.deleteOnBecomeReadyFailure)
                                        deleteAll(amWrappers, cli.silent, out, err, usedSliceUrn, sliceCredentials, logger, sa);
                                    return callFailed("Error getting sliver status at " + currentAuthority.getDefaultComponentManagerUrn(), e, err, callLogOutput);
                                }

                                if (!cli.silent)
                                    out.println("Status of sliver at  " + rspecComponentManager + " is " + statusDetails.getGlobalStatus());

                                if (statusDetails.getGlobalStatus().equals(StatusDetails.SliverStatus.FAIL) ||
                                        statusDetails.getGlobalStatus().equals(StatusDetails.SliverStatus.UNALLOCATED)) {
                                    if (cli.deleteOnBecomeReadyFailure)
                                        deleteAll(amWrappers, cli.silent, out, err, usedSliceUrn, sliceCredentials, logger, sa);
                                    return callFailed("ERROR: Sliver status at " + currentAuthority.getDefaultComponentManagerUrn() + " is " +
                                            statusDetails.getGlobalStatus() + " -> " + statusDetails, null, err, callLogOutput);
                                }
                            }
                        }

                        if (!lanToShare.isEmpty()) {
                            LOG.debug("Will try to share LANs");

                            if (!cli.silent)
                                out.println((lanToShare.size())+" LAN(s) need to be shared. Will wait 10 seconds before trying.");
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                //ignore
                            }

                            for (AggregateManagerWrapper am : amWrappers) {
                                Server currentAuthority = am.getAmServer();

                                for (AutoShareLanInfo info : lanToShare) {
                                    if (Objects.equals(currentAuthority.getDefaultComponentManagerAsGeniUrn(), info.componentManagerUrn)) {
                                        LOG.debug("Will try to share LAN "+info.lanClientId+" as "+info.sharedLanName+" on "+info.componentManagerUrn);
                                        if (am.isShareLanSupported()) {
                                            try {
                                                am.shareLan(usedSliceUrn, sliceCredentials, info.lanClientId, info.sharedLanName);

                                                if (!cli.silent) {
                                                    out.println("Shared LAN " + info.lanClientId + " as " + info.sharedLanName + " on " + info.componentManagerUrn);
                                                }
                                            } catch (JFedException e) {
                                                LOG.error("Failed to share LAN", e);
                                                err.println("Failed to share LAN " + info.lanClientId + " as " + info.sharedLanName + " on " + info.componentManagerUrn + ": "+e.getMessage());
                                                e.printStackTrace();
                                            }
                                        } else {
                                            err.println("Cannot share LAN "+info.lanClientId+" as "+info.sharedLanName+" on "+info.componentManagerUrn+": AM does not support share LAN");
                                        }
                                    }
                                }

                            }
                        }

                        if (!cli.silent)
                            if (rspecComponentManagers.size() == 1)
                                out.println("The sliver is ready.");
                            else
                                out.println("The " + rspecComponentManagers.size() + " slivers are ready.");
                    }
                }

                if (!ansiblePlaybooksToExecute.isEmpty()) {
                    LOG.debug("Will execute "+ansiblePlaybooksToExecute.size()+" Ansible Playbooks");

                    if (!cli.silent)
                        out.println((ansiblePlaybooksToExecute.size())+" ansible playbooks will be executed. Will wait 5 seconds before beginning.");
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        //ignore
                    }

                    int playbookIndex = 0;
                    for (AnsiblePlaybookInfo info : ansiblePlaybooksToExecute) {
                        File outputFile;
                        if (info.getExecuteAnsiblePlaybook().getOutputFilename() == null)
                            outputFile = new File(cli.ansibleDir, "playbook-"+playbookIndex+"-output.txt");
                        else
                            outputFile = cli.ansibleAllowAnyPlaybookOutputFile ? new File(info.getOutputFilename()) : new File(cli.ansibleDir, info.getOutputBasename());
                        File playbookFile = new File(cli.ansibleDir, "playbook-"+playbookIndex+".yml");
                        try {
                            IOUtils.stringToFile(playbookFile, info.getScriptContent());

                            if (!cli.silent)
                                out.println("Executing ansible playbook \""+playbookFile.getName()+"\"...");
                            callAnsible(cli.ansiblePlayBookExe, playbookFile, cli.ansibleDir, cli.ansibleDebug, outputFile);
                        } catch (Exception e) {
                            successExitStatus = false;
                            err.println("Exception while executing ansible playbook (will still try other ansible playbooks, if any, but exit status will indicate failure). Exception:");
                            e.printStackTrace();
                        }

                        playbookIndex++;
                    }
                }
            } finally {
                callLogOutput.close();

                if (cli.printCalls) {
                    Thread.yield();
                    try {
                        Thread.sleep(500L);
                    } catch (InterruptedException e) {
                        LOG.error("Unexpected InterruptedException", e);
                    }
                }
            }

            if (cli.debug)
                out.println("All done. Exiting.");

            if (successExitStatus)
                return 0;
            else
                return 2;

        } catch (ExperimenterCommandLineParser.CLIParseException e) {
            if (e.isError())
                return 3;
            else
                return 0;
        } catch (CLIException e) {
            if (e.isError())
                return 3;
            else
                return 0;
        }
    }

    private static void callAnsible(String ansiblePlaybookExe, File ansiblePlaybook, File ansibleDir, boolean ansibleDebug, File outputFile) throws IOException {
        PrintWriter pw = new PrintWriter(new FileWriter(outputFile));
        ArrayList<String> command = new ArrayList<>();
        assert ansiblePlaybookExe != null;
        String[] ansiblePlaybookExeParts = ansiblePlaybookExe.split(" ");
        Collections.addAll(command, ansiblePlaybookExeParts);
        command.add(ansiblePlaybook.getAbsolutePath());
        if (ansibleDebug)
            command.add("-vvvv");
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(ansibleDir);
        pb.redirectErrorStream(); //combine stderr and stout, so we can read both at once in one thread. Makes everything easy.

        Process p = pb.start();
        BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream()));

//        String output = "";
        String line;
        while ((line = input.readLine()) != null) {
//            output += line + System.lineSeparator();
            pw.println(line);
        }
        //note: input might close before terminal stops.
        input.close();

        try {
            p.waitFor();
        } catch (InterruptedException e) {
            //ignore
        }

        pw.close();
    }


    private static void deleteAll(List<AggregateManagerWrapper> amWrappers, boolean silent,
                                  PrintStream out, PrintStream err, GeniUrn usedSliceUrn,
                                  List<AnyCredential> sliceCredentials, Logger logger,
                                  UserAndSliceApiWrapper sa) {
        if (!silent)
            err.println("Failure: will delete all slivers, on "+amWrappers+" AMs.");

        for (AggregateManagerWrapper am : amWrappers) {
            Server currentAuthority = am.getAmServer();
            String rspecComponentManager = currentAuthority.getDefaultComponentManagerUrn();
            GeniUrn rspecComponentManagerUrn = currentAuthority.getDefaultComponentManagerAsGeniUrn();
            if (!silent)
                out.println("Contacting  " + rspecComponentManager + "...");

            try {
                am.deleteSliver(usedSliceUrn, sliceCredentials);

                if (!silent)
                    out.println("Slivers at  " + rspecComponentManager + " have been deleted.");

                try {
                    sa.unregisterAggregatesForSlice(logger, sliceCredentials, usedSliceUrn, rspecComponentManagerUrn, null/*sliverUrns*/);

                    if (!silent)
                        out.println("Slivers at  " + rspecComponentManager + " has been unregistered.");
                } catch (JFedException e) {
                    String message =
                            "Error unregistering sliver at " + rspecComponentManager + ". (this error will be ignored)";
                    if (e.getXmlRpcResult() != null)
                        err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                    else {
                        err.println(message + ":");
                        e.printStackTrace(err);
                    }
                }
            } catch (JFedException e) {
                String message =
                        "Error deleting sliver at " + rspecComponentManager + ". (this error will be ignored)";
                if (e.getXmlRpcResult() != null)
                    err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
                else {
                    err.println(message + ":");
                    e.printStackTrace(err);
                }
            }
        }
    }

    private static int callFailed(String message, JFedException e, PrintStream err, CallLogOutput callLogOutput) {
        Thread.yield();
        Thread.yield();
        callLogOutput.flush();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e1) {
            //ignored
        }
        Thread.yield();
        callLogOutput.flush();

        if (e == null)
            err.println(message);
        else {
            if (e.getXmlRpcResult() != null)
                err.println(message + ": " + "\"" + e.getXmlRpcResult().getResultXmlRpcString() + "\"");
            else {
                err.println(message + ":");
                e.printStackTrace(err);
            }
        }
        return 2;
    }

    private static void addToUserSpecs(List<UserSpec> userSpecs, GeniUrn userUrn, String sshKey) {
        assert userUrn != null;
        addToUserSpecs(userSpecs, userUrn, Collections.singletonList(sshKey));
    }
    private static void addToUserSpecs(List<UserSpec> userSpecs, String userUrnString, String sshKey) {
        addToUserSpecs(userSpecs, userUrnString, Collections.singletonList(sshKey));
    }
    private static void addToUserSpecs(List<UserSpec> userSpecs, GeniUrn userUrn, Collection<String> sshKeys) {
        assert userUrn != null;
        addToUserSpecs(userSpecs, userUrn.toString(), sshKeys);
    }
    private static void addToUserSpecs(List<UserSpec> userSpecs, String userUrnString, Collection<String> sshKeys) {
        if (sshKeys == null || sshKeys.isEmpty())
            return;
        for (UserSpec us : userSpecs) {
            if (Objects.equals(us.getUrn(), userUrnString)) {
                //TODO (low priority) make this work for SSH keys that differ only in comment, but not in actual key
                Set<String> newSskKeys = new HashSet<>(sshKeys);
                newSskKeys.removeAll(us.getSshKey());
                if (!newSskKeys.isEmpty())
                    us.getSshKey().addAll(newSskKeys);
                return;
            }
        }

        UserSpec newUs = new UserSpec(userUrnString, sshKeys);
        userSpecs.add(newUs);
    }

    private static class CallLogOutput {
        public FileOutputStream fos;
        public XMLEventWriter xtw;
        public XMLEventFactory eventFactory;
        public final PrintStream err;

        public CallLogOutput(PrintStream err) {
            this.err = err;
        }

        public void onFailure() {
            close();
            fos = null;
            xtw = null;
            eventFactory = null;
        }

        public void flush() {
            try {
                xtw.flush();
                fos.flush();
            } catch (Exception e) {
                LOG.warn("Unexpected error flusing", e);
            }
        }

        public boolean isDisabled() {
            return fos == null || xtw == null || eventFactory == null;
        }

        public void close() {
            if (xtw != null && eventFactory != null) {
                try {
                    xtw.add(eventFactory.createEndElement(new QName("calls"), null));
                    xtw.add(eventFactory.createEndDocument());
                    xtw.flush();
                } catch (XMLStreamException e) {
                    LOG.error("Exception while closing call log output", e);
                    err.println("Exception while closing call log output: " + e.getMessage());
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    LOG.error("Exception while closing call log output file", e);
                    err.println("Exception while closing call log output file: " + e.getMessage());
                }
            }
        }
    }

    public static List<String> extractCredentials(String multiCredentialString) {
        List<String> res = new ArrayList<>();

        //There can be multiple speaksfor credentials in one file.
        //We don't parse XML to detect them, we use some simple regex matching tricks to catch all(?) cases

        //NameSpaceStartChar	   ::=   	[A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
        //NameSpaceChar	   ::=   	NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]

        //<signed-credential or <[NameSpaceStartChar][NameSpaceChar]*:signed-credential
        //</signed-credential.*> or <[NameSpaceStartChar][NameSpaceChar]*:signed-credential.*>

        //TODO: add special chars

        String namespaceStartChar = "A-Za-z_\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; //java doesn't seem to like \x{10000}-\x{EFFFF}
        String namespaceChar = namespaceStartChar+".0-9\\xB7\\u0300-\\u036F\\u203F-\\u2040-";
        Pattern startCredentialPattern = Pattern.compile("<(["+namespaceStartChar+"]["+namespaceChar+"]*:)?signed-credential");
        Pattern endCredentialPattern = Pattern.compile("</(["+namespaceStartChar+"]["+namespaceChar+"]*:)?signed-credential[^>]*>");

        Matcher startMatcher = startCredentialPattern.matcher(multiCredentialString);
        Matcher endMatcher = endCredentialPattern.matcher(multiCredentialString);

        int i = 0;
        while (startMatcher.find(i) && endMatcher.find(startMatcher.end())) {
            String credentialString = multiCredentialString.substring(startMatcher.start(), endMatcher.end());
            res.add(credentialString);
            i = endMatcher.end();
        }

        return res;
    }

    private static ModelRspec getModelRspec(ModelRspec existingModel, StringRspec rspec) throws RspecParseException {
        if (existingModel != null)
            return existingModel;

        RequestRspecSource rspecSource = new RequestRspecSource(rspec, ModelRspecType.BASIC);
        ModelRspec modelRspec = rspecSource.getModelRspec();
        return modelRspec;

//        return new StaxRspecParser(rspec.getXmlString(), BasicRspecFactory.getInstance()).call();
    }

    private static class AutoShareLanInfo {
        private final GeniUrn componentManagerUrn;
        private final String lanClientId;
        private final String sharedLanName;

        public AutoShareLanInfo(GeniUrn componentManagerUrn, String lanClientId, String sharedLanName) {
            this.componentManagerUrn = componentManagerUrn;
            this.lanClientId = lanClientId;
            this.sharedLanName = sharedLanName;
        }
    }

    private static class AnsiblePlaybookInfo {
        public static class AnsiblePlaybookException extends Exception {
            public AnsiblePlaybookException(String message) {
                super(message);
            }

            public AnsiblePlaybookException(String message, Throwable cause) {
                super(message, cause);
            }
        }

        private final ExecuteAnsiblePlaybook executeAnsiblePlaybook;
        private String outputFilename;
        private String outputBasename;
        private String scriptContent;

        public AnsiblePlaybookInfo(ExecuteAnsiblePlaybook executeAnsiblePlaybook) {
            this.executeAnsiblePlaybook = executeAnsiblePlaybook;
            this.outputFilename = executeAnsiblePlaybook.getOutputFilename();
        }

        public void checkFilename(boolean mustBeBasename, boolean allowAnyOutputFile) throws AnsiblePlaybookException {
            if (outputFilename == null)
                return;

            boolean isBasename = outputFilename.indexOf(File.separatorChar) == -1;

            if (mustBeBasename && !isBasename)
                throw new AnsiblePlaybookException("Invalid Output filename: "+
                        RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart()+" "+
                        RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE.getLocalPart()+" \""+
                        outputFilename+"\" is not a base filename.");

            if (isBasename) {
                outputBasename = outputFilename;
                return;
            }

            File outputFile = new File(outputFilename);
            if (allowAnyOutputFile) {
                if (!outputFile.getParentFile().exists())
                    throw new AnsiblePlaybookException("Invalid Output filename: " +
                            RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart() + " " +
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE.getLocalPart() + " \"" +
                            outputFilename + "\" parent directory does not exist.");

                if (outputFile.exists() && !outputFile.isFile())
                    throw new AnsiblePlaybookException("Invalid Output filename: " +
                            RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart() + " " +
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_OUTPUT_FILE.getLocalPart() + " \"" +
                            outputFilename + "\" exists but is not a normal file.");
            } else {
                //else don't check, because we won't use it anyway.

                //overwrite outputFilename to be sure the full path is not used
                outputFilename = outputFile.getName();
            }

            outputBasename = outputFile.getName();
        }

        public void loadContent(boolean allowFile) throws AnsiblePlaybookException {
            if (executeAnsiblePlaybook.getSource() == null)
                throw new AnsiblePlaybookException("Invalid RSpec: "+
                        RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart()+" has no "+
                        RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart()+" attribute");

            if (executeAnsiblePlaybook.getSource().startsWith("http")) {
                URL url;
                try {
                    url = new URL(executeAnsiblePlaybook.getSource());
                } catch (MalformedURLException e) {
                    throw new AnsiblePlaybookException("Invalid "+
                            RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart()+" "+
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart()+
                            " url: \""+
                            executeAnsiblePlaybook.getSource()+
                            "\"", e);
                }
                try {
                    this.scriptContent = IOUtils.urlToString(url, Charset.defaultCharset());
                } catch (IOException e) {
                    throw new AnsiblePlaybookException("Error reading URL in "+
                            RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart()+" "+
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart()+
                            " attribute: \""+
                            executeAnsiblePlaybook.getSource()+
                            "\": "+e.getMessage(), e);
                }
            } else {
                if (!allowFile)
                    throw new AnsiblePlaybookException(""+
                            RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart()+" "+
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart()+
                            " is \""+
                            executeAnsiblePlaybook.getSource()+
                            "\" but the --ansible-allow-playbook-inputfile option is not set, " +
                            "so only URLs are allowed.");

                File localFile = new File(executeAnsiblePlaybook.getSource());

                if (!localFile.exists())
                    throw new AnsiblePlaybookException("Bad "+
                            RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart()+" "+
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart()+
                            " filename: \""+
                            executeAnsiblePlaybook.getSource()+
                            "\" does not exist");

                try {
                    this.scriptContent = IOUtils.fileToString(localFile);
                } catch (IOException e) {
                    throw new AnsiblePlaybookException("Error reading "+
                            RspecXmlConstants.Q_JFED_EXECUTE_ANSIBLE_PLAYBOOK.getLocalPart()+" "+
                            RspecXmlConstants.Q_ATT_JFED_EXECUTE_ANSIBLE_PLAYBOOK_SOURCE.getLocalPart()+
                            " file: \""+
                            executeAnsiblePlaybook.getSource()+
                            "\": "+e.getMessage(), e);
                }
            }
        }

        public ExecuteAnsiblePlaybook getExecuteAnsiblePlaybook() {
            return executeAnsiblePlaybook;
        }

        public String getOutputFilename() {
            return outputFilename;
        }

        public String getScriptContent() {
            return scriptContent;
        }

        public String getOutputBasename() {
            return outputBasename;
        }
    }

    private static boolean isBefore(Date a, Date b) {
        Date aa = new Date(a.getTime() + 999L);
        return aa.before(b);
    }


    private static GeniUrn verifyBindRspecUrn(ExperimenterCommandLineParser cli, PrintStream out, PrintStream err, AuthorityFinder authorityFinder) throws CLIException {
        if (cli.bindRspecArg != null) {
            GeniUrn bindRspecUrn = GeniUrn.parse(cli.bindRspecArg);
            if (bindRspecUrn == null) {
                err.println("Error parsing provided component_manager_id URN: " + cli.bindRspecArg);
                throw new CLIException(true, "Argument error");
            }
            Server bindRspecAuth = authorityFinder.findByUrn(bindRspecUrn, AuthorityFinder.Purpose.REQUEST_RSPEC);
            if (bindRspecAuth == null) {
                err.println("Error: could not find server matching component_manager_id URN provided by --bind-rspec: " + cli.bindRspecArg);
                throw new CLIException(true, "Unknown server");
            }
            return bindRspecUrn;
        }
        return null;
    }

    private static CallLogOutput initCallLogOutput(ExperimenterCommandLineParser cli, PrintStream out, PrintStream err, Logger logger) throws CLIException {
        CallLogOutput callLogOutput = new CallLogOutput(err);
        if (cli.callLogArg != null) {
            try {
                callLogOutput.fos = new FileOutputStream(cli.callLogArg);
            } catch (FileNotFoundException e) {
                throw new CLIException(true, "Failed to open call log file \""+cli.callLogArg+"\"", e);
            }

            try {
                XMLOutputFactory xof = XMLOutputFactory.newInstance();
                callLogOutput.xtw = new IndentingXMLEventWriter(xof.createXMLEventWriter(callLogOutput.fos, "UTF-8"));
                callLogOutput.eventFactory = XMLEventFactory.newInstance();

                callLogOutput.xtw.add(callLogOutput.eventFactory.createStartDocument());
                callLogOutput.xtw.add(callLogOutput.eventFactory.createStartElement(new QName("calls"), null, null));
            } catch (XMLStreamException e) {
                callLogOutput.onFailure();
                LOG.error("Exception generating XML for call output", e);
                err.println("Exception generating XML for call output: " + e.getMessage());
            }

            logger.addResultListener(result -> {
                if (callLogOutput.isDisabled())
                    return;

                SerializableApiCallDetails serializableApiCallDetails =
                        new SerializableApiCallDetailsFactory()
                                .createFromApiCallDetails(result);

                try {
                    if (result.getAuthority() != null)
                        callLogOutput.xtw.add(callLogOutput.eventFactory.createComment("" + result.getGeniMethodName() + " on " + result.getAuthority().getDefaultComponentManagerUrn() + ""));

                    SerializableApiCallDetailsWriter.writeApiCallDetails(serializableApiCallDetails, callLogOutput.xtw, callLogOutput.eventFactory);

                    //add 2 newlines
                    callLogOutput.xtw.add(callLogOutput.eventFactory.createCharacters(System.getProperty("line.separator") + System.getProperty("line.separator")));

                    callLogOutput.xtw.flush();
                    callLogOutput.fos.flush();
                } catch (IOException e) {
                    err.println("Error flushing: " + e.getMessage());
                    e.printStackTrace();
                } catch (XMLStreamException e) {
                    err.println("Error writing call to stream: " + e.getMessage());
                    e.printStackTrace();

                    callLogOutput.onFailure();
                }
            });
        } else {
            callLogOutput.fos = null;
            callLogOutput.xtw = null;
            callLogOutput.eventFactory = null;
        }
        if (cli.printCalls) {
            PrintStream printCallsOut = out;
            final String printCallsPrefix = "CALL: ";

            logger.addResultListener(result -> {
                printCallsOut.println();
                printCallsOut.println(TextUtil.indent(printCallsPrefix.length()+1, printCallsPrefix + "" + result.getGeniMethodName() +
                        " on " + result.getAuthority().getDefaultComponentManagerUrn() + "" +
                        " at "+result.getConnectionConfig().getServerUrlString()));

                String req = result.getXmlRpcRequestJsonString();
                String res = result.getXmlRpcResponseJsonString();
                if (req != null) printCallsOut.println(printCallsPrefix + "Request: " + req);
                if (res != null) printCallsOut.println(printCallsPrefix + "Reply: " + res);

                printCallsOut.println();
                printCallsOut.println();
                printCallsOut.flush();
            });
        }

        return callLogOutput;
    }

    private static Triplet<StringRspec, ModelRspec, List<String>> initRspec(ExperimenterCommandLineParser cli, PrintStream out, PrintStream err, GeniUrn bindRspecUrn) throws CLIException {
        String rspecString;
        StringRspec rspec = null;
        ModelRspec modelRspec = null;
        List<String> rspecComponentManagers = null;
        if (cli.rspecArg != null) {
            assert cli.rspecArg != null;
            if (cli.rspecArg.startsWith("http")) {
                URL url;
                try {
                    url = new URL(cli.rspecArg);
                } catch (MalformedURLException e) {
                    LOG.debug("The URL specified in the --rspec argument, \"" + cli.rspecArg + "\", is not valid", e);
                    err.println("The URL specified in the --rspec argument, \"" + cli.rspecArg + "\", is not valid: " + e.getMessage());
                    throw new CLIException(true, "Invalid RSpec URL", e);
                }
                try {
                    rspecString = IOUtils.urlToString(url, Charset.defaultCharset());
                } catch (IOException e) {
                    LOG.debug("Exception reading the URL specified in the --rspec argument, \"" + cli.rspecArg + "\"", e);
                    err.println("There was an error reading the URL specified in the --rspec argument, \"" + cli.rspecArg + "\": " + e.getMessage());
                    throw new CLIException(true, "Error downloading RSpec", e);
                }
            } else
                try {
                    rspecString = IOUtils.fileToString(cli.rspecArg);
                } catch (IOException e) {
                    err.println("The RSpec file \"" + cli.rspecArg + "\" was not found.");
                    throw new CLIException(true, "Error opening RSpec file", e);
                }
            rspec = new StringRspec(rspecString);

            if (cli.rewriteRspecArg) {
                try {
                    modelRspec = getModelRspec(modelRspec, rspec);

                    if (bindRspecUrn != null) {
                        int bindCount = 0;
                        for (RspecNode rspecNode : modelRspec.getNodes()) {
                            if (rspecNode.getComponentManagerId() == null) {
                                rspecNode.setComponentManagerId(bindRspecUrn);
                                bindCount++;
                            }
                        }
                        int nodeCount = modelRspec.getNodes().size();
                        if (bindCount == 0) {
                            if (!cli.silent)
                                err.println("WARNING: --bind-rspec was specified, but the RSpec did NOT contain any unbound nodes (0 nodes of "+nodeCount+" nodes have been bound).");
                        } else {
                            if (cli.debug)
                                out.println("--bind-rspec was specified: "+bindCount+" nodes of "+nodeCount+" nodes have been bound to "+bindRspecUrn);
                        }
                    }

                    RequestRspecSource rspecSource2 = new RequestRspecSource(modelRspec);
                    rspecString = rspecSource2.getRspecXmlString();
                    rspec = rspecSource2.getStringRspec();
                } catch (AssertionError | Exception t) {
                    LOG.error("Error rewriting RSpec", t);
                    err.println("Error parsing and rewriting RSpec, will use original RSpec! Error message: " + t.getMessage());
                }
            }


            rspecComponentManagers = rspec.getAllComponentManagerUrns();
            if (rspecComponentManagers.size() != 1 || cli.debug) {
                if (!cli.silent)
                    out.println("Note: This RSpec has " + rspecComponentManagers.size() + " component managers: " + rspecComponentManagers);
            }
            if (rspecComponentManagers.size() == 0) {
                if (cli.isCommand(CREATE)) {
                    err.println("ERROR: The provided RSpec has no component managers. Something went wrong during parsing, or this is an empty RSpec. Either way, 'createSliver' cannot continue.");
                    throw new CLIException(true, "RSpec error");
                }
                else
                    err.println("WARNING: The provided RSpec has no component managers. Something went wrong during parsing, or this is an empty RSpec. Either way, few commands can now meaningfully continue. Will try anyway.");
            }
        } else {
            //no RSpec provided, get rspecComponentManagers from SLIVER_INFO (or equivalent)
            //this is done below, after user info is retrieved
        }

        return new Triplet(rspec, modelRspec, rspecComponentManagers);
    }

    private static Object helperMethod3(ExperimenterCommandLineParser cli, PrintStream out, PrintStream err) throws CLIException {
        return null;
    }

    private static Object helperMethod4(ExperimenterCommandLineParser cli, PrintStream out, PrintStream err) throws CLIException {
        return null;
    }

    private static Object helperMethod5(ExperimenterCommandLineParser cli, PrintStream out, PrintStream err) throws CLIException {
        return null;
    }

    public static class CLIException extends Exception {
        private final boolean error;

        public CLIException(boolean error) { this.error = error; }
        public CLIException(boolean error, String message) { super(message); this.error = error; }
        public CLIException(boolean error, String message, Throwable cause) { super(message, cause); this.error = error; }
        public CLIException(boolean error, Throwable cause) { super(cause); this.error = error; }
        public CLIException(boolean error, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
            this.error = error;
        }

        public boolean isError() {
            return error;
        }
    }


    public static class Pair<T1, T2> {
        public final T1 first;
        public final T2 second;

        public Pair(T1 first, T2 second) {
            this.first = first;
            this.second = second;
        }
    }
    public static class Triplet<T1, T2, T3> {
        public final T1 first;
        public final T2 second;
        public final T3 third;

        public Triplet(T1 first, T2 second, T3 third) {
            this.first = first;
            this.second = second;
            this.third = third;
        }
    }
}
