mirror of
https://github.com/retspen/webvirtcloud
synced 2026-03-23 11:04:49 +00:00
Added V2 from scratch
This commit is contained in:
parent
5c2232f4e8
commit
6c2925a35d
478 changed files with 21437 additions and 134206 deletions
20
.gitignore
vendored
20
.gitignore
vendored
|
|
@ -1,12 +1,10 @@
|
|||
.vagrant
|
||||
venv
|
||||
.vscode
|
||||
.idea
|
||||
.DS_*
|
||||
*.pyc
|
||||
db.sqlite3*
|
||||
console/cert.pem*
|
||||
tags
|
||||
dhcpd.*
|
||||
webvirtcloud/settings.py
|
||||
*migrations/*
|
||||
*.pyo
|
||||
.idea
|
||||
.vscode
|
||||
.vagrant
|
||||
.DS_*
|
||||
__pycache__
|
||||
venv
|
||||
node_modules
|
||||
db.sqlite3
|
||||
|
|
|
|||
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
v0.0.1 (2018-09-01)
|
||||
|
||||
* Init project
|
||||
59
Dockerfile
59
Dockerfile
|
|
@ -1,50 +1,23 @@
|
|||
FROM phusion/baseimage:0.9.17
|
||||
MAINTAINER Jethro Yu <comet.jc@gmail.com>
|
||||
FROM ubuntu:18.04
|
||||
|
||||
RUN echo 'APT::Get::Clean=always;' >> /etc/apt/apt.conf.d/99AutomaticClean
|
||||
WORKDIR /usr/src/
|
||||
|
||||
RUN apt-get update -qqy
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get -qyy install \
|
||||
-o APT::Install-Suggests=false \
|
||||
git python-virtualenv python-dev python-lxml libvirt-dev zlib1g-dev nginx libsasl2-modules
|
||||
COPY backend/requirements.txt /usr/src/requirements.txt
|
||||
COPY backend/requirements-dev.txt /usr/src/requirements-dev.txt
|
||||
|
||||
ADD . /srv/webvirtcloud
|
||||
RUN chown -R www-data:www-data /srv/webvirtcloud
|
||||
RUN set -ex \
|
||||
&& apt-get update -q \
|
||||
&& apt-get -y install \
|
||||
python3-pip \
|
||||
libvirt-dev \
|
||||
libmariadbclient-dev
|
||||
|
||||
# Setup webvirtcloud
|
||||
RUN cd /srv/webvirtcloud && \
|
||||
virtualenv venv && \
|
||||
. venv/bin/activate && \
|
||||
pip install -U pip && \
|
||||
pip install -r conf/requirements.txt && \
|
||||
chown -R www-data:www-data /srv/webvirtcloud
|
||||
RUN pip3 install -r requirements-dev.txt
|
||||
RUN rm -f requirements.txt requirements-dev.txt
|
||||
|
||||
RUN cd /srv/webvirtcloud && . venv/bin/activate && \
|
||||
python manage.py migrate && \
|
||||
chown -R www-data:www-data /srv/webvirtcloud
|
||||
WORKDIR /app
|
||||
VOLUME /app
|
||||
|
||||
# Setup Nginx
|
||||
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf && \
|
||||
rm /etc/nginx/sites-enabled/default && \
|
||||
chown -R www-data:www-data /var/lib/nginx
|
||||
EXPOSE 8000 6080
|
||||
|
||||
ADD 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
|
||||
|
||||
# Define mountable directories.
|
||||
#VOLUME []
|
||||
|
||||
# Use baseimage-docker's init system.
|
||||
CMD ["/sbin/my_init"]
|
||||
CMD ["/bin/bash"]
|
||||
|
|
|
|||
177
LICENSE.md
Normal file
177
LICENSE.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
273
README.md
273
README.md
|
|
@ -1,272 +1 @@
|
|||
## WebVirtCloud Beta
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* 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!!!
|
||||
|
||||
How to update <code>gstfsd</code> daemon on hypervisor:
|
||||
|
||||
```bash
|
||||
wget -O - https://clck.ru/9VMRH | sudo tee -a /usr/local/bin/gstfsd
|
||||
sudo service supervisor restart
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### Generate secret key
|
||||
You should generate SECRET_KEY after cloning repo. Then put it into webvirtcloud/settings.py.
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
```bash
|
||||
sudo apt-get -y install git virtualenv python-virtualenv python-dev python-lxml libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python-guestfs
|
||||
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
|
||||
sudo cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d
|
||||
sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d
|
||||
cd ..
|
||||
sudo mv webvirtcloud /srv
|
||||
sudo chown -R www-data:www-data /srv/webvirtcloud
|
||||
cd /srv/webvirtcloud
|
||||
virtualenv venv
|
||||
source venv/bin/activate
|
||||
pip install -r conf/requirements.txt
|
||||
python manage.py migrate
|
||||
sudo chown -R www-data:www-data /srv/webvirtcloud
|
||||
sudo rm /etc/nginx/sites-enabled/default
|
||||
```
|
||||
|
||||
Restart services for running WebVirtCloud:
|
||||
|
||||
```bash
|
||||
sudo service nginx restart
|
||||
sudo service supervisor restart
|
||||
```
|
||||
|
||||
Setup libvirt and KVM on server
|
||||
|
||||
```bash
|
||||
wget -O - https://clck.ru/9V9fH | sudo sh
|
||||
```
|
||||
|
||||
### Install WebVirtCloud panel (CentOS)
|
||||
|
||||
```bash
|
||||
sudo yum -y install python-virtualenv python-devel libvirt-devel glibc gcc nginx supervisor python-lxml git python-libguestfs
|
||||
```
|
||||
|
||||
#### Creating directories and cloning repo
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
#### Configure the supervisor for CentOS
|
||||
Add the following after the [include] line (after **files = ... ** actually):
|
||||
```bash
|
||||
sudo vim /etc/supervisord.conf
|
||||
|
||||
[program:webvirtcloud]
|
||||
command=/srv/webvirtcloud/venv/bin/gunicorn webvirtcloud.wsgi:application -c /srv/webvirtcloud/gunicorn.conf.py
|
||||
directory=/srv/webvirtcloud
|
||||
user=nginx
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
|
||||
[program:novncd]
|
||||
command=/srv/webvirtcloud/venv/bin/python /srv/webvirtcloud/console/novncd
|
||||
directory=/srv/webvirtcloud
|
||||
user=nginx
|
||||
autostart=true
|
||||
autorestart=true
|
||||
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:
|
||||
|
||||
```
|
||||
# server {
|
||||
# listen 80 default_server;
|
||||
# listen [::]:80 default_server;
|
||||
# server_name _;
|
||||
# root /usr/share/nginx/html;
|
||||
#
|
||||
# # Load configuration files for the default server block.
|
||||
# include /etc/nginx/default.d/*.conf;
|
||||
#
|
||||
# location / {
|
||||
# }
|
||||
#
|
||||
# error_page 404 /404.html;
|
||||
# location = /40x.html {
|
||||
# }
|
||||
#
|
||||
# error_page 500 502 503 504 /50x.html;
|
||||
# location = /50x.html {
|
||||
# }
|
||||
# }
|
||||
}
|
||||
```
|
||||
|
||||
Also make sure file in **/etc/nginx/conf.d/webvirtcloud.conf** has the proper paths:
|
||||
```
|
||||
upstream gunicorn_server {
|
||||
#server unix:/srv/webvirtcloud/venv/wvcloud.socket fail_timeout=0;
|
||||
server 127.0.0.1:8000 fail_timeout=0;
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name servername.domain.com;
|
||||
access_log /var/log/nginx/webvirtcloud-access_log;
|
||||
|
||||
location /static/ {
|
||||
root /srv/webvirtcloud;
|
||||
expires max;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://gunicorn_server;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
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;
|
||||
client_max_body_size 1024M;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Change permissions so nginx can read the webvirtcloud folder:
|
||||
|
||||
```bash
|
||||
sudo chown -R nginx:nginx /srv/webvirtcloud
|
||||
```
|
||||
|
||||
Change permission for selinux:
|
||||
|
||||
```bash
|
||||
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/webvirtcloud(/.*)"
|
||||
```
|
||||
|
||||
Add required user to the kvm group:
|
||||
```bash
|
||||
sudo usermod -G kvm -a webvirtmgr
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
WSGIDaemonProcess webvirtcloud threads=2 maximum-requests=1000 display-name=webvirtcloud
|
||||
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
|
||||
```
|
||||
|
||||
Done!!
|
||||
|
||||
Go to http://serverip and you should see the login screen.
|
||||
|
||||
### Alternative running novncd via runit
|
||||
Alternative to running nonvcd via supervisor is runit.
|
||||
|
||||
On Debian systems install runit and configure novncd service
|
||||
```
|
||||
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>
|
||||
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.
|
||||
```
|
||||
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.
|
||||
```
|
||||
sudo -u www-data ssh-copy-id root@compute1
|
||||
```
|
||||
|
||||
### Cloud-init
|
||||
Currently supports only root ssh authorized keys and hostname. Example configuration of the cloud-init client follows.
|
||||
```
|
||||
datasource:
|
||||
OpenStack:
|
||||
metadata_urls: [ "http://webvirtcloud.domain.com/datasource" ]
|
||||
```
|
||||
|
||||
### How To Update
|
||||
```bash
|
||||
sudo virtualenv venv
|
||||
sudo source venv/bin/activate
|
||||
git pull
|
||||
pip install -U -r conf/requirements.txt
|
||||
python manage.py migrate
|
||||
sudo service supervisor restart
|
||||
```
|
||||
|
||||
### License
|
||||
|
||||
WebVirtCloud is licensed under the [Apache Licence, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).
|
||||
# webvirtcloud
|
||||
31
Vagrantfile
vendored
31
Vagrantfile
vendored
|
|
@ -2,17 +2,22 @@
|
|||
# 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
|
||||
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 python-lxml libvirt-dev zlib1g-dev
|
||||
virtualenv /vagrant/venv
|
||||
source /vagrant/venv/bin/activate
|
||||
pip install -r /vagrant/dev/requirements.txt
|
||||
SHELL
|
||||
config.vm.box = "bento/centos-7.5"
|
||||
config.ssh.insert_key = false
|
||||
config.vm.provision :shell, path: "devenv/vagrant/bootstrap.sh"
|
||||
config.vm.network "private_network", ip: "192.168.250.254", auto_config: false
|
||||
config.vm.network "private_network", ip: "10.16.0.254", auto_config: false
|
||||
config.vm.network "forwarded_port", guest: 9090, host: 9090
|
||||
config.vm.network "forwarded_port", guest: 16509, host: 16509
|
||||
|
||||
(0..1 - 1).each do |i|
|
||||
config.vm.define "node#{i}" do |kvm|
|
||||
kvm.vm.hostname = "node#{i}"
|
||||
kvm.vm.provider :virtualbox do |vb|
|
||||
vb.cpus = "2"
|
||||
vb.memory = "4096"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
theme: jekyll-theme-cayman
|
||||
|
|
@ -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 +0,0 @@
|
|||
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
|
||||
|
||||
|
||||
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')},)
|
||||
|
||||
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'))
|
||||
|
|
@ -1,29 +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),
|
||||
('instances', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserInstance',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('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)),
|
||||
],
|
||||
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),
|
||||
]
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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,35 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-06-25 12:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0009_auto_20171026_0805'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_cpus',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_disk_size',
|
||||
field=models.IntegerField(default=20, help_text=b'-1 for unlimited. Any integer value'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_instances',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_memory',
|
||||
field=models.IntegerField(default=2048, help_text=b'-1 for unlimited. Any integer value'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-06-25 13:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0010_auto_20180625_1236'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_cpus',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_disk_size',
|
||||
field=models.IntegerField(default=20, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_instances',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_memory',
|
||||
field=models.IntegerField(default=2048, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-06-25 13:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0011_auto_20180625_1313'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_cpus',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_disk_size',
|
||||
field=models.IntegerField(default=20, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_instances',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_memory',
|
||||
field=models.IntegerField(default=2048, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-06-25 13:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0012_auto_20180625_1331'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_cpus',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.'), django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_disk_size',
|
||||
field=models.IntegerField(default=20, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.'), django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_instances',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.'), django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_memory',
|
||||
field=models.IntegerField(default=2048, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.RegexValidator(re.compile('^-?\\d+\\Z'), code='invalid', message='Enter a valid integer.'), django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-08-08 11:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0013_auto_20180625_1358'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_cpus',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_disk_size',
|
||||
field=models.IntegerField(default=20, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_instances',
|
||||
field=models.IntegerField(default=1, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userattributes',
|
||||
name='max_memory',
|
||||
field=models.IntegerField(default=2048, help_text=b'-1 for unlimited. Any integer value', validators=[django.core.validators.MinValueValidator(-1)]),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-08-08 11:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0014_auto_20180808_1436'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='usersshkey',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from instances.models import Instance
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
|
||||
class UserInstance(models.Model):
|
||||
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
|
||||
|
||||
|
||||
class UserSSHKey(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.DO_NOTHING)
|
||||
keyname = models.CharField(max_length=25)
|
||||
keypublic = models.CharField(max_length=500)
|
||||
|
||||
def __unicode__(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, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1),])
|
||||
max_cpus = models.IntegerField(default=1, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)])
|
||||
max_memory = models.IntegerField(default=2048, help_text="-1 for unlimited. Any integer value", validators=[MinValueValidator(-1)])
|
||||
max_disk_size = models.IntegerField(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):
|
||||
return self.user.username
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "User" %} - {{ user }}{% endblock %}
|
||||
{% 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 -->
|
||||
|
||||
{% 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 %}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% block title %}{% trans "Users" %}{% endblock %}
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
{% include 'create_user_block.html' %}
|
||||
<div class="pull-right search">
|
||||
<input id="filter" class="form-control" type="text" placeholder="Search">
|
||||
</div>
|
||||
<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 %}
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Status</th>
|
||||
<th>Staff</th>
|
||||
<th>Superuser</th>
|
||||
<th>Clone</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="searchable">
|
||||
{% for user in users %}
|
||||
<tr class="{% if not user.is_active %}danger{% endif %}">
|
||||
<td>
|
||||
<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>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
{% trans "Active" %}
|
||||
{% else %}
|
||||
{% trans "Blocked" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{% if user.is_staff %}<span class="glyphicon glyphicon-ok"></span>{% endif %}</td>
|
||||
<td>{% if user.is_superuser %}<span class="glyphicon glyphicon-ok"></span>{% endif %}</td>
|
||||
<td>{% if user.userattributes.can_clone_instances %}<span class="glyphicon glyphicon-ok"></span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% for user in users %}
|
||||
<!-- 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">
|
||||
<form class="form-horizontal" method="post" role="form">{% csrf_token %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
function filter_table() {
|
||||
var rex = new RegExp($(this).val(), 'i');
|
||||
$('.searchable tr').hide();
|
||||
$('.searchable tr').filter(function () {
|
||||
return rex.test($(this).text());
|
||||
}).show();
|
||||
}
|
||||
$(document).ready(function () {
|
||||
(function ($) {
|
||||
$('#filter').keyup(filter_table)
|
||||
}(jQuery));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -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}}" required="True" >
|
||||
</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 }}" required="True">
|
||||
</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}}" required="True">
|
||||
</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 }}" required="True">
|
||||
</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 %}
|
||||
|
|
@ -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 btn-header 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">
|
||||
<form class="form-horizontal" method="post" action="" role="form">{% csrf_token %}
|
||||
<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">
|
||||
<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 +0,0 @@
|
|||
{% extends "base_auth.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "WebVirtCloud - Sign In" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="page-header">
|
||||
<a href="/"><h1>WebVirtCloud</h1></a>
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "base_auth.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "WebVirtCloud - Sign Out" %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="page-header">
|
||||
<a href="/"><h1>WebVirtCloud</h1></a>
|
||||
</div>
|
||||
<div class="col-xs-12" role="main">
|
||||
<div class="logout">
|
||||
<h1>{% trans "Successful log out" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load tags_fingerprint %}
|
||||
{% block title %}{% trans "Profile" %}{% endblock %}
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from django import template
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def ssh_to_fingerprint(line):
|
||||
try:
|
||||
key = base64.b64decode(line.strip().split()[1].encode('ascii'))
|
||||
fp_plain = hashlib.md5(key).hexdigest()
|
||||
return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
|
||||
except Exception:
|
||||
return 'Invalid key'
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib.auth import views as auth_views
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^login/$', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
|
||||
url(r'^logout/$', auth_views.LogoutView.as_view(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'),
|
||||
]
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
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 django.conf import settings
|
||||
from django.core.validators import ValidationError
|
||||
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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 '\n' in keypublic or '\r' in keypublic:
|
||||
msg = _("Invalid characters in public key")
|
||||
error_messages.append(msg)
|
||||
if not error_messages:
|
||||
addkeypublic = UserSSHKey(user_id=request.user.id, keyname=keyname, keypublic=keypublic)
|
||||
addkeypublic.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if 'keydelete' in request.POST:
|
||||
keyid = request.POST.get('keyid', '')
|
||||
delkeypublic = UserSSHKey.objects.get(id=keyid)
|
||||
delkeypublic.delete()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
return render(request, 'profile.html', locals())
|
||||
|
||||
@login_required
|
||||
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:
|
||||
CHECKBOX_MAPPING = {'on': True, 'off': False, }
|
||||
|
||||
user_id = request.POST.get('user_id', '')
|
||||
user_pass = request.POST.get('user_pass', '')
|
||||
user_edit = User.objects.get(id=user_id)
|
||||
|
||||
if user_pass != '': user_edit.set_password(user_pass)
|
||||
user_edit.is_staff = CHECKBOX_MAPPING.get(request.POST.get('user_is_staff', 'off'))
|
||||
user_edit.is_superuser = CHECKBOX_MAPPING.get(request.POST.get('user_is_superuser', 'off'))
|
||||
user_edit.save()
|
||||
|
||||
UserAttributes.create_missing_userattributes(user_edit)
|
||||
user_edit.userattributes.can_clone_instances = CHECKBOX_MAPPING.get(request.POST.get('userattributes_can_clone_instances', 'off'))
|
||||
user_edit.userattributes.max_instances = request.POST.get('userattributes_max_instances', 0)
|
||||
user_edit.userattributes.max_cpus = request.POST.get('userattributes_max_cpus', 0)
|
||||
user_edit.userattributes.max_memory = request.POST.get('userattributes_max_memory', 0)
|
||||
user_edit.userattributes.max_disk_size = request.POST.get('userattributes_max_disk_size', 0)
|
||||
|
||||
try:
|
||||
user_edit.userattributes.clean_fields()
|
||||
except ValidationError as exc:
|
||||
error_messages.append(exc)
|
||||
else:
|
||||
user_edit.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())
|
||||
|
||||
accounts_template_file = 'accounts.html'
|
||||
if settings.VIEW_ACCOUNTS_STYLE == "list":
|
||||
accounts_template_file = 'accounts-list.html'
|
||||
return render(request, accounts_template_file, locals())
|
||||
|
||||
|
||||
@login_required
|
||||
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')
|
||||
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', locals())
|
||||
5
backend/cloudinit/apps.py
Normal file
5
backend/cloudinit/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CloudinitConfig(AppConfig):
|
||||
name = 'cloudinit'
|
||||
3
backend/cloudinit/views.py
Normal file
3
backend/cloudinit/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
5
backend/compute/apps.py
Normal file
5
backend/compute/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ComputeConfig(AppConfig):
|
||||
name = 'compute'
|
||||
39
backend/compute/migrations/0001_initial.py
Normal file
39
backend/compute/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 2.0.8 on 2018-09-22 07:20
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('region', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Compute',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, validators=[django.core.validators.RegexValidator('^[a-zA-Z0-9.-_]+$')])),
|
||||
('description', models.CharField(blank=True, max_length=255)),
|
||||
('address', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator('^[a-zA-Z0-9._-]+$')], verbose_name='FQDN or IP Address')),
|
||||
('login', models.CharField(blank=True, max_length=20)),
|
||||
('password', models.CharField(blank=True, max_length=20)),
|
||||
('conn', models.IntegerField(choices=[(1, 'TCP'), (3, 'TLS'), (2, 'SSH'), (4, 'SOCKET')], default=1)),
|
||||
('is_active', models.BooleanField(default=False, verbose_name='Active')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_deleted', models.BooleanField(default=False, verbose_name='Deleted')),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='region.Region')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Compute',
|
||||
'verbose_name_plural': 'Computes',
|
||||
'ordering': ['-id'],
|
||||
},
|
||||
),
|
||||
]
|
||||
81
backend/compute/models.py
Normal file
81
backend/compute/models.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from django.db import models
|
||||
from django.core.validators import RegexValidator
|
||||
from region.models import Region
|
||||
|
||||
|
||||
class Compute(models.Model):
|
||||
|
||||
TCP = 1
|
||||
TLS = 3
|
||||
SSH = 2
|
||||
SOCKET = 4
|
||||
|
||||
CONN_CHOICES = (
|
||||
(TCP, 'TCP'),
|
||||
(TLS, 'TLS'),
|
||||
(SSH, 'SSH'),
|
||||
(SOCKET, 'SOCKET'),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=200,
|
||||
validators=[RegexValidator('^[a-zA-Z0-9.-_]+$')],
|
||||
)
|
||||
|
||||
region = models.ForeignKey(
|
||||
Region,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
address = models.CharField(
|
||||
'FQDN or IP Address',
|
||||
max_length=50,
|
||||
validators=[RegexValidator('^[a-zA-Z0-9._-]+$')],
|
||||
)
|
||||
|
||||
login = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
password = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
conn = models.IntegerField(
|
||||
choices=CONN_CHOICES,
|
||||
default=TCP,
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(
|
||||
'Active',
|
||||
default=False,
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
|
||||
is_deleted = models.BooleanField(
|
||||
'Deleted',
|
||||
default=False,
|
||||
)
|
||||
|
||||
deleted_at = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-id']
|
||||
verbose_name = "Compute"
|
||||
verbose_name_plural = "Computes"
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
3
backend/compute/views.py
Normal file
3
backend/compute/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
5
backend/flavor/apps.py
Normal file
5
backend/flavor/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FlavorConfig(AppConfig):
|
||||
name = 'flavor'
|
||||
61
backend/flavor/migrations/0001_initial.py
Normal file
61
backend/flavor/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Generated by Django 2.0.8 on 2018-09-22 07:20
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Image',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(max_length=200, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')])),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('url', models.URLField()),
|
||||
('md5sum', models.CharField(max_length=50)),
|
||||
('is_active', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Instance Image',
|
||||
'verbose_name_plural': 'Instance Images',
|
||||
'ordering': ['-id'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Size',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.CharField(max_length=200, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')])),
|
||||
('name', models.CharField(blank=True, max_length=255)),
|
||||
('cpu', models.IntegerField(default=1)),
|
||||
('disk_bytes', models.BigIntegerField(default=21474836480)),
|
||||
('memory_bytes', models.BigIntegerField(default=536870912)),
|
||||
('is_active', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Instance Size',
|
||||
'verbose_name_plural': 'Instance Sizes',
|
||||
'ordering': ['-id'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='required_size',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='flavor.Size'),
|
||||
),
|
||||
]
|
||||
111
backend/flavor/models.py
Normal file
111
backend/flavor/models.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
from django.db import models
|
||||
from django.core.validators import validate_slug
|
||||
|
||||
|
||||
class Size(models.Model):
|
||||
|
||||
slug = models.CharField(
|
||||
max_length=200,
|
||||
validators=[validate_slug],
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
cpu = models.IntegerField(
|
||||
default=1,
|
||||
)
|
||||
|
||||
disk_bytes = models.BigIntegerField(
|
||||
default=21474836480,
|
||||
)
|
||||
|
||||
memory_bytes = models.BigIntegerField(
|
||||
default=536870912,
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
|
||||
is_deleted = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
deleted_at = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-id']
|
||||
verbose_name = "Instance Size"
|
||||
verbose_name_plural = "Instance Sizes"
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def save(self):
|
||||
if self.ram < 1 * (1024 ** 2):
|
||||
self.ram = self.ram * (1024 ** 2)
|
||||
if self.disk < 1 * (1024 ** 3):
|
||||
self.disk = self.disk * (1024 ** 3)
|
||||
super(Size, self).save()
|
||||
|
||||
|
||||
class Image(models.Model):
|
||||
|
||||
slug = models.SlugField(
|
||||
max_length=200,
|
||||
validators=[validate_slug],
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
url = models.URLField(
|
||||
max_length=200,
|
||||
)
|
||||
|
||||
md5sum = models.CharField(
|
||||
max_length=50,
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
|
||||
is_deleted = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
deleted_at = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
required_size = models.ForeignKey(
|
||||
Size,
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-id']
|
||||
verbose_name = "Instance Image"
|
||||
verbose_name_plural = "Instance Images"
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
3
backend/flavor/views.py
Normal file
3
backend/flavor/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
5
backend/instance/apps.py
Normal file
5
backend/instance/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InstanceConfig(AppConfig):
|
||||
name = 'instance'
|
||||
107
backend/instance/models.py
Normal file
107
backend/instance/models.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
from region.models import Region
|
||||
from compute.models import Compute
|
||||
from flavor.models import Size, Image
|
||||
|
||||
|
||||
class Instance(models.Model):
|
||||
|
||||
ACTIVE = 1
|
||||
INACTIVE = 0
|
||||
STATUS_CHOICES = (
|
||||
(ACTIVE, 'active'),
|
||||
(INACTIVE, 'inactive'),
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
status = models.IntegerField(
|
||||
choices=STATUS_CHOICES,
|
||||
default=INACTIVE,
|
||||
)
|
||||
|
||||
uuid = models.CharField(
|
||||
max_length=40,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
compute = models.ForeignKey(
|
||||
Compute,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
size = models.ForeignKey(
|
||||
Size,
|
||||
)
|
||||
|
||||
image = models.ForeignKey(
|
||||
Image,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
region = models.ForeignKey(
|
||||
Region,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
disk_bytes = models.BigIntegerField(
|
||||
default=21474836480,
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
|
||||
deleted_at = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
is_task = models.BooleanField(
|
||||
default=True,
|
||||
)
|
||||
|
||||
task_id = models.IntegerField(
|
||||
default=1,
|
||||
)
|
||||
|
||||
is_private_network = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
is_locked = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
is_deleted = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
public_net_mac = models.CharField(
|
||||
max_length=18,
|
||||
default="52:54:00:01:02:03",
|
||||
)
|
||||
|
||||
private_net_mac = models.CharField(
|
||||
max_length=18,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-id']
|
||||
verbose_name = "Instance"
|
||||
verbose_name_plural = "Instances"
|
||||
|
||||
def __unicode__(self):
|
||||
return str(self.name)
|
||||
3
backend/instance/views.py
Normal file
3
backend/instance/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
2
backend/libvmgr/__init__.py
Normal file
2
backend/libvmgr/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
__author__ = "Anatoliy Guskov"
|
||||
__license__ = "Apache 2.0"
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import libvirt
|
||||
import threading
|
||||
import socket
|
||||
from vrtManager import util
|
||||
from rwlock import ReadWriteLock
|
||||
from django.conf import settings
|
||||
from libvmgr import util
|
||||
from libvmgr.rwlock import ReadWriteLock
|
||||
from libvirt import libvirtError
|
||||
|
||||
|
||||
|
|
@ -14,9 +13,36 @@ CONN_TCP = 1
|
|||
TLS_PORT = 16514
|
||||
SSH_PORT = 22
|
||||
TCP_PORT = 16509
|
||||
KEEPALIVE_COUNT = 30
|
||||
KEEPALIVE_INTERVAL = 5
|
||||
|
||||
|
||||
class wvmEventLoop(threading.Thread):
|
||||
def host_is_up(conn_type, hostname):
|
||||
"""
|
||||
returns True if the given host is up and we are able to establish
|
||||
a connection using the given credentials.
|
||||
"""
|
||||
try:
|
||||
socket_host = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
socket_host.settimeout(2)
|
||||
if conn_type == CONN_SSH:
|
||||
if ':' in hostname:
|
||||
LIBVIRT_HOST, PORT = (hostname).split(":")
|
||||
PORT = int(PORT)
|
||||
else:
|
||||
PORT = SSH_PORT
|
||||
LIBVIRT_HOST = hostname
|
||||
socket_host.connect((LIBVIRT_HOST, PORT))
|
||||
if conn_type == CONN_TCP:
|
||||
socket_host.connect((hostname, TCP_PORT))
|
||||
if conn_type == CONN_TLS:
|
||||
socket_host.connect((hostname, TLS_PORT))
|
||||
socket_host.close()
|
||||
except socket.error:
|
||||
raise libvirtError('Unable to connect to host server: Operation timed out')
|
||||
|
||||
|
||||
class wvcEventLoop(threading.Thread):
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
|
||||
# register the default event implementation
|
||||
# of libvirt, as we do not have an existing
|
||||
|
|
@ -26,7 +52,7 @@ class wvmEventLoop(threading.Thread):
|
|||
if name is None:
|
||||
name = 'libvirt event loop'
|
||||
|
||||
super(wvmEventLoop, self).__init__(group, target, name, args, kwargs)
|
||||
super(wvcEventLoop, self).__init__(group, target, name, args, kwargs)
|
||||
|
||||
# we run this thread in deamon mode, so it does
|
||||
# not block shutdown of the server
|
||||
|
|
@ -40,7 +66,7 @@ class wvmEventLoop(threading.Thread):
|
|||
libvirt.virEventRunDefaultImpl()
|
||||
|
||||
|
||||
class wvmConnection(object):
|
||||
class wvcConnection(object):
|
||||
"""
|
||||
class representing a single connection stored in the Connection Manager
|
||||
# to-do: may also need some locking to ensure to not connect simultaniously in 2 threads
|
||||
|
|
@ -86,10 +112,11 @@ class wvmConnection(object):
|
|||
# * set keep alive interval
|
||||
# * set connection close/fail handler
|
||||
try:
|
||||
self.connection.setKeepAlive(connection_manager.keepalive_interval, connection_manager.keepalive_count)
|
||||
self.connection.setKeepAlive(connection_manager.keepalive_interval,
|
||||
connection_manager.keepalive_count)
|
||||
try:
|
||||
self.connection.registerCloseCallback(self.__connection_close_callback, None)
|
||||
except:
|
||||
except Exception:
|
||||
# Temporary fix for libvirt > libvirt-0.10.2-41
|
||||
pass
|
||||
except libvirtError as e:
|
||||
|
|
@ -124,16 +151,7 @@ class wvmConnection(object):
|
|||
# on server shutdown libvirt module gets freed before the close callbacks are called
|
||||
# so we just check here if it is still present
|
||||
if libvirt is not None:
|
||||
if (reason == libvirt.VIR_CONNECT_CLOSE_REASON_ERROR):
|
||||
self.last_error = 'connection closed: Misc I/O error'
|
||||
elif (reason == libvirt.VIR_CONNECT_CLOSE_REASON_EOF):
|
||||
self.last_error = 'connection closed: End-of-file from server'
|
||||
elif (reason == libvirt.VIR_CONNECT_CLOSE_REASON_KEEPALIVE):
|
||||
self.last_error = 'connection closed: Keepalive timer triggered'
|
||||
elif (reason == libvirt.VIR_CONNECT_CLOSE_REASON_CLIENT):
|
||||
self.last_error = 'connection closed: Client requested it'
|
||||
else:
|
||||
self.last_error = 'connection closed: Unknown error'
|
||||
self.last_error = 'Connection closed'
|
||||
|
||||
# prevent other threads from using the connection (in the future)
|
||||
self.connection = None
|
||||
|
|
@ -211,7 +229,7 @@ class wvmConnection(object):
|
|||
# unregister callback (as it is no longer valid if this instance gets deleted)
|
||||
try:
|
||||
self.connection.unregisterCloseCallback()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __unicode__(self):
|
||||
|
|
@ -227,10 +245,10 @@ class wvmConnection(object):
|
|||
return u'qemu+{type}://{user}@{host}/system'.format(type=type_str, user=self.login, host=self.host)
|
||||
|
||||
def __repr__(self):
|
||||
return '<wvmConnection {connection_str}>'.format(connection_str=unicode(self))
|
||||
return '<wvcConnection {connection_str}>'.format(connection_str=str(self))
|
||||
|
||||
|
||||
class wvmConnectionManager(object):
|
||||
class wvcConnectionManager(object):
|
||||
def __init__(self, keepalive_interval=5, keepalive_count=5):
|
||||
self.keepalive_interval = keepalive_interval
|
||||
self.keepalive_count = keepalive_count
|
||||
|
|
@ -245,7 +263,7 @@ class wvmConnectionManager(object):
|
|||
self._connections_lock = ReadWriteLock()
|
||||
|
||||
# start event loop to handle keepalive requests and other events
|
||||
self._event_loop = wvmEventLoop()
|
||||
self._event_loop = wvcEventLoop()
|
||||
self._event_loop.start()
|
||||
|
||||
def _search_connection(self, host, login, passwd, conn):
|
||||
|
|
@ -271,10 +289,10 @@ class wvmConnectionManager(object):
|
|||
returns a connection object (as returned by the libvirt.open* methods) for the given host and credentials
|
||||
raises libvirtError if (re)connecting fails
|
||||
"""
|
||||
# force all string values to unicode
|
||||
host = unicode(host)
|
||||
login = unicode(login)
|
||||
passwd = unicode(passwd) if passwd is not None else None
|
||||
# force all string values to unicode changed for Python3 to str
|
||||
host = str(host)
|
||||
login = str(login)
|
||||
passwd = str(passwd) if passwd is not None else None
|
||||
|
||||
connection = self._search_connection(host, login, passwd, conn)
|
||||
|
||||
|
|
@ -286,7 +304,7 @@ class wvmConnectionManager(object):
|
|||
connection = self._search_connection(host, login, passwd, conn)
|
||||
if (connection is None):
|
||||
# create a new connection if a matching connection does not already exist
|
||||
connection = wvmConnection(host, login, passwd, conn)
|
||||
connection = wvcConnection(host, login, passwd, conn)
|
||||
|
||||
# add new connection to connection dict
|
||||
if host in self._connections:
|
||||
|
|
@ -307,268 +325,175 @@ class wvmConnectionManager(object):
|
|||
# raise libvirt error
|
||||
raise libvirtError(connection.last_error)
|
||||
|
||||
def host_is_up(self, conn_type, hostname):
|
||||
"""
|
||||
returns True if the given host is up and we are able to establish
|
||||
a connection using the given credentials.
|
||||
"""
|
||||
try:
|
||||
socket_host = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
socket_host.settimeout(1)
|
||||
if conn_type == CONN_SSH:
|
||||
if ':' in hostname:
|
||||
LIBVIRT_HOST, PORT = (hostname).split(":")
|
||||
PORT = int(PORT)
|
||||
else:
|
||||
PORT = SSH_PORT
|
||||
LIBVIRT_HOST = hostname
|
||||
socket_host.connect((LIBVIRT_HOST, PORT))
|
||||
if conn_type == CONN_TCP:
|
||||
socket_host.connect((hostname, TCP_PORT))
|
||||
if conn_type == CONN_TLS:
|
||||
socket_host.connect((hostname, TLS_PORT))
|
||||
socket_host.close()
|
||||
return True
|
||||
except Exception as err:
|
||||
return err
|
||||
|
||||
connection_manager = wvmConnectionManager(
|
||||
settings.LIBVIRT_KEEPALIVE_INTERVAL if hasattr(settings, 'LIBVIRT_KEEPALIVE_INTERVAL') else 5,
|
||||
settings.LIBVIRT_KEEPALIVE_COUNT if hasattr(settings, 'LIBVIRT_KEEPALIVE_COUNT') else 5
|
||||
)
|
||||
connection_manager = wvcConnectionManager(KEEPALIVE_INTERVAL, KEEPALIVE_COUNT)
|
||||
|
||||
|
||||
class wvmConnect(object):
|
||||
def __init__(self, host, login, passwd, conn):
|
||||
class wvcConnect(object):
|
||||
def __init__(self, host, login=None, passwd=None, conn_type=CONN_SOCKET, keepalive=True):
|
||||
self.login = login
|
||||
self.host = host
|
||||
self.passwd = passwd
|
||||
self.conn = conn
|
||||
self.conn_type = conn_type
|
||||
self.keepalive = keepalive
|
||||
|
||||
# is host up?
|
||||
host_is_up(self.conn_type, self.host)
|
||||
|
||||
# get connection from connection manager
|
||||
self.wvm = connection_manager.get_connection(host, login, passwd, conn)
|
||||
if self.keepalive:
|
||||
self.conn = connection_manager.get_connection(host, login, passwd, conn_type)
|
||||
else:
|
||||
if self.conn_type == CONN_TCP:
|
||||
def creds(credentials, user_data):
|
||||
for credential in credentials:
|
||||
if credential[0] == libvirt.VIR_CRED_AUTHNAME:
|
||||
credential[4] = self.login
|
||||
if len(credential[4]) == 0:
|
||||
credential[4] = credential[3]
|
||||
elif credential[0] == libvirt.VIR_CRED_PASSPHRASE:
|
||||
credential[4] = self.passwd
|
||||
else:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
flags = [libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_PASSPHRASE]
|
||||
auth = [flags, creds, None]
|
||||
uri = 'qemu+tcp://%s/system' % self.host
|
||||
try:
|
||||
self.conn = libvirt.openAuth(uri, auth, 0)
|
||||
except libvirtError:
|
||||
raise libvirtError('Connection Failed')
|
||||
|
||||
if self.conn_type == CONN_SSH:
|
||||
uri = 'qemu+ssh://%s@%s/system' % (self.login, self.host)
|
||||
try:
|
||||
self.conn = libvirt.open(uri)
|
||||
except libvirtError as err:
|
||||
raise err
|
||||
|
||||
if self.conn_type == CONN_TLS:
|
||||
def creds(credentials, user_data):
|
||||
for credential in credentials:
|
||||
if credential[0] == libvirt.VIR_CRED_AUTHNAME:
|
||||
credential[4] = self.login
|
||||
if len(credential[4]) == 0:
|
||||
credential[4] = credential[3]
|
||||
elif credential[0] == libvirt.VIR_CRED_PASSPHRASE:
|
||||
credential[4] = self.passwd
|
||||
else:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
flags = [libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_PASSPHRASE]
|
||||
auth = [flags, creds, None]
|
||||
uri = 'qemu+tls://%s@%s/system' % (self.login, self.host)
|
||||
try:
|
||||
self.conn = libvirt.openAuth(uri, auth, 0)
|
||||
except libvirtError:
|
||||
raise libvirtError('Connection Failed')
|
||||
|
||||
def get_cap_xml(self):
|
||||
"""Return xml capabilities"""
|
||||
return self.wvm.getCapabilities()
|
||||
|
||||
def get_dom_cap_xml(self):
|
||||
""" Return domcapabilities xml"""
|
||||
emulatorbin = self.emulator()
|
||||
machine = self.machine()
|
||||
arch = self.wvm.getInfo()[0]
|
||||
virttype = self.hypervisor_type()[arch][0]
|
||||
return self.wvm.getDomainCapabilities(emulatorbin, arch, machine, virttype)
|
||||
return self.conn.getCapabilities()
|
||||
|
||||
def is_kvm_supported(self):
|
||||
"""Return KVM capabilities."""
|
||||
return util.is_kvm_available(self.get_cap_xml())
|
||||
|
||||
def get_storages(self, only_actives=False):
|
||||
def get_storages(self):
|
||||
storages = []
|
||||
for pool in self.wvm.listStoragePools():
|
||||
for pool in self.conn.listStoragePools():
|
||||
storages.append(pool)
|
||||
for pool in self.conn.listDefinedStoragePools():
|
||||
storages.append(pool)
|
||||
if not only_actives:
|
||||
for pool in self.wvm.listDefinedStoragePools():
|
||||
storages.append(pool)
|
||||
return storages
|
||||
|
||||
def get_networks(self):
|
||||
virtnet = []
|
||||
for net in self.wvm.listNetworks():
|
||||
for net in self.conn.listNetworks():
|
||||
virtnet.append(net)
|
||||
for net in self.wvm.listDefinedNetworks():
|
||||
for net in self.conn.listDefinedNetworks():
|
||||
virtnet.append(net)
|
||||
return virtnet
|
||||
|
||||
def get_ifaces(self):
|
||||
interface = []
|
||||
for inface in self.wvm.listInterfaces():
|
||||
for inface in self.conn.listInterfaces():
|
||||
interface.append(inface)
|
||||
for inface in self.wvm.listDefinedInterfaces():
|
||||
for inface in self.conn.listDefinedInterfaces():
|
||||
interface.append(inface)
|
||||
return interface
|
||||
|
||||
def get_cache_modes(self):
|
||||
"""Get cache available modes"""
|
||||
return {
|
||||
'default': 'Default',
|
||||
'none': 'Disabled',
|
||||
'writethrough': 'Write through',
|
||||
'writeback': 'Write back',
|
||||
'directsync': 'Direct sync', # since libvirt 0.9.5
|
||||
'unsafe': 'Unsafe', # since libvirt 0.9.7
|
||||
}
|
||||
|
||||
def hypervisor_type(self):
|
||||
"""Return hypervisor type"""
|
||||
def hypervisors(ctx):
|
||||
result = {}
|
||||
for arch in ctx.xpath('/capabilities/guest/arch'):
|
||||
domain_types = arch.xpath('domain/@type')
|
||||
arch_name = arch.xpath('@name')[0]
|
||||
result[arch_name]= domain_types
|
||||
return result
|
||||
return util.get_xml_path(self.get_cap_xml(), func=hypervisors)
|
||||
|
||||
def emulator(self):
|
||||
"""Return emulator """
|
||||
return util.get_xml_path(self.get_cap_xml(), "/capabilities/guest/arch/emulator")
|
||||
|
||||
def machine(self):
|
||||
""" Return machine type of emulation"""
|
||||
return util.get_xml_path(self.get_cap_xml(), "/capabilities/guest/arch/machine")
|
||||
|
||||
def get_busses(self):
|
||||
"""Get available busses"""
|
||||
|
||||
def get_bus_list(ctx):
|
||||
result = []
|
||||
for disk_enum in ctx.xpath('/domainCapabilities/devices/disk/enum'):
|
||||
if disk_enum.xpath("@name")[0] == "bus":
|
||||
for values in disk_enum: result.append(values.text)
|
||||
return result
|
||||
|
||||
# return [ 'ide', 'scsi', 'usb', 'virtio' ]
|
||||
return util.get_xml_path(self.get_dom_cap_xml(), func=get_bus_list)
|
||||
|
||||
|
||||
def get_image_formats(self):
|
||||
"""Get available image formats"""
|
||||
return [ 'raw', 'qcow', 'qcow2' ]
|
||||
|
||||
def get_file_extensions(self):
|
||||
"""Get available image filename extensions"""
|
||||
return [ 'img', 'qcow', 'qcow2' ]
|
||||
|
||||
def get_video(self):
|
||||
""" Get available graphics video types """
|
||||
|
||||
def get_video_list(ctx):
|
||||
result = []
|
||||
for video_enum in ctx.xpath('/domainCapabilities/devices/video/enum'):
|
||||
if video_enum.xpath("@name")[0] == "modelType":
|
||||
for values in video_enum: result.append(values.text)
|
||||
return result
|
||||
return util.get_xml_path(self.get_dom_cap_xml(),func=get_video_list)
|
||||
|
||||
def get_iface(self, name):
|
||||
return self.wvm.interfaceLookupByName(name)
|
||||
return self.conn.interfaceLookupByName(name)
|
||||
|
||||
def get_secrets(self):
|
||||
return self.wvm.listSecrets()
|
||||
return self.conn.listSecrets()
|
||||
|
||||
def get_secret(self, uuid):
|
||||
return self.wvm.secretLookupByUUIDString(uuid)
|
||||
return self.conn.secretLookupByUUIDString(uuid)
|
||||
|
||||
def get_storage(self, name):
|
||||
return self.wvm.storagePoolLookupByName(name)
|
||||
return self.conn.storagePoolLookupByName(name)
|
||||
|
||||
def get_volume_by_path(self, path):
|
||||
return self.wvm.storageVolLookupByPath(path)
|
||||
return self.conn.storageVolLookupByPath(path)
|
||||
|
||||
def get_network(self, net):
|
||||
return self.wvm.networkLookupByName(net)
|
||||
return self.conn.networkLookupByName(net)
|
||||
|
||||
def get_instance(self, name):
|
||||
return self.wvm.lookupByName(name)
|
||||
return self.conn.lookupByName(name)
|
||||
|
||||
def get_instance_status(self, name):
|
||||
dom = self.conn.lookupByName(name)
|
||||
return dom.info()[0]
|
||||
|
||||
def get_instances(self):
|
||||
instances = []
|
||||
for inst_id in self.wvm.listDomainsID():
|
||||
dom = self.wvm.lookupByID(int(inst_id))
|
||||
for inst_id in self.conn.listDomainsID():
|
||||
dom = self.conn.lookupByID(int(inst_id))
|
||||
instances.append(dom.name())
|
||||
for name in self.wvm.listDefinedDomains():
|
||||
for name in self.conn.listDefinedDomains():
|
||||
instances.append(name)
|
||||
return instances
|
||||
|
||||
def get_snapshots(self):
|
||||
instance = []
|
||||
for snap_id in self.wvm.listDomainsID():
|
||||
dom = self.wvm.lookupByID(int(snap_id))
|
||||
for snap_id in self.conn.listDomainsID():
|
||||
dom = self.conn.lookupByID(int(snap_id))
|
||||
if dom.snapshotNum(0) != 0:
|
||||
instance.append(dom.name())
|
||||
for name in self.wvm.listDefinedDomains():
|
||||
dom = self.wvm.lookupByName(name)
|
||||
for name in self.conn.listDefinedDomains():
|
||||
dom = self.conn.lookupByName(name)
|
||||
if dom.snapshotNum(0) != 0:
|
||||
instance.append(dom.name())
|
||||
return instance
|
||||
|
||||
def get_net_device(self):
|
||||
netdevice = []
|
||||
|
||||
def get_info(doc):
|
||||
dev_type = util.get_xpath(doc, '/device/capability/@type')
|
||||
interface = util.get_xpath(doc, '/device/capability/interface')
|
||||
return dev_type, interface
|
||||
|
||||
for dev in self.wvm.listAllDevices(0):
|
||||
for dev in self.conn.listAllDevices(0):
|
||||
xml = dev.XMLDesc(0)
|
||||
(dev_type, interface) = util.get_xml_path(xml, func=get_info)
|
||||
if dev_type == 'net':
|
||||
netdevice.append(interface)
|
||||
if util.get_xml_data(xml, 'capability', 'type') == 'net':
|
||||
netdevice.append(util.get_xml_data(xml, 'capability/interface'))
|
||||
return netdevice
|
||||
|
||||
def get_host_instances(self, raw_mem_size=False):
|
||||
def get_host_instances(self):
|
||||
vname = {}
|
||||
def get_info(doc):
|
||||
mem = util.get_xpath(doc, "/domain/currentMemory")
|
||||
mem = int(mem) / 1024
|
||||
if raw_mem_size:
|
||||
mem = int(mem) * (1024*1024)
|
||||
cur_vcpu = util.get_xpath(doc, "/domain/vcpu/@current")
|
||||
if cur_vcpu:
|
||||
vcpu = cur_vcpu
|
||||
else:
|
||||
vcpu = util.get_xpath(doc, "/domain/vcpu")
|
||||
title = util.get_xpath(doc, "/domain/title")
|
||||
title = title if title else ''
|
||||
description = util.get_xpath(doc, "/domain/description")
|
||||
description = description if description else ''
|
||||
return (mem, vcpu, title, description)
|
||||
for name in self.get_instances():
|
||||
dom = self.get_instance(name)
|
||||
xml = dom.XMLDesc(0)
|
||||
(mem, vcpu, title, description) = util.get_xml_path(xml, func=get_info)
|
||||
vname[dom.name()] = {
|
||||
'status': dom.info()[0],
|
||||
'uuid': dom.UUIDString(),
|
||||
'vcpu': vcpu,
|
||||
'memory': mem,
|
||||
'title': title,
|
||||
'description': description,
|
||||
}
|
||||
return vname
|
||||
|
||||
def get_user_instances(self, name):
|
||||
dom = self.get_instance(name)
|
||||
xml = dom.XMLDesc(0)
|
||||
def get_info(ctx):
|
||||
mem = util.get_xpath(ctx, "/domain/currentMemory")
|
||||
mem = int(mem) / 1024
|
||||
cur_vcpu = util.get_xpath(ctx, "/domain/vcpu/@current")
|
||||
mem = util.get_xml_data(dom.XMLDesc(0), 'currentMemory')
|
||||
mem = round(int(mem) / 1024)
|
||||
cur_vcpu = util.get_xml_data(dom.XMLDesc(0), 'vcpu', 'current')
|
||||
if cur_vcpu:
|
||||
vcpu = cur_vcpu
|
||||
else:
|
||||
vcpu = util.get_xpath(ctx, "/domain/vcpu")
|
||||
title = util.get_xpath(ctx, "/domain/title")
|
||||
title = title if title else ''
|
||||
description = util.get_xpath(ctx, "/domain/description")
|
||||
description = description if description else ''
|
||||
return (mem, vcpu, title, description)
|
||||
(mem, vcpu, title, description) = util.get_xml_path(xml, func=get_info)
|
||||
return {
|
||||
'name': dom.name(),
|
||||
'status': dom.info()[0],
|
||||
'uuid': dom.UUIDString(),
|
||||
'vcpu': vcpu,
|
||||
'memory': mem,
|
||||
'title': title,
|
||||
'description': description,
|
||||
}
|
||||
vcpu = util.get_xml_data(dom.XMLDesc(0), 'vcpu')
|
||||
vname[dom.name()] = {'status': dom.info()[0], 'uuid': dom.UUIDString(), 'vcpu': vcpu, 'memory': mem}
|
||||
return vname
|
||||
|
||||
def close(self):
|
||||
"""Close connection"""
|
||||
# to-do: do not close connection ;)
|
||||
# self.wvm.close()
|
||||
pass
|
||||
if not self.keepalive:
|
||||
self.conn.close()
|
||||
81
backend/libvmgr/host.py
Normal file
81
backend/libvmgr/host.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import time
|
||||
from libvmgr import util
|
||||
from libvmgr.connect import wvcConnect
|
||||
|
||||
|
||||
class wvcHost(wvcConnect):
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
|
||||
def get_node_info(self):
|
||||
"""
|
||||
Function return host server information: hostname, cpu, memory, ...
|
||||
"""
|
||||
info = list()
|
||||
info.append(self.conn.getHostname())
|
||||
info.append(self.conn.getInfo()[0])
|
||||
info.append(self.conn.getInfo()[1] * (1024**2))
|
||||
info.append(self.conn.getInfo()[2])
|
||||
info.append(util.get_xml_data(self.conn.getSysinfo(0), 'processor/entry[6]'))
|
||||
info.append(self.conn.getURI())
|
||||
return info
|
||||
|
||||
def hypervisor_type(self):
|
||||
"""
|
||||
Return hypervisor type
|
||||
"""
|
||||
return util.get_xml_data(self.get_cap_xml(), 'guest/arch/domain', 'type')
|
||||
|
||||
def get_memory_usage(self):
|
||||
"""
|
||||
Function return memory usage on node.
|
||||
"""
|
||||
host_mem = self.conn.getInfo()[1] * (1024**2)
|
||||
free_mem = self.conn.getMemoryStats(-1, 0)
|
||||
if isinstance(free_mem, dict):
|
||||
mem = list(free_mem.values())
|
||||
free = (mem[1] + mem[2] + mem[3]) * 1024
|
||||
percent = (100 - ((free * 100) / host_mem))
|
||||
usage = (host_mem - free)
|
||||
mem_usage = {'size': host_mem, 'usage': usage, 'percent': round(percent)}
|
||||
else:
|
||||
mem_usage = {'size': 0, 'usage': 0, 'percent': 0}
|
||||
return mem_usage
|
||||
|
||||
def get_storage_usage(self, name):
|
||||
"""
|
||||
Function return storage usage on node by name.
|
||||
"""
|
||||
pool = self.get_storage(name)
|
||||
pool.refresh()
|
||||
if pool.isActive():
|
||||
size = pool.info()[1]
|
||||
free = pool.info()[3]
|
||||
used = size - free
|
||||
percent = (used * 100) / size
|
||||
return {'size': size, 'used': used, 'percent': percent}
|
||||
else:
|
||||
return {'size': 0, 'used': 0, 'percent': 0}
|
||||
|
||||
def get_cpu_usage(self):
|
||||
"""
|
||||
Function return cpu usage on node.
|
||||
"""
|
||||
prev_idle = 0
|
||||
prev_total = 0
|
||||
diff_usage = 0
|
||||
cpu = self.conn.getCPUStats(-1, 0)
|
||||
if isinstance(cpu, dict):
|
||||
for num in range(2):
|
||||
idle = self.conn.getCPUStats(-1, 0)['idle']
|
||||
total = sum(self.conn.getCPUStats(-1, 0).values())
|
||||
diff_idle = idle - prev_idle
|
||||
diff_total = total - prev_total
|
||||
diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10
|
||||
prev_total = total
|
||||
prev_idle = idle
|
||||
if num == 0:
|
||||
time.sleep(1)
|
||||
if diff_usage < 0:
|
||||
diff_usage = 0
|
||||
return {'usage': round(diff_usage)}
|
||||
|
|
@ -1,15 +1,20 @@
|
|||
import re
|
||||
import socket
|
||||
import random
|
||||
import lxml.etree as etree
|
||||
import libvirt
|
||||
import string
|
||||
import paramiko
|
||||
from time import sleep
|
||||
from xml.etree import ElementTree
|
||||
from string import ascii_letters, digits
|
||||
from passlib.hash import sha512_crypt
|
||||
|
||||
|
||||
def is_kvm_available(xml):
|
||||
kvm_domains = get_xml_path(xml, "//domain/@type='kvm'")
|
||||
if kvm_domains > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
tree = ElementTree.fromstring(xml)
|
||||
for dom in tree.findall('guest/arch/domain'):
|
||||
if 'kvm' in dom.get('type'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def randomMAC():
|
||||
|
|
@ -30,11 +35,6 @@ def randomUUID():
|
|||
return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2, "%02x" * 6]) % tuple(u)
|
||||
|
||||
|
||||
def randomPasswd(length=12, alphabet=string.letters + string.digits):
|
||||
"""Generate a random password"""
|
||||
return ''.join([random.choice(alphabet) for i in xrange(length)])
|
||||
|
||||
|
||||
def get_max_vcpus(conn, type=None):
|
||||
"""@param conn: libvirt connection to poll for max possible vcpus
|
||||
@type type: optional guest type (kvm, etc.)"""
|
||||
|
|
@ -71,7 +71,7 @@ def compareMAC(p, q):
|
|||
else:
|
||||
return -1
|
||||
|
||||
for i in xrange(len(pa)):
|
||||
for i in range(len(pa)):
|
||||
n = int(pa[i], 0x10) - int(qa[i], 0x10)
|
||||
if n > 0:
|
||||
return 1
|
||||
|
|
@ -80,40 +80,28 @@ def compareMAC(p, q):
|
|||
return 0
|
||||
|
||||
|
||||
def get_xml_path(xml, path=None, func=None):
|
||||
"""
|
||||
Return the content from the passed xml xpath, or return the result
|
||||
of a passed function (receives xpathContext as its only arg)
|
||||
"""
|
||||
doc = None
|
||||
result = None
|
||||
def get_xml_data(xml, path=None, element=None):
|
||||
res = ''
|
||||
if not path and not element:
|
||||
return ''
|
||||
|
||||
doc = etree.fromstring(xml)
|
||||
tree = ElementTree.fromstring(xml)
|
||||
if path:
|
||||
result = get_xpath(doc, path)
|
||||
elif func:
|
||||
result = func(doc)
|
||||
|
||||
child = tree.find(path)
|
||||
if child is not None:
|
||||
if element:
|
||||
res = child.get(element)
|
||||
else:
|
||||
res = child.text
|
||||
else:
|
||||
raise ValueError("'path' or 'func' is required.")
|
||||
return result
|
||||
res = tree.get(element)
|
||||
return res
|
||||
|
||||
|
||||
def get_xpath(doc, path):
|
||||
result = None
|
||||
def get_xml_findall(xml, string):
|
||||
tree = ElementTree.fromstring(xml)
|
||||
return tree.findall(string)
|
||||
|
||||
ret = doc.xpath(path)
|
||||
if ret is not None:
|
||||
if type(ret) == list:
|
||||
if len(ret) >= 1:
|
||||
if hasattr(ret[0],'text'):
|
||||
result = ret[0].text
|
||||
else:
|
||||
result = ret[0]
|
||||
else:
|
||||
result = ret
|
||||
|
||||
return result
|
||||
|
||||
def pretty_mem(val):
|
||||
val = int(val)
|
||||
|
|
@ -129,3 +117,43 @@ def pretty_bytes(val):
|
|||
return "%2.2f GB" % (val / (1024.0 * 1024.0 * 1024.0))
|
||||
else:
|
||||
return "%2.2f MB" % (val / (1024.0 * 1024.0))
|
||||
|
||||
|
||||
def gen_password(length=14):
|
||||
password = ''.join(
|
||||
[random.choice(ascii_letters + digits) for dummy in range(length)]
|
||||
)
|
||||
return password
|
||||
|
||||
|
||||
def password_to_hash(password):
|
||||
salt = gen_password(8)
|
||||
password_hash = sha512_crypt.encrypt(password, salt=salt, rounds=5000)
|
||||
return password_hash
|
||||
|
||||
|
||||
def similar_name(pattern, names):
|
||||
res = []
|
||||
for name in names:
|
||||
match = re.match(pattern, name)
|
||||
if match:
|
||||
res.append(name)
|
||||
return res
|
||||
|
||||
|
||||
def check_ssh_connection(hostname, password, username='root', timeout=90):
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
for i in range(timeout):
|
||||
try:
|
||||
ssh.connect(hostname, username=username, password=password)
|
||||
ssh.close()
|
||||
return True
|
||||
except (paramiko.BadHostKeyException,
|
||||
paramiko.AuthenticationException,
|
||||
paramiko.SSHException,
|
||||
socket.error,
|
||||
EOFError):
|
||||
sleep(1)
|
||||
return False
|
||||
15
backend/manage.py
Executable file
15
backend/manage.py
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webvirtcloud.settings.prod')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
5
backend/network/apps.py
Normal file
5
backend/network/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NetworkConfig(AppConfig):
|
||||
name = 'network'
|
||||
3
backend/network/views.py
Normal file
3
backend/network/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
5
backend/region/apps.py
Normal file
5
backend/region/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RegionConfig(AppConfig):
|
||||
name = 'region'
|
||||
33
backend/region/migrations/0001_initial.py
Normal file
33
backend/region/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 2.0.8 on 2018-09-22 07:20
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Region',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(max_length=200, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.", 'invalid')])),
|
||||
('name', models.CharField(blank=True, max_length=255)),
|
||||
('is_active', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Region',
|
||||
'verbose_name_plural': 'Regions',
|
||||
'ordering': ['-id'],
|
||||
},
|
||||
),
|
||||
]
|
||||
40
backend/region/models.py
Normal file
40
backend/region/models.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from django.db import models
|
||||
from django.core.validators import validate_slug
|
||||
|
||||
|
||||
class Region(models.Model):
|
||||
|
||||
slug = models.SlugField(
|
||||
max_length=200,
|
||||
validators=[validate_slug],
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
is_active = models.BooleanField(
|
||||
default=False,
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
)
|
||||
|
||||
is_deleted = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
|
||||
deleted_at = models.DateTimeField(
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-id']
|
||||
verbose_name = "Region"
|
||||
verbose_name_plural = "Regions"
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
3
backend/region/views.py
Normal file
3
backend/region/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
6
backend/requirements-dev.txt
Normal file
6
backend/requirements-dev.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-r requirements.txt
|
||||
flake8
|
||||
bpython
|
||||
django-cors-headers
|
||||
django-rest-swagger
|
||||
django-debug-toolbar
|
||||
8
backend/requirements.txt
Normal file
8
backend/requirements.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Django==2.0.8
|
||||
djangorestframework==3.8.2
|
||||
libvirt-python==4.6.0
|
||||
passlib==1.7.1
|
||||
paramiko==2.4.1
|
||||
django-rest-auth==0.9.3
|
||||
django-allauth==0.37.1
|
||||
mysqlclient==1.3.13
|
||||
5
backend/support/apps.py
Normal file
5
backend/support/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SupportConfig(AppConfig):
|
||||
name = 'support'
|
||||
3
backend/support/views.py
Normal file
3
backend/support/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue