mirror of
https://github.com/retspen/webvirtcloud
synced 2024-11-01 03:54:15 +00:00
commit
66b1d7b8a6
651 changed files with 292340 additions and 48535 deletions
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
70
.github/workflows/codeql-analysis.yml
vendored
Normal 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
89
.github/workflows/linter.yml
vendored
Normal 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
6
.gitignore
vendored
|
@ -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
5
.gitpod.yml
Normal 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"'
|
16
.travis.yml
16
.travis.yml
|
@ -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
|
||||
|
|
61
Dockerfile
61
Dockerfile
|
@ -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
237
README.md
|
@ -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).
|
||||
|
|
52
Vagrantfile
vendored
52
Vagrantfile
vendored
|
@ -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
|
||||
SHELL
|
||||
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
1
_config.yml
Normal file
|
@ -0,0 +1 @@
|
|||
theme: jekyll-theme-cayman
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'accounts.apps.AccountsConfig'
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
51
accounts/apps.py
Normal file
51
accounts/apps.py
Normal 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)
|
|
@ -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
|
||||
|
|
@ -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'))
|
||||
|
|
|
@ -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,),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
]
|
24
accounts/migrations/0002_permissionset.py
Normal file
24
accounts/migrations/0002_permissionset.py
Normal 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': (),
|
||||
},
|
||||
),
|
||||
]
|
24
accounts/migrations/0003_auto_20200604_0930.py
Normal file
24
accounts/migrations/0003_auto_20200604_0930.py
Normal 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)]),
|
||||
),
|
||||
]
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
44
accounts/migrations/0004_auto_20200615_0637.py
Normal file
44
accounts/migrations/0004_auto_20200615_0637.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
20
accounts/migrations/0005_auto_20200616_1039.py
Normal file
20
accounts/migrations/0005_auto_20200616_1039.py
Normal 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')},
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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 = [
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -1,140 +1,89 @@
|
|||
{% 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">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Key name" %}</th>
|
||||
<th>{% trans "Public key" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for publickey in publickeys %}
|
||||
<tr>
|
||||
<td>{{ publickey.keyname }}</td>
|
||||
<td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</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">×</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 %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="instances">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
{% for publickey in publickeys %}
|
||||
<tr>
|
||||
<td>{{ publickey.keyname }}</td>
|
||||
<td title="{{ publickey.keypublic }}">{{ publickey.keypublic|truncatechars:64 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
|
@ -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">×</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 %}
|
34
accounts/templates/accounts/change_password_form.html
Normal file
34
accounts/templates/accounts/change_password_form.html
Normal 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 %}
|
7
accounts/templates/accounts/email/otp.html
Normal file
7
accounts/templates/accounts/email/otp.html
Normal 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 %}
|
32
accounts/templates/accounts/email_otp_form.html
Normal file
32
accounts/templates/accounts/email_otp_form.html
Normal 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 %}
|
30
accounts/templates/accounts/otp_login.html
Normal file
30
accounts/templates/accounts/otp_login.html
Normal 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 %}
|
|
@ -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>
|
|
@ -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">×</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 %}
|
|
@ -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">×</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 %}
|
|
@ -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 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>
|
||||
{% trans "Incorrect username or password." %}
|
||||
</div>
|
||||
<div class="col-xs-12" role="main">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{% trans "Incorrect username or password." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form class="form-signin" method="post" role="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' %}">
|
||||
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<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="{% 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>
|
||||
{% endblock %}
|
|
@ -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>
|
||||
|
|
|
@ -1,117 +1,80 @@
|
|||
{% 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>
|
||||
{% if publickeys %}
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tbody style="text-align: 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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "Key name" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" name="keyname" placeholder="{% trans "Enter Name" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 control-label">{% trans "Public key" %}</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea name="keypublic" class="form-control" rows="6" placeholder="{% trans "Enter Public Key" %}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">{% trans "Add" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group mb-0 float-right">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% icon 'pencil' %} {% trans "Update" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane tab-pane-bordered fade" id="ssh-keys">
|
||||
{% if publickeys %}
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tbody class="text-center">
|
||||
{% for key in publickeys %}
|
||||
<tr>
|
||||
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
||||
<td>
|
||||
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
|
||||
{% icon 'trash' %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{%trans "Add SSH Key" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'accounts:ssh_key_create' %}" role="form" aria-label="Add key to user form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form ssh_key_form layout='horizontal' %}
|
||||
<div class="form-group mb-0 float-right">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% icon 'plus' %} {% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,7 +1,8 @@
|
|||
from django import template
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
62
accounts/utils.py
Normal 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,
|
||||
)
|
|
@ -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', '')
|
||||
|
||||
if settings.ALLOW_INSTANCE_MULTIPLE_OWNER:
|
||||
check_inst = UserInstance.objects.filter(instance_id=int(inst_id), user_id=int(user_id))
|
||||
else:
|
||||
check_inst = UserInstance.objects.filter(instance_id=int(inst_id))
|
||||
|
||||
if check_inst:
|
||||
msg = _("Instance already added")
|
||||
error_messages.append(msg)
|
||||
else:
|
||||
add_user_inst = UserInstance(instance_id=int(inst_id), user_id=int(user_id))
|
||||
add_user_inst.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
return render(
|
||||
request,
|
||||
"account.html",
|
||||
{
|
||||
"user": user,
|
||||
"user_insts": user_insts,
|
||||
"instances": instances,
|
||||
"publickeys": publickeys,
|
||||
"otp_enabled": settings.OTP_ENABLED,
|
||||
},
|
||||
)
|
||||
|
||||
return render(request, 'account.html', locals())
|
||||
|
||||
@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:
|
||||
return redirect(reverse("accounts:account", args=[user.id]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{"object": user_instance},
|
||||
)
|
||||
|
||||
|
||||
def email_otp(request):
|
||||
form = EmailOTPForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
UserModel = get_user_model()
|
||||
try:
|
||||
user = UserModel.objects.get(email=form.cleaned_data["email"])
|
||||
except UserModel.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
device = get_user_totp_device(user)
|
||||
send_email_with_otp(user, device)
|
||||
|
||||
messages.success(request, _("OTP Sent to %(email)s") % {"email": form.cleaned_data["email"]})
|
||||
return redirect("accounts:login")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"accounts/email_otp_form.html",
|
||||
{
|
||||
"form": form,
|
||||
"title": _("Email OTP"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@superuser_only
|
||||
def admin_email_otp(request, user_id):
|
||||
user = get_object_or_404(get_user_model(), pk=user_id)
|
||||
device = get_user_totp_device(user)
|
||||
if user.email != "":
|
||||
send_email_with_otp(user, device)
|
||||
messages.success(request, _("OTP QR code was emailed to user %(user)s") % {"user": user})
|
||||
else:
|
||||
messages.error(request, _("User email not set, failed to send QR code"))
|
||||
return redirect("accounts:account", user.id)
|
||||
|
|
1
admin/__init__.py
Normal file
1
admin/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
defautl_app_config = 'admin.apps.AdminConfig'
|
5
admin/apps.py
Normal file
5
admin/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AdminConfig(AppConfig):
|
||||
name = 'admin'
|
10
admin/decorators.py
Normal file
10
admin/decorators.py
Normal 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
117
admin/forms.py
Normal 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"]
|
29
admin/migrations/0001_initial.py
Normal file
29
admin/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
14
admin/migrations/0002_auto_20200609_0830.py
Normal file
14
admin/migrations/0002_auto_20200609_0830.py
Normal 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
13
admin/models.py
Normal 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
|
62
admin/templates/admin/group_list.html
Normal file
62
admin/templates/admin/group_list.html
Normal 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">×</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 %}
|
48
admin/templates/admin/logs.html
Normal file
48
admin/templates/admin/logs.html
Normal 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">×</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 %}
|
26
admin/templates/admin/user_form.html
Normal file
26
admin/templates/admin/user_form.html
Normal 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 %}
|
82
admin/templates/admin/user_list.html
Normal file
82
admin/templates/admin/user_list.html
Normal 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">×</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
120
admin/tests.py
Normal 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
18
admin/urls.py
Normal 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
206
admin/views.py
Normal 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
5
appsettings/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AppsettingsConfig(AppConfig):
|
||||
name = 'appsettings'
|
13
appsettings/context_processors.py
Normal file
13
appsettings/context_processors.py
Normal 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
10
appsettings/middleware.py
Normal 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)
|
25
appsettings/migrations/0001_initial.py
Normal file
25
appsettings/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
79
appsettings/migrations/0002_auto_20200527_1603.py
Normal file
79
appsettings/migrations/0002_auto_20200527_1603.py
Normal 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),
|
||||
]
|
38
appsettings/migrations/0003_auto_20200615_0637.py
Normal file
38
appsettings/migrations/0003_auto_20200615_0637.py
Normal 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'),
|
||||
),
|
||||
]
|
35
appsettings/migrations/0004_auto_20200716_0637.py
Normal file
35
appsettings/migrations/0004_auto_20200716_0637.py
Normal 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),
|
||||
]
|
18
appsettings/migrations/0005_auto_20200911_1233.py
Normal file
18
appsettings/migrations/0005_auto_20200911_1233.py
Normal 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
14
appsettings/models.py
Normal 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
18
appsettings/settings.py
Normal 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)
|
74
appsettings/templates/appsettings.html
Normal file
74
appsettings/templates/appsettings.html
Normal 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
91
appsettings/views.py
Normal 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())
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -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']
|
||||
|
|
|
@ -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,),
|
||||
),
|
||||
]
|
||||
|
|
18
computes/migrations/0002_auto_20200529_1320.py
Normal file
18
computes/migrations/0002_auto_20200529_1320.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
38
computes/migrations/0003_auto_20200615_0637.py
Normal file
38
computes/migrations/0003_auto_20200615_0637.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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">×</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 %}
|
28
computes/templates/computes/form.html
Normal file
28
computes/templates/computes/form.html
Normal 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 %}
|
116
computes/templates/computes/instances.html
Normal file
116
computes/templates/computes/instances.html
Normal 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">×</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 %}
|
68
computes/templates/computes/list.html
Normal file
68
computes/templates/computes/list.html
Normal 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">×</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 %}
|
|
@ -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">×</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>
|
||||
|
|
|
@ -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" %}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-hdd-o"></i> <a href="{% url 'storages' compute.id %}">{% trans "Storages" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-sitemap"></i> <a href="{% url 'networks' compute.id %}">{% trans "Networks" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-wifi"></i> <a href="{% url 'interfaces' compute.id %}">{% trans "Interfaces" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<i class="fa fa-key"></i> <a href="{% url 'secrets' compute.id %}">{% trans "Secrets" %}</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<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 class="breadcrumb-item">
|
||||
<a href="{% url 'instances' compute.id %}">{% icon 'server' %} {% trans "Instances" %}</a>
|
||||
</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>
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<div class="row" id="max-width-page">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h3 class="page-header">{% trans "Performance" %}</h3>
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-long-arrow-right"></i> {% trans "CPU utilization" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="flot-chart">
|
||||
<div class="flot-chart-content" id="flot-moving-line-chart" style="padding: 0px; position: relative;">
|
||||
<canvas id="cpuChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shadow-sm">
|
||||
<h3 class="page-header">{% trans "Basic details" %}</h3>
|
||||
<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="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;">
|
||||
<canvas id="memChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
{% 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>
|
||||
<span class="badge bg-secondary text-light">{% trans 'Libvirt' %} </span>
|
||||
<span class="badge bg-primary text-light">{{ lib_version }}</span>
|
||||
</dd>
|
||||
<dt class="col-3">{% trans "Memory" %}</dt>
|
||||
<dd class="col-9">{{ host_memory|filesizeformat }}</dd>
|
||||
<dt class="col-3">{% trans "Architecture" %}</dt>
|
||||
<dd class="col-9">{{ host_arch }}</dd>
|
||||
<dt class="col-3">{% trans "Logical CPUs" %}</dt>
|
||||
<dd class="col-9">{{ logical_cpu }}</dd>
|
||||
<dt class="col-3">{% trans "Processor" %}</dt>
|
||||
<dd class="col-9">{{ model_cpu }}</dd>
|
||||
<dt class="col-3">{% trans "Connection" %}</dt>
|
||||
<dd class="col-9">{{ uri_conn }}</dd>
|
||||
<dt class="col-3">{% trans "Details" %}</dt>
|
||||
<dd class="col-9">{{ compute.details }}</dd>
|
||||
</dl>
|
||||
|
||||
<h3 class="page-header">{% trans "Performance" %}</h3>
|
||||
<div class="mx-3 shadow-sm">
|
||||
<div class="my-3 card border-success">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fa fa-long-arrow-right"></i>
|
||||
{% trans "CPU Utilization" %}
|
||||
</h5>
|
||||
<canvas id="cpuChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 card border-primary">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title ">
|
||||
<i class="fa fa-long-arrow-right"></i> {% trans "RAM Utilization" %}
|
||||
</h5>
|
||||
<canvas id="memChart" width="735" height="160"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
if (Boolean({{ status }}) === true) {
|
||||
window.setInterval(function graph_usage() {
|
||||
$.getJSON('{% url 'compute_graph' compute_id %}', function (data) {
|
||||
cpuChart.data.labels.push(data.timeline);
|
||||
memChart.data.labels.push(data.timeline);
|
||||
|
||||
cpuChart.data.datasets[0].data.push(data.cpudata);
|
||||
if (cpuChart.data.datasets[0].data.length > 10){
|
||||
cpuChart.data.labels.shift();
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
}
|
||||
memChart.options.scales.yAxes[0].ticks.max = parseInt(data.memdata.total / 1048576);
|
||||
memChart.options.scales.yAxes[0].ticks.stepSize = parseInt(data.memdata.total / (1048576 * 5));
|
||||
memChart.data.datasets[0].data.push(parseInt(data.memdata.usage / 1048576));
|
||||
|
||||
if (memChart.data.datasets[0].data.length > 10){
|
||||
memChart.data.labels.shift();
|
||||
memChart.data.datasets[0].data.shift();
|
||||
}
|
||||
|
||||
cpuChart.update();
|
||||
memChart.update();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
var mem_ctx = $("#memChart").get(0).getContext("2d");
|
||||
var memChart = new Chart(mem_ctx).Line(memLineData, {
|
||||
animation: false,
|
||||
pointDotRadius: 2,
|
||||
scaleLabel: "<%=value%> Mb",
|
||||
responsive: true
|
||||
});
|
||||
|
||||
window.setInterval(function graph_usage() {
|
||||
$.getJSON('{% url 'compute_graph' compute_id %}', function (data) {
|
||||
cpuChart.scale.xLabels = data.timeline;
|
||||
memChart.scale.xLabels = data.timeline;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
cpuChart.datasets[0].points[i].value = data.cpudata[i];
|
||||
memChart.datasets[0].points[i].value = data.memdata[i];
|
||||
}
|
||||
cpuChart.update();
|
||||
memChart.update();
|
||||
});
|
||||
}, 5000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
13
computes/utils.py
Normal 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
24
computes/validators.py
Normal 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'))
|
|
@ -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:
|
||||
"""
|
||||
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"
|
||||
)
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
conn = wvmHostDetails(
|
||||
compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type,
|
||||
)
|
||||
hostname, host_arch, host_memory, logical_cpu, model_cpu, uri_conn = conn.get_node_info()
|
||||
hypervisor = conn.get_hypervisors_domain_types()
|
||||
mem_usage = conn.get_memory_usage()
|
||||
emulator = conn.get_emulator(host_arch)
|
||||
version = conn.get_version()
|
||||
lib_version = conn.get_lib_version()
|
||||
conn.close()
|
||||
|
||||
error_messages = []
|
||||
return render(request, "overview.html", locals())
|
||||
|
||||
|
||||
@superuser_only
|
||||
def instances(request, compute_id):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
|
||||
try:
|
||||
conn = wvmHostDetails(compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type)
|
||||
hostname, host_arch, host_memory, logical_cpu, model_cpu, uri_conn = conn.get_node_info()
|
||||
hypervisor = conn.hypervisor_type()
|
||||
mem_usage = conn.get_memory_usage()
|
||||
conn.close()
|
||||
except libvirtError as lib_err:
|
||||
error_messages.append(lib_err)
|
||||
utils.refresh_instance_database(compute)
|
||||
instances = Instance.objects.filter(compute=compute).prefetch_related("userinstance_set")
|
||||
|
||||
return render(request, 'overview.html', locals())
|
||||
return render(request, "computes/instances.html", {"compute": compute, "instances": instances})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def compute_create(request, FormClass):
|
||||
form = FormClass(request.POST or None)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("computes"))
|
||||
|
||||
return render(request, "computes/form.html", {"form": form})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def compute_update(request, compute_id):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
|
||||
if compute.type == 1:
|
||||
FormClass = TcpComputeForm
|
||||
elif compute.type == 2:
|
||||
FormClass = SshComputeForm
|
||||
elif compute.type == 3:
|
||||
FormClass = TlsComputeForm
|
||||
elif compute.type == 4:
|
||||
FormClass = SocketComputeForm
|
||||
|
||||
form = FormClass(request.POST or None, instance=compute)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("computes"))
|
||||
|
||||
return render(request, "computes/form.html", {"form": form})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def compute_delete(request, compute_id):
|
||||
compute = get_object_or_404(Compute, pk=compute_id)
|
||||
if request.method == "POST":
|
||||
compute.delete()
|
||||
return redirect("computes")
|
||||
|
||||
return render(
|
||||
request,
|
||||
"common/confirm_delete.html",
|
||||
{"object": compute},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def compute_graph(request, compute_id):
|
||||
"""
|
||||
: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,
|
||||
compute.login,
|
||||
compute.password,
|
||||
compute.type)
|
||||
conn = wvmHostDetails(
|
||||
compute.hostname,
|
||||
compute.login,
|
||||
compute.password,
|
||||
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))
|
||||
|
|
|
@ -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()
|
||||
|
|
37
conf/nginx/centos_nginx.conf
Normal file
37
conf/nginx/centos_nginx.conf
Normal 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;
|
||||
}
|
85
conf/nginx/debian_nginx.conf
Normal file
85
conf/nginx/debian_nginx.conf
Normal 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;
|
||||
# }
|
||||
#}
|
85
conf/nginx/ubuntu_nginx.conf
Normal file
85
conf/nginx/ubuntu_nginx.conf
Normal 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;
|
||||
# }
|
||||
#}
|
|
@ -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
Loading…
Reference in a new issue