1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-26 16:15:25 +00:00

Merge pull request #2 from retspen/master

update
This commit is contained in:
aiminick 2020-12-17 23:03:24 +08:00 committed by GitHub
commit 66b1d7b8a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
651 changed files with 292340 additions and 48535 deletions

25
.dockerignore Normal file
View file

@ -0,0 +1,25 @@
**/__pycache__
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
README.md

70
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
branches-ignore: [master]
schedule:
- cron: '0 21 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript', 'python']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

89
.github/workflows/linter.yml vendored Normal file
View file

@ -0,0 +1,89 @@
###########################
###########################
## Linter GitHub Actions ##
###########################
###########################
name: Lint Code Base
#
# Documentation:
# https://help.github.com/en/articles/workflow-syntax-for-github-actions
#
#############################
# Start the job on all push #
#############################
on:
push:
branches: [master]
pull_request:
branches-ignore: [master]
###############
# Set the Job #
###############
jobs:
build:
name: Lint Code Base
# Set the agent to run on
runs-on: ubuntu-latest
##################
# Load all steps #
###################
steps:
##########################
# Checkout the code base #
##########################
- name: Checkout Code
uses: actions/checkout@v2
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install Required packages
run: |
sudo apt-get install -y python3-virtualenv libvirt-dev python3-lxml zlib1g-dev libxslt1-dev
- name: Create & Activate VENV
run: |
python3 -m venv venv
source venv/bin/activate
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip3 install wheel
if [ -f dev/requirements.txt ]; then pip3 install -r dev/requirements.txt; else pip3 install -r conf/requirements.txt; fi
################################
# Run Linter against code base #
################################
- name: Lint Code Base
uses: docker://github/super-linter:latest
env:
FILTER_REGEX_EXCLUDE: .*(static|scss|venv|locale)/.*
DEFAULT_BRANCH: master
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_ALL_CODEBASE: false
VALIDATE_ANSIBLE: false
VALIDATE_CLOJURE: false
VALIDATE_COFFEE: false
VALIDATE_DART: false
VALIDATE_GO: false
VALIDATE_JSX: false
VALIDATE_KOTLIN: false
VALIDATE_POWERSHELL: false
VALIDATE_PERL: false
VALIDATE_PHP: false
VALIDATE_RAKU: false
VALIDATE_RUBY: false
VALIDATE_TSX: false
VALIDATE_TERRAFORM: false

6
.gitignore vendored
View file

@ -1,10 +1,16 @@
.vagrant .vagrant
venv venv
venv2
.vscode
.idea .idea
.DS_* .DS_*
.webvirtcloud
*.pyc *.pyc
db.sqlite3* db.sqlite3*
console/cert.pem* console/cert.pem*
tags tags
dhcpd.* dhcpd.*
webvirtcloud/settings.py webvirtcloud/settings.py
*migrations/*
.coverage
htmlcov

5
.gitpod.yml Normal file
View file

@ -0,0 +1,5 @@
image: gitpod/workspace-full
tasks:
- init: 'echo "TODO: Replace with init/build command"'
command: 'echo "TODO: Replace with command to start project"'

View file

@ -1,15 +1,15 @@
language: python language: python
python: python:
- "2.7" - "3.6"
env: env:
- DJANGO=1.8 - DJANGO=2.2.16
install: install:
- pip install -r dev/requirements.txt --use-mirrors - pip install -r dev/requirements.txt
script: script:
- pep8 --exclude=IPy.py --ignore=E501 vrtManager accounts computes \ - pep8 --exclude=IPy.py --ignore=E501 vrtManager accounts admin appsettings computes \
console create instances interfaces \ console create datasource instances interfaces \
networks secrets storages logs networks nwfilters secrets storages
- pyflakes vrtManager accounts computes console create instances interfaces \ - pyflakes vrtManager accounts admin appsettings computes console create datasource instances interfaces \
networks secrets storages nwfilters networks secrets storages logs
- python manage.py migrate - python manage.py migrate
- python manage.py test --settings=webvirtcloud.settings-dev - python manage.py test --settings=webvirtcloud.settings-dev

View file

@ -1,50 +1,63 @@
FROM phusion/baseimage:0.9.17 FROM phusion/baseimage:18.04-1.0.0
MAINTAINER Jethro Yu <comet.jc@gmail.com>
EXPOSE 80
EXPOSE 6080
# Use baseimage-docker's init system.
CMD ["/sbin/my_init"]
RUN echo 'APT::Get::Clean=always;' >> /etc/apt/apt.conf.d/99AutomaticClean RUN echo 'APT::Get::Clean=always;' >> /etc/apt/apt.conf.d/99AutomaticClean
RUN apt-get update -qqy RUN apt-get update -qqy \
RUN DEBIAN_FRONTEND=noninteractive apt-get -qyy install \ && DEBIAN_FRONTEND=noninteractive apt-get -qyy install \
-o APT::Install-Suggests=false \ --no-install-recommends \
git python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev nginx libsasl2-modules git \
python3-venv \
python3-dev \
python3-lxml \
libvirt-dev \
zlib1g-dev \
nginx \
pkg-config \
gcc \
libsasl2-modules \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ADD . /srv/webvirtcloud COPY . /srv/webvirtcloud
RUN chown -R www-data:www-data /srv/webvirtcloud RUN chown -R www-data:www-data /srv/webvirtcloud
# Setup webvirtcloud # Setup webvirtcloud
RUN cd /srv/webvirtcloud && \ WORKDIR /srv/webvirtcloud
virtualenv venv && \ RUN python3 -m venv venv && \
. venv/bin/activate && \ . venv/bin/activate && \
pip install -U pip && \ pip3 install -U pip && \
pip install -r conf/requirements.txt && \ pip3 install wheel && \
pip3 install -r conf/requirements.txt && \
chown -R www-data:www-data /srv/webvirtcloud chown -R www-data:www-data /srv/webvirtcloud
RUN cd /srv/webvirtcloud && . venv/bin/activate && \ RUN . venv/bin/activate && \
python manage.py migrate && \ python3 manage.py migrate && \
chown -R www-data:www-data /srv/webvirtcloud chown -R www-data:www-data /srv/webvirtcloud
# Setup Nginx # Setup Nginx
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf && \ RUN printf "\n%s" "daemon off;" >> /etc/nginx/nginx.conf && \
rm /etc/nginx/sites-enabled/default && \ rm /etc/nginx/sites-enabled/default && \
chown -R www-data:www-data /var/lib/nginx chown -R www-data:www-data /var/lib/nginx
ADD conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ COPY conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
# Register services to runit # Register services to runit
RUN mkdir /etc/service/nginx && \ RUN mkdir /etc/service/nginx && \
mkdir /etc/service/nginx-log-forwarder && \ mkdir /etc/service/nginx-log-forwarder && \
mkdir /etc/service/webvirtcloud && \ mkdir /etc/service/webvirtcloud && \
mkdir /etc/service/novnc mkdir /etc/service/novnc
ADD conf/runit/nginx /etc/service/nginx/run COPY conf/runit/nginx /etc/service/nginx/run
ADD conf/runit/nginx-log-forwarder /etc/service/nginx-log-forwarder/run COPY conf/runit/nginx-log-forwarder /etc/service/nginx-log-forwarder/run
ADD conf/runit/novncd.sh /etc/service/novnc/run COPY conf/runit/novncd.sh /etc/service/novnc/run
ADD conf/runit/webvirtcloud.sh /etc/service/webvirtcloud/run COPY conf/runit/webvirtcloud.sh /etc/service/webvirtcloud/run
EXPOSE 80
EXPOSE 6080
# Define mountable directories. # Define mountable directories.
#VOLUME [] #VOLUME []
# Use baseimage-docker's init system. WORKDIR /srv/webvirtcloud
CMD ["/sbin/my_init"]

237
README.md
View file

@ -1,10 +1,21 @@
## WebVirtCloud Beta [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/retspen/webvirtcloud)
# WebVirtCloud
###### Python3 & Django 2.2
## Features ## Features
* QEMU/KVM Hypervisor Management
* QEMU/KVM Instance Management - Create, Delete, Update
* Hypervisor & Instance web based stats
* Manage Multiple QEMU/KVM Hypervisor
* Manage Hypervisor Datastore pools
* Manage Hypervisor Networks
* Instance Console Access with Browsers
* Libvirt API based web management UI
* User Based Authorization and Authentication
* User can add SSH public key to root in Instance (Tested only Ubuntu) * User can add SSH public key to root in Instance (Tested only Ubuntu)
* User can change root password in Instance (Tested only Ubuntu) * User can change root password in Instance (Tested only Ubuntu)
* Supports cloud-init datasource interface
### Warning!!! ### Warning!!!
@ -15,23 +26,38 @@ wget -O - https://clck.ru/9VMRH | sudo tee -a /usr/local/bin/gstfsd
sudo service supervisor restart sudo service supervisor restart
``` ```
### Description ## Description
WebVirtCloud is a virtualization web interface for admins and users. It can delegate Virtual Machine's to users. A noVNC viewer presents a full graphical console to the guest domain. KVM is currently the only hypervisor supported. WebVirtCloud is a virtualization web interface for admins and users. It can delegate Virtual Machine's to users. A noVNC viewer presents a full graphical console to the guest domain. KVM is currently the only hypervisor supported.
## Quick Install with Installer (Beta)
Install an OS and run specified commands. Installer supported OSes: Ubuntu 18.04, Debian 10, Centos/OEL/RHEL 8.
It can be installed on a virtual machine, physical host or on a KVM host.
```bash
wget https://raw.githubusercontent.com/retspen/webvirtcloud/master/install.sh
chmod 744 install.sh
# run with sudo or root user
./install.sh
```
## Manual Installation
### Generate secret key ### Generate secret key
You should generate SECRET_KEY after cloning repo. Then put it into webvirtcloud/settings.py. You should generate SECRET_KEY after cloning repo. Then put it into webvirtcloud/settings.py.
```python ```python3
import random, string import random, string
haystack = string.ascii_letters + string.digits + string.punctuation haystack = string.ascii_letters + string.digits + string.punctuation
print(''.join([random.SystemRandom().choice(haystack) for _ in range(50)])) print(''.join([random.SystemRandom().choice(haystack) for _ in range(50)]))
``` ```
### Install WebVirtCloud panel (Ubuntu) ### Install WebVirtCloud panel (Ubuntu 18.04+ LTS)
```bash ```bash
sudo apt-get -y install git python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev nginx supervisor libsasl2-modules gcc pkg-config sudo apt-get -y install git virtualenv python3-virtualenv python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs
git clone https://github.com/retspen/webvirtcloud git clone https://github.com/retspen/webvirtcloud
cd webvirtcloud cd webvirtcloud
cp webvirtcloud/settings.py.template webvirtcloud/settings.py cp webvirtcloud/settings.py.template webvirtcloud/settings.py
@ -42,10 +68,10 @@ cd ..
sudo mv webvirtcloud /srv sudo mv webvirtcloud /srv
sudo chown -R www-data:www-data /srv/webvirtcloud sudo chown -R www-data:www-data /srv/webvirtcloud
cd /srv/webvirtcloud cd /srv/webvirtcloud
virtualenv venv virtualenv -p python3 venv
source venv/bin/activate source venv/bin/activate
pip install -r conf/requirements.txt pip install -r conf/requirements.txt
python manage.py migrate python3 manage.py migrate
sudo chown -R www-data:www-data /srv/webvirtcloud sudo chown -R www-data:www-data /srv/webvirtcloud
sudo rm /etc/nginx/sites-enabled/default sudo rm /etc/nginx/sites-enabled/default
``` ```
@ -63,10 +89,15 @@ Setup libvirt and KVM on server
wget -O - https://clck.ru/9V9fH | sudo sh wget -O - https://clck.ru/9V9fH | sudo sh
``` ```
### Install WebVirtCloud panel (CentOS) Done!!
Go to http://serverip and you should see the login screen.
### Install WebVirtCloud panel (CentOS8/OEL8)
```bash ```bash
sudo yum -y install python-virtualenv python-devel libvirt-devel glibc gcc nginx supervisor libxml2 libxml2-devel git sudo yum -y install epel-release
sudo yum -y install python3-virtualenv python3-devel libvirt-devel glibc gcc nginx supervisor python3-lxml git python3-libguestfs iproute-tc cyrus-sasl-md5 python3-libguestfs
``` ```
#### Creating directories and cloning repo #### Creating directories and cloning repo
@ -76,19 +107,23 @@ sudo mkdir /srv && cd /srv
sudo git clone https://github.com/retspen/webvirtcloud && cd webvirtcloud sudo git clone https://github.com/retspen/webvirtcloud && cd webvirtcloud
cp webvirtcloud/settings.py.template webvirtcloud/settings.py cp webvirtcloud/settings.py.template webvirtcloud/settings.py
# now put secret key to webvirtcloud/settings.py # now put secret key to webvirtcloud/settings.py
# create secret key manually or use that command
sudo sed -r "s/SECRET_KEY = ''/SECRET_KEY = '"`python3 /srv/webvirtcloud/conf/runit/secret_generator.py`"'/" -i /srv/webvirtcloud/webvirtcloud/settings.py
``` ```
#### Start installation webvirtcloud #### Start installation webvirtcloud
```
sudo virtualenv venv ```bash
sudo source venv/bin/activate virtualenv-3 venv
sudo venv/bin/pip install -r conf/requirements.txt source venv/bin/activate
sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ pip3 install -r conf/requirements.txt
sudo venv/bin/python manage.py migrate cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
python3 manage.py migrate
``` ```
#### Configure the supervisor for CentOS #### Configure the supervisor for CentOS
Add the following after the [include] line (after **files = ... ** actually):
Add the following after the [include] line (after **files = ...** actually):
```bash ```bash
sudo vim /etc/supervisord.conf sudo vim /etc/supervisord.conf
@ -101,7 +136,7 @@ autorestart=true
redirect_stderr=true redirect_stderr=true
[program:novncd] [program:novncd]
command=/srv/webvirtcloud/venv/bin/python /srv/webvirtcloud/console/novncd command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/novncd
directory=/srv/webvirtcloud directory=/srv/webvirtcloud
user=nginx user=nginx
autostart=true autostart=true
@ -110,9 +145,10 @@ redirect_stderr=true
``` ```
#### Edit the nginx.conf file #### Edit the nginx.conf file
You will need to edit the main nginx.conf file as the one that comes from the rpm's will not work. Comment the following lines: You will need to edit the main nginx.conf file as the one that comes from the rpm's will not work. Comment the following lines:
``` ```bash
# server { # server {
# listen 80 default_server; # listen 80 default_server;
# listen [::]:80 default_server; # listen [::]:80 default_server;
@ -137,7 +173,8 @@ You will need to edit the main nginx.conf file as the one that comes from the rp
``` ```
Also make sure file in **/etc/nginx/conf.d/webvirtcloud.conf** has the proper paths: Also make sure file in **/etc/nginx/conf.d/webvirtcloud.conf** has the proper paths:
```
```bash
upstream gunicorn_server { upstream gunicorn_server {
#server unix:/srv/webvirtcloud/venv/wvcloud.socket fail_timeout=0; #server unix:/srv/webvirtcloud/venv/wvcloud.socket fail_timeout=0;
server 127.0.0.1:8000 fail_timeout=0; server 127.0.0.1:8000 fail_timeout=0;
@ -159,9 +196,9 @@ server {
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header Host $host:$server_port; proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Proto $remote_addr; proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_connect_timeout 600; proxy_connect_timeout 1800;
proxy_read_timeout 600; proxy_read_timeout 1800;
proxy_send_timeout 600; proxy_send_timeout 1800;
client_max_body_size 1024M; client_max_body_size 1024M;
} }
} }
@ -177,34 +214,48 @@ Change permission for selinux:
```bash ```bash
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/webvirtcloud(/.*)" sudo semanage fcontext -a -t httpd_sys_content_t "/srv/webvirtcloud(/.*)"
sudo setsebool -P httpd_can_network_connect on -P
``` ```
Add required user to the kvm group: Add required user to the kvm group(if you not install with root):
```bash ```bash
sudo usermod -G kvm -a webvirtmgr sudo usermod -G kvm -a <username>
```
Allow http ports on firewall:
```bash
sudo firewall-cmd --add-service=http
sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-port=6080/tcp
sudo firewall-cmd --add-port=6080/tcp --permanent
``` ```
Let's restart nginx and the supervisord services: Let's restart nginx and the supervisord services:
```bash ```bash
sudo systemctl restart nginx && systemctl restart supervisord sudo systemctl restart nginx && systemctl restart supervisord
``` ```
And finally, check everything is running: And finally, check everything is running:
```bash ```bash
sudo supervisorctl status sudo supervisorctl status
gstfsd RUNNING pid 24662, uptime 6:01:40
novncd RUNNING pid 24186, uptime 2:59:14 novncd RUNNING pid 24661, uptime 6:01:40
webvirtcloud RUNNING pid 24185, uptime 2:59:14 webvirtcloud RUNNING pid 24660, uptime 6:01:40
``` ```
#### Apache mod_wsgi configuration #### Apache mod_wsgi configuration
```
```bash
WSGIDaemonProcess webvirtcloud threads=2 maximum-requests=1000 display-name=webvirtcloud WSGIDaemonProcess webvirtcloud threads=2 maximum-requests=1000 display-name=webvirtcloud
WSGIScriptAlias / /srv/webvirtcloud/webvirtcloud/wsgi.py WSGIScriptAlias / /srv/webvirtcloud/webvirtcloud/wsgi_custom.py
``` ```
#### Install final required packages for libvirtd and others on Host Server #### Install final required packages for libvirtd and others on Host Server
```bash ```bash
wget -O - https://clck.ru/9V9fH | sudo sh wget -O - https://clck.ru/9V9fH | sudo sh
``` ```
@ -213,19 +264,133 @@ Done!!
Go to http://serverip and you should see the login screen. Go to http://serverip and you should see the login screen.
### Alternative running novncd via runit(Debian)
Alternative to running nonvcd via supervisor is runit.
On Debian systems install runit and configure novncd service:
```bash
apt install runit runit-systemd
mkdir /etc/service/novncd/
ln -s /srv/webvirtcloud/conf/runit/novncd.sh /etc/service/novncd/run
systemctl start runit.service
```
### Default credentials ### Default credentials
<pre>
```html
login: admin login: admin
password: admin password: admin
</pre> ```
### Configuring Compute SSH connection
This is a short example of configuring cloud and compute side of the ssh connection.
On the webvirtcloud machine you need to generate ssh keys and optionally disable StrictHostKeyChecking.
### How To Update
```bash ```bash
chown www-data -R ~www-data
sudo -u www-data ssh-keygen
cat > ~www-data/.ssh/config << EOF
Host *
StrictHostKeyChecking no
EOF
chown www-data -R ~www-data/.ssh/config
```
You need to put cloud public key into authorized keys on the compute node. Simpliest way of doing this is to use ssh tool from the webvirtcloud server.
```bash
sudo -u www-data ssh-copy-id root@compute1
```
### Host SMBIOS information is not available
If you see warning
```bash
Unsupported configuration: Host SMBIOS information is not available
```
Then you need to install `dmidecode` package on your host using your package manager and restart libvirt daemon.
Debian/Ubuntu like:
```bash
sudo apt-get install dmidecode
sudo service libvirt-bin restart
```
Arch Linux
```bash
sudo pacman -S dmidecode
systemctl restart libvirtd
```
### Cloud-init
Currently supports only root ssh authorized keys and hostname. Example configuration of the cloud-init client follows.
```bash
datasource:
OpenStack:
metadata_urls: [ "http://webvirtcloud.domain.com/datasource" ]
```
### Reverse-Proxy
Edit WS_PUBLIC_PORT at settings.py file to expose redirect to 80 or 443. Default: 6080
```bash
WS_PUBLIC_PORT = 80
```
## How To Update
```bash
# Go to Installation Directory
cd /srv/webvirtcloud
source venv/bin/activate
git pull git pull
python manage.py migrate pip3 install -U -r conf/requirements.txt
python3 manage.py migrate
sudo service supervisor restart sudo service supervisor restart
``` ```
### License ### Running tests
Server on which tests will be performed must have libvirt up and running.
It must not contain vms.
It must have `default` storage which not contain any disk images.
It must have `default` network which must be on.
Setup venv
```bash
python -m venv venv
source venv/bin/activate
pip install -r conf/requirements.txt
```
Run tests
```bash
python manage.py test
```
## Screenshots
Instance Detail:
<img src="doc/images/instance.PNG" width="96%" align="center"/>
Instance List:</br>
<img src="doc/images/grouped.PNG" width="43%"/>
<img src="doc/images/nongrouped.PNG" width="53%"/>
Other: </br>
<img src="doc/images/hosts.PNG" width="47%"/>
<img src="doc/images/log.PNG" width="49%"/>
## License
WebVirtCloud is licensed under the [Apache Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). WebVirtCloud is licensed under the [Apache Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).

52
Vagrantfile vendored
View file

@ -2,17 +2,53 @@
# vi: set ft=ruby : # vi: set ft=ruby :
Vagrant.configure(2) do |config| Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/trusty64" # Default machine, if name not specified...
config.vm.hostname = "webvirtcloud" config.vm.define "dev", primary: true do |dev|
config.vm.network "private_network", ip: "192.168.33.10" dev.vm.box = "ubuntu/bionic64"
config.vm.provision "shell", inline: <<-SHELL dev.vm.hostname = "webvirtcloud"
dev.vm.network "private_network", ip: "192.168.33.10"
dev.vm.provision "shell", inline: <<-SHELL
sudo sh /vagrant/dev/libvirt-bootstrap.sh sudo sh /vagrant/dev/libvirt-bootstrap.sh
sudo sed -i 's/auth_tcp = \"sasl\"/auth_tcp = \"none\"/g' /etc/libvirt/libvirtd.conf sudo sed -i 's/auth_tcp = \"sasl\"/auth_tcp = \"none\"/g' /etc/libvirt/libvirtd.conf
sudo service libvirt-bin restart sudo service libvirt-bin restart
sudo adduser vagrant libvirtd sudo adduser vagrant libvirtd
sudo apt-get -y install python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev sudo apt-get -y install python3-virtualenv virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev python3-guestfs
virtualenv /vagrant/venv virtualenv -p python3 /vagrant/venv
source /vagrant/venv/bin/activate source /vagrant/venv/bin/activate
pip install -r /vagrant/dev/requirements.txt pip3 install -r /vagrant/dev/requirements.txt
SHELL SHELL
end
# To start this machine run "vagrant up prod"
# To enter this machine run "vagrant ssh prod"
config.vm.define "prod", autostart: false do |prod|
prod.vm.box = "ubuntu/bionic64"
prod.vm.hostname = "webvirtcloud"
prod.vm.network "private_network", ip: "192.168.33.11"
prod.vm.network "forwarded_port", guest: 80, host: 8081
#prod.vm.synced_folder ".", "/srv/webvirtcloud"
prod.vm.provision "shell", inline: <<-SHELL
sudo mkdir /srv/webvirtcloud
sudo cp -R /vagrant/* /srv/webvirtcloud
sudo sh /srv/webvirtcloud/dev/libvirt-bootstrap.sh
sudo sed -i 's/auth_tcp = \"sasl\"/auth_tcp = \"none\"/g' /etc/libvirt/libvirtd.conf
sudo service libvirt-bin restart
sudo adduser vagrant libvirtd
sudo chown -R vagrant:vagrant /srv/webvirtcloud
sudo apt-get -y install python3-virtualenv python3-dev python3-lxml python3-pip virtualenv libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs
virtualenv -p python3 /srv/webvirtcloud/venv
source /srv/webvirtcloud/venv/bin/activate
pip3 install -r /srv/webvirtcloud/requirements.txt
sudo cp /srv/webvirtcloud/conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d
sudo cp /srv/webvirtcloud/conf/nginx/webvirtcloud.conf /etc/nginx/conf.d
sudo cp /srv/webvirtcloud/webvirtcloud/settings.py.template /srv/webvirtcloud/webvirtcloud/settings.py
sudo sed "s/SECRET_KEY = ''/SECRET_KEY = '"`python3 /srv/webvirtcloud/conf/runit/secret_generator.py`"'/" -i /srv/webvirtcloud/webvirtcloud/settings.py
python3 /srv/webvirtcloud/manage.py migrate
sudo rm /etc/nginx/sites-enabled/default
sudo chown -R www-data:www-data /srv/webvirtcloud
sudo service nginx restart
sudo service supervisor restart
SHELL
end
end end

1
_config.yml Normal file
View file

@ -0,0 +1 @@
theme: jekyll-theme-cayman

View file

@ -0,0 +1 @@
default_app_config = 'accounts.apps.AccountsConfig'

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

51
accounts/apps.py Normal file
View file

@ -0,0 +1,51 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate
def apply_change_password(sender, **kwargs):
'''
Apply new change_password permission for all users
Depending on settings SHOW_PROFILE_EDIT_PASSWORD
'''
from django.conf import settings
from django.contrib.auth.models import Permission, User
if hasattr(settings, 'SHOW_PROFILE_EDIT_PASSWORD'):
print('\033[1m! \033[92mSHOW_PROFILE_EDIT_PASSWORD is found inside settings.py\033[0m')
print('\033[1m* \033[92mApplying permission can_change_password for all users\033[0m')
users = User.objects.all()
permission = Permission.objects.get(codename='change_password')
if settings.SHOW_PROFILE_EDIT_PASSWORD:
print('\033[1m! \033[91mWarning!!! Setting to True for all users\033[0m')
for user in users:
user.user_permissions.add(permission)
else:
print('\033[1m* \033[91mWarning!!! Setting to False for all users\033[0m')
for user in users:
user.user_permissions.remove(permission)
print('\033[1m! Don`t forget to remove the option from settings.py\033[0m')
def create_admin(sender, **kwargs):
'''
Create initial admin user
'''
from accounts.models import UserAttributes
from django.contrib.auth.models import User
plan = kwargs.get('plan', [])
for migration, rolled_back in plan:
if migration.app_label == 'accounts' and migration.name == '0001_initial' and not rolled_back:
if User.objects.count() == 0:
print('\033[1m* \033[92mCreating default admin user\033[0m')
admin = User.objects.create_superuser('admin', None, 'admin')
UserAttributes(user=admin, max_instances=-1, max_cpus=-1, max_memory=-1, max_disk_size=-1).save()
break
class AccountsConfig(AppConfig):
name = 'accounts'
verbose_name = 'Accounts'
def ready(self):
post_migrate.connect(create_admin, sender=self)
post_migrate.connect(apply_change_password, sender=self)

View file

@ -1,13 +0,0 @@
from django.contrib.auth.backends import RemoteUserBackend
from accounts.models import UserInstance, UserAttributes
from instances.models import Instance
class MyRemoteUserBackend(RemoteUserBackend):
#create_unknown_user = True
def configure_user(self, user):
#user.is_superuser = True
UserAttributes.configure_user(user)
return user

View file

@ -1,25 +1,75 @@
import re from appsettings.settings import app_settings
from django import forms from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _ from django.forms import EmailField, Form, ModelForm, ValidationError
from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _
from django.conf import settings
from .models import UserInstance, UserSSHKey
from .utils import validate_ssh_key
class UserAddForm(forms.Form): class UserInstanceForm(ModelForm):
name = forms.CharField(label="Name", def __init__(self, *args, **kwargs):
error_messages={'required': _('No User name has been entered')}, super(UserInstanceForm, self).__init__(*args, **kwargs)
max_length=20)
password = forms.CharField(required=not settings.ALLOW_EMPTY_PASSWORD, error_messages={'required': _('No password has been entered')},)
def clean_name(self): # Make user and instance fields not editable after creation
name = self.cleaned_data['name'] instance = getattr(self, 'instance', None)
have_symbol = re.match('^[a-z0-9]+$', name) if instance and instance.id is not None:
if not have_symbol: self.fields['user'].disabled = True
raise forms.ValidationError(_('The flavor name must not contain any special characters')) self.fields['instance'].disabled = True
elif len(name) > 20:
raise forms.ValidationError(_('The flavor name must not exceed 20 characters')) def clean_instance(self):
try: instance = self.cleaned_data['instance']
User.objects.get(username=name) if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == 'False':
except User.DoesNotExist: exists = UserInstance.objects.filter(instance=instance)
return name if exists:
raise forms.ValidationError(_('Flavor name is already use')) raise ValidationError(_('Instance owned by another user'))
return instance
class Meta:
model = UserInstance
fields = '__all__'
class ProfileForm(ModelForm):
class Meta:
model = get_user_model()
fields = ('first_name', 'last_name', 'email')
class UserSSHKeyForm(ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
self.publickeys = UserSSHKey.objects.filter(user=self.user)
super().__init__(*args, **kwargs)
def clean_keyname(self):
for key in self.publickeys:
if self.cleaned_data['keyname'] == key.keyname:
raise ValidationError(_("Key name already exist"))
return self.cleaned_data['keyname']
def clean_keypublic(self):
for key in self.publickeys:
if self.cleaned_data['keypublic'] == key.keypublic:
raise ValidationError(_("Public key already exist"))
if not validate_ssh_key(self.cleaned_data['keypublic']):
raise ValidationError(_('Invalid key'))
return self.cleaned_data['keypublic']
def save(self, commit=True):
ssh_key = super().save(commit=False)
ssh_key.user = self.user
if commit:
ssh_key.save()
return ssh_key
class Meta:
model = UserSSHKey
fields = ('keyname', 'keypublic')
class EmailOTPForm(Form):
email = EmailField(label=_('Email'))

View file

@ -1,29 +1,50 @@
# -*- coding: utf-8 -*- # Generated by Django 2.2.10 on 2020-01-28 07:01
from __future__ import unicode_literals import django.core.validators
import django.db.models.deletion
from django.db import models, migrations
from django.conf import settings from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('instances', '0001_initial'), ('instances', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel(
name='UserSSHKey',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('keyname', models.CharField(max_length=25)),
('keypublic', models.CharField(max_length=500)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel( migrations.CreateModel(
name='UserInstance', name='UserInstance',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_change', models.BooleanField(default=False)), ('is_change', models.BooleanField(default=False)),
('is_delete', models.BooleanField(default=False)), ('is_delete', models.BooleanField(default=False)),
('instance', models.ForeignKey(to='instances.Instance')), ('is_vnc', models.BooleanField(default=False)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), ('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='instances.Instance')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='UserAttributes',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('can_clone_instances', models.BooleanField(default=True)),
('max_instances', models.IntegerField(default=1, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
('max_cpus', models.IntegerField(default=1, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
('max_memory', models.IntegerField(default=2048, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
('max_disk_size', models.IntegerField(default=20, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)])),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
options={
},
bases=(models.Model,),
), ),
] ]

View file

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def add_useradmin(apps, schema_editor):
from django.utils import timezone
from django.contrib.auth.models import User
User.objects.create_superuser('admin', None, 'admin',
last_login=timezone.now()
)
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.RunPython(add_useradmin),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 2.2.12 on 2020-05-27 12:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PermissionSet',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'permissions': (('change_password', 'Can change password'), ),
'managed': False,
'default_permissions': (),
},
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 2.2.12 on 2020-06-04 09:30
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_permissionset'),
]
operations = [
migrations.AlterField(
model_name='userattributes',
name='max_cpus',
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
),
migrations.AlterField(
model_name='userattributes',
name='max_instances',
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
),
]

View file

@ -1,25 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0002_auto_20150325_0846'),
]
operations = [
migrations.CreateModel(
name='UserSSHKey',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('keyname', models.CharField(max_length=25)),
('keypublic', models.CharField(max_length=500)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,44 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_auto_20200604_0930'),
]
operations = [
migrations.AlterField(
model_name='userattributes',
name='max_cpus',
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max CPUs'),
),
migrations.AlterField(
model_name='userattributes',
name='max_disk_size',
field=models.IntegerField(default=20, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max disk size'),
),
migrations.AlterField(
model_name='userattributes',
name='max_instances',
field=models.IntegerField(default=2, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max instances'),
),
migrations.AlterField(
model_name='userattributes',
name='max_memory',
field=models.IntegerField(default=2048, help_text='-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)], verbose_name='max memory'),
),
migrations.AlterField(
model_name='usersshkey',
name='keyname',
field=models.CharField(max_length=25, verbose_name='key name'),
),
migrations.AlterField(
model_name='usersshkey',
name='keypublic',
field=models.CharField(max_length=500, verbose_name='public key'),
),
]

View file

@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0003_usersshkey'),
]
operations = [
migrations.CreateModel(
name='UserAttributes',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('max_instances', models.IntegerField(default=0)),
('max_cpus', models.IntegerField(default=0)),
('max_memory', models.IntegerField(default=0)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_usersshkey'),
]
operations = [
migrations.AddField(
model_name='userinstance',
name='is_vnc',
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 2.2.13 on 2020-06-16 10:39
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('instances', '0003_auto_20200615_0637'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0004_auto_20200615_0637'),
]
operations = [
migrations.AlterUniqueTogether(
name='userinstance',
unique_together={('user', 'instance')},
),
]

View file

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_userattributes'),
]
operations = [
migrations.AddField(
model_name='userattributes',
name='can_clone_instances',
field=models.BooleanField(default=False),
),
]

View file

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_userattributes_can_clone_instances'),
]
operations = [
migrations.AddField(
model_name='userattributes',
name='max_disk_size',
field=models.IntegerField(default=0),
),
]

View file

@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_userattributes_max_disk_size'),
]
operations = [
migrations.AlterField(
model_name='userattributes',
name='max_cpus',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='userattributes',
name='max_disk_size',
field=models.IntegerField(default=20),
),
migrations.AlterField(
model_name='userattributes',
name='max_instances',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='userattributes',
name='max_memory',
field=models.IntegerField(default=2048),
),
]

View file

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_userinstance_is_vnc'),
('accounts', '0007_auto_20160426_0635'),
]
operations = [
]

View file

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0008_merge'),
]
operations = [
migrations.AlterField(
model_name='userattributes',
name='can_clone_instances',
field=models.BooleanField(default=True),
),
]

View file

@ -1,57 +1,78 @@
from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from instances.models import Instance from instances.models import Instance
class UserInstanceManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('instance', 'user')
class UserInstance(models.Model): class UserInstance(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User, on_delete=models.CASCADE)
instance = models.ForeignKey(Instance) instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
is_change = models.BooleanField(default=False) is_change = models.BooleanField(default=False)
is_delete = models.BooleanField(default=False) is_delete = models.BooleanField(default=False)
is_vnc = models.BooleanField(default=False) is_vnc = models.BooleanField(default=False)
def __unicode__(self): objects = UserInstanceManager()
return self.instance.name
def __str__(self):
return _('Instance "%(inst)s" of user %(user)s') % {"inst": self.instance, "user": self.user}
class Meta:
unique_together = ['user', 'instance']
class UserSSHKey(models.Model): class UserSSHKey(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User, on_delete=models.DO_NOTHING)
keyname = models.CharField(max_length=25) keyname = models.CharField(_('key name'), max_length=25)
keypublic = models.CharField(max_length=500) keypublic = models.CharField(_('public key'), max_length=500)
def __unicode__(self): def __str__(self):
return self.keyname return self.keyname
class UserAttributes(models.Model): class UserAttributes(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE) user = models.OneToOneField(User, on_delete=models.CASCADE)
can_clone_instances = models.BooleanField(default=True) can_clone_instances = models.BooleanField(default=True)
max_instances = models.IntegerField(default=1) max_instances = models.IntegerField(_('max instances'),
max_cpus = models.IntegerField(default=1) default=2,
max_memory = models.IntegerField(default=2048) help_text=_("-1 for unlimited. Any integer value"),
max_disk_size = models.IntegerField(default=20) validators=[
MinValueValidator(-1),
])
max_cpus = models.IntegerField(
_('max CPUs'),
default=2,
help_text=_("-1 for unlimited. Any integer value"),
validators=[MinValueValidator(-1)],
)
max_memory = models.IntegerField(
_('max memory'),
default=2048,
help_text=_("-1 for unlimited. Any integer value"),
validators=[MinValueValidator(-1)],
)
max_disk_size = models.IntegerField(
_('max disk size'),
default=20,
help_text=_("-1 for unlimited. Any integer value"),
validators=[MinValueValidator(-1)],
)
@staticmethod def __str__(self):
def create_missing_userattributes(user):
try:
userattributes = user.userattributes
except UserAttributes.DoesNotExist:
userattributes = UserAttributes(user=user)
userattributes.save()
@staticmethod
def add_default_instances(user):
existing_instances = UserInstance.objects.filter(user=user)
if not existing_instances:
for instance_name in settings.NEW_USER_DEFAULT_INSTANCES:
instance = Instance.objects.get(name=instance_name)
user_instance = UserInstance(user=user, instance=instance)
user_instance.save()
@staticmethod
def configure_user(user):
UserAttributes.create_missing_userattributes(user)
UserAttributes.add_default_instances(user)
def __unicode__(self):
return self.user.username return self.user.username
class PermissionSet(models.Model):
"""
Dummy model for holding set of permissions we need to be automatically added by Django
"""
class Meta:
default_permissions = ()
permissions = (('change_password', _('Can change password')), )
managed = False

View file

@ -1,140 +1,89 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "User" %} - {{ user }}{% endblock %} {% load icons %}
{% load qr_code %}
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
{% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %}
{% block page_heading_extra %}
{% if otp_enabled %}
<a href="{% url 'accounts:admin_email_otp' user.id %}" class="btn btn-secondary" title="{% trans "Email OTP QR code" %}">
{% icon 'qrcode' %}
</a>
{% endif %}
<a href="{% url 'admin:user_update' user.id %}?next={% url 'accounts:account' user.id %}" class="btn btn-primary" title="{% trans "Edit user" %}">
{% icon 'pencil' %}
</a>
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success" title="{% trans "Create user instance" %}">
{% icon 'plus' %}
</a>
{% endblock page_heading_extra %}
{% block content %} {% block content %}
<!-- Page Heading --> <ul class="nav nav-tabs">
<div class="row"> <li class="nav-item">
<div class="col-lg-12"> <a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
{% include 'create_user_inst_block.html' %} </li>
<h1 class="page-header">{{ user }}</h1> <li class="nav-item">
</div> <a class="nav-link" data-toggle="tab" href="#public-keys">{% trans "Public Keys" %}</a>
</div> </li>
<!-- /.row --> </ul>
{% include 'errors_block.html' %} <div class="tab-content">
<div class="tab-pane active" id="instances">
{% if request.user.is_superuser and publickeys %} <table class="table table-striped table-hover">
<div class="row"> <thead>
<div class="col-lg-12"> <tr>
<div class="table-responsive"> <th scope="col">#</th>
<table class="table table-bordered table-hover"> <th scope="col">{% trans "Instance" %}</th>
<thead> <th scope="col">{% trans "VNC" %}</th>
<tr> <th scope="col">{% trans "Resize" %}</th>
<th>{% trans "Key name" %}</th> <th scope="col">{% trans "Delete" %}</th>
<th>{% trans "Public key" %}</th> <th scope="colgroup" colspan="2">{% trans "Action" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for publickey in publickeys %} {% for inst in user_insts %}
<tr> <tr>
<td>{{ publickey.keyname }}</td> <td>{{ forloop.counter }}</td>
<td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td> <td><a href="{% url 'instances:instance' inst.instance.id %}">{{ inst.instance.name }}</a></td>
</tr> <td>{{ inst.is_vnc }}</td>
{% endfor %} <td>{{ inst.is_change }}</td>
</tbody> <td>{{ inst.is_delete }}</td>
</table> <td style="width:5px;">
</div> <a href="{% url 'accounts:user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
</div> {% icon 'pencil' %}
</div> </a>
{% endif %} </td>
<td style="width:5px;">
<div class="row"> <a class="btn btn-sm btn-secondary" href="{% url 'accounts:user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
<div class="col-lg-12"> {% icon 'trash' %}
{% if not user_insts %} </a>
<div class="col-lg-12"> </td>
<div class="alert alert-warning alert-dismissable"> </tr>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> {% endfor %}
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning:" %}</strong> {% trans "User doesn't have any Instace" %} </tbody>
</div> </table>
</div> </div>
{% else %} <div class="tab-pane fade" id="public-keys">
<div class="table-responsive"> <table class="table table-striped table-hover">
<table class="table table-bordered table-hover"> <thead>
<thead> <tr>
<tr> <th scope="col">{% trans "Key name" %}</th>
<th>#</th> <th scope="col">{% trans "Public key" %}</th>
<th>{% trans "Instance" %}</th> </tr>
<th>{% trans "VNC" %}</th> </thead>
<th>{% trans "Resize" %}</th> <tbody>
<th>{% trans "Delete" %}</th> {% for publickey in publickeys %}
<th colspan="2">{% trans "Action" %}</th> <tr>
</tr> <td>{{ publickey.keyname }}</td>
</thead> <td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td>
<tbody> </tr>
{% for inst in user_insts %} {% endfor %}
<tr> </tbody>
<td>{{ forloop.counter }}</td> </table>
<td><a href="{% url 'instance' inst.instance.compute.id inst.instance.name %}">{{ inst.instance.name }}</a></td> </div>
<td>{{ inst.is_vnc }}</td> </div>
<td>{{ inst.is_change }}</td> {% endblock content %}
<td>{{ inst.is_delete }}</td>
<td style="width:5px;">
<a href="#editPriv{{ forloop.counter }}" type="button" class="btn btn-xs btn-default" data-toggle="modal">
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
</a>
<!-- Modal pool -->
<div class="modal fade" id="editPriv{{ forloop.counter }}" tabindex="-1" role="dialog" aria-labelledby="editPrivLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{% trans "Edit privilegies for" %} {{ inst.instance.name }}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
<input type="hidden" name="user_inst" value="{{ inst.id }}">
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "VNC" %}</label>
<div class="col-sm-6">
<select type="text" class="form-control" name="inst_vnc">
<option value="">False</option>
<option value="1" {% if inst.is_vnc %}selected{% endif %}>True</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Resize" %}</label>
<div class="col-sm-6">
<select type="text" class="form-control" name="inst_change">
<option value="">False</option>
<option value="1" {% if inst.is_change %}selected{% endif %}>True</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Delete" %}</label>
<div class="col-sm-6">
<select type="text" class="form-control" name="inst_delete">
<option value="">False</option>
<option value="1" {% if inst.is_delete %}selected{% endif %}>True</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary" name="permission">{% trans "Edit" %}</button>
</div>
</form>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /.modal -->
</td>
<td style="width:5px;">
<form action="" method="post" role="form">{% csrf_token %}
<input type="hidden" name="user_inst" value="{{ inst.id }}">
<button type="submit" class="btn btn-xs btn-default" name="delete" tittle="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -1,144 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Users" %}{% endblock %}
{% block content %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
{% include 'create_user_block.html' %}
<h1 class="page-header">{% trans "Users" %}</h1>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
<div class="row">
{% if not users %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning:" %}</strong> {% trans "You don't have any User" %}
</div>
</div>
{% else %}
{% for user in users %}
<div id="{{ user.username }}" class="col-xs-12 col-sm-4">
<div class="panel {% if user.is_active %}panel-success{% else %}panel-danger{% endif %} panel-data">
<div class="panel-heading">
<h3 class="panel-title">
<a href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a>
<a data-toggle="modal" href="#editUser{{ user.id }}" class="pull-right" title="{% trans "Edit" %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</h3>
</div>
<div class="panel-body">
<div class="col-xs-4 col-sm-4">
<p><strong>{% trans "Status:" %}</strong></p>
</div>
<div class="col-xs-4 col-sm-6">
{% if user.is_active %}
<p>{% trans "Active" %}</p>
{% else %}
<p>{% trans "Blocked" %}</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Modal Edit -->
<div class="modal fade" id="editUser{{ user.id }}" tabindex="-1" role="dialog" aria-labelledby="editUserLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{% trans "Edit user info" %}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Name" %}</label>
<div class="col-sm-6">
<input type="hidden" name="user_id" value="{{ user.id }}">
<input type="text" name="name" class="form-control" value="{{ user.username }}" disabled>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" name="user_pass" class="form-control" value="">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Is staff" %}</label>
<div class="col-sm-2">
<input type="checkbox" name="user_is_staff" {% if user.is_staff %}checked{% endif %}>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Is superuser" %}</label>
<div class="col-sm-2">
<input type="checkbox" name="user_is_superuser" {% if user.is_superuser %}checked{% endif %}>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Can clone instances" %}</label>
<div class="col-sm-2">
<input type="checkbox" name="userattributes_can_clone_instances" {% if user.userattributes.can_clone_instances %}checked{% endif %}>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Max instances" %}</label>
<div class="col-sm-6">
<input type="text" name="userattributes_max_instances" class="form-control" value="{{ user.userattributes.max_instances }}">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Max cpus" %}</label>
<div class="col-sm-6">
<input type="text" name="userattributes_max_cpus" class="form-control" value="{{ user.userattributes.max_cpus }}">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Max memory (MB)" %}</label>
<div class="col-sm-6">
<input type="text" name="userattributes_max_memory" class="form-control" value="{{ user.userattributes.max_memory }}">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Max disk size (GB)" %}</label>
<div class="col-sm-6">
<input type="text" name="userattributes_max_disk_size" class="form-control" value="{{ user.userattributes.max_disk_size }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="pull-left btn btn-danger" name="delete">
{% trans "Delete" %}
</button>
{% if user.is_active %}
<button type="submit" class="pull-left btn btn-warning" name="block">
{% trans "Block" %}
</button>
{% else %}
<button type="submit" class="pull-left btn btn-success" name="unblock">
{% trans "Unblock" %}
</button>
{% endif %}
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="edit">
{% trans "Edit" %}
</button>
</form>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
{% endfor %}
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load i18n %}
{% load icons %}
{% block title %}{%trans "Change Password" %}{% endblock title %}
{% block content %}
<div class="row">
<div class="offset-2 col-lg-8">
<div class="card">
<div class="card-header">
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
</div>
<div class="card-body">
<form method="post" id="password-change">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
</form>
</div>
<div class="card-footer">
<div class="float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %}
{% trans "Cancel" %}</a>
<button type="submit" form="password-change" class="btn btn-success">
{% icon 'check' %} {% trans "Change" %}
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,7 @@
{% load i18n %}
{% load qr_code %}
{% blocktrans %}
Scan this QR code to get OTP for account '{{ user }}'
{% endblocktrans %}
<br>
{% qr_from_text totp_url %}

View file

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load icons %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block page_heading %}{{ title }}{% endblock page_heading %}
{% block content %}
<div class="alert alert-info">
{% blocktrans %}
Enter email address OTP QR code will be sent to.
{% endblocktrans %}
</div>
<div class="card">
<div class="card-body">
<form id="create-update" action="" method="post">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
</form>
</div>
<div class="card-footer">
<div class="form-group mb-0 float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'arrow-left' %} {% trans "Cancel" %}</a>
<button type="submit" form="create-update" class="btn btn-success">
{% icon 'envelope-o' %} {% trans "Send" %}
</button>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap4 %}
{% block title %}WebVirtCloud{% endblock title %}
{% block page_heading %}WebVirtCloud{% endblock page_heading %}
{% block content %}
<div class="row">
<div class="col-6 offset-3" role="main">
<div class="card">
<div class="card-body">
{% if form.errors %}
{% bootstrap_form_errors form %}
{% endif %}
<form class="form-signin" method="post" role="form" aria-label="Login form">
{% csrf_token %}
{% bootstrap_field form.username layout='horizontal' %}
{% bootstrap_field form.password layout='horizontal' %}
{% bootstrap_field form.otp_token layout='horizontal' %}
<a href="{% url 'accounts:email_otp' %}" class="float-right mb-2">{% trans "I do not have/lost my OTP!" %}</a>
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View file

@ -1,41 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
<meta name="author" content="anatoliy.guskov@gmail.com">
<title>{% block title %}{% endblock %}</title>
<!-- Bootstrap Core CSS -->
<link href="{% static "css/bootstrap.min.css" %}" rel="stylesheet">
<!-- SB admin CSS -->
<link href="{% static "css/signin.css" %}" rel="stylesheet">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
<!-- jQuery -->
<script src="{% static "js/jquery.js" %}"></script>
<!-- Bootstrap Core JavaScript -->
<script src="{% static "js/bootstrap.min.js" %}"></script>
</body>
</html>

View file

@ -1,38 +0,0 @@
{% load i18n %}
{% if request.user.is_superuser %}
<a href="#AddUser" type="button" class="btn btn-success pull-right" data-toggle="modal">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
</a>
<!-- Modal pool -->
<div class="modal fade" id="AddUser" tabindex="-1" role="dialog" aria-labelledby="AddUserLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{% trans "Add New User" %}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Name" %}</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="name" placeholder="john" required pattern="[a-z0-9]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" class="form-control" name="password" placeholder="*******" {% if not allow_empty_password %}required{% endif %}>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary" name="create">{% trans "Create" %}</button>
</div>
</form>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /.modal -->
{% endif %}

View file

@ -1,36 +0,0 @@
{% load i18n %}
{% if request.user.is_superuser %}
<a href="#addUserInst" type="button" class="btn btn-success pull-right" data-toggle="modal">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
</a>
<!-- Modal pool -->
<div class="modal fade" id="addUserInst" tabindex="-1" role="dialog" aria-labelledby="addUserInstLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{% trans "Add Instance for User" %}</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Host / Instance" %}</label>
<div class="col-sm-6">
<select class="form-control" name="inst_id">
{% for inst in instances %}
<option value="{{ inst.id }}">{{ inst.compute.name }} / {{ inst.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary" name="add">{% trans "Add" %}</button>
</div>
</form>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /.modal -->
{% endif %}

View file

@ -1,25 +1,30 @@
{% extends "base_auth.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "WebVirtCloud - Sign In" %}{% endblock %} {% load static %}
{% block title %}{% trans "WebVirtCloud" %} - {% trans "Sign In" %}{% endblock %}
{% block style %}
<link href="{% static "css/signin.css" %}" rel="stylesheet">
{% endblock style %}
{% block content %} {% block content %}
<div class="row"> <div class="page-header">
<div class="page-header"> <a class="" href="/"><h1>WebVirtCloud</h1></a>
<a href="/"><h1>WebVirtCloud</h1></a> </div>
<div class="col-12" role="main">
{% if form.errors %}
<div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% trans "Incorrect username or password." %}
</div> </div>
<div class="col-xs-12" role="main"> {% endif %}
{% if form.errors %} <form class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
<div class="alert alert-danger"> <h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <input type="text" class="form-control" name="username" placeholder="{% trans "User" %}" autocapitalize="none" autocorrect="off" autofocus>
{% trans "Incorrect username or password." %} <input type="password" class="form-control" name="password" placeholder="{% trans "Password" %}">
</div> <input type="hidden" name="next" value="{{ next }}">
{% endif %} <button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
<form class="form-signin" method="post" role="form">{% csrf_token %} </form>
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2> </div>
<input type="text" class="form-control" name="username" placeholder="Login" autocapitalize="none" autocorrect="off" autofocus>
<input type="password" class="form-control" name="password" placeholder="Password">
<input name="next" id="next" type="hidden" value="{% url 'instances' %}">
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,16 @@
{% extends "base_auth.html" %} {% extends "base_auth.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "WebVirtCloud - Sign Out" %}{% endblock %} {% block title %}
{% trans "WebVirtCloud" %} - {% trans "Sign Out"%}
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div>
<div class="page-header"> <div class="page-header">
<a href="/"><h1>WebVirtCloud</h1></a> <a href="/"><h1>WebVirtCloud</h1></a>
</div> </div>
<div class="col-xs-12" role="main"> <div class="col-12" role="main">
<div class="logout"> <div class="logout">
<h1>{% trans "Successful log out" %}</h1> <h2>{% trans "Successful log out" %}</h2>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,117 +1,80 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap4 %}
{% load icons %}
{% load tags_fingerprint %} {% load tags_fingerprint %}
{% block title %}{% trans "Profile" %}{% endblock %}
{% block title %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock %}
{% block page_heading %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock page_heading %}
{% block content %} {% block content %}
<!-- Page Heading --> <ul class="nav nav-tabs">
<div class="row"> <li class="nav-item">
<div class="col-lg-12"> <a class="nav-link active" data-toggle="tab" href="#edit-profile">{% trans "Edit Profile" %}</a>
<h1 class="page-header">{% trans "Profile" %}</h1> </li>
</div> <li class="nav-item">
</div> <a class="nav-link" data-toggle="tab" href="#ssh-keys">{% trans "SSH Keys" %}</a>
<!-- /.row --> </li>
</ul>
{% include 'errors_block.html' %} <div class="tab-content">
<div class="tab-pane tab-pane-bordered active" id="edit-profile">
<div class="row"> <div class="card">
<div class="col-lg-12"> <div class="card-body">
<h3 class="page-header">{% trans "Edit Profile" %}</h3> <form method="post" action="" role="form" aria-label="Edit user info form">
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %} {% csrf_token %}
<div class="form-group"> {% bootstrap_form profile_form layout='horizontal' %}
<label class="col-sm-2 control-label">{% trans "Login" %}</label> {% if perms.accounts.change_password %}
<div class="col-sm-4"> <a href="{% url 'accounts:change_password' %}" class="btn btn-primary">
<input type="text" class="form-control" value="{{ request.user.username }}" disabled> {% icon 'lock' %} {% trans "Change Password" %}
</div> </a>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 control-label">{% trans "Username" %}</label>
<div class="col-sm-4">
<input type="text" class="form-control" name="username" value="{{ request.user.first_name }}" pattern="[0-9a-zA-Z]+">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 control-label">{% trans "Email" %}</label>
<div class="col-sm-4">
<input type="email" class="form-control" name="email" value="{{ request.user.email }}">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
</div>
</div>
</form>
{% if show_profile_edit_password %}
<h3 class="page-header">{% trans "Edit Password" %}</h3>
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Old" %}</label>
<div class="col-sm-4">
<input type="password" class="form-control" name="oldpasswd" value="">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 control-label">{% trans "New" %}</label>
<div class="col-sm-4">
<input type="password" class="form-control" name="passwd1" value="">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 control-label">{% trans "Retry" %}</label>
<div class="col-sm-4">
<input type="password" class="form-control" name="passwd2" value="">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
</div>
</div>
</form>
{% endif %} {% endif %}
<h3 class="page-header">{% trans "SSH Keys" %}</h3> <div class="form-group mb-0 float-right">
{% if publickeys %} <button type="submit" class="btn btn-primary">
<div class="col-lg-12"> {% icon 'pencil' %} {% trans "Update" %}
<div class="table-responsive"> </button>
<table class="table table-hover"> </div>
<tbody style="text-align: center;"> </form>
{% for key in publickeys %}
<tr>
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
<td>
<form action="" method="post" role="form">{% csrf_token %}
<input type="hidden" name="keyid" value="{{ key.id }}"/>
<button type="submit" class="btn btn-sm btn-default" name="keydelete" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
<span class="glyphicon glyphicon-trash"></span>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 control-label">{% trans "Key name" %}</label>
<div class="col-sm-4">
<input type="text" class="form-control" name="keyname" placeholder="{% trans "Enter Name" %}">
</div>
</div>
<div class="form-group bridge_name_form_group_dhcp">
<label class="col-sm-2 control-label">{% trans "Public key" %}</label>
<div class="col-sm-8">
<textarea name="keypublic" class="form-control" rows="6" placeholder="{% trans "Enter Public Key" %}"></textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">{% trans "Add" %}</button>
</div>
</div>
</form>
</div>
</div> </div>
</div>
</div>
<div class="tab-pane tab-pane-bordered fade" id="ssh-keys">
{% if publickeys %}
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-hover">
<tbody class="text-center">
{% for key in publickeys %}
<tr>
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
<td>
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
{% icon 'trash' %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-header">
{%trans "Add SSH Key" %}
</div>
<div class="card-body">
<form method="post" action="{% url 'accounts:ssh_key_create' %}" role="form" aria-label="Add key to user form">
{% csrf_token %}
{% bootstrap_form ssh_key_form layout='horizontal' %}
<div class="form-group mb-0 float-right">
<button type="submit" class="btn btn-primary">
{% icon 'plus' %} {% trans "Add" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,7 +1,8 @@
from django import template
import base64 import base64
import hashlib import hashlib
from django import template
register = template.Library() register = template.Library()

View file

@ -1,3 +1,267 @@
from django.test import TestCase from appsettings.settings import app_settings
from computes.models import Compute
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.shortcuts import reverse
from django.test import Client, TestCase
from instances.models import Instance
from instances.utils import refr
from libvirt import VIR_DOMAIN_UNDEFINE_NVRAM
from vrtManager.create import wvmCreate
# Create your tests here. from accounts.forms import UserInstanceForm, UserSSHKeyForm
from accounts.models import UserInstance, UserSSHKey
from accounts.utils import validate_ssh_key
class AccountsTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Add users for testing purposes
User = get_user_model()
cls.admin_user = User.objects.get(pk=1)
cls.test_user = User.objects.create_user(username='test', password='test')
# Add localhost compute
cls.compute = Compute(
name='test-compute',
hostname='localhost',
login='',
password='',
details='local',
type=4,
)
cls.compute.save()
cls.connection = wvmCreate(
cls.compute.hostname,
cls.compute.login,
cls.compute.password,
cls.compute.type,
)
# Add disks for testing
cls.connection.create_volume(
'default',
'test-volume',
1,
'qcow2',
False,
0,
0,
)
# XML for testing vm
with open('conf/test-vm.xml', 'r') as f:
cls.xml = f.read()
# Create testing vm from XML
cls.connection._defineXML(cls.xml)
refr(cls.compute)
cls.instance = Instance.objects.get(pk=1)
@classmethod
def tearDownClass(cls):
# Destroy testing vm
cls.instance.proxy.delete_all_disks()
cls.instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM)
super().tearDownClass()
def setUp(self):
self.client.login(username='admin', password='admin')
permission = Permission.objects.get(codename='change_password')
self.test_user.user_permissions.add(permission)
self.rsa_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6OOdbfv27QVnSC6sKxGaHb6YFc+3gxCkyVR3cTSXE/n5BEGf8aOgBpepULWa1RZfxYHY14PlKULDygdXSdrrR2kNSwoKz/Oo4d+3EE92L7ocl1+djZbptzgWgtw1OseLwbFik+iKlIdqPsH+IUQvX7yV545ZQtAP8Qj1R+uCqkw== test@test'
self.ecdsa_key = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJc5xpT3R0iFJYNZbmWgAiDlHquX/BcV1kVTsnBfiMsZgU3lGaqz2eb7IBcir/dxGnsVENTTmPQ6sNcxLxT9kkQ= realgecko@archlinux'
def test_profile(self):
response = self.client.get(reverse('accounts:profile'))
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
self.assertEqual(response.status_code, 200)
def test_account_with_otp(self):
settings.OTP_ENABLED = True
response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
self.assertEqual(response.status_code, 200)
def test_login_logout(self):
client = Client()
response = client.post(reverse("accounts:login"), {"username": "test", "password": "test"})
self.assertRedirects(response, reverse('accounts:profile'))
response = client.get(reverse('accounts:logout'))
self.assertRedirects(response, reverse('accounts:login'))
def test_change_password(self):
self.client.force_login(self.test_user)
response = self.client.get(reverse('accounts:change_password'))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('accounts:change_password'),
{
'old_password': 'wrongpass',
'new_password1': 'newpw',
'new_password2': 'newpw',
},
)
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('accounts:change_password'),
{
'old_password': 'test',
'new_password1': 'newpw',
'new_password2': 'newpw',
},
)
self.assertRedirects(response, reverse('accounts:profile'))
self.client.logout()
logged_in = self.client.login(username='test', password='newpw')
self.assertTrue(logged_in)
def test_user_instance_create_update_delete(self):
# create
response = self.client.get(reverse('accounts:user_instance_create', args=[self.test_user.id]))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('accounts:user_instance_create', args=[self.test_user.id]),
{
'user': self.test_user.id,
'instance': self.instance.id,
'is_change': False,
'is_delete': False,
'is_vnc': False,
},
)
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
user_instance: UserInstance = UserInstance.objects.get(pk=1)
self.assertEqual(user_instance.user, self.test_user)
self.assertEqual(user_instance.instance, self.instance)
self.assertEqual(user_instance.is_change, False)
self.assertEqual(user_instance.is_delete, False)
self.assertEqual(user_instance.is_vnc, False)
# update
response = self.client.get(reverse('accounts:user_instance_update', args=[user_instance.id]))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('accounts:user_instance_update', args=[user_instance.id]),
{
'user': self.test_user.id,
'instance': self.instance.id,
'is_change': True,
'is_delete': True,
'is_vnc': True,
},
)
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
user_instance: UserInstance = UserInstance.objects.get(pk=1)
self.assertEqual(user_instance.user, self.test_user)
self.assertEqual(user_instance.instance, self.instance)
self.assertEqual(user_instance.is_change, True)
self.assertEqual(user_instance.is_delete, True)
self.assertEqual(user_instance.is_vnc, True)
# delete
response = self.client.get(reverse('accounts:user_instance_delete', args=[user_instance.id]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('accounts:user_instance_delete', args=[user_instance.id]))
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
# test 'next' redirect during deletion
user_instance = UserInstance.objects.create(user=self.test_user, instance=self.instance)
response = self.client.post(
reverse('accounts:user_instance_delete', args=[user_instance.id]) + '?next=' + reverse('index'))
self.assertRedirects(response, reverse('index'))
def test_update_user_profile(self):
self.client.force_login(self.test_user)
user = get_user_model().objects.get(username='test')
self.assertEqual(user.first_name, '')
self.assertEqual(user.last_name, '')
self.assertEqual(user.email, '')
response = self.client.post(reverse('accounts:profile'), {
'first_name': 'first name',
'last_name': 'last name',
'email': 'email@mail.mail',
})
self.assertRedirects(response, reverse('accounts:profile'))
user = get_user_model().objects.get(username='test')
self.assertEqual(user.first_name, 'first name')
self.assertEqual(user.last_name, 'last name')
self.assertEqual(user.email, 'email@mail.mail')
def test_create_delete_ssh_key(self):
response = self.client.get(reverse('accounts:ssh_key_create'))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('accounts:ssh_key_create'), {
'keyname': 'keyname',
'keypublic': self.rsa_key,
})
self.assertRedirects(response, reverse('accounts:profile'))
key = UserSSHKey.objects.get(pk=1)
self.assertEqual(key.keyname, 'keyname')
self.assertEqual(key.keypublic, self.rsa_key)
response = self.client.get(reverse('accounts:ssh_key_delete', args=[1]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('accounts:ssh_key_delete', args=[1]))
self.assertRedirects(response, reverse('accounts:profile'))
def test_validate_ssh_key(self):
self.assertFalse(validate_ssh_key(''))
self.assertFalse(validate_ssh_key('ssh-rsa ABBA test@test'))
self.assertFalse(validate_ssh_key('ssh-rsa AAAABwdzZGY= test@test'))
self.assertFalse(validate_ssh_key('ssh-rsa AAA test@test'))
# validate ecdsa key
self.assertTrue(validate_ssh_key(self.ecdsa_key))
def test_forms(self):
# raise available validation errors for maximum coverage
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
form.save()
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
self.assertFalse(form.is_valid())
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': 'invalid key'}, user=self.test_user)
self.assertFalse(form.is_valid())
app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER = 'False'
form = UserInstanceForm({
'user': self.admin_user.id,
'instance': self.instance.id,
'is_change': False,
'is_delete': False,
'is_vnc': False,
})
form.save()
form = UserInstanceForm({
'user': self.test_user.id,
'instance': self.instance.id,
'is_change': False,
'is_delete': False,
'is_vnc': False,
})
self.assertFalse(form.is_valid())

View file

@ -1,12 +1,33 @@
from django.conf.urls import url from django.conf import settings
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path
from django_otp.forms import OTPAuthenticationForm
from . import views from . import views
app_name = 'accounts'
urlpatterns = [ urlpatterns = [
url(r'^login/$', 'django.contrib.auth.views.login', path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
{'template_name': 'login.html'}, name='login'), path('profile/', views.profile, name='profile'),
url(r'^logout/$', 'django.contrib.auth.views.logout', path('profile/<int:user_id>/', views.account, name='account'),
{'template_name': 'logout.html'}, name='logout'), path('change_password/', views.change_password, name='change_password'),
url(r'^profile/$', views.profile, name='profile'), path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'),
url(r'^$', views.accounts, name='accounts'), path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'),
url(r'^profile/(?P<user_id>[0-9]+)/$', views.account, name='account'), path('user_instance/<int:pk>/delete/', views.user_instance_delete, name='user_instance_delete'),
path('ssh_key/create/', views.ssh_key_create, name='ssh_key_create'),
path('ssh_key/<int:pk>/delete/', views.ssh_key_delete, name='ssh_key_delete'),
] ]
if settings.OTP_ENABLED:
urlpatterns += [
path(
'login/',
LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm),
name='login',
),
path('email_otp/', views.email_otp, name='email_otp'),
path('admin_email_otp/<int:user_id>/', views.admin_email_otp, name='admin_email_otp'),
]
else:
urlpatterns += path('login/', LoginView.as_view(template_name='login.html'), name='login'),

62
accounts/utils.py Normal file
View file

@ -0,0 +1,62 @@
import base64
import binascii
import struct
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice
def get_user_totp_device(user):
devices = devices_for_user(user)
for device in devices:
if isinstance(device, TOTPDevice):
return device
device = user.totpdevice_set.create()
return device
def validate_ssh_key(key):
array = key.encode().split()
# Each rsa-ssh key has 3 different strings in it, first one being
# typeofkey second one being keystring third one being username .
if len(array) != 3:
return False
typeofkey = array[0]
string = array[1]
username = array[2]
# must have only valid rsa-ssh key characters ie binascii characters
try:
data = base64.decodestring(string)
except binascii.Error:
return False
# unpack the contents of data, from data[:4] , property of ssh key .
try:
str_len = struct.unpack(">I", data[:4])[0]
except struct.error:
return False
# data[4:str_len] must have string which matches with the typeofkey, another ssh key property.
if data[4 : 4 + str_len] == typeofkey:
return True
else:
return False
def send_email_with_otp(user, device):
send_mail(
_("OTP QR Code"),
_("Please view HTML version of this message."),
None,
[user.email],
html_message=render_to_string(
"accounts/email/otp.html",
{
"totp_url": device.config_url,
"user": user,
},
),
fail_silently=False,
)

View file

@ -1,185 +1,197 @@
from django.shortcuts import render from admin.decorators import superuser_only
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from accounts.models import *
from instances.models import Instance
from accounts.forms import UserAddForm
from django.conf import settings from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model, update_session_auth_hash
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.forms import PasswordChangeForm
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from instances.models import Instance
from accounts.forms import EmailOTPForm, ProfileForm, UserSSHKeyForm
from accounts.models import *
from . import forms
from .utils import get_user_totp_device, send_email_with_otp
@login_required
def profile(request): def profile(request):
"""
:param request:
:return:
"""
error_messages = []
user = User.objects.get(id=request.user.id)
publickeys = UserSSHKey.objects.filter(user_id=request.user.id) publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
show_profile_edit_password = settings.SHOW_PROFILE_EDIT_PASSWORD profile_form = ProfileForm(request.POST or None, instance=request.user)
ssh_key_form = UserSSHKeyForm()
if request.method == 'POST': if profile_form.is_valid():
if 'username' in request.POST: profile_form.save()
username = request.POST.get('username', '') messages.success(request, _("Profile updated"))
email = request.POST.get('email', '') return redirect("accounts:profile")
user.first_name = username
user.email = email
user.save()
return HttpResponseRedirect(request.get_full_path())
if 'oldpasswd' in request.POST:
oldpasswd = request.POST.get('oldpasswd', '')
password1 = request.POST.get('passwd1', '')
password2 = request.POST.get('passwd2', '')
if not password1 or not password2:
error_messages.append("Passwords didn't enter")
if password1 and password2 and password1 != password2:
error_messages.append("Passwords don't match")
if not user.check_password(oldpasswd):
error_messages.append("Old password is wrong!")
if not error_messages:
user.set_password(password1)
user.save()
return HttpResponseRedirect(request.get_full_path())
if 'keyname' in request.POST:
keyname = request.POST.get('keyname', '')
keypublic = request.POST.get('keypublic', '')
for key in publickeys:
if keyname == key.keyname:
msg = _("Key name already exist")
error_messages.append(msg)
if keypublic == key.keypublic:
msg = _("Public key already exist")
error_messages.append(msg)
if not error_messages:
addkeypublic = UserSSHKey(user_id=request.user.id, keyname=keyname, keypublic=keypublic)
addkeypublic.save()
return HttpResponseRedirect(request.get_full_path())
if 'keydelete' in request.POST:
keyid = request.POST.get('keyid', '')
delkeypublic = UserSSHKey.objects.get(id=keyid)
delkeypublic.delete()
return HttpResponseRedirect(request.get_full_path())
return render(request, 'profile.html', locals())
@login_required return render(
def accounts(request): request,
""" "profile.html",
:param request: {
:return: "publickeys": publickeys,
""" "profile_form": profile_form,
if not request.user.is_superuser: "ssh_key_form": ssh_key_form,
return HttpResponseRedirect(reverse('index')) },
)
error_messages = []
users = User.objects.all().order_by('username')
allow_empty_password = settings.ALLOW_EMPTY_PASSWORD
if request.method == 'POST':
if 'create' in request.POST:
form = UserAddForm(request.POST)
if form.is_valid():
data = form.cleaned_data
else:
for msg_err in form.errors.values():
error_messages.append(msg_err.as_text())
if not error_messages:
new_user = User.objects.create_user(data['name'], None, data['password'])
new_user.save()
UserAttributes.configure_user(new_user)
return HttpResponseRedirect(request.get_full_path())
if 'edit' in request.POST:
user_id = request.POST.get('user_id', '')
user_pass = request.POST.get('user_pass', '')
user_edit = User.objects.get(id=user_id)
user_edit.set_password(user_pass)
user_edit.is_staff = request.POST.get('user_is_staff', False)
user_edit.is_superuser = request.POST.get('user_is_superuser', False)
user_edit.save()
userattributes = user_edit.userattributes
userattributes.can_clone_instances = request.POST.get('userattributes_can_clone_instances', False)
userattributes.max_instances = request.POST.get('userattributes_max_instances', 0)
userattributes.max_cpus = request.POST.get('userattributes_max_cpus', 0)
userattributes.max_memory = request.POST.get('userattributes_max_memory', 0)
userattributes.max_disk_size = request.POST.get('userattributes_max_disk_size', 0)
userattributes.save()
return HttpResponseRedirect(request.get_full_path())
if 'block' in request.POST:
user_id = request.POST.get('user_id', '')
user_block = User.objects.get(id=user_id)
user_block.is_active = False
user_block.save()
return HttpResponseRedirect(request.get_full_path())
if 'unblock' in request.POST:
user_id = request.POST.get('user_id', '')
user_unblock = User.objects.get(id=user_id)
user_unblock.is_active = True
user_unblock.save()
return HttpResponseRedirect(request.get_full_path())
if 'delete' in request.POST:
user_id = request.POST.get('user_id', '')
try:
del_user_inst = UserInstance.objects.filter(user_id=user_id)
del_user_inst.delete()
finally:
user_delete = User.objects.get(id=user_id)
user_delete.delete()
return HttpResponseRedirect(request.get_full_path())
return render(request, 'accounts.html', locals())
@login_required def ssh_key_create(request):
key_form = UserSSHKeyForm(request.POST or None, user=request.user)
if key_form.is_valid():
key_form.save()
messages.success(request, _("SSH key added"))
return redirect("accounts:profile")
return render(
request,
"common/form.html",
{
"form": key_form,
"title": _("Add SSH key"),
},
)
def ssh_key_delete(request, pk):
ssh_key = get_object_or_404(UserSSHKey, pk=pk, user=request.user)
if request.method == "POST":
ssh_key.delete()
messages.success(request, _("SSH key deleted"))
return redirect("accounts:profile")
return render(
request,
"common/confirm_delete.html",
{
"object": ssh_key,
"title": _("Delete SSH key"),
},
)
@superuser_only
def account(request, user_id): def account(request, user_id):
"""
:param request:
:return:
"""
if not request.user.is_superuser:
return HttpResponseRedirect(reverse('index'))
error_messages = []
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
user_insts = UserInstance.objects.filter(user_id=user_id) user_insts = UserInstance.objects.filter(user_id=user_id)
instances = Instance.objects.all().order_by('name') instances = Instance.objects.all().order_by("name")
publickeys = UserSSHKey.objects.filter(user_id=user_id) publickeys = UserSSHKey.objects.filter(user_id=user_id)
if request.method == 'POST': return render(
if 'delete' in request.POST: request,
user_inst = request.POST.get('user_inst', '') "account.html",
del_user_inst = UserInstance.objects.get(id=user_inst) {
del_user_inst.delete() "user": user,
return HttpResponseRedirect(request.get_full_path()) "user_insts": user_insts,
if 'permission' in request.POST: "instances": instances,
user_inst = request.POST.get('user_inst', '') "publickeys": publickeys,
inst_vnc = request.POST.get('inst_vnc', '') "otp_enabled": settings.OTP_ENABLED,
inst_change = request.POST.get('inst_change', '') },
inst_delete = request.POST.get('inst_delete', '') )
edit_user_inst = UserInstance.objects.get(id=user_inst)
edit_user_inst.is_change = bool(inst_change)
edit_user_inst.is_delete = bool(inst_delete)
edit_user_inst.is_vnc = bool(inst_vnc)
edit_user_inst.save()
return HttpResponseRedirect(request.get_full_path())
if 'add' in request.POST:
inst_id = request.POST.get('inst_id', '')
if settings.ALLOW_INSTANCE_MULTIPLE_OWNER:
check_inst = UserInstance.objects.filter(instance_id=int(inst_id), user_id=int(user_id))
else:
check_inst = UserInstance.objects.filter(instance_id=int(inst_id))
if check_inst: @permission_required("accounts.change_password", raise_exception=True)
msg = _("Instance already added") def change_password(request):
error_messages.append(msg) form = PasswordChangeForm(request.user, request.POST or None)
else:
add_user_inst = UserInstance(instance_id=int(inst_id), user_id=int(user_id))
add_user_inst.save()
return HttpResponseRedirect(request.get_full_path())
return render(request, 'account.html', locals()) if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important!
messages.success(request, _("Password Changed"))
return redirect("accounts:profile")
return render(request, "accounts/change_password_form.html", {"form": form})
@superuser_only
def user_instance_create(request, user_id):
user = get_object_or_404(User, pk=user_id)
form = forms.UserInstanceForm(request.POST or None, initial={"user": user})
if form.is_valid():
form.save()
return redirect(reverse("accounts:account", args=[user.id]))
return render(
request,
"common/form.html",
{
"form": form,
"title": _("Create User Instance"),
},
)
@superuser_only
def user_instance_update(request, pk):
user_instance = get_object_or_404(UserInstance, pk=pk)
form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
if form.is_valid():
form.save()
return redirect(reverse("accounts:account", args=[user_instance.user.id]))
return render(
request,
"common/form.html",
{
"form": form,
"title": _("Update User Instance"),
},
)
@superuser_only
def user_instance_delete(request, pk):
user_instance = get_object_or_404(UserInstance, pk=pk)
if request.method == "POST":
user = user_instance.user
user_instance.delete()
next = request.GET.get("next", None)
if next:
return redirect(next)
else:
return redirect(reverse("accounts:account", args=[user.id]))
return render(
request,
"common/confirm_delete.html",
{"object": user_instance},
)
def email_otp(request):
form = EmailOTPForm(request.POST or None)
if form.is_valid():
UserModel = get_user_model()
try:
user = UserModel.objects.get(email=form.cleaned_data["email"])
except UserModel.DoesNotExist:
pass
else:
device = get_user_totp_device(user)
send_email_with_otp(user, device)
messages.success(request, _("OTP Sent to %(email)s") % {"email": form.cleaned_data["email"]})
return redirect("accounts:login")
return render(
request,
"accounts/email_otp_form.html",
{
"form": form,
"title": _("Email OTP"),
},
)
@superuser_only
def admin_email_otp(request, user_id):
user = get_object_or_404(get_user_model(), pk=user_id)
device = get_user_totp_device(user)
if user.email != "":
send_email_with_otp(user, device)
messages.success(request, _("OTP QR code was emailed to user %(user)s") % {"user": user})
else:
messages.error(request, _("User email not set, failed to send QR code"))
return redirect("accounts:account", user.id)

1
admin/__init__.py Normal file
View file

@ -0,0 +1 @@
defautl_app_config = 'admin.apps.AdminConfig'

5
admin/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AdminConfig(AppConfig):
name = 'admin'

10
admin/decorators.py Normal file
View file

@ -0,0 +1,10 @@
from django.core.exceptions import PermissionDenied
def superuser_only(function):
def _inner(request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied
return function(request, *args, **kwargs)
return _inner

117
admin/forms.py Normal file
View file

@ -0,0 +1,117 @@
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth.models import Group, User
from django.urls import reverse_lazy
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from accounts.models import UserAttributes
from .models import Permission
class GroupForm(forms.ModelForm):
permissions = forms.ModelMultipleChoiceField(
widget=forms.CheckboxSelectMultiple,
queryset=Permission.objects.filter(content_type__model="permissionset"),
required=False,
)
users = forms.ModelMultipleChoiceField(
widget=forms.CheckboxSelectMultiple,
queryset=User.objects.all(),
required=False,
)
def __init__(self, *args, **kwargs):
super(GroupForm, self).__init__(*args, **kwargs)
instance = getattr(self, "instance", None)
if instance and instance.id:
self.fields["users"].initial = self.instance.user_set.all()
def save_m2m(self):
self.instance.user_set.set(self.cleaned_data["users"])
def save(self, *args, **kwargs):
instance = super(GroupForm, self).save()
self.save_m2m()
return instance
class Meta:
model = Group
fields = "__all__"
class UserForm(forms.ModelForm):
user_permissions = forms.ModelMultipleChoiceField(
widget=forms.CheckboxSelectMultiple,
queryset=Permission.objects.filter(content_type__model="permissionset"),
label=_("Permissions"),
required=False,
)
groups = forms.ModelMultipleChoiceField(
widget=forms.CheckboxSelectMultiple,
queryset=Group.objects.all(),
label=_("Groups"),
required=False,
)
class Meta:
model = User
fields = [
"username",
"groups",
"first_name",
"last_name",
"email",
"user_permissions",
"is_staff",
"is_active",
"is_superuser",
]
def __init__(self, *args, **kwargs):
super(UserForm, self).__init__(*args, **kwargs)
if self.instance.id:
password = ReadOnlyPasswordHashField(
label=_("Password"),
help_text=format_lazy(
_(
"""Raw passwords are not stored, so there is no way to see this user's password,
but you can change the password using <a href='{}'>this form</a>."""
),
reverse_lazy(
"admin:user_update_password",
args=[
self.instance.id,
],
),
),
)
self.fields["Password"] = password
class UserCreateForm(UserForm):
password = forms.CharField(widget=forms.PasswordInput)
class Meta:
model = User
fields = [
"username",
"password",
"groups",
"first_name",
"last_name",
"email",
"user_permissions",
"is_staff",
"is_active",
"is_superuser",
]
class UserAttributesForm(forms.ModelForm):
class Meta:
model = UserAttributes
exclude = ["user", "can_clone_instances"]

View file

@ -0,0 +1,29 @@
# Generated by Django 2.2.12 on 2020-05-27 07:01
import django.contrib.auth.models
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Permission',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('auth.permission',),
managers=[
('objects', django.contrib.auth.models.PermissionManager()),
],
),
]

View file

@ -0,0 +1,14 @@
# Generated by Django 2.2.12 on 2020-06-09 08:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('admin', '0001_initial'),
('auth', '0011_update_proxy_permissions'),
]
operations = [
]

13
admin/models.py Normal file
View file

@ -0,0 +1,13 @@
from django.contrib.auth.models import Permission as P
class Permission(P):
"""
Proxy model to Django Permissions model allows us to override __str__
"""
def __str__(self):
return f'{self.content_type.app_label}: {self.name}'
class Meta:
proxy = True

View file

@ -0,0 +1,62 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load icons %}
{% block title %}{% trans "Users" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<a href="{% url 'admin:group_create' %}" class="btn btn-success btn-header float-right">
{% icon 'plus' %}
</a>
<div class="float-right search">
<input id="filter" class="form-control" type="text" placeholder="{% trans "Search" %}">
</div>
<h1 class="page-header">{% trans "Groups" %}</h1>
</div>
</div>
<div class="row">
{% if not groups %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% icon 'exclamation-triangle '%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any groups" %}
</div>
</div>
{% else %}
<div class="col-lg-12">
<table class="table table-striped table-hover">
<thead>
<tr>
<th span="col">{% trans "Group Name" %}</th>
<th span="col">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="searchable">
{% for group in groups %}
<tr>
<td>
<a href=""><strong>{{ group.name }}</strong></a>
</td>
<td>
<div class="float-right btn-group">
<a class="btn btn-primary" href="{% url 'admin:group_update' group.id %}" title="{%trans "Edit" %}">
{% icon 'pencil' %}
</a>
<a class="btn btn-danger" href="{% url 'admin:group_delete' group.id %}" title="{%trans "Delete" %}">
{% icon 'times' %}
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock content %}
{% block script %}
<script src="{% static "js/filter-table.js" %}"></script>
{% endblock script %}

View file

@ -0,0 +1,48 @@
{% extends "base.html" %}
{% load i18n %}
{% load bootstrap4 %}
{% block title %}{% trans "Logs" %}{% endblock %}
{% block page_heading %}{% trans "Logs" %}{% endblock page_heading %}
{% block content %}
<div class="row">
<div class="col-lg-12">
{% if not logs %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any Logs" %}
</div>
</div>
{% else %}
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">{% trans "Date" %}</th>
<th scope="col">{% trans "User" %}</th>
<th scope="col">{% trans "Instance" %}</th>
<th scope="col">{% trans "Message" %}</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.id }}</td>
<td style="width:130px;">{{ log.date|date:"M d H:i:s" }}</td>
<td>{{ log.user }}</td>
<td>{{ log.instance }}</td>
<td>{{ log.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% bootstrap_pagination logs %}
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load icons %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block page_heading %}{{ title }}{% endblock page_heading %}
{% block content %}
<div class="card col-sm-10 offset-1">
<div class="card-body">
<form id="create-update" action="" method="post">
{% csrf_token %}
{% bootstrap_form user_form layout='horizontal' %}
{% bootstrap_form attributes_form layout='horizontal' %}
</form>
<div class="form-group float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
<button type="submit" form="create-update" class="btn btn-success">
{% icon 'check' %} {% trans "Save" %}
</button>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,82 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load common_tags %}
{% load icons %}
{% block title %}{{ title }}{% endblock %}
{% block page_heading %}{{ title }}{% endblock page_heading %}
{% block page_heading_extra %}
<a href="{% url 'admin:user_create' %}" class="btn btn-success btn-header float-right">
{% icon 'plus' %}
</a>
<div class="float-right search">
<input id="filter" class="form-control" type="text" placeholder="{% trans "Search" %}">
</div>
{% endblock page_heading_extra %}
{% block content %}
<div class="row">
{% if not users %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% icon 'exclamation-triangle '%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any user" %}
</div>
</div>
{% else %}
<div class="col-lg-12">
<table class="table table-striped table-hover">
<thead>
<tr>
<th span="col">{% trans "Username" %}</th>
<th span="col">{% trans "Status" %}</th>
<th span="col">{% trans "Staff" %}</th>
<th span="col">{% trans "Superuser" %}</th>
<th span="col">{% trans "Can Clone" %}</th>
<th span="col">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="searchable">
{% for user in users %}
{% has_perm user 'instances.clone_instances' as can_clone %}
<tr class="{% if not user.is_active %}danger{% endif %}">
<td>
{{ user.username }}
</td>
<td>
{% if user.is_active %}
{% trans "Active" %}
{% else %}
{% trans "Blocked" %}
{% endif %}
</td>
<td>{% if user.is_staff %}{% icon 'check' %}{% endif %}</td>
<td>{% if user.is_superuser %}{% icon 'check' %}</span>{% endif %}</td>
<td>{% if can_clone %}{% icon 'check' %}{% endif %}</td>
<td>
<div class="float-right btn-group">
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'accounts:account' user.id %}">{% icon 'eye' %}</a>
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% icon 'pencil' %}</a>
{% if user.is_active %}
<a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% icon 'stop' %}</a>
{% else %}
<a class="btn btn-success" title="{%trans "Unblock" %}" href="{% url 'admin:user_unblock' user.id %}">{% icon 'play' %}</a>
{% endif %}
<a class="btn btn-danger" title="{%trans "Delete" %}" href="{% url 'admin:user_delete' user.id %}">{% icon 'times' %}</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock content %}
{% block script %}
<script src="{% static "js/filter-table.js" %}"></script>
{% endblock script %}

120
admin/tests.py Normal file
View file

@ -0,0 +1,120 @@
from django.contrib.auth.models import Group, User
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import reverse
from django.test import TestCase
from accounts.models import UserAttributes
class AdminTestCase(TestCase):
def setUp(self):
self.client.login(username='admin', password='admin')
def test_group_list(self):
response = self.client.get(reverse('admin:group_list'))
self.assertEqual(response.status_code, 200)
def test_groups(self):
response = self.client.get(reverse('admin:group_create'))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('admin:group_create'), {'name': 'Test Group'})
self.assertRedirects(response, reverse('admin:group_list'))
group = Group.objects.get(name='Test Group')
self.assertEqual(group.id, 1)
response = self.client.get(reverse('admin:group_update', args=[1]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('admin:group_update', args=[1]), {'name': 'Updated Group Test'})
self.assertRedirects(response, reverse('admin:group_list'))
group = Group.objects.get(id=1)
self.assertEqual(group.name, 'Updated Group Test')
response = self.client.get(reverse('admin:group_delete', args=[1]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('admin:group_delete', args=[1]))
self.assertRedirects(response, reverse('admin:group_list'))
with self.assertRaises(ObjectDoesNotExist):
Group.objects.get(id=1)
def test_user_list(self):
response = self.client.get(reverse('admin:user_list'))
self.assertEqual(response.status_code, 200)
def test_users(self):
response = self.client.get(reverse('admin:user_create'))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('admin:user_create'),
{
'username': 'test',
'password': 'test',
'max_instances': 1,
'max_cpus': 1,
'max_memory': 1024,
'max_disk_size': 4,
},
)
self.assertRedirects(response, reverse('admin:user_list'))
user = User.objects.get(username='test')
self.assertEqual(user.id, 2)
ua: UserAttributes = UserAttributes.objects.get(id=2)
self.assertEqual(ua.user_id, 2)
self.assertEqual(ua.max_instances, 1)
self.assertEqual(ua.max_cpus, 1)
self.assertEqual(ua.max_memory, 1024)
self.assertEqual(ua.max_disk_size, 4)
response = self.client.get(reverse('admin:user_update', args=[2]))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('admin:user_update', args=[2]),
{
'username': 'utest',
'max_instances': 2,
'max_cpus': 2,
'max_memory': 2048,
'max_disk_size': 8,
},
)
self.assertRedirects(response, reverse('admin:user_list'))
user = User.objects.get(id=2)
self.assertEqual(user.username, 'utest')
ua: UserAttributes = UserAttributes.objects.get(id=2)
self.assertEqual(ua.user_id, 2)
self.assertEqual(ua.max_instances, 2)
self.assertEqual(ua.max_cpus, 2)
self.assertEqual(ua.max_memory, 2048)
self.assertEqual(ua.max_disk_size, 8)
response = self.client.get(reverse('admin:user_block', args=[2]))
user = User.objects.get(id=2)
self.assertFalse(user.is_active)
response = self.client.get(reverse('admin:user_unblock', args=[2]))
user = User.objects.get(id=2)
self.assertTrue(user.is_active)
response = self.client.get(reverse('admin:user_delete', args=[2]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('admin:user_delete', args=[2]))
self.assertRedirects(response, reverse('admin:user_list'))
with self.assertRaises(ObjectDoesNotExist):
User.objects.get(id=2)
def test_logs(self):
response = self.client.get(reverse('admin:logs'))
self.assertEqual(response.status_code, 200)

18
admin/urls.py Normal file
View file

@ -0,0 +1,18 @@
from django.urls import path
from . import views
urlpatterns = [
path('groups/', views.group_list, name='group_list'),
path('groups/create/', views.group_create, name='group_create'),
path('groups/<int:pk>/update/', views.group_update, name='group_update'),
path('groups/<int:pk>/delete/', views.group_delete, name='group_delete'),
path('users/', views.user_list, name='user_list'),
path('users/create/', views.user_create, name='user_create'),
path('users/<int:pk>/update_password/', views.user_update_password, name='user_update_password'),
path('users/<int:pk>/update/', views.user_update, name='user_update'),
path('users/<int:pk>/delete/', views.user_delete, name='user_delete'),
path('users/<int:pk>/block/', views.user_block, name='user_block'),
path('users/<int:pk>/unblock/', views.user_unblock, name='user_unblock'),
path('logs/', views.logs, name='logs'),
]

206
admin/views.py Normal file
View file

@ -0,0 +1,206 @@
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import AdminPasswordChangeForm
from django.contrib.auth.models import Group, User
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from accounts.models import UserAttributes, UserInstance, Instance
from appsettings.settings import app_settings
from logs.models import Logs
from . import forms
from .decorators import superuser_only
@superuser_only
def group_list(request):
groups = Group.objects.all()
return render(
request,
"admin/group_list.html",
{
"groups": groups,
},
)
@superuser_only
def group_create(request):
form = forms.GroupForm(request.POST or None)
if form.is_valid():
form.save()
return redirect("admin:group_list")
return render(
request,
"common/form.html",
{
"form": form,
"title": _("Create Group"),
},
)
@superuser_only
def group_update(request, pk):
group = get_object_or_404(Group, pk=pk)
form = forms.GroupForm(request.POST or None, instance=group)
if form.is_valid():
form.save()
return redirect("admin:group_list")
return render(
request,
"common/form.html",
{
"form": form,
"title": _("Update Group"),
},
)
@superuser_only
def group_delete(request, pk):
group = get_object_or_404(Group, pk=pk)
if request.method == "POST":
group.delete()
return redirect("admin:group_list")
return render(
request,
"common/confirm_delete.html",
{"object": group},
)
@superuser_only
def user_list(request):
users = User.objects.all()
return render(
request,
"admin/user_list.html",
{
"users": users,
"title": _("Users"),
},
)
@superuser_only
def user_create(request):
user_form = forms.UserCreateForm(request.POST or None)
attributes_form = forms.UserAttributesForm(request.POST or None)
if user_form.is_valid() and attributes_form.is_valid():
user = user_form.save()
password = user_form.cleaned_data["password"]
user.set_password(password)
user.save()
attributes = attributes_form.save(commit=False)
attributes.user = user
attributes.save()
add_default_instances(user)
return redirect("admin:user_list")
return render(
request,
"admin/user_form.html",
{"user_form": user_form, "attributes_form": attributes_form, "title": _("Create User")},
)
@superuser_only
def user_update(request, pk):
user = get_object_or_404(User, pk=pk)
attributes = UserAttributes.objects.get(user=user)
user_form = forms.UserForm(request.POST or None, instance=user)
attributes_form = forms.UserAttributesForm(request.POST or None, instance=attributes)
if user_form.is_valid() and attributes_form.is_valid():
user_form.save()
attributes_form.save()
next = request.GET.get("next")
return redirect(next or "admin:user_list")
return render(
request,
"admin/user_form.html",
{"user_form": user_form, "attributes_form": attributes_form, "title": _("Update User")},
)
@superuser_only
def user_update_password(request, pk):
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
form = AdminPasswordChangeForm(user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important!
messages.success(request, _("Password changed for %(user)s") % {"user": user.username})
return redirect("admin:user_list")
else:
messages.error(request, _("Wrong Data Provided"))
else:
form = AdminPasswordChangeForm(user)
return render(
request,
"accounts/change_password_form.html",
{
"form": form,
"user": user.username,
},
)
@superuser_only
def user_delete(request, pk):
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
user.delete()
return redirect("admin:user_list")
return render(
request,
"common/confirm_delete.html",
{"object": user},
)
@superuser_only
def user_block(request, pk):
user: User = get_object_or_404(User, pk=pk)
user.is_active = False
user.save()
return redirect("admin:user_list")
@superuser_only
def user_unblock(request, pk):
user: User = get_object_or_404(User, pk=pk)
user.is_active = True
user.save()
return redirect("admin:user_list")
@superuser_only
def logs(request):
l = Logs.objects.order_by("-date")
paginator = Paginator(l, int(app_settings.LOGS_PER_PAGE))
page = request.GET.get("page", 1)
logs = paginator.page(page)
return render(request, "admin/logs.html", {"logs": logs})
def add_default_instances(user):
"""
Adds instances listed in NEW_USER_DEFAULT_INSTANCES to user
"""
existing_instances = UserInstance.objects.filter(user=user)
if not existing_instances:
for instance_name in settings.NEW_USER_DEFAULT_INSTANCES:
instance = Instance.objects.get(name=instance_name)
user_instance = UserInstance(user=user, instance=instance)
user_instance.save()

5
appsettings/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AppsettingsConfig(AppConfig):
name = 'appsettings'

View file

@ -0,0 +1,13 @@
from .settings import app_settings as settings
def app_settings(request):
"""
Simple context processor that puts the config into every\
RequestContext. Just make sure you have a setting like this::
TEMPLATE_CONTEXT_PROCESSORS = (
# ...
'appsettings.context_processors.app_settings',
)
"""
return {"app_settings": settings}

10
appsettings/middleware.py Normal file
View file

@ -0,0 +1,10 @@
from .settings import app_settings, get_settings
class AppSettingsMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
get_settings()
return self.get_response(request)

View file

@ -0,0 +1,25 @@
# Generated by Django 2.2.12 on 2020-05-27 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AppSettings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=25)),
('key', models.CharField(db_index=True, max_length=50, unique=True)),
('value', models.CharField(max_length=25)),
('choices', models.CharField(max_length=70)),
('description', models.CharField(max_length=100, null=True)),
],
),
]

View file

@ -0,0 +1,79 @@
# Generated by Django 2.2.12 on 2020-05-23 12:05
from django.db import migrations
from django.utils.translation import gettext_lazy as _
def add_default_settings(apps, schema_editor):
setting = apps.get_model("appsettings", "AppSettings")
db_alias = schema_editor.connection.alias
setting.objects.using(db_alias).bulk_create([
setting(1, _("Theme"), "BOOTSTRAP_THEME", "flaty", "", _("Bootstrap CSS & Bootswatch Theme")),
setting(2, _("Theme SASS Path"), "SASS_DIR", "dev/scss/", "", _("Bootstrap SASS & Bootswatch SASS Directory")),
setting(3, _("All Instances View Style"), "VIEW_INSTANCES_LIST_STYLE", "grouped", "grouped,nongrouped", _("All instances list style")),
setting(4, _("Logs per Page"), "LOGS_PER_PAGE", "100", "", _("Pagination for logs")),
setting(5, _("Multiple Owner for VM"), "ALLOW_INSTANCE_MULTIPLE_OWNER", "True", "True,False", _("Allow to have multiple owner for instance")),
setting(6, _("Quota Debug"), "QUOTA_DEBUG", "True", "True,False", _("Debug for user quotas")),
setting(7, _("Disk Format"), "INSTANCE_VOLUME_DEFAULT_FORMAT", "qcow2", "raw,qcow,qcow2", _("Instance disk format")),
setting(8, _("Disk Bus"), "INSTANCE_VOLUME_DEFAULT_BUS", "virtio", "virtio,scsi,ide,usb,sata", _("Instance disk bus type")),
setting(9, _("Disk SCSI Controller"), "INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER", "virtio-scsi", "virtio-scsi, lsilogic, virtio-blk", _("SCSI controller type")),
setting(10, _("Disk Cache"), "INSTANCE_VOLUME_DEFAULT_CACHE", "directsync", "default,directsync,none,unsafe,writeback,writethrough", _("Disk volume cache type")),
setting(11, _("Disk IO Type"), "INSTANCE_VOLUME_DEFAULT_IO", "default", "default,native,threads", _("Volume io modes")),
setting(12, _("Disk Detect Zeroes"), "INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES", "default", "default,on,off,unmap", _("Volume detect zeroes mode")),
setting(13, _("Disk Discard"), "INSTANCE_VOLUME_DEFAULT_DISCARD", "default", "default,unmap,ignore", _("Volume discard mode")),
setting(14, _("Disk Owner UID"), "INSTANCE_VOLUME_DEFAULT_OWNER_UID", "0", "", _("Owner UID: up to os, 0=root, 107=qemu or libvirt-bin(for ubuntu)")),
setting(15, _("Disk Owner GID"), "INSTANCE_VOLUME_DEFAULT_OWNER_GID", "0", "", _("Owner GID: up to os, 0=root, 107=qemu or libvirt-bin(for ubuntu)")),
setting(16, _("VM CPU Mode"), "INSTANCE_CPU_DEFAULT_MODE", "host-model", "no-model,host-model,host-passthrough,custom", _("Cpu modes")),
setting(17, _("VM Machine Type"), "INSTANCE_MACHINE_DEFAULT_TYPE", "q35", "q35,x86_64", _("Chipset/Machine type")),
setting(18, _("VM Firmware Type"), "INSTANCE_FIRMWARE_DEFAULT_TYPE", "BIOS", "BIOS,UEFI", _("Firmware type for x86_64")),
setting(19, _("VM Architecture Type"), "INSTANCE_ARCH_DEFAULT_TYPE", "x86_64", "x86_64,i686", _("Architecture type: x86_64, i686, etc")),
setting(20, _("VM Console Type"), "QEMU_CONSOLE_DEFAULT_TYPE", "vnc", "vnc,spice", _("Default console type")),
setting(21, _("VM Clone Name Prefix"), "CLONE_INSTANCE_DEFAULT_PREFIX", "instance", "True,False", _("Prefix for cloned instance name")),
setting(22, _("VM Clone Auto Name"), "CLONE_INSTANCE_AUTO_NAME", "False", "True,False", _("Generated name for cloned instance")),
setting(23, _("VM Clone Auto Migrate"), "CLONE_INSTANCE_AUTO_MIGRATE", "False", "True,False", _("Auto migrate instance after clone")),
setting(24, _("VM Bottom Bar"), "VIEW_INSTANCE_DETAIL_BOTTOM_BAR", "True", "True,False", _("Bottom navbar for instance details")),
setting(25, _("Show Access Root Pass"), "SHOW_ACCESS_ROOT_PASSWORD", "False", "True,False", _("Show access root password")),
setting(26, _("Show Access SSH Keys"), "SHOW_ACCESS_SSH_KEYS", "False", "True,False", _("Show access ssh keys")),
])
def del_default_settings(apps, schema_editor):
setting = apps.get_model("appsettings", "AppSettings")
db_alias = schema_editor.connection.alias
setting.objects.using(db_alias).filter(key="QEMU_CONSOLE_DEFAULT_TYPE").delete()
setting.objects.using(db_alias).filter(key="ALLOW_INSTANCE_MULTIPLE_OWNER").delete()
setting.objects.using(db_alias).filter(key="CLONE_INSTANCE_DEFAULT_PREFIX").delete()
setting.objects.using(db_alias).filter(key="CLONE_INSTANCE_AUTO_NAME").delete()
setting.objects.using(db_alias).filter(key="CLONE_INSTANCE_AUTO_MIGRATE").delete()
setting.objects.using(db_alias).filter(key="LOGS_PER_PAGE").delete()
setting.objects.using(db_alias).filter(key="QUOTA_DEBUG").delete()
setting.objects.using(db_alias).filter(key="VIEW_INSTANCES_LIST_STYLE").delete()
setting.objects.using(db_alias).filter(key="VIEW_INSTANCE_DETAIL_BOTTOM_BAR").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_FORMAT").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_BUS").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_SCSI_CONTROLLER").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_CACHE").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_IO").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_DETECT_ZEROES").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_DISCARD").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_OWNER_UID").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_VOLUME_DEFAULT_OWNER_GID").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_CPU_DEFAULT_MODE").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_MACHINE_DEFAULT_TYPE").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_FIRMWARE_DEFAULT_TYPE").delete()
setting.objects.using(db_alias).filter(key="INSTANCE_ARCH_DEFAULT_TYPE").delete()
setting.objects.using(db_alias).filter(key="BOOTSTRAP_THEME").delete()
setting.objects.using(db_alias).filter(key="SASS_DIR").delete()
setting.objects.using(db_alias).filter(key="SHOW_ACCESS_ROOT_PASSWORD").delete()
setting.objects.using(db_alias).filter(key="SHOW_ACCESS_SSH_KEYS").delete()
class Migration(migrations.Migration):
dependencies = [
('appsettings', '0001_initial'),
]
operations = [
migrations.RunPython(add_default_settings, del_default_settings),
]

View file

@ -0,0 +1,38 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('appsettings', '0002_auto_20200527_1603'),
]
operations = [
migrations.AlterField(
model_name='appsettings',
name='choices',
field=models.CharField(max_length=70, verbose_name='choices'),
),
migrations.AlterField(
model_name='appsettings',
name='description',
field=models.CharField(max_length=100, null=True, verbose_name='description'),
),
migrations.AlterField(
model_name='appsettings',
name='key',
field=models.CharField(db_index=True, max_length=50, unique=True, verbose_name='key'),
),
migrations.AlterField(
model_name='appsettings',
name='name',
field=models.CharField(max_length=25, verbose_name='name'),
),
migrations.AlterField(
model_name='appsettings',
name='value',
field=models.CharField(max_length=25, verbose_name='value'),
),
]

View file

@ -0,0 +1,35 @@
# Generated by Django 2.2.13 on 2020-07-16 06:37
from django.db import migrations
from django.utils.translation import gettext_lazy as _
def add_default_settings(apps, schema_editor):
setting = apps.get_model("appsettings", "AppSettings")
db_alias = schema_editor.connection.alias
setting.objects.using(db_alias).bulk_create([
setting(27, _("Console Scale"), "CONSOLE_SCALE", "False", "True,False", _("Allow console to scaling view")),
setting(28, _("Console View-Only"), "CONSOLE_VIEW_ONLY", "False", "True,False", _("Allow only view not modify")),
setting(29, _("Console Resize Session"), "CONSOLE_RESIZE_SESSION", "False", "True,False", _("Allow to resize session for console")),
setting(30, _("Console Clip Viewport"), "CONSOLE_CLIP_VIEWPORT", "False", "True,False", _("Clip console viewport")),
])
def del_default_settings(apps, schema_editor):
setting = apps.get_model("appsettings", "AppSettings")
db_alias = schema_editor.connection.alias
setting.objects.using(db_alias).filter(key="CONSOLE_SCALE").delete()
setting.objects.using(db_alias).filter(key="CONSOLE_VIEW_ONLY").delete()
setting.objects.using(db_alias).filter(key="CONSOLE_RESIZE_SESSION").delete()
setting.objects.using(db_alias).filter(key="CONSOLE_CLIP_VIEWPORT").delete()
class Migration(migrations.Migration):
dependencies = [
('appsettings', '0003_auto_20200615_0637'),
]
operations = [
migrations.RunPython(add_default_settings, del_default_settings),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.14 on 2020-09-11 12:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('appsettings', '0004_auto_20200716_0637'),
]
operations = [
migrations.AlterField(
model_name='appsettings',
name='choices',
field=models.CharField(max_length=70, verbose_name='choices'),
),
]

14
appsettings/models.py Normal file
View file

@ -0,0 +1,14 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class AppSettings(models.Model):
def choices_as_list(self):
return self.choices.split(',')
name = models.CharField(_('name'), max_length=25, null=False)
key = models.CharField(_('key'), db_index=True, max_length=50, unique=True)
value = models.CharField(_('value'), max_length=25)
choices = models.CharField(_('choices'), max_length=70)
description = models.CharField(_('description'), max_length=100, null=True)

18
appsettings/settings.py Normal file
View file

@ -0,0 +1,18 @@
from .models import AppSettings
class Settings:
pass
app_settings = Settings()
def get_settings():
try:
entries = AppSettings.objects.all()
except:
pass
for entry in entries:
setattr(app_settings, entry.key, entry.value)

View file

@ -0,0 +1,74 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Edit Settings" %}{% endblock title %}
{% block page_heading %}{% trans "Edit Settings" %}{% endblock page_heading %}
{% block content %}
<div class="">
<div class="col-lg-12">
<h3 class="page-header">{% trans "App Settings" %}</h3>
<form action="{% url 'set_language' %}" method="post" style="display:inline" aria-label="Edit language.name_local settings form">{% csrf_token %}
<div class="form-group row">
<input name="next" type="hidden" value="{{ redirect_to }}">
<label class="col-sm-3 col-form-label">{% trans "Language" %}</label>
<div class="col-sm-6">
<select name="language" class="form-control" onchange="this.form.submit()">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
</div>
</div>
</form>
{% if request.user.is_superuser %}
<form method="post" action="" role="form" aria-label="Edit sass directory settings form">{% csrf_token %}
<div class="form-group row">
<label class="col-sm-3 col-form-label">{% trans sass_dir.name %}</label>
<div class="col-sm-6">
<input class="form-control" name="{{ sass_dir.key }}" value="{{ sass_dir.value }}" onchange="this.form.submit()" title="{% trans sass_dir.description %}"/>
</div>
</div>
</form>
<form method="post" action="" role="form" aria-label="Edit theme settings form">{% csrf_token %}
<div class="form-group row">
<label class="col-sm-3 col-form-label">{% trans bootstrap_theme.name %}</label>
<div class="col-sm-6">
<select class="form-control" name="{{ bootstrap_theme.key }}" onchange="this.form.submit()" title="{% trans bootstrap_theme.description %}">
{% for theme in themes_list %}
<option {% if bootstrap_theme.value == theme %}selected{% endif %} value="{{ theme }}">{{ theme }}</option>
{% endfor %}
</select>
<span class="text-muted">{% trans "After change please full refresh page with 'Ctrl + F5' "%}</span>
</div>
</div>
</form>
{% endif %}
<h3 class="page-header">{% trans "Other Settings" %}</h3>
{% for setting in appsettings %}
<form method="post" action="" role="form" aria-label="{{setting.name}} form">{% csrf_token %}
<div class="form-group row">
<label class="col-sm-3 col-form-label">{% trans setting.name %}</label>
<div class="col-sm-6">
{% if setting.choices %}
<select class="form-control" name="{{ setting.key }}" onchange="this.form.submit()" title="{% trans setting.description %}">
{% for choice in setting.choices_as_list %}
<option {% if setting.value == choice %} selected {% endif %} value={{ choice }}>{% trans choice %}</option>
{% endfor %}
</select>
{% else %}
<input class="form-control" name="{{ setting.key }}" value="{{ setting.value }}" title="{% trans setting.description %}" onchange="this.form.submit()"/>
{% endif%}
</div>
</div>
</form>
{% endfor %}
</div>
</div>
{% endblock %}

91
appsettings/views.py Normal file
View file

@ -0,0 +1,91 @@
import os
import sass
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from logs.views import addlogmsg
from appsettings.models import AppSettings
@login_required
def appsettings(request):
"""
:param request:
:return:
"""
main_css = "wvc-main.min.css"
sass_dir = AppSettings.objects.get(key="SASS_DIR")
bootstrap_theme = AppSettings.objects.get(key="BOOTSTRAP_THEME")
try:
themes_list = os.listdir(sass_dir.value + "/wvc-theme")
except FileNotFoundError as err:
messages.error(request, err)
addlogmsg(request.user.username, "", err)
# Bootstrap settings related with filesystems, because of that they are excluded from other settings
appsettings = AppSettings.objects.exclude(description__startswith="Bootstrap").order_by("name")
if request.method == "POST":
if "SASS_DIR" in request.POST:
try:
sass_dir.value = request.POST.get("SASS_DIR", "")
sass_dir.save()
msg = _("SASS directory path is changed. Now: %(dir)s") % {"dir": sass_dir.value}
messages.success(request, msg)
except Exception as err:
msg = err
messages.error(request, msg)
addlogmsg(request.user.username, "", msg)
return HttpResponseRedirect(request.get_full_path())
if "BOOTSTRAP_THEME" in request.POST:
theme = request.POST.get("BOOTSTRAP_THEME", "")
scss_var = f"@import '{sass_dir.value}/wvc-theme/{theme}/variables';"
scss_bootswatch = f"@import '{sass_dir.value}/wvc-theme/{theme}/bootswatch';"
scss_boot = f"@import '{sass_dir.value}/bootstrap-overrides.scss';"
try:
with open(sass_dir.value + "/wvc-main.scss", "w") as main:
main.write(scss_var + "\n" + scss_boot + "\n" + scss_bootswatch + "\n")
css_compressed = sass.compile(
string=scss_var + "\n" + scss_boot + "\n" + scss_bootswatch,
output_style="compressed",
)
with open("static/css/" + main_css, "w") as css:
css.write(css_compressed)
bootstrap_theme.value = theme
bootstrap_theme.save()
msg = _("Theme is changed. Now: %(theme)s") % {"theme": theme}
messages.success(request, msg)
except Exception as err:
msg = err
messages.error(request, msg)
addlogmsg(request.user.username, "", msg)
return HttpResponseRedirect(request.get_full_path())
for setting in appsettings:
if setting.key in request.POST:
try:
setting.value = request.POST.get(setting.key, "")
setting.save()
msg = _("%(setting)s is changed. Now: %(value)s") % {"setting": setting.name, "value": setting.value}
messages.success(request, msg)
except Exception as err:
msg = err
messages.error(request, msg)
addlogmsg(request.user.username, "", msg)
return HttpResponseRedirect(request.get_full_path())
return render(request, "appsettings.html", locals())

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -1,166 +1,45 @@
import re
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from vrtManager.connection import CONN_SOCKET, CONN_SSH, CONN_TCP, CONN_TLS
from computes.models import Compute from computes.models import Compute
from .validators import validate_hostname
class ComputeAddTcpForm(forms.Form):
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
max_length=20)
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
max_length=100)
login = forms.CharField(error_messages={'required': _('No login has been entered')},
max_length=100)
password = forms.CharField(error_messages={'required': _('No password has been entered')},
max_length=100)
def clean_name(self):
name = self.cleaned_data['name']
have_symbol = re.match('[^a-zA-Z0-9._-]+', name)
if have_symbol:
raise forms.ValidationError(_('The host name must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The host name must not exceed 20 characters'))
try:
Compute.objects.get(name=name)
except Compute.DoesNotExist:
return name
raise forms.ValidationError(_('This host is already connected'))
def clean_hostname(self):
hostname = self.cleaned_data['hostname']
have_symbol = re.match('[^a-z0-9.-]+', hostname)
wrong_ip = re.match('^0.|^255.', hostname)
if have_symbol:
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
elif wrong_ip:
raise forms.ValidationError(_('Wrong IP address'))
try:
Compute.objects.get(hostname=hostname)
except Compute.DoesNotExist:
return hostname
raise forms.ValidationError(_('This host is already connected'))
class ComputeAddSshForm(forms.Form): class TcpComputeForm(forms.ModelForm):
name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, hostname = forms.CharField(validators=[validate_hostname])
max_length=20) type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP)
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
max_length=100)
login = forms.CharField(error_messages={'required': _('No login has been entered')},
max_length=20)
def clean_name(self): class Meta:
name = self.cleaned_data['name'] model = Compute
have_symbol = re.match('[^a-zA-Z0-9._-]+', name) widgets = {'password': forms.PasswordInput()}
if have_symbol: fields = '__all__'
raise forms.ValidationError(_('The name of the host must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The name of the host must not exceed 20 characters'))
try:
Compute.objects.get(name=name)
except Compute.DoesNotExist:
return name
raise forms.ValidationError(_('This host is already connected'))
def clean_hostname(self):
hostname = self.cleaned_data['hostname']
have_symbol = re.match('[^a-zA-Z0-9._-]+', hostname)
wrong_ip = re.match('^0.|^255.', hostname)
if have_symbol:
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
elif wrong_ip:
raise forms.ValidationError(_('Wrong IP address'))
try:
Compute.objects.get(hostname=hostname)
except Compute.DoesNotExist:
return hostname
raise forms.ValidationError(_('This host is already connected'))
class ComputeAddTlsForm(forms.Form): class SshComputeForm(forms.ModelForm):
name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP"))
max_length=20) type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH)
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
max_length=100)
login = forms.CharField(error_messages={'required': _('No login has been entered')},
max_length=100)
password = forms.CharField(error_messages={'required': _('No password has been entered')},
max_length=100)
def clean_name(self): class Meta:
name = self.cleaned_data['name'] model = Compute
have_symbol = re.match('[^a-zA-Z0-9._-]+', name) exclude = ['password']
if have_symbol:
raise forms.ValidationError(_('The host name must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The host name must not exceed 20 characters'))
try:
Compute.objects.get(name=name)
except Compute.DoesNotExist:
return name
raise forms.ValidationError(_('This host is already connected'))
def clean_hostname(self):
hostname = self.cleaned_data['hostname']
have_symbol = re.match('[^a-z0-9.-]+', hostname)
wrong_ip = re.match('^0.|^255.', hostname)
if have_symbol:
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
elif wrong_ip:
raise forms.ValidationError(_('Wrong IP address'))
try:
Compute.objects.get(hostname=hostname)
except Compute.DoesNotExist:
return hostname
raise forms.ValidationError(_('This host is already connected'))
class ComputeEditHostForm(forms.Form): class TlsComputeForm(forms.ModelForm):
host_id = forms.CharField() hostname = forms.CharField(validators=[validate_hostname])
name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS)
max_length=20)
hostname = forms.CharField(error_messages={'required': _('No IP / Domain name has been entered')},
max_length=100)
login = forms.CharField(error_messages={'required': _('No login has been entered')},
max_length=100)
password = forms.CharField(max_length=100)
def clean_name(self): class Meta:
name = self.cleaned_data['name'] model = Compute
have_symbol = re.match('[^a-zA-Z0-9._-]+', name) widgets = {'password': forms.PasswordInput()}
if have_symbol: fields = '__all__'
raise forms.ValidationError(_('The name of the host must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The name of the host must not exceed 20 characters'))
return name
def clean_hostname(self):
hostname = self.cleaned_data['hostname']
have_symbol = re.match('[^a-zA-Z0-9._-]+', hostname)
wrong_ip = re.match('^0.|^255.', hostname)
if have_symbol:
raise forms.ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
elif wrong_ip:
raise forms.ValidationError(_('Wrong IP address'))
return hostname
class ComputeAddSocketForm(forms.Form): class SocketComputeForm(forms.ModelForm):
name = forms.CharField(error_messages={'required': _('No hostname has been entered')}, hostname = forms.CharField(widget=forms.HiddenInput, initial='localhost')
max_length=20) type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET)
details = forms.CharField(error_messages={'required': _('No details has been entred')},
max_length=50)
def clean_name(self): class Meta:
name = self.cleaned_data['name'] model = Compute
have_symbol = re.match('[^a-zA-Z0-9._-]+', name) fields = ['name', 'details', 'hostname', 'type']
if have_symbol:
raise forms.ValidationError(_('The host name must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The host name must not exceed 20 characters'))
try:
Compute.objects.get(name=name)
except Compute.DoesNotExist:
return name
raise forms.ValidationError(_('This host is already connected'))

View file

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*- # Generated by Django 2.2.10 on 2020-01-28 07:01
from __future__ import unicode_literals
from django.db import models, migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
] ]
@ -13,15 +14,13 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Compute', name='Compute',
fields=[ fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20)), ('name', models.CharField(max_length=64)),
('hostname', models.CharField(max_length=20)), ('hostname', models.CharField(max_length=64)),
('login', models.CharField(max_length=20)), ('login', models.CharField(max_length=20)),
('password', models.CharField(max_length=14, null=True, blank=True)), ('password', models.CharField(blank=True, max_length=14, null=True)),
('details', models.CharField(blank=True, max_length=64, null=True)),
('type', models.IntegerField()), ('type', models.IntegerField()),
], ],
options={
},
bases=(models.Model,),
), ),
] ]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.12 on 2020-05-29 13:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('computes', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='compute',
name='name',
field=models.CharField(max_length=64, unique=True),
),
]

View file

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('computes', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='Compute',
name='details',
field=models.CharField(max_length=50, null=True, blank=True),
),
]

View file

@ -0,0 +1,38 @@
# Generated by Django 2.2.13 on 2020-06-15 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('computes', '0002_auto_20200529_1320'),
]
operations = [
migrations.AlterField(
model_name='compute',
name='details',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='details'),
),
migrations.AlterField(
model_name='compute',
name='hostname',
field=models.CharField(max_length=64, verbose_name='hostname'),
),
migrations.AlterField(
model_name='compute',
name='login',
field=models.CharField(max_length=20, verbose_name='login'),
),
migrations.AlterField(
model_name='compute',
name='name',
field=models.CharField(max_length=64, unique=True, verbose_name='name'),
),
migrations.AlterField(
model_name='compute',
name='password',
field=models.CharField(blank=True, max_length=14, null=True, verbose_name='password'),
),
]

View file

@ -1,13 +1,61 @@
from django.db import models from django.db.models import CharField, IntegerField, Model
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from libvirt import virConnect
from vrtManager.connection import connection_manager
from vrtManager.hostdetails import wvmHostDetails
class Compute(models.Model): class Compute(Model):
name = models.CharField(max_length=20) name = CharField(_('name'), max_length=64, unique=True)
hostname = models.CharField(max_length=20) hostname = CharField(_('hostname'), max_length=64)
login = models.CharField(max_length=20) login = CharField(_('login'), max_length=20)
password = models.CharField(max_length=14, blank=True, null=True) password = CharField(_('password'), max_length=14, blank=True, null=True)
details = models.CharField(max_length=50, null=True, blank=True) details = CharField(_('details'), max_length=64, null=True, blank=True)
type = models.IntegerField() type = IntegerField()
def __unicode__(self): @cached_property
return self.hostname def status(self):
# return connection_manager.host_is_up(self.type, self.hostname)
# TODO: looks like socket has problems connecting via VPN
if isinstance(self.connection, virConnect):
return True
else:
return self.connection
@cached_property
def connection(self):
try:
return connection_manager.get_connection(
self.hostname,
self.login,
self.password,
self.type,
)
except Exception as e:
return e
@cached_property
def proxy(self):
return wvmHostDetails(
self.hostname,
self.login,
self.password,
self.type,
)
@cached_property
def cpu_count(self):
return self.proxy.get_node_info()[3]
@cached_property
def ram_size(self):
return self.proxy.get_node_info()[2]
@cached_property
def ram_usage(self):
return self.proxy.get_memory_usage()['percent']
def __str__(self):
return self.name

View file

@ -1,226 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Computes" %}{% endblock %}
{% block content %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
{% include 'create_comp_block.html' %}
<h1 class="page-header">{% trans "Computes" %}</h1>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
<div class="row">
{% if computes_info %}
{% for compute in computes_info %}
<div id="{{ compute.name }}" class="col-xs-12 col-sm-4">
<div class="panel {% if compute.status %}panel-success{% else %}panel-danger{% endif %} panel-data">
<div class="panel-heading">
{% ifequal compute.status 1 %}
<h3 class="panel-title">
<a href="{% url 'overview' compute.id %}"><strong>{{ compute.name }}</strong></a>
<a data-toggle="modal" href="#editHost{{ compute.id }}" class="pull-right" title="{% trans "Edit" %}">
<i class="fa fa-cog"></i>
</a>
</h3>
{% else %}
<h3 class="panel-title"><strong>{{ compute.name }}</strong>
<a data-toggle="modal" href="#editHost{{ compute.id }}" class="pull-right" title="{% trans "Edit" %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</h3>
{% endifequal %}
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-4 col-sm-4">
<p><strong>{% trans "Status:" %}</strong></p>
</div>
<div class="col-xs-4 col-sm-6">
{% if compute.status %}
<p>{% trans "Connected" %}</p>
{% else %}
<p>{% trans "Not Connected" %}</p>
{% endif %}
{% if compute.details %}
<p>{% trans compute.details %}</p>
{% else %}
<p>{% trans "No details available" %}</p>
{% endif %}
</div>
</div>
<!-- Modal Edit -->
<div class="modal fade" id="editHost{{ compute.id }}" tabindex="-1" role="dialog" aria-labelledby="editHostLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{% trans "Edit connection" %}</h4>
</div>
{% ifequal compute.type 1 %}
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-zA-Z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" value="{{ compute.login }}">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" name="password" class="form-control" value="{{ compute.password }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="pull-left btn btn-danger" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_edit">
{% trans "Change" %}
</button>
</form>
</div>
{% endifequal %}
{% ifequal compute.type 2 %}
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<p class="modal-body">{% trans "Need create ssh <a href='https://github.com/retspen/webvirtmgr/wiki/Setup-SSH-Authorization'>authorization key</a>. If you have another SSH port on your server, you can add IP:PORT like '192.168.1.1:2222'." %}</p>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\:\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" value="{{ compute.login }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="pull-left btn btn-danger" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_edit">
{% trans "Change" %}
</button>
</form>
</div>
{% endifequal %}
{% ifequal compute.type 3 %}
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" value="{{ compute.hostname }}" required pattern="[a-z0-9\:\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" placeholder="{% trans "Name" %}">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" name="password" class="form-control" value="{{ compute.password }}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="pull-left btn btn-danger" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_edit">
{% trans "Change" %}
</button>
</form>
</div>
{% endifequal %}
{% ifequal compute.type 4 %}
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="hidden" name="host_id" value="{{ compute.id }}">
<input type="text" name="name" class="form-control" value="{{ compute.name }}" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="pull-left btn btn-danger" name="host_del">
{% trans "Delete" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_edit">
{% trans "Change" %}
</button>
</form>
</div>
{% endifequal %}
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<i class="fa fa-exclamation-triangle"></i> <strong>{% trans "Warning:" %}</strong> {% trans "Hypervisor doesn't have any Computes" %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% load bootstrap4 %}
{% load icons %}
{% load i18n %}
{% block title %}{% trans "Add Compute" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<h2 class="page-header">{% trans "Create Compute" %}</h2>
</div>
</div>
<div class="row">
<div class="thumbnail col-sm-10 offset-1">
<form id="create-update" action="" method="post">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
</form>
<div class="form-group float-right">
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
<button type="submit" form="create-update" class="btn btn-success">
{% icon 'check' %} {% trans "Save" %}
</button>
</div>
</div>
</div>
{% endblock content %}

View file

@ -0,0 +1,116 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load icons %}
{% block title %}{% trans "Instances" %} - {{ compute.name }}{% endblock %}
{% block style %}
<link rel="stylesheet" href="{% static "css/sortable-theme-bootstrap.css" %}" />
{% endblock %}
{% block page_heading %}{{ compute.name }} - {% trans "Instances" %}{% endblock page_heading %}
{% block page_heading_extra %}
<a href="{% url 'instances:create_instance_select_type' compute.id %}"
class="btn btn-success btn-header float-right">
{% icon 'plus' %}
</a>
{% if instances %}
<div class="float-right search">
<input id="filter" class="form-control" type="text" placeholder="{% trans 'Search' %}">
</div>
{% endif %}
{% endblock page_heading_extra %}
{% block content %}
<div class="row">
<div class="col-lg-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb bg-light shadow-sm">
<li class="breadcrumb-item active">
<a href="{% url 'overview' compute.id %}">{% icon 'dashboard' %} {% trans "Overview" %}</a>
</li>
<li class="breadcrumb-item">
<span class="font-weight-bold">{% icon 'server' %} {% trans "Instances" %}</span>
</li>
<li class="breadcrumb-item">
<a href="{% url 'storages' compute.id %}">{% icon 'hdd-o' %} {% trans "Storages" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'networks' compute.id %}">{% icon 'sitemap' %} {% trans "Networks" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'interfaces' compute.id %}">{% icon 'wifi' %} {% trans "Interfaces" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'nwfilters' compute.id %}">{% icon 'filter' %} {% trans "NWFilters" %}</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'secrets' compute.id %}">{% icon 'key' %} {% trans "Secrets" %}</a>
</li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col-lg-12">
{% if not instances %}
<div class="alert alert-warning alert-dismissable fade show">
{% icon 'exclamation-triangle' %} <strong>{% trans "Warning" %}:</strong>
{% trans "Hypervisor doesn't have any Instances" %}
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
</div>
</div>
{% else %}
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
<thead>
<tr>
<th scope="col">{% trans 'Name' %}<br>{% trans 'Description' %}</th>
<th scope="col">{% trans 'User' %}</th>
<th scope="col">{% trans 'Status' %}</th>
<th scope="col">{% trans 'VCPU' %}</th>
<th scope="col">{% trans 'Memory' %}</th>
<th scope="col" data-sortable="false">{% trans 'Actions' %}</th>
</tr>
</thead>
<tbody class="searchable">
{% for instance in instances %}
<tr>
<td>
<a class="text-secondary" href="{% url 'instances:instance' instance.id %}">
{{ instance.name }}
</a>
</td>
<td>
<em>
{% if instance.userinstance_set.all.count > 0 %}
{{ instance.userinstance_set.all.0.user }}
{% if instance.userinstance_set.all.count > 1 %}
(+{{ instance.userinstance_set.all.count|add:"-1" }})
{% endif %}
{% endif %}
</em>
</td>
<td>
{% if instance.proxy.instance.info.0 == 1 %}<span
class="text-success">{% trans "Active" %}</span>{% endif %}
{% if instance.proxy.instance.info.0 == 5 %}<span
class="text-danger">{% trans "Off" %}</span>{% endif %}
{% if instance.proxy.instance.info.0 == 3 %}<span
class="text-warning">{% trans "Suspended" %}</span>{% endif %}
</td>
<td>{{ instance.proxy.instance.info.3 }}</td>
<td>{% widthratio instance.proxy.instance.info.1 1024 1 %} MiB</td>
<td class="text-nowrap">
{% include 'instance_actions.html' %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endblock %}
{% block script %}
<script src="{% static "js/sortable.min.js" %}"></script>
<script src="{% static 'js/filter-table.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load common_tags %}
{% load icons %}
{% block title %}{% trans "Computes" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12">
{% include 'create_comp_block.html' %}
{% include 'search_block.html' %}
<h3 class="page-header">{% trans "Computes" %}</h3>
</div>
</div>
<div class="row">
{% if not computes %}
<div class="col-lg-12">
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% icon 'exclamation-triangle '%} <strong>{% trans "Warning" %}:</strong> {% trans "You don't have any computes" %}
</div>
</div>
{% else %}
<div class="col-lg-12">
<table class="table table-striped table-hover">
<thead>
<tr class="d-flex">
<th span="col" class="col-sm-3">{% trans "Name" %}</th>
<th span="col" class="col-sm-2">{% trans "Status" %}</th>
<th span="col" class="col-sm-5">{% trans "Details" %}</th>
<th span="col" class="col-sm-2 text-center">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="searchable">
{% for compute in computes %}
<tr class="d-flex">
<td class="col-sm-3">
{{ compute.name }}
</td>
<td class="col-sm-2">
{% if compute.status is True %}{% trans "Connected" %}{% else %}{% trans "Not Connected" %}{% endif %}
</td>
<td class="col-sm-5">
{{ compute.details|default:"" }}
</td>
<td class="col-sm-2">
<div class="float-right btn-group">
{% if compute.status is True %}
<a class="btn btn-success" title="{%trans "Overview" %}" href="{% url 'overview' compute.id %}">{% icon 'eye' %}</a>
{% else %}
<a class="btn btn-light" title="{%trans "Overview" %}">{% icon 'eye' %}</a>
{% endif %}
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'compute_update' compute.id %}">{% icon 'pencil' %}</a>
<a class="btn btn-danger" title="{%trans "Delete" %}" href="{% url 'compute_delete' compute.id %}">{% icon 'times' %}</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock content %}
{% block script %}
<script src="{% static "js/filter-table.js" %}"></script>
{% endblock script %}

View file

@ -1,167 +1,10 @@
{% load i18n %} {% load i18n %}
{% if request.user.is_superuser %} {% load bootstrap4 %}
<a href="#addHost" type="button" class="btn btn-success pull-right" data-toggle="modal"> {% load icons %}
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <div class="btn-group float-right mt-1" role="group" aria-label="Add host button group">
</a> <a href="{% url 'add_tcp_host' %}" class="btn btn-success">{% trans "TCP" %}</a>
<a href="{% url 'add_ssh_host' %}" class="btn btn-success">{% trans "SSH" %}</a>
<!-- Modal --> <a href="{% url 'add_tls_host' %}" class="btn btn-success">{% trans "TLS" %}</a>
<div class="modal fade" id="addHost" tabindex="-1" role="dialog" aria-labelledby="addHostLabel" aria-hidden="true"> <a href="{% url 'add_socket_host' %}" class="btn btn-success">{% trans "Local" %}</a>
<div class="modal-dialog"> <a href="#" class="btn btn-success disabled" title="{% trans "Add new host" %}">{% icon "plus" %}</a>
<div class="modal-content"> </div>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{% trans "Add Connection" %}</h4>
</div>
<div class="tabbable">
<ul class="nav nav-tabs">
<li class="active">
<a href="#1" data-toggle="tab">{% trans "TCP Connections" %}</a>
</li>
<li><a href="#2" data-toggle="tab">{% trans "SSH Connections" %}</a></li>
<li><a href="#3" data-toggle="tab">{% trans "TLS Connection" %}</a></li>
<li><a href="#4" data-toggle="tab">{% trans "Local Socket" %}</a></li>
</ul>
</div>
<div class="tab-content">
<div class="tab-pane active" id="1">
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" name="password" class="form-control" placeholder="{% trans "Password" %}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_tcp_add">
{% trans "Add" %}
</button>
</div>
</form>
</div>
<div class="tab-pane" id="2">
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<p class="modal-body">{% trans "You must create ssh <a href='https://github.com/retspen/webvirtmgr/wiki/Setup-SSH-Authorization'>authorization key</a>. If you have another SSH port on your server, you can add IP:PORT like '192.168.1.1:2222'." %}</p>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\:\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_ssh_add">
{% trans "Add" %}
</button>
</div>
</form>
</div>
<div class="tab-pane" id="3">
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "FQDN / IP" %}</label>
<div class="col-sm-6">
<input type="text" name="hostname" class="form-control" placeholder="{% trans "FQDN or IP Address" %}" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Username" %}</label>
<div class="col-sm-6">
<input type="text" name="login" class="form-control" placeholder="{% trans "Username" %}">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Password" %}</label>
<div class="col-sm-6">
<input type="password" name="password" class="form-control" placeholder="{% trans "Password" %}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_tls_add">
{% trans "Add" %}
</button>
</div>
</form>
</div>
<div class="tab-pane" id="4">
<div class="modal-body">
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Label" %}</label>
<div class="col-sm-6">
<input type="text" name="name" class="form-control" placeholder="Label Name" maxlength="20" required pattern="[a-z0-9\.\-_]+">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label">{% trans "Details" %}</label>
<div class="col-sm-6">
<input type="text" name="details" class="form-control" placeholder="{% trans "Details" %}">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
{% trans "Close" %}
</button>
<button type="submit" class="btn btn-primary" name="host_socket_add">
{% trans "Add" %}
</button>
</div>
</form>
</div>
</div> <!-- /.tab-content -->
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div><!-- /.modal -->
{% endif %}

View file

@ -1,152 +1,249 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load staticfiles %}
{% load icons %}
{% block title %}{% trans "Overview" %} - {{ compute.name }}{% endblock %} {% block title %}{% trans "Overview" %} - {{ compute.name }}{% endblock %}
{% block page_heading %}{{ compute.name }}{% endblock page_heading %}
{% block content %} {% block content %}
<!-- Page Heading --> <div class="row">
<div class="row"> <div class="col-lg-12">
<div class="col-lg-12"> <nav aria-label="breadcrumb">
<h1 class="page-header">{{ compute.name }}</h1> <ol class="breadcrumb bg-light shadow-sm">
<ol class="breadcrumb"> <li class="breadcrumb-item">
<li class="active"> <span class="font-weight-bold">{% icon 'dashboard' %} {% trans "Overview" %}</span>
<i class="fa fa-dashboard"></i> {% trans "Overview" %} </li>
</li> <li class="breadcrumb-item">
<li> <a href="{% url 'instances' compute.id %}">{% icon 'server' %} {% trans "Instances" %}</a>
<i class="fa fa-hdd-o"></i> <a href="{% url 'storages' compute.id %}">{% trans "Storages" %}</a> </li>
</li> <li class="breadcrumb-item">
<li> <a href="{% url 'storages' compute.id %}">{% icon 'hdd-o' %} {% trans "Storages" %}</a>
<i class="fa fa-sitemap"></i> <a href="{% url 'networks' compute.id %}">{% trans "Networks" %}</a> </li>
</li> <li class="breadcrumb-item">
<li> <a href="{% url 'networks' compute.id %}">{% icon 'sitemap' %} {% trans "Networks" %}</a>
<i class="fa fa-wifi"></i> <a href="{% url 'interfaces' compute.id %}">{% trans "Interfaces" %}</a> </li>
</li> <li class="breadcrumb-item">
<li> <a href="{% url 'interfaces' compute.id %}">{% icon 'wifi' %} {% trans "Interfaces" %}</a>
<i class="fa fa-key"></i> <a href="{% url 'secrets' compute.id %}">{% trans "Secrets" %}</a> </li>
</li> <li class="breadcrumb-item">
</ol> <a href="{% url 'nwfilters' compute.id %}">{% icon 'filter' %} {% trans "NWFilters" %}</a>
</div> </li>
</div> <li class="breadcrumb-item">
<!-- /.row --> <a href="{% url 'secrets' compute.id %}">{% icon 'key' %} {% trans "Secrets" %}</a>
</li>
</ol>
</nav>
</div>
</div>
{% include 'errors_block.html' %} <div class="shadow-sm">
<h3 class="page-header">{% trans "Basic details" %}</h3>
<div class="row" id="max-width-page"> <dl class="mx-3 row">
<h3 class="page-header">{% trans "Basic details" %}</h3> <dt class="col-3">{% trans "Hostname" %}</dt>
<div class="col-xs-4 col-sm-3"> <dd class="col-9">{{ hostname }}</dd>
<p>{% trans "Hostname" %}</p> <dt class="col-3">{% trans "Hypervisors" %}</dt>
<p>{% trans "Hypervisor" %}</p> <dd class="col-9">
<p>{% trans "Memory" %}</p> <div class="dropdown">
<p>{% trans "Architecture" %}</p> {% for arch, hpv in hypervisor.items|slice:":4" %}
<p>{% trans "Logical CPUs" %}</p> <button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<p>{% trans "Processor" %}</p> {{ arch }}
<p>{% trans "Connection" %}</p> </button>
<p>{% trans "Details" %}</p> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
</div> {% for h in hpv %}
<div class="col-xs-8 col-sm-7"> <a class="dropdown-item" href="#">{{ h }}</a>
<p>{{ hostname }}</p> {% endfor %}
<p>{{ hypervisor }}</p>
<p>{{ host_memory|filesizeformat }}</p>
<p>{{ host_arch }}</p>
<p>{{ logical_cpu }}</p>
<p>{{ model_cpu }}</p>
<p>{{ uri_conn }}</p>
<p>{{ compute.details }}</p>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<h3 class="page-header">{% trans "Performance" %}</h3>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-long-arrow-right"></i> {% trans "CPU utilization" %}</h3>
</div>
<div class="panel-body">
<div class="flot-chart">
<div class="flot-chart-content" id="flot-moving-line-chart" style="padding: 0px; position: relative;">
<canvas id="cpuChart" width="735" height="160"></canvas>
</div>
</div>
</div>
</div> </div>
<div class="panel panel-info"> {% endfor %}
<div class="panel-heading"> </div>
<h3 class="panel-title"><i class="fa fa-long-arrow-right"></i> {% trans "RAM utilization" %}</h3> <div class="dropdown">
</div> {% if hypervisor.items|length > 4 %}
<div class="panel-body"> <button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton{{ forloop.counter0 }}" data-toggle="dropdown">
<div class="flot-chart"> {{ hypervisor.items|slice:"4:"|length }} {% trans 'more' %}...
<div class="flot-chart-content" id="flot-moving-line-chart" style="padding: 0px; position: relative;"> </button>
<canvas id="memChart" width="735" height="160"></canvas> <div class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ forloop.counter0 }}" role="menu">
</div> {% for arc in hypervisor.keys|slice:"4:" %}
</div> <a class="dropdown-item" tabindex="-1" href="#">{{ arc }}</a>
</div> {% endfor %}
</div>
{% endif %}
</div>
</dd>
<dt class="col-3">{% trans "Emulator" %}</dt>
<dd class="col-9">{{ emulator }}</dd>
<dt class="col-3">{% trans "Version" %}</dt>
<dd class="col-9">
<span class="badge bg-secondary text-light">{% trans 'Qemu' %} </span>
<span class="badge bg-primary text-light">{{ version }}</span> &nbsp;
<span class="badge bg-secondary text-light">{% trans 'Libvirt' %} </span>
<span class="badge bg-primary text-light">{{ lib_version }}</span> &nbsp;
</dd>
<dt class="col-3">{% trans "Memory" %}</dt>
<dd class="col-9">{{ host_memory|filesizeformat }}</dd>
<dt class="col-3">{% trans "Architecture" %}</dt>
<dd class="col-9">{{ host_arch }}</dd>
<dt class="col-3">{% trans "Logical CPUs" %}</dt>
<dd class="col-9">{{ logical_cpu }}</dd>
<dt class="col-3">{% trans "Processor" %}</dt>
<dd class="col-9">{{ model_cpu }}</dd>
<dt class="col-3">{% trans "Connection" %}</dt>
<dd class="col-9">{{ uri_conn }}</dd>
<dt class="col-3">{% trans "Details" %}</dt>
<dd class="col-9">{{ compute.details }}</dd>
</dl>
<h3 class="page-header">{% trans "Performance" %}</h3>
<div class="mx-3 shadow-sm">
<div class="my-3 card border-success">
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-long-arrow-right"></i>
{% trans "CPU Utilization" %}
</h5>
<canvas id="cpuChart" width="735" height="160"></canvas>
</div>
</div>
<div class="my-3 card border-primary">
<div class="card-body">
<h5 class="card-title ">
<i class="fa fa-long-arrow-right"></i> {% trans "RAM Utilization" %}
</h5>
<canvas id="memChart" width="735" height="160"></canvas>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script src="{% static "js/Chart.min.js" %}"></script> <script src="{% static "js/Chart.bundle.min.js" %}"></script>
<script> <script>
var cpuLineData = {
labels : [0, 0, 0, 0, 0],
datasets : [
{
fillColor: "rgba(241,72,70,0.5)",
strokeColor: "rgba(241,72,70,1)",
pointColor : "rgba(241,72,70,1)",
pointStrokeColor : "#fff",
pointHighlightFill : "#fff",
pointHighlightStroke : "rgba(220,220,220,1)",
data : [0, 0, 0, 0, 0]
}
]
}
var cpu_ctx = document.getElementById("cpuChart").getContext("2d"); var cpu_ctx = document.getElementById("cpuChart").getContext("2d");
var cpuChart = new Chart(cpu_ctx).Line(cpuLineData, { var cpuChart = new Chart(cpu_ctx, {
animation: false, type: 'line',
pointDotRadius: 2, data: {
scaleLabel: "<%=value%> %", datasets : [{
scaleOverride: true, label: 'Usage',
scaleSteps: 5, backgroundColor: "rgba(241,72,70,0.5)",
scaleStepWidth: 20, pointRadius: 2,
scaleStartValue: 0, }]
responsive: true },
options: {
responsive: true,
legend: {
display: false
},
scales: {
xAxes:[{
offset: false,
ticks: {
beginAtZero: false,
autoSkip: true,
maxTicksLimit: 10,
maxRotation: 0,
minRotation: 0,
stepSize: 10,
},
}],
yAxes: [{
ticks: {
max: 100,
min: 0,
stepSize: 20,
callback: function(value, index, values) {
return value + ' %';
}
},
}],
},
tooltips: {
callbacks: {
label: function (tooltipItem, chart) {
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
return label += tooltipItem.yLabel + ' %';
}
}
}
}
}); });
var memLineData = { var mem_ctx = document.getElementById("memChart").getContext("2d");
labels : [0, 0, 0, 0, 0], var memChart = new Chart(mem_ctx, {
datasets : [ type: 'line',
{ data: {
fillColor : "rgba(249,134,33,0.5)", datasets: [{
strokeColor : "rgba(249,134,33,1)", pointRadius: 2,
pointColor : "rgba(249,134,33,1)", }]
pointStrokeColor : "#fff", },
pointHighlightFill : "#fff", options: {
pointHighlightStroke : "rgba(151,187,205,1)", responsive: true,
data : [0, 0, 0, 0, 0] legend: {
display: false
},
scales: {
xAxes:[{
offset: false,
ticks: {
beginAtZero: false,
autoSkip: true,
maxTicksLimit: 10,
maxRotation: 0,
minRotation: 0
}
}],
yAxes: [{
ticks:{
suggestedMin: 0,
suggestedMax: 100,
callback: function(value, index, values) {
return value + ' MB';
}
},
}],
},
tooltips: {
callbacks: {
label: function (tooltipItem, chart) {
var label = chart.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
return label += tooltipItem.yLabel + ' MB';
}
}
} }
] }
});
if (Boolean({{ status }}) === true) {
window.setInterval(function graph_usage() {
$.getJSON('{% url 'compute_graph' compute_id %}', function (data) {
cpuChart.data.labels.push(data.timeline);
memChart.data.labels.push(data.timeline);
cpuChart.data.datasets[0].data.push(data.cpudata);
if (cpuChart.data.datasets[0].data.length > 10){
cpuChart.data.labels.shift();
cpuChart.data.datasets[0].data.shift();
}
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1048576);
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1048576 * 5));
memChart.data.datasets[0].data.push(parseInt(data.memdata.usage / 1048576));
if (memChart.data.datasets[0].data.length > 10){
memChart.data.labels.shift();
memChart.data.datasets[0].data.shift();
}
cpuChart.update();
memChart.update();
});
}, 5000);
} }
var mem_ctx = $("#memChart").get(0).getContext("2d");
var memChart = new Chart(mem_ctx).Line(memLineData, {
animation: false,
pointDotRadius: 2,
scaleLabel: "<%=value%> Mb",
responsive: true
});
window.setInterval(function graph_usage() {
$.getJSON('{% url 'compute_graph' compute_id %}', function (data) {
cpuChart.scale.xLabels = data.timeline;
memChart.scale.xLabels = data.timeline;
for (var i = 0; i < 5; i++) {
cpuChart.datasets[0].points[i].value = data.cpudata[i];
memChart.datasets[0].points[i].value = data.memdata[i];
}
cpuChart.update();
memChart.update();
});
}, 5000);
</script> </script>
{% endblock %} {% endblock %}

View file

@ -1,3 +1,143 @@
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import reverse
from django.test import TestCase from django.test import TestCase
# Create your tests here. from .models import Compute
class ComputesTestCase(TestCase):
def setUp(self):
self.client.login(username='admin', password='admin')
Compute(
name='local',
hostname='localhost',
login='',
password='',
details='local',
type=4,
).save()
def test_index(self):
response = self.client.get(reverse('computes'))
self.assertEqual(response.status_code, 200)
def test_create_update_delete(self):
response = self.client.get(reverse('add_socket_host'))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('add_socket_host'),
{
'name': 'l1',
'details': 'Created',
'hostname': 'localhost',
'type': 4,
},
)
self.assertRedirects(response, reverse('computes'))
compute = Compute.objects.get(pk=2)
self.assertEqual(compute.name, 'l1')
self.assertEqual(compute.details, 'Created')
response = self.client.get(reverse('compute_update', args=[2]))
self.assertEqual(response.status_code, 200)
response = self.client.post(
reverse('compute_update', args=[2]),
{
'name': 'l2',
'details': 'Updated',
'hostname': 'localhost',
'type': 4,
},
)
self.assertRedirects(response, reverse('computes'))
compute = Compute.objects.get(pk=2)
self.assertEqual(compute.name, 'l2')
self.assertEqual(compute.details, 'Updated')
response = self.client.get(reverse('compute_delete', args=[2]))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse('compute_delete', args=[2]))
self.assertRedirects(response, reverse('computes'))
with self.assertRaises(ObjectDoesNotExist):
Compute.objects.get(id=2)
def test_overview(self):
response = self.client.get(reverse('overview', args=[1]))
self.assertEqual(response.status_code, 200)
def test_graph(self):
response = self.client.get(reverse('compute_graph', args=[1]))
self.assertEqual(response.status_code, 200)
def test_instances(self):
response = self.client.get(reverse('instances', args=[1]))
self.assertEqual(response.status_code, 200)
def test_storages(self):
response = self.client.get(reverse('storages', args=[1]))
self.assertEqual(response.status_code, 200)
def test_storage(self):
pass
def test_default_storage_volumes(self):
response = self.client.get(reverse('volumes', kwargs={'compute_id': 1, 'pool': 'default'}))
self.assertEqual(response.status_code, 200)
def test_default_storage(self):
response = self.client.get(reverse('storage', kwargs={'compute_id': 1, 'pool': 'default'}))
self.assertEqual(response.status_code, 200)
def test_networks(self):
response = self.client.get(reverse('networks', args=[1]))
self.assertEqual(response.status_code, 200)
def test_default_network(self):
response = self.client.get(reverse('network', kwargs={'compute_id': 1, 'pool': 'default'}))
self.assertEqual(response.status_code, 200)
def test_interfaces(self):
response = self.client.get(reverse('interfaces', args=[1]))
self.assertEqual(response.status_code, 200)
# TODO: add test for single interface
def test_nwfilters(self):
response = self.client.get(reverse('nwfilters', args=[1]))
self.assertEqual(response.status_code, 200)
# TODO: add test for single nwfilter
def test_secrets(self):
response = self.client.get(reverse('secrets', args=[1]))
self.assertEqual(response.status_code, 200)
# def test_create_instance_select_type(self):
# response = self.client.get(reverse('create_instance_select_type', args=[1]))
# self.assertEqual(response.status_code, 200)
# TODO: create_instance
def test_machines(self):
response = self.client.get(reverse('machines', kwargs={'compute_id': 1, 'arch': 'x86_64'}))
self.assertEqual(response.status_code, 200)
def test_compute_disk_buses(self):
response = self.client.get(
reverse('buses', kwargs={
'compute_id': 1,
'arch': 'x86_64',
'machine': 'pc',
'disk': 'disk',
}))
self.assertEqual(response.status_code, 200)
def test_dom_capabilities(self):
response = self.client.get(reverse('domcaps', kwargs={'compute_id': 1, 'arch': 'x86_64', 'machine': 'pc'}))
self.assertEqual(response.status_code, 200)

View file

@ -1,9 +1,47 @@
from django.conf.urls import url from secrets.views import secrets
from . import views
from django.urls import include, path
# from instances.views import create_instance, create_instance_select_type
from interfaces.views import interface, interfaces
from networks.views import network, networks
from nwfilters.views import nwfilter, nwfilters
from storages.views import create_volume, get_volumes, storage, storages
from . import forms, views
urlpatterns = [ urlpatterns = [
url(r'^$', views.computes, name='computes'), path('', views.computes, name='computes'),
url(r'^overview/(?P<compute_id>[0-9]+)/$', views.overview, name='overview'), path('add_tcp_host/', views.compute_create, {'FormClass': forms.TcpComputeForm}, name='add_tcp_host'),
url(r'^statistics/(?P<compute_id>[0-9]+)/$', path('add_ssh_host/', views.compute_create, {'FormClass': forms.SshComputeForm}, name='add_ssh_host'),
views.compute_graph, name='compute_graph'), path('add_tls_host/', views.compute_create, {'FormClass': forms.TlsComputeForm}, name='add_tls_host'),
path('add_socket_host/', views.compute_create, {'FormClass': forms.SocketComputeForm}, name='add_socket_host'),
path(
'<int:compute_id>/',
include([
path('', views.overview, name='overview'),
path('update/', views.compute_update, name='compute_update'),
path('delete/', views.compute_delete, name='compute_delete'),
path('statistics', views.compute_graph, name='compute_graph'),
path('instances/', views.instances, name='instances'),
path('storages/', storages, name='storages'),
path('storage/<str:pool>/volumes/', get_volumes, name='volumes'),
path('storage/<str:pool>/', storage, name='storage'),
path('storage/<str:pool>/create_volume/', create_volume, name='create_volume'),
path('networks/', networks, name='networks'),
path('network/<str:pool>/', network, name='network'),
path('interfaces/', interfaces, name='interfaces'),
path('interface/<str:iface>/', interface, name='interface'),
path('nwfilters/', nwfilters, name='nwfilters'),
path('nwfilter/<str:nwfltr>/', nwfilter, name='nwfilter'),
path('secrets/', secrets, name='secrets'),
# path('create/', create_instance_select_type, name='create_instance_select_type'),
# path('create/archs/<str:arch>/machines/<str:machine>/', create_instance, name='create_instance'),
path('archs/<str:arch>/machines/', views.get_compute_machine_types, name='machines'),
path(
'archs/<str:arch>/machines/<str:machine>/disks/<str:disk>/buses/',
views.get_compute_disk_buses,
name='buses',
),
path('archs/<str:arch>/machines/<str:machine>/capabilities/', views.get_dom_capabilities, name='domcaps'),
])),
] ]

13
computes/utils.py Normal file
View file

@ -0,0 +1,13 @@
from instances.models import Instance
def refresh_instance_database(compute):
domains = compute.proxy.wvm.listAllDomains()
domain_names = [d.name() for d in domains]
# Delete instances that're not on host from DB
Instance.objects.filter(compute=compute).exclude(name__in=domain_names).delete()
# Create instances that're on host but not in DB
names = Instance.objects.filter(compute=compute).values_list('name', flat=True)
for domain in domains:
if domain.name() not in names:
Instance(compute=compute, name=domain.name(), uuid=domain.UUIDString()).save()

24
computes/validators.py Normal file
View file

@ -0,0 +1,24 @@
import re
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
have_symbol = re.compile('[^a-zA-Z0-9._-]+')
wrong_ip = re.compile('^0.|^255.')
wrong_name = re.compile('[^a-zA-Z0-9._-]+')
def validate_hostname(value):
sym = have_symbol.match(value)
wip = wrong_ip.match(value)
if sym:
raise ValidationError(_('Hostname must contain only numbers, or the domain name separated by "."'))
elif wip:
raise ValidationError(_('Wrong IP address'))
def validate_name(value):
have_symbol = wrong_name.match('[^a-zA-Z0-9._-]+')
if have_symbol:
raise ValidationError(_('The hostname must not contain any special characters'))

View file

@ -1,223 +1,258 @@
import time
import json import json
from django.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404 from django.shortcuts import get_object_or_404, redirect, render
from django.contrib.auth.decorators import login_required from django.urls import reverse
from computes.models import Compute from django.utils import timezone
from instances.models import Instance
from accounts.models import UserInstance
from computes.forms import ComputeAddTcpForm, ComputeAddSshForm, ComputeEditHostForm, ComputeAddTlsForm, ComputeAddSocketForm
from vrtManager.hostdetails import wvmHostDetails
from vrtManager.connection import CONN_SSH, CONN_TCP, CONN_TLS, CONN_SOCKET, connection_manager
from libvirt import libvirtError from libvirt import libvirtError
from admin.decorators import superuser_only
from computes.forms import SocketComputeForm, SshComputeForm, TcpComputeForm, TlsComputeForm
from computes.models import Compute
from instances.models import Instance
from vrtManager.connection import (
CONN_SOCKET,
CONN_SSH,
CONN_TCP,
CONN_TLS,
connection_manager,
wvmConnect,
)
from vrtManager.hostdetails import wvmHostDetails
@login_required from . import utils
@superuser_only
def computes(request): def computes(request):
""" """
:param request: :param request:
:return: :return:
""" """
if not request.user.is_superuser: computes = Compute.objects.filter().order_by("name")
return HttpResponseRedirect(reverse('index'))
def get_hosts_status(computes): return render(request, "computes/list.html", {"computes": computes})
"""
Function return all hosts all vds on host
"""
compute_data = []
for compute in computes:
compute_data.append({'id': compute.id,
'name': compute.name,
'hostname': compute.hostname,
'status': connection_manager.host_is_up(compute.type, compute.hostname),
'type': compute.type,
'login': compute.login,
'password': compute.password,
'details': compute.details
})
return compute_data
error_messages = []
computes = Compute.objects.filter().order_by('name')
computes_info = get_hosts_status(computes)
if request.method == 'POST':
if 'host_del' in request.POST:
compute_id = request.POST.get('host_id', '')
try:
del_user_inst_on_host = UserInstance.objects.filter(instance__compute_id=compute_id)
del_user_inst_on_host.delete()
finally:
try:
del_inst_on_host = Instance.objects.filter(compute_id=compute_id)
del_inst_on_host.delete()
finally:
del_host = Compute.objects.get(id=compute_id)
del_host.delete()
return HttpResponseRedirect(request.get_full_path())
if 'host_tcp_add' in request.POST:
form = ComputeAddTcpForm(request.POST)
if form.is_valid():
data = form.cleaned_data
new_tcp_host = Compute(name=data['name'],
hostname=data['hostname'],
type=CONN_TCP,
login=data['login'],
password=data['password'])
new_tcp_host.save()
return HttpResponseRedirect(request.get_full_path())
else:
for msg_err in form.errors.values():
error_messages.append(msg_err.as_text())
if 'host_ssh_add' in request.POST:
form = ComputeAddSshForm(request.POST)
if form.is_valid():
data = form.cleaned_data
new_ssh_host = Compute(name=data['name'],
hostname=data['hostname'],
type=CONN_SSH,
login=data['login'])
new_ssh_host.save()
return HttpResponseRedirect(request.get_full_path())
else:
for msg_err in form.errors.values():
error_messages.append(msg_err.as_text())
if 'host_tls_add' in request.POST:
form = ComputeAddTlsForm(request.POST)
if form.is_valid():
data = form.cleaned_data
new_tls_host = Compute(name=data['name'],
hostname=data['hostname'],
type=CONN_TLS,
login=data['login'],
password=data['password'])
new_tls_host.save()
return HttpResponseRedirect(request.get_full_path())
else:
for msg_err in form.errors.values():
error_messages.append(msg_err.as_text())
if 'host_socket_add' in request.POST:
form = ComputeAddSocketForm(request.POST)
if form.is_valid():
data = form.cleaned_data
new_socket_host = Compute(name=data['name'],
details=data['details'],
hostname='localhost',
type=CONN_SOCKET,
login='',
password='')
new_socket_host.save()
return HttpResponseRedirect(request.get_full_path())
else:
for msg_err in form.errors.values():
error_messages.append(msg_err.as_text())
if 'host_edit' in request.POST:
form = ComputeEditHostForm(request.POST)
if form.is_valid():
data = form.cleaned_data
compute_edit = Compute.objects.get(id=data['host_id'])
compute_edit.name = data['name']
compute_edit.hostname = data['hostname']
compute_edit.login = data['login']
compute_edit.password = data['password']
compute.edit_details = data['details']
compute_edit.save()
return HttpResponseRedirect(request.get_full_path())
else:
for msg_err in form.errors.values():
error_messages.append(msg_err.as_text())
return render(request, 'computes.html', locals())
@login_required @superuser_only
def overview(request, compute_id): def overview(request, compute_id):
""" compute = get_object_or_404(Compute, pk=compute_id)
:param request: status = (
:return: "true" if connection_manager.host_is_up(compute.type, compute.hostname) is True else "false"
""" )
if not request.user.is_superuser: conn = wvmHostDetails(
return HttpResponseRedirect(reverse('index')) compute.hostname,
compute.login,
compute.password,
compute.type,
)
hostname, host_arch, host_memory, logical_cpu, model_cpu, uri_conn = conn.get_node_info()
hypervisor = conn.get_hypervisors_domain_types()
mem_usage = conn.get_memory_usage()
emulator = conn.get_emulator(host_arch)
version = conn.get_version()
lib_version = conn.get_lib_version()
conn.close()
error_messages = [] return render(request, "overview.html", locals())
@superuser_only
def instances(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id) compute = get_object_or_404(Compute, pk=compute_id)
try: utils.refresh_instance_database(compute)
conn = wvmHostDetails(compute.hostname, instances = Instance.objects.filter(compute=compute).prefetch_related("userinstance_set")
compute.login,
compute.password,
compute.type)
hostname, host_arch, host_memory, logical_cpu, model_cpu, uri_conn = conn.get_node_info()
hypervisor = conn.hypervisor_type()
mem_usage = conn.get_memory_usage()
conn.close()
except libvirtError as lib_err:
error_messages.append(lib_err)
return render(request, 'overview.html', locals()) return render(request, "computes/instances.html", {"compute": compute, "instances": instances})
@superuser_only
def compute_create(request, FormClass):
form = FormClass(request.POST or None)
if form.is_valid():
form.save()
return redirect(reverse("computes"))
return render(request, "computes/form.html", {"form": form})
@superuser_only
def compute_update(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
if compute.type == 1:
FormClass = TcpComputeForm
elif compute.type == 2:
FormClass = SshComputeForm
elif compute.type == 3:
FormClass = TlsComputeForm
elif compute.type == 4:
FormClass = SocketComputeForm
form = FormClass(request.POST or None, instance=compute)
if form.is_valid():
form.save()
return redirect(reverse("computes"))
return render(request, "computes/form.html", {"form": form})
@superuser_only
def compute_delete(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
if request.method == "POST":
compute.delete()
return redirect("computes")
return render(
request,
"common/confirm_delete.html",
{"object": compute},
)
@login_required
def compute_graph(request, compute_id): def compute_graph(request, compute_id):
""" """
:param request: :param request:
:param compute_id:
:return: :return:
""" """
points = 5
datasets = {}
cookies = {}
compute = get_object_or_404(Compute, pk=compute_id) compute = get_object_or_404(Compute, pk=compute_id)
curent_time = time.strftime("%H:%M:%S")
try: try:
conn = wvmHostDetails(compute.hostname, conn = wvmHostDetails(
compute.login, compute.hostname,
compute.password, compute.login,
compute.type) compute.password,
compute.type,
)
current_time = timezone.now().strftime("%H:%M:%S")
cpu_usage = conn.get_cpu_usage() cpu_usage = conn.get_cpu_usage()
mem_usage = conn.get_memory_usage() mem_usage = conn.get_memory_usage()
conn.close() conn.close()
except libvirtError: except libvirtError:
cpu_usage = 0 cpu_usage = {"usage": 0}
mem_usage = 0 mem_usage = {"usage": 0}
current_time = 0
try: data = json.dumps(
cookies['cpu'] = request.COOKIES['cpu'] {
cookies['mem'] = request.COOKIES['mem'] "cpudata": cpu_usage["usage"],
cookies['timer'] = request.COOKIES['timer'] "memdata": mem_usage,
except KeyError: "timeline": current_time,
cookies['cpu'] = None }
cookies['mem'] = None )
if not cookies['cpu'] or not cookies['mem']:
datasets['cpu'] = [0] * points
datasets['mem'] = [0] * points
datasets['timer'] = [0] * points
else:
datasets['cpu'] = eval(cookies['cpu'])
datasets['mem'] = eval(cookies['mem'])
datasets['timer'] = eval(cookies['timer'])
datasets['timer'].append(curent_time)
datasets['cpu'].append(int(cpu_usage['usage']))
datasets['mem'].append(int(mem_usage['usage']) / 1048576)
if len(datasets['timer']) > points:
datasets['timer'].pop(0)
if len(datasets['cpu']) > points:
datasets['cpu'].pop(0)
if len(datasets['mem']) > points:
datasets['mem'].pop(0)
data = json.dumps({'cpudata': datasets['cpu'], 'memdata': datasets['mem'], 'timeline': datasets['timer']})
response = HttpResponse() response = HttpResponse()
response['Content-Type'] = "text/javascript" response["Content-Type"] = "text/javascript"
response.cookies['cpu'] = datasets['cpu']
response.cookies['timer'] = datasets['timer']
response.cookies['mem'] = datasets['mem']
response.write(data) response.write(data)
return response return response
def get_compute_disk_buses(request, compute_id, arch, machine, disk):
"""
:param request:
:param compute_id:
:param arch:
:param machine:
:param disk:
:return:
"""
data = dict()
compute = get_object_or_404(Compute, pk=compute_id)
try:
conn = wvmConnect(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
disk_device_types = conn.get_disk_device_types(arch, machine)
if disk in disk_device_types:
if disk == "disk":
data["bus"] = sorted(disk_device_types)
elif disk == "cdrom":
data["bus"] = ["ide", "sata", "scsi"]
elif disk == "floppy":
data["bus"] = ["fdc"]
elif disk == "lun":
data["bus"] = ["scsi"]
except libvirtError:
pass
return HttpResponse(json.dumps(data))
def get_compute_machine_types(request, compute_id, arch):
"""
:param request:
:param compute_id:
:param arch:
:return:
"""
data = dict()
try:
compute = get_object_or_404(Compute, pk=compute_id)
conn = wvmConnect(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
data["machines"] = conn.get_machine_types(arch)
except libvirtError:
pass
return HttpResponse(json.dumps(data))
def get_compute_video_models(request, compute_id, arch, machine):
"""
:param request:
:param compute_id:
:param arch:
:param machine:
:return:
"""
data = dict()
try:
compute = get_object_or_404(Compute, pk=compute_id)
conn = wvmConnect(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
data["videos"] = conn.get_video_models(arch, machine)
except libvirtError:
pass
return HttpResponse(json.dumps(data))
def get_dom_capabilities(request, compute_id, arch, machine):
"""
:param request:
:param compute_id:
:param arch:
:param machine:
:return:
"""
data = dict()
try:
compute = get_object_or_404(Compute, pk=compute_id)
conn = wvmConnect(
compute.hostname,
compute.login,
compute.password,
compute.type,
)
data["videos"] = conn.get_disk_device_types(arch, machine)
data["bus"] = conn.get_disk_device_types(arch, machine)
except libvirtError:
pass
return HttpResponse(json.dumps(data))

View file

@ -3,24 +3,24 @@
# gstfsd - WebVirtCloud daemon for managing VM's filesystem # gstfsd - WebVirtCloud daemon for managing VM's filesystem
# #
import SocketServer import socketserver
import json import json
import guestfs import guestfs
import re import re
PORT = 16510 PORT = 16510
ADDRESS = "0.0.0.0" ADDRESS = "0.0.0.0"
class MyTCPServer(SocketServer.ThreadingTCPServer): class MyTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True allow_reuse_address = True
class MyTCPServerHandler(SocketServer.BaseRequestHandler): class MyTCPServerHandler(socketserver.BaseRequestHandler):
def handle(self): def handle(self):
# recive data # recive data
data = json.loads(self.request.recv(1024).strip()) d = self.request.recv(1024).strip()
data = json.loads(d)
# GuestFS # GuestFS
gfs = guestfs.GuestFS(python_return_dict=True) gfs = guestfs.GuestFS(python_return_dict=True)
@ -42,17 +42,18 @@ class MyTCPServerHandler(SocketServer.BaseRequestHandler):
if data['action'] == 'publickey': if data['action'] == 'publickey':
if not gfs.is_dir('/root/.ssh'): if not gfs.is_dir('/root/.ssh'):
gfs.mkdir('/root/.ssh') gfs.mkdir('/root/.ssh')
gfs.chmod(0700, "/root/.ssh") gfs.chmod(700, "/root/.ssh")
gfs.write('/root/.ssh/authorized_keys', data['key']) gfs.write('/root/.ssh/authorized_keys', data['key'])
gfs.chmod(0600, '/root/.ssh/authorized_keys') gfs.chmod(600, '/root/.ssh/authorized_keys')
self.request.sendall(json.dumps({'return': 'success'})) self.request.sendall(json.dumps({'return': 'success'}))
gfs.umount(part) gfs.umount(part)
except RuntimeError: except RuntimeError:
pass pass
gfs.shutdown() gfs.shutdown()
gfs.close() gfs.close()
except RuntimeError, err: except Exception as err:
self.request.sendall(json.dumps({'return': 'error', 'message': err.message})) self.request.sendall(bytes(json.dumps({'return': 'error', 'message': str(err)}).encode()))
server = MyTCPServer((ADDRESS, PORT), MyTCPServerHandler) server = MyTCPServer((ADDRESS, PORT), MyTCPServerHandler)
server.serve_forever() server.serve_forever()

View file

@ -0,0 +1,37 @@
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
}

View file

@ -0,0 +1,85 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP5rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

View file

@ -0,0 +1,85 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP5rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

View file

@ -15,9 +15,21 @@ server {
proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
proxy_set_header Host $host:$server_port; proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Proto $remote_addr; proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_connect_timeout 600; proxy_set_header X-Forwarded-Ssl off;
proxy_read_timeout 600; proxy_connect_timeout 1800;
proxy_send_timeout 600; proxy_read_timeout 1800;
proxy_send_timeout 1800;
client_max_body_size 1024M; client_max_body_size 1024M;
} }
location /novncd/ {
proxy_pass http://wsnovncd;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
upstream wsnovncd {
server 127.0.0.1:6080;
} }

Some files were not shown because too many files have changed in this diff Show more