def write_instance(instance_configuration): """Write a new or updated instance""" instance_name = instance_configuration["name"] instance_file = os.path.join( get_etc_instance_path(), instance_name + ".conf" ) instance_directory = os.path.join(get_etc_instance_path(), instance_name) try: log("Configuration:", instance_configuration, pretty=True, lvl=debug) with open(instance_file, "w") as f: f.write(dumps(instance_configuration)) log("Instance configuration stored.", lvl=debug) if not os.path.exists(instance_directory): os.mkdir(instance_directory) log("Instance configuration directory created.", lvl=debug) except PermissionError: log( "PermissionError: Could not write instance management configuration " "file or create instance configuration directory.", lvl=error, ) abort(EXIT_NO_PERMISSION)
def _get_configuration(ctx): try: log("Configuration:", ctx.obj["config"], lvl=verbose, pretty=True) log("Instance:", ctx.obj["instance"], lvl=debug) except KeyError: log("Invalid configuration, stopping.", lvl=error) abort(EXIT_INVALID_CONFIGURATION) try: instance_configuration = ctx.obj["instances"][ctx.obj["instance"]] log("Instance Configuration:", instance_configuration, lvl=verbose, pretty=True) except NonExistentKey: log("Instance %s does not exist" % ctx.obj["instance"], lvl=warn) abort(EXIT_INSTANCE_UNKNOWN) return environment_name = instance_configuration["environment"] environment_config = instance_configuration["environments"][ environment_name] ctx.obj["environment"] = environment_config ctx.obj["instance_configuration"] = instance_configuration
def _launch_service(ctx): """Actually enable and launch newly set up environment""" instance_name = ctx.obj["instance"] service_name = "isomer-" + instance_name + ".service" success, result = run_process("/", ["systemctl", "enable", service_name], sudo="root") if not success: log("Error activating service:", format_result(result), pretty=True, lvl=error) abort(5000) log("Launching service") success, result = run_process("/", ["systemctl", "start", service_name], sudo="root") if not success: log("Error activating service:", format_result(result), pretty=True, lvl=error) abort(5000) return True
def _get_credentials(username=None, password=None, dbhost=None): """Obtain user credentials by arguments or asking the user""" # Database salt system_config = dbhost.objectmodels["systemconfig"].find_one( {"active": True}) try: salt = system_config.salt.encode("ascii") except (KeyError, AttributeError): log("No systemconfig or it is without a salt! " "Reinstall the system provisioning with" "iso install provisions -p system") abort(3) return if username is None: username = ask("Please enter username: "******"utf-8") except UnicodeDecodeError: password = password password_hash = bcrypt.hashpw(password, salt).decode('ascii') return username, password_hash
def remote(ctx, name, install, platform, source, url, existing): """Remote instance control (Work in Progress!)""" ctx.obj["remote"] = name ctx.obj["platform"] = platform ctx.obj["source"] = source ctx.obj["url"] = url ctx.obj["existing"] = existing if ctx.invoked_subcommand == "add": return remotes = ctx.obj["remotes"] = load_remotes() if ctx.invoked_subcommand == "list": return # log('Remote configurations:', remotes, pretty=True) host_config = remotes.get(name, None) if host_config is None: log("Cannot proceed, remote unknown", lvl=error) abort(5000) ctx.obj["host_config"] = host_config if platform is None: platform = ctx.obj["host_config"].get("platform", "debian") ctx.obj["platform"] = platform spur_config = dict(host_config["login"]) if spur_config["private_key_file"] == "": spur_config.pop("private_key_file") if spur_config["port"] != 22: log( "Warning! Using any port other than 22 is not supported right now.", lvl=warn, ) spur_config.pop("port") shell = spur.SshShell(**spur_config) if install: success, result = run_process("/", ["iso", "info"], shell) if success: log("Isomer version on remote:", format_result(result)) else: log('Use "remote install" for now') # if existing is None: # get_isomer(source, url, '/root', shell=shell) # destination = '/' + host_config['login']['username'] + '/repository' # else: # destination = existing # install_isomer(platform, host_config.get('use_sudo', True), shell, cwd=destination) ctx.obj["shell"] = shell
def main(): """Try to load the tool and launch it. If it can't be loaded, try to install all required things first.""" try: from isomer.tool.tool import isotool except ImportError as import_exception: print( "\033[1;33;41mIsomer startup error! Please check your Isomer/Python installation.\033[0m" ) print(type(import_exception), ":", import_exception, "\n") if not ask( "Maybe the dependencies not installed, do you want to try to install them", default=False, data_type="bool", show_hint=True, ): abort(EXIT_CANNOT_IMPORT_TOOL) install_isomer() print("Please restart the tool now.") sys.exit() isotool(obj={}, auto_envvar_prefix="ISOMER")
def rename(ctx, source, destination, keep, clear_target): """Rename Mongodb databases""" from pymongo import MongoClient client = MongoClient(ctx.obj["dbhost"]) if source not in client.list_database_names(): log("Source database", source, "does not exist!", lvl=warn) abort(-1) database = client.admin log("Copying", source, "to", destination) if destination in client.list_database_names(): log("Destination exists") if clear_target: log("Clearing") client.drop_database(destination) else: log("Not destroying existing data", lvl=warn) abort(-1) database.command("copydb", fromdb=source, todb=destination) if not keep: log("Deleting old database") client.drop_database(source) finish(ctx)
def install(ctx, **kwargs): """Install a new environment of an instance""" log("Installing instance") env = ctx.obj["instance_configuration"]["environments"] green = env["green"]["installed"] blue = env["blue"]["installed"] if green or blue: log( "At least one environment is installed or in a non-clear state.\n" "Please use 'iso instance upgrade' to upgrade an instance.", lvl=warn, ) abort(50081) _clear_instance(ctx, force=kwargs["force"], clear=False, no_archive=True) _install_environment(ctx, **kwargs) ctx.obj["instance_configuration"]["source"] = kwargs["source"] ctx.obj["instance_configuration"]["url"] = kwargs["url"] write_instance(ctx.obj["instance_configuration"]) _turnover(ctx, force=kwargs["force"]) finish(ctx)
def configure(ctx): """Generate a skeleton configuration for Isomer (needs sudo)""" if os.path.exists(get_etc_path()): abort(EXIT_NOT_OVERWRITING_CONFIGURATION) ctx = create_configuration(ctx) finish(ctx)
def check_root(): """Check if current user has root permissions""" if os.geteuid() != 0: log("If you installed into a virtual environment, don't forget to " "specify the interpreter binary for sudo, e.g:\n" "$ sudo /home/user/.virtualenv/isomer/bin/python3 iso") abort(EXIT_ROOT_REQUIRED)
def configure(ctx): """Generate a skeleton configuration for Isomer (needs sudo)""" if os.path.exists(os.path.join(get_etc_path(), "isomer.conf")): abort(EXIT_NOT_OVERWRITING_CONFIGURATION) ctx = create_configuration(ctx) write_configuration(ctx.obj['config']) finish(ctx)
def archive(ctx, force, dynamic): """Archive the specified or non-active environment""" result = _archive(ctx, force, dynamic) if result: log("Archived to '%s'" % result) finish(ctx) else: log("Could not archive.", lvl=error) abort(50060)
def write_remote(remote): """Write a new or updated remote""" filename = os.path.join(get_etc_remote_path(), remote["name"] + ".conf") try: with open(filename, "w") as f: f.write(dumps(remote)) log("Instance configuration stored.", lvl=debug) except PermissionError: log( "PermissionError: Could not write instance management configuration file", lvl=error, ) abort(EXIT_NO_PERMISSION)
def remove(ctx, clear, no_archive): """Irrevocably remove a whole instance""" if clear: log("Destructively removing instance:", ctx.obj["instance"], lvl=warn) if not ask("Are you sure", default=False, data_type="bool"): abort(EXIT_USER_BAILED_OUT) if clear: _clear_instance(ctx, force=True, clear=clear, no_archive=no_archive) remove_instance(ctx.obj["instance"]) finish(ctx)
def frontend(ctx, dev, rebuild, no_install, build_type): """Build and install frontend""" # TODO: Move this to the environment handling and deprecate it here installed = install_frontend( force_rebuild=rebuild, development=dev, install=not no_install, build_type=build_type, ) if installed is True: finish(ctx) else: abort(EXIT_FRONTEND_BUILD_FAILED, ctx)
def create(ctx, instance_name): """Create a new instance""" if instance_name == "": instance_name = ctx.obj["instance"] if instance_name in ctx.obj["instances"]: abort(EXIT_INSTANCE_EXISTS) log("Creating instance:", instance_name) instance_configuration = instance_template instance_configuration["name"] = instance_name ctx.obj["instances"][instance_name] = instance_configuration write_instance(instance_configuration) finish(ctx)
def update_service(ctx, next_environment): """Updates the specified service configuration""" validated, message = validate_services(ctx) if not validated: log("Service configuration validation failed:", message, lvl=error) abort(EXIT_SERVICE_INVALID) init = ctx.obj["config"]["meta"]["init"] environment_config = ctx.obj["instance_configuration"]["environments"][ next_environment] log("Updating %s configuration of instance %s to %s" % (init, ctx.obj["instance"], next_environment)) log("New environment:", environment_config, pretty=True) # TODO: Add update for systemd # * Stop possibly running service (it should not be running, though!) # * Actually update service files instance_name = ctx.obj["instance"] config = ctx.obj["instance_configuration"] env_path = "/var/lib/isomer/" + instance_name + "/" + next_environment log("Updating systemd service for %s (%s)" % (instance_name, next_environment)) launcher = os.path.join(env_path, "repository/iso") executable = os.path.join(env_path, "venv/bin/python3") + " " + launcher executable += " --quiet --instance " + instance_name + " launch" definitions = { "instance": instance_name, "executable": executable, "environment": next_environment, "user_name": config["user"], "user_group": config["group"], } service_name = "isomer-" + instance_name + ".service" write_template( service_template, os.path.join("/etc/systemd/system/", service_name), definitions, )
def cert(ctx, selfsigned): """instance a local SSL certificate""" instance_configuration = ctx.obj["instance_configuration"] instance_name = ctx.obj["instance"] next_environment = get_next_environment(ctx) set_instance(instance_name, next_environment) if selfsigned: _instance_selfsigned(instance_configuration) else: log("This is work in progress") abort(55555) _instance_letsencrypt(instance_configuration) finish(ctx)
def install_environment_modules(ctx, source, force, urls, store_url): """Add and install a module only to a single environment Note: This does not modify the instance configuration, so this will not be permanent during upgrades etc. """ instance_name = ctx.obj["instance"] instance_configuration = ctx.obj["instances"][instance_name] next_environment = get_next_environment(ctx) user = instance_configuration["user"] installed = instance_configuration["environments"][next_environment][ "installed"] if not installed: log("Please install the '%s' environment first." % next_environment, lvl=error) abort(50000) set_instance(instance_name, next_environment) for url in urls: result = _install_module(source, url, force=force, user=user, store_url=store_url) if result is False: log("Installation failed!", lvl=error) abort(50000) package_name, package_version = result descriptor = {"version": package_version, "source": source, "url": url} if store_url != DEFAULT_STORE_URL: descriptor["store_url"] = store_url instance_configuration["environments"][next_environment]["modules"][ package_name] = descriptor write_instance(instance_configuration) finish(ctx)
def set_parameter(ctx, parameter, value, force): """Set a configuration parameter of an environment""" # TODO: Generalize and improve this. # - To get less code redundancy (this is also in instance.py) # - To be able to set lists, dicts and other types correctly log("Setting %s to %s" % (parameter, value)) next_environment = get_next_environment(ctx) environment_configuration = ctx.obj["instance_configuration"][ 'environments'][next_environment] defaults = environment_template converted_value = None try: parameter_type = type(defaults[parameter]) log(parameter_type, pretty=True, lvl=debug) if parameter_type == tomlkit.items.Integer: converted_value = int(value) elif parameter_type == bool: converted_value = value.upper() == "TRUE" else: converted_value = value except KeyError: log("Available parameters:", sorted(list(defaults.keys()))) abort(EXIT_INVALID_PARAMETER) if converted_value is None: log("Converted value was None! Recheck the new config!", lvl=warn) environment_configuration[parameter] = converted_value log("New config:", environment_configuration, pretty=True, lvl=debug) ctx.obj["instances"][ctx.obj["instance"]]['environments'][ next_environment] = environment_configuration if valid_configuration(ctx) or force: write_instance(ctx.obj["instances"][ctx.obj["instance"]]) finish(ctx) else: log("New configuration would not be valid", lvl=critical) abort(EXIT_INVALID_CONFIGURATION)
def set_parameter(ctx, login, parameter, value): """Set a configuration parameter of an instance""" log("Setting %s to %s" % (parameter, value)) remote_config = ctx.obj["host_config"] defaults = remote_template converted_value = None try: if login: parameter_type = type(defaults["login"][parameter]) else: parameter_type = type(defaults[parameter]) log(parameter_type, pretty=True, lvl=verbose) if parameter_type == tomlkit.api.Integer: converted_value = int(value) else: converted_value = value except KeyError: log( "Invalid parameter specified. Available parameters:", sorted(list(defaults.keys())), lvl=warn, ) abort(EXIT_INVALID_PARAMETER) if converted_value is None: abort(EXIT_INVALID_VALUE) if login: remote_config["login"][parameter] = converted_value else: remote_config[parameter] = converted_value log("New config:", remote_config, pretty=True, lvl=debug) ctx.obj["remotes"][ctx.obj["remote"]] = remote_config write_remote(remote_config)
def info_instance(ctx): """Print information about the selected instance""" instance_name = ctx.obj["instance"] instances = ctx.obj["instances"] instance_configuration = instances[instance_name] environment_name = instance_configuration["environment"] environment_config = instance_configuration["environments"][ environment_name] if instance_name not in instances: log("Instance %s unknown!" % instance_name, lvl=warn) abort(EXIT_INSTANCE_UNKNOWN) log("Instance configuration:", instance_configuration, pretty=True) log("Active environment (%s):" % environment_name, environment_config, pretty=True) finish(ctx)
def check_instance(ctx): """Check health of the selected instance""" instance_name = ctx.obj["instance"] instances = ctx.obj["instances"] instance_configuration = instances[instance_name] environment_name = instance_configuration["environment"] environment_config = instance_configuration["environments"][ environment_name] if instance_name not in instances: log("Instance %s unknown!" % instance_name, lvl=warn) abort(EXIT_INSTANCE_UNKNOWN) # TODO: Validate instance config _check_environment(ctx, "blue") _check_environment(ctx, "green") finish(ctx)
def set_parameter(ctx, parameter, value): """Set a configuration parameter of an instance""" log("Setting %s to %s" % (parameter, value)) instance_configuration = ctx.obj["instance_configuration"] defaults = instance_template converted_value = None path = parameter.split(".") try: default = nested_map_find(defaults, path) parameter_type = type(default) log(parameter_type, pretty=True, lvl=debug) if parameter_type == tomlkit.items.Integer: converted_value = int(value) elif parameter_type == bool: converted_value = value.upper() == "TRUE" else: converted_value = value except KeyError: log("Available parameters:", sorted(list(defaults.keys()))) abort(EXIT_INVALID_PARAMETER) if converted_value is None: log("Converted value was None! Recheck the new config!", lvl=warn) nested_map_update(instance_configuration, converted_value, path) #instance_configuration[parameter] = converted_value log("New config:", instance_configuration, pretty=True, lvl=debug) ctx.obj["instances"][ctx.obj["instance"]] = instance_configuration if valid_configuration(ctx): write_instance(instance_configuration) finish(ctx) else: log("New configuration would not be valid", lvl=critical) abort(EXIT_INVALID_CONFIGURATION)
def install_instance_modules(ctx, source, urls, install_env, force, store_url): """Add (and optionally immediately install) modules for an instance. This will add them to the instance's configuration, so they will be upgraded as well as reinstalled on other environment changes. If you're installing from a store, you can specify a custom store URL with the --store-url argument. """ instance_name = ctx.obj["instance"] instance_configuration = ctx.obj["instances"][instance_name] for url in urls: descriptor = [source, url] if store_url != DEFAULT_STORE_URL: descriptor.append(store_url) if descriptor not in instance_configuration["modules"]: instance_configuration["modules"].append(descriptor) elif not force: log("Module %s is already installed. Use --force to install anyway." % url) abort(50000) write_instance(instance_configuration) if install_env is True: next_environment = get_next_environment(ctx) environments = instance_configuration['environments'] if environments[next_environment]["installed"] is False: log("Environment %s is not installed, cannot install modules." % next_environment, lvl=warn) abort(50600) return del ctx.params["install_env"] ctx.forward(install_environment_modules) finish(ctx)
def _system_all(ctx): use_sudo = ctx.obj["use_sudo"] config_path = get_etc_path() log("Generating configuration at", config_path) if os.path.exists(config_path): abort(EXIT_NOT_OVERWRITING_CONFIGURATION) ctx = create_configuration(ctx) log("Installing Isomer system wide") install_isomer(ctx.obj["platform"], use_sudo, show=ctx.obj["log_actions"], omit_platform=ctx.obj['omit_platform'], omit_common=True) log("Adding Isomer system user") _add_system_user(use_sudo) log("Creating Isomer filesystem locations") _create_system_folders(use_sudo)
def _turnover(ctx, force): """Internal turnover operation""" # if ctx.obj['acting_environment'] is not None: # next_environment = ctx.obj['acting_environment'] # else: next_environment = get_next_environment(ctx) log("Activating environment:", next_environment) env = ctx.obj["instance_configuration"]["environments"][next_environment] log("Inspecting new environment") if not force: if env.get("database", "") == "": log("Database has not been set up correctly.", lvl=critical) abort(EXIT_INSTALLATION_FAILED) if (not env.get("installed", False) or not env.get("tested", False) or not env.get("migrated", False)): log("Installation failed, cannot activate!", lvl=critical) abort(EXIT_INSTALLATION_FAILED) update_service(ctx, next_environment) ctx.obj["instance_configuration"]["environment"] = next_environment write_instance(ctx.obj["instance_configuration"]) # TODO: Effect reload of service # * Systemctl reload # * (Re)start service # * confirm correct operation # - if not, switch back to the other instance, maybe indicate a broken # state for next_environment # - if yes, Store instance configuration and terminate, we're done log("Turned instance over to", next_environment)
def create_module(clear_target, target): """Creates a new template Isomer plugin module""" if os.path.exists(target): if clear_target: shutil.rmtree(target) else: log("Target exists! Use --clear to delete it first.", emitter="MANAGE") abort(2) done = False info = None while not done: info = _ask_questionnaire() pprint(info) done = ask("Is the above correct", default="y", data_type="bool") augmented_info = _augment_info(info) log("Constructing module %(plugin_name)s" % info) _construct_module(augmented_info, target)
def _get_versions(ctx, source, url, fetch): instance_configuration = ctx.obj["instance_configuration"] source = source if source is not None else instance_configuration["source"] releases = {} if source == "github": releases = get_github_releases() elif source == "pypi": releases = get_pypi_releases() elif source == "git": if url != "": repo = url else: repo = instance_configuration["url"] releases = get_git_releases(repo, fetch) else: log("Other methods to acquire versions than github are currently WiP") abort(60001) return releases
def _instance_letsencrypt(instance_configuration): hostnames = instance_configuration.get("web", {}).get("hostnames", False) hostnames = hostnames.replace(" ", "") if not hostnames or hostnames == "localhost": log( "Please configure the public fully qualified domain names of this instance.\n" "Use 'iso instance set web_hostnames your.hostname.tld' to do that.\n" "You can add multiple names by separating them with commas.", lvl=error, ) abort(50031) contact = instance_configuration.get("contact", False) if not contact: log( "You need to specify a contact mail address for this instance to generate certificates.\n" "Use 'iso instance set contact [email protected]' to do that.", lvl=error, ) abort(50032) success, result = run_process( "/", [ "certbot", "--nginx", "certonly", "-m", contact, "-d", hostnames, "--agree-tos", "-n", ], ) if not success: log( "Error getting certificate:", format_result(result), pretty=True, lvl=error, ) abort(50033)