1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-25 15:45:23 +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
venv
venv2
.vscode
.idea
.DS_*
.webvirtcloud
*.pyc
db.sqlite3*
console/cert.pem*
tags
dhcpd.*
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
python:
- "2.7"
- "3.6"
env:
- DJANGO=1.8
- DJANGO=2.2.16
install:
- pip install -r dev/requirements.txt --use-mirrors
- pip install -r dev/requirements.txt
script:
- pep8 --exclude=IPy.py --ignore=E501 vrtManager accounts computes \
console create instances interfaces \
networks secrets storages
- pyflakes vrtManager accounts computes console create instances interfaces \
networks secrets storages
- pep8 --exclude=IPy.py --ignore=E501 vrtManager accounts admin appsettings computes \
console create datasource instances interfaces \
logs networks nwfilters secrets storages
- pyflakes vrtManager accounts admin appsettings computes console create datasource instances interfaces \
nwfilters networks secrets storages logs
- python manage.py migrate
- python manage.py test --settings=webvirtcloud.settings-dev

View file

@ -1,50 +1,63 @@
FROM phusion/baseimage:0.9.17
MAINTAINER Jethro Yu <comet.jc@gmail.com>
FROM phusion/baseimage:18.04-1.0.0
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 apt-get update -qqy
RUN DEBIAN_FRONTEND=noninteractive apt-get -qyy install \
-o APT::Install-Suggests=false \
git python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev nginx libsasl2-modules
RUN apt-get update -qqy \
&& DEBIAN_FRONTEND=noninteractive apt-get -qyy install \
--no-install-recommends \
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
# Setup webvirtcloud
RUN cd /srv/webvirtcloud && \
virtualenv venv && \
WORKDIR /srv/webvirtcloud
RUN python3 -m venv venv && \
. venv/bin/activate && \
pip install -U pip && \
pip install -r conf/requirements.txt && \
pip3 install -U pip && \
pip3 install wheel && \
pip3 install -r conf/requirements.txt && \
chown -R www-data:www-data /srv/webvirtcloud
RUN cd /srv/webvirtcloud && . venv/bin/activate && \
python manage.py migrate && \
RUN . venv/bin/activate && \
python3 manage.py migrate && \
chown -R www-data:www-data /srv/webvirtcloud
# 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 && \
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
RUN mkdir /etc/service/nginx && \
mkdir /etc/service/nginx-log-forwarder && \
mkdir /etc/service/webvirtcloud && \
mkdir /etc/service/novnc
ADD conf/runit/nginx /etc/service/nginx/run
ADD conf/runit/nginx-log-forwarder /etc/service/nginx-log-forwarder/run
ADD conf/runit/novncd.sh /etc/service/novnc/run
ADD conf/runit/webvirtcloud.sh /etc/service/webvirtcloud/run
EXPOSE 80
EXPOSE 6080
COPY conf/runit/nginx /etc/service/nginx/run
COPY conf/runit/nginx-log-forwarder /etc/service/nginx-log-forwarder/run
COPY conf/runit/novncd.sh /etc/service/novnc/run
COPY conf/runit/webvirtcloud.sh /etc/service/webvirtcloud/run
# Define mountable directories.
#VOLUME []
# Use baseimage-docker's init system.
CMD ["/sbin/my_init"]
WORKDIR /srv/webvirtcloud

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
* 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 change root password in Instance (Tested only Ubuntu)
* Supports cloud-init datasource interface
### Warning!!!
@ -15,23 +26,38 @@ wget -O - https://clck.ru/9VMRH | sudo tee -a /usr/local/bin/gstfsd
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.
## 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
You should generate SECRET_KEY after cloning repo. Then put it into webvirtcloud/settings.py.
```python
```python3
import random, string
haystack = string.ascii_letters + string.digits + string.punctuation
print(''.join([random.SystemRandom().choice(haystack) for _ in range(50)]))
```
### Install WebVirtCloud panel (Ubuntu)
### Install WebVirtCloud panel (Ubuntu 18.04+ LTS)
```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
cd webvirtcloud
cp webvirtcloud/settings.py.template webvirtcloud/settings.py
@ -42,10 +68,10 @@ cd ..
sudo mv webvirtcloud /srv
sudo chown -R www-data:www-data /srv/webvirtcloud
cd /srv/webvirtcloud
virtualenv venv
virtualenv -p python3 venv
source venv/bin/activate
pip install -r conf/requirements.txt
python manage.py migrate
python3 manage.py migrate
sudo chown -R www-data:www-data /srv/webvirtcloud
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
```
### Install WebVirtCloud panel (CentOS)
Done!!
Go to http://serverip and you should see the login screen.
### Install WebVirtCloud panel (CentOS8/OEL8)
```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
@ -76,19 +107,23 @@ sudo mkdir /srv && cd /srv
sudo git clone https://github.com/retspen/webvirtcloud && cd webvirtcloud
cp webvirtcloud/settings.py.template 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
```
sudo virtualenv venv
sudo source venv/bin/activate
sudo venv/bin/pip install -r conf/requirements.txt
sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
sudo venv/bin/python manage.py migrate
```bash
virtualenv-3 venv
source venv/bin/activate
pip3 install -r conf/requirements.txt
cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/
python3 manage.py migrate
```
#### 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
sudo vim /etc/supervisord.conf
@ -101,7 +136,7 @@ autorestart=true
redirect_stderr=true
[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
user=nginx
autostart=true
@ -110,9 +145,10 @@ redirect_stderr=true
```
#### 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:
```
```bash
# 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:
```
```bash
upstream gunicorn_server {
#server unix:/srv/webvirtcloud/venv/wvcloud.socket 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 Host $host:$server_port;
proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
proxy_connect_timeout 1800;
proxy_read_timeout 1800;
proxy_send_timeout 1800;
client_max_body_size 1024M;
}
}
@ -177,34 +214,48 @@ Change permission for selinux:
```bash
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
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:
```bash
sudo systemctl restart nginx && systemctl restart supervisord
```
And finally, check everything is running:
```bash
sudo supervisorctl status
novncd RUNNING pid 24186, uptime 2:59:14
webvirtcloud RUNNING pid 24185, uptime 2:59:14
gstfsd RUNNING pid 24662, uptime 6:01:40
novncd RUNNING pid 24661, uptime 6:01:40
webvirtcloud RUNNING pid 24660, uptime 6:01:40
```
#### Apache mod_wsgi configuration
```
```bash
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
```bash
wget -O - https://clck.ru/9V9fH | sudo sh
```
@ -213,19 +264,133 @@ Done!!
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
<pre>
```html
login: 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
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
python manage.py migrate
pip3 install -U -r conf/requirements.txt
python3 manage.py migrate
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).

50
Vagrantfile vendored
View file

@ -2,17 +2,53 @@
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.hostname = "webvirtcloud"
config.vm.network "private_network", ip: "192.168.33.10"
config.vm.provision "shell", inline: <<-SHELL
# Default machine, if name not specified...
config.vm.define "dev", primary: true do |dev|
dev.vm.box = "ubuntu/bionic64"
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 sed -i 's/auth_tcp = \"sasl\"/auth_tcp = \"none\"/g' /etc/libvirt/libvirtd.conf
sudo service libvirt-bin restart
sudo adduser vagrant libvirtd
sudo apt-get -y install python-virtualenv python-dev libxml2-dev libvirt-dev zlib1g-dev
virtualenv /vagrant/venv
sudo apt-get -y install python3-virtualenv virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev python3-guestfs
virtualenv -p python3 /vagrant/venv
source /vagrant/venv/bin/activate
pip install -r /vagrant/dev/requirements.txt
pip3 install -r /vagrant/dev/requirements.txt
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

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 django import forms
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.conf import settings
from appsettings.settings import app_settings
from django.contrib.auth import get_user_model
from django.forms import EmailField, Form, ModelForm, ValidationError
from django.utils.translation import gettext_lazy as _
from .models import UserInstance, UserSSHKey
from .utils import validate_ssh_key
class UserAddForm(forms.Form):
name = forms.CharField(label="Name",
error_messages={'required': _('No User name has been entered')},
max_length=20)
password = forms.CharField(required=not settings.ALLOW_EMPTY_PASSWORD, error_messages={'required': _('No password has been entered')},)
class UserInstanceForm(ModelForm):
def __init__(self, *args, **kwargs):
super(UserInstanceForm, self).__init__(*args, **kwargs)
def clean_name(self):
name = self.cleaned_data['name']
have_symbol = re.match('^[a-z0-9]+$', name)
if not have_symbol:
raise forms.ValidationError(_('The flavor name must not contain any special characters'))
elif len(name) > 20:
raise forms.ValidationError(_('The flavor name must not exceed 20 characters'))
try:
User.objects.get(username=name)
except User.DoesNotExist:
return name
raise forms.ValidationError(_('Flavor name is already use'))
# Make user and instance fields not editable after creation
instance = getattr(self, 'instance', None)
if instance and instance.id is not None:
self.fields['user'].disabled = True
self.fields['instance'].disabled = True
def clean_instance(self):
instance = self.cleaned_data['instance']
if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == 'False':
exists = UserInstance.objects.filter(instance=instance)
if exists:
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 -*-
from __future__ import unicode_literals
from django.db import models, migrations
# Generated by Django 2.2.10 on 2020-01-28 07:01
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('instances', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
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(
name='UserInstance',
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_delete', models.BooleanField(default=False)),
('instance', models.ForeignKey(to='instances.Instance')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('is_vnc', models.BooleanField(default=False)),
('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.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
class UserInstanceManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('instance', 'user')
class UserInstance(models.Model):
user = models.ForeignKey(User)
instance = models.ForeignKey(Instance)
user = models.ForeignKey(User, on_delete=models.CASCADE)
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
is_change = models.BooleanField(default=False)
is_delete = models.BooleanField(default=False)
is_vnc = models.BooleanField(default=False)
def __unicode__(self):
return self.instance.name
objects = UserInstanceManager()
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):
user = models.ForeignKey(User)
keyname = models.CharField(max_length=25)
keypublic = models.CharField(max_length=500)
user = models.ForeignKey(User, on_delete=models.DO_NOTHING)
keyname = models.CharField(_('key name'), max_length=25)
keypublic = models.CharField(_('public key'), max_length=500)
def __unicode__(self):
def __str__(self):
return self.keyname
class UserAttributes(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
can_clone_instances = models.BooleanField(default=True)
max_instances = models.IntegerField(default=1)
max_cpus = models.IntegerField(default=1)
max_memory = models.IntegerField(default=2048)
max_disk_size = models.IntegerField(default=20)
max_instances = models.IntegerField(_('max instances'),
default=2,
help_text=_("-1 for unlimited. Any integer value"),
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 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):
def __str__(self):
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,27 +1,78 @@
{% extends "base.html" %}
{% 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 %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
{% include 'create_user_inst_block.html' %}
<h1 class="page-header">{{ user }}</h1>
</div>
</div>
<!-- /.row -->
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#public-keys">{% trans "Public Keys" %}</a>
</li>
</ul>
{% include 'errors_block.html' %}
{% if request.user.is_superuser and publickeys %}
<div class="row">
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<div class="tab-content">
<div class="tab-pane active" id="instances">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>{% trans "Key name" %}</th>
<th>{% trans "Public key" %}</th>
<th scope="col">#</th>
<th scope="col">{% trans "Instance" %}</th>
<th scope="col">{% trans "VNC" %}</th>
<th scope="col">{% trans "Resize" %}</th>
<th scope="col">{% trans "Delete" %}</th>
<th scope="colgroup" colspan="2">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for inst in user_insts %}
<tr>
<td>{{ forloop.counter }}</td>
<td><a href="{% url 'instances:instance' inst.instance.id %}">{{ inst.instance.name }}</a></td>
<td>{{ inst.is_vnc }}</td>
<td>{{ inst.is_change }}</td>
<td>{{ inst.is_delete }}</td>
<td style="width:5px;">
<a href="{% url 'accounts:user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
{% icon 'pencil' %}
</a>
</td>
<td style="width:5px;">
<a class="btn btn-sm btn-secondary" href="{% url 'accounts:user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
{% icon 'trash' %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="tab-pane fade" id="public-keys">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{% trans "Key name" %}</th>
<th scope="col">{% trans "Public key" %}</th>
</tr>
</thead>
<tbody>
@ -35,106 +86,4 @@
</table>
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-lg-12">
{% if not user_insts %}
<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 "User doesn't have any Instace" %}
</div>
</div>
{% else %}
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>{% trans "Instance" %}</th>
<th>{% trans "VNC" %}</th>
<th>{% trans "Resize" %}</th>
<th>{% trans "Delete" %}</th>
<th colspan="2">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for inst in user_insts %}
<tr>
<td>{{ forloop.counter }}</td>
<td><a href="{% url 'instance' inst.instance.compute.id inst.instance.name %}">{{ inst.instance.name }}</a></td>
<td>{{ inst.is_vnc }}</td>
<td>{{ inst.is_change }}</td>
<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 %}
{% endblock content %}

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 %}
{% 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 %}
<div class="row">
<div class="page-header">
<a href="/"><h1>WebVirtCloud</h1></a>
</div>
<div class="col-xs-12" role="main">
<div class="page-header">
<a class="" 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">×</button>
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{% trans "Incorrect username or password." %}
</div>
{% endif %}
<form class="form-signin" method="post" role="form">{% csrf_token %}
<form class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
<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' %}">
<input type="text" class="form-control" name="username" placeholder="{% trans "User" %}" autocapitalize="none" autocorrect="off" autofocus>
<input type="password" class="form-control" name="password" placeholder="{% trans "Password" %}">
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}

View file

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

View file

@ -1,90 +1,56 @@
{% extends "base.html" %}
{% load i18n %}
{% load bootstrap4 %}
{% load icons %}
{% 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 %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">{% trans "Profile" %}</h1>
</div>
</div>
<!-- /.row -->
{% include 'errors_block.html' %}
<div class="row">
<div class="col-lg-12">
<h3 class="page-header">{% trans "Edit Profile" %}</h3>
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Login" %}</label>
<div class="col-sm-4">
<input type="text" class="form-control" value="{{ request.user.username }}" disabled>
</div>
</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>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#edit-profile">{% trans "Edit Profile" %}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#ssh-keys">{% trans "SSH Keys" %}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane tab-pane-bordered active" id="edit-profile">
<div class="card">
<div class="card-body">
<form method="post" action="" role="form" aria-label="Edit user info form">
{% csrf_token %}
{% bootstrap_form profile_form layout='horizontal' %}
{% if perms.accounts.change_password %}
<a href="{% url 'accounts:change_password' %}" class="btn btn-primary">
{% icon 'lock' %} {% trans "Change Password" %}
</a>
{% endif %}
<h3 class="page-header">{% trans "SSH Keys" %}</h3>
<div class="form-group mb-0 float-right">
<button type="submit" class="btn btn-primary">
{% icon 'pencil' %} {% trans "Update" %}
</button>
</div>
</form>
</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 style="text-align: center;">
<tbody class="text-center">
{% 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>
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
{% icon 'trash' %}
</a>
</td>
</tr>
{% endfor %}
@ -93,25 +59,22 @@
</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 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 %}

View file

@ -1,7 +1,8 @@
from django import template
import base64
import hashlib
from django import template
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
app_name = 'accounts'
urlpatterns = [
url(r'^login/$', 'django.contrib.auth.views.login',
{'template_name': 'login.html'}, name='login'),
url(r'^logout/$', 'django.contrib.auth.views.logout',
{'template_name': 'logout.html'}, name='logout'),
url(r'^profile/$', views.profile, name='profile'),
url(r'^$', views.accounts, name='accounts'),
url(r'^profile/(?P<user_id>[0-9]+)/$', views.account, name='account'),
path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
path('profile/', views.profile, name='profile'),
path('profile/<int:user_id>/', views.account, name='account'),
path('change_password/', views.change_password, name='change_password'),
path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'),
path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'),
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 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 admin.decorators import superuser_only
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):
"""
:param request:
:return:
"""
error_messages = []
user = User.objects.get(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 'username' in request.POST:
username = request.POST.get('username', '')
email = request.POST.get('email', '')
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())
if profile_form.is_valid():
profile_form.save()
messages.success(request, _("Profile updated"))
return redirect("accounts:profile")
@login_required
def accounts(request):
"""
:param request:
:return:
"""
if not request.user.is_superuser:
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())
return render(
request,
"profile.html",
{
"publickeys": publickeys,
"profile_form": profile_form,
"ssh_key_form": ssh_key_form,
},
)
@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):
"""
:param request:
:return:
"""
if not request.user.is_superuser:
return HttpResponseRedirect(reverse('index'))
error_messages = []
user = User.objects.get(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)
if request.method == 'POST':
if 'delete' in request.POST:
user_inst = request.POST.get('user_inst', '')
del_user_inst = UserInstance.objects.get(id=user_inst)
del_user_inst.delete()
return HttpResponseRedirect(request.get_full_path())
if 'permission' in request.POST:
user_inst = request.POST.get('user_inst', '')
inst_vnc = request.POST.get('inst_vnc', '')
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', '')
return render(
request,
"account.html",
{
"user": user,
"user_insts": user_insts,
"instances": instances,
"publickeys": publickeys,
"otp_enabled": settings.OTP_ENABLED,
},
)
if settings.ALLOW_INSTANCE_MULTIPLE_OWNER:
check_inst = UserInstance.objects.filter(instance_id=int(inst_id), user_id=int(user_id))
@permission_required("accounts.change_password", raise_exception=True)
def change_password(request):
form = PasswordChangeForm(request.user, request.POST or None)
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:
check_inst = UserInstance.objects.filter(instance_id=int(inst_id))
return redirect(reverse("accounts:account", args=[user.id]))
if check_inst:
msg = _("Instance already added")
error_messages.append(msg)
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:
add_user_inst = UserInstance(instance_id=int(inst_id), user_id=int(user_id))
add_user_inst.save()
return HttpResponseRedirect(request.get_full_path())
device = get_user_totp_device(user)
send_email_with_otp(user, device)
return render(request, 'account.html', locals())
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.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
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'))
from .validators import validate_hostname
class ComputeAddSshForm(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=20)
class TcpComputeForm(forms.ModelForm):
hostname = forms.CharField(validators=[validate_hostname])
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP)
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 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 Meta:
model = Compute
widgets = {'password': forms.PasswordInput()}
fields = '__all__'
class ComputeAddTlsForm(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)
class SshComputeForm(forms.ModelForm):
hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP"))
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH)
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 Meta:
model = Compute
exclude = ['password']
class ComputeEditHostForm(forms.Form):
host_id = forms.CharField()
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(max_length=100)
class TlsComputeForm(forms.ModelForm):
hostname = forms.CharField(validators=[validate_hostname])
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS)
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 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 Meta:
model = Compute
widgets = {'password': forms.PasswordInput()}
fields = '__all__'
class ComputeAddSocketForm(forms.Form):
name = forms.CharField(error_messages={'required': _('No hostname has been entered')},
max_length=20)
details = forms.CharField(error_messages={'required': _('No details has been entred')},
max_length=50)
class SocketComputeForm(forms.ModelForm):
hostname = forms.CharField(widget=forms.HiddenInput, initial='localhost')
type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET)
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'))
class Meta:
model = Compute
fields = ['name', 'details', 'hostname', 'type']

View file

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
# Generated by Django 2.2.10 on 2020-01-28 07:01
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
@ -13,15 +14,13 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Compute',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=20)),
('hostname', models.CharField(max_length=20)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
('hostname', models.CharField(max_length=64)),
('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()),
],
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):
name = models.CharField(max_length=20)
hostname = models.CharField(max_length=20)
login = models.CharField(max_length=20)
password = models.CharField(max_length=14, blank=True, null=True)
details = models.CharField(max_length=50, null=True, blank=True)
type = models.IntegerField()
class Compute(Model):
name = CharField(_('name'), max_length=64, unique=True)
hostname = CharField(_('hostname'), max_length=64)
login = CharField(_('login'), max_length=20)
password = CharField(_('password'), max_length=14, blank=True, null=True)
details = CharField(_('details'), max_length=64, null=True, blank=True)
type = IntegerField()
def __unicode__(self):
return self.hostname
@cached_property
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 %}
{% if request.user.is_superuser %}
<a href="#addHost" type="button" class="btn btn-success pull-right" data-toggle="modal">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
</a>
<!-- Modal -->
<div class="modal fade" id="addHost" tabindex="-1" role="dialog" aria-labelledby="addHostLabel" 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 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 %}
{% load bootstrap4 %}
{% load icons %}
<div class="btn-group float-right mt-1" role="group" aria-label="Add host button group">
<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>
<a href="{% url 'add_tls_host' %}" class="btn btn-success">{% trans "TLS" %}</a>
<a href="{% url 'add_socket_host' %}" class="btn btn-success">{% trans "Local" %}</a>
<a href="#" class="btn btn-success disabled" title="{% trans "Add new host" %}">{% icon "plus" %}</a>
</div>

View file

@ -1,152 +1,249 @@
{% extends "base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load icons %}
{% block title %}{% trans "Overview" %} - {{ compute.name }}{% endblock %}
{% block page_heading %}{{ compute.name }}{% endblock page_heading %}
{% block content %}
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">{{ compute.name }}</h1>
<ol class="breadcrumb">
<li class="active">
<i class="fa fa-dashboard"></i> {% trans "Overview" %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb bg-light shadow-sm">
<li class="breadcrumb-item">
<span class="font-weight-bold">{% icon 'dashboard' %} {% trans "Overview" %}</span>
</li>
<li>
<i class="fa fa-hdd-o"></i> <a href="{% url 'storages' compute.id %}">{% trans "Storages" %}</a>
<li class="breadcrumb-item">
<a href="{% url 'instances' compute.id %}">{% icon 'server' %} {% trans "Instances" %}</a>
</li>
<li>
<i class="fa fa-sitemap"></i> <a href="{% url 'networks' compute.id %}">{% trans "Networks" %}</a>
<li class="breadcrumb-item">
<a href="{% url 'storages' compute.id %}">{% icon 'hdd-o' %} {% trans "Storages" %}</a>
</li>
<li>
<i class="fa fa-wifi"></i> <a href="{% url 'interfaces' compute.id %}">{% trans "Interfaces" %}</a>
<li class="breadcrumb-item">
<a href="{% url 'networks' compute.id %}">{% icon 'sitemap' %} {% trans "Networks" %}</a>
</li>
<li>
<i class="fa fa-key"></i> <a href="{% url 'secrets' compute.id %}">{% trans "Secrets" %}</a>
<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>
<!-- /.row -->
{% include 'errors_block.html' %}
<div class="row" id="max-width-page">
<div class="shadow-sm">
<h3 class="page-header">{% trans "Basic details" %}</h3>
<div class="col-xs-4 col-sm-3">
<p>{% trans "Hostname" %}</p>
<p>{% trans "Hypervisor" %}</p>
<p>{% trans "Memory" %}</p>
<p>{% trans "Architecture" %}</p>
<p>{% trans "Logical CPUs" %}</p>
<p>{% trans "Processor" %}</p>
<p>{% trans "Connection" %}</p>
<p>{% trans "Details" %}</p>
<dl class="mx-3 row">
<dt class="col-3">{% trans "Hostname" %}</dt>
<dd class="col-9">{{ hostname }}</dd>
<dt class="col-3">{% trans "Hypervisors" %}</dt>
<dd class="col-9">
<div class="dropdown">
{% for arch, hpv in hypervisor.items|slice:":4" %}
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ arch }}
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
{% for h in hpv %}
<a class="dropdown-item" href="#">{{ h }}</a>
{% endfor %}
</div>
<div class="col-xs-8 col-sm-7">
<p>{{ hostname }}</p>
<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>
{% endfor %}
</div>
<div class="dropdown">
{% if hypervisor.items|length > 4 %}
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton{{ forloop.counter0 }}" data-toggle="dropdown">
{{ hypervisor.items|slice:"4:"|length }} {% trans 'more' %}...
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton{{ forloop.counter0 }}" role="menu">
{% for arc in hypervisor.keys|slice:"4:" %}
<a class="dropdown-item" tabindex="-1" href="#">{{ arc }}</a>
{% endfor %}
</div>
<div class="row">
<div class="col-lg-12">
{% 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="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;">
<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>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-long-arrow-right"></i> {% trans "RAM 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;">
<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>
{% endblock %}
{% block script %}
<script src="{% static "js/Chart.min.js" %}"></script>
<script src="{% static "js/Chart.bundle.min.js" %}"></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 cpuChart = new Chart(cpu_ctx).Line(cpuLineData, {
animation: false,
pointDotRadius: 2,
scaleLabel: "<%=value%> %",
scaleOverride: true,
scaleSteps: 5,
scaleStepWidth: 20,
scaleStartValue: 0,
responsive: true
var cpuChart = new Chart(cpu_ctx, {
type: 'line',
data: {
datasets : [{
label: 'Usage',
backgroundColor: "rgba(241,72,70,0.5)",
pointRadius: 2,
}]
},
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 = {
labels : [0, 0, 0, 0, 0],
datasets : [
{
fillColor : "rgba(249,134,33,0.5)",
strokeColor : "rgba(249,134,33,1)",
pointColor : "rgba(249,134,33,1)",
pointStrokeColor : "#fff",
pointHighlightFill : "#fff",
pointHighlightStroke : "rgba(151,187,205,1)",
data : [0, 0, 0, 0, 0]
var mem_ctx = document.getElementById("memChart").getContext("2d");
var memChart = new Chart(mem_ctx, {
type: 'line',
data: {
datasets: [{
pointRadius: 2,
}]
},
options: {
responsive: true,
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';
}
}
}
]
}
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
});
if (Boolean({{ status }}) === 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.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);
}
</script>
{% endblock %}

View file

@ -1,3 +1,143 @@
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import reverse
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 . import views
from secrets.views import secrets
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 = [
url(r'^$', views.computes, name='computes'),
url(r'^overview/(?P<compute_id>[0-9]+)/$', views.overview, name='overview'),
url(r'^statistics/(?P<compute_id>[0-9]+)/$',
views.compute_graph, name='compute_graph'),
path('', views.computes, name='computes'),
path('add_tcp_host/', views.compute_create, {'FormClass': forms.TcpComputeForm}, name='add_tcp_host'),
path('add_ssh_host/', views.compute_create, {'FormClass': forms.SshComputeForm}, name='add_ssh_host'),
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
from django.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from computes.models import Compute
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 django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
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):
"""
:param request:
:return:
"""
if not request.user.is_superuser:
return HttpResponseRedirect(reverse('index'))
computes = Compute.objects.filter().order_by("name")
def get_hosts_status(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())
return render(request, "computes/list.html", {"computes": computes})
@login_required
@superuser_only
def overview(request, compute_id):
"""
:param request:
:return:
"""
if not request.user.is_superuser:
return HttpResponseRedirect(reverse('index'))
error_messages = []
compute = get_object_or_404(Compute, pk=compute_id)
status = (
"true" if connection_manager.host_is_up(compute.type, compute.hostname) is True else "false"
)
try:
conn = wvmHostDetails(compute.hostname,
conn = wvmHostDetails(
compute.hostname,
compute.login,
compute.password,
compute.type)
compute.type,
)
hostname, host_arch, host_memory, logical_cpu, model_cpu, uri_conn = conn.get_node_info()
hypervisor = conn.hypervisor_type()
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()
except libvirtError as lib_err:
error_messages.append(lib_err)
return render(request, 'overview.html', locals())
return render(request, "overview.html", locals())
@superuser_only
def instances(request, compute_id):
compute = get_object_or_404(Compute, pk=compute_id)
utils.refresh_instance_database(compute)
instances = Instance.objects.filter(compute=compute).prefetch_related("userinstance_set")
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):
"""
:param request:
:param compute_id:
:return:
"""
points = 5
datasets = {}
cookies = {}
compute = get_object_or_404(Compute, pk=compute_id)
curent_time = time.strftime("%H:%M:%S")
try:
conn = wvmHostDetails(compute.hostname,
conn = wvmHostDetails(
compute.hostname,
compute.login,
compute.password,
compute.type)
compute.type,
)
current_time = timezone.now().strftime("%H:%M:%S")
cpu_usage = conn.get_cpu_usage()
mem_usage = conn.get_memory_usage()
conn.close()
except libvirtError:
cpu_usage = 0
mem_usage = 0
cpu_usage = {"usage": 0}
mem_usage = {"usage": 0}
current_time = 0
try:
cookies['cpu'] = request.COOKIES['cpu']
cookies['mem'] = request.COOKIES['mem']
cookies['timer'] = request.COOKIES['timer']
except KeyError:
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']})
data = json.dumps(
{
"cpudata": cpu_usage["usage"],
"memdata": mem_usage,
"timeline": current_time,
}
)
response = HttpResponse()
response['Content-Type'] = "text/javascript"
response.cookies['cpu'] = datasets['cpu']
response.cookies['timer'] = datasets['timer']
response.cookies['mem'] = datasets['mem']
response["Content-Type"] = "text/javascript"
response.write(data)
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
#
import SocketServer
import socketserver
import json
import guestfs
import re
PORT = 16510
ADDRESS = "0.0.0.0"
class MyTCPServer(SocketServer.ThreadingTCPServer):
class MyTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
class MyTCPServerHandler(SocketServer.BaseRequestHandler):
class MyTCPServerHandler(socketserver.BaseRequestHandler):
def handle(self):
# recive data
data = json.loads(self.request.recv(1024).strip())
d = self.request.recv(1024).strip()
data = json.loads(d)
# GuestFS
gfs = guestfs.GuestFS(python_return_dict=True)
@ -42,17 +42,18 @@ class MyTCPServerHandler(SocketServer.BaseRequestHandler):
if data['action'] == 'publickey':
if not gfs.is_dir('/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.chmod(0600, '/root/.ssh/authorized_keys')
gfs.chmod(600, '/root/.ssh/authorized_keys')
self.request.sendall(json.dumps({'return': 'success'}))
gfs.umount(part)
except RuntimeError:
pass
gfs.shutdown()
gfs.close()
except RuntimeError, err:
self.request.sendall(json.dumps({'return': 'error', 'message': err.message}))
except Exception as err:
self.request.sendall(bytes(json.dumps({'return': 'error', 'message': str(err)}).encode()))
server = MyTCPServer((ADDRESS, PORT), MyTCPServerHandler)
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 Host $host:$server_port;
proxy_set_header X-Forwarded-Proto $remote_addr;
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
proxy_set_header X-Forwarded-Ssl off;
proxy_connect_timeout 1800;
proxy_read_timeout 1800;
proxy_send_timeout 1800;
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