diff options
author | Ramón M. Gómez <ramongomez@us.es> | 2019-04-09 11:20:15 +0200 |
---|---|---|
committer | Ramón M. Gómez <ramongomez@us.es> | 2019-04-09 11:20:15 +0200 |
commit | ce738c82052cda8f576c1227b89efcee04c05d56 (patch) | |
tree | b6a7fc8e6588b71dc1e1bec4a7037cda8773b0af | |
parent | 6e4ee7974fe1787ae955543f8a73f85595ff031a (diff) | |
parent | 18789001de4763b88ac61eaea8272d5228d85d64 (diff) |
#761 #750: Merge branch {{{ogagent-oglive}}} into {{{webconsole3}}}.
29 files changed, 989 insertions, 58 deletions
diff --git a/admin/Sources/Clients/ogagent/oglive/Makefile b/admin/Sources/Clients/ogagent/oglive/Makefile new file mode 100644 index 00000000..832e2dc7 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/Makefile @@ -0,0 +1,57 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +# Directories +SOURCEDIR := ../src +LIBDIR := $(DESTDIR)/usr/share/OGAgent +BINDIR := $(DESTDIR)/usr/bin +SBINDIR = $(DESTDIR)/usr/sbin +APPSDIR := $(DESTDIR)/usr/share/applications +CFGDIR := $(DESTDIR)/etc/ogagent +INITDIR := $(DESTDIR)/etc/init.d + +PYC := $(shell find $(SOURCEDIR) -name '*.py[co]') +CACHES := $(shell find $(SOURCEDIR) -name '__pycache__') + +clean: + rm -rf $(PYC) $(CACHES) $(DESTDIR) +install-ogagent: + rm -rf $(DESTDIR) + mkdir -p $(LIBDIR) + mkdir -p $(BINDIR) + mkdir -p $(SBINDIR) + mkdir -p $(APPSDIR) + mkdir -p $(CFGDIR) + + mkdir $(LIBDIR)/img + + # Cleans up .pyc and cache folders + rm -f $(PYC) $(CACHES) + + cp -r $(SOURCEDIR)/opengnsys $(LIBDIR)/opengnsys + cp -r $(SOURCEDIR)/cfg $(LIBDIR)/cfg + + # scripts + cp scripts/ogagent $(BINDIR) + cp scripts/OGAgentTool-startup $(BINDIR) + cp scripts/OGAgentTool $(BINDIR) + + # Fix permissions + chmod 755 $(BINDIR)/ogagent + chmod 755 $(BINDIR)/OGAgentTool-startup + chmod 600 $(LIBDIR)/cfg/ogagent.cfg + + # If for red hat based, copy init.d +ifeq ($(DISTRO),rh) + mkdir -p $(INITDIR) + cp debian/ogagent.init $(INITDIR)/ogagent + chmod +x $(INITDIR)/ogagent + ln -fs /usr/share/OGAgent/cfg/ogagent.cfg $(CFGDIR) + ln -fs /usr/share/OGAgent/cfg/ogclient.cfg $(CFGDIR) +endif + + # chmod 0755 $(BINDIR)/ogagent +uninstall: + rm -rf $(LIBDIR) + # rm -f $(BINDIR)/ogagent + rm -rf $(CFGDIR) diff --git a/admin/Sources/Clients/ogagent/oglive/build-package.sh b/admin/Sources/Clients/ogagent/oglive/build-package.sh new file mode 100755 index 00000000..c0e03c5f --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/build-package.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cd $(dirname "$0") + +# Build package +dpkg-buildpackage -b -d + diff --git a/admin/Sources/Clients/ogagent/oglive/debian/changelog b/admin/Sources/Clients/ogagent/oglive/debian/changelog new file mode 100644 index 00000000..57a05aba --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/changelog @@ -0,0 +1,6 @@ +ogagent-oglive (1.1.1) unstable; urgency=medium + + * Initial release. + + -- Ramón M. Gómez <ramongomez@us.es> Mon, 18 Jun 2018 13:00:00 +0200 + diff --git a/admin/Sources/Clients/ogagent/oglive/debian/compat b/admin/Sources/Clients/ogagent/oglive/debian/compat new file mode 100644 index 00000000..f11c82a4 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/compat @@ -0,0 +1 @@ +9
\ No newline at end of file diff --git a/admin/Sources/Clients/ogagent/oglive/debian/control b/admin/Sources/Clients/ogagent/oglive/debian/control new file mode 100644 index 00000000..374bc554 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/control @@ -0,0 +1,15 @@ +Source: ogagent-oglive +Section: admin +Priority: optional +Maintainer: Ramón M. Gómez <ramongomez@us.es> +Build-Depends: debhelper (>= 7), po-debconf +Standards-Version: 3.9.2 +Homepage: https://opengnsys.es + +Package: ogagent-oglive +Section: admin +Priority: optional +Architecture: all +Depends: python-requests (>=0.8.2), python-six(>=1.1), python-prctl(>=1.1.1), python (>=2.7), libxss1, ${misc:Depends} +Description: OpenGnsys Agent for ogLive client + This package provides the required components to allow this machine to work on an environment managed by OpenGnsys. diff --git a/admin/Sources/Clients/ogagent/oglive/debian/copyright b/admin/Sources/Clients/ogagent/oglive/debian/copyright new file mode 100644 index 00000000..7b6ef31b --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/copyright @@ -0,0 +1,26 @@ +Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135 +Name: ogagent +Maintainer: Ramón M. Gómez +Source: https://opengnsys.es + +Copyright: 2014 Virtual Cable S.L.U. +License: BSD-3-clause + +License: GPL-2+ +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +. +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +. +On Debian systems, the full text of the GNU General Public +License version 2 can be found in the file +`/usr/share/common-licenses/GPL-2'. diff --git a/admin/Sources/Clients/ogagent/oglive/debian/docs b/admin/Sources/Clients/ogagent/oglive/debian/docs new file mode 100644 index 00000000..b2b2a781 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/docs @@ -0,0 +1 @@ +readme.txt diff --git a/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.links b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.links new file mode 100644 index 00000000..9b970d7b --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.links @@ -0,0 +1,2 @@ +/usr/share/OGAgent/cfg/ogagent.cfg /etc/ogagent/ogagent.cfg +/usr/share/OGAgent/cfg/ogclient.cfg /etc/ogagent/ogclient.cfg diff --git a/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postinst b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postinst new file mode 100644 index 00000000..b59cfa6f --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postinst @@ -0,0 +1,21 @@ +#!/bin/sh + +. /usr/share/debconf/confmodule + +set -e +case "$1" in + configure) + chmod 600 /usr/share/OGAgent/cfg/ogagent.cfg + ;; + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 diff --git a/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postinst.debhelper b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postinst.debhelper new file mode 100644 index 00000000..e75924dc --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postinst.debhelper @@ -0,0 +1,5 @@ +# Automatically added by dh_installinit +if [ -x "/etc/init.d/ogagent" ]; then + update-rc.d ogagent defaults >/dev/null || exit $? +fi +# End automatically added section diff --git a/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postrm b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postrm new file mode 100644 index 00000000..a46fa487 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postrm @@ -0,0 +1,10 @@ +#!/bin/sh -e + +. /usr/share/debconf/confmodule + +set -e + +if [ "$1" = "purge" ] ; then + rm -rf /usr/share/OGAgent || true > /dev/null 2>&1 +fi + diff --git a/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postrm.debhelper b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postrm.debhelper new file mode 100644 index 00000000..3167f1f8 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.postrm.debhelper @@ -0,0 +1,12 @@ +# Automatically added by dh_installinit +if [ "$1" = "purge" ] ; then + update-rc.d ogagent remove >/dev/null +fi + + +# In case this system is running systemd, we make systemd reload the unit files +# to pick up changes. +if [ -d /run/systemd/system ] ; then + systemctl --system daemon-reload >/dev/null || true +fi +# End automatically added section diff --git a/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.substvars b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.substvars new file mode 100644 index 00000000..978fc8b5 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/ogagent-oglive.substvars @@ -0,0 +1,2 @@ +misc:Depends= +misc:Pre-Depends= diff --git a/admin/Sources/Clients/ogagent/oglive/debian/ogagent.init b/admin/Sources/Clients/ogagent/oglive/debian/ogagent.init new file mode 100644 index 00000000..3eee35c1 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/ogagent.init @@ -0,0 +1,23 @@ +#!/bin/sh -e +### BEGIN INIT INFO +# Provides: ogagent +# Required-Start: $local_fs $remote_fs $network $syslog $named +# Required-Stop: $local_fs $remote_fs $network $syslog $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: OpenGnsys Agent Service +### END INIT INFO +# + +# . /lib/lsb/init-functions + +case "$1" in + start|stop|restart) + /usr/bin/ogagent $1 + ;; + force-reload) + /usr/bin/ogagent restart + ;; + *) echo "Usage: $0 {start|stop|restart|force-reload}" >&2; exit 1 ;; +esac + diff --git a/admin/Sources/Clients/ogagent/oglive/debian/rules b/admin/Sources/Clients/ogagent/oglive/debian/rules new file mode 100755 index 00000000..0a5c3e5c --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/rules @@ -0,0 +1,44 @@ +#!/usr/bin/make -f +# -*- makefile -*- +configure: configure-stamp +configure-stamp: + dh_testdir + touch configure-stamp +build: build-arch build-indep +build-arch: build-stamp +build-indep: build-stamp +build-stamp: configure-stamp + dh_testdir + $(MAKE) + touch $@ +clean: + dh_testdir + dh_testroot + rm -f build-stamp configure-stamp + dh_clean +install: build + dh_testdir + dh_testroot + dh_prep + dh_installdirs + $(MAKE) DESTDIR=$(CURDIR)/debian/ogagent-oglive install-ogagent +binary-arch: build install + # emptyness +binary-indep: build install + dh_testdir + dh_testroot + dh_installchangelogs + dh_installdocs + dh_installdebconf + dh_installinit --no-start + dh_python2=python + dh_compress + dh_link + dh_fixperms + dh_installdeb + dh_shlibdeps + dh_gencontrol + dh_md5sums + dh_builddeb +binary: binary-indep +.PHONY: build clean binary-indep binary install configure diff --git a/admin/Sources/Clients/ogagent/oglive/debian/source/format b/admin/Sources/Clients/ogagent/oglive/debian/source/format new file mode 100644 index 00000000..9f674278 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/debian/source/format @@ -0,0 +1 @@ +3.0 (native)
\ No newline at end of file diff --git a/admin/Sources/Clients/ogagent/oglive/readme.txt b/admin/Sources/Clients/ogagent/oglive/readme.txt new file mode 100644 index 00000000..a2771def --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/readme.txt @@ -0,0 +1,3 @@ +OGAgent is the agent intended for OpengGnsys interaction. + +Please, visit https://opengnsys.es for more information diff --git a/admin/Sources/Clients/ogagent/oglive/scripts/OGAgentTool b/admin/Sources/Clients/ogagent/oglive/scripts/OGAgentTool new file mode 100644 index 00000000..5b300523 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/scripts/OGAgentTool @@ -0,0 +1,6 @@ +#!/bin/sh + +FOLDER=/usr/share/OGAgent + +cd $FOLDER +python OGAgentUser.py $@ diff --git a/admin/Sources/Clients/ogagent/oglive/scripts/OGAgentTool-startup b/admin/Sources/Clients/ogagent/oglive/scripts/OGAgentTool-startup new file mode 100644 index 00000000..bb3a848e --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/scripts/OGAgentTool-startup @@ -0,0 +1,10 @@ +#!/bin/sh + +# Simple hack to wait for systray to be present +# Exec tool if not already runned by session manager +ps -ef | grep "$USER" | grep -v grep | grep -v OGAgentTool-startup | grep 'OGAgentTool' -q +# If not already running +if [ $? -eq 1 ]; then + sleep 5 + exec /usr/bin/OGAgentTool +fi
\ No newline at end of file diff --git a/admin/Sources/Clients/ogagent/oglive/scripts/ogagent b/admin/Sources/Clients/ogagent/oglive/scripts/ogagent new file mode 100644 index 00000000..1bcc29b0 --- /dev/null +++ b/admin/Sources/Clients/ogagent/oglive/scripts/ogagent @@ -0,0 +1,6 @@ +#!/bin/sh + +FOLDER=/usr/share/OGAgent + +cd $FOLDER +python -m opengnsys.linux.OGAgentService $@ diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/linux/operations.py b/admin/Sources/Clients/ogagent/src/opengnsys/linux/operations.py index 0c08f95f..db6ea18c 100644 --- a/admin/Sources/Clients/ogagent/src/opengnsys/linux/operations.py +++ b/admin/Sources/Clients/ogagent/src/opengnsys/linux/operations.py @@ -152,11 +152,7 @@ def reboot(flags=0): import threading threading._DummyThread._Thread__stop = lambda x: 42 - # Check for OpenGnsys Client or GNU/Linux distribution. - if os.path.exists('/scripts/oginit'): - subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/reboot', shell=True) - else: - subprocess.call(['/sbin/reboot']) + subprocess.call(['/sbin/reboot']) def poweroff(flags=0): @@ -168,11 +164,7 @@ def poweroff(flags=0): import threading threading._DummyThread._Thread__stop = lambda x: 42 - # Check for OpenGnsys Client or GNU/Linux distribution. - if os.path.exists('/scripts/oginit'): - subprocess.call('source /opt/opengnsys/etc/preinit/loadenviron.sh; /opt/opengnsys/scripts/poweroff', shell=True) - else: - subprocess.call(['/sbin/poweroff']) + subprocess.call(['/sbin/poweroff']) def logoff(): diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py b/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py index 850dfb0f..d9704259 100644 --- a/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -25,31 +25,30 @@ # 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. -''' +""" @author: Ramón M. Gómez, ramongomez at us dot es -''' +""" from __future__ import unicode_literals -import subprocess -import threading -import thread import os -import platform -import time import random import shutil import string +import threading +import time import urllib -from opengnsys.workers import ServerWorker -from opengnsys import REST, RESTError +from opengnsys import REST from opengnsys import operations from opengnsys.log import logger from opengnsys.scriptThread import ScriptExecutorThread +from opengnsys.workers import ServerWorker +from six.moves.urllib import parse + # Error handler decorator. -def catchBackgroundError(fnc): +def catch_background_error(fnc): def wrapper(*args, **kwargs): this = args[0] try: @@ -59,11 +58,39 @@ def catchBackgroundError(fnc): return wrapper +def check_locked_partition(sync=False): + """ + Decorator to check if a partition is locked + """ + def outer(fnc): + def wrapper(*args, **kwargs): + part_id = 'None' + try: + this, path, get_params, post_params, server = args # @UnusedVariable + part_id = post_params['disk'] + post_params['part'] + if this.locked.get(part_id, False): + this.locked[part_id] = True + fnc(*args, **kwargs) + else: + return 'partition locked' + except Exception as e: + this.locked[part_id] = False + return 'error {}'.format(e) + finally: + if sync is True: + this.locked[part_id] = False + logger.debug('Lock status: {} {}'.format(fnc, this.locked)) + return wrapper + return outer + + class OpenGnSysWorker(ServerWorker): name = 'opengnsys' interface = None # Bound interface for OpenGnsys + REST = None # REST object loggedin = False # User session flag - locked = {} + locked = {} # Locked partitions + commands = [] # Running commands random = None # Random string for secure connections length = 32 # Random string length @@ -82,9 +109,13 @@ class OpenGnSysWorker(ServerWorker): """ Sends OGAgent activation notification to OpenGnsys server """ - self.cmd = None # Ensure cfg has required configuration variables or an exception will be thrown - self.REST = REST(self.service.config.get('opengnsys', 'remote')) + url = self.service.config.get('opengnsys', 'remote') + if operations.os_type == 'ogLive' and 'oglive' in os.environ: + # Replacing server IP if its running on ogLive clinet + logger.debug('Activating on ogLive client, new server is {}'.format(os.environ['oglive'])) + url = parse.urlsplit(url)._replace(netloc=os.environ['oglive']).geturl() + self.REST = REST(url) # Get network interfaces until they are active or timeout (5 minutes) for t in range(0, 300): try: @@ -119,8 +150,13 @@ class OpenGnSysWorker(ServerWorker): try: try: self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, - 'secret': self.random, 'ostype': operations.osType, - 'osversion': operations.osVersion}) + 'secret': self.random, 'ostype': operations.os_type, + 'osversion': operations.os_version}) + # New web compatibility. + #self.REST.sendMessage('clients/statuses', {'mac': self.interface.mac, 'ip': self.interface.ip, + # 'secret': self.random, 'ostype': operations.os_type, + # 'osversion': operations.os_version, + # 'status': operations.os_type.lower()}) except: # Trying to initialize on alternative server, if defined # (used in "exam mode" from the University of Seville) @@ -128,6 +164,11 @@ class OpenGnSysWorker(ServerWorker): self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, 'secret': self.random, 'ostype': operations.osType, 'osversion': operations.osVersion, 'alt_url': True}) + # New web compatibility. + #self.REST.sendMessage('clients/statuses', {'mac': self.interface.mac, 'ip': self.interface.ip, + # 'secret': self.random, 'ostype': operations.os_type, + # 'osversion': operations.os_version, + # 'status': operations.os_type.lower()}) except: logger.error('Initialization error') @@ -136,8 +177,11 @@ class OpenGnSysWorker(ServerWorker): Sends OGAgent stopping notification to OpenGnsys server """ logger.debug('onDeactivation') - self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip, - 'ostype': operations.osType, 'osversion': operations.osVersion}) + #self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip, + # 'ostype': operations.os_type, 'osversion': operations.os_version}) + self.REST.sendMessage('clients/statuses', {'mac': self.interface.mac, 'ip': self.interface.ip, + 'ostype': operations.os_type, 'osversion': operations.os_version + 'status': 'off'}) def processClientMessage(self, message, data): logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data)) @@ -150,7 +194,7 @@ class OpenGnSysWorker(ServerWorker): logger.debug('Received login for {} with language {}'.format(user, language)) self.loggedin = True self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language, - 'ostype': operations.osType, 'osversion': operations.osVersion}) + 'ostype': operations.os_type, 'osversion': operations.os_version}) def onLogout(self, user): """ @@ -190,25 +234,16 @@ class OpenGnSysWorker(ServerWorker): def process_status(self, path, getParams, postParams, server): """ - Returns client status. + Returns client status (OS type and login status). """ - res = {'status': '', 'loggedin': self.loggedin} - if platform.system() == 'Linux': # GNU/Linux - # Check if it's OpenGnsys Client. - if os.path.exists('/scripts/oginit'): - # Check if OpenGnsys Client is busy. - if self.locked: - res['status'] = 'BSY' - else: - res['status'] = 'OPG' - else: - # Check if there is an active session. - res['status'] = 'LNX' - elif platform.system() == 'Windows': # Windows - # Check if there is an active session. - res['status'] = 'WIN' - elif platform.system() == 'Darwin': # Mac OS X ?? - res['status'] = 'OSX' + res = {'loggedin': self.loggedin} + try: + res['status'] = operations.os_type.lower() + except KeyError: + res['status'] = '' + # Check if OpenGnsys Client is busy + if res['status'] == 'oglive' and self.locked: + res['status'] = 'busy' return res def process_reboot(self, path, getParams, postParams, server): @@ -277,3 +312,157 @@ class OpenGnSysWorker(ServerWorker): def process_client_popup(self, params): self.REST.sendMessage('popup_done', params) + + def process_config(self, path, get_params, post_params, server): + """ + Returns client configuration + :param path: + :param get_params: + :param post_params: + :param server: + :return: object + """ + serialno = '' # Serial number + storage = [] # Storage configuration + warnings = 0 # Number of warnings + logger.debug('Received getconfig operation') + self.checkSecret(server) + # Processing data + for row in operations.get_configuration().split(';'): + cols = row.split(':') + if len(cols) == 1: + if cols[0] != '': + # Serial number + serialno = cols[0] + else: + # Skip blank rows + pass + elif len(cols) == 7: + disk, npart, tpart, fs, opsys, size, usage = cols + try: + if int(npart) == 0: + # Disk information + storage.append({'disk': int(disk), 'parttable': int(tpart), 'size': int(size)}) + else: + # Partition information + storage.append({'disk': int(disk), 'partition': int(npart), 'parttype': tpart, + 'filesystem': fs, 'operatingsystem': opsys, 'size': int(size), + 'usage': int(usage)}) + except ValueError: + logger.warn('Configuration parameter error: {}'.format(cols)) + warnings += 1 + else: + # Logging warnings + logger.warn('Configuration data error: {}'.format(cols)) + warnings += 1 + # Returning configuration data and count of warnings + return {'serialno': serialno, 'storage': storage, 'warnings': warnings} + + def task_command(self, code, route, op_id): + """ + Task to execute a command + :param code: Code to execute + :param route: server callback REST route to return results + :param op_id: operation id. + """ + # Executing command + (stat, out, err) = operations.exec_command(code) + # Removing command from the list + for c in self.commands: + if c.getName() == op_id: + self.commands.remove(c) + # Sending results + self.REST.sendMessage(route, {'client': self.interface.ip, 'trace': op_id, 'status': stat, 'output': out, + 'error': err}) + + def process_command(self, path, get_params, post_params, server): + """ + Launches a thread to executing a command + :param path: ignored + :param get_params: ignored + :param post_params: object with format: + id: operation id. + script: command code + redirect_url: callback REST route + :param server: headers data + :rtype: object with launching status + """ + logger.debug('Received command operation with params: {}'.format(post_params)) + self.checkSecret(server) + # Processing data + try: + script = post_params.get('script') + op_id = post_params.get('id') + route = post_params.get('redirect_url') + # Checking if the thread id. exists + for c in self.commands: + if c.getName() == str(op_id): + raise Exception('Task id. already exists: {}'.format(op_id)) + # Launching a new thread + thr = threading.Thread(name=op_id, target=self.task_command, args=(script, route, op_id)) + thr.start() + self.commands.append(thr) + except Exception as e: + logger.error('Got exception {}'.format(e)) + return {'error': e} + return {'op': 'launched'} + + def process_execinfo(self, path, get_params, post_params, server): + """ + Returns running commands information + :param path: + :param get_params: + :param post_params: + :param server: + :return: object + """ + data = [] + logger.debug('Received execinfo operation') + self.checkSecret(server) + # Returning the arguments of all running threads + for c in self.commands: + if c.is_alive(): + data.append(c.__dict__['_Thread__args']) + return data + + def process_stopcmd(self, path, get_params, post_params, server): + logger.debug('Received stopcmd operation with params {}:'.format(post_params)) + self.checkSecret(server) + op_id = post_params.get('trace') + for c in self.commands: + if c.is_alive() and c.getName() == str(op_id): + c._Thread__stop() + return {"stopped": op_id} + return {} + + def process_hardware(self, path, get_params, post_params, server): + """ + Returns client's hardware profile + :param path: + :param get_params: + :param post_params: + :param server: + """ + data = [] + logger.debug('Received hardware operation') + self.checkSecret(server) + # Processing data + try: + for comp in operations.get_hardware(): + data.append({'component': comp.split('=')[0], 'value': comp.split('=')[1]}) + except: + pass + # Return list of hardware components + return data + + def process_software(self, path, get_params, post_params, server): + """ + Returns software profile installed on an operating system + :param path: + :param get_params: + :param post_params: + :param server: + :return: + """ + logger.debug('Received software operation with params: {}'.format(post_params)) + return operations.get_software(post_params.get('disk'), post_params.get('part')) diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/oglive/__init__.py b/admin/Sources/Clients/ogagent/src/opengnsys/oglive/__init__.py new file mode 100644 index 00000000..3a98c780 --- /dev/null +++ b/admin/Sources/Clients/ogagent/src/opengnsys/oglive/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. + +''' +@author: Adolfo Gómez, dkmaster at dkmon dot com +''' +from __future__ import unicode_literals diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/oglive/daemon.py b/admin/Sources/Clients/ogagent/src/opengnsys/oglive/daemon.py new file mode 100644 index 00000000..3753808a --- /dev/null +++ b/admin/Sources/Clients/ogagent/src/opengnsys/oglive/daemon.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. +''' +@author: : http://www.jejik.com/authors/sander_marechal/ +@see: : http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ +''' + +from __future__ import unicode_literals +import sys +import os +import time +import atexit +from opengnsys.log import logger + +from signal import SIGTERM + + +class Daemon: + """ + A generic daemon class. + + Usage: subclass the Daemon class and override the run() method + """ + def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.pidfile = pidfile + + def daemonize(self): + """ + do the UNIX double-fork magic, see Stevens' "Advanced + Programming in the UNIX Environment" for details (ISBN 0201563177) + http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + """ + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + logger.error("fork #1 error: {}".format(e)) + sys.stderr.write("fork #1 failed: {}\n".format(e)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as e: + logger.error("fork #2 error: {}".format(e)) + sys.stderr.write("fork #2 failed: {}\n".format(e)) + sys.exit(1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = open(self.stdin, 'r') + so = open(self.stdout, 'a+') + se = open(self.stderr, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # write pidfile + atexit.register(self.delpid) + pid = str(os.getpid()) + with open(self.pidfile, 'w+') as f: + f.write("{}\n".format(pid)) + + def delpid(self): + try: + os.remove(self.pidfile) + except Exception: + # Not found/not permissions or whatever... + pass + + def start(self): + """ + Start the daemon + """ + logger.debug('Starting daemon') + # Check for a pidfile to see if the daemon already runs + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid: + message = "pidfile {} already exist. Daemon already running?\n".format(pid) + logger.error(message) + sys.stderr.write(message) + sys.exit(1) + + # Start the daemon + self.daemonize() + try: + self.run() + except Exception as e: + logger.error('Exception running process: {}'.format(e)) + + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + + def stop(self): + """ + Stop the daemon + """ + # Get the pid from the pidfile + try: + pf = open(self.pidfile, 'r') + pid = int(pf.read().strip()) + pf.close() + except IOError: + pid = None + + if pid is None: + message = "pidfile {} does not exist. Daemon not running?\n".format(self.pidfile) + logger.info(message) + # sys.stderr.write(message) + return # not an error in a restart + + # Try killing the daemon process + try: + for i in range(10): + os.kill(pid, SIGTERM) + time.sleep(1) + except OSError as err: + if err.errno == 3: # No such process + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + sys.stderr.write(err) + sys.exit(1) + + def restart(self): + """ + Restart the daemon + """ + self.stop() + self.start() + + # Overridables + def run(self): + """ + You should override this method when you subclass Daemon. It will be called after the process has been + daemonized by start() or restart(). + """ diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/oglive/operations.py b/admin/Sources/Clients/ogagent/src/opengnsys/oglive/operations.py new file mode 100644 index 00000000..342cfb06 --- /dev/null +++ b/admin/Sources/Clients/ogagent/src/opengnsys/oglive/operations.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2014 Virtual Cable S.L. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * 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. +# * Neither the name of Virtual Cable S.L. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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. + +""" +@author: Ramón M. Gómez, ramongomez at us dot es +""" +from __future__ import unicode_literals + +import socket +import platform +import fcntl +import subprocess +import struct +import array +import six +from opengnsys import utils + + +def _getMacAddr(ifname): + """ + Returns the mac address of an interface + Mac is returned as unicode utf-8 encoded + """ + if isinstance(ifname, list): + return dict([(name, _getMacAddr(name)) for name in ifname]) + if isinstance(ifname, six.text_type): + ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifname[:15]))) + return six.text_type(''.join(['%02x:' % char for char in info[18:24]])[:-1]) + except Exception: + return None + + +def _getIpAddr(ifname): + """ + Returns the ip address of an interface + Ip is returned as unicode utf-8 encoded + """ + if isinstance(ifname, list): + return dict([(name, _getIpAddr(name)) for name in ifname]) + if isinstance(ifname, six.text_type): + ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return six.text_type(socket.inet_ntoa(fcntl.ioctl( + s.fileno(), + 0x8915, # SIOCGIFADDR + struct.pack(str('256s'), ifname[:15]) + )[20:24])) + except Exception: + return None + + +def _getInterfaces(): + """ + Returns a list of interfaces names coded in utf-8 + """ + max_possible = 128 # arbitrary. raise if needed. + space = max_possible * 16 + if platform.architecture()[0] == '32bit': + offset, length = 32, 32 + elif platform.architecture()[0] == '64bit': + offset, length = 16, 40 + else: + raise OSError('Unknown arquitecture {0}'.format(platform.architecture()[0])) + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + names = array.array(str('B'), b'\0' * space) + outbytes = struct.unpack(str('iL'), fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack(str('iL'), space, names.buffer_info()[0]) + ))[0] + namestr = names.tostring() + # return namestr, outbytes + return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)] + + +def _getIpAndMac(ifname): + ip, mac = _getIpAddr(ifname), _getMacAddr(ifname) + return ip, mac + + +def _exec_ogcommand(ogcmd): + """ + Loads OpenGnsys environment variables, executes the command and returns the result + """ + ret = subprocess.check_output(ogcmd, shell=True) + return ret + + +def getComputerName(): + """ + Returns computer name, with no domain + """ + return socket.gethostname().split('.')[0] + + +def getNetworkInfo(): + """ + Obtains a list of network interfaces + :return: A "generator" of elements, that are dict-as-object, with this elements: + name: Name of the interface + mac: mac of the interface + ip: ip of the interface + """ + for ifname in _getInterfaces(): + ip, mac = _getIpAndMac(ifname) + if mac != '00:00:00:00:00:00': # Skips local interfaces + yield utils.Bunch(name=ifname, mac=mac, ip=ip) + + +def getDomainName(): + return '' + + +def get_oglive_version(): + """ + Returns ogLive Kernel version and architecture + :return: kernel version + """ + kv = platform.os.uname() + return kv[2] + ', ' + kv[4] + + +def reboot(): + """ + Simple reboot using OpenGnsys script + """ + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + _exec_ogcommand('/opt/opengnsys/scripts/reboot') + + +def poweroff(): + """ + Simple power off using OpenGnsys script + """ + # Workaround for dummy thread + if six.PY3 is False: + import threading + threading._DummyThread._Thread__stop = lambda x: 42 + + _exec_ogcommand('/opt/opengnsys/scripts/poweroff') + + +def get_configuration(): + """ + Returns client's configuration + Warning: this operation may take some time + :return: + """ + try: + _exec_ogcommand('/opt/opengnsys/interfaceAdm/getConfiguration') + # Returns content of configuration file + cfgdata = open('/tmp/getconfig', 'r').read().strip() + except IOError: + cfgdata = '' + return cfgdata + + +def exec_command(cmd): + """ + Executing a shell command + :param cmd: + :return: object with components: + output: standard output + error: error output + exit: exit code + """ + proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = proc.communicate() + stat = proc.returncode + return stat, out, err + + +def get_hardware(): + """ + Returns client's hardware list + :return: + """ + try: + filepath = _exec_ogcommand('/opt/opengnsys/scripts/listHardwareInfo').strip() + # Returns content of configuration file, skipping the header line and newline characters + with open(filepath, 'r') as f: + harddata = map(str.strip, f.readlines()[1:]) + except IOError: + harddata = '' + return harddata + + +def get_software(disk, part): + """ + Returns software list installed on an operating system + :param disk: + :param part: + :return: + """ + try: + filepath = _exec_ogcommand('/opt/opengnsys/scripts/listSoftwareInfo {} {}'.format(disk, part)).strip() + # Returns content of configuration file, skipping the header line and newline characters + with open(filepath, 'r') as f: + softdata = map(str.strip, f.readlines()) + except IOError: + softdata = '' + return softdata diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/operations.py b/admin/Sources/Clients/ogagent/src/opengnsys/operations.py index dcfa40cb..a81d0c0d 100644 --- a/admin/Sources/Clients/ogagent/src/opengnsys/operations.py +++ b/admin/Sources/Clients/ogagent/src/opengnsys/operations.py @@ -26,25 +26,32 @@ # 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. -''' -@author: Adolfo Gómez, dkmaster at dkmon dot com -''' +""" +@author: Ramón M. Gómez, ramongomez at us dot es +""" # pylint: disable=unused-wildcard-import,wildcard-import from __future__ import unicode_literals import sys +import os + # Importing platform operations and getting operating system data. if sys.platform == 'win32': from .windows.operations import * # @UnusedWildImport - osType = 'Windows' - osVersion = getWindowsVersion() + os_type = 'Windows' + os_version = getWindowsVersion() else: if sys.platform == 'darwin': from .macos.operations import * # @UnusedWildImport - osType = 'MacOS' - osVersion = getMacosVersion().replace(',','') + os_type = 'MacOS' + os_version = getMacosVersion().replace(',', '') else: - from .linux.operations import * # @UnusedWildImport - osType = 'Linux' - osVersion = getLinuxVersion().replace(',','') + if os.path.exists('/scripts/oginit'): + from .oglive.operations import * # @UnusedWildImport + os_type = 'ogLive' + os_version = get_oglive_version().replace(',', '') + else: + from .linux.operations import * # @UnusedWildImport + os_type = 'Linux' + os_version = getLinuxVersion().replace(',', '') diff --git a/admin/Sources/Clients/ogagent/src/setup.py b/admin/Sources/Clients/ogagent/src/setup.py index 15254f4f..617d5186 100644 --- a/admin/Sources/Clients/ogagent/src/setup.py +++ b/admin/Sources/Clients/ogagent/src/setup.py @@ -139,5 +139,5 @@ setup( description='OpenGnsys Agent', author='Adolfo Gomez', author_email='agomez@virtualcable.es', - zipfile='OGAgent.zip', + zipfile='OGAgent.zip', requires=['six'] ) diff --git a/admin/WebConsole/rest/ogagent.php b/admin/WebConsole/rest/ogagent.php index be62a8d0..3e78da0e 100644 --- a/admin/WebConsole/rest/ogagent.php +++ b/admin/WebConsole/rest/ogagent.php @@ -271,4 +271,39 @@ EOD; } ); +// Processing command results (TESTING). +$app->post('/ogagent/command_done', + // 'validateClient', + function() use ($app) { + global $cmd; + + try { + // Reading parameters. + $input = json_decode($app->request()->getBody()); + $client = htmlspecialchars($input->client); + $id = htmlspecialchars($input->trace); + $status = htmlspecialchars($input->status); + $output = htmlspecialchars($input->output); + $error = htmlspecialchars($input->error); + $client = $_SERVER['REMOTE_ADDR']; + // Check sender agent type. + if (empty(preg_match('/^python-requests\//', $_SERVER['HTTP_USER_AGENT'])) or $client !== $_SERVER['REMOTE_ADDR']) { + throw new Exception("Bad OGAgent: client=$client, sender=".$_SERVER['REMOTE_ADDR'].", agent=".$_SERVER['HTTP_USER_AGENT']); + } + // TODO: truncating outputs. + if ($status == 0) { + writeLog($app->request()->getResourceUri().": Operation OK: client=$client, id=$id, output=$output"); + } else { + writeLog($app->request()->getResourceUri().": Operation ERROR: client=$client, id=$id, status=$status, error=$error"); + } + $response = ""; + jsonResponse(200, $response); + } catch (Exception $e) { + // Communication error. + $response["message"] = $e->getMessage(); + writeLog($app->request()->getResourceUri().": ERROR: ".$response["message"]); + jsonResponse(400, $response); + } + } +); diff --git a/installer/vagrant/Vagrantfile-ogagent-vbox b/installer/vagrant/Vagrantfile-ogagent-vbox index 1bf48faa..31760d65 100644 --- a/installer/vagrant/Vagrantfile-ogagent-vbox +++ b/installer/vagrant/Vagrantfile-ogagent-vbox @@ -21,7 +21,7 @@ localectl set-x11-keymap ${LANG%_*} # Update repositories. dnf install -y http://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # Install main dependencies. -dnf install -y gcc-c++ debhelper dpkg-dev pyqt4-devel rpm-build subversion samba-winbind wine.i686 mingw32-wine-gecko wine-mono cabextract xar +dnf install -y gcc-c++ perl-Digest-SHA debhelper dpkg-dev pyqt4-devel rpm-build subversion samba-winbind wine.i686 mingw32-wine-gecko wine-mono cabextract xar setsebool -P wine_mmap_zero_ignore=on mmap_low_allowed=on # Install desktop (XFCE) and GUI utils. dnf install -y @xfce-desktop-environment firefox VirtualBox-guest kmod-VirtualBox akmod-VirtualBox akmods |