Skip to content

Commit

Permalink
Initial support for parsing a provision file.
Browse files Browse the repository at this point in the history
  • Loading branch information
tresf committed Oct 13, 2023
1 parent 2605e2a commit 697cef3
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 0 deletions.
81 changes: 81 additions & 0 deletions src/qz/installer/provision/Provisioner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package qz.installer.provision;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

import static qz.installer.provision.Step.*;

public class Provisioner {
protected static final Logger log = LogManager.getLogger(Provisioner.class);
ArrayList<Step> steps;

public Provisioner(InputStream in) throws IOException, JSONException {
this(IOUtils.toString(in, StandardCharsets.UTF_8));
}

public Provisioner(File jsonFile) throws IOException, JSONException {
this(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8));
}

public Provisioner(String jsonData) throws JSONException {
steps = parse(jsonData);
}

public void invoke() {
for(Step step : steps) {
try {
step.invoke();
} catch(Exception e) {
log.error("Provisioning step failed: {}", step, e);
}
}
}

private ArrayList<Step> parse(String jsonData) throws JSONException {
JSONArray jsonArray = new JSONArray(jsonData);

ArrayList<Step> steps = new ArrayList<>();
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject step = jsonArray.getJSONObject(i);

Type type = Type.parse(step.optString("type", null));
String data = step.optString("data", null);

Os os = null;
if(step.has("os")) {
String osString = step.optString("os", "all");
os = Os.parse(osString);
if(os == null) {
log.warn("Os provided \"{}\" could not be parsed", osString);
}
}

Phase phase = null;
if(step.has("phase")) {
String phaseString = step.optString("phase", null);
phase = Phase.parse(phaseString);
if(phase == null) {
log.warn("Phase provided \"{}\" could not be parsed", phaseString);
}
}

try {
steps.add(new Step(type, os, phase, data));
} catch(Exception e) {
log.warn("Unable to add Step{}", step, e);
}
}
return steps;
}
}
198 changes: 198 additions & 0 deletions src/qz/installer/provision/Step.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package qz.installer.provision;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.utils.FileUtilities;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;

import java.io.File;
import java.util.ArrayList;
import java.util.EnumSet;

import static qz.utils.ArgParser.ExitStatus.*;

public class Step {
public enum Type {
SCRIPT,
CERT,
PROPERTY,
PREFERENCE;

public static Type parse(String input) {
return Step.parse(Type.class, input);
}
}

public enum Os {
WINDOWS,
MAC,
LINUX,
ALL;

public static Os parse(String input) {
// Handle ALL="*"
if(input != null && input.equals("*")) {
return ALL;
}
return Step.parse(Os.class, input);
}
}

public enum Phase {
INSTALL,
STARTUP;

public static Phase parse(String input) {
return Step.parse(Phase.class, input);
}
}

public enum Script {
PS1,
BAT,
SH,
PY,
RB;

public static Script parse(String input) {
if(input != null) {
if(input.contains(".")) {
String extension = input.substring(input.lastIndexOf(".") + 1);
return Step.parse(Script.class, input);
} else {
// If no file extension, assume a shell script
return SH;
}

}
return null;
}

/**
* Returns the interpreter command (and if needed, arguments) to invoke the script file
*
* An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3
* which will allow the OS to select the correct interpreter for the given file
*
* No special attention is given to "shebang", behavior may differ between OSs
*/
public ArrayList<String> getInterpreter(Os os) {
ArrayList<String> interpreter = new ArrayList<>();
switch(this) {
case PS1:
interpreter.add(os == Os.WINDOWS ? "powershell.exe" : "pwsh");
interpreter.add("-File");
break;
case PY:
interpreter.add(os == Os.WINDOWS ? "python3.exe" : "python3");
break;
case BAT:
interpreter.add(os == Os.WINDOWS ? "cmd.exe" : "wineconsole");
break;
case RB:
interpreter.add(os == Os.WINDOWS ? "ruby.exe" : "ruby");
break;
case SH:
default:
}
return interpreter;
}
}


protected static final Logger log = LogManager.getLogger(Step.class);

private Type type;
private Os os;
private Phase phase;
private String data;

public Step(Type type, Os os, Phase phase, String data) {
this.type = type;
this.os = os;
this.phase = phase;
this.data = data;

// Curate inputs
if(type == null || data == null) {
throw new UnsupportedOperationException("Type and Data cannot be null");
}

// Handle nulls
if(phase == null) {
this.phase = type == Type.PREFERENCE ? Phase.STARTUP : Phase.INSTALL;
log.info("Phase is null, defaulting to {} based on Type {}", this.phase, this.type);
}
if(os == null) {
this.os = Os.ALL;
log.info("Os {} is null, defaulting to ", this.os);
}

// Override conflicts
if (type == Type.PROPERTY || type == Type.CERT) {
if (phase == Phase.STARTUP) {
this.phase = Phase.INSTALL;
log.warn("Phase {} is unsupported for {}, defaulting to ", phase, type, this.phase);
}
}
if (type == Type.PREFERENCE) {
if (phase == Phase.INSTALL) {
this.phase = Phase.STARTUP;
log.warn("Phase {} is unsupported for {}, defaulting to ", phase, type, this.phase);
}
}
}

public boolean invoke() throws Exception {
switch(type) {
case CERT:
File cert = normalizePath(this.data);
return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS;
case SCRIPT:
File file = normalizePath(this.data);
Script engine = Script.parse(this.data);
ArrayList<String> command = engine.getInterpreter(this.os);
command.add(file.toString());
return ShellUtilities.execute(command.toArray(new String[command.size()]));
case PROPERTY:
// TODO: How should we pass this parameter in?
case PREFERENCE:
// TODO: Calculate this
default:
throw new UnsupportedOperationException("Type " + type + " is not yet supported.");
}
}

@Override
public String toString() {
return "Step{" +
"type=" + type +
", os=" + os +
", phase=" + phase +
", data='" + data + '\'' +
'}';
}

/**
* Basic enum parser
*/
private static <T extends Enum<T>> T parse(Class<T> clazz, String s) {
for(T en : EnumSet.allOf(clazz)){
if(en.name().equalsIgnoreCase(s)){
return en;
}
}
return null;
}

private File normalizePath(String relative) {
if(this.type == Type.CERT || this.type == Type.SCRIPT) {
File normalized = SystemUtilities.getJarParentPath(".").resolve(relative).toFile();
log.info("Normalizing path {} to {}", relative, normalized);
return normalized;
}
throw new UnsupportedOperationException("Path conversion for Type " + this.type + " not yet supported");
}
}
19 changes: 19 additions & 0 deletions test/qz/installer/provision/ProvisionerTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package qz.installer.provision;

import org.codehaus.jettison.json.JSONException;

import java.io.IOException;
import java.io.InputStream;

public class ProvisionerTests {

public static void main(String ... args) throws JSONException, IOException {
InputStream in = ProvisionerTests.class.getResourceAsStream("resources/provision.json");

// Parse the JSON
Provisioner provisioner = new Provisioner(in);

// Invoke all parsed steps
provisioner.invoke();
}
}
32 changes: 32 additions & 0 deletions test/qz/installer/provision/resources/provision.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

[
{
"type": "script",
"os": "windows",
"phase": "install",
"data": "provision/script1.ps1"
},
{
"type": "cert",
"os": "*",
// "phase": "install" is assumed
"data": "provision/cert1.crt"
},
{
"type": "property",
"os": "*",
// "phase": "install" is assumed
"data": "wss.host=0.0.0.0"
},
{
"type": "preference",
"os": "*",
// "phase": "startup" is assumed
"data": "tray.notifications=true"
},
{ // Deliberately throws error
"os": "*",
// "phase": "startup" is assumed
"data": "tray.notifications=true"
}
]

0 comments on commit 697cef3

Please sign in to comment.