def mount(self, passwd=None): """ Mount partition. :param str passwd: If disk is encrypted, use this passphrase to unlock """ if self.mountpoint and os.path.ismount(self.mountpoint): raise errors.InvalidConfigError("Virtual disk already mounted") signals.emit("filesystems", "pre_mount", self) if not os.path.isdir(os.path.join("/media", self.id)): os.makedirs(os.path.join("/media", self.id)) mount_point = self.mountpoint or os.path.join("/media", self.id) luks_point = os.path.join("/dev/mapper", self.id) # Find a free loopback device and mount loop = losetup.find_unused_loop_device() loop.mount(str(self.path), offset=1048576) if self.crypt and passwd: # If it's an encrypted virtual disk, decrypt first then mount s = crypto.luks_open(loop.device, self.id, passwd) if s != 0: loop.unmount() excmsg = "Failed to decrypt {0} with errno {1}" raise errors.OperationFailedError( excmsg.format(self.id, str(s))) s = libc.mount(ctypes.c_char_p(b(luks_point)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: crypto.luks_close(self.id) loop.unmount() excmsg = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) elif self.crypt and not passwd: excstr = "Must provide password to decrypt encrypted container" raise errors.InvalidConfigError(excstr) else: s = libc.mount(ctypes.c_char_p(b(loop.device)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: loop.unmount() excstr = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) signals.emit("filesystems", "post_mount", self) self.mountpoint = mount_point
def verify_composer(): """Verify Composer installation status.""" if not find_executable("composer"): install_composer() if not find_executable("composer"): logmsg = "Composer installation failed." raise errors.OperationFailedError(logmsg)
def post_install(self, extra_vars, dbpasswd=""): # Make sure the webapps config points to # the _site directory and generate it. c = nginx.loadf(os.path.join('/etc/nginx/sites-available', self.id)) for x in c.servers: if x.filter('Key', 'root'): x.filter('Key', 'root')[0].value = \ os.path.join(self.path, '_site') nginx.dumpf(c, os.path.join('/etc/nginx/sites-available', self.id)) s = shell('jekyll build --source {0} --destination {1}'.format( self.path, os.path.join(self.path, '_site'))) if s["code"] != 0: raise errors.OperationFailedError( 'Jekyll failed to build: {0}'.format(str(s["stderr"]))) uid, gid = users.get_system("http").uid, groups.get_system("http").gid for r, d, f in os.walk(self.path): for x in d: os.chmod(os.path.join(r, x), 0o755) os.chown(os.path.join(r, x), uid, gid) for x in f: os.chmod(os.path.join(r, x), 0o644) os.chown(os.path.join(r, x), uid, gid) # Return an explicatory message. return 'Jekyll has been setup, with a sample site at {0}. '\ 'Modify these files as you like. To learn how to use Jekyll, '\ 'visit http://jekyllrb.com/docs/usage. After making changes, '\ 'click the site icon to edit, then "Regenerate Site" '\ 'to bring your changes live.'.format(self.path)
def disable(self): """Disable connection on boot.""" s = shell("netctl disable {0}".format(self.id)) if s["code"] == 0: self.enabled = False else: raise errors.OperationFailedError("Network disable failed")
def install(*mods, **kwargs): """ Install a set of NPM packages. Include ``as_global`` in kwargs to install package as global. :param *mods: Packages to install :param **kwargs: Extra keyword arguments to pass to NPM """ as_global = "-g " if kwargs.get("as_global") else "" mods = " ".join(x for x in mods) opt_str = "" if kwargs.get("opts"): opt_str += " --" for k, v in kwargs.get("opts", {}): opt_str += " --".join(k + v if v[0] == '=' else k + " " + v) cwd = os.getcwd() if "install_path" in kwargs: os.chdir(kwargs["install_path"]) s = shell("npm install {0}{1}{2}".format( as_global, mods, )) os.chdir(cwd) if s["code"] != 0: errmsg = "NPM install of {0} failed.".format(mods) logmsg = "NPM install failure details:\n{0}" logger.error("NodeJS", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg)
def post_install(self, extra_vars, dbpasswd=""): secret_key = random_string() dbengine = 'mysql' \ if self.app.selected_dbengine == 'db-mariadb' \ else 'sqlite' # Write a standard Wallabag config file config_file = os.path.join(self.path, 'app/config/parameters.yml') with open(config_file + ".dist", 'r') as f: ic = f.readlines() with open(config_file, 'w') as f: for l in ic: if "database_driver: " in l: pdo = "pdo_mysql" if dbengine == "mysql" else "pdo_sqlite" l = " database_driver: {0}\n".format(pdo) elif "database_path: " in l and dbengine == 'sqlite': l = " database_path: {0}\n".format(self.db.path) elif "database_name: " in l and dbengine == 'mysql': l = " database_name: {0}\n".format(self.db.id) elif "database_user: "******" database_user: {0}\n".format(self.db.id) elif "database_password: "******"{0}"\n'.format(dbpasswd) elif "secret: " in l: l = " secret: {0}\n".format(secret_key) f.write(l) # Make sure that the correct PHP settings are enabled php.enable_mod('sqlite3', 'bcmath', 'pdo_mysql' if dbengine == 'mysql' else 'pdo_sqlite', 'zip', 'tidy') php.open_basedir('add', '/usr/bin/php') uid, gid = users.get_system("http").uid, groups.get_system("http").gid # Set up the database then delete the install folder if dbengine == 'sqlite3': php.open_basedir('add', '/var/lib/sqlite3') cwd = os.getcwd() os.chdir(self.path) s = shell("php bin/console wallabag:install --env=prod -n") if s["code"] != 0: logger.error("Websites", s["stderr"].decode()) raise errors.OperationFailedError( "Failed to populate database. See logs for more info") os.chdir(cwd) if dbengine == 'sqlite3': os.chown("/var/lib/sqlite3/{0}.db".format(self.db.id), -1, gid) os.chmod("/var/lib/sqlite3/{0}.db".format(self.db.id), 0o660) # Finally, make sure that permissions are set so that Wallabag # can make adjustments and save plugins when need be. for r, d, f in os.walk(self.path): for x in d: os.chown(os.path.join(r, x), uid, gid) for x in f: os.chown(os.path.join(r, x), uid, gid)
def mount(self, passwd=None): """ Mount partition. :param str passwd: If disk is encrypted, use this passphrase to unlock """ if self.mountpoint and os.path.ismount(self.mountpoint): raise errors.InvalidConfigError("Disk partition already mounted") elif self.fstype == "Unknown": emsg = "Cannot mount a partition of unknown type" raise errors.InvalidConfigError(emsg) signals.emit("filesystems", "pre_mount", self) mount_point = self.mountpoint or os.path.join("/media", self.id) luks_point = os.path.join("/dev/mapper", self.id) if not os.path.isdir(mount_point): os.makedirs(mount_point) if self.crypt and passwd: # Decrypt the disk first if it's an encrypted disk s = crypto.luks_open(self.path, self.id, passwd) if s != 0: excmsg = "Failed to decrypt {0} with errno {1}" excmsg = excmsg.format(self.id, str(s)) raise errors.OperationFailedError(excmsg) s = libc.mount(ctypes.c_char_p(b(luks_point)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: crypto.luks_close(self.id) excmsg = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) elif self.crypt and not passwd: emsg = "Must provide password to decrypt encrypted disk" raise errors.InvalidConfigError(emsg) else: s = libc.mount(ctypes.c_char_p(b(self.path)), ctypes.c_char_p(b(mount_point)), ctypes.c_char_p(b(self.fstype)), 0, ctypes.c_char_p(b"")) if s == -1: excmsg = "Failed to mount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) signals.emit("filesystems", "post_mount", self) self.mountpoint = mount_point
def disconnect(self): """Disconnect from network.""" signals.emit("networks", "pre_disconnect", self) s = shell("netctl stop {0}".format(self.id)) if s["code"] == 0: self.connected = False signals.emit("networks", "post_disconnect", self) else: raise errors.OperationFailedError("Network disconnection failed")
def encrypt(self, passwd, cipher=config.get("filesystems", "cipher"), keysize=config.get("filesystems", "keysize"), mount=False): """ Encrypt virtual disk image. :params str passwd: Passphrase to encrypt disk with :params str cipher: cipher suite to use (default aes-xts-plain64) :params str keysize: default key size to use (default 256) :params bool mount: mount after encrypt? """ cipher = cipher or "aes-xts-plain64" keysize = keysize or 256 vdisk_dir = config.get("filesystems", "vdisk_dir") os.rename(self.path, os.path.join(vdisk_dir, self.id + ".crypt")) self.path = os.path.join(vdisk_dir, self.id + ".crypt") # Find an open loopback device and mount loop = losetup.find_unused_loop_device() loop.mount(str(self.path), offset=1048576) # Encrypt the file inside the loopback and mount s = crypto.luks_format(loop.device, passwd, cipher, int(keysize)) if s != 0: loop.unmount() os.rename(self.path, os.path.join(vdisk_dir, self.id + ".img")) excstr = "Failed to encrypt {0} with errno {1}" raise errors.OperationFailedError(excstr.format(self.id, str(s))) s = crypto.luks_open(loop.device, self.id, passwd) if s != 0: loop.unmount() excstr = "Failed to decrypt {0} with errno {1}" raise errors.OperationFailedError(excstr.format(self.id, str(s))) # Create a filesystem inside the encrypted device s = shell("mkfs.ext4 /dev/mapper/{0}".format(self.id)) crypto.luks_close(self.id) loop.unmount() if s["code"] != 0: excstr = "Failed to format loop device: {0}" raise errors.OperationFailedError(excstr.format(s["stderr"])) self.crypt = True if mount: self.mount(passwd)
def add_db(self): if re.search('\.|-|`|\\\\|\/|[ ]', self.id): raise errors.InvalidConfigError( 'Name must not contain spaces, dots, dashes or other ' 'special characters') self.manager.chkpath() status = shell("sqlite3 {0} \"ATTACH '{1}' AS {2};\"".format( self.path, self.path, self.id)) if status["code"] >= 1: raise errors.OperationFailedError(status["stderr"])
def pre_backup(self): """Reimplement.""" s = shell("slapcat -n 1") if s["code"] != 0: emsg = ("Could not backup LDAP database. " "Please check logs for errors.") logger.error("Backup", s["stderr"].decode()) raise errors.OperationFailedError(emsg) with open("/tmp/ldap.ldif", "wb") as f: f.write(s["stdout"])
def post_restore(self): """Reimplement.""" if not os.path.exists("/tmp/ldap.ldif"): emsg = ("Could not backup LDAP database. " "Please check logs for errors.") logger.error("Backup", "/tmp/ldap.ldif not found") raise errors.OperationFailedError(emsg) with open("/tmp/ldap.ldif", "r") as f: ldif = f.read() s = shell('ldapadd -D "cn=admin,dc=arkos-servers,dc=org" -w {0}' .format(secrets.get("ldap")), stdin=ldif) if os.path.exists("/tmp/ldap.ldif"): os.unlink("/tmp/ldap.ldif") if s["code"] != 0: emsg = ("Could not restore LDAP database. " "Please check logs for errors.") logger.error("Backup", s["stderr"].decode()) raise errors.OperationFailedError(emsg)
def connect(self): """Connect to network.""" signals.emit("networks", "pre_connect", self) for x in get_connections(iface=self.config.get("interface")): x.disconnect() s = shell("netctl start {0}".format(self.id)) if s["code"] == 0: self.connected = True signals.emit("networks", "post_connect", self) else: raise errors.OperationFailedError("Network connection failed")
def remove(*mods): """ Remove a set of Python packages from the system. :param *mods: packages to remove """ s = shell("pip uninstall {0}".format(mods)) if s["code"] != 0: errmsg = "PyPI uninstall of {0} failed.".format(mods) logmsg = "PyPI uninstall failure details:\n{0}" logger.error("Python", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg)
def remove(*mods): """ Remove a set of NPM packages. :param *mods: Packages to remove """ mods = " ".join(x for x in mods) s = shell("npm uninstall {0}".format(mods), stderr=True) if s["code"] != 0: errmsg = "NPM removal of {0} failed.".format(mods) logmsg = "NPM remove failure details:\n{0}" logger.error("NodeJS", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg)
def get_jail_config(jcfg=""): """ Load fail2ban firewall jail configuration from file. :param str jailconf: path to jail configuration file :returns: jail config :rtype: configparser.RawConfigParser """ cfg = configparser.RawConfigParser() if not cfg.read(jcfg or jailconf): emsg = "Fail2Ban config not found or not readable" raise errors.OperationFailedError(emsg) return cfg
def umount(self): """Unmount partition.""" signals.emit("filesystems", "pre_umount", self) if not self.mountpoint: return s = libc.umount2(ctypes.c_char_p(b(self.mountpoint)), 0) if s == -1: excmsg = "Failed to unmount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) if self.crypt: crypto.luks_close(b(self.id)) signals.emit("filesystems", "post_umount", self) self.mountpoint = None
def generate_dh_params(path, size=2048): """ Create and save Diffie-Hellman parameters. :param str path: File path to save to :param int size: Key size """ s = shell("openssl dhparam {0} -out {1}".format(size, path)) if s["code"] != 0: emsg = "Failed to generate Diffie-Hellman parameters" logger.error("Certificates", s["stderr"].decode()) raise errors.OperationFailedError(emsg) os.chown(path, -1, gid) os.chmod(path, 0o750)
def create(self, mount=False, will_crypt=False, nthread=NotificationThread()): """ Create virtual disk image. :param bool mount: Mount after creation? :param bool will_crypt: Will this disk be encrypted later? :param NotificationThread nthread: notification thread to use """ nthread.title = "Creating virtual disk" vdisk_dir = config.get("filesystems", "vdisk_dir") if not os.path.exists(vdisk_dir): os.mkdir(vdisk_dir) self.path = str(os.path.join(vdisk_dir, self.id + ".img")) if os.path.exists(self.path): raise errors.InvalidConfigError("This virtual disk already exists") # Create an empty file matching disk size signals.emit("filesystems", "pre_add", self) msg = "Creating virtual disk..." nthread.update(Notification("info", "Filesystems", msg)) with open(self.path, "wb") as f: written = 0 with open("/dev/zero", "rb") as zero: while self.size > written: written += 1024 f.write(zero.read(1024)) if not will_crypt: # Get a free loopback device and mount loop = losetup.find_unused_loop_device() loop.mount(str(self.path), offset=1048576) # Make a filesystem msg = "Writing filesystem..." nthread.update(Notification("info", "Filesystems", msg)) s = shell("mkfs.ext4 {0}".format(loop.device)) if s["code"] != 0: excmsg = "Failed to format loop device: {0}" raise errors.OperationFailedError(excmsg.format(s["stderr"])) loop.unmount() msg = "Virtual disk created successfully" nthread.complete(Notification("success", "Filesystems", msg)) signals.emit("filesystems", "post_add", self) if mount: self.mount()
def set_datetime(ut=0): """ Set system time from provided Unix timestamp (or current time via NTP). :param int ut: Unix timestamp """ ut = int(ut) if ut else int(get_idatetime()) librt = ctypes.CDLL(ctypes.util.find_library("rt"), use_errno=True) ts = timespec() ts.tv_sec, ts.tv_nsec = ut, 0 res = librt.clock_settime(0, ctypes.byref(ts)) if res == -1: raise errors.OperationFailedError("Could not set time: {0}".format( os.strerror(ctypes.get_errno()))) signals.emit("config", "time_changed", ut)
def install(pkg, version=None, py2=False): """ Install a set of Python packages from PyPI. :param str pkg: package to install :param str version: If present, install this specific version :param bool py2: If True, install for Python 2.x instead """ if version: pkg = pkg + "==" + version s = shell("pip{0} install {1}".format("2" if py2 else "", pkg)) if s["code"] != 0: errmsg = "PyPI install of {0} failed.".format(pkg) logmsg = "PyPI install failure details:\n{0}" logger.error("Python", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg)
def regenerate(self): path = self.path if not path.endswith("_site"): path = os.path.join(self.path, "_site") s = shell('jekyll build --source {0} --destination {1}'.format( self.path.split('/_site')[0], path)) if s["code"] != 0: raise errors.OperationFailedError( 'Jekyll failed to build: {0}'.format(str(s["stderr"]))) uid, gid = users.get_system("http").uid, groups.get_system("http").gid for r, d, f in os.walk(self.path): for x in d: os.chmod(os.path.join(r, x), 0o755) os.chown(os.path.join(r, x), uid, gid) for x in f: os.chmod(os.path.join(r, x), 0o644) os.chown(os.path.join(r, x), uid, gid)
def verify_time(update=True, crit=True): """ Verify system time with NTP, set it if it is more than an hour off. :param bool update: Update system time if it is too far off. :param bool crit: Raise an exception if time cannot be retrieved. """ try: os = get_offset() except Exception as e: if crit: raise errors.OperationFailedError( "System time could not be retrieved. Error: {0}".format(e)) else: return "UNKNOWN" if (os < -3600 or os > 3600) and update: set_datetime() return os
def install_composer(): """Install Composer to the system.""" cwd = os.getcwd() os.chdir("/root") os.environ["COMPOSER_HOME"] = "/root" enable_mod("phar") open_basedir("add", "/root") r = requests.get("https://getcomposer.org/installer") s = shell("php", stdin=r.text) os.chdir(cwd) if s["code"] != 0: errmsg = "Composer download/config failed." logmsg = "Composer install failure details:\n{0}" logger.error("PHP", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg) os.rename("/root/composer.phar", "/usr/local/bin/composer") os.chmod("/usr/local/bin/composer", 0o755) open_basedir("add", "/usr/local/bin")
def install(gem, version=None, update=False): """ Install a Ruby gem to the system. :param str gem: Gem name :param str version: If present, install this specific version :param bool update: If true, force an update """ verify_path() if version: gem = gem + ":" + version s = shell("gem {0} -N --no-user-install {1}".format( "update" if update else "install", gem)) if s["code"] != 0: errmsg = "Gem install of {0} failed.".format(gem) logmsg = "Gem install failure details:\n{0}" logger.error("Ruby", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg)
def install_from_package(path, stat="production", opts={}): """ Install a set of NPM package dependencies from an NPM package.json. :param str path: path to directory with package.json :param str stat: Install to "production"? """ stat = (" --" + stat) if stat else "" opts = (" --" + " --".join(x + "=" + y for x, y in opts)) if opts else "" cwd = os.getcwd() os.chdir(path) s = shell("npm install {0}{1}".format(stat, opts)) os.chdir(cwd) if s["code"] != 0: errmsg = "NPM install of {0} failed.".format(path) logmsg = "NPM install failure details:\n{0}" logger.error("NodeJS", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg)
def umount(self): """Unmount disk.""" if not self.mountpoint: return signals.emit("filesystems", "pre_umount", self) loops = losetup.get_loop_devices() for l in loops: if loops[l].is_used() and loops[l].get_filename() == self.path: dev = loops[l] break s = libc.umount2(ctypes.c_char_p(b(self.mountpoint)), 0) if s == -1: excmsg = "Failed to unmount {0}: {1}" raise errors.OperationFailedError( excmsg.format(self.id, os.strerror(ctypes.get_errno()))) if self.crypt: crypto.luks_close(self.id) if dev: dev.unmount() signals.emit("filesystems", "post_umount", self) self.mountpoint = None
def composer_install(path, flags={}, env={}): """ Install a PHP application bundle via Composer. :param str path: path to app directory :param dict flags: command line flags to pass to Composer :param dict env: command line environment variables """ verify_composer() cwd = os.getcwd() os.chdir(path) shell("composer self-update") cmdflags = [] for x in flags: flg = "-{0}".format(x) if len(x) == 1 else "--{0}".format(x) cmdflags.append("{0}={1}".format(flg, flags[x]) if flags[x] else flg) s = shell("composer install {0}".format(" ".join(cmdflags)), env=env) os.chdir(cwd) if s["code"] != 0: errmsg = "Composer install of {0} failed.".format(path) logmsg = "Composer install failure details:\n{0}" logger.error("PHP", logmsg.format(s["stderr"].decode())) raise errors.OperationFailedError(errmsg)
def site_edited(self): # Remove the existing trusted_sites array # then add a new one based on the new addr if os.path.exists(os.path.join(self.path, 'config', 'config.php')): path = os.path.join(self.path, 'config', 'config.php') else: raise errors.OperationFailedError( "Nextcloud config file not found") with open(path, "r") as f: data = f.read() while re.search("\n(\s*('|\")trusted_domains.*?\).*?\n)", data, re.DOTALL): data = data.replace( re.search("\n(\s*('|\")trusted_domains.*?\).*?\n)", data, re.DOTALL).group(1), "") data = data.split("\n") with open(path, "w") as f: for x in data: if not x.endswith("\n"): x += "\n" if x.startswith(");"): f.write(" 'trusted_domains' => " "array('localhost','{0}'),\n".format(self.domain)) f.write(x)
def _install(self, extra_vars, enable, nthread): nthread.title = "Installing website" msg = Notification("info", "Webs", "Preparing to install...") nthread.update(msg) # Make sure the chosen port is indeed open if not tracked_services.is_open_port(self.port, self.domain): cname = "({0})".format(self.app.id) raise errors.InvalidConfigError(cname, nthread)\ from tracked_services.PortConflictError(self.port, self.domain) # Set some metadata values specialmsg, dbpasswd = "", "" site_dir = config.get("websites", "site_dir") path = (self.path or os.path.join(site_dir, self.id)) self.path = path self.php = extra_vars.get("php") or self.php \ or self.app.uses_php or False self.version = self.app.version.rsplit("-", 1)[0] \ if self.app.website_updates else None # Classify the source package type if not self.app.download_url: ending = "" elif self.app.download_url.endswith(".tar.gz"): ending = ".tar.gz" elif self.app.download_url.endswith(".tgz"): ending = ".tgz" elif self.app.download_url.endswith(".tar.bz2"): ending = ".tar.bz2" elif self.app.download_url.endswith(".zip"): ending = ".zip" elif self.app.download_url.endswith(".git"): ending = ".git" else: raise errors.InvalidConfigError( "Invalid source archive format in {0}".format(self.app.id)) msg = "Running pre-installation..." uid, gid = users.get_system("http").uid, groups.get_system("http").gid nthread.update(Notification("info", "Webs", msg)) # Call website type's pre-install hook self.pre_install(extra_vars) # If needs DB and user didn't select an engine, choose one for them if len(self.app.database_engines) > 1 \ and extra_vars.get("dbengine", None): self.app.selected_dbengine = extra_vars.get("dbengine") if not getattr(self.app, "selected_dbengine", None)\ and self.app.database_engines: self.app.selected_dbengine = self.app.database_engines[0] # Create DB and/or DB user as necessary if getattr(self.app, "selected_dbengine", None): msg = "Creating database..." nthread.update(Notification("info", "Webs", msg)) mgr = databases.get_managers(self.app.selected_dbengine) if not mgr: estr = "No manager found for {0}" raise errors.InvalidConfigError( estr.format(self.app.selected_dbengine)) # Make sure DB daemon is running if it has one if not mgr.state: svc = services.get(mgr.meta.database_service) svc.restart() self.db = mgr.add_db(self.id) if hasattr(self.db, "path"): os.chmod(self.db.path, 0o660) os.chown(self.db.path, -1, gid) # If multiuser DB type, create user if mgr.meta.database_multiuser: dbpasswd = random_string(16) db_user = mgr.add_user(self.id, dbpasswd) db_user.chperm("grant", self.db) # Make sure the target directory exists, but is empty pkg_path = os.path.join("/tmp", self.id + ending) if os.path.isdir(self.path): shutil.rmtree(self.path) os.makedirs(self.path) # Download and extract the source repo / package msg = "Downloading website source..." nthread.update(Notification("info", "Webs", msg)) if self.app.download_url and ending == ".git": g = git.Repo.clone_from(self.app.download_url, self.path) if hasattr(self.app, "download_at_tag"): g = git.Git(self.path) g.checkout(self.app.download_git_tag) elif self.app.download_url: download(self.app.download_url, file=pkg_path, crit=True) # Format extraction command according to type msg = "Extracting source..." nthread.update(Notification("info", "Webs", msg)) if ending in [".tar.gz", ".tgz", ".tar.bz2"]: arch = tarfile.open(pkg_path, "r:gz") r = (x for x in arch.getnames() if re.match("^[^/]*$", x)) toplvl = next(r, None) if not toplvl: raise errors.OperationFailedError( "Malformed source archive") arch.extractall(site_dir) os.rename(os.path.join(site_dir, toplvl), self.path) else: arch = zipfile.ZipFile(pkg_path) r = (x for x in arch.namelist() if re.match("^[^/]*/$", x)) toplvl = next(r, None) if not toplvl: raise errors.OperationFailedError( "Malformed source archive") arch.extractall(site_dir) os.rename(os.path.join(site_dir, toplvl.rstrip("/")), self.path) os.remove(pkg_path) # Set proper starting permissions on source directory os.chmod(self.path, 0o755) os.chown(self.path, uid, gid) for r, d, f in os.walk(self.path): for x in d: os.chmod(os.path.join(r, x), 0o755) os.chown(os.path.join(r, x), uid, gid) for x in f: os.chmod(os.path.join(r, x), 0o644) os.chown(os.path.join(r, x), uid, gid) # If there is a custom path for the data directory, set it up if getattr(self.app, "website_datapaths", None) \ and extra_vars.get("datadir"): self.data_path = extra_vars["datadir"] if not os.path.exists(self.data_path): os.makedirs(self.data_path) os.chmod(self.data_path, 0o755) os.chown(self.data_path, uid, gid) elif hasattr(self, "website_default_data_subdir"): self.data_path = os.path.join(self.path, self.website_default_data_subdir) else: self.data_path = self.path # Create the nginx serverblock addtoblock = self.addtoblock or [] if extra_vars.get("addtoblock"): addtoblock += nginx.loads(extra_vars.get("addtoblock"), False) default_index = "index." + ("php" if self.php else "html") if hasattr(self.app, "website_root"): webroot = os.path.join(self.path, self.app.website_root) else: webroot = self.path block = nginx.Conf() server = nginx.Server( nginx.Key("listen", str(self.port)), nginx.Key("listen", "[::]:" + str(self.port)), nginx.Key("server_name", self.domain), nginx.Key("root", webroot), nginx.Key( "index", getattr(self.app, "website_index", None) or default_index), nginx.Location("/.well-known/acme-challenge/", nginx.Key("root", self.path))) if addtoblock: server.add(*[x for x in addtoblock]) block.add(server) nginx.dumpf(block, os.path.join("/etc/nginx/sites-available", self.id)) challenge_dir = os.path.join(self.path, ".well-known/acme-challenge/") if not os.path.exists(challenge_dir): os.makedirs(challenge_dir) # Create arkOS metadata file meta = configparser.SafeConfigParser() meta.add_section("website") meta.set("website", "id", self.id) meta.set("website", "app", self.app.id) meta.set("website", "ssl", self.cert.id if getattr(self, "cert", None) else "None") meta.set("website", "version", self.version or "None") if getattr(self.app, "website_datapaths", None) \ and self.data_path: meta.set("website", "data_path", self.data_path) meta.set("website", "dbengine", "") meta.set("website", "dbengine", getattr(self.app, "selected_dbengine", "")) with open(os.path.join(self.path, ".arkos"), "w") as f: meta.write(f) # Call site type's post-installation hook msg = "Running post-installation. This may take a few minutes..." nthread.update(Notification("info", "Webs", msg)) specialmsg = self.post_install(extra_vars, dbpasswd) # Cleanup and reload daemons msg = "Finishing..." nthread.update(Notification("info", "Webs", msg)) self.installed = True storage.websites[self.id] = self if self.port == 80: cleanup_acme_dummy(self.domain) signals.emit("websites", "site_installed", self) if enable: self.nginx_enable() if enable and self.php: php.open_basedir("add", "/srv/http/") php_reload() msg = "{0} site installed successfully".format(self.app.name) nthread.complete(Notification("success", "Webs", msg)) if specialmsg: return specialmsg