From 619600303213ef64e69d8d2cccea9bffab94bd5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Sat, 22 Dec 2018 12:03:41 +0100 Subject: [PATCH 01/92] Created the CONTRIBUTING file This file is a guide on how to work with Git forks, Git Flow and GitHub --- CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..38ccd3e86 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing + +## New feature + +1. Fork the `isard-vdi/isard` repository +2. Clone **your** Isard fork and move (if you already have your fork clonned, make sure you have the latest changes: `git fetch upstream`) +3. Add the upstream remote: `git remote add upstream https://github.com/isard-vdi/isard` + +1. Initialize Git Flow: `git flow init` +2. Create the feature: `git flow feature start ` +3. Work and commit it +4. Publish the feature branch: `git flow feature publish ` +5. Create a pull request from `your username/isard` `feature/` to `isard-vdi/isard` `develop` branch + + + +## New release + +1. Clone the `isard-vdi/isard` repository +2. Create the release: `git flow release start X.X.X` +3. Publish the release branch: `git flow publish release X.X.X` +4. Create a pull request from the `isard-vdi/isard` `release/X.X.X` to `isard-vdi/isard` `master` +5. Update the Changelog, the `docker-compose.yml` file... +6. Merge the release to master +7. Create a new release to GitHub using as description the Changelog for the version +8. Pull the changes to the local `isard-vdi/isard` clone +9. Change to the new version tag: `git checkout X.X.X` +10. Build the Docker images and push them to Docker Hub + From 036ddfb334125129a71f47cc624fd6a8b6339c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Sun, 23 Dec 2018 18:05:55 +0100 Subject: [PATCH 02/92] Added badges to the README Now the README has more appeal and has some useful links --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 40655f16c..81d37e5a8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Isard**VDI** +[![](https://img.shields.io/github/release/isard-vdi/isard.svg)](https://github.com/isard-vdi/isard/releases) [![](https://img.shields.io/badge/docker--compose-ready-blue.svg)](https://github.com/isard-vdi/isard/blob/master/docker-compose.yml) [![](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://isardvdi.readthedocs.io/en/latest/) [![](https://img.shields.io/badge/license-AGPL%20v3.0-brightgreen.svg)](https://github.com/isard-vdi/isard/blob/master/LICENSE) + Open Source KVM Virtual Desktops based on KVM Linux and dockers. - Engine that monitors hypervisors and domains (desktops) From ec57febb68f7c7b03b4ae52fd16bbdf9dc0bd4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Mon, 24 Dec 2018 15:33:45 +0100 Subject: [PATCH 03/92] Improved the Docker Compose file Also created a file to build and tag correctly all the Docker images --- build-docker-images.sh | 34 +++++++ docker-compose.yml | 190 ++++++++++++++++++----------------- dockers/app/Dockerfile | 6 +- dockers/app/supervisord.conf | 5 +- src/isard.conf.docker | 2 +- 5 files changed, 138 insertions(+), 99 deletions(-) create mode 100755 build-docker-images.sh diff --git a/build-docker-images.sh b/build-docker-images.sh new file mode 100755 index 000000000..c5e90a1a2 --- /dev/null +++ b/build-docker-images.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Check that the version number was provided +if [ -z "$1" ]; then + echo "You need to specify a IsardVDI version! e.g. '1.1.0'" + exit 1 +fi + +MAJOR=${1:0:1} +MINOR=${1:0:3} +PATCH=$1 + +# If a command fails, the whole script is going to stop +set -e + +# Checkout to the specified version tag +git checkout $1 > /dev/null + +# Array containing all the images to build +images=( + alpine-pandas + nginx + hypervisor + app +) + +# Build all the images and tag them correctly +for image in "${images[@]}"; do + echo -e "\n\n\n" + echo "Building $image" + echo -e "\n\n\n" + docker build -f=dockers/$image/Dockerfile -t isard/$image:latest -t isard/$image:$MAJOR -t isard/$image:$MINOR -t isard/$image:$PATCH . +done + diff --git a/docker-compose.yml b/docker-compose.yml index b6bad79b2..5845e6904 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,97 +1,103 @@ -version: '2' +version: "3.2" services: -# Will take long time to build, better get dockerhub already build -# isard-alpine-pandas: -# image: isard/alpine-pandas:1.0.0 -# build: -# context: . -# dockerfile: dockers/alpine-pandas/Dockerfile - isard-database: - restart: always - image: rethinkdb - hostname: isard-database - volumes: - - "/opt/isard/database:/data" - expose: - - "28015" - networks: - main: - aliases: - - rethinkdb + isard-database: + volumes: + - type: bind + source: /opt/isard/database + target: /data + read_only: false + networks: + - isard_network + image: rethinkdb + restart: always - isard-nginx: - restart: always - image: isard/nginx:1.0.0 - build: - context: . - dockerfile: dockers/nginx/Dockerfile - ports: - - "80:80" - - "443:443" - volumes: - - "/opt/isard/certs/default:/etc/nginx/external" - - "/opt/isard/logs/nginx:/var/log/nginx" - hostname: isard-nginx - links: - - "isard-app" - networks: - main: - aliases: - - isard-nginx - - isard-hypervisor: - restart: always - image: isard/hypervisor:1.0.0 - build: - context: . - dockerfile: dockers/hypervisor/Dockerfile - hostname: isard-hypervisor - ports: - - "5900-5949:5900-5949" - - "55900-55949:55900-55949" - expose: - - "22" - privileged: true - volumes: - - "sshkeys:/root/.ssh" - - "/opt/isard:/isard" - - "/opt/isard/certs/default:/etc/pki/libvirt-spice" - networks: - main: - aliases: - - isard-hypervisor - command: /usr/bin/supervisord -c /etc/supervisord.conf + isard-nginx: + volumes: + - type: bind + source: /opt/isard/certs/default + target: /etc/nginx/external + read_only: false + - type: bind + source: /opt/isard/logs/nginx + target: /var/log/nginx + read_only: false + ports: + - target: 80 + published: 80 + protocol: tcp + mode: host + - target: 443 + published: 443 + protocol: tcp + mode: host + networks: + - isard_network + image: isard/nginx:1.0.0 + restart: always + + isard-hypervisor: + volumes: + - type: volume + source: sshkeys + target: /root/.ssh + read_only: false + - type: bind + source: /opt/isard + target: /isard + read_only: false + - type: bind + source: /opt/isard/certs/default + target: /etc/pki/libvirt-spice + read_only: false + ports: + - "5900-5949:5900-5949" + - "55900-55949:55900-55949" + networks: + - isard_network + image: isard/hypervisor:1.0.0 + privileged: true + restart: always - isard-app: - restart: always - image: isard/app:1.0.0 - build: - context: . - dockerfile: dockers/app/Dockerfile - links: - - "isard-database" - - "isard-hypervisor" - hostname: isard-app - volumes: - - "sshkeys:/root/.ssh" - - "/opt/isard/certs:/certs" - - "/opt/isard/logs:/isard/logs" - - "/opt/isard/database/wizard:/isard/install/wizard" - - "/opt/isard/backups:/isard/backups" - - "/opt/isard/uploads:/isard/uploads" - expose: - - "5000" - environment: - PYTHONUNBUFFERED: 0 - extra_hosts: - - "isard-engine:127.0.0.1" - networks: - main: - aliases: - - isard-app - command: /usr/bin/supervisord -c /etc/supervisord.conf + isard-app: + volumes: + - type: volume + source: sshkeys + target: /root/.ssh + read_only: false + - type: bind + source: /opt/isard/certs/default + target: /etc/pki/libvirt-spice + read_only: false + - type: bind + source: /opt/isard/logs + target: /isard/logs + read_only: false + - type: bind + source: /opt/isard/database/wizard + target: /isard/install/wizard + read_only: false + - type: bind + source: /opt/isard/backups + target: /isard/backups + read_only: false + - type: bind + source: /opt/isard/uploads + target: /isard/uploads + read_only: false + extra_hosts: + - "isard-engine:127.0.0.1" + networks: + - isard_network + image: isard/app:1.0.0 + restart: always + depends_on: + - isard-database + - isard-hypervisor + - isard-nginx -networks: - main: volumes: - sshkeys: + sshkeys: + +networks: + isard_network: + external: false diff --git a/dockers/app/Dockerfile b/dockers/app/Dockerfile index 40713db9f..643a056eb 100644 --- a/dockers/app/Dockerfile +++ b/dockers/app/Dockerfile @@ -24,7 +24,7 @@ RUN apk add supervisor RUN mkdir -p /var/log/supervisor COPY dockers/app/supervisord.conf /etc/supervisord.conf +EXPOSE 5000 + COPY dockers/app/certs.sh / -CMD /usr/bin/supervisord -c /etc/supervisord.conf -#CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] -#CMD ["sh", "/init.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/dockers/app/supervisord.conf b/dockers/app/supervisord.conf index 429e2c878..85bcfeea5 100644 --- a/dockers/app/supervisord.conf +++ b/dockers/app/supervisord.conf @@ -1,4 +1,5 @@ [supervisord] +user=root nodaemon=true logfile=/dev/stdout loglevel=error @@ -14,7 +15,6 @@ stdout_logfile=/isard/logs/certs.log stderr_logfile=/isard/logs/certs-error.log [program:webapp] -user=root directory=/isard command=python3 run_webapp.py #1>/isard/logs/webapp.log 2>/isard/logs/webapp-error.log @@ -26,9 +26,8 @@ stdout_logfile=/isard/logs/webapp.log stderr_logfile=/isard/logs/webapp-error.log [program:engine] -user=root directory=/isard -command=sh -c "sleep 5 && python3 run_engine.py" +command=sh -c "python3 run_engine.py" # 1>/isard/logs/engine.log 2>/isard/logs/engine-error.log" #command=python3 run_engine.py autostart=true diff --git a/src/isard.conf.docker b/src/isard.conf.docker index 96d041e0a..b6c409679 100644 --- a/src/isard.conf.docker +++ b/src/isard.conf.docker @@ -1,5 +1,5 @@ [RETHINKDB] -HOST: rethinkdb +HOST: isard-database PORT: 28015 DBNAME: isard From 76152730e120435909ecdf381f3e8623d84d317e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Mon, 24 Dec 2018 20:08:41 +0100 Subject: [PATCH 04/92] Fixed certificates Now all the trafic goes encrypted --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5845e6904..432e2b58a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,8 +65,8 @@ services: target: /root/.ssh read_only: false - type: bind - source: /opt/isard/certs/default - target: /etc/pki/libvirt-spice + source: /opt/isard/certs/ + target: /certs read_only: false - type: bind source: /opt/isard/logs From e9068504ca74b7a99e4692fc074119d848d9cfc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Tue, 25 Dec 2018 01:32:37 +0100 Subject: [PATCH 05/92] Fixed all the errors Also fixed a bug that prevented the Dockers to function after a restart or executing `docker-compose down` and `docker-compose up` --- docker-compose.yml | 2 +- dockers/app/supervisord.conf | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 432e2b58a..10a9330e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,7 +65,7 @@ services: target: /root/.ssh read_only: false - type: bind - source: /opt/isard/certs/ + source: /opt/isard/certs target: /certs read_only: false - type: bind diff --git a/dockers/app/supervisord.conf b/dockers/app/supervisord.conf index 85bcfeea5..619302229 100644 --- a/dockers/app/supervisord.conf +++ b/dockers/app/supervisord.conf @@ -17,7 +17,6 @@ stderr_logfile=/isard/logs/certs-error.log [program:webapp] directory=/isard command=python3 run_webapp.py -#1>/isard/logs/webapp.log 2>/isard/logs/webapp-error.log autostart=true autorestart=true startsecs=2 @@ -27,9 +26,7 @@ stderr_logfile=/isard/logs/webapp-error.log [program:engine] directory=/isard -command=sh -c "python3 run_engine.py" -# 1>/isard/logs/engine.log 2>/isard/logs/engine-error.log" -#command=python3 run_engine.py +command=sh -c "virsh -c qemu+ssh://isard-hypervisor/system quit && python3 run_engine.py" autostart=true autorestart=false startsecs=2 From e338f088567764d3978f4eb6b68f831220302077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Tue, 25 Dec 2018 01:35:59 +0100 Subject: [PATCH 06/92] Be less specific in the IsardVDI versions With this change, the users are going to be able to download the hotfixes without changing the Docker Compose file. This ensures more stability and security for the end user --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 10a9330e9..f47c492ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: mode: host networks: - isard_network - image: isard/nginx:1.0.0 + image: isard/nginx:1.0 restart: always isard-hypervisor: @@ -54,7 +54,7 @@ services: - "55900-55949:55900-55949" networks: - isard_network - image: isard/hypervisor:1.0.0 + image: isard/hypervisor:1.0 privileged: true restart: always @@ -88,7 +88,7 @@ services: - "isard-engine:127.0.0.1" networks: - isard_network - image: isard/app:1.0.0 + image: isard/app:1.0 restart: always depends_on: - isard-database From 675ee5ca287ac439feacab69cc1de8a931a1026f Mon Sep 17 00:00:00 2001 From: beto Date: Wed, 26 Dec 2018 12:08:33 +0100 Subject: [PATCH 07/92] changes in init threads, testing pendent --- dockers/devel-debug.yml | 4 +- src/engine/config.py | 2 +- src/engine/models/manager_hypervisors.py | 82 ++++++++++++++++++------ src/engine/services/db/domains.py | 21 +++++- src/engine/services/lib/qcow.py | 3 + src/engine/services/threads/threads.py | 7 +- 6 files changed, 96 insertions(+), 23 deletions(-) diff --git a/dockers/devel-debug.yml b/dockers/devel-debug.yml index a80681312..f1829d8bb 100644 --- a/dockers/devel-debug.yml +++ b/dockers/devel-debug.yml @@ -67,7 +67,7 @@ services: isard-app: restart: always - image: isard/app:1.0.0 + image: isard/app:1.0.1b build: context: . dockerfile: dockers/app_devel/Dockerfile @@ -78,7 +78,7 @@ services: volumes: ##### - only devel - ############################ - "/opt/isard_devel/src:/isard" - - "/opt/ipython_profile_default:/root/.ipython/profile_default" + #- "/opt/ipython_profile_default:/root/.ipython/profile_default" ################################################# - "sshkeys:/root/.ssh" - "/opt/isard/certs:/certs" diff --git a/src/engine/config.py b/src/engine/config.py index c85dba735..95140be7e 100644 --- a/src/engine/config.py +++ b/src/engine/config.py @@ -61,7 +61,7 @@ POLLING_INTERVAL_TRANSITIONAL_STATES = rconfig['intervals']['transitional_states_polling'] GRAFANA = grafana -TRANSITIONAL_STATUS = ('Starting', 'Stopping') +TRANSITIONAL_STATUS = ('Starting', 'Stopping', 'Deleting') # CONFIG_DICT = {k: {l[0]:l[1] for l in c.items(k)} for k in c.sections()} diff --git a/src/engine/models/manager_hypervisors.py b/src/engine/models/manager_hypervisors.py index 51e3b46aa..fb45ed971 100644 --- a/src/engine/models/manager_hypervisors.py +++ b/src/engine/models/manager_hypervisors.py @@ -28,6 +28,7 @@ set_unknown_domains_not_in_hyps, get_domain, remove_domain, update_domain_history_from_id_domain from engine.services.db.domains import update_domain_status, update_domain_start_after_created, update_domain_delete_after_stopped from engine.services.lib.functions import get_threads_running, get_tid, engine_restart +from engine.services.lib.qcow import test_hypers_disk_operations from engine.services.log import logs from engine.services.threads.download_thread import launch_thread_download_changes from engine.services.threads.threads import launch_try_hyps, set_domains_coherence, launch_thread_worker, \ @@ -50,7 +51,7 @@ def __init__(self, launch_threads=True, with_status_threads=True, status_polling_interval=STATUS_POLLING_INTERVAL, test_hyp_fail_interval=TEST_HYP_FAIL_INTERVAL): - logs.main.info('MAIN PID: {}'.format(get_tid())) + logs.main.info('MAIN TID: {}'.format(get_tid())) self.time_between_polling = TIME_BETWEEN_POLLING self.polling_interval_background = POLLING_INTERVAL_BACKGROUND @@ -181,6 +182,8 @@ def launch_threads_disk_and_long_operations(self): self.manager.hypers_disk_operations = get_hypers_disk_operations() + test_hypers_disk_operations(self.manager.hypers_disk_operations) + for hyp_disk_operations in self.manager.hypers_disk_operations: hyp_long_operations = hyp_disk_operations d = get_hyp_hostname_user_port_from_id(hyp_disk_operations) @@ -201,6 +204,17 @@ def launch_threads_disk_and_long_operations(self): ) def test_hyps_and_start_threads(self): + """If status of hypervisor is Error or Offline and are enabled, + this function try to connect and launch threads. + If hypervisor pass connection test, status change to ReadyToStart, + then change to StartingThreads previous to launch threads, when + threads are running state is Online. Status sequence is: + (Offline,Error) => ReadyToStart => StartingThreads => (Online,Error)""" + + # DISK_OPERATIONS: + if len(self.manager.t_disk_operations) == 0: + self.launch_threads_disk_and_long_operations() + l_hyps_to_test = get_hyps_with_status(list_status=['Error', 'Offline'], empty=True) @@ -209,7 +223,11 @@ def test_hyps_and_start_threads(self): 'user': d['user'] if 'user' in d.keys() else 'root'} for d in l_hyps_to_test} + #TRY hypervisor connexion and UPDATE hypervisors status + # update status: ReadyToStart if all ok launch_try_hyps(dict_hyps_to_test) + + #hyp_hostnames of hyps ready to start dict_hyps_ready = self.manager.dict_hyps_ready = get_hyps_ready_to_start() if len(dict_hyps_ready) > 0: @@ -219,31 +237,39 @@ def test_hyps_and_start_threads(self): if self.manager.t_events is None: logs.main.info('launching hypervisor events thread') self.manager.t_events = launch_thread_hyps_event(dict_hyps_ready) - else: - #if new hypervisor has added then add hypervisor to receive events - logs.main.info('hypervisors added to thread events') - logs.main.info(pprint.pformat(dict_hyps_ready)) - self.manager.t_events.hyps.update(dict_hyps_ready) - for hyp_id, hostname in self.manager.t_events.hyps.items(): - self.manager.t_events.add_hyp_to_receive_events(hyp_id) + # else: + # #if new hypervisor has added then add hypervisor to receive events + # logs.main.info('hypervisors added to thread events') + # logs.main.info(pprint.pformat(dict_hyps_ready)) + # self.manager.t_events.hyps.update(dict_hyps_ready) + # for hyp_id, hostname in self.manager.t_events.hyps.items(): + # self.manager.t_events.add_hyp_to_receive_events(hyp_id) + set_unknown_domains_not_in_hyps(dict_hyps_ready.keys()) set_domains_coherence(dict_hyps_ready) pools = set() for hyp_id, hostname in dict_hyps_ready.items(): update_hyp_status(hyp_id, 'StartingThreads') - # start worker thread + + # launch worker thread self.manager.t_workers[hyp_id], self.manager.q.workers[hyp_id] = launch_thread_worker(hyp_id) + + # LAUNCH status thread if self.manager.with_status_threads is True: self.manager.t_status[hyp_id] = launch_thread_status(hyp_id, self.manager.STATUS_POLLING_INTERVAL) + # ADD hyp to receive_events + self.manager.t_events.add_hyp_to_receive_events(hyp_id) + # self.manager.launch_threads(hyp_id) # INFO TO DEVELOPER FALTA VERIFICAR QUE REALMENTE ESTÁN ARRANCADOS LOS THREADS?? # comprobar alguna variable a true en alguno de los threads update_hyp_status(hyp_id, 'Online') pools.update(get_pools_from_hyp(hyp_id)) + #if hypervisor no in pools defined in manager add it for id_pool in pools: if id_pool not in self.manager.pools.keys(): self.manager.pools[id_pool] = PoolHypervisors(id_pool, self.manager, len(dict_hyps_ready)) @@ -254,7 +280,9 @@ def run(self): q = self.manager.q.background first_loop = True - clean_intermediate_status() + # if domains have intermedite states (updating, download_aborting...) + # to Failed or Delete + clean_intermediate_states() l_hyps_to_test = get_hyps_with_status(list_status=['Error', 'Offline'], empty=True) while len(l_hyps_to_test) == 0: @@ -263,45 +291,62 @@ def run(self): l_hyps_to_test = get_hyps_with_status(list_status=['Error', 'Offline'], empty=True) while self.manager.quit is False: + #################################################################### + ### MAIN LOOP ###################################################### + # ONLY FOR DEBUG logs.main.debug('##### THREADS ##################') - get_threads_running() - self.manager.update_info_threads_engine() + theads_running = get_threads_running() + alive, dead, not_defined = self.manager.update_info_threads_engine() - # DISK_OPERATIONS: - if len(self.manager.t_disk_operations) == 0: - self.launch_threads_disk_and_long_operations() + # Threads that must be running always, with or withouth hypervisor + # changes_hyp, changes_domains, disk_operations and long_operations, + # downloads_changes, events, broom # TEST HYPS AND START THREADS FROM RETHINK self.test_hyps_and_start_threads() - # LAUNCH CHANGES THREADS + # LAUNCH MAIN THREADS if first_loop is True: update_table_field('engine', 'engine', 'status_all_threads', 'Starting') + # launch changes_hyp thread self.manager.t_changes_hyps = self.manager.HypervisorChangesThread('changes_hyp', self.manager) self.manager.t_changes_hyps.daemon = True self.manager.t_changes_hyps.start() + #launch changes_domains_thread self.manager.t_changes_domains = self.manager.DomainsChangesThread('changes_domains', self.manager) self.manager.t_changes_domains.daemon = True self.manager.t_changes_domains.start() + #launch downloads changes thread logs.main.debug('Launching Download Changes Thread') self.manager.t_downloads_changes = launch_thread_download_changes(self.manager) + #launch brrom thread self.manager.t_broom = launch_thread_broom(self.manager) + #launch events thread + logs.main.debug('launching hypervisor events thread') + self.manager.t_events = launch_thread_hyps_event({}) + first_loop = False logs.main.info('THREADS LAUNCHED FROM BACKGROUND THREAD') update_table_field('engine', 'engine', 'status_all_threads', 'Starting') + while True: + #wait all sleep(0.1) alive, dead, not_defined = self.manager.update_info_threads_engine() pprint.pprint({'alive':alive, 'dead':dead, 'not_defined':not_defined}) - if len(not_defined) == 0 and len(dead) == 0: + #if thread events is None and len(dict_hyps ready) == 0, must recheck hypers + if len(not_defined) > 0 and len(self.manager.dict_hyps_ready) == 0: + sleep(3) + self.test_hyps_and_start_threads() + elif len(not_defined) == 0 and len(dead) == 0: update_table_field('engine', 'engine', 'status_all_threads', 'Started') self.manager.num_workers = len(self.manager.t_workers) self.manager.threads_started = True @@ -525,7 +570,8 @@ def run(self): if old_status == 'Stopped' and new_status == "CreatingTemplate": ui.create_template_disks_from_domain(domain_id) - if old_status == 'Stopped' and new_status == "Deleting": + if old_status == 'Stopped' and new_status == "Deleting" or \ + old_status == 'Downloaded' and new_status == "Deleting": ui.deleting_disks_from_domain(domain_id) if (old_status == 'Stopped' and new_status == "Updating") or \ diff --git a/src/engine/services/db/domains.py b/src/engine/services/db/domains.py index 3069ae520..7a23dddd9 100644 --- a/src/engine/services/db/domains.py +++ b/src/engine/services/db/domains.py @@ -41,6 +41,25 @@ def update_domain_force_hyp(id_domain, hyp_id=None): close_rethink_connection(r_conn) return results +def update_domain_parents(id_domain): + r_conn = new_rethink_connection() + rtable = r.table('domains') + d = rtable.get(id_domain).pluck({'create_dict': 'origin'}, 'parents').run(r_conn) + + if 'parents' not in d.keys(): + parents_with_new_origin = [] + elif type(d['parents']) is not list: + parents_with_new_origin = [] + else: + parents_with_new_origin = d['parents'].copy() + + if 'origin' in d['create_dict'].keys(): + parents_with_new_origin.append(d['create_dict']['origin']) + results = rtable.get_all(id_domain, index='id').update({'parents': parents_with_new_origin}).run(r_conn) + + close_rethink_connection(r_conn) + return results + def update_domain_status(status, id_domain, hyp_id=None, detail='', keep_hyp_id=False): r_conn = new_rethink_connection() @@ -56,7 +75,7 @@ def update_domain_status(status, id_domain, hyp_id=None, detail='', keep_hyp_id= if hyp_id is None: - # print('ojojojo') + # print('ojojojo')rtable.get(id_domain) results = rtable.get_all(id_domain, index='id').update({ 'status': status, 'hyp_started': '', diff --git a/src/engine/services/lib/qcow.py b/src/engine/services/lib/qcow.py index 78387ad40..4ab96f65b 100644 --- a/src/engine/services/lib/qcow.py +++ b/src/engine/services/lib/qcow.py @@ -487,3 +487,6 @@ def get_host_and_path_diskoperations_to_write_in_path(type_path, relative_path, else: path_absolute = path_selected + '/' + relative_path return host_disk_operations_selected, path_absolute + +def test_hypers_disk_operations(hypers_disk_operations): + pass diff --git a/src/engine/services/threads/threads.py b/src/engine/services/threads/threads.py index beddfa817..c89c106a0 100644 --- a/src/engine/services/threads/threads.py +++ b/src/engine/services/threads/threads.py @@ -16,7 +16,7 @@ get_domains_started_in_hyp, update_domains_started_in_hyp_to_unknown, remove_media from engine.services.db.downloads import update_status_media_from_path from engine.services.db.db import update_table_field -from engine.services.db.domains import update_domain_status +from engine.services.db.domains import update_domain_status, update_domain_parents from engine.services.db.hypervisors import update_hyp_status, get_hyp_hostname_from_id, \ update_hypervisor_failed_connection, update_db_hyp_info from engine.services.lib.functions import dict_domain_libvirt_state_to_isard_state, state_and_cause_to_str, \ @@ -147,10 +147,13 @@ def launch_action_disk(action, hostname, user, port, from_scratch=False): list_backing_chain = extract_list_backing_chain(out_cmd_backing_chain) if id_domain is not False: + update_domain_parents(id_domain) update_disk_backing_chain(id_domain, index_disk, disk_path, list_backing_chain) ##INFO TO DEVELOPER # ahora ya se puede llamar a starting paused if id_domain is not False: + #update parents if have + update_domain_parents(id_domain) update_domain_status('CreatingDomain', id_domain, None, detail='new disk created, now go to creating desktop and testing if desktop start') else: @@ -235,6 +238,8 @@ def launch_action_create_template_disk(action, hostname, user, port): new_template=True, list_backing_chain_template=backing_chain_template) + # disk created, update parents and status + update_domain_parents(id_domain) update_domain_status(status='TemplateDiskCreated', id_domain=id_domain, hyp_id=False, From f271015d3af60c397983a3f3408e507b9694384e Mon Sep 17 00:00:00 2001 From: beto Date: Wed, 26 Dec 2018 12:16:47 +0100 Subject: [PATCH 08/92] added twitter --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 81d37e5a8..867b1722e 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,6 @@ Go to [IsardVDI Project website](http://www.isardvdi.com/) ### Support/Contact Please send us an email to info@isardvdi.com if you have any questions or fill in an issue. + +### Social Networks +Twitter: @isard_vdi From 4e493ba009de88c19691a756818c3e5d42b72267 Mon Sep 17 00:00:00 2001 From: darta Date: Wed, 26 Dec 2018 15:55:22 -0600 Subject: [PATCH 09/92] Cleaned --- dockers/alpine-pandas/Dockerfile | 3 +-- dockers/app/Dockerfile | 16 ++++++---------- dockers/app/supervisord.conf | 8 ++------ dockers/hypervisor/Dockerfile | 23 ++++------------------- 4 files changed, 13 insertions(+), 37 deletions(-) diff --git a/dockers/alpine-pandas/Dockerfile b/dockers/alpine-pandas/Dockerfile index f4352dd68..d2dfa00ef 100644 --- a/dockers/alpine-pandas/Dockerfile +++ b/dockers/alpine-pandas/Dockerfile @@ -1,7 +1,6 @@ -FROM alpine:latest +FROM alpine:3.8 MAINTAINER isard -RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories RUN apk update RUN apk add --no-cache python3 && \ python3 -m ensurepip && \ diff --git a/dockers/app/Dockerfile b/dockers/app/Dockerfile index 40713db9f..ed8a9a6ae 100644 --- a/dockers/app/Dockerfile +++ b/dockers/app/Dockerfile @@ -1,7 +1,7 @@ -FROM isard/alpine-pandas:1.0.0 +FROM isard/alpine-pandas:latest MAINTAINER isard -RUN apk add --no-cache git yarn py3-libvirt py3-paramiko py3-lxml py3-xmltodict py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-flask-login py3-netaddr py3-requests curl +RUN apk add --no-cache bash yarn py3-libvirt py3-paramiko py3-lxml py3-xmltodict py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-flask-login py3-netaddr py3-requests curl openssh-client RUN mkdir /isard ADD ./src /isard @@ -16,15 +16,11 @@ RUN echo "Host isard-hypervisor \ StrictHostKeyChecking no" >/root/.ssh/config RUN chmod 600 /root/.ssh/config -RUN apk add --update bash -RUN apk add yarn -RUN apk add openssh-client - -RUN apk add supervisor +RUN apk add --no-cache supervisor RUN mkdir -p /var/log/supervisor COPY dockers/app/supervisord.conf /etc/supervisord.conf +EXPOSE 5000 + COPY dockers/app/certs.sh / -CMD /usr/bin/supervisord -c /etc/supervisord.conf -#CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] -#CMD ["sh", "/init.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/dockers/app/supervisord.conf b/dockers/app/supervisord.conf index 429e2c878..619302229 100644 --- a/dockers/app/supervisord.conf +++ b/dockers/app/supervisord.conf @@ -1,4 +1,5 @@ [supervisord] +user=root nodaemon=true logfile=/dev/stdout loglevel=error @@ -14,10 +15,8 @@ stdout_logfile=/isard/logs/certs.log stderr_logfile=/isard/logs/certs-error.log [program:webapp] -user=root directory=/isard command=python3 run_webapp.py -#1>/isard/logs/webapp.log 2>/isard/logs/webapp-error.log autostart=true autorestart=true startsecs=2 @@ -26,11 +25,8 @@ stdout_logfile=/isard/logs/webapp.log stderr_logfile=/isard/logs/webapp-error.log [program:engine] -user=root directory=/isard -command=sh -c "sleep 5 && python3 run_engine.py" -# 1>/isard/logs/engine.log 2>/isard/logs/engine-error.log" -#command=python3 run_engine.py +command=sh -c "virsh -c qemu+ssh://isard-hypervisor/system quit && python3 run_engine.py" autostart=true autorestart=false startsecs=2 diff --git a/dockers/hypervisor/Dockerfile b/dockers/hypervisor/Dockerfile index b04c340ed..342dfd8c6 100644 --- a/dockers/hypervisor/Dockerfile +++ b/dockers/hypervisor/Dockerfile @@ -3,7 +3,7 @@ MAINTAINER isard RUN pip3 uninstall pandas pytz python-dateutil six -y -RUN apk add qemu-system-x86_64 libvirt netcat-openbsd libvirt-daemon dbus polkit qemu-img +RUN apk --no-cache add qemu-system-x86_64 libvirt netcat-openbsd libvirt-daemon dbus polkit qemu-img RUN ln -s /usr/bin/qemu-system-x86_64 /usr/bin/qemu-kvm RUN apk add openssh curl bash RUN ssh-keygen -A @@ -23,33 +23,18 @@ echo 'spice_tls = 1' >> /etc/libvirt/qemu.conf && \ echo 'spice_tls_x509_cert_dir = "/etc/pki/libvirt-spice"' >> /etc/libvirt/qemu.conf RUN mkdir -p /etc/pki/libvirt-spice -# Add default network -#ADD dockers/hypervisor/customlibvirtpost.sh /customlibvirtpost.sh -#RUN chmod a+x /customlibvirtpost.sh -#ADD dockers/hypervisor/network.xml /network.xml - -#RUN mkdir /root/.ssh - -#spice-html5 proxy -#RUN mkdir -p /var/www/html/spice -#COPY html5/html /var/www/html/spice - -ADD dockers/hypervisor/requirements.pip3 / RUN apk add --no-cache --virtual .build_deps build-base python3-dev -#libffi-dev openssl-dev -RUN pip3 install websockify +RUN pip3 install --no-cache-dir websockify==0.8.0 RUN apk del .build_deps ADD dockers/hypervisor/start_proxy.py / EXPOSE 22 -EXPOSE 16509 EXPOSE 5900-5950 -EXPOSE 5700-5750 -EXPOSE 55900-55900 +EXPOSE 55900-55950 VOLUME ["/isard" ] -RUN apk add supervisor +RUN apk add --no-cache supervisor RUN mkdir -p /var/log/supervisor COPY dockers/hypervisor/supervisord.conf /etc/supervisord.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] From e60cf2d12ed626a9064936484c73103d11eb2700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Thu, 27 Dec 2018 00:44:45 +0100 Subject: [PATCH 10/92] Removed the documentation from the repository Now the documentation has it's own repository: https://github.com/isard-vdi/docs --- docs/about/license.md | 0 docs/images/list | 1 - docs/images/main.png | Bin 166415 -> 0 bytes docs/index.md | 59 --------------------- docs/install.md | 9 ---- docs/install/docker-compose.md | 6 --- docs/install/linux.md | 91 --------------------------------- docs/quick-start/first.md | 0 mkdocs.yml | 60 ---------------------- 9 files changed, 226 deletions(-) delete mode 100644 docs/about/license.md delete mode 100644 docs/images/list delete mode 100644 docs/images/main.png delete mode 100644 docs/index.md delete mode 100644 docs/install.md delete mode 100644 docs/install/docker-compose.md delete mode 100644 docs/install/linux.md delete mode 100644 docs/quick-start/first.md delete mode 100644 mkdocs.yml diff --git a/docs/about/license.md b/docs/about/license.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/images/list b/docs/images/list deleted file mode 100644 index 8b1378917..000000000 --- a/docs/images/list +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/images/main.png b/docs/images/main.png deleted file mode 100644 index a1b417cf5c94041698ea324c57e9d31796b5b706..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166415 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

F2j3;)*o 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 diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 044f93669..000000000 --- a/docs/index.md +++ /dev/null @@ -1,59 +0,0 @@ -# Isard**VDI** - -Open Source VDI deployment based on KVM Linux. - -## What is it - -A quick and real time web interface to manage your virtual desktops. - -Bring it up: - -``` -git clone https://github.com/isard-vdi/isard -cd isard -docker-compose up -d -``` - -Connect with browser to the server and follow the wizard. You are ready -to test virtual desktops: - -- Start **demo desktops** and connect to it using your browser and spice or -vnc protocol. Nothing to be installed, but already secured with certificates. -- Install virt-viewer and connect to it using the spice client. **Sound -and USB** transparent plug will be available. - -Download new precreated desktops, isos and lots of resources from the **Updates** menu. - -Create your own desktop using isos downloaded from Updates or **Media** -menu option. When you finish installing the operating system and -applications create a **Template** and decide which users or categories -you want to be able to create a desktop identical to that template. Thanks to the **incremental disk creation** all this can be done within -minutes. - -Don't get tied to an 'stand-alone' installation in one server. You can -add more hypervisors to your **pool** and let IsardVDI decide where to -start each desktop. Each hypervisor needs only the KVM/qemu and libvirt -packages and SSH access. You should keep the storage shared between -those hypervisors. - -We currenly manage a **large IsardVDI infrastructure** at Escola del -Treball in Barcelona. 3K students and teachers have IsardVDI available -from our self-made pacemaker dual nas cluster and six hypervisors, -ranging from top level intel server dual core mainboards to gigabyte -gaming ones. - -We have experience in different **thin clients** that we use to lower renovation and -consumption costs at classrooms. - -[IsardVDI Project website](http://www.isardvdi.com/) - -### Authors -+ Josep Maria Viñolas Auquer -+ Alberto Larraz Dalmases - -### Contributors -+ Daniel Criado Casas -+ Néfix Estrada - -### Support/Contact -Please send us an email to info@isardvdi.com if you have any questions or fill in an issue. diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index dd9f07500..000000000 --- a/docs/install.md +++ /dev/null @@ -1,9 +0,0 @@ -# Installation - -## On a linux system - -We have tested on debian, ubuntu and fedora latests versions - -## Using docker-compose - -You can get a fully working IsardVDI installation within minutes with docker. diff --git a/docs/install/docker-compose.md b/docs/install/docker-compose.md deleted file mode 100644 index ef768612b..000000000 --- a/docs/install/docker-compose.md +++ /dev/null @@ -1,6 +0,0 @@ - Install docker: https://docs.docker.com/engine/installation/ - Install docker-compose: https://docs.docker.com/compose/install/ - Clone the repository: git clone https://github.com/isard-vdi/isard.git - cd isard && docker-compose up - -You should be able to access your IsardVDI through https://localhost diff --git a/docs/install/linux.md b/docs/install/linux.md deleted file mode 100644 index 0e38ad4d5..000000000 --- a/docs/install/linux.md +++ /dev/null @@ -1,91 +0,0 @@ -# IsardVDI installation on FEDORA 25 - -## Install OS - -Minimal Fedora 25 install -sudo dnf update -y - -## Clone IsardVDI repository - -``` -sudo dnf install git -git clone https://github.com/isard-vdi/isard.git -``` - -## Install IsardVDI requirements - -``` -cd isard/install/ -sudo dnf install wget gcc redhat-rpm-config python3-devel openldap-devel openssl-devel libvirt-python3 npm -sudo pip3 install -r requirements.pip3 -``` - -``` -sudo npm -g install bower -bower install -``` - -``` -sudo wget http://download.rethinkdb.com/centos/7/`uname -m`/rethinkdb.repo -O /etc/yum.repos.d/rethinkdb.repo -sudo dnf install -y rethinkdb -sudo cp /etc/rethinkdb/default.conf.sample /etc/rethinkdb/instances.d/default.conf -sudo systemctl daemon-reload -sudo systemctl start rethinkdb -``` - -## Selinux and Firewalld -For testing purposes, just disable both till next reboot: - -``` -sudo setenforce 0 -sudo systemctl stop firewalld -``` - -**Do not disable them in production!, please follow nginx.md and selinux.md documentation** - -## Run the application - -``` -cd .. -./run.sh -``` - -You can browse to your computer port 5000 -Default user is 'admin' and password 'isard' - - -# KNOWN ISSUES - -## IsardVDI engine can't contact hypervisor(s) - -### ssh authentication fail when connect: Server 'vdesktop6.escoladeltreball.org' not found in known_hosts - -You should generate int your IsardVDI machine your ssh key and copy it to the hypervisor(s): -``` -ssh-keygen -ssh-copy-id root@ -``` - - -Now you should be able to connect to frontend through http://localhost:5000 -Default user is admin and password isard. - - -# In hypervisors we need - - -in Fedora or centos: -``` -dnf -y install openssh-server qemu-kvm libguestfs-tools -``` - -Check that you can connect to the hypervisor using ssh root@ - -NOTE: Service sshd on hypervisor(s) should use ssh-rsa keys. Please check **/etc/ssh/sshd_config** on hypervisor that you have only **HostKey /etc/ssh/ssh_host_rsa_key** option active - -## IsardVDI does not start - -+ Check that you have rethinkdb database running: **systemctl status rethinkdb** -+ Check that rethinkdb tcp port 28015 it is open: **netstat -tulpn | grep 28015** -+ Check that there are no error logs on output. - diff --git a/docs/quick-start/first.md b/docs/quick-start/first.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 23f0a5506..000000000 --- a/mkdocs.yml +++ /dev/null @@ -1,60 +0,0 @@ -docs_dir: docs - -# Project information -site_name: IsardVDI -site_url: http://www.isardvdi.com -site_description: IsardVDI Open Source Virtual Desktops. -site_author: IsardVDI - -# Repository -repo_name: 'isard-vdi/isard' -repo_url: https://github.com/isard-vdi/isard - -# Copyright -copyright: Copyright © 2016 IsardVDI. - -# Documentation and theme -#theme: 'material' -theme: - name: "readthedocs" -strict: true - -# Options -extra: - logo: 'images/logo.svg' - palette: - primary: 'indigo' - accent: 'indigo' - font: - text: 'Roboto' - code: 'Roboto Mono' - social: - - type: 'github' - link: 'https://github.com/john-doe' - - type: 'twitter' - link: 'https://twitter.com/john-doe' - - type: 'linkedin' - link: 'https://de.linkedin.com/in/john-doe' - -# Google Analytics -#google_analytics: -# - 'UA-XXXXXXXX-X' -# - 'auto' - -# Extensions -markdown_extensions: - - admonition - - codehilite: - guess_lang: false - - toc: - permalink: true - -# TOC -pages: -- Introduction: index.md -- Installation: - - On linux: install/linux.md - - Using docker-compose: install/docker-compose.md -- Quick Start: quick-start/first.md -- About: about/license.md - From b52f35faa28b95e3713f9ed861ce1f0ecc466689 Mon Sep 17 00:00:00 2001 From: beto Date: Sat, 29 Dec 2018 09:24:42 +0100 Subject: [PATCH 11/92] add .dev and other configurations to devel --- .env | 2 + build-docker-images.sh | 4 +- dockers/build-docker-images-devel.sh | 34 ++++++++ dockers/devel-debug.yml | 98 +++++++++------------- dockers/docker-compose-devel.yml | 119 +++++++++++++++++++++++++++ dockers/nginx/nginx.conf | 2 +- 6 files changed, 199 insertions(+), 60 deletions(-) create mode 100644 .env create mode 100755 dockers/build-docker-images-devel.sh create mode 100644 dockers/docker-compose-devel.yml diff --git a/.env b/.env new file mode 100644 index 000000000..c1617f923 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +TAG=1.0 +TAG_DEVEL=1.0 diff --git a/build-docker-images.sh b/build-docker-images.sh index c5e90a1a2..07f6ba207 100755 --- a/build-docker-images.sh +++ b/build-docker-images.sh @@ -14,11 +14,11 @@ PATCH=$1 set -e # Checkout to the specified version tag -git checkout $1 > /dev/null +#git checkout $1 > /dev/null # Array containing all the images to build images=( - alpine-pandas + #alpine-pandas nginx hypervisor app diff --git a/dockers/build-docker-images-devel.sh b/dockers/build-docker-images-devel.sh new file mode 100755 index 000000000..ebba7ca60 --- /dev/null +++ b/dockers/build-docker-images-devel.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Check that the version number was provided +if [ -z "$1" ]; then + echo "You need to specify a IsardVDI version! e.g. '1.1.0'" + exit 1 +fi + +MAJOR=${1:0:1} +MINOR=${1:0:3} +PATCH=$1 + +# If a command fails, the whole script is going to stop +set -e + +# Checkout to the specified version tag +#git checkout $1 > /dev/null + +# Array containing all the images to build +images=( + #alpine-pandas + #nginx + #hypervisor + app_devel +) + +# Build all the images and tag them correctly +for image in "${images[@]}"; do + echo -e "\n\n\n" + echo "Building $image" + echo -e "\n\n\n" + docker build -f=dockers/$image/Dockerfile -t isard/$image:latest -t isard/$image:$MAJOR -t isard/$image:$MINOR -t isard/$image:$PATCH . +done + diff --git a/dockers/devel-debug.yml b/dockers/devel-debug.yml index a80681312..7b236cfb9 100644 --- a/dockers/devel-debug.yml +++ b/dockers/devel-debug.yml @@ -1,79 +1,58 @@ version: '2' services: - isard-alpine-pandas: - image: isard/alpine-pandas:1.0.0 -# Will take long time to build -# build: -# context: . -# dockerfile: dockers/alpine-pandas/Dockerfile isard-database: - restart: always - image: rethinkdb hostname: isard-database volumes: - - "/opt/isard/src/database:/data" + - "/opt/isard/database:/data" + networks: + - isard_network ##### - only devel - ############################ ports: - "8080:8080" - ################################################# expose: - "28015" - networks: - main: - aliases: - - rethinkdb + ################################################# + #aliases: + # - rethinkdb + image: "rethinkdb" + restart: "no" isard-nginx: - restart: always - image: isard/nginx:1.0.1b + volumes: + - "/opt/isard/certs/default:/etc/nginx/external" + - "/opt/isard/logs/nginx:/var/log/nginx" build: context: . dockerfile: dockers/nginx/Dockerfile ports: - "80:80" - "443:443" - volumes: - - "/opt/isard/certs/default:/etc/nginx/external" - hostname: isard-nginx - links: - - "isard-app" networks: - main: - aliases: - - isard-nginx + - isard_network + image: isard/nginx:${TAG_DEVEL} + restart: "no" + depends_on: + - "isard-app" isard-hypervisor: - restart: always - image: isard/hypervisor:1.0.1b - build: - context: . - dockerfile: dockers/hypervisor/Dockerfile - hostname: isard-hypervisor + volumes: + - "sshkeys:/root/.ssh" + - "/opt/isard:/isard" + - "/opt/isard/certs/default:/etc/pki/libvirt-spice" ports: - "5900-5949:5900-5949" - "55900-55949:55900-55949" + ################ only for devel ############### expose: - "22" - privileged: true - volumes: - - "sshkeys:/root/.ssh" - - "/opt/isard:/isard" - - "/opt/isard/certs/default:/etc/pki/libvirt-spice" + ############################################### networks: - main: - aliases: - - isard-hypervisor - command: /usr/bin/supervisord -c /etc/supervisord.conf - + - isard_network + image: "isard/hypervisor:${TAG_DEVEL}" + privileged: true + restart: "no" + isard-app: - restart: always - image: isard/app:1.0.0 - build: - context: . - dockerfile: dockers/app_devel/Dockerfile - links: - - "isard-database" - - "isard-hypervisor" hostname: isard-app volumes: ##### - only devel - ############################ @@ -83,21 +62,26 @@ services: - "sshkeys:/root/.ssh" - "/opt/isard/certs:/certs" - "/opt/isard/logs:/isard/logs" + - "/opt/isard/backups:/isard/backups" + - "/opt/isard/uploads:/isard/uploads" - "/opt/isard/database/wizard:/isard/install/wizard" - + ########### - only devel ################# expose: - "5000" - environment: - PYTHONUNBUFFERED: 0 + ########################################## extra_hosts: - "isard-engine:127.0.0.1" networks: - main: - aliases: - - isard-app - command: /usr/bin/supervisord -c /etc/supervisord.conf + - isard_network + image: "isard/app_devel:${TAG_DEVEL}" + restart: "no" + depends_on: + - "isard-database" + - "isard-hypervisor" -networks: - main: volumes: sshkeys: + +networks: + isard_network: + external: false diff --git a/dockers/docker-compose-devel.yml b/dockers/docker-compose-devel.yml new file mode 100644 index 000000000..75d8f6baa --- /dev/null +++ b/dockers/docker-compose-devel.yml @@ -0,0 +1,119 @@ +version: "3.2" +services: + isard-database: + volumes: + - type: bind + source: /opt/isard/database + target: /data + read_only: false + networks: + - isard_network + ##### - only devel - ############################ + ports: + - target: 8080 + published: 8080 + protocol: tcp + mode: host + - target: 28015 + published: 28015 + protocol: tcp + mode: host + ################################################# + image: rethinkdb + restart: always + + isard-nginx: + volumes: + - type: bind + source: /opt/isard/certs/default + target: /etc/nginx/external + read_only: false + - type: bind + source: /opt/isard/logs/nginx + target: /var/log/nginx + read_only: false + ports: + - target: 80 + published: 80 + protocol: tcp + mode: host + - target: 443 + published: 443 + protocol: tcp + mode: host + networks: + - isard_network + image: "isard/nginx:${$PROVA}" + command: echo ${PROVA} + restart: always + + isard-hypervisor: + volumes: + - type: volume + source: sshkeys + target: /root/.ssh + read_only: false + - type: bind + source: /opt/isard + target: /isard + read_only: false + - type: bind + source: /opt/isard/certs/default + target: /etc/pki/libvirt-spice + read_only: false + ports: + - "5900-5949:5900-5949" + - "55900-55949:55900-55949" + networks: + - isard_network + image: "isard/hypervisor:${TAG_DEVEL}" + privileged: true + restart: always + + isard-app: + volumes: + - type: volume + source: sshkeys + target: /root/.ssh + read_only: false + - type: bind + source: /opt/isard/certs + target: /certs + read_only: false + - type: bind + source: /opt/isard/logs + target: /isard/logs + read_only: false + - type: bind + source: /opt/isard/database/wizard + target: /isard/install/wizard + read_only: false + - type: bind + source: /opt/isard/backups + target: /isard/backups + read_only: false + - type: bind + source: /opt/isard/uploads + target: /isard/uploads + read_only: false + ##### - only devel - ############################ + - "/opt/isard_devel/src:/isard" + - "/opt/ipython_profile_default:/root/.ipython/profile_default" + ################################################# + extra_hosts: + - "isard-engine:127.0.0.1" + networks: + - isard_network + image: "isard/app_devel:${TAG_DEVEL}" + restart: always + depends_on: + - isard-database + - isard-hypervisor + - isard-nginx + +volumes: + sshkeys: + +networks: + isard_network: + external: false diff --git a/dockers/nginx/nginx.conf b/dockers/nginx/nginx.conf index facbdf22f..08ec7897b 100644 --- a/dockers/nginx/nginx.conf +++ b/dockers/nginx/nginx.conf @@ -29,7 +29,7 @@ http { #gzip on; upstream isard-fe { - server isard-app:5000 fail_timeout=0; + server isard-app:5000 max_fails=5 fail_timeout=2s; } server { From 03c7cdfdd0a403a9d4dc878cac3ce23296f50d20 Mon Sep 17 00:00:00 2001 From: beto Date: Fri, 4 Jan 2019 04:11:02 +0100 Subject: [PATCH 12/92] working, not stable, events fail --- src/engine/api/__init__.py | 5 +- src/engine/controllers/events_recolector.py | 47 ++++-- src/engine/models/manager_hypervisors.py | 164 ++++++++++++-------- src/engine/services/lib/qcow.py | 53 ++++++- 4 files changed, 186 insertions(+), 83 deletions(-) diff --git a/src/engine/api/__init__.py b/src/engine/api/__init__.py index c96960259..3bc5a4dd8 100644 --- a/src/engine/api/__init__.py +++ b/src/engine/api/__init__.py @@ -82,12 +82,11 @@ def stop_threads(): @api.route('/engine_restart', methods=['GET']) def engine_restart(): - app.m.stop_threads() while True: - alive, dead, not_defined = app.m.update_info_threads_engine() - if len(alive) == 0: + app.m.update_info_threads_engine() + if len(app.m.threads_info_main['alive']) == 0 and len(app.m.threads_info_hyps['alive']) == 0: action = {} action['type'] = 'stop' app.m.q.background.put(action) diff --git a/src/engine/controllers/events_recolector.py b/src/engine/controllers/events_recolector.py index 8d2600054..429b13476 100644 --- a/src/engine/controllers/events_recolector.py +++ b/src/engine/controllers/events_recolector.py @@ -26,6 +26,9 @@ from engine.services.log import * +NUM_TRY_REGISTER_EVENTS = 5 +SLEEP_BETWEEN_TRY_REGISTER_EVENTS = 1.0 + # Reference: https://github.com/libvirt/libvirt-python/blob/master/examples/event-test.py from pprint import pprint def virEventLoopNativeRun(stop): @@ -61,6 +64,7 @@ def virEventLoopNativeStart(stop): ########################################################################## def domEventToString(event): + #from https://github.com/libvirt/libvirt-python/blob/master/examples/event-test.py domEventStrings = ("Defined", "Undefined", "Started", @@ -75,18 +79,26 @@ def domEventToString(event): def domDetailToString(event, detail): - domEventStrings = ( - ("Added", "Updated"), - ("Removed",), - ("Booted", "Migrated", "Restored", "Snapshot", "Wakeup"), - ("Paused", "Migrated", "IOError", "Watchdog", "Restored", "Snapshot", "API error"), - ("Unpaused", "Migrated", "Snapshot"), - ("Shutdown", "Destroyed", "Crashed", "Migrated", "Saved", "Failed", "Snapshot"), - ("Finished",), - ("Memory", "Disk"), - ("Panicked",), + # from https://github.com/libvirt/libvirt-python/blob/master/examples/event-test.py + DOM_EVENTS = ( + ("Defined", ("Added", "Updated", "Renamed", "Snapshot")), + ("Undefined", ("Removed", "Renamed")), + ("Started", ("Booted", "Migrated", "Restored", "Snapshot", "Wakeup")), + ("Suspended", ("Paused", "Migrated", "IOError", "Watchdog", "Restored", "Snapshot", "API error", "Postcopy", + "Postcopy failed")), + ("Resumed", ("Unpaused", "Migrated", "Snapshot", "Postcopy")), + ("Stopped", ("Shutdown", "Destroyed", "Crashed", "Migrated", "Saved", "Failed", "Snapshot", "Daemon")), + ("Shutdown", ("Finished", "On guest request", "On host request")), + ("PMSuspended", ("Memory", "Disk")), + ("Crashed", ("Panicked",)), ) - return domEventStrings[event][detail] + try: + return DOM_EVENTS[event][1][detail] + except Exception as e: + logs.status.error(f'Detail not defined in DOM_EVENTS. index_event:{event}, index_detail{detail}') + logs.status.error(e) + return 'Detail undefined' + def blockJobTypeToString(type): @@ -501,8 +513,17 @@ def add_hyp_to_receive_events(self, hyp_id): logs.status.error(e) if conn_ok is True: - self.events_ids[hyp_id] = self.register_events(self.hyps_conn[hyp_id]) - self.hyps[hyp_id] = hostname + for i in range(NUM_TRY_REGISTER_EVENTS): + #try 5 + try: + self.events_ids[hyp_id] = self.register_events(self.hyps_conn[hyp_id]) + self.hyps[hyp_id] = hostname + break + except libvirt.libvirtError as e: + logs.status.error(f'Error when register_events, wait {SLEEP_BETWEEN_TRY_REGISTER_EVENTS}, try {i+1} of {NUM_TRY_REGISTER_EVENTS}') + logs.status.error(e) + time.sleep(SLEEP_BETWEEN_TRY_REGISTER_EVENTS) + def del_hyp_to_receive_events(self, hyp_id): self.unregister_events(self.hyps_conn[hyp_id], self.events_ids[hyp_id]) diff --git a/src/engine/models/manager_hypervisors.py b/src/engine/models/manager_hypervisors.py index fb45ed971..4f986413e 100644 --- a/src/engine/models/manager_hypervisors.py +++ b/src/engine/models/manager_hypervisors.py @@ -4,12 +4,11 @@ # License: AGPLv3 # coding=utf-8 -import pprint - import queue import threading from datetime import datetime from time import sleep +import pprint import rethinkdb as r @@ -73,8 +72,12 @@ def __init__(self, launch_threads=True, with_status_threads=True, self.t_downloads_changes = None self.quit = False + self.threads_info_main = {} + self.threads_info_hyps = {} + self.hypers_disk_operations_tested = [] + self.num_workers = 0 - self.threads_started = False + self.threads_main_started = False self.STATUS_POLLING_INTERVAL = status_polling_interval self.TEST_HYP_FAIL_INTERVAL = test_hyp_fail_interval @@ -89,13 +92,13 @@ def launch_thread_background_polling(self): self.t_background.start() def check_actions_domains_enabled(self): - if self.num_workers > 0 and self.threads_started is True: + if self.num_workers > 0 and self.threads_main_started is True: return True else: return False def update_info_threads_engine(self): - d = {} + d_mains = {} alive=[] dead=[] not_defined=[] @@ -107,19 +110,30 @@ def update_info_threads_engine(self): #thread not defined not_defined.append(name) + d_mains['alive']=alive + d_mains['dead']=dead + d_mains['not_defined']=not_defined + self.threads_info_main = d_mains.copy() + + d_hyps = {} + alive=[] + dead=[] + not_defined=[] for name in ['workers','status','disk_operations','long_operations']: for hyp,t in self.__getattribute__('t_'+name).items(): try: alive.append(name + '_' + hyp) if t.is_alive() else dead.append(name + '_' + hyp) except: not_defined.append(name) - pass - d['alive']=alive - d['dead']=dead - d['not_defined']=not_defined - update_table_field('engine', 'engine', 'threads', d) - return alive,dead,not_defined + d_hyps['alive']=alive + d_hyps['dead']=dead + d_hyps['not_defined']=not_defined + self.threads_info_hyps = d_hyps.copy() + update_table_field('engine', 'engine', 'threads_info_main', d_mains) + update_table_field('engine', 'engine', 'threads_info_hyps', d_hyps) + + return True def stop_threads(self): # events and broom @@ -182,26 +196,27 @@ def launch_threads_disk_and_long_operations(self): self.manager.hypers_disk_operations = get_hypers_disk_operations() - test_hypers_disk_operations(self.manager.hypers_disk_operations) + self.manager.hypers_disk_operations_tested = test_hypers_disk_operations(self.manager.hypers_disk_operations) - for hyp_disk_operations in self.manager.hypers_disk_operations: + for hyp_disk_operations in self.manager.hypers_disk_operations_tested: hyp_long_operations = hyp_disk_operations d = get_hyp_hostname_user_port_from_id(hyp_disk_operations) - self.manager.t_disk_operations[hyp_disk_operations], \ - self.manager.q_disk_operations[hyp_disk_operations] = launch_disk_operations_thread( - hyp_id=hyp_disk_operations, - hostname=d['hostname'], - user=d['user'], - port=d['port'] - ) - self.manager.t_long_operations[hyp_long_operations], \ - self.manager.q_long_operations[hyp_long_operations] = launch_long_operations_thread( - hyp_id=hyp_long_operations, - hostname=d['hostname'], - user=d['user'], - port=d['port'] - ) + if hyp_disk_operations not in self.manager.t_disk_operations.keys(): + self.manager.t_disk_operations[hyp_disk_operations], \ + self.manager.q_disk_operations[hyp_disk_operations] = launch_disk_operations_thread( + hyp_id=hyp_disk_operations, + hostname=d['hostname'], + user=d['user'], + port=d['port'] + ) + self.manager.t_long_operations[hyp_long_operations], \ + self.manager.q_long_operations[hyp_long_operations] = launch_long_operations_thread( + hyp_id=hyp_long_operations, + hostname=d['hostname'], + user=d['user'], + port=d['port'] + ) def test_hyps_and_start_threads(self): """If status of hypervisor is Error or Offline and are enabled, @@ -211,9 +226,8 @@ def test_hyps_and_start_threads(self): threads are running state is Online. Status sequence is: (Offline,Error) => ReadyToStart => StartingThreads => (Online,Error)""" - # DISK_OPERATIONS: - if len(self.manager.t_disk_operations) == 0: - self.launch_threads_disk_and_long_operations() + # DISK_OPERATIONS: launch threads if test disk operations passed and is not launched + self.launch_threads_disk_and_long_operations() l_hyps_to_test = get_hyps_with_status(list_status=['Error', 'Offline'], empty=True) @@ -233,7 +247,7 @@ def test_hyps_and_start_threads(self): if len(dict_hyps_ready) > 0: logs.main.debug('hyps_ready_to_start: ' + pprint.pformat(dict_hyps_ready)) - #launch thread events + #launch thread events if is None if self.manager.t_events is None: logs.main.info('launching hypervisor events thread') self.manager.t_events = launch_thread_hyps_event(dict_hyps_ready) @@ -269,26 +283,26 @@ def test_hyps_and_start_threads(self): update_hyp_status(hyp_id, 'Online') pools.update(get_pools_from_hyp(hyp_id)) - #if hypervisor no in pools defined in manager add it + #if hypervisor not in pools defined in manager add it for id_pool in pools: if id_pool not in self.manager.pools.keys(): self.manager.pools[id_pool] = PoolHypervisors(id_pool, self.manager, len(dict_hyps_ready)) def run(self): self.tid = get_tid() - logs.main.info('starting thread: {} (TID {})'.format(self.name, self.tid)) + logs.main.info('starting thread background: {} (TID {})'.format(self.name, self.tid)) q = self.manager.q.background first_loop = True # if domains have intermedite states (updating, download_aborting...) # to Failed or Delete - clean_intermediate_states() + clean_intermediate_status() l_hyps_to_test = get_hyps_with_status(list_status=['Error', 'Offline'], empty=True) - while len(l_hyps_to_test) == 0: - logs.main.error('no hypervisor enable, waiting for one hypervisor') - sleep(0.5) - l_hyps_to_test = get_hyps_with_status(list_status=['Error', 'Offline'], empty=True) + # while len(l_hyps_to_test) == 0: + # logs.main.error('no hypervisor enable, waiting for one hypervisor') + # sleep(0.5) + # l_hyps_to_test = get_hyps_with_status(list_status=['Error', 'Offline'], empty=True) while self.manager.quit is False: #################################################################### @@ -296,15 +310,23 @@ def run(self): # ONLY FOR DEBUG logs.main.debug('##### THREADS ##################') - theads_running = get_threads_running() - alive, dead, not_defined = self.manager.update_info_threads_engine() + threads_running = get_threads_running() + #self.manager.update_info_threads_engine() - # Threads that must be running always, with or withouth hypervisor - # changes_hyp, changes_domains, disk_operations and long_operations, - # downloads_changes, events, broom + # Threads that must be running always, with or withouth hypervisor: + # - changes_hyp + # - changes_domains + # - downloads_changes + # - broom + # - events + + # Threads that depends on hypervisors availavility: + # - disk_operations + # - long_operations + # - for every hypervisor: + # - worker + # - status - # TEST HYPS AND START THREADS FROM RETHINK - self.test_hyps_and_start_threads() # LAUNCH MAIN THREADS if first_loop is True: @@ -330,28 +352,47 @@ def run(self): logs.main.debug('launching hypervisor events thread') self.manager.t_events = launch_thread_hyps_event({}) - first_loop = False - logs.main.info('THREADS LAUNCHED FROM BACKGROUND THREAD') update_table_field('engine', 'engine', 'status_all_threads', 'Starting') while True: #wait all sleep(0.1) - alive, dead, not_defined = self.manager.update_info_threads_engine() - pprint.pprint({'alive':alive, - 'dead':dead, - 'not_defined':not_defined}) - #if thread events is None and len(dict_hyps ready) == 0, must recheck hypers - if len(not_defined) > 0 and len(self.manager.dict_hyps_ready) == 0: - sleep(3) - self.test_hyps_and_start_threads() - elif len(not_defined) == 0 and len(dead) == 0: + self.manager.update_info_threads_engine() + + #if len(self.manager.threads_info_main['not_defined']) > 0 and len(self.manager.dict_hyps_ready) == 0: + if len(self.manager.threads_info_main['not_defined']) > 0 or len(self.manager.threads_info_main['dead']) > 0: + print('MAIN THREADS starting, wait a second extra') + sleep(1) + self.manager.update_info_threads_engine() + pprint.pprint(self.manager.threads_info_main) + #self.test_hyps_and_start_threads() + if len(self.manager.threads_info_main['not_defined']) == 0 and len(self.manager.threads_info_main['dead']) == 0: update_table_field('engine', 'engine', 'status_all_threads', 'Started') - self.manager.num_workers = len(self.manager.t_workers) - self.manager.threads_started = True + self.manager.threads_main_started = True break + # TEST HYPS AND START THREADS FOR HYPERVISORS + self.test_hyps_and_start_threads() + self.manager.num_workers = len(self.manager.t_workers) + + # Test hypervisor disk operations + # Create Test disk in hypervisor disk operations + if first_loop is True: + first_loop = False + # virtio_test_disk_relative_path = 'admin/admin/admin/virtio_testdisk.qcow2' + # ui.creating_test_disk(test_disk_relative_route=virtio_test_disk_relative_path) + + self.manager.update_info_threads_engine() + if len(self.manager.threads_info_hyps['not_defined']) > 0: + logs.main.error('something was wrong when launching threads for hypervisors, threads not defined') + logs.main.error(pprint.pformat(self.manager.threads_info_hyps)) + if len(self.manager.threads_info_hyps['dead']) > 0: + logs.main.error('something was wrong when launching threads for hypervisors, threads are dead') + logs.main.error(pprint.pformat(self.manager.threads_info_hyps)) + if len(self.manager.threads_info_hyps['dead']) == 0 and len(self.manager.threads_info_hyps['not_defined']) == 0: + pass + try: action = q.get(timeout=self.manager.TEST_HYP_FAIL_INTERVAL) if action['type'] == 'stop': @@ -451,12 +492,6 @@ def run(self): logs.changes.debug('^^^^^^^^^^^^^^^^^^^ DOMAIN CHANGES THREAD ^^^^^^^^^^^^^^^^^') ui = UiActions(self.manager) - # Test hypervisor disk operations - # Create Test disk in hypervisor disk operations - virtio_test_disk_relative_path = 'admin/admin/admin/virtio_testdisk.qcow2' - ui.creating_test_disk(test_disk_relative_route=virtio_test_disk_relative_path) - - self.r_conn = new_rethink_connection() cursor = r.table('domains').pluck('id', 'kind', 'status', 'detail').merge({'table': 'domains'}).changes().\ @@ -490,7 +525,6 @@ def run(self): new_domain = False new_status = False old_status = False - import pprint logs.changes.debug(pprint.pformat(c)) diff --git a/src/engine/services/lib/qcow.py b/src/engine/services/lib/qcow.py index 4ab96f65b..7f0e59045 100644 --- a/src/engine/services/lib/qcow.py +++ b/src/engine/services/lib/qcow.py @@ -6,12 +6,23 @@ # coding=utf-8 import json +import string +from random import choices + from os.path import dirname as extract_dir_path from engine.services.db.db import get_pool from engine.services.lib.functions import exec_remote_cmd, size_format, get_threads_names_running, weighted_choice, \ backing_chain_cmd from engine.services.log import * +from engine.services.db import get_hyp_hostname_user_port_from_id +from engine.services.lib.functions import execute_commands +from engine import config +from engine.services.db.db import get_pools_from_hyp + + + + VDESKTOP_DISK_OPERATINOS = CONFIG_DICT['REMOTEOPERATIONS']['host_remote_disk_operatinos'] @@ -488,5 +499,43 @@ def get_host_and_path_diskoperations_to_write_in_path(type_path, relative_path, path_absolute = path_selected + '/' + relative_path return host_disk_operations_selected, path_absolute -def test_hypers_disk_operations(hypers_disk_operations): - pass +def test_hypers_disk_operations(hyps_disk_operations): + list_hyps_ok = list() + str_random = ''.join(choices(string.ascii_uppercase + string.digits, k=8)) + for hyp_id in hyps_disk_operations: + d_hyp = get_hyp_hostname_user_port_from_id(hyp_id) + cmds1 = list() + for pool_id in get_pools_from_hyp(hyp_id): + paths = {k: [l['path'] for l in d] for k, d in get_pool(pool_id)['paths'].items()} + for k, p in paths.items(): + for path in p: + cmds1.append({'title': f'try create dir if not exists - pool:{pool_id}, hypervisor: {hyp_id}, path_kind: {k}', + 'cmd': f'mkdir -p {path}'}) + cmds1.append({'title': f'touch random file - pool:{pool_id}, hypervisor: {hyp_id}, path_kind: {k}', + 'cmd': f'touch {path}/test_random_{str_random}'}) + cmds1.append({'title': 'delete random file - pool:{pool_id}, hypervisor: {hyp_id}, path_kind: {k}', + 'cmd': f'rm -f {path}/test_random_{str_random}'}) + try: + array_out_err = execute_commands(d_hyp['hostname'], + ssh_commands=cmds1, + dict_mode=True, + user=d_hyp['user'], + port=d_hyp['port']) + #if error in some path hypervisor is not valid + if len([d['err'] for d in array_out_err if len(d['err']) > 0]) > 0: + logs.main.error(f'Hypervisor {hyp_id} can not be disk_operations, some errors when testing if can create files in all paths_') + for d_cmd_err in [d for d in array_out_err if len(d['err']) > 0]: + cmd = d_cmd_err['cmd'] + err = d_cmd_err['err'] + logs.main.error(f'Command: {cmd} -- Error: {err}') + else: + list_hyps_ok.append(hyp_id) + + except Exception as e: + if __name__ == '__main__': + logs.main.err(f'Error when launch commands to test hypervisor {hyp_id} disk_operations: {e}') + + return list_hyps_ok + + + From c2c49a8353510e874b5012ebbb131c010b0adf17 Mon Sep 17 00:00:00 2001 From: beto Date: Fri, 4 Jan 2019 13:03:52 +0100 Subject: [PATCH 13/92] finished refactoring how threads start in engine --- src/engine/controllers/events_recolector.py | 58 ++++++++++++--------- src/engine/models/manager_hypervisors.py | 15 ++++-- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/engine/controllers/events_recolector.py b/src/engine/controllers/events_recolector.py index 429b13476..82c268974 100644 --- a/src/engine/controllers/events_recolector.py +++ b/src/engine/controllers/events_recolector.py @@ -14,6 +14,8 @@ import sys import threading import time +import queue +import traceback import libvirt @@ -25,7 +27,7 @@ from engine.services.lib.functions import hostname_to_uri, get_tid from engine.services.log import * - +TIMEOUT_QUEUE_REGISTER_EVENTS = 1 NUM_TRY_REGISTER_EVENTS = 5 SLEEP_BETWEEN_TRY_REGISTER_EVENTS = 1.0 @@ -444,7 +446,6 @@ def myDomainEventGraphicsCallbackRethink(conn, dom, phase, localAddr, remoteAddr class ThreadHypEvents(threading.Thread): def __init__(self, name, - dict_hyps, register_graphics_events=True ): threading.Thread.__init__(self) @@ -452,10 +453,11 @@ def __init__(self, name, self.stop = False self.stop_event_loop = [False] self.REGISTER_GRAPHICS_EVENTS = register_graphics_events - self.hyps = dict_hyps + self.hyps = {} # self.hostname = get_hyp_hostname_from_id(hyp_id) self.hyps_conn = dict() self.events_ids = dict() + self.q_event_register = queue.Queue() def run(self): # Close connection on exit (to test cleanup paths) @@ -471,29 +473,33 @@ def exit(): sys.exitfunc = exit - # self.r_status = RethinkHypEvent() - - while True: - if len(self.hyps) == 0: - if self.stop: - break - time.sleep(0.1) - else: - self.thread_event_loop = virEventLoopNativeStart(self.stop_event_loop) + self.thread_event_loop = virEventLoopNativeStart(self.stop_event_loop) - for hyp_id, hostname in self.hyps.items(): + # self.r_status = RethinkHypEvent() + while self.stop is not True: + try: + action = self.q_event_register.get(timeout=TIMEOUT_QUEUE_REGISTER_EVENTS) + if action['type'] in ['add_hyp_to_receive_events']: + hyp_id = action['hyp_id'] self.add_hyp_to_receive_events(hyp_id) - - while self.stop is not True: - time.sleep(0.1) - - if self.stop is True: - for hyp_id in list(self.hyps): - self.del_hyp_to_receive_events(hyp_id) - self.stop_event_loop[0] = True - while self.thread_event_loop.is_alive(): - pass - break + elif action['type'] in ['del_hyp_to_receive_events']: + hyp_id = action['hyp_id'] + self.del_hyp_to_receive_events(hyp_id) + elif action['type'] == 'stop_thread': + self.stop = True + else: + logs.status.error('type action {} not supported'.format(action['type'])) + except queue.Empty: + pass + except Exception as e: + log.error('Exception in ThreadHypEvents main loop: {}'.format(e)) + log.error('Action: {}'.format(pprint.pformat(action))) + log.error('Traceback: {}'.format(traceback.format_exc())) + return False + + self.stop_event_loop[0] = True + while self.thread_event_loop.is_alive(): + pass def add_hyp_to_receive_events(self, hyp_id): d_hyp_parameters = get_hyp_hostname_user_port_from_id(hyp_id) @@ -592,10 +598,10 @@ def unregister_events(self, hyp_libvirt_conn, cb_ids): hyp_libvirt_conn.unregisterCloseCallback() -def launch_thread_hyps_event(dict_hyps): +def launch_thread_hyps_event(): # t = threading.Thread(name= 'events',target=events_from_hyps, args=[list_hostnames]) - t = ThreadHypEvents(name='hyps_events', dict_hyps=dict_hyps) + t = ThreadHypEvents(name='hyps_events') t.daemon = True t.start() return t diff --git a/src/engine/models/manager_hypervisors.py b/src/engine/models/manager_hypervisors.py index 4f986413e..87bb07b2f 100644 --- a/src/engine/models/manager_hypervisors.py +++ b/src/engine/models/manager_hypervisors.py @@ -35,6 +35,8 @@ launch_long_operations_thread from engine.services.lib.functions import clean_intermediate_status +WAIT_HYP_ONLINE = 2.0 + class ManagerHypervisors(object): """Main class that control and launch all threads. Main thread ThreadBackground is launched and control all threads @@ -250,7 +252,7 @@ def test_hyps_and_start_threads(self): #launch thread events if is None if self.manager.t_events is None: logs.main.info('launching hypervisor events thread') - self.manager.t_events = launch_thread_hyps_event(dict_hyps_ready) + self.manager.t_events = launch_thread_hyps_event() # else: # #if new hypervisor has added then add hypervisor to receive events # logs.main.info('hypervisors added to thread events') @@ -275,7 +277,7 @@ def test_hyps_and_start_threads(self): self.manager.STATUS_POLLING_INTERVAL) # ADD hyp to receive_events - self.manager.t_events.add_hyp_to_receive_events(hyp_id) + self.manager.t_events.q_event_register.put({'type': 'add_hyp_to_receive_events', 'hyp_id': hyp_id}) # self.manager.launch_threads(hyp_id) # INFO TO DEVELOPER FALTA VERIFICAR QUE REALMENTE ESTÁN ARRANCADOS LOS THREADS?? @@ -311,6 +313,7 @@ def run(self): # ONLY FOR DEBUG logs.main.debug('##### THREADS ##################') threads_running = get_threads_running() + #pprint.pprint(threads_running) #self.manager.update_info_threads_engine() # Threads that must be running always, with or withouth hypervisor: @@ -350,7 +353,7 @@ def run(self): #launch events thread logs.main.debug('launching hypervisor events thread') - self.manager.t_events = launch_thread_hyps_event({}) + self.manager.t_events = launch_thread_hyps_event() logs.main.info('THREADS LAUNCHED FROM BACKGROUND THREAD') update_table_field('engine', 'engine', 'status_all_threads', 'Starting') @@ -394,7 +397,11 @@ def run(self): pass try: - action = q.get(timeout=self.manager.TEST_HYP_FAIL_INTERVAL) + if len(self.manager.t_workers) == 0: + timeout_queue = WAIT_HYP_ONLINE + else: + timeout_queue = TEST_HYP_FAIL_INTERVAL + action = q.get(timeout=timeout_queue) if action['type'] == 'stop': self.manager.quit = True logs.main.info('engine end') From 3cd579068d9cd3d574cc73e63d5b0aec8477be55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 5 Jan 2019 20:54:30 +0100 Subject: [PATCH 14/92] progress --- docker-compose.yml | 7 ++-- src/webapp/static/js/viewer.js | 30 +++++++++++----- .../templates/pages/desktops_detail.html | 34 +++++++++++++------ 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b6bad79b2..c3b4f7d9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: isard-nginx: restart: always - image: isard/nginx:1.0.0 + image: isard/nginx:1.0 build: context: . dockerfile: dockers/nginx/Dockerfile @@ -41,7 +41,7 @@ services: isard-hypervisor: restart: always - image: isard/hypervisor:1.0.0 + image: isard/hypervisor:1.0 build: context: . dockerfile: dockers/hypervisor/Dockerfile @@ -64,7 +64,7 @@ services: isard-app: restart: always - image: isard/app:1.0.0 + image: isard/app:1.0 build: context: . dockerfile: dockers/app/Dockerfile @@ -73,6 +73,7 @@ services: - "isard-hypervisor" hostname: isard-app volumes: + - "/home/darta/jvinolas/isard/src:/isard" - "sshkeys:/root/.ssh" - "/opt/isard/certs:/certs" - "/opt/isard/logs:/isard/logs" diff --git a/src/webapp/static/js/viewer.js b/src/webapp/static/js/viewer.js index 2f03f96a6..3036d0cd8 100644 --- a/src/webapp/static/js/viewer.js +++ b/src/webapp/static/js/viewer.js @@ -158,7 +158,18 @@ function setViewerButtons(id,socket,offer){ function startClientViewerSocket(socket){ socket.on('domain_viewer', function (data) { var data = JSON.parse(data); - $("#hiddenpass-"+data["id"]).val('proves'); + + th = document.createElement('th'); + th.onclick = function () { + //~ this.parentElement.removeChild(this); + copyToClipboard('la password'); + }; + var ev = document.createEvent("MouseEvents"); + ev.initMouseEvent("click", true, false, self, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + th.dispatchEvent(ev); + + + //~ $("#btn-pwd-"+data["id"]).data('pwd')='1234'; if(data['kind']=='url'){ viewer=data['viewer'] window.open(viewer.replace('',document.domain)); @@ -205,15 +216,16 @@ function getOS() { } -//~ function copyToClipboard(el) { - //~ var $temp = $(""); - //~ $("body").append($temp); - //~ id=$(el).data('id') +function copyToClipboard(pwd) { + var $temp = $(""); + $("body").append($temp); + //~ pwd=$(el).data('pwd') + //~ console.log(pwd) //~ console.log($("#hiddenpass-"+id).text()) - //~ $temp.val($("#hiddenpass-"+id).text()).select(); - //~ document.execCommand("copy"); - //~ $temp.remove(); -//~ } + $temp.val(pwd).select(); + document.execCommand("copy"); + $temp.remove(); +} //~ function getClientViewer(data,socket){ //~ if(detectXpiPlugin()){ diff --git a/src/webapp/templates/pages/desktops_detail.html b/src/webapp/templates/pages/desktops_detail.html index f596b242b..5f2861d92 100644 --- a/src/webapp/templates/pages/desktops_detail.html +++ b/src/webapp/templates/pages/desktops_detail.html @@ -1,22 +1,35 @@

-
+
{% if(current_user.role!='user') %} - +
+ +
{% endif %} +
+ +
+
+ +
{% if(current_user.role=='admin') %} - - - - {% endif %} - - +
+
+ +
+
+ +
+
+ +
+ {% endif %}
-
+
@@ -24,8 +37,7 @@

Status detailed info:

From d3c264331d59b0a54a389287d8da7c6dbaa9d167 Mon Sep 17 00:00:00 2001 From: darta Date: Sun, 6 Jan 2019 18:10:45 -0600 Subject: [PATCH 15/92] Added add-hypervisor.sh script to add new hypervisors --- dockers/app/Dockerfile | 1 + dockers/app/add-hypervisor.sh | 34 +++++++++++++++++++++++++++++++ dockers/app/certs.sh | 38 ++++------------------------------- 3 files changed, 39 insertions(+), 34 deletions(-) create mode 100755 dockers/app/add-hypervisor.sh diff --git a/dockers/app/Dockerfile b/dockers/app/Dockerfile index 643a056eb..08d0f1b57 100644 --- a/dockers/app/Dockerfile +++ b/dockers/app/Dockerfile @@ -27,4 +27,5 @@ COPY dockers/app/supervisord.conf /etc/supervisord.conf EXPOSE 5000 COPY dockers/app/certs.sh / +COPY dockers/app/add-hypervisor.sh / CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/dockers/app/add-hypervisor.sh b/dockers/app/add-hypervisor.sh new file mode 100755 index 000000000..71f2669bb --- /dev/null +++ b/dockers/app/add-hypervisor.sh @@ -0,0 +1,34 @@ +apk add sshpass +if [ -f /NEWHYPER ] +then + rm /NEWHYPER +fi +echo "Trying to ssh into $HYPERVISOR..." +ssh-keyscan $HYPERVISOR > /NEWHYPER +if [ ! -s /NEWHYPER ] +then + echo "Hypervisor $HYPERVISOR could not be reached. Aborting" + exit 1 +else + cat /NEWHYPER >> /root/.ssh/known_hosts + sshpass -p "$PASSWORD" ssh-copy-id root@"$HYPERVISOR" + if [ $? -ne 0 ] + then + sed '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts > /root/.ssh/known_hosts + echo "Can't access $HYPERVISOR as root user. Aborting" + exit 1 + fi +fi + +echo "Hypervisor ssh access granted." +virsh -c qemu+ssh://"$HYPERVISOR"/system quit +if [ $? -ne 0 ] +then + echo "Can't access libvirtd daemon. Please ensure that libvirt daemon is running in $HYPERVISOR. Aborting" + sed '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts > /root/.ssh/known_hosts + exit 1 +fi + +echo "Access to $HYPERVISOR granted and found libvirtd service running." +echo "Now you can create this hypervisor in IsardVDI web interface." + diff --git a/dockers/app/certs.sh b/dockers/app/certs.sh index 61f85acf8..4eb184e94 100755 --- a/dockers/app/certs.sh +++ b/dockers/app/certs.sh @@ -2,18 +2,11 @@ public_key="/root/.ssh/authorized_keys" if [ -f "$public_key" ] then - echo "$public_key found, so not generating new ones." + echo "$public_key found, so not generating new ones." else - echo "$public_key not found, generating new ones." - cat /dev/zero | ssh-keygen -q -N "" - mv /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys - - #ssh-keyscan isard-hypervisor > /tmp/known_hosts - #DIFF=$(diff /root/.ssh/know_hosts /tmp/known_hosts) - #if [ "$DIFF" != "" ] - #then - # echo "The HYPERVISOR key has been regenerated" - # rm /root/.ssh/known_hosts + echo "$public_key not found, generating new ones." + cat /dev/zero | ssh-keygen -q -N "" + cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys echo "Scanning isard-hypervisor key..." ssh-keyscan isard-hypervisor > /root/.ssh/known_hosts @@ -24,27 +17,4 @@ else ssh-keyscan isard-hypervisor > /root/.ssh/known_hosts done echo "isard-hypervisor online..." - - #fi - - ######## Only on development - ####echo -e "isard\nisard" | (passwd --stdin root) - echo -e "isard\nisard" | passwd root - ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' - #ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N '' - #/usr/sbin/sshd - ######## - fi - -#!/bin/bash -#~ cd /isard - -#~ echo "Waiting for isard-hypervisor to be online" -#~ while [ ! -e /libvirt/libvirt-admin-sock ] -#~ do - #~ sleep 2 -#~ done -#~ echo "isard-hypervisor online, starting engine..." -#~ python3 /isard/run_engine.py - From 9175397aac981b1d4e8f57bbf280c5b418069d22 Mon Sep 17 00:00:00 2001 From: darta Date: Mon, 7 Jan 2019 04:10:30 -0600 Subject: [PATCH 16/92] Working --- dockers/app/add-hypervisor.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/dockers/app/add-hypervisor.sh b/dockers/app/add-hypervisor.sh index 71f2669bb..bb4ee024e 100755 --- a/dockers/app/add-hypervisor.sh +++ b/dockers/app/add-hypervisor.sh @@ -1,8 +1,17 @@ +if [[ -z $HYPERVISOR || -z $PASSWORD ]] +then + echo "You should add environment variables:" + echo " docker exec -e HYPERVISOR= -e PASSWORD= isard_isard-app_1 bash -c '/add-hypervisor.sh'" + echo "Please run it again setting environment variables" + exit 1 +fi + apk add sshpass if [ -f /NEWHYPER ] then rm /NEWHYPER fi +sed -i '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts echo "Trying to ssh into $HYPERVISOR..." ssh-keyscan $HYPERVISOR > /NEWHYPER if [ ! -s /NEWHYPER ] @@ -14,7 +23,7 @@ else sshpass -p "$PASSWORD" ssh-copy-id root@"$HYPERVISOR" if [ $? -ne 0 ] then - sed '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts > /root/.ssh/known_hosts + sed -i '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts echo "Can't access $HYPERVISOR as root user. Aborting" exit 1 fi @@ -25,10 +34,12 @@ virsh -c qemu+ssh://"$HYPERVISOR"/system quit if [ $? -ne 0 ] then echo "Can't access libvirtd daemon. Please ensure that libvirt daemon is running in $HYPERVISOR. Aborting" - sed '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts > /root/.ssh/known_hosts + sed -i '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts exit 1 fi + echo "Access to $HYPERVISOR granted and found libvirtd service running." echo "Now you can create this hypervisor in IsardVDI web interface." + From 6556aaa77fc275bf6da082ce1eb9e2d32866ef3c Mon Sep 17 00:00:00 2001 From: darta Date: Mon, 7 Jan 2019 08:40:15 -0600 Subject: [PATCH 17/92] Added new hyper and port and username --- dockers/app/add-hypervisor.sh | 28 +++++++++++++------ dockers/generic-hyper/README.md | 5 ++++ dockers/generic-hyper/isard-generic-hyper.yml | 26 +++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 dockers/generic-hyper/README.md create mode 100644 dockers/generic-hyper/isard-generic-hyper.yml diff --git a/dockers/app/add-hypervisor.sh b/dockers/app/add-hypervisor.sh index bb4ee024e..24428f73e 100755 --- a/dockers/app/add-hypervisor.sh +++ b/dockers/app/add-hypervisor.sh @@ -1,11 +1,23 @@ if [[ -z $HYPERVISOR || -z $PASSWORD ]] then echo "You should add environment variables:" - echo " docker exec -e HYPERVISOR= -e PASSWORD= isard_isard-app_1 bash -c '/add-hypervisor.sh'" + echo " docker exec -e HYPERVISOR= -e PASSWORD= isard_isard-app_1 bash -c '/add-hypervisor.sh'" + echo "Optional parameters: USER (default is root), PORT (default is 22)" + echo "" echo "Please run it again setting environment variables" exit 1 fi +if [[ -z $PORT ]] +then + PORT=22 +fi + +if [[ -z $USER ]] +then + USER=root +fi + apk add sshpass if [ -f /NEWHYPER ] then @@ -13,33 +25,33 @@ then fi sed -i '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts echo "Trying to ssh into $HYPERVISOR..." -ssh-keyscan $HYPERVISOR > /NEWHYPER +ssh-keyscan -p $PORT $HYPERVISOR > /NEWHYPER if [ ! -s /NEWHYPER ] then - echo "Hypervisor $HYPERVISOR could not be reached. Aborting" + echo "Hypervisor $HYPERVISOR:$PORT could not be reached. Aborting" exit 1 else cat /NEWHYPER >> /root/.ssh/known_hosts - sshpass -p "$PASSWORD" ssh-copy-id root@"$HYPERVISOR" + sshpass -p "$PASSWORD" ssh-copy-id -p $PORT $USER@"$HYPERVISOR" if [ $? -ne 0 ] then sed -i '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts - echo "Can't access $HYPERVISOR as root user. Aborting" + echo "Can't access $USER@$HYPERVISOR:$PORT. Aborting" exit 1 fi fi echo "Hypervisor ssh access granted." -virsh -c qemu+ssh://"$HYPERVISOR"/system quit +virsh -c qemu+ssh://"$USER"@"$HYPERVISOR":"$PORT"/system quit if [ $? -ne 0 ] then - echo "Can't access libvirtd daemon. Please ensure that libvirt daemon is running in $HYPERVISOR. Aborting" + echo "Can't access libvirtd daemon. Please ensure that libvirt daemon is running in $USER@$HYPERVISOR:$PORT. Aborting" sed -i '/'"$HYPERVISOR"'/d' /root/.ssh/known_hosts exit 1 fi -echo "Access to $HYPERVISOR granted and found libvirtd service running." +echo "Access to $USER@$HYPERVISOR:$PORT granted and found libvirtd service running." echo "Now you can create this hypervisor in IsardVDI web interface." diff --git a/dockers/generic-hyper/README.md b/dockers/generic-hyper/README.md new file mode 100644 index 000000000..7ae214edc --- /dev/null +++ b/dockers/generic-hyper/README.md @@ -0,0 +1,5 @@ +# IsardVDI Generic Hypervisor + +It brings up a remote hypervisor. Instructions can be found at documentation: + +https://isardvdi.readthedocs.io/en/latest/admin/hypervisors/ diff --git a/dockers/generic-hyper/isard-generic-hyper.yml b/dockers/generic-hyper/isard-generic-hyper.yml new file mode 100644 index 000000000..7750e41af --- /dev/null +++ b/dockers/generic-hyper/isard-generic-hyper.yml @@ -0,0 +1,26 @@ +version: "3.2" +services: + isard-hypervisor: + volumes: + - type: volume + source: sshkeys + target: /root/.ssh + read_only: false + - type: bind + source: /opt/isard + target: /isard + read_only: false + - type: bind + source: /opt/isard/certs/default + target: /etc/pki/libvirt-spice + read_only: false + ports: + - "2022:22" + - "5900-5949:5900-5949" + - "55900-55949:55900-55949" + image: isard/hypervisor:1.0 + privileged: true + restart: always + +volumes: + sshkeys: From c3eb8f66746ab6d05530df4ba40faa5433d2ca27 Mon Sep 17 00:00:00 2001 From: darta Date: Mon, 7 Jan 2019 09:51:49 -0600 Subject: [PATCH 18/92] Added script to restart keys in hypervisor --- dockers/hypervisor/Dockerfile | 2 ++ dockers/hypervisor/reset-hyper.sh | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 dockers/hypervisor/reset-hyper.sh diff --git a/dockers/hypervisor/Dockerfile b/dockers/hypervisor/Dockerfile index b04c340ed..b6a79b109 100644 --- a/dockers/hypervisor/Dockerfile +++ b/dockers/hypervisor/Dockerfile @@ -7,6 +7,8 @@ RUN apk add qemu-system-x86_64 libvirt netcat-openbsd libvirt-daemon dbus polkit RUN ln -s /usr/bin/qemu-system-x86_64 /usr/bin/qemu-kvm RUN apk add openssh curl bash RUN ssh-keygen -A +COPY reset-hyper.sh / +RUN chmod 744 reset-hyper.sh RUN echo "root:isard" |chpasswd RUN sed -i 's|[#]*PermitRootLogin prohibit-password|PermitRootLogin yes|g' /etc/ssh/sshd_config diff --git a/dockers/hypervisor/reset-hyper.sh b/dockers/hypervisor/reset-hyper.sh new file mode 100644 index 000000000..cd607bbaf --- /dev/null +++ b/dockers/hypervisor/reset-hyper.sh @@ -0,0 +1,13 @@ +if [[ -z $PASSWORD ]] +then + echo "Usage:" + echo " docker exec -e PASSWORD= isard_isard-generic-hyper_1 bash -c '/reset-hyper.sh'" + exit 1 +fi + +/bin/rm -v /etc/ssh/ssh_host_* +ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa +ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa +echo "root:$PASSWORD" |chpasswd +pkill -9 sshd +echo "You can add this new hypervisor in IsardVDI with PORT=2022 and your new root password" From c0ecfd4313bae064c0c127df64c396de702f248e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Mon, 7 Jan 2019 18:19:23 +0100 Subject: [PATCH 19/92] websocket viewers fails --- dockers/{generic-hyper => external_hyper}/README.md | 0 .../docker-compose.yml} | 2 +- dockers/hypervisor/Dockerfile | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename dockers/{generic-hyper => external_hyper}/README.md (100%) rename dockers/{generic-hyper/isard-generic-hyper.yml => external_hyper/docker-compose.yml} (96%) diff --git a/dockers/generic-hyper/README.md b/dockers/external_hyper/README.md similarity index 100% rename from dockers/generic-hyper/README.md rename to dockers/external_hyper/README.md diff --git a/dockers/generic-hyper/isard-generic-hyper.yml b/dockers/external_hyper/docker-compose.yml similarity index 96% rename from dockers/generic-hyper/isard-generic-hyper.yml rename to dockers/external_hyper/docker-compose.yml index 7750e41af..78d232a05 100644 --- a/dockers/generic-hyper/isard-generic-hyper.yml +++ b/dockers/external_hyper/docker-compose.yml @@ -15,7 +15,7 @@ services: target: /etc/pki/libvirt-spice read_only: false ports: - - "2022:22" + - "22:22" - "5900-5949:5900-5949" - "55900-55949:55900-55949" image: isard/hypervisor:1.0 diff --git a/dockers/hypervisor/Dockerfile b/dockers/hypervisor/Dockerfile index b6a79b109..d0a8f94ce 100644 --- a/dockers/hypervisor/Dockerfile +++ b/dockers/hypervisor/Dockerfile @@ -7,7 +7,7 @@ RUN apk add qemu-system-x86_64 libvirt netcat-openbsd libvirt-daemon dbus polkit RUN ln -s /usr/bin/qemu-system-x86_64 /usr/bin/qemu-kvm RUN apk add openssh curl bash RUN ssh-keygen -A -COPY reset-hyper.sh / +ADD dockers/hypervisor/reset-hyper.sh / RUN chmod 744 reset-hyper.sh RUN echo "root:isard" |chpasswd From cf908b6539fed488ea39fcb6fde03428a3a1ac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Mon, 7 Jan 2019 18:30:30 +0100 Subject: [PATCH 20/92] Updated image link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81d37e5a8..077bfb67d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ It will create a template from that desktop as it was now. You can create as man In Updates menu you will have access to different resources you can download from our IsardVDI updates server. -![Main admin screen](docs/images/main.png?raw=true "Main admin") +![Main admin screen](https://isardvdi.readthedocs.io/en/latest/images/main.png?raw=true "Main admin") ## Documentation From 28e95c7e6d0d1888d635b2a2dd90f74302597aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Mon, 7 Jan 2019 21:39:41 +0100 Subject: [PATCH 21/92] Update README.md Perfect! Co-Authored-By: jvinolas --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 077bfb67d..8024db7a7 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ It will create a template from that desktop as it was now. You can create as man In Updates menu you will have access to different resources you can download from our IsardVDI updates server. -![Main admin screen](https://isardvdi.readthedocs.io/en/latest/images/main.png?raw=true "Main admin") +![Main admin screen](https://isardvdi.readthedocs.io/en/latest/images/main.png) ## Documentation From 78df3066ed7d673d20756208a60b52e52400d8bc Mon Sep 17 00:00:00 2001 From: darta Date: Mon, 7 Jan 2019 23:19:57 +0100 Subject: [PATCH 22/92] Desktops detail buttons now in a column --- build-docker-images.sh | 4 +- docker-compose.yml | 5 +- src/webapp/admin/views/AdminDomainsViews.py | 39 +--- src/webapp/static/admin/js/domains.js | 50 ++++- src/webapp/static/js/desktops.js | 14 +- src/webapp/static/js/viewer.js | 207 ++++++++---------- .../templates/pages/desktops_detail.html | 3 - 7 files changed, 158 insertions(+), 164 deletions(-) diff --git a/build-docker-images.sh b/build-docker-images.sh index c5e90a1a2..f04070a25 100755 --- a/build-docker-images.sh +++ b/build-docker-images.sh @@ -14,11 +14,11 @@ PATCH=$1 set -e # Checkout to the specified version tag -git checkout $1 > /dev/null +#git checkout $1 > /dev/null # Array containing all the images to build images=( - alpine-pandas +# alpine-pandas nginx hypervisor app diff --git a/docker-compose.yml b/docker-compose.yml index eb371973b..35488b302 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,6 +60,10 @@ services: isard-app: volumes: + - type: bind + source: /home/darta/jvinolas/isard/src + target: /isard + read_only: false - type: volume source: sshkeys target: /root/.ssh @@ -94,7 +98,6 @@ services: - isard-database - isard-hypervisor - isard-nginx ->>>>>>> develop volumes: sshkeys: diff --git a/src/webapp/admin/views/AdminDomainsViews.py b/src/webapp/admin/views/AdminDomainsViews.py index 1b7a31769..53fed7a3f 100644 --- a/src/webapp/admin/views/AdminDomainsViews.py +++ b/src/webapp/admin/views/AdminDomainsViews.py @@ -80,44 +80,25 @@ def admin_domains_xml(id): @login_required @isAdmin def admin_domains_events(id): - # ~ if request.method == 'POST': - # ~ res=app.adminapi.update_table_dict('domains',id,request.get_json(force=True)) - # ~ if res: - # ~ return json.dumps(res), 200, {'ContentType': 'application/json'} - # ~ else: - # ~ return json.dumps(res), 500, {'ContentType': 'application/json'} return json.dumps(app.isardapi.get_domain_last_events(id)), 200, {'ContentType': 'application/json'} @app.route('/admin/domains/messages/', methods=['GET']) @login_required @isAdmin def admin_domains_messages(id): - # ~ if request.method == 'POST': - # ~ res=app.adminapi.update_table_dict('domains',id,request.get_json(force=True)) - # ~ if res: - # ~ return json.dumps(res), 200, {'ContentType': 'application/json'} - # ~ else: - # ~ return json.dumps(res), 500, {'ContentType': 'application/json'} return json.dumps(app.isardapi.get_domain_last_messages(id)), 200, {'ContentType': 'application/json'} -''' -VIRT BUILDER TESTS (IMPORT NEW BUILDERS?) -''' -@app.route('/admin/domains/virtrebuild') -@login_required -@isAdmin -def admin_domains_get_builders(): - #~ import subprocess - #~ command_output=subprocess.getoutput(['virt-builder --list']) - #~ blist=[] - #~ for l in command_output.split('\n'): - #~ blist.append({'dwn':False,'id':l[0:24].strip(),'arch':l[25:35].strip(),'name':l[36:].strip()}) - #~ app.adminapi.cmd_virtbuilder('cirros-0.3.1','/isard/cirros.qcow2','1') - app.adminapi.update_virtbuilder() - app.adminapi.update_virtinstall() - #~ images=app.adminapi.get_admin_table('domains_virt_builder') - return json.dumps(''), 200, {'ContentType': 'application/json'} +# ~ ''' +# ~ VIRT BUILDER TESTS (IMPORT NEW BUILDERS?) +# ~ ''' +# ~ @app.route('/admin/domains/virtrebuild') +# ~ @login_required +# ~ @isAdmin +# ~ def admin_domains_get_builders(): + # ~ app.adminapi.update_virtbuilder() + # ~ app.adminapi.update_virtinstall() + # ~ return json.dumps(''), 200, {'ContentType': 'application/json'} diff --git a/src/webapp/static/admin/js/domains.js b/src/webapp/static/admin/js/domains.js index 5de6cf462..8ed5b9f4c 100644 --- a/src/webapp/static/admin/js/domains.js +++ b/src/webapp/static/admin/js/domains.js @@ -352,7 +352,7 @@ $(document).ready(function() { function actionsDomainDetail(){ $('.btn-edit').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); //~ console.log(pk) setHardwareOptions('#modalEditDesktop'); setHardwareDomainDefaults('#modalEditDesktop',pk); @@ -367,7 +367,7 @@ function actionsDomainDetail(){ }); $('.btn-xml').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); $("#modalEditXmlForm")[0].reset(); $('#modalEditXml').modal({ backdrop: 'static', @@ -380,12 +380,50 @@ function actionsDomainDetail(){ success: function(data) { var data = JSON.parse(data); - $('#xml').val(data); + $('#modalEditXmlForm #xml').val(data); } }); //~ $('#modalEdit').parsley(); //~ modal_edit_desktop_datatables(pk); }); + + $('.btn-events').on('click', function () { + var pk=$(this).closest("[data-pk]").attr("data-pk"); + $("#modalShowInfoForm")[0].reset(); + $('#modalShowInfo').modal({ + backdrop: 'static', + keyboard: false + }).modal('show'); + $('#modalShowInfoForm #id').val(pk); + $.ajax({ + type: "GET", + url:"/admin/domains/events/" + pk, + success: function(data) + { + var data = JSON.parse(data); + $('#modalShowInfoForm #xml').val(JSON.stringify(data, undefined, 4)); + } + }); + }); + + $('.btn-messages').on('click', function () { + var pk=$(this).closest("[data-pk]").attr("data-pk"); + $("#modalShowInfoForm")[0].reset(); + $('#modalShowInfo').modal({ + backdrop: 'static', + keyboard: false + }).modal('show'); + $('#modalShowInfoForm #id').val(pk); + $.ajax({ + type: "GET", + url:"/admin/domains/messages/" + pk, + success: function(data) + { + //~ var data = JSON.parse(data); + $('#modalShowInfoForm #xml').val(JSON.stringify(data, undefined, 4)); + } + }); + }); if(url=="Desktops"){ @@ -401,7 +439,7 @@ function actionsDomainDetail(){ type: 'error' }); }else{ - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); setDefaultsTemplate(pk); setHardwareOptions('#modalTemplateDesktop'); setHardwareDomainDefaults('#modalTemplateDesktop',pk); @@ -413,8 +451,8 @@ function actionsDomainDetail(){ }); $('.btn-delete').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); - var name=$(this).closest("div").attr("data-name"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); + var name=$(this).closest("[data-pk]").attr("data-name"); new PNotify({ title: 'Confirmation Needed', text: "Are you sure you want to delete virtual machine: "+name+"?", diff --git a/src/webapp/static/js/desktops.js b/src/webapp/static/js/desktops.js index e60eb40e8..9b5ec6a20 100644 --- a/src/webapp/static/js/desktops.js +++ b/src/webapp/static/js/desktops.js @@ -335,7 +335,7 @@ $(document).ready(function() { function actionsDesktopDetail(){ $('.btn-edit').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); setHardwareOptions('#modalEditDesktop'); setHardwareDomainDefaults('#modalEditDesktop',pk); $("#modalEdit")[0].reset(); @@ -363,7 +363,7 @@ function actionsDesktopDetail(){ type: 'error' }); }else{ - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); setDefaultsTemplate(pk); setHardwareOptions('#modalTemplateDesktop'); @@ -382,8 +382,8 @@ function actionsDesktopDetail(){ }); $('.btn-delete').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); - var name=$(this).closest("div").attr("data-name"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); + var name=$(this).closest("[data-pk]").attr("data-name"); new PNotify({ title: 'Confirmation Needed', text: "Are you sure you want to delete virtual machine: "+name+"?", @@ -407,7 +407,7 @@ function actionsDesktopDetail(){ }); $('.btn-xml').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); $("#modalShowInfoForm")[0].reset(); $('#modalEditXml').modal({ backdrop: 'static', @@ -428,7 +428,7 @@ function actionsDesktopDetail(){ }); $('.btn-events').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); $("#modalShowInfoForm")[0].reset(); $('#modalShowInfo').modal({ backdrop: 'static', @@ -449,7 +449,7 @@ function actionsDesktopDetail(){ }); $('.btn-messages').on('click', function () { - var pk=$(this).closest("div").attr("data-pk"); + var pk=$(this).closest("[data-pk]").attr("data-pk"); $("#modalShowInfoForm")[0].reset(); $('#modalShowInfo').modal({ backdrop: 'static', diff --git a/src/webapp/static/js/viewer.js b/src/webapp/static/js/viewer.js index 3036d0cd8..1aaa0886f 100644 --- a/src/webapp/static/js/viewer.js +++ b/src/webapp/static/js/viewer.js @@ -5,97 +5,6 @@ * License: AGPLv3 */ -//~ function chooseViewer(data,socket){ - //~ os=getOS() - //~ new PNotify({ - //~ title: 'Choose display connection', - //~ text: 'Open in browser (html5) or download remote-viewer file.', - //~ icon: 'glyphicon glyphicon-question-sign', - //~ hide: false, - //~ delay: 3000, - //~ confirm: { - //~ confirm: true, - //~ buttons: [ - //~ { - //~ text: 'SPICE BROWSER', - //~ addClass: 'btn-primary', - //~ click: function(notice){ - //~ notice.update({ - //~ title: 'You choosed spice browser viewer', text: 'Viewer will be opened in new window.\n Please allow popups!', icon: true, type: 'info', hide: true, - //~ confirm: { - //~ confirm: false - //~ }, - //~ buttons: { - //~ closer: true, - //~ sticker: false - //~ } - //~ }); - //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'spice-html5','os':os}); - //~ } - //~ }, - //~ { - //~ text: 'SPICE CLIENT', - //~ addClass: 'btn-primary', - //~ click: function(notice){ - //~ notice.update({ - //~ title: 'You choosed spice client viewer', text: 'File will be downloaded. Open it with spice remote-viewer.', icon: true, type: 'info', hide: true, - //~ confirm: { - //~ confirm: false - //~ }, - //~ buttons: { - //~ closer: true, - //~ sticker: false - //~ } - //~ }); - //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'spice-client','os':os}); - //~ } - //~ }, - //~ { - //~ text: 'VNC BROWSER', - //~ addClass: 'btn-primary', - //~ click: function(notice){ - //~ notice.update({ - //~ title: 'You choosed VNC browser viewer', text: 'Viewer will be opened in new window.\n Please allow popups!', icon: true, type: 'info', hide: true, - //~ confirm: { - //~ confirm: false - //~ }, - //~ buttons: { - //~ closer: true, - //~ sticker: false - //~ } - //~ }); - //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'vnc-html5','os':os}); - //~ } - //~ }, - //~ { - //~ text: 'VNC CLIENT', - //~ addClass: 'btn-primary', - //~ click: function(notice){ - //~ notice.update({ - //~ title: 'You choosed VNC client viewer', text: 'File will be downloaded. Open it with VNC client app.', icon: true, type: 'info', hide: true, - //~ confirm: { - //~ confirm: false - //~ }, - //~ buttons: { - //~ closer: true, - //~ sticker: false - //~ } - //~ }); - //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'vnc-client','os':os}); - //~ } - //~ }, - //~ ] - //~ }, - //~ buttons: { - //~ closer: false, - //~ sticker: false - //~ }, - //~ history: { - //~ history: false - //~ } - //~ }); -//~ } - function setViewerButtons(id,socket,offer){ offer=[ { @@ -158,18 +67,7 @@ function setViewerButtons(id,socket,offer){ function startClientViewerSocket(socket){ socket.on('domain_viewer', function (data) { var data = JSON.parse(data); - - th = document.createElement('th'); - th.onclick = function () { - //~ this.parentElement.removeChild(this); - copyToClipboard('la password'); - }; - var ev = document.createEvent("MouseEvents"); - ev.initMouseEvent("click", true, false, self, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - th.dispatchEvent(ev); - - - //~ $("#btn-pwd-"+data["id"]).data('pwd')='1234'; + if(data['kind']=='url'){ viewer=data['viewer'] window.open(viewer.replace('',document.domain)); @@ -214,18 +112,6 @@ function getOS() { return os; } - - -function copyToClipboard(pwd) { - var $temp = $(""); - $("body").append($temp); - //~ pwd=$(el).data('pwd') - //~ console.log(pwd) - //~ console.log($("#hiddenpass-"+id).text()) - $temp.val(pwd).select(); - document.execCommand("copy"); - $temp.remove(); -} //~ function getClientViewer(data,socket){ //~ if(detectXpiPlugin()){ @@ -383,4 +269,93 @@ function copyToClipboard(pwd) { //~ embed.connect(); //~ } - +//~ function chooseViewer(data,socket){ + //~ os=getOS() + //~ new PNotify({ + //~ title: 'Choose display connection', + //~ text: 'Open in browser (html5) or download remote-viewer file.', + //~ icon: 'glyphicon glyphicon-question-sign', + //~ hide: false, + //~ delay: 3000, + //~ confirm: { + //~ confirm: true, + //~ buttons: [ + //~ { + //~ text: 'SPICE BROWSER', + //~ addClass: 'btn-primary', + //~ click: function(notice){ + //~ notice.update({ + //~ title: 'You choosed spice browser viewer', text: 'Viewer will be opened in new window.\n Please allow popups!', icon: true, type: 'info', hide: true, + //~ confirm: { + //~ confirm: false + //~ }, + //~ buttons: { + //~ closer: true, + //~ sticker: false + //~ } + //~ }); + //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'spice-html5','os':os}); + //~ } + //~ }, + //~ { + //~ text: 'SPICE CLIENT', + //~ addClass: 'btn-primary', + //~ click: function(notice){ + //~ notice.update({ + //~ title: 'You choosed spice client viewer', text: 'File will be downloaded. Open it with spice remote-viewer.', icon: true, type: 'info', hide: true, + //~ confirm: { + //~ confirm: false + //~ }, + //~ buttons: { + //~ closer: true, + //~ sticker: false + //~ } + //~ }); + //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'spice-client','os':os}); + //~ } + //~ }, + //~ { + //~ text: 'VNC BROWSER', + //~ addClass: 'btn-primary', + //~ click: function(notice){ + //~ notice.update({ + //~ title: 'You choosed VNC browser viewer', text: 'Viewer will be opened in new window.\n Please allow popups!', icon: true, type: 'info', hide: true, + //~ confirm: { + //~ confirm: false + //~ }, + //~ buttons: { + //~ closer: true, + //~ sticker: false + //~ } + //~ }); + //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'vnc-html5','os':os}); + //~ } + //~ }, + //~ { + //~ text: 'VNC CLIENT', + //~ addClass: 'btn-primary', + //~ click: function(notice){ + //~ notice.update({ + //~ title: 'You choosed VNC client viewer', text: 'File will be downloaded. Open it with VNC client app.', icon: true, type: 'info', hide: true, + //~ confirm: { + //~ confirm: false + //~ }, + //~ buttons: { + //~ closer: true, + //~ sticker: false + //~ } + //~ }); + //~ socket.emit('domain_viewer',{'pk':data['id'],'kind':'vnc-client','os':os}); + //~ } + //~ }, + //~ ] + //~ }, + //~ buttons: { + //~ closer: false, + //~ sticker: false + //~ }, + //~ history: { + //~ history: false + //~ } + //~ }); +//~ } diff --git a/src/webapp/templates/pages/desktops_detail.html b/src/webapp/templates/pages/desktops_detail.html index 5f2861d92..7b3822c97 100644 --- a/src/webapp/templates/pages/desktops_detail.html +++ b/src/webapp/templates/pages/desktops_detail.html @@ -36,9 +36,6 @@

Status detailed info:

-
From c045da6a73750b00e24d86cf78e57c846a7cf99b Mon Sep 17 00:00:00 2001 From: beto Date: Tue, 8 Jan 2019 00:50:43 +0100 Subject: [PATCH 23/92] issue 29 user and port were hardcoded, solved --- src/engine/services/threads/threads.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/services/threads/threads.py b/src/engine/services/threads/threads.py index beddfa817..c23ed635b 100644 --- a/src/engine/services/threads/threads.py +++ b/src/engine/services/threads/threads.py @@ -62,8 +62,8 @@ def launch_disk_operations_thread(hyp_id, hostname, user='root', port=22): hyp_id=hyp_id, hostname=hostname, queue_actions=queue_disk_operation, - user='root', - port=22) + user=user, + port=port) thread_disk_operation.daemon = True thread_disk_operation.start() return thread_disk_operation, queue_disk_operation @@ -78,8 +78,8 @@ def launch_long_operations_thread(hyp_id, hostname, user='root', port=22): hyp_id=hyp_id, hostname=hostname, queue_actions=queue_long_operation, - user='root', - port=22) + user=user, + port=port) thread_long_operation.daemon = True thread_long_operation.start() return thread_long_operation, queue_long_operation From e0408706595f18e854f489b8017c1b9ae812043a Mon Sep 17 00:00:00 2001 From: beto Date: Tue, 8 Jan 2019 02:16:55 +0100 Subject: [PATCH 24/92] dock image creation with -f option and new versions --- build-docker-images.sh | 28 ++++++++++++++++++++++------ docker-compose.yml | 6 +++--- dockers/devel-debug.yml | 6 +++--- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/build-docker-images.sh b/build-docker-images.sh index c5e90a1a2..8539c84e3 100755 --- a/build-docker-images.sh +++ b/build-docker-images.sh @@ -6,19 +6,33 @@ if [ -z "$1" ]; then exit 1 fi -MAJOR=${1:0:1} -MINOR=${1:0:3} -PATCH=$1 +if [ $1 = "-f" ]; then + force=1 + if [ -z "$2" ]; then + echo "You need to specify a IsardVDI version with -f option! e.g. '1.1.0'" + exit 1 + fi + version=$2 +else + force=0 + version=$1 +fi + +MAJOR=${version:0:1} +MINOR=${version:0:3} +PATCH=$version # If a command fails, the whole script is going to stop set -e # Checkout to the specified version tag -git checkout $1 > /dev/null +if [ force = 1 ]; then + git checkout $1 > /dev/null +fi # Array containing all the images to build images=( - alpine-pandas + #alpine-pandas nginx hypervisor app @@ -29,6 +43,8 @@ for image in "${images[@]}"; do echo -e "\n\n\n" echo "Building $image" echo -e "\n\n\n" - docker build -f=dockers/$image/Dockerfile -t isard/$image:latest -t isard/$image:$MAJOR -t isard/$image:$MINOR -t isard/$image:$PATCH . + cmd="docker build -f dockers/$image/Dockerfile -t isard/$image:latest -t isard/$image:$MAJOR -t isard/$image:$MINOR -t isard/$image:$PATCH ." + echo $cmd + $cmd done diff --git a/docker-compose.yml b/docker-compose.yml index f47c492ce..8c4870022 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: mode: host networks: - isard_network - image: isard/nginx:1.0 + image: isard/nginx:1.1 restart: always isard-hypervisor: @@ -54,7 +54,7 @@ services: - "55900-55949:55900-55949" networks: - isard_network - image: isard/hypervisor:1.0 + image: isard/hypervisor:1.1 privileged: true restart: always @@ -88,7 +88,7 @@ services: - "isard-engine:127.0.0.1" networks: - isard_network - image: isard/app:1.0 + image: isard/app:1.1 restart: always depends_on: - isard-database diff --git a/dockers/devel-debug.yml b/dockers/devel-debug.yml index a80681312..b8f5efaa9 100644 --- a/dockers/devel-debug.yml +++ b/dockers/devel-debug.yml @@ -25,7 +25,7 @@ services: isard-nginx: restart: always - image: isard/nginx:1.0.1b + image: isard/nginx:1.1 build: context: . dockerfile: dockers/nginx/Dockerfile @@ -44,7 +44,7 @@ services: isard-hypervisor: restart: always - image: isard/hypervisor:1.0.1b + image: isard/hypervisor:1.1 build: context: . dockerfile: dockers/hypervisor/Dockerfile @@ -67,7 +67,7 @@ services: isard-app: restart: always - image: isard/app:1.0.0 + image: isard/app:1.1 build: context: . dockerfile: dockers/app_devel/Dockerfile From 635328ef5be2671b7826095ee40226d631846433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas=20Auquer?= Date: Tue, 8 Jan 2019 08:32:06 +0100 Subject: [PATCH 25/92] Update build-docker-images.sh Removed local modifications --- build-docker-images.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-docker-images.sh b/build-docker-images.sh index f04070a25..c5e90a1a2 100755 --- a/build-docker-images.sh +++ b/build-docker-images.sh @@ -14,11 +14,11 @@ PATCH=$1 set -e # Checkout to the specified version tag -#git checkout $1 > /dev/null +git checkout $1 > /dev/null # Array containing all the images to build images=( -# alpine-pandas + alpine-pandas nginx hypervisor app From 7b0dff99da914436233288b20288a44e4797141e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas=20Auquer?= Date: Tue, 8 Jan 2019 08:33:12 +0100 Subject: [PATCH 26/92] removed modifications for local development Removed local development --- docker-compose.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 35488b302..f47c492ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,10 +60,6 @@ services: isard-app: volumes: - - type: bind - source: /home/darta/jvinolas/isard/src - target: /isard - read_only: false - type: volume source: sshkeys target: /root/.ssh From 7a84450f609b600f075d4930e7784464a34b7ffd Mon Sep 17 00:00:00 2001 From: darta Date: Tue, 8 Jan 2019 12:02:48 +0100 Subject: [PATCH 27/92] Modified names --- .gitignore | 3 +++ dockers/{app_devel => app-devel}/Dockerfile | 0 dockers/{app_devel => app-devel}/requirements.pip3 | 0 dockers/{app_devel => app-devel}/supervisord.conf | 0 dockers/{external_hyper => remote-hyper}/README.md | 0 dockers/{external_hyper => remote-hyper}/docker-compose.yml | 2 +- 6 files changed, 4 insertions(+), 1 deletion(-) rename dockers/{app_devel => app-devel}/Dockerfile (100%) rename dockers/{app_devel => app-devel}/requirements.pip3 (100%) rename dockers/{app_devel => app-devel}/supervisord.conf (100%) rename dockers/{external_hyper => remote-hyper}/README.md (100%) rename dockers/{external_hyper => remote-hyper}/docker-compose.yml (96%) diff --git a/.gitignore b/.gitignore index a6c8bbf73..cbe86e279 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ csv/ #Wizard disable install/.wizard + +devel.yml +build-devel-images.sh diff --git a/dockers/app_devel/Dockerfile b/dockers/app-devel/Dockerfile similarity index 100% rename from dockers/app_devel/Dockerfile rename to dockers/app-devel/Dockerfile diff --git a/dockers/app_devel/requirements.pip3 b/dockers/app-devel/requirements.pip3 similarity index 100% rename from dockers/app_devel/requirements.pip3 rename to dockers/app-devel/requirements.pip3 diff --git a/dockers/app_devel/supervisord.conf b/dockers/app-devel/supervisord.conf similarity index 100% rename from dockers/app_devel/supervisord.conf rename to dockers/app-devel/supervisord.conf diff --git a/dockers/external_hyper/README.md b/dockers/remote-hyper/README.md similarity index 100% rename from dockers/external_hyper/README.md rename to dockers/remote-hyper/README.md diff --git a/dockers/external_hyper/docker-compose.yml b/dockers/remote-hyper/docker-compose.yml similarity index 96% rename from dockers/external_hyper/docker-compose.yml rename to dockers/remote-hyper/docker-compose.yml index 78d232a05..7750e41af 100644 --- a/dockers/external_hyper/docker-compose.yml +++ b/dockers/remote-hyper/docker-compose.yml @@ -15,7 +15,7 @@ services: target: /etc/pki/libvirt-spice read_only: false ports: - - "22:22" + - "2022:22" - "5900-5949:5900-5949" - "55900-55949:55900-55949" image: isard/hypervisor:1.0 From 255141af3abd143480c9ea909e22dbac778fe7d2 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 8 Jan 2019 15:18:36 +0100 Subject: [PATCH 28/92] Fixed python libs in app container --- dockers/app/Dockerfile | 2 +- dockers/app/requirements.pip3 | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dockers/app/Dockerfile b/dockers/app/Dockerfile index 9858c2102..521b3a450 100644 --- a/dockers/app/Dockerfile +++ b/dockers/app/Dockerfile @@ -1,7 +1,7 @@ FROM isard/alpine-pandas:latest MAINTAINER isard -RUN apk add --no-cache bash yarn py3-libvirt py3-paramiko py3-lxml py3-xmltodict py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-flask-login py3-netaddr py3-requests curl openssh-client +RUN apk add --no-cache bash yarn py3-libvirt py3-paramiko py3-lxml py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-netaddr py3-requests curl openssh-client RUN mkdir /isard ADD ./src /isard diff --git a/dockers/app/requirements.pip3 b/dockers/app/requirements.pip3 index e59852427..e9e5f2db0 100644 --- a/dockers/app/requirements.pip3 +++ b/dockers/app/requirements.pip3 @@ -5,3 +5,6 @@ rethinkdb==2.3.0.post6 pynpm==0.1.1 graphyte==1.4 pem==18.2.0 +Flask-Login==0.4.1 +xmltodict==0.11.0 + From ae0d7f187d790cf2822b94bc67a729e1a1421f48 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 8 Jan 2019 19:24:56 +0100 Subject: [PATCH 29/92] testing improvement scripts --- dockers/app/certs.sh | 28 ++++++++++++++-------------- dockers/app/known_hosts | 12 ++++++++++++ 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 dockers/app/known_hosts diff --git a/dockers/app/certs.sh b/dockers/app/certs.sh index 4eb184e94..da668a23f 100755 --- a/dockers/app/certs.sh +++ b/dockers/app/certs.sh @@ -1,20 +1,20 @@ #!/bin/bash -public_key="/root/.ssh/authorized_keys" -if [ -f "$public_key" ] + +# Remove all isard-hypervisor lines from known_hosts +sed -i '/isard-hypervisor/d' /root/.ssh/known_hosts + +# If no id_rsa.pub key yet, create new one +auth_keys="/root/.ssh/id_rsa.pub" +if [ -f "$auth_keys" ] then - echo "$public_key found, so not generating new ones." + echo "$auth_keys found, so not generating new ones." else - echo "$public_key not found, generating new ones." + echo "$auth_keys not found, generating new ones." cat /dev/zero | ssh-keygen -q -N "" + #Copy new host key to authorized_keys (so isard-hypervisor can get it also) cp /root/.ssh/id_rsa.pub /root/.ssh/authorized_keys - - echo "Scanning isard-hypervisor key..." - ssh-keyscan isard-hypervisor > /root/.ssh/known_hosts - while [ ! -s /root/.ssh/known_hosts ] - do - sleep .5 - echo "Waiting for isard-hypervisor to be online..." - ssh-keyscan isard-hypervisor > /root/.ssh/known_hosts - done - echo "isard-hypervisor online..." fi + +# Now scan for isard-hypervisor for 10 seconds (should be more than enough) +echo "Scanning isard-hypervisor key..." +ssh-keyscan -T 10 isard-hypervisor > /root/.ssh/known_hosts diff --git a/dockers/app/known_hosts b/dockers/app/known_hosts new file mode 100644 index 000000000..a48013d4a --- /dev/null +++ b/dockers/app/known_hosts @@ -0,0 +1,12 @@ +192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= +192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ +192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd +192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= +192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ +192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd +192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ +192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= +192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd +192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ +192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= +192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd From c8758172fcd3611f9127d1bc436e17355b39bb66 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 8 Jan 2019 19:45:28 +0100 Subject: [PATCH 30/92] removed unnecessary files from commit --- .gitignore | 1 + dockers/app/known_hosts | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 dockers/app/known_hosts diff --git a/.gitignore b/.gitignore index cbe86e279..eee807834 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ install/.wizard devel.yml build-devel-images.sh +known_hosts diff --git a/dockers/app/known_hosts b/dockers/app/known_hosts deleted file mode 100644 index a48013d4a..000000000 --- a/dockers/app/known_hosts +++ /dev/null @@ -1,12 +0,0 @@ -192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= -192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ -192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd -192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= -192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ -192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd -192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ -192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= -192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd -192.168.0.222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9aaZRxWz6AN8y2RSpR/662ZhCdiLS+lw+uDm73VLiNs8dmo37CSuH53hanep7Z+DgL405Pa/eZCPN6kYUl6Lu+AgQyNytywGricS71mBclT9T+DjNflnYd76b7EkOXaWmUJO3T1Unlkq2eLYqSUqkFldHzHkUrTRxozhAOmYy4zO/xCJ2RlVjNDtLrVW806eZkEfw47kdsrfKlFS6A//9alD/isM1tc6yJFicEPIMVpQLvovsa4gFeCAHSt+keWLblsUXU0IMrZTv4SPThECsigtghRDmST3KJqHJnvjj7f1eAkAVGOetqsbK+DjCqr5qxie3J35f4eUQZxe60zwZ -192.168.0.222 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdkxnApgdWxU7gefRb5BMQXXNo4FSCPv40rbJa6rsxV9PdYXvzn6W6POaCurY4aEMXkcxYufF5jfzmKgk60vNE= -192.168.0.222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHRI7iZNdHKO/fc9/iThRnbuDLRqg/GF7P1LrVusGJGd From 905399561839dcb146347932950da58bf0483111 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 8 Jan 2019 20:40:45 +0100 Subject: [PATCH 31/92] working --- dockers/app/supervisord.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockers/app/supervisord.conf b/dockers/app/supervisord.conf index 619302229..94ac83d38 100644 --- a/dockers/app/supervisord.conf +++ b/dockers/app/supervisord.conf @@ -26,7 +26,7 @@ stderr_logfile=/isard/logs/webapp-error.log [program:engine] directory=/isard -command=sh -c "virsh -c qemu+ssh://isard-hypervisor/system quit && python3 run_engine.py" +command=python3 run_engine.py autostart=true autorestart=false startsecs=2 From 7e9caf416ef3dbd7dbc017108ef4beb882e1e2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Wed, 9 Jan 2019 10:16:54 +0100 Subject: [PATCH 32/92] Added Mastodon to the README and fixed Twitter Now there's the Mastodon profile in the README. Also added a link to the Twitter account --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f6456b607..0c2842cae 100644 --- a/README.md +++ b/README.md @@ -64,4 +64,6 @@ Go to [IsardVDI Project website](http://www.isardvdi.com/) Please send us an email to info@isardvdi.com if you have any questions or fill in an issue. ### Social Networks -Twitter: @isard_vdi +Twitter: [@isard_vdi](https://twitter.com/isard_vdi) +Mastodon: [@isard@fosstodon.org](https://fosstodon.org/@isard) + From 81bff7d8c7a6d1d4f7a341eb490fbd8c54260dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Wed, 9 Jan 2019 10:25:55 +0100 Subject: [PATCH 33/92] Added IsardVDI logo to the README With this commit, the readme is more appealing Smalled the README logo Also moved it a bit up Fixed the logo width Now the logo should get the correct width --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0c2842cae..794d32e56 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Isard**VDI** +IsardVDI Logo + [![](https://img.shields.io/github/release/isard-vdi/isard.svg)](https://github.com/isard-vdi/isard/releases) [![](https://img.shields.io/badge/docker--compose-ready-blue.svg)](https://github.com/isard-vdi/isard/blob/master/docker-compose.yml) [![](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://isardvdi.readthedocs.io/en/latest/) [![](https://img.shields.io/badge/license-AGPL%20v3.0-brightgreen.svg)](https://github.com/isard-vdi/isard/blob/master/LICENSE) Open Source KVM Virtual Desktops based on KVM Linux and dockers. From 1f61ccc804edf88cbfefe8bbe61c1ef0f7c013d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Wed, 9 Jan 2019 10:38:58 +0100 Subject: [PATCH 34/92] Changed the social media accounts order Now Mastodon is in the first position. Support free software! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 794d32e56..539729a0a 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,6 @@ Go to [IsardVDI Project website](http://www.isardvdi.com/) Please send us an email to info@isardvdi.com if you have any questions or fill in an issue. ### Social Networks +Mastodon: [@isard@fosstodon.org](https://fosstodon.org/@isard) Twitter: [@isard_vdi](https://twitter.com/isard_vdi) -Mastodon: [@isard@fosstodon.org](https://fosstodon.org/@isard) From 186346a8360a0cb6555fd3cb94455b7a4cccdcb9 Mon Sep 17 00:00:00 2001 From: beto Date: Thu, 10 Jan 2019 01:56:26 +0100 Subject: [PATCH 35/92] download thread refactorized waiting other threads like disk operations --- dockers/app-devel/Dockerfile | 9 +-- dockers/app-devel/requirements.pip3 | 3 + dockers/app-devel/supervisord.conf | 20 +++---- dockers/build-docker-images-devel.sh | 2 +- dockers/devel-debug.yml | 2 +- src/engine/config.py | 18 +++++- src/engine/models/manager_hypervisors.py | 2 +- src/engine/services/lib/qcow.py | 1 + src/engine/services/log.py | 1 + .../services/threads/download_thread.py | 60 ++++++++++++++----- 10 files changed, 85 insertions(+), 33 deletions(-) diff --git a/dockers/app-devel/Dockerfile b/dockers/app-devel/Dockerfile index e60e95adb..2997c89a4 100644 --- a/dockers/app-devel/Dockerfile +++ b/dockers/app-devel/Dockerfile @@ -1,7 +1,8 @@ FROM isard/alpine-pandas:latest MAINTAINER isard -RUN apk add --no-cache git yarn py3-libvirt py3-paramiko py3-lxml py3-xmltodict py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-flask-login py3-netaddr py3-requests curl +RUN apk add --no-cache bash yarn py3-libvirt py3-paramiko py3-lxml py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-netaddr py3-requests curl openssh-client +RUN apk add --no-cache git ######## only devel ######## #RUN mkdir /isard @@ -9,7 +10,7 @@ RUN apk add --no-cache git yarn py3-libvirt py3-paramiko py3-lxml py3-xmltodict ############################ ######## only devel ######## -COPY dockers/app_devel/requirements.pip3 /requirements.pip3 +COPY dockers/app-devel/requirements.pip3 /requirements.pip3 ############################ RUN pip3 install --no-cache-dir -r requirements.pip3 @@ -37,10 +38,10 @@ RUN apk add supervisor RUN mkdir -p /var/log/supervisor ######## only devel ######## -COPY dockers/app_devel/supervisord.conf /etc/supervisord.conf +COPY dockers/app-devel/supervisord.conf /etc/supervisord.conf ############################ COPY dockers/app/certs.sh / CMD /usr/bin/supervisord -c /etc/supervisord.conf #CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] -#CMD ["sh", "/init.sh"] \ No newline at end of file +#CMD ["sh", "/init.sh"] diff --git a/dockers/app-devel/requirements.pip3 b/dockers/app-devel/requirements.pip3 index e59852427..e9e5f2db0 100644 --- a/dockers/app-devel/requirements.pip3 +++ b/dockers/app-devel/requirements.pip3 @@ -5,3 +5,6 @@ rethinkdb==2.3.0.post6 pynpm==0.1.1 graphyte==1.4 pem==18.2.0 +Flask-Login==0.4.1 +xmltodict==0.11.0 + diff --git a/dockers/app-devel/supervisord.conf b/dockers/app-devel/supervisord.conf index 55cd2b8de..5a206ad35 100644 --- a/dockers/app-devel/supervisord.conf +++ b/dockers/app-devel/supervisord.conf @@ -15,21 +15,21 @@ stderr_logfile=/isard/logs/certs-error.log [program:webapp] directory=/isard -command=python3 run_webapp.py 1>/isard/logs/webapp.log 2>/isard/logs/webapp-error.log +command=python3 run_webapp.py autostart=true autorestart=true startsecs=2 priority=10 -stdout_logfile=/isard/logs/webapp-supervisord.log -stderr_logfile=/isard/logs/webapp-supervisord-error.log +stdout_logfile=/isard/logs/webapp.log +stderr_logfile=/isard/logs/webapp-error.log [program:engine] directory=/isard -command=sh -c "sleep 15 && python3 run_engine.py 1>/isard/logs/engine.log 2>/isard/logs/engine-error.log" -#command=python3 run_engine.py +#command=sh -c "sleep 15 && python3 run_engine.py 1>/isard/logs/engine.log 2>/isard/logs/engine-error.log" +command=python3 run_engine.py autostart=true -autorestart=true -startsecs=10 -priority=5 -stdout_logfile=/isard/logs/engine-supervisord.log -stderr_logfile=/isard/logs/engine-supervisord-error.log +autorestart=false +startsecs=2 +priority=11 +stdout_logfile=/isard/logs/engine.log +stderr_logfile=/isard/logs/engine-error.log diff --git a/dockers/build-docker-images-devel.sh b/dockers/build-docker-images-devel.sh index ebba7ca60..2f51f9747 100755 --- a/dockers/build-docker-images-devel.sh +++ b/dockers/build-docker-images-devel.sh @@ -21,7 +21,7 @@ images=( #alpine-pandas #nginx #hypervisor - app_devel + app-devel ) # Build all the images and tag them correctly diff --git a/dockers/devel-debug.yml b/dockers/devel-debug.yml index 7b236cfb9..eb9a59036 100644 --- a/dockers/devel-debug.yml +++ b/dockers/devel-debug.yml @@ -73,7 +73,7 @@ services: - "isard-engine:127.0.0.1" networks: - isard_network - image: "isard/app_devel:${TAG_DEVEL}" + image: "isard/app-devel:${TAG_DEVEL}" restart: "no" depends_on: - "isard-database" diff --git a/src/engine/config.py b/src/engine/config.py index 95140be7e..d2f487f29 100644 --- a/src/engine/config.py +++ b/src/engine/config.py @@ -27,6 +27,8 @@ # ~ sys.exit(0) config_exists=False +first_loop = True +fail_first_loop = False while not config_exists: try: rcfg = configparser.ConfigParser() @@ -34,11 +36,18 @@ RETHINK_HOST = rcfg.get('RETHINKDB', 'HOST') RETHINK_PORT = rcfg.get('RETHINKDB', 'PORT') RETHINK_DB = rcfg.get('RETHINKDB', 'DBNAME') + if fail_first_loop: + print('ENGINE STARTING, isard.conf accesed') config_exists=True except: - print('ENGINE START PENDING: Missing isard.conf file. Run webapp and access to http://localhost:5000 or https://localhost on dockers.') + if first_loop is True: + print('ENGINE START PENDING: Missing isard.conf file. Run webapp and access to http://localhost:5000 or https://localhost on dockers.') + first_loop = False + fail_first_loop = True time.sleep(1) +first_loop = True +fail_first_loop = False table_exists=False while not table_exists: try: @@ -47,8 +56,13 @@ grafana= rconfig['grafana'] rconfig = rconfig['engine'] table_exists=True + if fail_first_loop: + print('ENGINE STARTING, database is online') except: - print('ENGINE START PENDING: Missing database isard. Run webapp and access to http://localhost:5000 or https://localhost on dockers.') + if first_loop is True: + print('ENGINE START PENDING: Missing database isard. Run webapp and access to http://localhost:5000 or https://localhost on dockers.') + first_loop = False + fail_first_loop = True time.sleep(1) #print(rconfig) diff --git a/src/engine/models/manager_hypervisors.py b/src/engine/models/manager_hypervisors.py index 87bb07b2f..92815c583 100644 --- a/src/engine/models/manager_hypervisors.py +++ b/src/engine/models/manager_hypervisors.py @@ -348,7 +348,7 @@ def run(self): logs.main.debug('Launching Download Changes Thread') self.manager.t_downloads_changes = launch_thread_download_changes(self.manager) - #launch brrom thread + #launch brom thread self.manager.t_broom = launch_thread_broom(self.manager) #launch events thread diff --git a/src/engine/services/lib/qcow.py b/src/engine/services/lib/qcow.py index 7f0e59045..0d3db4320 100644 --- a/src/engine/services/lib/qcow.py +++ b/src/engine/services/lib/qcow.py @@ -506,6 +506,7 @@ def test_hypers_disk_operations(hyps_disk_operations): d_hyp = get_hyp_hostname_user_port_from_id(hyp_id) cmds1 = list() for pool_id in get_pools_from_hyp(hyp_id): + # test write permissions in root dir of all paths defined in pool paths = {k: [l['path'] for l in d] for k, d in get_pool(pool_id)['paths'].items()} for k, p in paths.items(): for path in p: diff --git a/src/engine/services/log.py b/src/engine/services/log.py index dd8024fab..e7471e8c3 100644 --- a/src/engine/services/log.py +++ b/src/engine/services/log.py @@ -36,6 +36,7 @@ # logger = log.getLogger() # logger.setLevel(LOG_LEVEL_NUM) # log.Formatter(fmt=LOG_FORMAT,datefmt=LOG_DATE_FORMAT) +print(f'Engine log level: {LOG_LEVEL} ({LOG_LEVEL_NUM})') # log.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT,level=LOG_LEVEL_NUM) log.basicConfig(filename=LOG_DIR + '/' + LOG_FILE, diff --git a/src/engine/services/threads/download_thread.py b/src/engine/services/threads/download_thread.py index edd347ce6..5f7276f82 100644 --- a/src/engine/services/threads/download_thread.py +++ b/src/engine/services/threads/download_thread.py @@ -11,6 +11,7 @@ import os import subprocess import rethinkdb as r +from time import sleep from engine.config import CONFIG_DICT from engine.services.db.db import new_rethink_connection, remove_media @@ -24,23 +25,28 @@ from engine.services.lib.functions import get_tid URL_DOWNLOAD_INSECURE_SSL = True +TIMEOUT_WAITING_HYPERVISOR_TO_DOWNLOAD = 10 class DownloadThread(threading.Thread, object): - def __init__(self, hyp_hostname, url, path, table, id_down, dict_header, finalished_threads): + def __init__(self, url, path, path_selected, table, id_down, dict_header, finalished_threads,manager,pool_id,type_path_selected): threading.Thread.__init__(self) self.name = '_'.join([table, id_down]) self.table = table self.path = path + self.path_selected = path_selected self.id = id_down self.url = url self.dict_header = dict_header self.stop = False - d = get_hyp_hostname_user_port_from_id(hyp_hostname) - self.hostname = d['hostname'] - self.user = d['user'] - self.port = d['port'] self.finalished_threads = finalished_threads + self.manager = manager + self.hostname = None + self.user = None + self.port = None + self.pool_id = pool_id + self.type_path_selected = type_path_selected + def run(self): # if self.table == 'domains': @@ -58,6 +64,35 @@ def run(self): # # hyp_to_disk_create = get_host_disk_operations_from_path(path_selected, pool=self.pool, # type_path=type_path_selected) + + # hypervisor to launch download command + # wait to threads disk_operations are alive + time_elapsed = 0 + path_selected = self.path_selected + while True: + if len(self.manager.t_disk_operations) > 0: + + hyp_to_disk_create = get_host_disk_operations_from_path(path_selected, + pool=self.pool_id, + type_path=self.type_path_selected) + if self.manager.t_disk_operations.get(hyp_to_disk_create,False) is not False: + if self.manager.t_disk_operations[hyp_to_disk_create].is_alive(): + d = get_hyp_hostname_user_port_from_id(hyp_to_disk_create) + self.hostname = d['hostname'] + self.user = d['user'] + self.port = d['port'] + break + sleep(0.2) + time_elapsed += 0.2 + if time_elapsed > TIMEOUT_WAITING_HYPERVISOR_TO_DOWNLOAD: + logs.downloads.info(f'Timeout ({TIMEOUT_WAITING_HYPERVISOR_TO_DOWNLOAD} sec) waiting hypervisor online to download {url_base}') + if self.table == 'domains': + update_domain_status('DownloadFailed', self.id, detail="downloaded disk") + else: + update_status_table(self.table, 'DownloadFailed', self.id) + self.finalished_threads.append(self.path) + return False + header_template = "--header '{header_key}: {header_value}' " headers = '' @@ -291,12 +326,6 @@ def start_download(self, dict_changes): if len(subdir_url) > 0: url_base = url_base + '/' + subdir_url - - # hypervisor to launch download command - hyp_to_disk_create = get_host_disk_operations_from_path(path_selected, - pool=pool_id, - type_path=type_path_selected) - if dict_changes.get('url-web',False) is not False: url = dict_changes['url-web'] @@ -322,13 +351,16 @@ def start_download(self, dict_changes): # launching download threads if new_file_path not in self.download_threads: - self.download_threads[new_file_path] = DownloadThread(hyp_to_disk_create, - url, + self.download_threads[new_file_path] = DownloadThread(url, new_file_path, + path_selected, table, id_down, header_dict, - self.finalished_threads) + self.finalished_threads, + self.manager, + pool_id, + type_path_selected) self.download_threads[new_file_path].daemon = True self.download_threads[new_file_path].start() From 33a8f280df7173db26c3aeddfe0d9a61b4015a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Thu, 10 Jan 2019 23:00:50 +0100 Subject: [PATCH 36/92] Update dockers/app-devel/Dockerfile Co-Authored-By: alarraz --- dockers/app-devel/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dockers/app-devel/Dockerfile b/dockers/app-devel/Dockerfile index 2997c89a4..8782a0f14 100644 --- a/dockers/app-devel/Dockerfile +++ b/dockers/app-devel/Dockerfile @@ -1,7 +1,7 @@ FROM isard/alpine-pandas:latest MAINTAINER isard -RUN apk add --no-cache bash yarn py3-libvirt py3-paramiko py3-lxml py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-netaddr py3-requests curl openssh-client +RUN apk add --no-cache git bash yarn py3-libvirt py3-paramiko py3-lxml py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-netaddr py3-requests curl openssh-client RUN apk add --no-cache git ######## only devel ######## From b6af9ca45f5fc23ce06d7b1f79905dcc42a96d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 09:34:32 +0100 Subject: [PATCH 37/92] Updated template name from Private to Template --- src/webapp/templates/pages/desktops_modals.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/templates/pages/desktops_modals.html b/src/webapp/templates/pages/desktops_modals.html index 5146fc178..1ab6e46d0 100644 --- a/src/webapp/templates/pages/desktops_modals.html +++ b/src/webapp/templates/pages/desktops_modals.html @@ -250,7 +250,7 @@

New template properties

From d73c158f49977bb77a0f2e0b5d8d9d813587791c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 09:45:23 +0100 Subject: [PATCH 38/92] Only in failed and stopped state we allow edit or delete --- src/webapp/static/js/desktops.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/static/js/desktops.js b/src/webapp/static/js/desktops.js index 9b5ec6a20..f6cfe261f 100644 --- a/src/webapp/static/js/desktops.js +++ b/src/webapp/static/js/desktops.js @@ -488,7 +488,7 @@ function setDesktopDetailButtonsStatus(id,status){ }else{ $('#actions-'+id+' *[class^="btn"]').prop('disabled', true); } - if(status!='Started'){ + if(status=='Failed'){ $('#actions-'+id+' .btn-edit').prop('disabled', false); $('#actions-'+id+' .btn-delete').prop('disabled', false); } From d90d9d9498a11b222551bf681ca23a62006dee43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 10:52:11 +0100 Subject: [PATCH 39/92] Updated links --- src/webapp/templates/pages/about.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webapp/templates/pages/about.html b/src/webapp/templates/pages/about.html index 7da503934..9f3ac6c60 100644 --- a/src/webapp/templates/pages/about.html +++ b/src/webapp/templates/pages/about.html @@ -28,8 +28,10 @@

IsardVDI

Virtual Desktops Infrastructure

Contact IsardVDI

@@ -42,6 +44,7 @@

License:

+ {% endblock %} From 050b43cb441df48159948097f87a24d697380ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 19:54:18 +0100 Subject: [PATCH 40/92] Fixed domain status checks --- src/webapp/lib/api.py | 139 ++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 86 deletions(-) diff --git a/src/webapp/lib/api.py b/src/webapp/lib/api.py index c49af43ee..018ba3359 100644 --- a/src/webapp/lib/api.py +++ b/src/webapp/lib/api.py @@ -37,70 +37,61 @@ def __init__(self): #~ GENERIC def check(self,dict,action): - #~ These are the actions: - #~ {u'skipped': 0, u'deleted': 1, u'unchanged': 0, u'errors': 0, u'replaced': 0, u'inserted': 0} + ''' + These are the actions: + {u'skipped': 0, u'deleted': 1, u'unchanged': 0, u'errors': 0, u'replaced': 0, u'inserted': 0} + ''' if dict[action]: return True if not dict['errors']: return True return False - #~ def update_desktop_status(self,user,data,remote_addr): - #~ try: - #~ if data['name']=='status': - #~ if data['value']=='Stopping': - #~ if app.isardapi.update_table_value('domains', data['pk'], data['name'], data['value']): - #~ return json.dumps({'title':'Desktop stopping success','text':'Desktop '+data['pk']+' will be stopped','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} - #~ else: - #~ return json.dumps({'title':'Desktop stopping error','text':'Desktop '+data['pk']+' can\'t be stopped now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} - #~ if data['value']=='Deleting': - #~ if app.isardapi.update_table_value('domains', data['pk'], data['name'], data['value']): - #~ return json.dumps({'title':'Desktop deleting success','text':'Desktop '+data['pk']+' will be deleted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} - #~ else: - #~ return json.dumps({'title':'Desktop deleting error','text':'Desktop '+data['pk']+' can\'t be deleted now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} - #~ if data['value']=='Starting': - #~ if float(app.isardapi.get_user_quotas(current_user.username)['rqp']) >= 100: - #~ return json.dumps({'title':'Quota exceeded','text':'Desktop '+data['pk']+' can\'t be started because you have exceeded quota','icon':'warning','type':'warning'}), 500, {'ContentType':'application/json'} - #~ self.auto_interface_set(user,data['pk'],remote_addr) - #~ if app.isardapi.update_table_value('domains', data['pk'], data['name'], data['value']): - #~ return json.dumps({'title':'Desktop starting success','text':'Desktop '+data['pk']+' will be started','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} - #~ else: - #~ return json.dumps({'title':'Desktop starting error','text':'Desktop '+data['pk']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} - #~ return json.dumps({'title':'Method not allowd','text':'Desktop '+data['pk']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} - #~ except Exception as e: - #~ print('Error updating desktop status for domain '+data['pk']+': '+str(e)) - #~ return json.dumps({'title':'Desktop starting error','text':'Desktop '+data['pk']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} - def update_table_status(self,user,table,data,remote_addr): item = table[:-1].capitalize() + # ~ with app.app_context(): + # ~ dom = r.table('domains').get('pk').pluck('status','name') try: if data['name']=='status': if data['value']=='DownloadAborting': - if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' aborting success','text':item+' '+data['pk']+' will be aborted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + if dom['status'] in ['Downloading']: + if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): + return json.dumps({'title':item+' aborting success','text':item+' '+data['name']+' will be aborted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + else: + return json.dumps({'title':item+' aborting error','text':item+' '+data['name']+' can\'t be aborted. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + else: + return json.dumps({'title':item+' aborting error','text':item+' '+data['name']+' can\'t be aborted while not Downloading','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Stopping': - if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' stopping success','text':item+' '+data['pk']+' will be stopped','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + if dom['status'] in ['Downloading']: + if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): + return json.dumps({'title':item+' stopping success','text':item+' '+data['name']+' will be stopped','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + else: + return json.dumps({'title':item+' stopping error','text':item+' '+data['name']+' can\'t be stopped. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' stopping error','text':item+' '+data['pk']+' can\'t be stopped now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' stopping error','text':item+' '+data['name']+' can\'t be stopped while not Started','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Deleting': - if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' deleting success','text':item+' '+data['pk']+' will be deleted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + if dom['status'] in ['Stopped','Failed']: + if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): + return json.dumps({'title':item+' deleting success','text':item+' '+data['name']+' will be deleted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + else: + return json.dumps({'title':item+' deleting error','text':item+' '+data['name']+' can\'t be deleted. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' deleting error','text':item+' '+data['pk']+' can\'t be deleted now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' deleting error','text':item+' '+data['name']+' can\'t be deleted while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Starting': - if float(app.isardapi.get_user_quotas(current_user.username)['rqp']) >= 100: - return json.dumps({'title':'Quota exceeded','text':item+' '+data['pk']+' can\'t be started because you have exceeded quota','icon':'warning','type':'warning'}), 500, {'ContentType':'application/json'} - self.auto_interface_set(user,data['pk'],remote_addr) - if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' starting success','text':item+' '+data['pk']+' will be started','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + if dom['status'] in ['Stopped','Failed']: + if float(app.isardapi.get_user_quotas(current_user.username)['rqp']) >= 100: + return json.dumps({'title':'Quota exceeded','text':item+' '+dom['name']+' can\'t be started because you have exceeded quota','icon':'warning','type':'warning'}), 500, {'ContentType':'application/json'} + self.auto_interface_set(user,data['pk'],remote_addr) + if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): + return json.dumps({'title':item+' starting success','text':item+' '+data['pk']+' will be started','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + else: + return json.dumps({'title':item+' starting error','text':item+' '+data['pk']+' can\'t be started. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' starting error','text':item+' '+data['pk']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} - return json.dumps({'title':'Method not allowd','text':item+' '+data['pk']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' starting error','text':item+' '+data['name']+' can\'t be started while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':'Method not allowed','text':'That action is not allowed!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} except Exception as e: log.error('Error updating status for '+data['pk']+': '+str(e)) return json.dumps({'title':item+' starting error','text':item+' '+data['pk']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} - def auto_interface_set(self,user,id, remote_addr): with app.app_context(): dict=r.table('domains').get(id).pluck("create_dict").run(db.conn)['create_dict'] @@ -170,7 +161,6 @@ def add_listOfDicts2table(self,list,table): return False def show_disposable(self,client_ip): - # ~ return False disposables_config=self.config['disposable_desktops'] if disposables_config['active']: with app.app_context(): @@ -183,8 +173,7 @@ def show_disposable(self,client_ip): ''' MEDIA ''' - def get_user_media(self, user): #, filterdict=False): - #~ if not filterdict: filterdict={'kind': 'desktop'} + def get_user_media(self, user): with app.app_context(): media=list(r.table('media').get_all(user, index='user').run(db.conn)) return media @@ -193,25 +182,9 @@ def get_media_installs(self): with app.app_context(): data=r.table('virt_install').run(db.conn) return self.f.table_values_bstrap(data) - #~ if pluck and not id: - #~ if order: - #~ data=r.table(table).order_by(order).pluck(pluck).run(db.conn) - #~ return self.f.table_values_bstrap(data) if flatten else list(data) - #~ else: - #~ data=r.table(table).pluck(pluck).run(db.conn) - #~ return self.f.table_values_bstrap(data) if flatten else list(data) - #~ if pluck and id: - #~ data=r.table(table).get(id).pluck(pluck).run(db.conn) - #~ return self.f.flatten_dict(data) if flatten else data - #~ if order: - #~ data=r.table(table).order_by(order).run(db.conn) - #~ return self.f.table_values_bstrap(data) if flatten else list(data) - #~ else: - #~ data=r.table(table).run(db.conn) - #~ return self.f.table_values_bstrap(data) if flatten else list(data) - - -#~ STATUS + ''' + STATUS + ''' def get_domain_last_messages(self, id): with app.app_context(): return r.table('domains_status').get_all(id, index='name').order_by(r.desc('when')).pluck('when',{'status':['state','state_reason']}).limit(10).run(db.conn) @@ -220,7 +193,9 @@ def get_domain_last_events(self, id): with app.app_context(): return r.table('hypervisors_events').get_all(id, index='domain').order_by(r.desc('when')).limit(10).run(db.conn) - + ''' + USER + ''' def get_user(self, user): with app.app_context(): user=self.f.flatten_dict(r.table('users').get(user).run(db.conn)) @@ -230,7 +205,6 @@ def get_user(self, user): def get_user_domains(self, user, filterdict=False): if not filterdict: filterdict={'kind': 'desktop'} with app.app_context(): - # ~ domains=self.f.table_values_bstrap(r.table('domains').get_all(user, index='user').filter(filterdict).without('xml').run(db.conn)) domains=list(r.table('domains').get_all(user, index='user').filter(filterdict).without('xml','history_domain','allowed').run(db.conn)) return domains @@ -246,8 +220,6 @@ def get_category_domains(self, user, filterdict=False): domains=self.f.table_values_bstrap(r.table('domains').get_all(category, index='category').filter(filterdict).without('xml').run(db.conn)) return domains - - def get_group_users(self, group,pluck=''): with app.app_context(): users=list(r.table('users').get_all(group, index='group').order_by('username').pluck(pluck).run(db.conn)) @@ -256,7 +228,6 @@ def get_group_users(self, group,pluck=''): def get_domain(self, id, human_size=False, flatten=True): #~ Should verify something??? with app.app_context(): - domain = r.table('domains').get(id).without('xml','history_domain','progress').run(db.conn) try: if flatten: @@ -265,17 +236,14 @@ def get_domain(self, id, human_size=False, flatten=True): domain['hardware-memory']=self.human_size(domain['hardware-memory'] * 1000) if 'disks_info' in domain: for i,dict in enumerate(domain['disks_info']): - #~ print(dict) for key in dict.keys(): if 'size' in key: domain['disks_info'][i][key]=self.human_size(domain['disks_info'][i][key]) else: - # This is not used and will do nothing as we should implement a recursive function to look for all the nested 'size' fields + ''' This is not used and will do nothing as we should implement a recursive function to look for all the nested 'size' fields ''' if human_size: domain['hardware']['memory']=self.human_size(domain['hardware']['memory'] * 1000) if 'disks_info' in domain: - #~ import pprint - #~ pprint.pprint(domain['disks_info']) for i,dict in enumerate(domain['disks_info']): for key in dict.keys(): if 'size' in key: @@ -284,8 +252,6 @@ def get_domain(self, id, human_size=False, flatten=True): exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] log.error(exc_type, fname, exc_tb.tb_lineno) - log.error('DomainsStatusThread error:'+str(e)) - log.error('get_domain: '+str(e)) return domain def get_domain_media(self,id): @@ -298,7 +264,7 @@ def get_domain_media(self,id): iso=r.table('media').get(m['id']).pluck('id','name').run(db.conn) media['isos'].append(iso) except: - # Media does not exist + ''' Media does not exist ''' None if 'floppies' in domain_cd and domain_cd['floppies'] is not []: for m in domain_cd['floppies']: @@ -306,12 +272,12 @@ def get_domain_media(self,id): fd=r.table('media').get(m['id']).pluck('id','name').run(db.conn) media['floppies'].append(fd) except: - # media does not exist + ''' Media does not exist ''' None return media def user_hardware_quota(self, user, human_size=False, flatten=True): - #~ Should verify something??? + ''' Should verify something??? ''' with app.app_context(): domain = r.table('users').get(user).run(db.conn) try: @@ -324,7 +290,7 @@ def user_hardware_quota(self, user, human_size=False, flatten=True): if 'size' in key: domain['disks_info'][i][key]=self.human_size(domain['disks_info'][i][key]) else: - # This is not used and will do nothing as we should implement a recursive function to look for all the nested 'size' fields + ''' This is not used and will do nothing as we should implement a recursive function to look for all the nested 'size' fields ''' if human_size: domain['hardware']['memory']=self.human_size(domain['hardware']['memory'] * 1000) for i,dict in enumerate(domain['disks_info']): @@ -332,9 +298,9 @@ def user_hardware_quota(self, user, human_size=False, flatten=True): if 'size' in key: domain['disks_info'][i][key]=self.human_size(domain['disks_info'][i][key]) except Exception as e: - log.error('get_domain: '+str(e)) - #~ import pprint - #~ pprint.pprint(domain) + exc_type, exc_obj, exc_tb = sys.exc_info() + fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] + log.error(exc_type, fname, exc_tb.tb_lineno) return domain def get_backing_ids(self,id): @@ -347,8 +313,9 @@ def get_backing_ids(self,id): try: idchain.append(list(r.table("domains").filter(lambda disks: disks['hardware']['disks'][0]['file']==f).pluck('id','name').run(db.conn))[0]) except Exception as e: - log.error('get_backing_ids:'+str(e)) - #~ print(e) + exc_type, exc_obj, exc_tb = sys.exc_info() + fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] + log.error(exc_type, fname, exc_tb.tb_lineno) break return idchain From 0788d395a023a7ad579ea43173e52be1335ea1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 20:13:07 +0100 Subject: [PATCH 41/92] Fixed tabs --- src/webapp/lib/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/webapp/lib/api.py b/src/webapp/lib/api.py index 018ba3359..4ad861223 100644 --- a/src/webapp/lib/api.py +++ b/src/webapp/lib/api.py @@ -183,7 +183,7 @@ def get_media_installs(self): data=r.table('virt_install').run(db.conn) return self.f.table_values_bstrap(data) ''' - STATUS + STATUS ''' def get_domain_last_messages(self, id): with app.app_context(): @@ -193,9 +193,9 @@ def get_domain_last_events(self, id): with app.app_context(): return r.table('hypervisors_events').get_all(id, index='domain').order_by(r.desc('when')).limit(10).run(db.conn) - ''' - USER - ''' + ''' + USER + ''' def get_user(self, user): with app.app_context(): user=self.f.flatten_dict(r.table('users').get(user).run(db.conn)) From 74e01ef77b99515eaa094d22cfd2ce61dae847af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 20:25:49 +0100 Subject: [PATCH 42/92] Fixed var name error --- src/webapp/lib/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/webapp/lib/api.py b/src/webapp/lib/api.py index 4ad861223..ad7ff28c9 100644 --- a/src/webapp/lib/api.py +++ b/src/webapp/lib/api.py @@ -53,7 +53,7 @@ def update_table_status(self,user,table,data,remote_addr): try: if data['name']=='status': if data['value']=='DownloadAborting': - if dom['status'] in ['Downloading']: + if data['status'] in ['Downloading']: if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): return json.dumps({'title':item+' aborting success','text':item+' '+data['name']+' will be aborted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: @@ -61,7 +61,7 @@ def update_table_status(self,user,table,data,remote_addr): else: return json.dumps({'title':item+' aborting error','text':item+' '+data['name']+' can\'t be aborted while not Downloading','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Stopping': - if dom['status'] in ['Downloading']: + if data['status'] in ['Downloading']: if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): return json.dumps({'title':item+' stopping success','text':item+' '+data['name']+' will be stopped','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: @@ -69,7 +69,7 @@ def update_table_status(self,user,table,data,remote_addr): else: return json.dumps({'title':item+' stopping error','text':item+' '+data['name']+' can\'t be stopped while not Started','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Deleting': - if dom['status'] in ['Stopped','Failed']: + if data['status'] in ['Stopped','Failed']: if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): return json.dumps({'title':item+' deleting success','text':item+' '+data['name']+' will be deleted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: @@ -77,9 +77,9 @@ def update_table_status(self,user,table,data,remote_addr): else: return json.dumps({'title':item+' deleting error','text':item+' '+data['name']+' can\'t be deleted while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Starting': - if dom['status'] in ['Stopped','Failed']: + if data['status'] in ['Stopped','Failed']: if float(app.isardapi.get_user_quotas(current_user.username)['rqp']) >= 100: - return json.dumps({'title':'Quota exceeded','text':item+' '+dom['name']+' can\'t be started because you have exceeded quota','icon':'warning','type':'warning'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':'Quota exceeded','text':item+' '+data['name']+' can\'t be started because you have exceeded quota','icon':'warning','type':'warning'}), 500, {'ContentType':'application/json'} self.auto_interface_set(user,data['pk'],remote_addr) if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): return json.dumps({'title':item+' starting success','text':item+' '+data['pk']+' will be started','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} From 4c1c8124afbe09aa2b56473e2a49bf5544a32ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 20:40:38 +0100 Subject: [PATCH 43/92] Fixed domain get data before change status --- src/webapp/lib/api.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/webapp/lib/api.py b/src/webapp/lib/api.py index ad7ff28c9..ebe3c6764 100644 --- a/src/webapp/lib/api.py +++ b/src/webapp/lib/api.py @@ -48,45 +48,45 @@ def check(self,dict,action): def update_table_status(self,user,table,data,remote_addr): item = table[:-1].capitalize() - # ~ with app.app_context(): - # ~ dom = r.table('domains').get('pk').pluck('status','name') + with app.app_context(): + dom = r.table('domains').get(data['pk']).pluck('status','name') try: if data['name']=='status': if data['value']=='DownloadAborting': - if data['status'] in ['Downloading']: + if dom['status'] in ['Downloading']: if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' aborting success','text':item+' '+data['name']+' will be aborted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + return json.dumps({'title':item+' aborting success','text':item+' '+dom['name']+' will be aborted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' aborting error','text':item+' '+data['name']+' can\'t be aborted. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' aborting error','text':item+' '+dom['name']+' can\'t be aborted. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' aborting error','text':item+' '+data['name']+' can\'t be aborted while not Downloading','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' aborting error','text':item+' '+dom['name']+' can\'t be aborted while not Downloading','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Stopping': - if data['status'] in ['Downloading']: + if dom['status'] in ['Downloading']: if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' stopping success','text':item+' '+data['name']+' will be stopped','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + return json.dumps({'title':item+' stopping success','text':item+' '+dom['name']+' will be stopped','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' stopping error','text':item+' '+data['name']+' can\'t be stopped. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' stopping error','text':item+' '+dom['name']+' can\'t be stopped. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' stopping error','text':item+' '+data['name']+' can\'t be stopped while not Started','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' stopping error','text':item+' '+dom['name']+' can\'t be stopped while not Started','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Deleting': - if data['status'] in ['Stopped','Failed']: + if dom['status'] in ['Stopped','Failed']: if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' deleting success','text':item+' '+data['name']+' will be deleted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + return json.dumps({'title':item+' deleting success','text':item+' '+dom['name']+' will be deleted','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' deleting error','text':item+' '+data['name']+' can\'t be deleted. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' deleting error','text':item+' '+dom['name']+' can\'t be deleted. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' deleting error','text':item+' '+data['name']+' can\'t be deleted while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' deleting error','text':item+' '+dom['name']+' can\'t be deleted while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Starting': - if data['status'] in ['Stopped','Failed']: + if dom['status'] in ['Stopped','Failed']: if float(app.isardapi.get_user_quotas(current_user.username)['rqp']) >= 100: - return json.dumps({'title':'Quota exceeded','text':item+' '+data['name']+' can\'t be started because you have exceeded quota','icon':'warning','type':'warning'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':'Quota exceeded','text':item+' '+dom['name']+' can\'t be started because you have exceeded quota','icon':'warning','type':'warning'}), 500, {'ContentType':'application/json'} self.auto_interface_set(user,data['pk'],remote_addr) if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): - return json.dumps({'title':item+' starting success','text':item+' '+data['pk']+' will be started','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} + return json.dumps({'title':item+' starting success','text':item+' '+dom['name']+' will be started','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' starting error','text':item+' '+data['pk']+' can\'t be started. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' starting error','text':item+' '+dom['name']+' can\'t be started. Something went wrong!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} else: - return json.dumps({'title':item+' starting error','text':item+' '+data['name']+' can\'t be started while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + return json.dumps({'title':item+' starting error','text':item+' '+dom['name']+' can\'t be started while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} return json.dumps({'title':'Method not allowed','text':'That action is not allowed!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} except Exception as e: log.error('Error updating status for '+data['pk']+': '+str(e)) From eef14e2e53d1a927436c9d9586de675f3aa62e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 12 Jan 2019 20:47:13 +0100 Subject: [PATCH 44/92] Added missing rethink run --- src/webapp/lib/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webapp/lib/api.py b/src/webapp/lib/api.py index ebe3c6764..ddcf804ff 100644 --- a/src/webapp/lib/api.py +++ b/src/webapp/lib/api.py @@ -49,7 +49,7 @@ def check(self,dict,action): def update_table_status(self,user,table,data,remote_addr): item = table[:-1].capitalize() with app.app_context(): - dom = r.table('domains').get(data['pk']).pluck('status','name') + dom = r.table('domains').get(data['pk']).pluck('status','name').run(db.conn) try: if data['name']=='status': if data['value']=='DownloadAborting': @@ -89,8 +89,8 @@ def update_table_status(self,user,table,data,remote_addr): return json.dumps({'title':item+' starting error','text':item+' '+dom['name']+' can\'t be started while not Stopped or Failed','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} return json.dumps({'title':'Method not allowed','text':'That action is not allowed!','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} except Exception as e: - log.error('Error updating status for '+data['pk']+': '+str(e)) - return json.dumps({'title':item+' starting error','text':item+' '+data['pk']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} + log.error('Error updating status for '+dom['name']+': '+str(e)) + return json.dumps({'title':item+' starting error','text':item+' '+dom['name']+' can\'t be started now','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} def auto_interface_set(self,user,id, remote_addr): with app.app_context(): From 187bae4adf0d4123dfd1116b4770de8b49db903d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sun, 13 Jan 2019 10:53:55 +0100 Subject: [PATCH 45/92] Fixed stopping status --- src/webapp/lib/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webapp/lib/api.py b/src/webapp/lib/api.py index ddcf804ff..e7a02feea 100644 --- a/src/webapp/lib/api.py +++ b/src/webapp/lib/api.py @@ -61,7 +61,7 @@ def update_table_status(self,user,table,data,remote_addr): else: return json.dumps({'title':item+' aborting error','text':item+' '+dom['name']+' can\'t be aborted while not Downloading','icon':'warning','type':'error'}), 500, {'ContentType':'application/json'} if data['value']=='Stopping': - if dom['status'] in ['Downloading']: + if dom['status'] in ['Started']: if app.isardapi.update_table_value(table, data['pk'], data['name'], data['value']): return json.dumps({'title':item+' stopping success','text':item+' '+dom['name']+' will be stopped','icon':'success','type':'info'}), 200, {'ContentType':'application/json'} else: From d1b5237398aa1aee65acf88fbf0957c37134a6f7 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 14 Jan 2019 15:00:05 +0100 Subject: [PATCH 46/92] Domain can be updated --- src/webapp/lib/isardSocketio.py | 21 +++++++++++++++++++ .../static/admin/js/hypervisors_pools.js | 13 +++++++++++- .../admin/pages/hypervisors_modals.html | 6 +++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/webapp/lib/isardSocketio.py b/src/webapp/lib/isardSocketio.py index 9fa650685..9629fa703 100644 --- a/src/webapp/lib/isardSocketio.py +++ b/src/webapp/lib/isardSocketio.py @@ -544,6 +544,27 @@ def socketio_hyper_domains_stop(data): info, namespace='/sio_admins', room='hyper') + +@socketio.on('hyperpool_edit', namespace='/sio_admins') +def socketio_hyperpool_edit(form_data): + if current_user.role == 'admin': + data=app.isardapi.f.unflatten_dict(form_data) + res=app.adminapi.update_table_dict('hypervisors_pools','default',{'viewer':{'domain':data['viewer']['domain']}}) + + if res is True: + info=json.dumps({'result':True,'title':'Edit hypervisor pool','text':'Hypervisor pool '+'default'+' has been edited.','icon':'success','type':'success'}) + else: + info=json.dumps({'result':False,'title':'Edit hypervisor pool','text':'Hypervisor pool'+'default'+' can\'t be edited now.','icon':'warning','type':'error'}) + socketio.emit('add_form_result', + info, + namespace='/sio_admins', + room='hyper') + else: + info=json.dumps({'result':False,'title':'Hypervisor pool edit error','text':'Hypervisor pool should have at least one capability!','icon':'warning','type':'error'}) + socketio.emit('result', + info, + namespace='/sio_admins', + room='hyper') ''' USERS diff --git a/src/webapp/static/admin/js/hypervisors_pools.js b/src/webapp/static/admin/js/hypervisors_pools.js index 7dec9d238..53d3a6f8f 100644 --- a/src/webapp/static/admin/js/hypervisors_pools.js +++ b/src/webapp/static/admin/js/hypervisors_pools.js @@ -221,7 +221,7 @@ $(document).ready(function() { $('.btn-viewer-pool-edit').on('click', function(){ pk=$(this).attr("data-pk"); //~ setHardwareDomainDefaults('#modalEditDesktop',pk); - $("#modalEditViewer #modalEdit")[0].reset(); + $("#modalEditViewer #modalEditViewerForm")[0].reset(); $('#modalEditViewer').modal({ backdrop: 'static', keyboard: false @@ -230,6 +230,17 @@ $(document).ready(function() { //~ $('#modalEdit').parsley(); //~ modal_edit_desktop_datatables(pk); }); + + $("#modalEditViewer #send").on('click', function(e){ + var form = $('#modalEditViewer #modalEditViewerForm'); + form.parsley().validate(); + if (form.parsley().isValid()){ + data=$('#modalEditViewer #modalEditViewerForm').serializeObject(); + console.log(data) + socket.emit('hyperpool_edit',data) + } + }); + } } ); diff --git a/src/webapp/templates/admin/pages/hypervisors_modals.html b/src/webapp/templates/admin/pages/hypervisors_modals.html index f0a29f134..b098ee2ac 100644 --- a/src/webapp/templates/admin/pages/hypervisors_modals.html +++ b/src/webapp/templates/admin/pages/hypervisors_modals.html @@ -641,7 +641,7 @@
--> -
@@ -406,7 +406,6 @@

Grafana

-
@@ -430,7 +429,7 @@

Grafana

---> +
From d4200660bf7481bf23fbf410891777b248227a8e Mon Sep 17 00:00:00 2001 From: root Date: Thu, 17 Jan 2019 12:11:32 +0100 Subject: [PATCH 53/92] Removed log --- src/webapp/static/admin/js/config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webapp/static/admin/js/config.js b/src/webapp/static/admin/js/config.js index b2268e96e..5f5e6ce27 100644 --- a/src/webapp/static/admin/js/config.js +++ b/src/webapp/static/admin/js/config.js @@ -14,7 +14,6 @@ $(document).ready(function() { if(value){$('#'+key).iCheck('check');} }else{ $('#'+key).val(value).prop('disabled',true); - console.log(key+' - '+value) } }); From 3df78269b42b434c77fc0619e877fc84d74e828d Mon Sep 17 00:00:00 2001 From: darta Date: Thu, 17 Jan 2019 16:11:20 -0600 Subject: [PATCH 54/92] fixed grafana in ui and database --- dockers/remote-grafana/README.md | 5 + dockers/remote-grafana/docker-compose.yml | 40 +++++ src/webapp/config/populate.py | 2 +- src/webapp/config/upgrade.py | 62 +++++++- src/webapp/templates/admin/pages/config.html | 155 ++++++++----------- src/webapp/wizard/WizardLib.py | 3 +- 6 files changed, 170 insertions(+), 97 deletions(-) create mode 100644 dockers/remote-grafana/README.md create mode 100644 dockers/remote-grafana/docker-compose.yml diff --git a/dockers/remote-grafana/README.md b/dockers/remote-grafana/README.md new file mode 100644 index 000000000..c775a13a0 --- /dev/null +++ b/dockers/remote-grafana/README.md @@ -0,0 +1,5 @@ +# IsardVDI Remote Grafana + +It brings up a remote grafana host. Instructions can be found at documentation: + +https://isardvdi.readthedocs.io/en/latest/admin/grafana/ diff --git a/dockers/remote-grafana/docker-compose.yml b/dockers/remote-grafana/docker-compose.yml new file mode 100644 index 000000000..08cd568e4 --- /dev/null +++ b/dockers/remote-grafana/docker-compose.yml @@ -0,0 +1,40 @@ +version: "3.2" +services: + isard-grafana: + volumes: + - type: bind + source: /opt/isard/grafana/grafana/data + target: /grafana/data + read_only: false + #~ - type: bind + #~ source: /opt/isard/grafana/graphite/storage + #~ target: /opt/graphite/storage + #~ read_only: false + #~ - type: bind + #~ source: /opt/isard/grafana/graphite/conf + #~ target: /opt/graphite/conf + #~ read_only: false + ports: + - target: 3000 + published: 3000 + protocol: tcp + mode: host + - target: 8080 + published: 8081 + protocol: tcp + mode: host + - target: 2003 + published: 2003 + protocol: tcp + mode: host + - target: 2004 + published: 2004 + protocol: tcp + mode: host + - target: 7002 + published: 7002 + protocol: tcp + mode: host + image: isard/grafana:1.1 + restart: always + diff --git a/src/webapp/config/populate.py b/src/webapp/config/populate.py index b0d360046..062be32e1 100644 --- a/src/webapp/config/populate.py +++ b/src/webapp/config/populate.py @@ -117,7 +117,7 @@ def config(self): 'timeout_between_retries_hyp_is_alive': 1, 'retries_hyp_is_alive': 3 }}, - 'grafana':{'active':False,'url':'http://isard-grafana','web_port':80,'carbon_port':2003,'graphite_port':3000}, + 'grafana':{'active':False,'url':'','host':'isard-grafana','web_port':80,'carbon_port':2004}, 'version':0, 'resources': {'code':False, 'url':'http://www.isardvdi.com:5050'} diff --git a/src/webapp/config/upgrade.py b/src/webapp/config/upgrade.py index ac4beb3c7..edbc529ba 100644 --- a/src/webapp/config/upgrade.py +++ b/src/webapp/config/upgrade.py @@ -17,7 +17,7 @@ ''' Update to new database release version when new code version release ''' -release_version = 5 +release_version = 6 tables=['config','hypervisors','hypervisors_pools','domains','media'] @@ -78,9 +78,6 @@ def config(self,version): table='config' d=r.table(table).get(1).run() log.info('UPGRADING '+table+' TABLE TO VERSION '+str(version)) - if version == 5: - d['engine']['log']['log_level'] = 'WARNING' - r.table(table).update(d).run() if version == 1: ''' CONVERSION FIELDS PRE CHECKS ''' @@ -126,10 +123,63 @@ def config(self,version): except Exception as e: log.error('Could not update table '+table+' remove fields for db version '+version+'!') log.error('Error detail: '+str(e)) - - return True + if version == 5: + d['engine']['log']['log_level'] = 'WARNING' + r.table(table).update(d).run() + if version == 6: + + ''' CONVERSION FIELDS PRE CHECKS ''' + try: + url=d['engine']['grafana']['url'] + except: + url="" + try: + if not self.check_done( d, + [], + ['engine']): + ##### CONVERSION FIELDS + url="" + d['engine']['grafana']={"active": False , + "carbon_port": 2004 , + "host": "isard-grafana", + "url": url, + "web_port": 80} + r.table(table).update(d).run() + except Exception as e: + log.error('Could not update table '+table+' conversion fields for db version '+version+'!') + log.error('Error detail: '+str(e)) + + # ~ ''' NEW FIELDS PRE CHECKS ''' + # ~ try: + # ~ if not self.check_done( d, + # ~ ['resources','voucher_access',['engine','api','token']], + # ~ []): + # ~ ##### NEW FIELDS + # ~ self.add_keys(table, [ + # ~ {'resources': { 'code':False, + # ~ 'url':'http://www.isardvdi.com:5050'}}, + # ~ {'voucher_access':{'active':False}}, + # ~ {'engine':{'api':{ "token": "fosdem", + # ~ "url": 'http://isard-engine', + # ~ "web_port": 5555}}}]) + # ~ except Exception as e: + # ~ log.error('Could not update table '+table+' new fields for db version '+version+'!') + # ~ log.error('Error detail: '+str(e)) + + ''' REMOVE FIELDS PRE CHECKS ''' + try: + if not self.check_done( d, + [], + ['grafana']): + #### REMOVE FIELDS + self.del_keys(table,['grafana']) + except Exception as e: + log.error('Could not update table '+table+' remove fields for db version '+version+'!') + log.error('Error detail: '+str(e)) + return True + ''' HYPERVISORS TABLE UPGRADES ''' diff --git a/src/webapp/templates/admin/pages/config.html b/src/webapp/templates/admin/pages/config.html index 51030d7d5..39e3ce71c 100644 --- a/src/webapp/templates/admin/pages/config.html +++ b/src/webapp/templates/admin/pages/config.html @@ -87,6 +87,72 @@

LDAP authentication

+ +
+
+
+

Grafana

+ +
+
+
+ + +
+
+

Grafana

+ +
+ +
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + + + +
+
+ +
+ +
-
-
-

STATISTICS

- -
-
- -
- - - -
-
-
-
-

Grafana

- -
-
-
- -
-
-
-

Grafana

- -
- -
-
-
- -
-
- -
- -
-
- -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- - - -
-
-
- -
diff --git a/src/webapp/wizard/WizardLib.py b/src/webapp/wizard/WizardLib.py index dc6cb56f5..59292ec74 100644 --- a/src/webapp/wizard/WizardLib.py +++ b/src/webapp/wizard/WizardLib.py @@ -388,7 +388,8 @@ def valid_hypervisor(self,remote_addr=False): def update_hypervisor_viewer(self,remote_addr): try: - if r.table('hypervisors').update({'viewer_hostname':remote_addr}).run() is not None: + r.table('config').get(1).update({'engine':{'grafana':{'url':'http://'+str(remote_addr)}}}).run() + if r.table('hypervisors').get('isard-hypervisor').update({'viewer_hostname':remote_addr}).run() is not None: return True return False except: From 326fa214efef14422a9dfb67d4af05c10a86746a Mon Sep 17 00:00:00 2001 From: root Date: Thu, 17 Jan 2019 23:46:32 +0100 Subject: [PATCH 55/92] Added interval field --- src/webapp/config/populate.py | 2 +- src/webapp/config/upgrade.py | 7 +++---- src/webapp/templates/admin/pages/config.html | 15 +++++++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/webapp/config/populate.py b/src/webapp/config/populate.py index 062be32e1..b533471b4 100644 --- a/src/webapp/config/populate.py +++ b/src/webapp/config/populate.py @@ -117,7 +117,7 @@ def config(self): 'timeout_between_retries_hyp_is_alive': 1, 'retries_hyp_is_alive': 3 }}, - 'grafana':{'active':False,'url':'','host':'isard-grafana','web_port':80,'carbon_port':2004}, + 'grafana':{'active':False,'url':'','hostname':'isard-grafana','carbon_port':2004,"interval": 5}, 'version':0, 'resources': {'code':False, 'url':'http://www.isardvdi.com:5050'} diff --git a/src/webapp/config/upgrade.py b/src/webapp/config/upgrade.py index edbc529ba..607ac0f8b 100644 --- a/src/webapp/config/upgrade.py +++ b/src/webapp/config/upgrade.py @@ -140,12 +140,11 @@ def config(self,version): [], ['engine']): ##### CONVERSION FIELDS - url="" d['engine']['grafana']={"active": False , "carbon_port": 2004 , - "host": "isard-grafana", - "url": url, - "web_port": 80} + "interval": 5, + "hostname": "isard-grafana", + "url": url} r.table(table).update(d).run() except Exception as e: log.error('Could not update table '+table+' conversion fields for db version '+version+'!') diff --git a/src/webapp/templates/admin/pages/config.html b/src/webapp/templates/admin/pages/config.html index 39e3ce71c..c62ddaba2 100644 --- a/src/webapp/templates/admin/pages/config.html +++ b/src/webapp/templates/admin/pages/config.html @@ -115,18 +115,18 @@

Grafana

-
-
@@ -136,6 +136,13 @@

Grafana

+
+ +
+ +
+
From 0c563bee16d931761a7bddb6610c7324bbc85519 Mon Sep 17 00:00:00 2001 From: beto Date: Fri, 18 Jan 2019 01:53:51 +0100 Subject: [PATCH 56/92] new thread grafana in engine running --- build-docker-images.sh | 2 +- src/engine/models/manager_hypervisors.py | 6 + src/engine/services/lib/functions.py | 11 ++ src/engine/services/lib/grafana.py | 73 +++++++++++++ src/engine/services/threads/grafana_thread.py | 103 ++++++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/engine/services/lib/grafana.py create mode 100644 src/engine/services/threads/grafana_thread.py diff --git a/build-docker-images.sh b/build-docker-images.sh index c67af32ba..132f521f7 100755 --- a/build-docker-images.sh +++ b/build-docker-images.sh @@ -33,7 +33,7 @@ fi # Array containing all the images to build images=( #alpine-pandas - #grafana + grafana nginx hypervisor app diff --git a/src/engine/models/manager_hypervisors.py b/src/engine/models/manager_hypervisors.py index 92815c583..e39211265 100644 --- a/src/engine/models/manager_hypervisors.py +++ b/src/engine/models/manager_hypervisors.py @@ -34,6 +34,7 @@ launch_disk_operations_thread, \ launch_long_operations_thread from engine.services.lib.functions import clean_intermediate_status +from engine.services.threads.grafana_thread import GrafanaThread,launch_grafana_thread WAIT_HYP_ONLINE = 2.0 @@ -72,6 +73,7 @@ def __init__(self, launch_threads=True, with_status_threads=True, self.t_broom = None self.t_background = None self.t_downloads_changes = None + self.t_grafana = None self.quit = False self.threads_info_main = {} @@ -355,6 +357,10 @@ def run(self): logs.main.debug('launching hypervisor events thread') self.manager.t_events = launch_thread_hyps_event() + #launch grafana thread + logs.main.debug('launching grafana thread') + self.manager.t_grafana = launch_grafana_thread(self.manager.t_status) + logs.main.info('THREADS LAUNCHED FROM BACKGROUND THREAD') update_table_field('engine', 'engine', 'status_all_threads', 'Starting') diff --git a/src/engine/services/lib/functions.py b/src/engine/services/lib/functions.py index 3ca9f59e7..70293d7e9 100644 --- a/src/engine/services/lib/functions.py +++ b/src/engine/services/lib/functions.py @@ -1075,3 +1075,14 @@ def clean_intermediate_status(): [update_domain_status('Failed', d['id'], detail='change status from {} when isard engine restart'.format(d['status'])) for d in all_domains if d['status'] in status_to_failed] + + +def flatten_dict(d): + def items(): + for key, value in list(d.items()): + if isinstance(value, dict): + for subkey, subvalue in list(flatten_dict(value).items()): + yield key + "." + subkey, subvalue + else: + yield key, value + return dict(items()) \ No newline at end of file diff --git a/src/engine/services/lib/grafana.py b/src/engine/services/lib/grafana.py new file mode 100644 index 000000000..293ea17f1 --- /dev/null +++ b/src/engine/services/lib/grafana.py @@ -0,0 +1,73 @@ +import socket +import pickle +import struct +import time + +from engine.services.log import * +from engine.services.lib.functions import flatten_dict + +TIMEOUT_SOCKET_CONNECTION = 30 + + +def send_dict_to_grafana(d,host,port=2004,prefix='isard'): + sender = create_socket_grafana(host=host,port=port) + if sender is not False: + flatten_and_send_dict(d, sender, prefix=prefix) + sender.close() + return True + else: + return False + + +def create_socket_grafana(host,port=2004): + s = socket.socket() + s.settimeout(TIMEOUT_SOCKET_CONNECTION) + + try: + s.connect((host, port)) + return s + + except socket.error as e: + log.error(e) + log.error(f'Failed connection to grafana server: {host} in port {port}') + try: + ip = socket.gethostbyname(host) + except socket.error as e: + log.error(e) + log.error('not resolves ip from hostname of grafana server: {}'.format(host)) + return False + return False + + +def flatten_and_send_dict(d,sender,prefix='isard'): + type_ok = (int,float) + try: + now = int(time.time()) + tuples = ([]) + lines = [] + # We're gonna report all three loadavg values + d_flat = flatten_dict(d) + for k,v in d_flat.items(): + k = prefix + '.' + k + + #check if type is ok + if type(v) is bool: + v = 1 if v is True else 0 + + if type(v) in type_ok: + tuples.append((k, (now, v))) + lines.append(f'({now}) {k}: {v}') + + if type(v) is str: + tuples.append((k + '.' + v, (now, 1))) + lines.append(f'({now}) {k}.{v}: 0') + + message = '\n'.join(lines) + '\n' # all lines must end in a newline + logs.main.debug('sending to grafana:') + logs.main.debug(message) + package = pickle.dumps(tuples, 1) + size = struct.pack('!L', len(package)) + sender.sendall(size) + sender.sendall(package) + except Exception as e: + log.error(f'Exception when send dictionary of values to grafana: {e}') \ No newline at end of file diff --git a/src/engine/services/threads/grafana_thread.py b/src/engine/services/threads/grafana_thread.py new file mode 100644 index 000000000..7f543e5b6 --- /dev/null +++ b/src/engine/services/threads/grafana_thread.py @@ -0,0 +1,103 @@ +# Copyright 2019 the Isard-vdi project authors: +# Alberto Larraz Dalmases +# Josep Maria Viñolas Auquer +# License: AGPLv3 +# coding=utf-8 +import threading +from time import sleep + +from engine.services.log import logs +from engine.services.lib.functions import get_tid, flatten_dict +from engine.services.db import get_hyp_hostnames_online +from engine.services.lib.grafana import send_dict_to_grafana + +SEND_TO_GRAFANA_INTERVAL = 5 +SEND_STATIC_VALUES_INTERVAL = 30 + +def launch_grafana_thread(d_threads_status): + t = GrafanaThread(name='grafana', + d_threads_status=d_threads_status) + t.daemon = True + t.start() + return t + +class GrafanaThread(threading.Thread): + def __init__(self, name,d_threads_status): + threading.Thread.__init__(self) + self.name = name + self.stop = False + self.t_status = d_threads_status + + + def get_hostname_grafana(self): + dict_grafana = { + "active": True, + "carbon_port": 2004, + "hostname": "isard-grafana", + "interval": 5, + } + if dict_grafana["active"] is not True: + return False + else: + self.host_grafana = dict_grafana["hostname"] + self.port = dict_grafana["carbon_port"] + self.interval = dict_grafana["interval"] + return True + + def send(self,d): + send_dict_to_grafana(d, self.host_grafana, self.port) + + def run(self): + self.tid = get_tid() + logs.main.info('starting thread: {} (TID {})'.format(self.name, self.tid)) + + #get hostname grafana config + if self.get_hostname_grafana() is not True: + return False + + hyps_online = [] + + elapsed = SEND_STATIC_VALUES_INTERVAL + while self.stop is False: + sleep(SEND_TO_GRAFANA_INTERVAL) + elapsed += SEND_TO_GRAFANA_INTERVAL + + + for i,id_hyp in enumerate(self.t_status.keys()): + try: + if self.t_status[id_hyp].status_obj.hyp_obj.connected is True: + if id_hyp not in hyps_online: + hyps_online.append(id_hyp) + except: + logs.main.error(f'hypervisor {id_hyp} problem checking if is connected') + + #send static values of hypervisors + if elapsed >= SEND_STATIC_VALUES_INTERVAL: + d_hyps_info = dict() + for i, id_hyp in enumerate(hyps_online): + d_hyps_info[f'hyp-info-{i}'] = self.t_status[id_hyp].status_obj.hyp_obj.info + + self.send(d_hyps_info) + elapsed = 0 + + #send stats + dict_to_send = dict() + j=0 + for i, id_hyp in enumerate(hyps_online): + if id_hyp in self.t_status.keys(): + #stats_hyp = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp + stats_hyp_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp_now + #stats_domains = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains + if len(stats_hyp_now) > 0: + dict_to_send[f'hyp-stats-{i}'] = {'hyp-id':{id_hyp:1},'last': stats_hyp_now} + stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now + if len(stats_hyp_now) > 0: + for id_domain,d_stats in stats_domains_now.items(): + dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} + j+=1 + if len(dict_to_send) > 0: + self.send(dict_to_send) + + + + From 6d11189a52327eef59ad245a8f5c24ab30b11687 Mon Sep 17 00:00:00 2001 From: darta Date: Fri, 18 Jan 2019 20:42:05 +0100 Subject: [PATCH 57/92] config updated --- src/engine/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/config.py b/src/engine/config.py index d2f487f29..09de37e0c 100644 --- a/src/engine/config.py +++ b/src/engine/config.py @@ -53,7 +53,7 @@ try: with r.connect(host=RETHINK_HOST, port=RETHINK_PORT) as conn: rconfig = r.db(RETHINK_DB).table('config').get(1).run(conn) - grafana= rconfig['grafana'] + grafana= rconfig['engine']['grafana'] rconfig = rconfig['engine'] table_exists=True if fail_first_loop: From 26c7401407c94f72dc222d8737827f15aefe0056 Mon Sep 17 00:00:00 2001 From: darta Date: Sat, 19 Jan 2019 00:23:49 +0100 Subject: [PATCH 58/92] Fixed engine error, changed keys and updated grafana template --- dockers/grafana/data.v1/grafana.db | Bin 0 -> 409600 bytes dockers/grafana/data.v1/log/.gitkeep | 0 dockers/grafana/data.v1/plugins/.gitkeep | 0 dockers/grafana/data.v1/png/.gitkeep | 0 dockers/grafana/data.v1/sessions/.gitkeep | 0 dockers/grafana/data/grafana.db | Bin 409600 -> 454656 bytes src/engine/services/threads/grafana_thread.py | 14 ++++++++------ 7 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 dockers/grafana/data.v1/grafana.db create mode 100644 dockers/grafana/data.v1/log/.gitkeep create mode 100644 dockers/grafana/data.v1/plugins/.gitkeep create mode 100644 dockers/grafana/data.v1/png/.gitkeep create mode 100644 dockers/grafana/data.v1/sessions/.gitkeep diff --git a/dockers/grafana/data.v1/grafana.db b/dockers/grafana/data.v1/grafana.db new file mode 100644 index 0000000000000000000000000000000000000000..9be0c64e85e650988966347508daf3096d09cf7b GIT binary patch literal 409600 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

buYhIbQ$=P!rN>1WV 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 A>Hq)$ delta 984 zcmcgqT}V?=96#sYv$K1q*RvUG_%Sx`LqSY-w>ixlVX$5#5GCIt=5hB zFMY5Kj&F?+LZmPk-o!v3>ahYJdZ-s4f&z(#jSLmnaViPXTl8`c{C@w>-~X3cuE}Jo zMr#a90D$m4XA4|<)-~a%EvHR@4%1KcJ)Ngh^dW7cUuiczL)++3&Z63=&1w|gFdRzt zoQsE&;ozl6G8K*Y31e;U!v}dSne1`Ka?wgs=}>ZP*nJ9>i=rd9KJ)2<2WF8`*?u(e1u4{BuS2NBsJ0rD*@!Vc^@utB9@LPQYUx+ zYi#fbT47HMsFgWy!`2EO(4CAX(tJ%tQ&|5Ce51O-rCe4tze~BIXsS!O%HIK(5)CVw zUOk7WAe+7mHD+1Bc9w`?iFM55A(ky713b(8Z*YJuzQrGnyDi!DJACBOjxIjU>-Vaj z=6cmr@7aSI8rwSi>-*vz7y4&MA$C6IqL|StpsRe{FOP;i?{qxg<$YU8b~ zprCIBMXc&54>wyLWy}+T`DR|cKaC_o%mVUC%<|F?L^M(-l<5=S(P{V}m8?@nnqe6r zl064G#m-U`_jpHZ%Wb5hk8?y?HjU-Pw)ZuvVe~Lb?Tbt()~T& ze`f$kg=fITjV;hMKsV_cS8#<|kw~B*iiGS?1#W*@$XS-Zm8?&;t)D2mp9@gcVa8Bg F_yu*@BsBm4 diff --git a/src/engine/services/threads/grafana_thread.py b/src/engine/services/threads/grafana_thread.py index 7f543e5b6..a17e144ba 100644 --- a/src/engine/services/threads/grafana_thread.py +++ b/src/engine/services/threads/grafana_thread.py @@ -76,8 +76,7 @@ def run(self): d_hyps_info = dict() for i, id_hyp in enumerate(hyps_online): d_hyps_info[f'hyp-info-{i}'] = self.t_status[id_hyp].status_obj.hyp_obj.info - - self.send(d_hyps_info) + # ~ self.send(d_hyps_info) elapsed = 0 #send stats @@ -89,12 +88,15 @@ def run(self): stats_hyp_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp_now #stats_domains = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains if len(stats_hyp_now) > 0: - dict_to_send[f'hyp-stats-{i}'] = {'hyp-id':{id_hyp:1},'last': stats_hyp_now} + dict_to_send[f'hypers.'+id_hyp] = {'stats':stats_hyp_now,'info':d_hyps_info['hyp-info-'+str(i)],'domains':{}} stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now + # ~ for id_domain,d_stats in stats_domains_now.items(): if len(stats_hyp_now) > 0: - for id_domain,d_stats in stats_domains_now.items(): - dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} - j+=1 + # ~ for id_domain,d_stats in stats_domains_now.items(): + # ~ dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} + dict_to_send[f'hypers.'+id_hyp]['domains']={x:0 for x in stats_domains_now} + # ~ j+=1 + if len(dict_to_send) > 0: self.send(dict_to_send) From a90085b07dd8265ea846f0b90b1884739b139de1 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Jan 2019 00:18:06 +0100 Subject: [PATCH 59/92] It works and can be restarted --- docker-compose-grafana.yml | 24 +++++++++--------- dockers/app/Dockerfile | 9 +++---- dockers/grafana/Dockerfile | 1 - dockers/grafana/data.v1/grafana.db | Bin 409600 -> 0 bytes dockers/grafana/data.v1/log/.gitkeep | 0 dockers/grafana/data.v1/plugins/.gitkeep | 0 dockers/grafana/data.v1/png/.gitkeep | 0 dockers/grafana/data.v1/sessions/.gitkeep | 0 dockers/grafana/data/grafana.db | Bin 454656 -> 528384 bytes dockers/grafana/run.sh | 2 +- grafana.yml | 21 +++++++++++++++ src/engine/services/threads/grafana_thread.py | 7 ++--- 12 files changed, 42 insertions(+), 22 deletions(-) delete mode 100644 dockers/grafana/data.v1/grafana.db delete mode 100644 dockers/grafana/data.v1/log/.gitkeep delete mode 100644 dockers/grafana/data.v1/plugins/.gitkeep delete mode 100644 dockers/grafana/data.v1/png/.gitkeep delete mode 100644 dockers/grafana/data.v1/sessions/.gitkeep create mode 100644 grafana.yml diff --git a/docker-compose-grafana.yml b/docker-compose-grafana.yml index 04e0aef42..87ec82250 100644 --- a/docker-compose-grafana.yml +++ b/docker-compose-grafana.yml @@ -101,14 +101,14 @@ services: source: /opt/isard/grafana/grafana/data target: /grafana/data read_only: false - #~ - type: bind - #~ source: /opt/isard/grafana/graphite/storage - #~ target: /opt/graphite/storage - #~ read_only: false - #~ - type: bind - #~ source: /opt/isard/grafana/graphite/conf - #~ target: /opt/graphite/conf - #~ read_only: false + - type: bind + source: /opt/isard/grafana/graphite/storage + target: /opt/graphite/storage + read_only: false + - type: bind + source: /opt/isard/grafana/graphite/conf + target: /opt/graphite/conf + read_only: false ports: - target: 3000 published: 3000 @@ -118,13 +118,13 @@ services: - isard_network image: isard/grafana:1.1 restart: always -# depends_on: -# - isard-database -# - isard-nginx - + #~ depends_on: + #~ - isard-app + volumes: sshkeys: networks: isard_network: external: false + diff --git a/dockers/app/Dockerfile b/dockers/app/Dockerfile index 521b3a450..08158fb17 100644 --- a/dockers/app/Dockerfile +++ b/dockers/app/Dockerfile @@ -3,14 +3,9 @@ MAINTAINER isard RUN apk add --no-cache bash yarn py3-libvirt py3-paramiko py3-lxml py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-netaddr py3-requests curl openssh-client -RUN mkdir /isard -ADD ./src /isard - COPY dockers/app/requirements.pip3 /requirements.pip3 RUN pip3 install --no-cache-dir -r requirements.pip3 -RUN mv /isard/isard.conf.docker /isard/isard.conf - RUN mkdir -p /root/.ssh RUN echo "Host isard-hypervisor \ StrictHostKeyChecking no" >/root/.ssh/config @@ -25,4 +20,8 @@ EXPOSE 5000 COPY dockers/app/certs.sh / COPY dockers/app/add-hypervisor.sh / +RUN mkdir /isard +ADD ./src /isard +RUN mv /isard/isard.conf.docker /isard/isard.conf + CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/dockers/grafana/Dockerfile b/dockers/grafana/Dockerfile index e02106608..60317a61e 100644 --- a/dockers/grafana/Dockerfile +++ b/dockers/grafana/Dockerfile @@ -54,7 +54,6 @@ RUN set -ex \ ADD dockers/grafana/grafana-defaults.ini /grafana/conf/defaults.ini - EXPOSE 8080 EXPOSE 3000 EXPOSE 2003 diff --git a/dockers/grafana/data.v1/grafana.db b/dockers/grafana/data.v1/grafana.db deleted file mode 100644 index 9be0c64e85e650988966347508daf3096d09cf7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 409600 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

buYhIbQ$=P!rN>1WV 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 DueK`r delta 541 zcmYk3&ubGw7>4KDot^B)Y`!FGbkVlE7cmFRZW^^V2l3#=ORUgC(L)lgqS$1m1%-Oq zAhd^ih(?&BL5deIwE^eg$-h9o2;NG6P*Jolt)jN5VjXTU_8;5@Ej4xi%_e2ByN5ij8dEMWufPTK#PY}wv`tz=`W2NTul zs~3an)I|B_<*DgwL7lcr-eCpbv~GD7laQqGkLav<89#o)9V!&_M~iuXsX~_p_2C2 zLO3G&ztBUlf9t$r5s<|CmH!AuNF)cMvB7TZ+@A-JO4apeO`vXXk)|<(1D#2AL zdyA}3GxQPQjS%*UUoSyRF~EpmB&QoV7j1%wHWe!r^fT-__ZQh5YpQ}Z>CbW;a7R8y z0^E`C164fQqWJ_bkF#7_#&z|NO2KW}8n-3!4sA^ZP-yr5JC&;MUD>*nIhu$ZmI+~! zr%$s%Q5j`zVG^^4I{Y!UfuC)Q{6 WJ|kZoI$j!^8LS8AYBP^tG50sXAC$QO diff --git a/dockers/grafana/run.sh b/dockers/grafana/run.sh index 0fa67d83f..645dd455c 100644 --- a/dockers/grafana/run.sh +++ b/dockers/grafana/run.sh @@ -1,7 +1,7 @@ #!/bin/bash +mkdir -p /opt/graphite/storage/{ceres,lists,log/webapp,rrd,whisper} cd /opt/graphite - if [ ! -f /opt/graphite/conf/local_settings.py ]; then echo "Creating default config for graphite-web..." cp /opt/graphite/conf.example/local_settings.py /opt/graphite/conf/local_settings.py diff --git a/grafana.yml b/grafana.yml new file mode 100644 index 000000000..8f080718e --- /dev/null +++ b/grafana.yml @@ -0,0 +1,21 @@ +version: "3.2" +services: + isard-grafana: + volumes: + - type: bind + source: /opt/isard/grafana/grafana/data + target: /grafana/data + read_only: false + ports: + - target: 3000 + published: 3000 + protocol: tcp + mode: host + networks: + - isard_network + image: isard/grafana:1.1 + restart: always + +networks: + isard_network: + external: false diff --git a/src/engine/services/threads/grafana_thread.py b/src/engine/services/threads/grafana_thread.py index a17e144ba..1fe9e6b34 100644 --- a/src/engine/services/threads/grafana_thread.py +++ b/src/engine/services/threads/grafana_thread.py @@ -89,12 +89,13 @@ def run(self): #stats_domains = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains if len(stats_hyp_now) > 0: dict_to_send[f'hypers.'+id_hyp] = {'stats':stats_hyp_now,'info':d_hyps_info['hyp-info-'+str(i)],'domains':{}} - stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now + stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now # ~ for id_domain,d_stats in stats_domains_now.items(): - if len(stats_hyp_now) > 0: + # ~ if len(stats_hyp_now) > 0: # ~ for id_domain,d_stats in stats_domains_now.items(): # ~ dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} - dict_to_send[f'hypers.'+id_hyp]['domains']={x:0 for x in stats_domains_now} + dict_to_send[f'hypers.'+id_hyp]['domains']=stats_domains_now #{x:0 for x in stats_domains_now} + # ~ print(stats_domains_now) # ~ j+=1 if len(dict_to_send) > 0: From d0964c358cde26b341f824313f19b66bb17669bf Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Jan 2019 11:54:19 +0100 Subject: [PATCH 60/92] Modified logs and added container and network names in yml --- docker-compose-grafana.yml | 130 ------------------ docker-compose.yml | 9 +- dockers/grafana/etc/supervisor.d/carbon.ini | 4 +- dockers/grafana/etc/supervisor.d/grafana.ini | 10 +- dockers/grafana/etc/supervisor.d/gunicorn.ini | 4 +- dockers/grafana/etc/supervisor.d/nginx.ini | 4 +- dockers/grafana/etc/supervisord.conf | 10 +- extras/grafana/docker-compose.yml | 35 +++++ grafana.yml | 21 --- 9 files changed, 64 insertions(+), 163 deletions(-) delete mode 100644 docker-compose-grafana.yml create mode 100644 extras/grafana/docker-compose.yml delete mode 100644 grafana.yml diff --git a/docker-compose-grafana.yml b/docker-compose-grafana.yml deleted file mode 100644 index 87ec82250..000000000 --- a/docker-compose-grafana.yml +++ /dev/null @@ -1,130 +0,0 @@ -version: "3.2" -services: - isard-database: - volumes: - - type: bind - source: /opt/isard/database - target: /data - read_only: false - networks: - - isard_network - image: rethinkdb - restart: always - - isard-nginx: - volumes: - - type: bind - source: /opt/isard/certs/default - target: /etc/nginx/external - read_only: false - - type: bind - source: /opt/isard/logs/nginx - target: /var/log/nginx - read_only: false - ports: - - target: 80 - published: 80 - protocol: tcp - mode: host - - target: 443 - published: 443 - protocol: tcp - mode: host - networks: - - isard_network - image: isard/nginx:1.1 - restart: always - - isard-hypervisor: - volumes: - - type: volume - source: sshkeys - target: /root/.ssh - read_only: false - - type: bind - source: /opt/isard - target: /isard - read_only: false - - type: bind - source: /opt/isard/certs/default - target: /etc/pki/libvirt-spice - read_only: false - ports: - - "5900-5949:5900-5949" - - "55900-55949:55900-55949" - networks: - - isard_network - image: isard/hypervisor:1.1 - privileged: true - restart: always - - isard-app: - volumes: - - type: volume - source: sshkeys - target: /root/.ssh - read_only: false - - type: bind - source: /opt/isard/certs - target: /certs - read_only: false - - type: bind - source: /opt/isard/logs - target: /isard/logs - read_only: false - - type: bind - source: /opt/isard/database/wizard - target: /isard/install/wizard - read_only: false - - type: bind - source: /opt/isard/backups - target: /isard/backups - read_only: false - - type: bind - source: /opt/isard/uploads - target: /isard/uploads - read_only: false - extra_hosts: - - "isard-engine:127.0.0.1" - networks: - - isard_network - image: isard/app:1.1 - restart: always - depends_on: - - isard-database - - isard-hypervisor - - isard-nginx - - isard-grafana: - volumes: - - type: bind - source: /opt/isard/grafana/grafana/data - target: /grafana/data - read_only: false - - type: bind - source: /opt/isard/grafana/graphite/storage - target: /opt/graphite/storage - read_only: false - - type: bind - source: /opt/isard/grafana/graphite/conf - target: /opt/graphite/conf - read_only: false - ports: - - target: 3000 - published: 3000 - protocol: tcp - mode: host - networks: - - isard_network - image: isard/grafana:1.1 - restart: always - #~ depends_on: - #~ - isard-app - -volumes: - sshkeys: - -networks: - isard_network: - external: false - diff --git a/docker-compose.yml b/docker-compose.yml index 8c4870022..c932ad3fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ -version: "3.2" +version: "3.5" services: isard-database: + container_name: isard-database volumes: - type: bind source: /opt/isard/database @@ -10,8 +11,11 @@ services: - isard_network image: rethinkdb restart: always + logging: + driver: none isard-nginx: + container_name: isard-nginx volumes: - type: bind source: /opt/isard/certs/default @@ -36,6 +40,7 @@ services: restart: always isard-hypervisor: + container_name: isard-hypervisor volumes: - type: volume source: sshkeys @@ -59,6 +64,7 @@ services: restart: always isard-app: + container_name: isard-app volumes: - type: volume source: sshkeys @@ -101,3 +107,4 @@ volumes: networks: isard_network: external: false + name: isard_network diff --git a/dockers/grafana/etc/supervisor.d/carbon.ini b/dockers/grafana/etc/supervisor.d/carbon.ini index fb93a95fb..c3493b7e5 100644 --- a/dockers/grafana/etc/supervisor.d/carbon.ini +++ b/dockers/grafana/etc/supervisor.d/carbon.ini @@ -1,7 +1,9 @@ [program:carbon-cache] autostart = true autorestart = true -stdout_events_enabled = true +stdout_logfile=/grafana/logs/grafana-carbon.log +stderr_logfile=/grafana/logs/grafana-carbon-error.log +stdout_events_enabled = false stderr_events_enabled = true stdout_logfile_maxbytes = 1MB stdout_logfile_backups = 0 diff --git a/dockers/grafana/etc/supervisor.d/grafana.ini b/dockers/grafana/etc/supervisor.d/grafana.ini index 6d2dca1dd..3d701fd68 100644 --- a/dockers/grafana/etc/supervisor.d/grafana.ini +++ b/dockers/grafana/etc/supervisor.d/grafana.ini @@ -1,11 +1,15 @@ [program:grafana] autostart = true autorestart = true -stdout_events_enabled = true -stderr_events_enabled = true +#stdout_events_enabled = true +#stderr_events_enabled = true + +stdout_logfile=/grafana/logs/grafana.log +stderr_logfile=/grafana/logs/grafana-error.log stdout_logfile_maxbytes = 1MB + stdout_logfile_backups = 0 stderr_logfile_maxbytes = 1MB stderr_logfile_backups = 0 -command = /usr/local/bin/grafana-server --homepath=/grafana +command = /usr/local/bin/grafana-server --homepath=/grafana >> /grafana/logs/grafana.log diff --git a/dockers/grafana/etc/supervisor.d/gunicorn.ini b/dockers/grafana/etc/supervisor.d/gunicorn.ini index 7a94ac882..ba328835c 100644 --- a/dockers/grafana/etc/supervisor.d/gunicorn.ini +++ b/dockers/grafana/etc/supervisor.d/gunicorn.ini @@ -1,7 +1,9 @@ [program:graphite-webapp] autostart = true autorestart = true -stdout_events_enabled = true +stdout_logfile=/grafana/logs/grafana-gunicorn.log +stderr_logfile=/grafana/logs/grafana-gunicorn-error.log +stdout_events_enabled = false stderr_events_enabled = true stdout_logfile_maxbytes = 1MB stdout_logfile_backups = 0 diff --git a/dockers/grafana/etc/supervisor.d/nginx.ini b/dockers/grafana/etc/supervisor.d/nginx.ini index be2615cb3..620ed0506 100644 --- a/dockers/grafana/etc/supervisor.d/nginx.ini +++ b/dockers/grafana/etc/supervisor.d/nginx.ini @@ -1,7 +1,9 @@ [program:nginx] autostart = true autorestart = true -stdout_events_enabled = true +stdout_logfile=/grafana/logs/grafana-nginx.log +stderr_logfile=/grafana/logs/grafana-nginx-error.log +stdout_events_enabled = false stderr_events_enabled = true stdout_logfile_maxbytes = 1MB stdout_logfile_backups = 0 diff --git a/dockers/grafana/etc/supervisord.conf b/dockers/grafana/etc/supervisord.conf index 01799ab55..81541080d 100644 --- a/dockers/grafana/etc/supervisord.conf +++ b/dockers/grafana/etc/supervisord.conf @@ -16,11 +16,11 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///run/supervisord.sock -[eventlistener:stdout] -command = supervisor_stdout -buffer_size = 100 -events = PROCESS_LOG -result_handler = supervisor_stdout:event_handler +#[eventlistener:stdout] +#command = supervisor_stdout +#buffer_size = 100 +#events = PROCESS_LOG +#result_handler = supervisor_stdout:event_handler [include] files = /etc/supervisor.d/*.ini diff --git a/extras/grafana/docker-compose.yml b/extras/grafana/docker-compose.yml new file mode 100644 index 000000000..63212f20d --- /dev/null +++ b/extras/grafana/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3.5" +services: + isard-grafana: + container_name: isard-grafana + volumes: + - type: bind + source: /opt/isard/grafana/grafana/data + target: /grafana/data + read_only: false + - type: bind + source: /opt/isard/grafana/graphite/storage + target: /opt/graphite/storage + read_only: false + - type: bind + source: /opt/isard/grafana/graphite/conf + target: /opt/graphite/conf + read_only: false + ports: + - target: 3000 + published: 3000 + protocol: tcp + mode: host + networks: + - isard_network + image: isard/grafana:1.1 + restart: always + logging: + driver: none + #~ depends_on: + #~ - isard-app + +networks: + isard_network: + external: false + name: isard_network diff --git a/grafana.yml b/grafana.yml deleted file mode 100644 index 8f080718e..000000000 --- a/grafana.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: "3.2" -services: - isard-grafana: - volumes: - - type: bind - source: /opt/isard/grafana/grafana/data - target: /grafana/data - read_only: false - ports: - - target: 3000 - published: 3000 - protocol: tcp - mode: host - networks: - - isard_network - image: isard/grafana:1.1 - restart: always - -networks: - isard_network: - external: false From 63f3a288b4b9e42606876e648b5c72435b102733 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Jan 2019 12:24:57 +0100 Subject: [PATCH 61/92] Moved grafana to extras folder --- {dockers => extras}/grafana/Dockerfile | 10 ++-- extras/grafana/README.md | 27 ++++++++++ extras/grafana/build.sh | 47 ++++++++++++++++++ {dockers => extras}/grafana/conf/carbon.conf | 0 .../grafana/conf/local_settings.py | 0 .../grafana/conf/storage-aggregation.conf | 0 .../grafana/conf/storage-schemas.conf | 0 {dockers => extras}/grafana/data/grafana.db | Bin {dockers => extras}/grafana/data/log/.gitkeep | 0 .../grafana/data/plugins/.gitkeep | 0 {dockers => extras}/grafana/data/png/.gitkeep | 0 .../grafana/data/sessions/.gitkeep | 0 .../grafana/etc/nginx/conf.d/graphite | 0 .../grafana/etc/nginx/nginx.conf | 0 .../grafana/etc/supervisor.d/carbon.ini | 0 .../grafana/etc/supervisor.d/grafana.ini | 0 .../grafana/etc/supervisor.d/gunicorn.ini | 0 .../grafana/etc/supervisor.d/nginx.ini | 0 .../grafana/etc/supervisord.conf | 0 .../grafana/grafana-defaults.ini | 0 extras/grafana/remote-grafana.yml | 41 +++++++++++++++ {dockers => extras}/grafana/run.sh | 0 22 files changed, 120 insertions(+), 5 deletions(-) rename {dockers => extras}/grafana/Dockerfile (89%) create mode 100644 extras/grafana/README.md create mode 100755 extras/grafana/build.sh rename {dockers => extras}/grafana/conf/carbon.conf (100%) rename {dockers => extras}/grafana/conf/local_settings.py (100%) rename {dockers => extras}/grafana/conf/storage-aggregation.conf (100%) rename {dockers => extras}/grafana/conf/storage-schemas.conf (100%) rename {dockers => extras}/grafana/data/grafana.db (100%) rename {dockers => extras}/grafana/data/log/.gitkeep (100%) rename {dockers => extras}/grafana/data/plugins/.gitkeep (100%) rename {dockers => extras}/grafana/data/png/.gitkeep (100%) rename {dockers => extras}/grafana/data/sessions/.gitkeep (100%) rename {dockers => extras}/grafana/etc/nginx/conf.d/graphite (100%) rename {dockers => extras}/grafana/etc/nginx/nginx.conf (100%) rename {dockers => extras}/grafana/etc/supervisor.d/carbon.ini (100%) rename {dockers => extras}/grafana/etc/supervisor.d/grafana.ini (100%) rename {dockers => extras}/grafana/etc/supervisor.d/gunicorn.ini (100%) rename {dockers => extras}/grafana/etc/supervisor.d/nginx.ini (100%) rename {dockers => extras}/grafana/etc/supervisord.conf (100%) rename {dockers => extras}/grafana/grafana-defaults.ini (100%) create mode 100644 extras/grafana/remote-grafana.yml rename {dockers => extras}/grafana/run.sh (100%) diff --git a/dockers/grafana/Dockerfile b/extras/grafana/Dockerfile similarity index 89% rename from dockers/grafana/Dockerfile rename to extras/grafana/Dockerfile index 60317a61e..d8d408e52 100644 --- a/dockers/grafana/Dockerfile +++ b/extras/grafana/Dockerfile @@ -52,7 +52,7 @@ RUN set -ex \ && grafana-cli plugins update-all \ && rm -rf /tmp/setup -ADD dockers/grafana/grafana-defaults.ini /grafana/conf/defaults.ini +ADD grafana-defaults.ini /grafana/conf/defaults.ini EXPOSE 8080 EXPOSE 3000 @@ -62,10 +62,10 @@ EXPOSE 7002 VOLUME ["/opt/graphite/conf", "/opt/graphite/storage"] -COPY dockers/grafana/run.sh /run.sh -COPY dockers/grafana/etc/ /etc/ -COPY dockers/grafana/data/ /grafana/data_init/ -COPY dockers/grafana/conf/ /opt/graphite/conf.example/ +COPY run.sh /run.sh +COPY etc/ /etc/ +COPY data/ /grafana/data_init/ +COPY conf/ /opt/graphite/conf.example/ # Enable tiny init ENTRYPOINT ["/sbin/tini", "--"] diff --git a/extras/grafana/README.md b/extras/grafana/README.md new file mode 100644 index 000000000..db54c2cee --- /dev/null +++ b/extras/grafana/README.md @@ -0,0 +1,27 @@ +# Grafana + +This is an optional (but cool) extra that will bring up a carbon+graphite+grafana container plugged to your IsardVDI with predefined dashboards. + +## Installation + +``` +./build.sh +docker-compose up -d +``` + +Connect to your IsardVDI server on port 3000 to access grafana dashboards. + +NOTE: Check that you have grafana enabled in IsardVDI config menu. + +## Remote Grafana + +You can put your grafana in another server by building and running there the remote yml: + +``` +./build.sh +docker-compose -f remote-grafana.yml up -d +``` + +## More info + +https://isardvdi.readthedocs.io/en/latest/ diff --git a/extras/grafana/build.sh b/extras/grafana/build.sh new file mode 100755 index 000000000..84498b6a9 --- /dev/null +++ b/extras/grafana/build.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Check that the version number was provided +if [ -z "$1" ]; then + echo "You need to specify a IsardVDI version! e.g. '1.1.0'" + exit 1 +fi + +if [ $1 = "-f" ]; then + force=1 + if [ -z "$2" ]; then + echo "You need to specify a IsardVDI version with -f option! e.g. '1.1.0'" + exit 1 + fi + version=$2 +else + force=0 + version=$1 +fi + +MAJOR=${version:0:1} +MINOR=${version:0:3} +PATCH=$version + +# If a command fails, the whole script is going to stop +set -e + +# Checkout to the specified version tag +if [ force = 1 ]; then + git checkout $1 > /dev/null +fi + +# Array containing all the images to build +images=( + grafana +) + +# Build all the images and tag them correctly +for image in "${images[@]}"; do + echo -e "\n\n\n" + echo "Building $image" + echo -e "\n\n\n" + cmd="docker build -t isard/$image:latest -t isard/$image:$MAJOR -t isard/$image:$MINOR -t isard/$image:$PATCH ." + echo $cmd + $cmd +done + diff --git a/dockers/grafana/conf/carbon.conf b/extras/grafana/conf/carbon.conf similarity index 100% rename from dockers/grafana/conf/carbon.conf rename to extras/grafana/conf/carbon.conf diff --git a/dockers/grafana/conf/local_settings.py b/extras/grafana/conf/local_settings.py similarity index 100% rename from dockers/grafana/conf/local_settings.py rename to extras/grafana/conf/local_settings.py diff --git a/dockers/grafana/conf/storage-aggregation.conf b/extras/grafana/conf/storage-aggregation.conf similarity index 100% rename from dockers/grafana/conf/storage-aggregation.conf rename to extras/grafana/conf/storage-aggregation.conf diff --git a/dockers/grafana/conf/storage-schemas.conf b/extras/grafana/conf/storage-schemas.conf similarity index 100% rename from dockers/grafana/conf/storage-schemas.conf rename to extras/grafana/conf/storage-schemas.conf diff --git a/dockers/grafana/data/grafana.db b/extras/grafana/data/grafana.db similarity index 100% rename from dockers/grafana/data/grafana.db rename to extras/grafana/data/grafana.db diff --git a/dockers/grafana/data/log/.gitkeep b/extras/grafana/data/log/.gitkeep similarity index 100% rename from dockers/grafana/data/log/.gitkeep rename to extras/grafana/data/log/.gitkeep diff --git a/dockers/grafana/data/plugins/.gitkeep b/extras/grafana/data/plugins/.gitkeep similarity index 100% rename from dockers/grafana/data/plugins/.gitkeep rename to extras/grafana/data/plugins/.gitkeep diff --git a/dockers/grafana/data/png/.gitkeep b/extras/grafana/data/png/.gitkeep similarity index 100% rename from dockers/grafana/data/png/.gitkeep rename to extras/grafana/data/png/.gitkeep diff --git a/dockers/grafana/data/sessions/.gitkeep b/extras/grafana/data/sessions/.gitkeep similarity index 100% rename from dockers/grafana/data/sessions/.gitkeep rename to extras/grafana/data/sessions/.gitkeep diff --git a/dockers/grafana/etc/nginx/conf.d/graphite b/extras/grafana/etc/nginx/conf.d/graphite similarity index 100% rename from dockers/grafana/etc/nginx/conf.d/graphite rename to extras/grafana/etc/nginx/conf.d/graphite diff --git a/dockers/grafana/etc/nginx/nginx.conf b/extras/grafana/etc/nginx/nginx.conf similarity index 100% rename from dockers/grafana/etc/nginx/nginx.conf rename to extras/grafana/etc/nginx/nginx.conf diff --git a/dockers/grafana/etc/supervisor.d/carbon.ini b/extras/grafana/etc/supervisor.d/carbon.ini similarity index 100% rename from dockers/grafana/etc/supervisor.d/carbon.ini rename to extras/grafana/etc/supervisor.d/carbon.ini diff --git a/dockers/grafana/etc/supervisor.d/grafana.ini b/extras/grafana/etc/supervisor.d/grafana.ini similarity index 100% rename from dockers/grafana/etc/supervisor.d/grafana.ini rename to extras/grafana/etc/supervisor.d/grafana.ini diff --git a/dockers/grafana/etc/supervisor.d/gunicorn.ini b/extras/grafana/etc/supervisor.d/gunicorn.ini similarity index 100% rename from dockers/grafana/etc/supervisor.d/gunicorn.ini rename to extras/grafana/etc/supervisor.d/gunicorn.ini diff --git a/dockers/grafana/etc/supervisor.d/nginx.ini b/extras/grafana/etc/supervisor.d/nginx.ini similarity index 100% rename from dockers/grafana/etc/supervisor.d/nginx.ini rename to extras/grafana/etc/supervisor.d/nginx.ini diff --git a/dockers/grafana/etc/supervisord.conf b/extras/grafana/etc/supervisord.conf similarity index 100% rename from dockers/grafana/etc/supervisord.conf rename to extras/grafana/etc/supervisord.conf diff --git a/dockers/grafana/grafana-defaults.ini b/extras/grafana/grafana-defaults.ini similarity index 100% rename from dockers/grafana/grafana-defaults.ini rename to extras/grafana/grafana-defaults.ini diff --git a/extras/grafana/remote-grafana.yml b/extras/grafana/remote-grafana.yml new file mode 100644 index 000000000..c0da2f418 --- /dev/null +++ b/extras/grafana/remote-grafana.yml @@ -0,0 +1,41 @@ +version: "3.5" +services: + isard-grafana: + container_name: isard-grafana + volumes: + - type: bind + source: /opt/isard/grafana/grafana/data + target: /grafana/data + read_only: false + #~ - type: bind + #~ source: /opt/isard/grafana/graphite/storage + #~ target: /opt/graphite/storage + #~ read_only: false + #~ - type: bind + #~ source: /opt/isard/grafana/graphite/conf + #~ target: /opt/graphite/conf + #~ read_only: false + ports: + - target: 3000 + published: 3000 + protocol: tcp + mode: host + - target: 8080 + published: 8081 + protocol: tcp + mode: host + - target: 2003 + published: 2003 + protocol: tcp + mode: host + - target: 2004 + published: 2004 + protocol: tcp + mode: host + - target: 7002 + published: 7002 + protocol: tcp + mode: host + image: isard/grafana:1.1 + restart: always + diff --git a/dockers/grafana/run.sh b/extras/grafana/run.sh similarity index 100% rename from dockers/grafana/run.sh rename to extras/grafana/run.sh From fae9d0680bf042e049fa12ba57636ed6b91691e9 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Jan 2019 12:30:13 +0100 Subject: [PATCH 62/92] Updated to v3.5 as it allows for network and container names --- docker-compose.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8c4870022..c932ad3fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ -version: "3.2" +version: "3.5" services: isard-database: + container_name: isard-database volumes: - type: bind source: /opt/isard/database @@ -10,8 +11,11 @@ services: - isard_network image: rethinkdb restart: always + logging: + driver: none isard-nginx: + container_name: isard-nginx volumes: - type: bind source: /opt/isard/certs/default @@ -36,6 +40,7 @@ services: restart: always isard-hypervisor: + container_name: isard-hypervisor volumes: - type: volume source: sshkeys @@ -59,6 +64,7 @@ services: restart: always isard-app: + container_name: isard-app volumes: - type: volume source: sshkeys @@ -101,3 +107,4 @@ volumes: networks: isard_network: external: false + name: isard_network From 41bed09dc254ce88c9f90b897943fad9794905ed Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Jan 2019 18:08:09 +0100 Subject: [PATCH 63/92] Volume paths in shot format to allow for automatic creation on docker-compose up --- extras/grafana/docker-compose.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/extras/grafana/docker-compose.yml b/extras/grafana/docker-compose.yml index 63212f20d..919f912f5 100644 --- a/extras/grafana/docker-compose.yml +++ b/extras/grafana/docker-compose.yml @@ -3,18 +3,9 @@ services: isard-grafana: container_name: isard-grafana volumes: - - type: bind - source: /opt/isard/grafana/grafana/data - target: /grafana/data - read_only: false - - type: bind - source: /opt/isard/grafana/graphite/storage - target: /opt/graphite/storage - read_only: false - - type: bind - source: /opt/isard/grafana/graphite/conf - target: /opt/graphite/conf - read_only: false + - "/opt/isard/grafana/grafana/data:/grafana/data" + - "/opt/isard/grafana/graphite/storage:/opt/graphite/storage" + - "/opt/isard/grafana/graphite/conf:/opt/graphite/conf" ports: - target: 3000 published: 3000 From 36fcb6c53888a8b100e184cf98d3b8be2a0580f6 Mon Sep 17 00:00:00 2001 From: darta Date: Sun, 20 Jan 2019 18:17:56 +0100 Subject: [PATCH 64/92] updated volumes for compose v3.5 --- docker-compose.yml | 60 ++++++++++------------------------------------ 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c932ad3fb..2e982ab9e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,10 +3,7 @@ services: isard-database: container_name: isard-database volumes: - - type: bind - source: /opt/isard/database - target: /data - read_only: false + - "/opt/isard/database:/data" networks: - isard_network image: rethinkdb @@ -17,14 +14,8 @@ services: isard-nginx: container_name: isard-nginx volumes: - - type: bind - source: /opt/isard/certs/default - target: /etc/nginx/external - read_only: false - - type: bind - source: /opt/isard/logs/nginx - target: /var/log/nginx - read_only: false + - "/opt/isard/certs/default:/etc/nginx/external" + - "/opt/isard/logs/nginx:/var/log/nginx" ports: - target: 80 published: 80 @@ -42,18 +33,9 @@ services: isard-hypervisor: container_name: isard-hypervisor volumes: - - type: volume - source: sshkeys - target: /root/.ssh - read_only: false - - type: bind - source: /opt/isard - target: /isard - read_only: false - - type: bind - source: /opt/isard/certs/default - target: /etc/pki/libvirt-spice - read_only: false + - "sshkeys:/root/.ssh" + - "/opt/isard:/isard" + - "/opt/isard/certs/default:/etc/pki/libvirt-spice" ports: - "5900-5949:5900-5949" - "55900-55949:55900-55949" @@ -66,30 +48,12 @@ services: isard-app: container_name: isard-app volumes: - - type: volume - source: sshkeys - target: /root/.ssh - read_only: false - - type: bind - source: /opt/isard/certs - target: /certs - read_only: false - - type: bind - source: /opt/isard/logs - target: /isard/logs - read_only: false - - type: bind - source: /opt/isard/database/wizard - target: /isard/install/wizard - read_only: false - - type: bind - source: /opt/isard/backups - target: /isard/backups - read_only: false - - type: bind - source: /opt/isard/uploads - target: /isard/uploads - read_only: false + - "sshkeys:/root/.ssh" + - "/opt/isard/certs:/certs" + - "/opt/isard/logs:/isard/logs" + - "/opt/isard/database/wizard:/isard/install/wizard" + - "/opt/isard/backups:/isard/backups" + - "/opt/isard/uploads:/isard/uploads" extra_hosts: - "isard-engine:127.0.0.1" networks: From ca4b76585f4ab4200c1627626c908a3ccc7b26a1 Mon Sep 17 00:00:00 2001 From: beto Date: Wed, 23 Jan 2019 02:38:15 +0100 Subject: [PATCH 65/92] debug in extra directory, read README in extras/app-devel/ with guide and tricks" --- build-docker-images.sh | 2 +- dockers/app-devel/Dockerfile | 47 ------- dockers/app-devel/requirements.pip3 | 10 -- dockers/app-devel/supervisord.conf | 35 ------ dockers/devel-debug.yml | 87 ------------- dockers/docker-compose-devel.yml | 119 ------------------ extras/app-devel/Dockerfile | 37 ++++++ extras/app-devel/README | 83 ++++++++++++ .../app-devel}/build-docker-images-devel.sh | 6 +- extras/app-devel/devel-debug.yml | 102 +++++++++++++++ extras/app-devel/run_web_app.sh | 3 + .../services/threads/download_thread.py | 14 ++- src/engine/services/threads/grafana_thread.py | 59 ++++----- 13 files changed, 269 insertions(+), 335 deletions(-) delete mode 100644 dockers/app-devel/Dockerfile delete mode 100644 dockers/app-devel/requirements.pip3 delete mode 100644 dockers/app-devel/supervisord.conf delete mode 100644 dockers/devel-debug.yml delete mode 100644 dockers/docker-compose-devel.yml create mode 100644 extras/app-devel/Dockerfile create mode 100644 extras/app-devel/README rename {dockers => extras/app-devel}/build-docker-images-devel.sh (75%) create mode 100644 extras/app-devel/devel-debug.yml create mode 100755 extras/app-devel/run_web_app.sh diff --git a/build-docker-images.sh b/build-docker-images.sh index 132f521f7..c67af32ba 100755 --- a/build-docker-images.sh +++ b/build-docker-images.sh @@ -33,7 +33,7 @@ fi # Array containing all the images to build images=( #alpine-pandas - grafana + #grafana nginx hypervisor app diff --git a/dockers/app-devel/Dockerfile b/dockers/app-devel/Dockerfile deleted file mode 100644 index 8782a0f14..000000000 --- a/dockers/app-devel/Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -FROM isard/alpine-pandas:latest -MAINTAINER isard - -RUN apk add --no-cache git bash yarn py3-libvirt py3-paramiko py3-lxml py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-netaddr py3-requests curl openssh-client -RUN apk add --no-cache git - -######## only devel ######## -#RUN mkdir /isard -#ADD ./src /isard -############################ - -######## only devel ######## -COPY dockers/app-devel/requirements.pip3 /requirements.pip3 -############################ - -RUN pip3 install --no-cache-dir -r requirements.pip3 - -######## only devel ######## -RUN pip3 install ipython pytest -#not run in devel -#RUN mv /isard/isard.conf.docker /isard/isard.conf -############################ - -RUN mkdir -p /root/.ssh -RUN echo "Host isard-hypervisor \ - StrictHostKeyChecking no" >/root/.ssh/config -RUN chmod 600 /root/.ssh/config - -RUN apk add --update bash -RUN apk add yarn -RUN apk add openssh-client - -######## only devel ######## -RUN apk add vim openssh -############################ - -RUN apk add supervisor -RUN mkdir -p /var/log/supervisor - -######## only devel ######## -COPY dockers/app-devel/supervisord.conf /etc/supervisord.conf -############################ - -COPY dockers/app/certs.sh / -CMD /usr/bin/supervisord -c /etc/supervisord.conf -#CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] -#CMD ["sh", "/init.sh"] diff --git a/dockers/app-devel/requirements.pip3 b/dockers/app-devel/requirements.pip3 deleted file mode 100644 index e9e5f2db0..000000000 --- a/dockers/app-devel/requirements.pip3 +++ /dev/null @@ -1,10 +0,0 @@ -APScheduler==3.3.1 -Flask-SocketIO==2.8.6 -iniparse==0.4 -rethinkdb==2.3.0.post6 -pynpm==0.1.1 -graphyte==1.4 -pem==18.2.0 -Flask-Login==0.4.1 -xmltodict==0.11.0 - diff --git a/dockers/app-devel/supervisord.conf b/dockers/app-devel/supervisord.conf deleted file mode 100644 index 5a206ad35..000000000 --- a/dockers/app-devel/supervisord.conf +++ /dev/null @@ -1,35 +0,0 @@ -[supervisord] -nodaemon=true -logfile=/dev/stdout -loglevel=error -logfile_maxbytes=0 - -[program:certs] -command=sh /certs.sh -autostart=true -autorestart=false -startsecs=0 -priority=1 -stdout_logfile=/isard/logs/certs.log -stderr_logfile=/isard/logs/certs-error.log - -[program:webapp] -directory=/isard -command=python3 run_webapp.py -autostart=true -autorestart=true -startsecs=2 -priority=10 -stdout_logfile=/isard/logs/webapp.log -stderr_logfile=/isard/logs/webapp-error.log - -[program:engine] -directory=/isard -#command=sh -c "sleep 15 && python3 run_engine.py 1>/isard/logs/engine.log 2>/isard/logs/engine-error.log" -command=python3 run_engine.py -autostart=true -autorestart=false -startsecs=2 -priority=11 -stdout_logfile=/isard/logs/engine.log -stderr_logfile=/isard/logs/engine-error.log diff --git a/dockers/devel-debug.yml b/dockers/devel-debug.yml deleted file mode 100644 index eb9a59036..000000000 --- a/dockers/devel-debug.yml +++ /dev/null @@ -1,87 +0,0 @@ -version: '2' -services: - isard-database: - hostname: isard-database - volumes: - - "/opt/isard/database:/data" - networks: - - isard_network - ##### - only devel - ############################ - ports: - - "8080:8080" - expose: - - "28015" - ################################################# - #aliases: - # - rethinkdb - image: "rethinkdb" - restart: "no" - - isard-nginx: - volumes: - - "/opt/isard/certs/default:/etc/nginx/external" - - "/opt/isard/logs/nginx:/var/log/nginx" - build: - context: . - dockerfile: dockers/nginx/Dockerfile - ports: - - "80:80" - - "443:443" - networks: - - isard_network - image: isard/nginx:${TAG_DEVEL} - restart: "no" - depends_on: - - "isard-app" - - isard-hypervisor: - volumes: - - "sshkeys:/root/.ssh" - - "/opt/isard:/isard" - - "/opt/isard/certs/default:/etc/pki/libvirt-spice" - ports: - - "5900-5949:5900-5949" - - "55900-55949:55900-55949" - ################ only for devel ############### - expose: - - "22" - ############################################### - networks: - - isard_network - image: "isard/hypervisor:${TAG_DEVEL}" - privileged: true - restart: "no" - - isard-app: - hostname: isard-app - volumes: - ##### - only devel - ############################ - - "/opt/isard_devel/src:/isard" - - "/opt/ipython_profile_default:/root/.ipython/profile_default" - ################################################# - - "sshkeys:/root/.ssh" - - "/opt/isard/certs:/certs" - - "/opt/isard/logs:/isard/logs" - - "/opt/isard/backups:/isard/backups" - - "/opt/isard/uploads:/isard/uploads" - - "/opt/isard/database/wizard:/isard/install/wizard" - ########### - only devel ################# - expose: - - "5000" - ########################################## - extra_hosts: - - "isard-engine:127.0.0.1" - networks: - - isard_network - image: "isard/app-devel:${TAG_DEVEL}" - restart: "no" - depends_on: - - "isard-database" - - "isard-hypervisor" - -volumes: - sshkeys: - -networks: - isard_network: - external: false diff --git a/dockers/docker-compose-devel.yml b/dockers/docker-compose-devel.yml deleted file mode 100644 index 75d8f6baa..000000000 --- a/dockers/docker-compose-devel.yml +++ /dev/null @@ -1,119 +0,0 @@ -version: "3.2" -services: - isard-database: - volumes: - - type: bind - source: /opt/isard/database - target: /data - read_only: false - networks: - - isard_network - ##### - only devel - ############################ - ports: - - target: 8080 - published: 8080 - protocol: tcp - mode: host - - target: 28015 - published: 28015 - protocol: tcp - mode: host - ################################################# - image: rethinkdb - restart: always - - isard-nginx: - volumes: - - type: bind - source: /opt/isard/certs/default - target: /etc/nginx/external - read_only: false - - type: bind - source: /opt/isard/logs/nginx - target: /var/log/nginx - read_only: false - ports: - - target: 80 - published: 80 - protocol: tcp - mode: host - - target: 443 - published: 443 - protocol: tcp - mode: host - networks: - - isard_network - image: "isard/nginx:${$PROVA}" - command: echo ${PROVA} - restart: always - - isard-hypervisor: - volumes: - - type: volume - source: sshkeys - target: /root/.ssh - read_only: false - - type: bind - source: /opt/isard - target: /isard - read_only: false - - type: bind - source: /opt/isard/certs/default - target: /etc/pki/libvirt-spice - read_only: false - ports: - - "5900-5949:5900-5949" - - "55900-55949:55900-55949" - networks: - - isard_network - image: "isard/hypervisor:${TAG_DEVEL}" - privileged: true - restart: always - - isard-app: - volumes: - - type: volume - source: sshkeys - target: /root/.ssh - read_only: false - - type: bind - source: /opt/isard/certs - target: /certs - read_only: false - - type: bind - source: /opt/isard/logs - target: /isard/logs - read_only: false - - type: bind - source: /opt/isard/database/wizard - target: /isard/install/wizard - read_only: false - - type: bind - source: /opt/isard/backups - target: /isard/backups - read_only: false - - type: bind - source: /opt/isard/uploads - target: /isard/uploads - read_only: false - ##### - only devel - ############################ - - "/opt/isard_devel/src:/isard" - - "/opt/ipython_profile_default:/root/.ipython/profile_default" - ################################################# - extra_hosts: - - "isard-engine:127.0.0.1" - networks: - - isard_network - image: "isard/app_devel:${TAG_DEVEL}" - restart: always - depends_on: - - isard-database - - isard-hypervisor - - isard-nginx - -volumes: - sshkeys: - -networks: - isard_network: - external: false diff --git a/extras/app-devel/Dockerfile b/extras/app-devel/Dockerfile new file mode 100644 index 000000000..b9980b831 --- /dev/null +++ b/extras/app-devel/Dockerfile @@ -0,0 +1,37 @@ +FROM isard/alpine-pandas:latest +MAINTAINER isard + +RUN apk add --no-cache bash yarn py3-libvirt py3-paramiko py3-lxml py3-pexpect py3-openssl py3-bcrypt py3-gevent py3-flask py3-netaddr py3-requests curl openssh-client + +######## only devel ######## +RUN apk add --no-cache git vim openssh +############################ + +COPY dockers/app/requirements.pip3 /requirements.pip3 +RUN pip3 install --no-cache-dir -r requirements.pip3 + +######## only devel ######## +RUN pip3 install ipython pytest +############################ + +RUN mkdir -p /root/.ssh +RUN echo "Host isard-hypervisor \ + StrictHostKeyChecking no" >/root/.ssh/config +RUN chmod 600 /root/.ssh/config + +RUN apk add --no-cache supervisor +RUN mkdir -p /var/log/supervisor +COPY dockers/app/supervisord.conf /etc/supervisord.conf + +EXPOSE 5000 + +COPY dockers/app/certs.sh / +COPY dockers/app/add-hypervisor.sh / + +######## not in devel ######## +# RUN mkdir /isard +# ADD ./src /isard +# RUN mv /isard/isard.conf.docker /isard/isard.conf +############################ + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"] diff --git a/extras/app-devel/README b/extras/app-devel/README new file mode 100644 index 000000000..71810ed9e --- /dev/null +++ b/extras/app-devel/README @@ -0,0 +1,83 @@ +The local isard repo must be in the path: + + ln -s /your_isard_repo_dev_path /opt/isard_devel/ + +The version of devel for tag in image: + + echo "TAG=1.0" > /opt/isard_devel/.env + echo "TAG_DEVEL=1.1" >> /opt/isard_devel/.env + +Activate debug level creating a file in src: + + touch src/LOG_LEVEL_DEBUG + +You need to create a symbolic link in root of isard local repo to debug and run. + + ln -s extras/app-devel/devel-debug.yml devel-debug.yml + +And then run create devel image and run docker-compose: + + bash extras/app-devel/build-docker-images-devel.sh + docker-compose -f devel-debug.yml up + + +And then if you want to debug with ipython: + + sudo docker exec -it isard-beto_isard-app_1 ipython3 + +ipython profile history is saved out of container in: + + /opt/ipython_profile_default + + +Old configuration to run engine if have problems with running threads: + + #command=sh -c "sleep 15 && python3 run_engine.py 1>/isard/logs/engine.log 2>/isard/logs/engine-error.log" + + +Useful commands to devel: + + # stop all daemons + docker stop $(docker ps -a -q) + + # destroy containers (down) + sudo docker-compose -f devel-debug.yml down + + # list all containers + sudo docker container list --all + + # list all images + sudo docker images + + # rm docker image + sudo docker image rm isard/app-devel:latest + + # show logs + sudo docker container logs isard-hypervisor + + # prune all + sudo docker system prune --all --force --volumes + + # delete all /opt/isard (virtual disks, databases, logs...) + sudo docker system prune --all --force --volumes + + +# DEBUG wigh PyCharm + +run webapp in another terminal: + + sudo docker exec -it isard-app python3 run_webapp.py + +To debug in Pycharm: + +''' +1. Define python interpreter +--- docker-compose with docker-compose file: devel-debug.yml + +2. Debug with python interpreter: +--- name: docker-devel +--- python interpreter: remote python 3.X from docker-compose +--- script path: /home/beto/dev/isard-beto/src/run_engine.py +--- working directory: /your_devel_path/src +--- path mappings: /your_devel_path/src=/isard +''' diff --git a/dockers/build-docker-images-devel.sh b/extras/app-devel/build-docker-images-devel.sh similarity index 75% rename from dockers/build-docker-images-devel.sh rename to extras/app-devel/build-docker-images-devel.sh index 2f51f9747..d36a4fe59 100755 --- a/dockers/build-docker-images-devel.sh +++ b/extras/app-devel/build-docker-images-devel.sh @@ -18,9 +18,7 @@ set -e # Array containing all the images to build images=( - #alpine-pandas - #nginx - #hypervisor + #grafana app-devel ) @@ -29,6 +27,6 @@ for image in "${images[@]}"; do echo -e "\n\n\n" echo "Building $image" echo -e "\n\n\n" - docker build -f=dockers/$image/Dockerfile -t isard/$image:latest -t isard/$image:$MAJOR -t isard/$image:$MINOR -t isard/$image:$PATCH . + docker build -f=extras/$image/Dockerfile -t isard/$image:latest -t isard/$image:$MAJOR -t isard/$image:$MINOR -t isard/$image:$PATCH . done diff --git a/extras/app-devel/devel-debug.yml b/extras/app-devel/devel-debug.yml new file mode 100644 index 000000000..2be92891d --- /dev/null +++ b/extras/app-devel/devel-debug.yml @@ -0,0 +1,102 @@ +version: "3.5" +services: + isard-database: + container_name: isard-database + volumes: + - "/opt/isard/database:/data" + networks: + - isard_network + ##### - only devel - ############################ + ports: + - "8080:8080" + expose: + - "28015" + ################################################# + image: rethinkdb + restart: always + + isard-nginx: + container_name: isard-nginx + volumes: + - "/opt/isard/certs/default:/etc/nginx/external" + - "/opt/isard/logs/nginx:/var/log/nginx" + ports: + - "80:80" + - "443:443" + networks: + - isard_network + image: isard/nginx:1.1 + restart: "no" + + isard-hypervisor: + container_name: isard-hypervisor + volumes: + - "sshkeys:/root/.ssh" + - "/opt/isard:/isard" + - "/opt/isard/certs/default:/etc/pki/libvirt-spice" + ports: + - "5900-5949:5900-5949" + - "55900-55949:55900-55949" + networks: + - isard_network + image: isard/hypervisor:1.1 + privileged: true + ################ only for devel ############### + expose: + - "22" + ############################################### + restart: "no" + + isard-app: + container_name: isard-app + volumes: + - "sshkeys:/root/.ssh" + - "/opt/isard/certs:/certs" + - "/opt/isard/logs:/isard/logs" + - "/opt/isard/database/wizard:/isard/install/wizard" + - "/opt/isard/backups:/isard/backups" + - "/opt/isard/uploads:/isard/uploads" + + ##### - only devel - ############################ + - "/opt/isard_devel/src:/isard" + - "/opt/ipython_profile_default:/root/.ipython/profile_default" + ################################################# + + ########### - only devel ################# + expose: + - "5000" + ########################################## + extra_hosts: + - "isard-engine:127.0.0.1" + networks: + - isard_network + image: "isard/app-devel:${TAG_DEVEL}" + restart: "no" + depends_on: + - isard-database + - isard-hypervisor + - isard-nginx + - isard-grafana + + isard-grafana: + container_name: isard-grafana + volumes: + - "/opt/isard/grafana/grafana/data:/grafana/data" + - "/opt/isard/grafana/graphite/storage:/opt/graphite/storage" + - "/opt/isard/grafana/graphite/conf:/opt/graphite/conf" + ports: + - 3000:3000 + networks: + - isard_network + image: isard/grafana:1.1 + restart: "no" + logging: + driver: none + +volumes: + sshkeys: + +networks: + isard_network: + external: false + #name: isard_network diff --git a/extras/app-devel/run_web_app.sh b/extras/app-devel/run_web_app.sh new file mode 100755 index 000000000..a584aa105 --- /dev/null +++ b/extras/app-devel/run_web_app.sh @@ -0,0 +1,3 @@ +#!/bin/bash +sleep 10 +sudo docker exec -it isard-app python3 run_webapp.py \ No newline at end of file diff --git a/src/engine/services/threads/download_thread.py b/src/engine/services/threads/download_thread.py index 5f7276f82..23744a223 100644 --- a/src/engine/services/threads/download_thread.py +++ b/src/engine/services/threads/download_thread.py @@ -193,10 +193,16 @@ def run(self): logs.downloads.debug(self.url) logs.downloads.debug(line) d_progress = dict(zip(keys,values)) - d_progress['total_percent'] = int(float(d_progress['total_percent'])) - d_progress['received_percent'] = int(float(d_progress['received_percent'])) - update_download_percent(d_progress, self.table, self.id) - line = p.stderr.read(60).decode('utf8') + try: + d_progress['total_percent'] = int(float(d_progress['total_percent'])) + d_progress['received_percent'] = int(float(d_progress['received_percent'])) + if d_progress['received_percent'] > 1: + pass + except: + d_progress['total_percent'] = 0 + d_progress['received_percent'] = 0 + update_download_percent(d_progress, self.table, self.id) + line = p.stderr.read(60).decode('utf8') else: line = line + c diff --git a/src/engine/services/threads/grafana_thread.py b/src/engine/services/threads/grafana_thread.py index 1fe9e6b34..5580b3aa5 100644 --- a/src/engine/services/threads/grafana_thread.py +++ b/src/engine/services/threads/grafana_thread.py @@ -68,38 +68,41 @@ def run(self): if self.t_status[id_hyp].status_obj.hyp_obj.connected is True: if id_hyp not in hyps_online: hyps_online.append(id_hyp) + check_hyp = True except: logs.main.error(f'hypervisor {id_hyp} problem checking if is connected') - - #send static values of hypervisors - if elapsed >= SEND_STATIC_VALUES_INTERVAL: - d_hyps_info = dict() + check_hyp = False + + if len(hyps_online) > 0 and check_hyp is True: + #send static values of hypervisors + if elapsed >= SEND_STATIC_VALUES_INTERVAL: + d_hyps_info = dict() + for i, id_hyp in enumerate(hyps_online): + d_hyps_info[f'hyp-info-{i}'] = self.t_status[id_hyp].status_obj.hyp_obj.info + # ~ self.send(d_hyps_info) + elapsed = 0 + + #send stats + dict_to_send = dict() + j=0 for i, id_hyp in enumerate(hyps_online): - d_hyps_info[f'hyp-info-{i}'] = self.t_status[id_hyp].status_obj.hyp_obj.info - # ~ self.send(d_hyps_info) - elapsed = 0 - - #send stats - dict_to_send = dict() - j=0 - for i, id_hyp in enumerate(hyps_online): - if id_hyp in self.t_status.keys(): - #stats_hyp = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp - stats_hyp_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp_now - #stats_domains = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains - if len(stats_hyp_now) > 0: - dict_to_send[f'hypers.'+id_hyp] = {'stats':stats_hyp_now,'info':d_hyps_info['hyp-info-'+str(i)],'domains':{}} - stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now - # ~ for id_domain,d_stats in stats_domains_now.items(): - # ~ if len(stats_hyp_now) > 0: + if id_hyp in self.t_status.keys(): + #stats_hyp = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp + stats_hyp_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp_now + #stats_domains = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains + if len(stats_hyp_now) > 0: + dict_to_send[f'hypers.'+id_hyp] = {'stats':stats_hyp_now,'info':d_hyps_info['hyp-info-'+str(i)],'domains':{}} + stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now # ~ for id_domain,d_stats in stats_domains_now.items(): - # ~ dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} - dict_to_send[f'hypers.'+id_hyp]['domains']=stats_domains_now #{x:0 for x in stats_domains_now} - # ~ print(stats_domains_now) - # ~ j+=1 - - if len(dict_to_send) > 0: - self.send(dict_to_send) + # ~ if len(stats_hyp_now) > 0: + # ~ for id_domain,d_stats in stats_domains_now.items(): + # ~ dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} + dict_to_send[f'hypers.'+id_hyp]['domains']=stats_domains_now #{x:0 for x in stats_domains_now} + # ~ print(stats_domains_now) + # ~ j+=1 + + if len(dict_to_send) > 0: + self.send(dict_to_send) From fca8ca2866d9f87b7aa85154de13e732486a5da3 Mon Sep 17 00:00:00 2001 From: beto Date: Wed, 23 Jan 2019 17:53:18 +0100 Subject: [PATCH 66/92] grafana disabled when waiting c conf at start engine --- docker-compose.yml | 13 ++++--------- extras/app-devel/devel-debug.yml | 3 ++- src/engine/api/__init__.py | 20 +++++++++++++++++++- src/engine/config.py | 4 ++-- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2e982ab9e..68924523a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,18 +17,14 @@ services: - "/opt/isard/certs/default:/etc/nginx/external" - "/opt/isard/logs/nginx:/var/log/nginx" ports: - - target: 80 - published: 80 - protocol: tcp - mode: host - - target: 443 - published: 443 - protocol: tcp - mode: host + - "80:80" + - "443:443" networks: - isard_network image: isard/nginx:1.1 restart: always + depends_on: + - isard-app isard-hypervisor: container_name: isard-hypervisor @@ -63,7 +59,6 @@ services: depends_on: - isard-database - isard-hypervisor - - isard-nginx volumes: sshkeys: diff --git a/extras/app-devel/devel-debug.yml b/extras/app-devel/devel-debug.yml index 2be92891d..e65fdeb61 100644 --- a/extras/app-devel/devel-debug.yml +++ b/extras/app-devel/devel-debug.yml @@ -27,6 +27,8 @@ services: - isard_network image: isard/nginx:1.1 restart: "no" + depends_on: + - isard-app isard-hypervisor: container_name: isard-hypervisor @@ -75,7 +77,6 @@ services: depends_on: - isard-database - isard-hypervisor - - isard-nginx - isard-grafana isard-grafana: diff --git a/src/engine/api/__init__.py b/src/engine/api/__init__.py index 3bc5a4dd8..cba423a04 100644 --- a/src/engine/api/__init__.py +++ b/src/engine/api/__init__.py @@ -12,7 +12,7 @@ api = Blueprint('api', __name__) app = current_app -from . import evaluate +#from . import evaluate def shutdown_server(): func = request.environ.get('werkzeug.server.shutdown') @@ -103,6 +103,24 @@ def engine_restart(): break return jsonify({'engine_restart':True}), 200 + +@api.route('/engine/status') +def engine_status(): + '''all main threads are running''' + pass + + +@api.route('/pool//status') +def pool_status(id_pool): + '''hypervisors ready to start and create disks''' + pass + +@api.route('/grafana/reload') +def grafana_reload(): + '''changes in grafana parameters''' + pass + + @api.route('/engine_info', methods=['GET']) def engine_info(): d_engine = {} diff --git a/src/engine/config.py b/src/engine/config.py index 09de37e0c..95825d21d 100644 --- a/src/engine/config.py +++ b/src/engine/config.py @@ -53,7 +53,7 @@ try: with r.connect(host=RETHINK_HOST, port=RETHINK_PORT) as conn: rconfig = r.db(RETHINK_DB).table('config').get(1).run(conn) - grafana= rconfig['engine']['grafana'] + #grafana= rconfig['engine']['grafana'] rconfig = rconfig['engine'] table_exists=True if fail_first_loop: @@ -73,7 +73,7 @@ TEST_HYP_FAIL_INTERVAL = rconfig['intervals']['test_hyp_fail'] POLLING_INTERVAL_BACKGROUND = rconfig['intervals']['background_polling'] POLLING_INTERVAL_TRANSITIONAL_STATES = rconfig['intervals']['transitional_states_polling'] -GRAFANA = grafana +#GRAFANA = grafana TRANSITIONAL_STATUS = ('Starting', 'Stopping', 'Deleting') From 3a111f5d80bcb1910c7d7e8d5a97bb8bad553190 Mon Sep 17 00:00:00 2001 From: beto Date: Wed, 23 Jan 2019 18:05:16 +0100 Subject: [PATCH 67/92] insert download domains in wizard in step 5 --- src/webapp/wizard/WizardLib.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webapp/wizard/WizardLib.py b/src/webapp/wizard/WizardLib.py index 59292ec74..64ac667da 100644 --- a/src/webapp/wizard/WizardLib.py +++ b/src/webapp/wizard/WizardLib.py @@ -283,8 +283,8 @@ def valid_isard_database(self): ## Maybe ask for a backup? ## No invasive p.check_integrity(commit=True) - if self.register_isard_updates(): - self.insert_updates_demo() + # if self.register_isard_updates(): + # self.insert_updates_demo() return True else: return False @@ -407,6 +407,8 @@ def valid_server(self,server=False): try: conn.request("HEAD", "/") conn.close() + if self.register_isard_updates(): + self.insert_updates_demo() return True except Exception as e: conn.close() From 0b72ef8d3347a27fb3279954e718d84c8f5e50c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Wed, 23 Jan 2019 20:34:34 +0100 Subject: [PATCH 68/92] Trying change behaviour --- src/webapp/wizard/WizardLib.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/webapp/wizard/WizardLib.py b/src/webapp/wizard/WizardLib.py index 64ac667da..e8ff067c6 100644 --- a/src/webapp/wizard/WizardLib.py +++ b/src/webapp/wizard/WizardLib.py @@ -397,23 +397,34 @@ def update_hypervisor_viewer(self,remote_addr): def valid_server(self,server=False): if server is False: - if self.url is not False: - wlog.warning('self.url='+str(self.url)) - server=self.url.split('//')[1] - else: - server='isardvdi.com' + server='isardvdi.com' import http.client as httplib conn = httplib.HTTPConnection(server, timeout=5) try: conn.request("HEAD", "/") conn.close() - if self.register_isard_updates(): - self.insert_updates_demo() return True except Exception as e: conn.close() return False + def callfor_updates_demo(self): + if self.url is not False: + wlog.warning('self.url='+str(self.url)) + server=self.url.split('//')[1] + else: + server='isardvdi.com' + import http.client as httplib + conn = httplib.HTTPConnection(server, timeout=5) + try: + conn.request("HEAD", "/") + conn.close() + if self.register_isard_updates(): + self.insert_updates_demo() + return True + except Exception as e: + conn.close() + return False def create_isard_database(self): from ..config.populate import Populate @@ -421,6 +432,7 @@ def create_isard_database(self): if p.database(): # ~ p.defaults() p.check_integrity(commit=True) + self.callfor_updates_demo() return True return False @@ -431,7 +443,7 @@ def check_all(self): res = {'yarn':self.valid_js(), 'config':True, 'config_stx':True, - 'internet':self.valid_server('isardvdi.com'), + 'internet': True #self.valid_server('isardvdi.com'), 'rethinkdb':self.valid_rethinkdb(), 'isard_db':self.valid_isard_database(), 'passwd':self.valid_password(), @@ -442,7 +454,7 @@ def check_all(self): res = {'yarn':self.valid_js(), 'config':self.valid_config_file(), 'config_stx':self.valid_config_syntax(), - 'internet':self.valid_server('isardvdi.com'), + 'internet': True #self.valid_server('isardvdi.com'), 'rethinkdb':self.valid_rethinkdb(), 'isard_db':self.valid_isard_database(), 'passwd':self.valid_password(), @@ -578,7 +590,7 @@ def wizard_validate_step(step): if step is '5': return json.dumps(self.valid_hypervisor() if self.valid_isard_database() else False) if step is '6': - return json.dumps(self.valid_server()) + return json.dumps(self.valid_server('isardvdi.com')) @self.wapp.route('/content', methods=['POST']) def wizard_content(): @@ -617,7 +629,7 @@ def wizard_content(): return html[5]['ko'] return html[5]['ok'] if step == '6': - if not self.valid_server(): + if not self.valid_server('isardvdi.com'): return html[6]['noservice'] if self.is_registered() is False: return html[6]['noregister'] From f5fdca8581202417db51304799348f01e0def402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Wed, 23 Jan 2019 22:25:19 +0100 Subject: [PATCH 69/92] Updated wizard behaviour --- src/webapp/config/populate.py | 8 ++- src/webapp/wizard/WizardLib.py | 113 ++++++++++++++++++--------------- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/src/webapp/config/populate.py b/src/webapp/config/populate.py index b533471b4..7290f3238 100644 --- a/src/webapp/config/populate.py +++ b/src/webapp/config/populate.py @@ -15,7 +15,9 @@ from ..lib.admin_api import Certificates class Populate(object): - def __init__(self): + def __init__(self,dreg): + self.register_code=dreg['resources']['code'] + self.register_url=dreg['resources']['url'] self.cfg=load_config() try: self.conn = r.connect( self.cfg['RETHINKDB_HOST'],self.cfg['RETHINKDB_PORT'],self.cfg['RETHINKDB_DB']).repl() @@ -119,8 +121,8 @@ def config(self): }}, 'grafana':{'active':False,'url':'','hostname':'isard-grafana','carbon_port':2004,"interval": 5}, 'version':0, - 'resources': {'code':False, - 'url':'http://www.isardvdi.com:5050'} + 'resources': {'code':self.register_code, + 'url':self.register_url} }], conflict='update').run()) log.info("Table config populated with defaults.") return True diff --git a/src/webapp/wizard/WizardLib.py b/src/webapp/wizard/WizardLib.py index e8ff067c6..366367881 100644 --- a/src/webapp/wizard/WizardLib.py +++ b/src/webapp/wizard/WizardLib.py @@ -50,7 +50,7 @@ class Wizard(): def __init__(self): self.register_isard=False self.code=False - self.url=False + self.url='http://www.isardvdi.com:5050' self.doWizard=True if self.first_start() else False if self.doWizard: # WIZARD WAS FORCED BY DELETING install/x.wizard file @@ -131,7 +131,7 @@ def insert_updates_demo(self): missing_resources=self.get_missing_resources(dom,'admin') for k,v in missing_resources.items(): for resource in v: - r.table(k).insert(v).run() + r.db('isard').table(k).insert(v).run() self.insert_update('domains',[dom]) wlog.info('New download: '+u) @@ -140,7 +140,7 @@ def insert_updates_demo(self): for vi in virt_installs: for u in updates: if u == vi['id']: - r.table('virt_install').insert(vi).run() + r.db('isard').table('virt_install').insert(vi).run() wlog.info('New virt_install: '+u) # Useful default media with drivers for Microsfot #~ medias=self.get_updates_new_kind('media','admin') @@ -174,11 +174,11 @@ def insert_update(self,kind,data): # ~ d['percentage']=0 d['status']='DownloadStarting' d['path']=userpath+d['url-isard'] - r.table(kind).insert(data).run() + r.db('isard').table(kind).insert(data).run() def get_updates_new_kind(self,kind,username): web=self.get_updates_kind(kind=kind) - dbb=list(r.table(kind).run()) + dbb=list(r.db('isard').table(kind).run()) result=[] for w in web: found=False @@ -212,7 +212,7 @@ def get_missing_resources(self,domain,username): missing_resources={'videos':[]} dom_videos=domain['create_dict']['hardware']['videos'] - sys_videos=list(r.table('videos').pluck('id').run()) + sys_videos=list(r.db('isard').table('videos').pluck('id').run()) sys_videos=[sv['id'] for sv in sys_videos] for v in dom_videos: if v not in sys_videos: @@ -223,8 +223,9 @@ def get_missing_resources(self,domain,username): return missing_resources def is_registered(self): - if not self.code is False: return True - return False + if self.code is False: + return False + return True def render_updates(self,dict): html='

    ' @@ -276,50 +277,57 @@ def valid_rethinkdb(self): def valid_isard_database(self): try: - if 'isard' in r.db_list().run(): - from ..config.populate import Populate - p=Populate() + resources=r.db('isard').table('config').get(1).pluck('resources').run()['resources'] + self.code=resources['code'] + self.url=resources['url'] + # ~ if 'isard' in r.db_list().run(): + # ~ from ..config.populate import Populate + # ~ dict=self.register_isard_updates() + # ~ p=Populate(dict) ## Ideally we should inform user that some tables will be deleted and others created. ## Maybe ask for a backup? ## No invasive - p.check_integrity(commit=True) + # ~ p.check_integrity(commit=True) # if self.register_isard_updates(): # self.insert_updates_demo() - return True - else: - return False + # ~ return True + # ~ else: + # ~ return False + return True except Exception as e: wlog.error(str(e)) return False def register_isard_updates(self): + dict={'resources':{'url':self.url,'code':self.code}} if not self.register_isard: - return False + return dict else: # USER WANTS TO REGISTER ISARD - try: - cfg=r.table('config').get(1).pluck('resources').run() - except Exception as e: - return False - if 'resources' in cfg.keys(): - self.url=cfg['resources']['url'] - self.code=cfg['resources']['code'] + # ~ try: + # ~ cfg=r.table('config').get(1).pluck('resources').run() + # ~ except Exception as e: + # ~ return False + # ~ if 'resources' in cfg.keys(): + # ~ self.url=cfg['resources']['url'] + # ~ self.code=cfg['resources']['code'] if self.code is False: - if self.url is False: self.url='http://www.isardvdi.com:5050' + # ~ if self.url is False: self.url='http://www.isardvdi.com:5050' try: req= requests.post(self.url+'/register' ,allow_redirects=False, verify=False, timeout=3) if req.status_code==200: self.code=req.json() - r.table('config').get(1).update({'resources':{'url':self.url,'code':req.json()}}).run() + # ~ r.table('config').get(1).update({'resources':{'url':self.url,'code':req.json()}}).run() + dict={'resources':{'url':self.url,'code':self.code}} wlog.warning('Isard app registered') - return True + return dict else: wlog.info('Isard app registering error response code: '+str(req.status_code)+'\nDetail: '+r.json()) - return False + return dict except Exception as e: wlog.warning("Error contacting.\n"+str(e)) - return False - return True + return dict + return dict def valid_password(self): try: @@ -378,9 +386,10 @@ def valid_engine(self): def valid_hypervisor(self,remote_addr=False): try: - if r.table('hypervisors').filter({'status':'Online'}).pluck('status').run() is not None: + if r.db('isard').table('hypervisors').filter({'status':'Online'}).pluck('status').run() is not None: # ~ if remote_addr is not False: # ~ self.update_hypervisor_viewer(remote_addr) + self.callfor_updates_demo() return True return False except: @@ -388,11 +397,11 @@ def valid_hypervisor(self,remote_addr=False): def update_hypervisor_viewer(self,remote_addr): try: - r.table('config').get(1).update({'engine':{'grafana':{'url':'http://'+str(remote_addr)}}}).run() - if r.table('hypervisors').get('isard-hypervisor').update({'viewer_hostname':remote_addr}).run() is not None: + r.db('isard').table('config').get(1).update({'engine':{'grafana':{'url':'http://'+str(remote_addr)}}}).run() + if r.db('isard').table('hypervisors').get('isard-hypervisor').update({'viewer_hostname':remote_addr}).run() is not None: return True return False - except: + except Exception as e: return False def valid_server(self,server=False): @@ -409,30 +418,33 @@ def valid_server(self,server=False): return False def callfor_updates_demo(self): - if self.url is not False: - wlog.warning('self.url='+str(self.url)) - server=self.url.split('//')[1] - else: - server='isardvdi.com' - import http.client as httplib - conn = httplib.HTTPConnection(server, timeout=5) + # ~ if self.url is not False: + # ~ wlog.warning('self.url='+str(self.url)) + server=self.url.split('//')[1] + # ~ else: + # ~ server='isardvdi.com' + if not self.valid_server(server): return False + # ~ import http.client as httplib + # ~ conn = httplib.HTTPConnection(server, timeout=5) try: - conn.request("HEAD", "/") - conn.close() - if self.register_isard_updates(): - self.insert_updates_demo() + # ~ conn.request("HEAD", "/") + # ~ conn.close() + # ~ if self.register_isard_updates(): + self.insert_updates_demo() return True except Exception as e: - conn.close() + # ~ conn.close() + wlog.warning('Could not register.') return False def create_isard_database(self): from ..config.populate import Populate - p=Populate() + dict=self.register_isard_updates() + p=Populate(dict) if p.database(): # ~ p.defaults() p.check_integrity(commit=True) - self.callfor_updates_demo() + return True return False @@ -443,7 +455,7 @@ def check_all(self): res = {'yarn':self.valid_js(), 'config':True, 'config_stx':True, - 'internet': True #self.valid_server('isardvdi.com'), + 'internet': True, #self.valid_server('isardvdi.com'), 'rethinkdb':self.valid_rethinkdb(), 'isard_db':self.valid_isard_database(), 'passwd':self.valid_password(), @@ -454,7 +466,7 @@ def check_all(self): res = {'yarn':self.valid_js(), 'config':self.valid_config_file(), 'config_stx':self.valid_config_syntax(), - 'internet': True #self.valid_server('isardvdi.com'), + 'internet': True, #self.valid_server('isardvdi.com'), 'rethinkdb':self.valid_rethinkdb(), 'isard_db':self.valid_isard_database(), 'passwd':self.valid_password(), @@ -588,7 +600,8 @@ def wizard_validate_step(step): if step is '4': return json.dumps(self.valid_engine()) if step is '5': - return json.dumps(self.valid_hypervisor() if self.valid_isard_database() else False) + # ~ return json.dumps(self.valid_hypervisor() if self.valid_isard_database() else False) + return json.dumps(self.valid_hypervisor()) if step is '6': return json.dumps(self.valid_server('isardvdi.com')) From 09b7a1bcca84ed379066c324ebe0fd7608bfdde2 Mon Sep 17 00:00:00 2001 From: beto Date: Thu, 24 Jan 2019 02:20:09 +0100 Subject: [PATCH 70/92] more verbose --- src/engine/services/threads/download_thread.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engine/services/threads/download_thread.py b/src/engine/services/threads/download_thread.py index 23744a223..2fee57eba 100644 --- a/src/engine/services/threads/download_thread.py +++ b/src/engine/services/threads/download_thread.py @@ -75,6 +75,7 @@ def run(self): hyp_to_disk_create = get_host_disk_operations_from_path(path_selected, pool=self.pool_id, type_path=self.type_path_selected) + logs.downloads.debug(f'Thread download started to url: {url} in hypervisor: {hyp_to_disk_create}') if self.manager.t_disk_operations.get(hyp_to_disk_create,False) is not False: if self.manager.t_disk_operations[hyp_to_disk_create].is_alive(): d = get_hyp_hostname_user_port_from_id(hyp_to_disk_create) @@ -355,8 +356,9 @@ def start_download(self, dict_changes): d_update_domain['hardware']['disks'][0]['path_selected'] = path_selected update_domain_dict_create_dict(id_down, d_update_domain) - # launching download threads if new_file_path not in self.download_threads: + # launching download threads + logs.downloads.debug(f'ready tu start DownloadThread --> url:{url} , path:{new_file_path}') self.download_threads[new_file_path] = DownloadThread(url, new_file_path, path_selected, @@ -371,7 +373,7 @@ def start_download(self, dict_changes): self.download_threads[new_file_path].start() else: - logs.downloads.info('download thread launched to this path: {}'.format(new_file_path)) + logs.downloads.error('download thread launched previously to this path: {}'.format(new_file_path)) def run(self): self.tid = get_tid() From 133092017859afa0fb09375347fa3719b95ec82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9fix=20Estrada?= Date: Thu, 24 Jan 2019 14:48:19 +0100 Subject: [PATCH 71/92] Changed docker restart policy to unless-stopped Now it's more user-friendly --- docker-compose.yml | 8 ++++---- dockers/remote-grafana/docker-compose.yml | 2 +- dockers/remote-hyper/docker-compose.yml | 2 +- extras/app-devel/devel-debug.yml | 2 +- extras/grafana/docker-compose.yml | 2 +- extras/grafana/remote-grafana.yml | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 68924523a..fb0c787ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: networks: - isard_network image: rethinkdb - restart: always + restart: unless-stopped logging: driver: none @@ -22,7 +22,7 @@ services: networks: - isard_network image: isard/nginx:1.1 - restart: always + restart: unless-stopped depends_on: - isard-app @@ -39,7 +39,7 @@ services: - isard_network image: isard/hypervisor:1.1 privileged: true - restart: always + restart: unless-stopped isard-app: container_name: isard-app @@ -55,7 +55,7 @@ services: networks: - isard_network image: isard/app:1.1 - restart: always + restart: unless-stopped depends_on: - isard-database - isard-hypervisor diff --git a/dockers/remote-grafana/docker-compose.yml b/dockers/remote-grafana/docker-compose.yml index 08cd568e4..1fd4543bf 100644 --- a/dockers/remote-grafana/docker-compose.yml +++ b/dockers/remote-grafana/docker-compose.yml @@ -36,5 +36,5 @@ services: protocol: tcp mode: host image: isard/grafana:1.1 - restart: always + restart: unless-stopped diff --git a/dockers/remote-hyper/docker-compose.yml b/dockers/remote-hyper/docker-compose.yml index 7750e41af..0218a8395 100644 --- a/dockers/remote-hyper/docker-compose.yml +++ b/dockers/remote-hyper/docker-compose.yml @@ -20,7 +20,7 @@ services: - "55900-55949:55900-55949" image: isard/hypervisor:1.0 privileged: true - restart: always + restart: unless-stopped volumes: sshkeys: diff --git a/extras/app-devel/devel-debug.yml b/extras/app-devel/devel-debug.yml index e65fdeb61..056e28622 100644 --- a/extras/app-devel/devel-debug.yml +++ b/extras/app-devel/devel-debug.yml @@ -13,7 +13,7 @@ services: - "28015" ################################################# image: rethinkdb - restart: always + restart: unless-stopped isard-nginx: container_name: isard-nginx diff --git a/extras/grafana/docker-compose.yml b/extras/grafana/docker-compose.yml index 919f912f5..ef2fff286 100644 --- a/extras/grafana/docker-compose.yml +++ b/extras/grafana/docker-compose.yml @@ -14,7 +14,7 @@ services: networks: - isard_network image: isard/grafana:1.1 - restart: always + restart: unless-stopped logging: driver: none #~ depends_on: diff --git a/extras/grafana/remote-grafana.yml b/extras/grafana/remote-grafana.yml index c0da2f418..878de7c50 100644 --- a/extras/grafana/remote-grafana.yml +++ b/extras/grafana/remote-grafana.yml @@ -37,5 +37,5 @@ services: protocol: tcp mode: host image: isard/grafana:1.1 - restart: always + restart: unless-stopped From 27609e8542a7aa9ab141cad01a8af7998679d3c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 26 Jan 2019 01:01:14 +0100 Subject: [PATCH 72/92] Removed unused containers --- dockers/remote-grafana/README.md | 5 --- dockers/remote-grafana/docker-compose.yml | 40 ------------------- {dockers => extras}/remote-hyper/README.md | 0 .../remote-hyper/docker-compose.yml | 0 4 files changed, 45 deletions(-) delete mode 100644 dockers/remote-grafana/README.md delete mode 100644 dockers/remote-grafana/docker-compose.yml rename {dockers => extras}/remote-hyper/README.md (100%) rename {dockers => extras}/remote-hyper/docker-compose.yml (100%) diff --git a/dockers/remote-grafana/README.md b/dockers/remote-grafana/README.md deleted file mode 100644 index c775a13a0..000000000 --- a/dockers/remote-grafana/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# IsardVDI Remote Grafana - -It brings up a remote grafana host. Instructions can be found at documentation: - -https://isardvdi.readthedocs.io/en/latest/admin/grafana/ diff --git a/dockers/remote-grafana/docker-compose.yml b/dockers/remote-grafana/docker-compose.yml deleted file mode 100644 index 08cd568e4..000000000 --- a/dockers/remote-grafana/docker-compose.yml +++ /dev/null @@ -1,40 +0,0 @@ -version: "3.2" -services: - isard-grafana: - volumes: - - type: bind - source: /opt/isard/grafana/grafana/data - target: /grafana/data - read_only: false - #~ - type: bind - #~ source: /opt/isard/grafana/graphite/storage - #~ target: /opt/graphite/storage - #~ read_only: false - #~ - type: bind - #~ source: /opt/isard/grafana/graphite/conf - #~ target: /opt/graphite/conf - #~ read_only: false - ports: - - target: 3000 - published: 3000 - protocol: tcp - mode: host - - target: 8080 - published: 8081 - protocol: tcp - mode: host - - target: 2003 - published: 2003 - protocol: tcp - mode: host - - target: 2004 - published: 2004 - protocol: tcp - mode: host - - target: 7002 - published: 7002 - protocol: tcp - mode: host - image: isard/grafana:1.1 - restart: always - diff --git a/dockers/remote-hyper/README.md b/extras/remote-hyper/README.md similarity index 100% rename from dockers/remote-hyper/README.md rename to extras/remote-hyper/README.md diff --git a/dockers/remote-hyper/docker-compose.yml b/extras/remote-hyper/docker-compose.yml similarity index 100% rename from dockers/remote-hyper/docker-compose.yml rename to extras/remote-hyper/docker-compose.yml From 7affad3e824e21c9cdbab8ab458580555b72c805 Mon Sep 17 00:00:00 2001 From: beto Date: Sat, 26 Jan 2019 21:17:20 +0100 Subject: [PATCH 73/92] percents download updated" --- .../services/threads/download_thread.py | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/engine/services/threads/download_thread.py b/src/engine/services/threads/download_thread.py index 2fee57eba..b9439c1f0 100644 --- a/src/engine/services/threads/download_thread.py +++ b/src/engine/services/threads/download_thread.py @@ -75,7 +75,7 @@ def run(self): hyp_to_disk_create = get_host_disk_operations_from_path(path_selected, pool=self.pool_id, type_path=self.type_path_selected) - logs.downloads.debug(f'Thread download started to url: {url} in hypervisor: {hyp_to_disk_create}') + logs.downloads.debug(f'Thread download started to in hypervisor: {hyp_to_disk_create}') if self.manager.t_disk_operations.get(hyp_to_disk_create,False) is not False: if self.manager.t_disk_operations[hyp_to_disk_create].is_alive(): d = get_hyp_hostname_user_port_from_id(hyp_to_disk_create) @@ -189,7 +189,6 @@ def run(self): break if c == '\r': if len(line) > 60: - logs.downloads.debug(line) values = line.split() logs.downloads.debug(self.url) logs.downloads.debug(line) @@ -202,8 +201,8 @@ def run(self): except: d_progress['total_percent'] = 0 d_progress['received_percent'] = 0 - update_download_percent(d_progress, self.table, self.id) - line = p.stderr.read(60).decode('utf8') + update_download_percent(d_progress, self.table, self.id) + line = p.stderr.read(60).decode('utf8') else: line = line + c @@ -440,6 +439,56 @@ def run(self): elif c['old_val']['status'] == 'Downloading' and c['new_val']['status'] == 'DownloadAborting': self.abort_download(c['new_val']) +KEYS = ['total_percent', + 'total', + 'received_percent', + 'received', + 'xferd_percent', + 'xferd', + 'speed_download_average', + 'speed_upload_average', + 'time_total', + 'time_spent', + 'time_left', + 'speed_current'] + +class CurlRunning(): + def __init__(self,subprocess_object): + self.p = subprocess_object + self.stop = False + + def stop_local_curl(self): + pass + + def update_stats(self): + line = '' + while True: + c = p.stderr.read(1).decode('utf8') + if not c: + break + if c == '\r': + if len(line) > 60: + values = line.split() + #logs.downloads.debug(line) + print(line) + d_progress = dict(zip(KEYS, values)) + try: + d_progress['total_percent'] = int(float(d_progress['total_percent'])) + d_progress['received_percent'] = int(float(d_progress['received_percent'])) + if d_progress['received_percent'] > 1: + pass + except: + d_progress['total_percent'] = 0 + d_progress['received_percent'] = 0 + #update_download_percent(d_progress, self.table, self.id) + #line = p.stderr.read(60).decode('utf8') + pprint(d_progress) + line = '' + + else: + line = line + c + + #rc = p.poll() def launch_thread_download_changes(manager): t = DownloadChangesThread(manager) From d0139d118ac3576e0008ca0937001a8937ff769c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sat, 26 Jan 2019 23:24:24 +0100 Subject: [PATCH 74/92] New reset passwd button --- src/webapp/lib/admin_api.py | 7 ++- src/webapp/lib/isardSocketio.py | 19 ++++++-- src/webapp/static/admin/js/users.js | 45 ++++++++++++------- .../templates/admin/pages/users_detail.html | 1 + .../templates/admin/pages/users_modals.html | 12 ++--- 5 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/webapp/lib/admin_api.py b/src/webapp/lib/admin_api.py index f5da9bd11..8e8392440 100644 --- a/src/webapp/lib/admin_api.py +++ b/src/webapp/lib/admin_api.py @@ -221,7 +221,7 @@ def user_edit(self,user): # ~ d': 'prova', 'password': 'prova', 'name': 'prova', # ~ 'quota': {'hardware': {'vcpus': 1, 'memory': 1000}, # ~ 'domains': {'templates': 1, 'running': 1, 'isos': 1, 'desktops': 1}}} - p = Password() + # ~ p = Password() #### Removed kind. Kind cannot be modified, so the update will #### not interfere with this field usr = {'active': True, @@ -240,6 +240,11 @@ def user_edit(self,user): user['quota']['domains']={**qdomains, **user['quota']['domains']} return self.check(r.table('users').update(user).run(db.conn),'replaced') + def user_passwd(self,user): + p = Password() + usr = {'password': p.encrypt(user['password'])} + return self.check(r.table('users').update(usr).run(db.conn),'replaced') + def user_toggle_active(self,id): with app.app_context(): is_active = not r.table('users').get(id).pluck('active').run(db.conn)['active'] diff --git a/src/webapp/lib/isardSocketio.py b/src/webapp/lib/isardSocketio.py index 9629fa703..4eed99216 100644 --- a/src/webapp/lib/isardSocketio.py +++ b/src/webapp/lib/isardSocketio.py @@ -587,18 +587,29 @@ def socketio_user_add(form_data): @socketio.on('user_edit', namespace='/sio_admins') def socketio_user_edit(form_data): if current_user.role == 'admin': - # ~ create_dict=app.isardapi.f.unflatten_dict(form_data) - # ~ print(create_dict) res=app.adminapi.user_edit(form_data) if res is True: - data=json.dumps({'result':True,'title':'New user','text':'User '+form_data['name']+' has been created...','icon':'success','type':'success'}) + data=json.dumps({'result':True,'title':'User edit','text':'User '+form_data['name']+' has been updated...','icon':'success','type':'success'}) else: - data=json.dumps({'result':False,'title':'New user','text':'User '+form_data['name']+' can\'t be created. Maybe it already exists!','icon':'warning','type':'error'}) + data=json.dumps({'result':False,'title':'User edit','text':'User '+form_data['name']+' can\'t be updated!','icon':'warning','type':'error'}) socketio.emit('add_form_result', data, namespace='/sio_admins', room='users') +@socketio.on('user_passwd', namespace='/sio_admins') +def socketio_user_passwd(form_data): + if current_user.role == 'admin': + res=app.adminapi.user_passwd(form_data) + if res is True: + data=json.dumps({'result':True,'title':'User edit','text':'User '+form_data['name']+' has been updated...','icon':'success','type':'success'}) + else: + data=json.dumps({'result':False,'title':'User edit','text':'User '+form_data['name']+' can\'t be updated!','icon':'warning','type':'error'}) + socketio.emit('add_form_result', + data, + namespace='/sio_admins', + room='users') + @socketio.on('user_delete', namespace='/sio_admins') def socketio_user_delete(form_data): if current_user.role == 'admin': diff --git a/src/webapp/static/admin/js/users.js b/src/webapp/static/admin/js/users.js index b77d11589..49364b714 100644 --- a/src/webapp/static/admin/js/users.js +++ b/src/webapp/static/admin/js/users.js @@ -59,21 +59,18 @@ $(document).ready(function() { } }); - //~ $("#modalEditUser #send").on('click', function(e){ - //~ var form = $('#modalEditUserForm'); - //~ data=quota2dict($('#modalEditUserForm').serializeObject()); - //~ console.log(data) - //~ form.parsley().validate(); - //~ if (form.parsley().isValid()){ - - //~ data=quota2dict($('#modalEditUserForm').serializeObject()); - //~ delete data['password2'] - //~ data['id']=data['username']=$('#modalEditUserForm #id').val(); - //~ console.log(data) - //~ socket.emit('user_edit',data) - //~ } - //~ }); - + $("#modalPasswdUser #send").on('click', function(e){ + var form = $('#modalPasswdUserForm'); + form.parsley().validate(); + if (form.parsley().isValid()){ + data={} + data['id']=data['username']=$('#modalPasswdUserForm #id').val(); + data['name']=$('#modalPasswdUserForm #name').val(); + data['password']=$('#modalPasswdUserForm #password').val(); + socket.emit('user_passwd',data) + } + }); + $("#modalDeleteUser #send").on('click', function(e){ var form = $('#modalDeleteUserForm'); data=$('#modalDeleteUserForm').serializeObject(); @@ -345,6 +342,24 @@ function actionsUserDetail(){ //~ modal_edit_desktop_datatables(pk); }); + $('.btn-passwd').on('click', function () { + //~ setQuotaOptions('#edit-users-quota'); + var closest=$(this).closest("div"); + var pk=closest.attr("data-pk"); + var name=closest.attr("data-name"); + //~ var user=closest.attr("data-user"); + $("#modalPasswdUserForm")[0].reset(); + $('#modalPasswdUser').modal({ + backdrop: 'static', + keyboard: false + }).modal('show'); + $('#modalPasswdUserForm #name').val(name); + $('#modalPasswdUserForm #id').val(pk); + //~ $('#hardware-block').hide(); + //~ $('#modalEdit').parsley(); + //~ modal_edit_desktop_datatables(pk); + }); + $('.btn-delete').on('click', function () { //~ setQuotaOptions('#edit-users-quota'); var pk=$(this).closest("div").attr("data-pk"); diff --git a/src/webapp/templates/admin/pages/users_detail.html b/src/webapp/templates/admin/pages/users_detail.html index a64a3a2fb..eda771d1c 100644 --- a/src/webapp/templates/admin/pages/users_detail.html +++ b/src/webapp/templates/admin/pages/users_detail.html @@ -29,6 +29,7 @@

    d.name

    +
    diff --git a/src/webapp/templates/admin/pages/users_modals.html b/src/webapp/templates/admin/pages/users_modals.html index 9d8c25503..875b8f2d2 100644 --- a/src/webapp/templates/admin/pages/users_modals.html +++ b/src/webapp/templates/admin/pages/users_modals.html @@ -214,27 +214,27 @@

    Boots

    - - + {% endblock %} - -{% include '/snippets/alloweds_form.html' %} diff --git a/src/webapp/views/AllowedsViews.py b/src/webapp/views/AllowedsViews.py index 64eb41f98..ea4898bcd 100644 --- a/src/webapp/views/AllowedsViews.py +++ b/src/webapp/views/AllowedsViews.py @@ -66,8 +66,8 @@ def domains_hadware(): @login_required @ownsid def alloweds_table(table): - if table in ['domains','media']: - return json.dumps(app.isardapi.get_alloweds_select2(app.adminapi.get_admin_table(table, pluck=['allowed'], id=request.get_json(force=True)['pk'], flatten=False)['allowed'])) + # ~ if table in ['domains','media']: + return json.dumps(app.isardapi.get_alloweds_select2(app.adminapi.get_admin_table(table, pluck=['allowed'], id=request.get_json(force=True)['pk'], flatten=False)['allowed'])) # Gets all list of roles, categories, groups and users from a 2+ chars term From f6a84c5130df650920c4d56d168410c888483642 Mon Sep 17 00:00:00 2001 From: beto Date: Sun, 27 Jan 2019 02:44:40 +0100 Subject: [PATCH 78/92] parents chain solved --- src/engine/controllers/ui_actions.py | 4 ++++ src/engine/services/threads/threads.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/engine/controllers/ui_actions.py b/src/engine/controllers/ui_actions.py index 69a78b1f2..0052209fd 100644 --- a/src/engine/controllers/ui_actions.py +++ b/src/engine/controllers/ui_actions.py @@ -742,6 +742,9 @@ def creating_and_test_xml_start(self, id_domain, creating_from_create_dict=False id_template = domain['create_dict']['origin'] template = get_domain(id_template) xml_from = template['xml'] + parents_chain = template['parents'] + domain['parents'] + update_table_field('domains', id_domain, 'parents', parents_chain) + elif xml_from_virt_install is True: xml_from = domain['xml_virt_install'] @@ -751,6 +754,7 @@ def creating_and_test_xml_start(self, id_domain, creating_from_create_dict=False update_table_field('domains', id_domain, 'xml', xml_from) + xml_raw = update_xml_from_dict_domain(id_domain) if xml_raw is False: update_domain_status(status='FailedCreatingDomain', diff --git a/src/engine/services/threads/threads.py b/src/engine/services/threads/threads.py index 9add0cadd..ec1e96a4d 100644 --- a/src/engine/services/threads/threads.py +++ b/src/engine/services/threads/threads.py @@ -153,7 +153,7 @@ def launch_action_disk(action, hostname, user, port, from_scratch=False): # ahora ya se puede llamar a starting paused if id_domain is not False: #update parents if have - update_domain_parents(id_domain) + #update_domain_parents(id_domain) update_domain_status('CreatingDomain', id_domain, None, detail='new disk created, now go to creating desktop and testing if desktop start') else: From a6a9dfba50185ec4b18dcbf500b6b9e2476900e8 Mon Sep 17 00:00:00 2001 From: beto Date: Sun, 27 Jan 2019 16:44:39 +0100 Subject: [PATCH 79/92] fixing downloads bugs --- src/engine/controllers/broom.py | 3 +- src/engine/models/manager_hypervisors.py | 3 + src/engine/services/db/db.py | 19 +++- src/engine/services/db/domains.py | 32 +++--- src/engine/services/lib/functions.py | 3 +- .../services/threads/download_thread.py | 102 ++++++++++++++---- .../services/threads/hyp_worker_thread.py | 12 ++- src/engine/services/threads/threads.py | 21 +++- 8 files changed, 149 insertions(+), 46 deletions(-) diff --git a/src/engine/controllers/broom.py b/src/engine/controllers/broom.py index 896eafea0..362c68d9a 100644 --- a/src/engine/controllers/broom.py +++ b/src/engine/controllers/broom.py @@ -37,9 +37,10 @@ def polling(self): interval += 0.1 if self.stop is True: break - if self.manager.check_actions_domains_enabled(): + if self.manager.check_actions_domains_enabled() is False: continue + l = get_domains_with_transitional_status() list_domains_without_hyp = [d for d in l if 'hyp_started' not in d.keys()] diff --git a/src/engine/models/manager_hypervisors.py b/src/engine/models/manager_hypervisors.py index e39211265..a8b3fa7a9 100644 --- a/src/engine/models/manager_hypervisors.py +++ b/src/engine/models/manager_hypervisors.py @@ -297,6 +297,9 @@ def run(self): logs.main.info('starting thread background: {} (TID {})'.format(self.name, self.tid)) q = self.manager.q.background first_loop = True + pool_id = 'default' + #can't launch downloads if download changes thread is not ready and hyps are not online + update_table_field('hypervisors_pools', pool_id, 'download_changes', 'Stopped') # if domains have intermedite states (updating, download_aborting...) # to Failed or Delete diff --git a/src/engine/services/db/db.py b/src/engine/services/db/db.py index 445177580..6442560fe 100644 --- a/src/engine/services/db/db.py +++ b/src/engine/services/db/db.py @@ -381,4 +381,21 @@ def remove_media(id): result = rtable.get(id).delete().run(r_conn) close_rethink_connection(r_conn) - return result \ No newline at end of file + return result + +def get_media_with_status(status): + """ + get media with status + :param status + :return: list id_domains + """ + r_conn = new_rethink_connection() + rtable = r.table('media') + try: + results = rtable.get_all(status, index='status').pluck('id').run(r_conn) + close_rethink_connection(r_conn) + except: + # if results is None: + close_rethink_connection(r_conn) + return [] + return [d['id'] for d in results] \ No newline at end of file diff --git a/src/engine/services/db/domains.py b/src/engine/services/db/domains.py index 7a23dddd9..35c623b15 100644 --- a/src/engine/services/db/domains.py +++ b/src/engine/services/db/domains.py @@ -197,22 +197,22 @@ def get_domain_hyp_started_and_status_and_detail(id_domain): # return results -# def get_domains_with_status(status): -# """ -# NOT USED -# :param status: -# :return: -# """ -# r_conn = new_rethink_connection() -# rtable = r.table('domains') -# try: -# results = rtable.get_all(status, index='status').pluck('id').run(r_conn) -# close_rethink_connection(r_conn) -# except: -# # if results is None: -# close_rethink_connection(r_conn) -# return [] -# return [d['id'] for d in results] +def get_domains_with_status(status): + """ + get domain with status + :param status + :return: list id_domains + """ + r_conn = new_rethink_connection() + rtable = r.table('domains') + try: + results = rtable.get_all(status, index='status').pluck('id').run(r_conn) + close_rethink_connection(r_conn) + except: + # if results is None: + close_rethink_connection(r_conn) + return [] + return [d['id'] for d in results] def get_domains_with_transitional_status(list_status=TRANSITIONAL_STATUS): diff --git a/src/engine/services/lib/functions.py b/src/engine/services/lib/functions.py index 70293d7e9..db2421ab0 100644 --- a/src/engine/services/lib/functions.py +++ b/src/engine/services/lib/functions.py @@ -1066,7 +1066,8 @@ def engine_restart(): return True def clean_intermediate_status(): - status_to_delete = ['DownloadAborting'] + #status_to_delete = ['DownloadAborting'] + status_to_delete = [] status_to_failed = ['Updating'] all_domains = get_all_domains_with_id_and_status() diff --git a/src/engine/services/threads/download_thread.py b/src/engine/services/threads/download_thread.py index e5bc2ac49..c2705a791 100644 --- a/src/engine/services/threads/download_thread.py +++ b/src/engine/services/threads/download_thread.py @@ -24,6 +24,10 @@ from engine.services.lib.qcow import get_host_disk_operations_from_path, get_path_to_disk, create_cmds_delete_disk from engine.services.lib.functions import get_tid from engine.services.lib.download import test_url_for_download +from engine.services.db.domains import get_domains_with_status +from engine.services.db.db import get_media_with_status +from engine.services.db.hypervisors import get_hypers_in_pool + URL_DOWNLOAD_INSECURE_SSL = True TIMEOUT_WAITING_HYPERVISOR_TO_DOWNLOAD = 10 @@ -92,9 +96,9 @@ def run(self): logs.downloads.info( f'Timeout ({TIMEOUT_WAITING_HYPERVISOR_TO_DOWNLOAD} sec) waiting hypervisor online to download {url_base}') if self.table == 'domains': - update_domain_status('FailedDownload', self.id, detail="downloaded disk") + update_domain_status('DownloadFailed', self.id, detail="downloaded disk") else: - update_status_table(self.table, 'FailedDownload', self.id) + update_status_table(self.table, 'DownloadFailed', self.id) self.finalished_threads.append(self.path) return False @@ -120,7 +124,7 @@ def run(self): if ok is False: logs.downloads.error(f'URL check failed for url: {self.url}') logs.downloads.error(f'Failed url check reason: {error_msg}') - update_status_table(self.table, 'FailedDownload', self.id, detail=error_msg) + update_status_table(self.table, 'DownloadFailed', self.id, detail=error_msg) return False @@ -205,7 +209,7 @@ def run(self): remove_media(self.id) if self.table == 'domains': delete_domain(self.id) - # update_status_table(self.table, 'FailedDownload', self.id, detail="download aborted") + # update_status_table(self.table, 'DownloadFailed', self.id, detail="download aborted") return False if not c: break @@ -299,28 +303,40 @@ def get_file_path(self, dict_changes): type_path=type_path_selected) return new_file_path, path_selected, type_path_selected, pool_id - def abort_download(self, dict_changes): + def killall_curl(self,hyp_id): + action = dict() + action['type'] = 'killall_curl' + + pool_id = 'default' + self.manager.q.workers[hyp_id].put(action) + + def abort_download(self, dict_changes,final_status='Deleted'): logs.downloads.debug('aborting download function') new_file_path, path_selected, type_path_selected, pool_id = self.get_file_path(dict_changes) if new_file_path in self.download_threads.keys(): self.download_threads[new_file_path].stop = True else: - update_status_table(dict_changes['table'], 'FailedDownload', dict_changes['id']) + update_status_table(dict_changes['table'], 'DownloadFailed', dict_changes['id']) # and delete partial download cmds = create_cmds_delete_disk(new_file_path) # change for other pools when pools are implemented in all media - pool_id = 'default' - next_hyp = self.manager.pools[pool_id].get_next() - logs.downloads.debug('hypervisor where delete media {}: {}'.format(new_file_path, next_hyp)) - - action = dict() - action['id_media'] = dict_changes['id'] - action['path'] = new_file_path - action['type'] = 'delete_media' - action['ssh_commands'] = cmds - - self.manager.q.workers[next_hyp].put(action) + try: + pool_id = 'default' + next_hyp = self.manager.pools[pool_id].get_next() + logs.downloads.debug('hypervisor where delete media {}: {}'.format(new_file_path, next_hyp)) + + action = dict() + action['id_media'] = dict_changes['id'] + action['path'] = new_file_path + action['type'] = 'delete_media' + action['final_status'] = final_status + action['ssh_commands'] = cmds + + self.manager.q.workers[next_hyp].put(action) + return True + except Exception as e: + logs.downloads.error('next hypervisor fail: ' + str(e)) def delete_media(self, dict_changes): table = dict_changes['table'] @@ -409,11 +425,49 @@ def start_download(self, dict_changes): def run(self): self.tid = get_tid() logs.downloads.debug('RUN-DOWNLOAD-THREAD-------------------------------------') + pool_id = 'default' + first_loop = True if self.stop is False: + if first_loop is True: + # if domains or media have status Downloading when engine restart + # we need to resetdownloading deleting file and + first_loop = False + # wait a hyp to downloads + next_hyp = False + while next_hyp is False: + logs.downloads.info('waiting an hypervisor online to launch downloading actions') + if pool_id in self.manager.pools.keys(): + next_hyp = self.manager.pools[pool_id].get_next() + sleep(1) + + for hyp_id in get_hypers_in_pool(): + self.killall_curl(hyp_id) + + domains_status_downloading = get_domains_with_status('Downloading') + medias_status_downloading = get_media_with_status('Downloading') + + for id_domain in domains_status_downloading: + create_dict = get_domain(id_domain)['create_dict'] + dict_changes = {'id': id_domain, + 'table': 'domains', + 'create_dict': create_dict} + update_domain_status('ResetDownloading', id_domain) + self.abort_download(dict_changes, final_status='DownloadFailed') + + for id_media in medias_status_downloading: + dict_media = get_media(id_media) + dict_changes = {'id': id_media, + 'table': 'media', + 'path': dict_media['path'], + 'hypervisors_pools': dict_media['hypervisors_pools']} + update_status_table('media', 'ResetDownloading', id_media) + self.abort_download(dict_changes, final_status='DownloadFailed') + self.r_conn = new_rethink_connection() + update_table_field('hypervisors_pools',pool_id,'download_changes','Started') for c in r.table('media').get_all(r.args( - ['Deleting', 'Deleted', 'Downloaded', 'DownloadStarting', 'Downloading', 'Download', - 'DownloadAborting']), index='status'). \ + ['Deleting', 'Deleted', 'Downloaded', 'DownloadFailed', 'DownloadStarting', 'Downloading', 'Download', + 'DownloadAborting','ResetDownloading']), index='status'). \ pluck('id', 'path', 'url-isard', @@ -422,7 +476,7 @@ def run(self): ).merge( {'table': 'media'}).changes(include_initial=True).union( r.table('domains').get_all( - r.args(['Downloaded', 'DownloadStarting', 'Downloading', 'DownloadAborting']), index='status'). \ + r.args(['Downloaded', 'DownloadFailed','DownloadStarting', 'Downloading', 'DownloadAborting','ResetDownloading']), index='status'). \ pluck('id', 'create_dict', 'url-isard', @@ -452,7 +506,7 @@ def run(self): self.remove_download_thread(c['old_val']) elif 'old_val' in c and 'new_val' in c: - if c['old_val']['status'] == 'FailedDownload' and c['new_val']['status'] == 'DownloadStarting': + if c['old_val']['status'] == 'DownloadFailed' and c['new_val']['status'] == 'DownloadStarting': self.start_download(c['new_val']) elif c['old_val']['status'] == 'Downloaded' and c['new_val']['status'] == 'Deleting': @@ -463,7 +517,7 @@ def run(self): if c['new_val']['table'] == 'media': remove_media(c['new_val']['id']) - elif c['old_val']['status'] == 'Downloading' and c['new_val']['status'] == 'FailedDownload': + elif c['old_val']['status'] == 'Downloading' and c['new_val']['status'] == 'DownloadFailed': pass elif c['old_val']['status'] == 'DownloadStarting' and c['new_val']['status'] == 'Downloading': @@ -475,6 +529,10 @@ def run(self): elif c['old_val']['status'] == 'Downloading' and c['new_val']['status'] == 'DownloadAborting': self.abort_download(c['new_val']) + elif c['old_val']['status'] == 'Downloading' and c['new_val']['status'] == 'ResetDownloading': + self.abort_download(c['new_val'], final_status='DownloadFailed') + + def launch_thread_download_changes(manager): t = DownloadChangesThread(manager) diff --git a/src/engine/services/threads/hyp_worker_thread.py b/src/engine/services/threads/hyp_worker_thread.py index 4b627f691..9c359f835 100644 --- a/src/engine/services/threads/hyp_worker_thread.py +++ b/src/engine/services/threads/hyp_worker_thread.py @@ -18,7 +18,7 @@ from engine.services.lib.functions import get_tid, engine_restart from engine.services.log import logs from engine.services.threads.threads import TIMEOUT_QUEUES, launch_action_disk, RETRIES_HYP_IS_ALIVE, \ - TIMEOUT_BETWEEN_RETRIES_HYP_IS_ALIVE, launch_delete_media + TIMEOUT_BETWEEN_RETRIES_HYP_IS_ALIVE, launch_delete_media, launch_killall_curl from engine.models.domain_xml import XML_SNIPPET_CDROM, XML_SNIPPET_DISK_VIRTIO, XML_SNIPPET_DISK_CUSTOM class HypWorkerThread(threading.Thread): @@ -171,13 +171,19 @@ def run(self): elif action['type'] in ['add_media_hot']: pass - + elif action['type'] in ['killall_curl']: + launch_killall_curl(self.hostname, + user, + port) elif action['type'] in ['delete_media']: + final_status = action.get('final_status','Deleted') + launch_delete_media (action, self.hostname, user, - port) + port, + final_status=final_status) # ## DESTROY THREAD # elif action['type'] == 'destroy_thread': diff --git a/src/engine/services/threads/threads.py b/src/engine/services/threads/threads.py index ec1e96a4d..85b653378 100644 --- a/src/engine/services/threads/threads.py +++ b/src/engine/services/threads/threads.py @@ -108,7 +108,21 @@ def launch_action_delete_disk(action, hostname, user, port): if len([k['err'] for k in array_out_err if len(k['err']) == 1]): log.debug('all operations deleting disk {} for domain {} runned ok'.format(disk_path, id_domain)) -def launch_delete_media(action,hostname,user,port): +def launch_killall_curl(hostname,user,port): + ssh_commands = ['killall curl'] + try: + array_out_err = execute_commands(hostname, + ssh_commands=ssh_commands, + user=user, + port=port) + out = array_out_err[0]['out'] + err = array_out_err[0]['err'] + logs.downloads.info(f'kill al curl process in hypervisor {hostname}: {out} {err}') + return True + except Exception as e: + logs.downloads.error(f'Kill all curl process in hypervisor {hostname} fail: {e}') + +def launch_delete_media(action,hostname,user,port,final_status='Deleted'): array_out_err = execute_commands(hostname, ssh_commands=action['ssh_commands'], user=user, @@ -121,7 +135,10 @@ def launch_delete_media(action,hostname,user,port): return False # ls of the file after deleted failed, has deleted ok elif len(array_out_err[2]['err']) > 0: - update_status_media_from_path(path, 'Deleted') + if final_status == 'DownloadFailed': + update_status_media_from_path(path, final_status) + else: + update_status_media_from_path(path, 'Deleted') return True else: log.error('failed deleting media {}'.format(id_media)) From e32e9427bed26ce0c5837ddbcdf39421dfc1d254 Mon Sep 17 00:00:00 2001 From: beto Date: Sun, 27 Jan 2019 19:32:07 +0100 Subject: [PATCH 80/92] bug engine fixed and grafana config from db --- src/engine/api/__init__.py | 4 + src/engine/config.py | 6 +- src/engine/controllers/ui_actions.py | 4 +- src/engine/models/manager_hypervisors.py | 3 + src/engine/services/threads/grafana_thread.py | 131 ++++++++++-------- 5 files changed, 86 insertions(+), 62 deletions(-) diff --git a/src/engine/api/__init__.py b/src/engine/api/__init__.py index cba423a04..240b877e1 100644 --- a/src/engine/api/__init__.py +++ b/src/engine/api/__init__.py @@ -103,10 +103,14 @@ def engine_restart(): break return jsonify({'engine_restart':True}), 200 +@api.route('/grafana/restart', methods=['GET']) +def grafana_restart(): + app.m.t_grafana.restart_send_config = True @api.route('/engine/status') def engine_status(): '''all main threads are running''' + pass diff --git a/src/engine/config.py b/src/engine/config.py index 95825d21d..afb391f4e 100644 --- a/src/engine/config.py +++ b/src/engine/config.py @@ -107,12 +107,12 @@ 'TIMEOUTS':rconfig['timeouts'], 'REMOTEOPERATIONS':{ -'host_remote_disk_operatinos': 'vdesktop1.escoladeltreball.org', -'default_group_dir': '/vimet/groups/a' +'host_remote_disk_operatinos': 'localhost', +'default_group_dir': '/opt/isard/groups/' }, 'FERRARY':{ 'prefix': '__f_', -'dir_to_ferrary_disks': '/vimet/groups/ferrary' +'dir_to_ferrary_disks': '/opt/isard/groups/ferrary' } } diff --git a/src/engine/controllers/ui_actions.py b/src/engine/controllers/ui_actions.py index 0052209fd..f28cf162c 100644 --- a/src/engine/controllers/ui_actions.py +++ b/src/engine/controllers/ui_actions.py @@ -742,7 +742,7 @@ def creating_and_test_xml_start(self, id_domain, creating_from_create_dict=False id_template = domain['create_dict']['origin'] template = get_domain(id_template) xml_from = template['xml'] - parents_chain = template['parents'] + domain['parents'] + parents_chain = template.get('parents',[]) + domain.get('parents',[]) update_table_field('domains', id_domain, 'parents', parents_chain) @@ -846,7 +846,7 @@ def domain_from_template(self, old_path_disk = dict_domain_template['hardware']['disks'][0]['file'] old_path_dir = extract_dir_path(old_path_disk) - DEFAULT_GROUP_DIR = CONFIG_DICT['REMOTEOPERATIONS']['default_group_dir'] + #DEFAULT_GROUP_DIR = CONFIG_DICT['REMOTEOPERATIONS']['default_group_dir'] if path_to_disk_dir is None: path_to_disk_dir = DEFAULT_GROUP_DIR + '/' + \ diff --git a/src/engine/models/manager_hypervisors.py b/src/engine/models/manager_hypervisors.py index a8b3fa7a9..83b6a548d 100644 --- a/src/engine/models/manager_hypervisors.py +++ b/src/engine/models/manager_hypervisors.py @@ -162,6 +162,8 @@ def stop_threads(self): self.q_disk_operations + #self.t_downloads_changes.stop = True + # changes @@ -327,6 +329,7 @@ def run(self): # - downloads_changes # - broom # - events + # - grafana # Threads that depends on hypervisors availavility: # - disk_operations diff --git a/src/engine/services/threads/grafana_thread.py b/src/engine/services/threads/grafana_thread.py index 5580b3aa5..86238551d 100644 --- a/src/engine/services/threads/grafana_thread.py +++ b/src/engine/services/threads/grafana_thread.py @@ -10,6 +10,7 @@ from engine.services.lib.functions import get_tid, flatten_dict from engine.services.db import get_hyp_hostnames_online from engine.services.lib.grafana import send_dict_to_grafana +from engine.services.db.config import get_config SEND_TO_GRAFANA_INTERVAL = 5 SEND_STATIC_VALUES_INTERVAL = 30 @@ -27,22 +28,35 @@ def __init__(self, name,d_threads_status): self.name = name self.stop = False self.t_status = d_threads_status + self.restart_send_config = False + self.active = False + self.send_to_grafana_interval = SEND_TO_GRAFANA_INTERVAL + self.send_static_values_interval = SEND_STATIC_VALUES_INTERVAL + self.host_grafana = False + self.port = False + def get_hostname_grafana(self): - dict_grafana = { - "active": True, - "carbon_port": 2004, - "hostname": "isard-grafana", - "interval": 5, - } - if dict_grafana["active"] is not True: + try: + dict_grafana = get_config()['engine']['grafana'] + + if dict_grafana["active"] is not True: + self.active = False + return False + else: + self.host_grafana = dict_grafana["hostname"] + self.port = dict_grafana["carbon_port"] + self.send_static_values_interval = dict_grafana.get('send_static_values_interval', + SEND_STATIC_VALUES_INTERVAL) + self.send_to_grafana_interval = dict_grafana.get('interval', + SEND_TO_GRAFANA_INTERVAL) + + return True + except Exception as e: + logs.main.error(f'grafana config error: {e}') + self.active = False return False - else: - self.host_grafana = dict_grafana["hostname"] - self.port = dict_grafana["carbon_port"] - self.interval = dict_grafana["interval"] - return True def send(self,d): send_dict_to_grafana(d, self.host_grafana, self.port) @@ -52,57 +66,60 @@ def run(self): logs.main.info('starting thread: {} (TID {})'.format(self.name, self.tid)) #get hostname grafana config - if self.get_hostname_grafana() is not True: - return False + self.get_hostname_grafana() hyps_online = [] - elapsed = SEND_STATIC_VALUES_INTERVAL + elapsed = self.send_static_values_interval while self.stop is False: - sleep(SEND_TO_GRAFANA_INTERVAL) - elapsed += SEND_TO_GRAFANA_INTERVAL - - - for i,id_hyp in enumerate(self.t_status.keys()): - try: - if self.t_status[id_hyp].status_obj.hyp_obj.connected is True: - if id_hyp not in hyps_online: - hyps_online.append(id_hyp) - check_hyp = True - except: - logs.main.error(f'hypervisor {id_hyp} problem checking if is connected') - check_hyp = False - - if len(hyps_online) > 0 and check_hyp is True: - #send static values of hypervisors - if elapsed >= SEND_STATIC_VALUES_INTERVAL: - d_hyps_info = dict() + sleep(self.send_to_grafana_interval) + elapsed += self.send_to_grafana_interval + + if self.restart_send_config is True: + self.restart_send_config = False + self.get_hostname_grafana() + + if self.active is True: + for i,id_hyp in enumerate(self.t_status.keys()): + try: + if self.t_status[id_hyp].status_obj.hyp_obj.connected is True: + if id_hyp not in hyps_online: + hyps_online.append(id_hyp) + check_hyp = True + except: + logs.main.error(f'hypervisor {id_hyp} problem checking if is connected') + check_hyp = False + + if len(hyps_online) > 0 and check_hyp is True: + #send static values of hypervisors + if elapsed >= self.send_static_values_interval: + d_hyps_info = dict() + for i, id_hyp in enumerate(hyps_online): + d_hyps_info[f'hyp-info-{i}'] = self.t_status[id_hyp].status_obj.hyp_obj.info + # ~ self.send(d_hyps_info) + elapsed = 0 + + #send stats + dict_to_send = dict() + j=0 for i, id_hyp in enumerate(hyps_online): - d_hyps_info[f'hyp-info-{i}'] = self.t_status[id_hyp].status_obj.hyp_obj.info - # ~ self.send(d_hyps_info) - elapsed = 0 - - #send stats - dict_to_send = dict() - j=0 - for i, id_hyp in enumerate(hyps_online): - if id_hyp in self.t_status.keys(): - #stats_hyp = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp - stats_hyp_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp_now - #stats_domains = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains - if len(stats_hyp_now) > 0: - dict_to_send[f'hypers.'+id_hyp] = {'stats':stats_hyp_now,'info':d_hyps_info['hyp-info-'+str(i)],'domains':{}} - stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now - # ~ for id_domain,d_stats in stats_domains_now.items(): - # ~ if len(stats_hyp_now) > 0: + if id_hyp in self.t_status.keys(): + #stats_hyp = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp + stats_hyp_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_hyp_now + #stats_domains = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains + if len(stats_hyp_now) > 0: + dict_to_send[f'hypers.'+id_hyp] = {'stats':stats_hyp_now,'info':d_hyps_info['hyp-info-'+str(i)],'domains':{}} + stats_domains_now = self.t_status[id_hyp].status_obj.hyp_obj.stats_domains_now # ~ for id_domain,d_stats in stats_domains_now.items(): - # ~ dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} - dict_to_send[f'hypers.'+id_hyp]['domains']=stats_domains_now #{x:0 for x in stats_domains_now} - # ~ print(stats_domains_now) - # ~ j+=1 - - if len(dict_to_send) > 0: - self.send(dict_to_send) + # ~ if len(stats_hyp_now) > 0: + # ~ for id_domain,d_stats in stats_domains_now.items(): + # ~ dict_to_send[f'domain-stats-{j}'] = {'domain-id':{id_domain:1},'last': d_stats,} + dict_to_send[f'hypers.'+id_hyp]['domains']=stats_domains_now #{x:0 for x in stats_domains_now} + # ~ print(stats_domains_now) + # ~ j+=1 + + if len(dict_to_send) > 0: + self.send(dict_to_send) From 746373a3b13f02a59be4e8642c5d3c8f0a5bb650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20Maria=20Vi=C3=B1olas?= Date: Sun, 27 Jan 2019 21:03:00 +0100 Subject: [PATCH 81/92] Updated wizard to new behaviour --- src/webapp/wizard/WizardLib.py | 27 +++--- src/webapp/wizard/templates/wizard_main.html | 88 ++++++++++++-------- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/src/webapp/wizard/WizardLib.py b/src/webapp/wizard/WizardLib.py index 366367881..89a4a3d08 100644 --- a/src/webapp/wizard/WizardLib.py +++ b/src/webapp/wizard/WizardLib.py @@ -374,6 +374,13 @@ def valid_engine(self): from ..lib.load_config import load_config dict=load_config()['DEFAULT_HYPERVISORS'] valid_engine=self.valid_server('isard-engine:5555' if 'isard-hypervisor' in dict.keys() else 'localhost:5555') + if valid_engine: + try: + status = r.db('isard').table('hypervisors_pools').get('default').pluck('download_changes').run()['download_changes'] + if status != 'Started': return False + except Exception as e: + print(e) + return False if valid_engine: if 'isard-hypervisor' in dict.keys(): url='http://isard-engine' @@ -382,6 +389,7 @@ def valid_engine(self): url='http://localhost' web_port=5555 r.db('isard').table('config').get(1).update({'engine':{'api':{'url':url,'web_port':web_port,'token':'fosdem'}}}).run() + self.callfor_updates_demo() return valid_engine def valid_hypervisor(self,remote_addr=False): @@ -389,7 +397,6 @@ def valid_hypervisor(self,remote_addr=False): if r.db('isard').table('hypervisors').filter({'status':'Online'}).pluck('status').run() is not None: # ~ if remote_addr is not False: # ~ self.update_hypervisor_viewer(remote_addr) - self.callfor_updates_demo() return True return False except: @@ -499,12 +506,12 @@ def check_steps(self): # ~ else: # ~ errors.append({'stepnum':4,'iserror':False}) - if not res['engine']: + if not res['hyper']: errors.append({'stepnum':4,'iserror':True}) else: errors.append({'stepnum':4,'iserror':False}) - if not res['hyper']: + if not res['engine']: errors.append({'stepnum':5,'iserror':True}) else: errors.append({'stepnum':5,'iserror':False}) @@ -598,10 +605,10 @@ def wizard_validate_step(step): # ~ if step is '4': # ~ return json.dumps(self.valid_server('isardvdi.com')) if step is '4': - return json.dumps(self.valid_engine()) - if step is '5': # ~ return json.dumps(self.valid_hypervisor() if self.valid_isard_database() else False) - return json.dumps(self.valid_hypervisor()) + return json.dumps(self.valid_hypervisor()) + if step is '5': + return json.dumps(self.valid_engine()) if step is '6': return json.dumps(self.valid_server('isardvdi.com')) @@ -634,13 +641,13 @@ def wizard_content(): # ~ return html[4]['ko'] # ~ return html[4]['ok'] if step == '4': + if not (self.valid_hypervisor() if self.valid_isard_database() else False): + return html[5]['ko'] + return html[5]['ok'] + if step == '5': if not self.valid_engine(): return html[4]['ko'] return html[4]['ok'] - if step == '5': - if not (self.valid_hypervisor() if self.valid_isard_database() else False): - return html[5]['ko'] - return html[5]['ok'] if step == '6': if not self.valid_server('isardvdi.com'): return html[6]['noservice'] diff --git a/src/webapp/wizard/templates/wizard_main.html b/src/webapp/wizard/templates/wizard_main.html index 29e3e7f18..74378fc6a 100644 --- a/src/webapp/wizard/templates/wizard_main.html +++ b/src/webapp/wizard/templates/wizard_main.html @@ -91,7 +91,7 @@ border-radius: 4px; background: #60c7c1; text-decoration: none; - transition: all 0.4s; + transition: all 0.1s; line-height: normal; border: none; } @@ -103,6 +103,7 @@ display: inline-block; margin: 100px auto; } + @@ -115,7 +116,8 @@

    IsardVDI

    installation wizard

@@ -159,15 +161,15 @@

installation wizard

  • - Engine
    - Backend engine + Hypervisor
    + Available hypervisors
  • - Hypervisor
    - Available hypervisors + Engine
    + Backend engine
  • @@ -201,11 +203,11 @@

    Step 4 Connection

    -->
    -

    Step 4 Engine

    +

    Step 4 Hypervisor

    -

    Step 5 Hypervisors

    +

    Step 5 Engine

    @@ -269,6 +271,10 @@