Option to import backup instead of setting up from scratch on new install (#44)

This commit is contained in:
Arjan H
2022-04-29 19:23:15 +02:00
parent 286a7667a1
commit 6a67044372
4 changed files with 159 additions and 9 deletions

View File

@@ -203,7 +203,7 @@ case $txt in
;;
"backup-restore")
read backup
/labca/restore $backup
/labca/restore "$backup"
;;
"server-restart")
cd /boulder

View File

@@ -1610,9 +1610,15 @@ func _progress(stage string) int {
func _helptext(stage string) template.HTML {
if stage == "register" {
return template.HTML(fmt.Sprint("<p>You need to create an admin account for managing this instance of\n",
"LabCA. There can only be one admin account, but you can configure all its attributes once the\n",
"initial setup has completed.</p>"))
return template.HTML(fmt.Sprint("<p class=\"form-register\">You need to create an admin account for\n",
"managing this instance of LabCA. There can only be one admin account, but you can configure all\n",
"its attributes once the initial setup has completed.<br><br>Instead, you can also\n",
"<a href=\"#\" onclick=\"false\" class=\"toggle-restore\">restore from a backup file</a> of a\n",
"previous LabCA installation.</p>\n",
"<p class=\"form-restore\">If you have a backup file from a previous LabCA installation and want to\n",
"restore this instance with the exact same configuration, use that backup file here.\n",
"<br><br>Otherwise you should follow the <a href=\"#\" onclick=\"false\"\n",
"class=\"toggle-register\">standard setup</a>.</p>"))
} else if stage == "setup" {
return template.HTML(fmt.Sprint("<p>The fully qualified domain name (FQDN) is what end users will use\n",
"to connect to this server. It was provided in the initial setup and is shown here for reference.</p>\n",
@@ -1652,11 +1658,114 @@ func _setupAdminUser(w http.ResponseWriter, r *http.Request) bool {
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
} else if r.Method == "POST" {
if err := r.ParseForm(); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
isMultipart := true
if err := r.ParseMultipartForm(1 * 1024 * 1024); err != nil {
isMultipart = false
if err != http.ErrNotMultipart {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
} else if err := r.ParseForm(); err != nil {
errorHandler(w, r, err, http.StatusInternalServerError)
return false
}
}
// Restore a backup file
if isMultipart {
reg := &User{
Errors: make(map[string]string),
RequestBase: r.Header.Get("X-Request-Base"),
}
file, header, err := r.FormFile("file")
if err != nil {
fmt.Println(err)
reg.Errors["File"] = "Could not read uploaded file"
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
defer file.Close()
out, err := os.Create("/backup/" + header.Filename)
if err != nil {
fmt.Println(err)
reg.Errors["File"] = "Could not create local file"
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
defer out.Close()
_, copyError := io.Copy(out, file)
if copyError != nil {
fmt.Println(err)
reg.Errors["File"] = "Could not store uploaded file"
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
// Cannot use _hostCommand() as we need different error handling
conn, err := net.Dial("tcp", "control:3030")
if err != nil {
fmt.Println(err)
reg.Errors["File"] = "Could not import backup file: error communicating with control"
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
defer conn.Close()
fmt.Fprintf(conn, "backup-restore\n"+header.Filename+"\n")
reader := bufio.NewReader(conn)
message, err := ioutil.ReadAll(reader)
if err != nil {
fmt.Println(err)
reg.Errors["File"] = "Could not import backup file: error reading control response"
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
if strings.Compare(string(message), "ok\n") == 0 {
if err := viper.ReadInConfig(); err != nil {
fmt.Println(err)
reg.Errors["File"] = "Could not read config after importing backup"
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
viper.Set("config.complete", false)
viper.WriteConfig()
err = _applyConfig()
if err != nil {
fmt.Println(err)
reg.Errors["File"] = "Could not apply config after importing backup"
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
defer _hostCommand(w, r, "docker-restart")
http.Redirect(w, r, r.Header.Get("X-Request-Base")+"/final", http.StatusFound)
return true
}
if len(message) >= 4 {
tail := message[len(message)-4:]
if strings.Compare(string(tail), "\nok\n") == 0 {
msg := message[0 : len(message)-4]
log.Printf("Message from server: '%s'", msg)
lines := strings.Split(strings.TrimSpace(string(msg)), "\n")
reg.Errors["File"] = "Could not import backup file: " + lines[0]
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
}
log.Printf("ERROR: Message from server: '%s'", message)
lines := strings.Split(strings.TrimSpace(string(message)), "\n")
reg.Errors["File"] = "Could not import backup file: " + lines[0]
render(w, r, "register:manage", map[string]interface{}{"User": reg, "IsLogin": true, "Progress": _progress("register"), "HelpText": _helptext("register")})
return false
}
// Regular setup form handling
reg := &User{
Name: r.Form.Get("username"),
Email: r.Form.Get("email"),

View File

@@ -1,10 +1,11 @@
{{ define "body" }}
</div>
<div class="col-md-6 col-sm-12">
<h3>Create admin account</h3>
<h3 class="form-register">Create admin account</h3>
<h3 class="form-restore" style="display: none;">Restore LabCA</h3>
{{with .User}}
<form role="form" action="{{ .RequestBase }}/setup" method="POST">
<form role="form" action="{{ .RequestBase }}/setup" method="POST" class="form-register">
<div class="form-group">
<label>User name:</label>
<input class="form-control non-fluid" type="text" name="username" id="username" value="{{ .Name }}" required>
@@ -42,6 +43,20 @@
<input class="btn btn-default" type="submit" value="Create">
</div>
</form>
<form role="form" action="{{ .RequestBase }}/setup" method="POST" class="form-restore" style="display: none;" enctype="multipart/form-data">
<div class="form-group">
<label for="file">File to restore:</label>
<input class="form-control non-fluid" type="file" id="file" name="file" required>
{{ with .Errors.File }}
<span class="error" id="file-error">{{ . }}</span>
{{ end }}
</div>
<div class="form-group">
<input class="btn btn-default" type="submit" value="Restore">
</div>
</form>
<p id="processing" class="hidden"><br/>Applying configuration...<br/>
<img id="restart-spinner" src="static/img/spinner.gif" height="36"></p>
{{end}}
{{ template "partials/progress.tmpl" . }}
{{end}}
@@ -53,6 +68,30 @@
$(function() {
pwduxInit('#password-strength', '#password');
pwduxHandlers('#password-strength', '#password', ['#username', '#email']);
if ($("#file-error").text() == "") {
$(".form-register").show();
$(".form-restore").hide();
} else {
$(".form-restore").show();
$(".form-register").hide();
}
$(".toggle-restore").click(function() {
$(".form-restore").show();
$(".form-register").hide();
positionFooter();
});
$(".toggle-register").click(function() {
$(".form-register").show();
$(".form-restore").hide();
positionFooter();
});
$(".form-restore").submit(function() {
$("#processing").removeClass("hidden");
});
});
</script>
{{ end }}

View File

@@ -11,13 +11,15 @@ BASE=$(echo "$FILE" | perl -p0e "s/.*\/(.*).tgz/\1/")
TMPDIR=/tmp/$BASE
cd /tmp
tar xzf $FILE
tar xzf $FILE 2>&1
cd /boulder
[ -f $TMPDIR/boulder_sa_integration.sql ] || (echo "MySQL backup file not found"; exit 1)
docker-compose exec -T bmysql mysql boulder_sa_integration <$TMPDIR/boulder_sa_integration.sql
mv -f $TMPDIR/*key* $TMPDIR/*cert.pem $TMPDIR/*.csr /etc/nginx/ssl/
[ -d $TMPDIR/data ] || (echo "Data folder backup not found"; exit 1)
rm -rf /admin/data && mv $TMPDIR/data /admin/
rm -rf $TMPDIR