-- @@SHEBANG -- -*- Lua -*- -- gitano-setup -- -- Git (with) Augmented network operations -- Instance setup tool -- -- Copyright 2012-2017 Daniel Silverstone -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without -- modification, are permitted provided that the following conditions -- are met: -- 1. Redistributions of source code must retain the above copyright -- notice, this list of conditions and the following disclaimer. -- 2. Redistributions in binary form must reproduce the above copyright -- notice, this list of conditions and the following disclaimer in the -- documentation and/or other materials provided with the distribution. -- 3. Neither the name of the author nor the names of their contributors -- may be used to endorse or promote products derived from this software -- without specific prior written permission. -- -- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -- ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -- SUCH DAMAGE. -- -- -- @@GITANO_LUA_PATH local gitano = require "gitano" local pat = gitano.patterns local gall = require "gall" local luxio = require "luxio" local sio = require "luxio.simple" local clod = require "clod" -- @@GITANO_BIN_PATH -- @@GITANO_SHARE_PATH -- @@GITANO_I18N_PATH -- @@GITANO_PLUGIN_PATH local possible_answers = {...} if (possible_answers[1] == "--help" or possible_answers[1] == "-h" or possible_answers[1] == "--usage") then sio.stderr:write(gitano.i18n.expand("SETUP_USAGE")) return 1 end local conf = clod.parse("") gitano.log.set_prefix("gitano-setup") gitano.log.bump_level(gitano.log.level.CHAT) local force_batch = false for i = #possible_answers, 1, -1 do local answer_file = possible_answers[i] gitano.log.debug(gitano.i18n.expand("SETUP_DEBUG_PARSING_ANSWERS", { file=answer_file })) local file_content, file_name if answer_file == "-" then file_content = io.stdin:read "*a" file_name = "@stdin" force_batch = true else file_content = assert(io.open(answer_file, "r")):read "*a" file_name = "@" .. answer_file end local one_conf = assert(clod.parse(file_content, file_name)) gitano.log.debug(gitano.i18n.expand("SETUP_DEBUG_COMBINE_ANSWERS")) for k,v in one_conf:each() do gitano.log.ddebug(tostring(k) .. " = " .. tostring(v)) conf.settings[k] = v end end if force_batch then conf.settings["setup.batch"] = true end gitano.log.chat(gitano.i18n.expand("SETUP_WELCOME")) gitano.log.chat(gitano.i18n.expand("SETUP_DO_CHECKS")) -- Check that Supple functions properly local ok, msg = pcall(function() gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE")) local s = require 'supple' local ok, a, b, c = s.host.run("return ...", "test", 12, 'hello', true) if not ok then gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_UNABLE")) os.exit(1) end if a ~= 12 or b ~= "hello" or c ~= true then gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_VALUES")) os.exit(1) end local ok, v = s.host.run("local a = ...\nreturn a.banana", "test", {banana=14}) if not ok then gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_UNABLE")) os.exit(1) end if v ~= 14 then gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_SUPPLE_BIDIRECTIONAL")) os.exit(1) end end) if not ok then gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_FAILED")) gitano.log.chat("... " .. tostring(msg)) os.exit(1) end gitano.log.chat(gitano.i18n.expand("SETUP_CHECK_OK")) function get(key) return conf.settings[key] end function ask_for(key, prompt, default) local cur_value = conf.settings[key] or default gitano.log.ddebug("ask_for(", tostring(key), ", ", tostring(prompt), ", ", tostring(default), ") [", tostring(cur_value), " ]") if not conf.settings["setup.batch"] then local default_str = (cur_value == nil) and "" or " [" .. tostring(cur_value) .. "]" sio.stdout:write((prompt or key) .. default_str .. ": ") local new_value = sio.stdin:read("*l") if new_value and new_value ~= "" then cur_value = new_value end end gitano.log.info(gitano.i18n.expand("SETUP_SETTING_IS", { key = key, value = tostring(cur_value)})) conf.settings[key] = cur_value return cur_value end function look_for_path(path) local ret, stat = luxio.stat(path) if ret ~= 0 then return false, path .. ": " .. luxio.strerror(stat) end if luxio.S_ISDIR(stat.mode) == 0 then return false, gitano.i18n.expand("SETUP_NOT_A_DIR", { path=path }) end return true end function validate_path(path) local ok, msg = look_for_path(path) if not ok then error(msg, 2) end end function file_exists(path) local fh = io.open(tostring(path), "r") if not fh then return false end fh:close() return true end function validate_name(n, pattern) if not n:match(pattern) then error(gitano.i18n.expand("SETUP_ERROR_INVALID_NAME", { name=n }), 2) end end if conf.settings["setup.batch"] then gitano.log.info(gitano.i18n.expand("SETUP_BATCH_MODE")) else gitano.log.info(gitano.i18n.expand("SETUP_INTERACTIVE_MODE")) end gitano.log.chat(gitano.i18n.expand("SETUP_STEP_1")) validate_path(ask_for("paths.home", gitano.i18n.expand("SETUP_PATHS_HOME_INFO"), os.getenv "HOME")) ask_for("paths.ssh", gitano.i18n.expand("SETUP_PATHS_SSH_INFO"), get("paths.home") .. "/.ssh") local pubkey_path if look_for_path(get("paths.ssh")) then -- Try and find a pubkey to use for _, ktype in ipairs { "rsa", "ecdsa" } do local pk = get("paths.ssh") .. "/id_" .. ktype .. ".pub" if file_exists(pk) then pubkey_path = pk break end end end assert(file_exists(ask_for("paths.bypasskey", gitano.i18n.expand("SETUP_PATHS_BYPASSKEY_INFO"), pubkey_path)), gitano.i18n.expand("SETUP_PATHS_BYPASSKEY_NOT_FOUND")) assert(file_exists(ask_for("paths.pubkey", gitano.i18n.expand("SETUP_PATHS_PUBKEY_INFO"), get("paths.home") .. "/admin.pub")), gitano.i18n.expand("SETUP_PATHS_PUBKEY_NOT_FOUND")) ask_for("paths.repos", gitano.i18n.expand("SETUP_PATHS_REPOS_INFO"), get("paths.home") .. "/repos") validate_name(ask_for("admin.username", gitano.i18n.expand("SETUP_ADMIN_USERNAME_INFO"), "admin"), pat.VALID_USERNAME) ask_for("admin.realname", gitano.i18n.expand("SETUP_ADMIN_REALNAME_INFO"), "Administrator") ask_for("admin.email", gitano.i18n.expand("SETUP_ADMIN_EMAIL_INFO"), "admin@administrator.local") validate_name(ask_for("admin.keyname", gitano.i18n.expand("SETUP_ADMIN_KEYNAME_INFO"), "default"), pat.VALID_SSHKEYNAME) ask_for("site.name", gitano.i18n.expand("SETUP_SITE_NAME_INFO"), "a random Gitano instance") ask_for("log.prefix", gitano.i18n.expand("SETUP_LOG_PREFIX_INFO"), "gitano") ask_for("use.htpasswd", gitano.i18n.expand("SETUP_USE_HTPASSWD_INFO"), "no") ask_for("paths.skel", gitano.i18n.expand("SETUP_PATHS_SKEL_INFO"), gitano.config.share_path() .. "/skel/gitano-admin") gitano.log.chat(gitano.i18n.expand("SETUP_STEP_2")) gitano.log.info(gitano.i18n.expand("SETUP_PREP_SITE_CONFIG")) local completely_flat = {} local site_conf = clod.parse("") site_conf.settings["site_name"] = get "site.name" site_conf.settings["log.prefix"] = get "log.prefix" site_conf.settings["use_htpasswd"] = get "use.htpasswd" completely_flat["site.conf"] = site_conf:serialise() -- Acquire the contents of the skeleton gitano-admin repository gitano.log.info(gitano.i18n.expand("SETUP_ACQUIRE_SKEL")) local skel_path = get "paths.skel" local skel = assert(sio.opendir(skel_path)) local function acquire(dir, base, path) gitano.log.ddebug("Acquire skeleton in:", path) for ent in dir:iterate() do if not (ent == "." or ent == "..") then local entpath = path .. "/" .. ent local treeent = base .. ent if look_for_path(entpath) then local subdir = assert(sio.opendir(entpath)) acquire(subdir, treeent .. "/", entpath) subdir:close() else local fh = io.open(entpath, "r") completely_flat[treeent] = fh:read "*a" fh:close() end end end end acquire(skel, "", skel_path) skel:close() -- Now build the user files do gitano.log.info(gitano.i18n.expand("SETUP_PREP_BYPASS_USER", {user="gitano-bypass"})) local userpath = "users/gitano-bypass/user.conf" local keypath = "users/gitano-bypass/initial.key" local userconf = clod.parse("") userconf.settings.real_name = gitano.i18n.expand("SETUP_BYPASS_USER_REAL_NAME") userconf.settings.email_address = get("admin.email") completely_flat[userpath] = userconf:serialise() completely_flat[keypath] = assert(sio.open(get("paths.bypasskey"), "r")):read "*a" end do gitano.log.info(gitano.i18n.expand("SETUP_PREP_ADMIN_USER", {user=get("admin.username")})) local userpath = "users/" .. get("admin.username") .. "/user.conf" local keypath = "users/" .. get("admin.username") .. "/" .. get("admin.keyname") .. ".key" local userconf = clod.parse("") userconf.settings.real_name = get("admin.realname") userconf.settings.email_address = get("admin.email") completely_flat[userpath] = userconf:serialise() completely_flat[keypath] = assert(sio.open(get("paths.pubkey"), "r")):read "*a" end -- And now the gitano-admin group gitano.log.info(gitano.i18n.expand("SETUP_PREP_GITANO_ADMIN")) local groupconf = clod.parse("") groupconf.settings.description = "Gitano Instance Administrators" groupconf.settings["members.*"] = get("admin.username") completely_flat["groups/gitano-admin.conf"] = groupconf:serialise() gitano.log.chat(gitano.i18n.expand("SETUP_STEP_3")) gitano.log.info(gitano.i18n.expand("SETUP_MAKE_PATHS")) gitano.util.mkdir_p(get("paths.repos") .. "/.graveyard") gitano.util.mkdir_p(get "paths.ssh") assert(sio.chmod(get "paths.ssh", "0700")) gitano.config.repo_path(get "paths.repos") gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_REPO")) local raw_repo = assert(gall.repository.create(get("paths.repos") .. "/gitano-admin.git")) gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_FLAT_TREE")) for k, v in pairs(completely_flat) do gitano.log.debug(" => Make object", k) completely_flat[k] = gall.object.create(raw_repo, "blob", v) end gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_COMMIT")) local real_tree = assert(gall.tree.create(raw_repo, completely_flat)) local person = { realname = get "admin.realname", email = get "admin.email", } local commit_data = { author = person, committer = person, tree = real_tree, message = "Initial setup", } local commit_obj = assert(gall.commit.create(raw_repo, commit_data)) gitano.log.info(gitano.i18n.expand("SETUP_PREPARE_MASTER")) assert(raw_repo:update_ref("refs/heads/master", commit_obj.sha, "Create initial master ref")) gitano.log.info(gitano.i18n.expand("SETUP_CHECK_ADMIN_REPO")) local admin_head = raw_repo:get(raw_repo.HEAD) if not admin_head then gitano.log.fatal(gitano.i18n.expand("ERROR_BAD_ADMIN_REPO")); end local config = assert(gitano.config.parse(admin_head)) -- Restore the prefix for our logging gitano.log.set_prefix("gitano-setup") -- Verify that our user exists assert(config.users[get "admin.username"], gitano.i18n.expand("SETUP_ERROR_NO_USER")) assert(config.groups["gitano-admin"].filtered_members[get "admin.username"], gitano.i18n.expand("SETUP_ERROR_NOT_ADMIN")) gitano.log.info(gitano.i18n.expand("SETUP_ADMIN_CONFIG")) config.repo:conf_set_and_save("project.description", "Instance administration repository") config.repo:conf_set_and_save("project.owner", get "admin.username") gitano.log.info(gitano.i18n.expand("SETUP_WRITE_SSHKEYS")) gitano.config.writessh(config, get("paths.ssh") .. "/authorized_keys") assert(sio.chmod(get("paths.ssh") .. "/authorized_keys", "0600")) gitano.log.info(gitano.i18n.expand("SETUP_COMPLETED"))