First.
This commit is contained in:
commit
ff55f10a95
16 changed files with 1391 additions and 0 deletions
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*,cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
#proxmaster custom ignores
|
||||||
|
|
||||||
|
log/
|
||||||
|
config.ini
|
||||||
|
*.json
|
||||||
|
|
18
LICENSE
Normal file
18
LICENSE
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
Copyright (c) 2015 deflax.net
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgement in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
|
13
README.md
Normal file
13
README.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#Proxmaster
|
||||||
|
Python RESTful API for managing a grid of vm slaves
|
||||||
|
|
||||||
|
##Installation instructions:
|
||||||
|
```
|
||||||
|
1. sudo pip3 install -r requirements.txt
|
||||||
|
2. create config.ini with the following format:
|
||||||
|
3. chmod +x start.sh
|
||||||
|
4. create nginx vhost via the provided template files:
|
||||||
|
- config.ini.dist
|
||||||
|
- nginx_example_vhost.txt
|
||||||
|
5. o/
|
||||||
|
```
|
62
clientsdb.py
Normal file
62
clientsdb.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# -*- coding: utf-8
|
||||||
|
#
|
||||||
|
# manage clientsdb.json
|
||||||
|
|
||||||
|
#import site packages
|
||||||
|
import json
|
||||||
|
|
||||||
|
#import local packages
|
||||||
|
import ioconfig
|
||||||
|
import utils
|
||||||
|
|
||||||
|
def addclient(vmid, vmname, clientid, clientname):
|
||||||
|
""" add new client to the clientsdb.json """
|
||||||
|
clientsdb = readclientsdb()
|
||||||
|
if str(clientid) in clientsdb:
|
||||||
|
ioconfig.logger.info('clients> client ' + clientid + ' already exists. merging.')
|
||||||
|
else:
|
||||||
|
ioconfig.logger.info('clients> client ' + clientid + ' does not exist. creating.')
|
||||||
|
vcard = { 'name':str(clientname) }
|
||||||
|
newclient = { str(clientid):vcard }
|
||||||
|
clientsdb.update(newclient)
|
||||||
|
|
||||||
|
ioconfig.logger.info('clients> vmid ' + vmid + ' will be owned by ' + clientid + ' (' + clientname + ')')
|
||||||
|
vmdata = { 'name':str(vmname), 'vmid':str(vmid), 'ownerid':str(clientid) }
|
||||||
|
clientsdb[str(clientid)][str(vmid)] = vmdata
|
||||||
|
writeclientsdb(clientsdb)
|
||||||
|
|
||||||
|
|
||||||
|
def vmowner(vmid, vmname, verbose):
|
||||||
|
""" find the owner of the vm """
|
||||||
|
clientsdb = readclientsdb()
|
||||||
|
try:
|
||||||
|
clientid = utils.get_rec(clientsdb, str(vmid))[0]['ownerid']
|
||||||
|
clientname = clientsdb[str(clientid)]['name']
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
clientid = '0' #unknown owner
|
||||||
|
clientname = 'unknown'
|
||||||
|
if verbose:
|
||||||
|
ioconfig.logger.info('clients> the owner of ' + str(vmid) + ' (' + vmname + ') is ' + str(clientid) + ' (' + clientname + ')')
|
||||||
|
return clientid
|
||||||
|
|
||||||
|
|
||||||
|
def readclientsdb():
|
||||||
|
""" read client db """
|
||||||
|
try:
|
||||||
|
with open('clients.json') as dbr:
|
||||||
|
clientsdb = json.load(dbr)
|
||||||
|
dbr.close()
|
||||||
|
except:
|
||||||
|
clientsdb = {}
|
||||||
|
ioconfig.logger.warning('clients> initializing...')
|
||||||
|
#writeclientsdb(clientsdb)
|
||||||
|
return clientsdb
|
||||||
|
|
||||||
|
|
||||||
|
def writeclientsdb(clientsdb):
|
||||||
|
""" write db """
|
||||||
|
with open('clients.json', 'w') as dbw:
|
||||||
|
json.dump(clientsdb, dbw)
|
||||||
|
dbw.close()
|
||||||
|
|
28
config.ini.dist
Normal file
28
config.ini.dist
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[uwsgi]
|
||||||
|
socket = 127.0.0.1:5117
|
||||||
|
workers = 3
|
||||||
|
|
||||||
|
[general]
|
||||||
|
logfile = log/proxmaster.log
|
||||||
|
adminuser = masteradmin@pve
|
||||||
|
apipass = CHANGEME
|
||||||
|
vmid_min = 1000
|
||||||
|
vmid_max = 999999
|
||||||
|
novnc_url = http://FQDN/noVNC
|
||||||
|
|
||||||
|
[region_0]
|
||||||
|
name = CHANGEME
|
||||||
|
ipv4_min = 192.168.0.4
|
||||||
|
ipv4_max = 192.168.0.254
|
||||||
|
|
||||||
|
[slave_0]
|
||||||
|
name = CHANGEME
|
||||||
|
masterip = 192.168.0.2
|
||||||
|
password = CHANGEME
|
||||||
|
regionid = 0
|
||||||
|
|
||||||
|
[slave_1]
|
||||||
|
name = CHANGEME
|
||||||
|
masterip = 192.168.0.3
|
||||||
|
password = CHANGEME
|
||||||
|
regionid = 0
|
396
grid.py
Normal file
396
grid.py
Normal file
|
@ -0,0 +1,396 @@
|
||||||
|
#. -*- coding: utf-8
|
||||||
|
#
|
||||||
|
# vdc
|
||||||
|
|
||||||
|
#import site packages
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
import netaddr
|
||||||
|
|
||||||
|
#import local packages
|
||||||
|
import utils
|
||||||
|
import plugin
|
||||||
|
import ioconfig
|
||||||
|
import clientsdb
|
||||||
|
import journaldb
|
||||||
|
|
||||||
|
logger = ioconfig.logger
|
||||||
|
config = ioconfig.parser
|
||||||
|
|
||||||
|
|
||||||
|
def sync(cached=True):
|
||||||
|
""" calls slave objects and mix their nodes in a common cluster grid """
|
||||||
|
a = datetime.datetime.now()
|
||||||
|
grid_vmid_min = config.get('general', 'vmid_min')
|
||||||
|
grid_vmid_max = config.get('general', 'vmid_max')
|
||||||
|
real_grid = {'name':'real', "vmid_min":grid_vmid_min, "vmid_max":grid_vmid_max }
|
||||||
|
cache_grid = {'name':'cache', "vmid_min":grid_vmid_min, "vmid_max":grid_vmid_max }
|
||||||
|
regionselector = [i for i, x in enumerate(config.sections()) if re.match(r'\W*' + 'region' + r'\W*', x)]
|
||||||
|
for ri in range(len(regionselector)):
|
||||||
|
region_section = config.sections()[int(regionselector[ri])]
|
||||||
|
region_id = region_section.split("_")[1]
|
||||||
|
region_name = config.get(region_section, 'name')
|
||||||
|
region_range_min = config.get(region_section, 'ipv4_min')
|
||||||
|
region_range_max = config.get(region_section, 'ipv4_max')
|
||||||
|
slaveselector = [i for i, x in enumerate(config.sections()) if re.match(r'\W*' + 'slave' + r'\W*', x)]
|
||||||
|
real_region = { "id":region_id, "region":region_name, "ipv4_min":region_range_min, "ipv4_max":region_range_max }
|
||||||
|
cache_region = real_region.copy() #same region both in cache nad status
|
||||||
|
|
||||||
|
for si in range(len(slaveselector)):
|
||||||
|
slave_section = config.sections()[int(slaveselector[si])]
|
||||||
|
slave_id = slave_section.split("_")[1]
|
||||||
|
slave_name = config.get(slave_section, 'name')
|
||||||
|
slave_masterip = config.get(slave_section, 'masterip')
|
||||||
|
slave_password = config.get(slave_section, 'password')
|
||||||
|
slave_regionid = config.get(slave_section, 'regionid')
|
||||||
|
enc_slave_password = base64.b64encode(slave_password.encode('ascii')) #encode base64 to avoid shoulder surfers
|
||||||
|
decoded_password = enc_slave_password.decode('utf-8')
|
||||||
|
real_slave = { "id":slave_id, "slave":slave_name, "masterip":slave_masterip, "password":decoded_password }
|
||||||
|
optional_slave = {}
|
||||||
|
cache_file = 'cache-slave-' + slave_id + '.json'
|
||||||
|
prefix = 'cache> [' + slave_id + '] '
|
||||||
|
|
||||||
|
# check if slave is in current region and include it in current dict if it is
|
||||||
|
if slave_regionid == region_id:
|
||||||
|
try:
|
||||||
|
#trying to connect to slave host
|
||||||
|
#vmlist = plugin.vmlist(slave_id, slave_masterip, enc_slave_password.decode('utf-8'))
|
||||||
|
proxobject = plugin.auth(slave_id, slave_masterip, enc_slave_password)
|
||||||
|
vmlist = plugin.vmlist(proxobject)
|
||||||
|
real_slave['alive'] = 'up'
|
||||||
|
logger.info(prefix + 'is up')
|
||||||
|
except:
|
||||||
|
#raise
|
||||||
|
#slave cant be read so it will be marked down.
|
||||||
|
real_slave['alive'] = 'down'
|
||||||
|
logger.warning(prefix + 'is down')
|
||||||
|
|
||||||
|
if real_slave['alive'] == 'up':
|
||||||
|
#populate grid with vms then
|
||||||
|
for vm in vmlist:
|
||||||
|
#static parameters that CAN go to to cache:
|
||||||
|
vm_id = vm['vmid']
|
||||||
|
vm_name = vm['name']
|
||||||
|
vm_owner = clientsdb.vmowner(vm_id, vm_name, cached) #read clientsdb cache
|
||||||
|
static_vm = { "vmid":str(vm_id), "hostname":vm_name, 'type':vm['vmtype'], 'owner':vm_owner }
|
||||||
|
real_slave[str(vm_id)] = static_vm
|
||||||
|
#dynamic parameters that SHOULD NOT go to the cache:
|
||||||
|
dynamic_vm = { "uptime":vm['uptime'] }
|
||||||
|
optional_slave[str(vm_id)] = dynamic_vm
|
||||||
|
|
||||||
|
#check current cache
|
||||||
|
cache_slave = real_slave.copy() #fallback to the current state
|
||||||
|
try:
|
||||||
|
with open(cache_file) as fcr:
|
||||||
|
cache_slave = json.load(fcr)
|
||||||
|
fcr.close()
|
||||||
|
except:
|
||||||
|
logger.info(prefix + 'does not exist in cache. Initializing...')
|
||||||
|
|
||||||
|
if cache_slave['alive'] == 'up':
|
||||||
|
#slave was not down so it must be up...
|
||||||
|
cache_slave = update_cache(real_slave, cache_file, prefix, 'up')
|
||||||
|
logger.info(prefix + 'sync success o/')
|
||||||
|
else:
|
||||||
|
#if the slave was down before, compare the state before overwriting the cache
|
||||||
|
cache_slave['alive'] = 'up' #even if alive status in cache is still down we ignore it by forcing it to up
|
||||||
|
logger.info(prefix + 'was down')
|
||||||
|
#show the differences in log for manual (or maybe automatic at some point fixing)
|
||||||
|
findDiff(cache_slave, real_slave)
|
||||||
|
if cache_slave != real_slave:
|
||||||
|
logger.warning(prefix + 'cache != current status. please restore host!')
|
||||||
|
cache_slave = update_cache(cache_slave, cache_file, prefix, 'down')
|
||||||
|
else:
|
||||||
|
logger.info(prefix + 'cache == current status. host restored. o/')
|
||||||
|
cache_slave = update_cache(cache_slave, cache_file, prefix, 'up')
|
||||||
|
|
||||||
|
#what to do with cache if host is down
|
||||||
|
if real_slave['alive'] == 'down':
|
||||||
|
try:
|
||||||
|
logger.warning(prefix + 'loading cache...')
|
||||||
|
with open(cache_file) as fscr:
|
||||||
|
cache_slave = json.load(fscr)
|
||||||
|
fscr.close()
|
||||||
|
logger.warning(prefix + '...done')
|
||||||
|
cache_slave = update_cache(cache_slave, cache_file, prefix, 'down')
|
||||||
|
except:
|
||||||
|
logger.error(prefix + 'sync failure!')
|
||||||
|
cache_slave = real_slave.copy()
|
||||||
|
#raise
|
||||||
|
|
||||||
|
#we safely mix the dynamic ids now that we dont deal with cache anymore
|
||||||
|
mergedslave = utils.dict_merge({}, real_slave, optional_slave)
|
||||||
|
real_region[slave_id] = mergedslave
|
||||||
|
cache_region[slave_id] = cache_slave
|
||||||
|
#the region is finally included in the grid
|
||||||
|
real_grid[region_id] = real_region
|
||||||
|
cache_grid[region_id] = cache_region
|
||||||
|
|
||||||
|
b = datetime.datetime.now()
|
||||||
|
real_grid["synctime"] = str(b-a)
|
||||||
|
#dump all data to json
|
||||||
|
WriteCache(cache_grid, 'grid-cache.json')
|
||||||
|
WriteCache(real_grid, 'grid-real.json')
|
||||||
|
if cached == True:
|
||||||
|
return cache_grid
|
||||||
|
else:
|
||||||
|
return real_grid
|
||||||
|
|
||||||
|
|
||||||
|
def update_cache(cachedata, cachefile, prefix, newstatus):
|
||||||
|
""" update administravite status """
|
||||||
|
cachedata['alive'] = newstatus
|
||||||
|
WriteCache(cachedata, cachefile)
|
||||||
|
#TODO send mail
|
||||||
|
logger.info(prefix + 'administratively ' + newstatus)
|
||||||
|
return cachedata
|
||||||
|
|
||||||
|
|
||||||
|
def WriteCache(src_data, cache_file):
|
||||||
|
with open(cache_file, 'w') as fcw:
|
||||||
|
json.dump(src_data, fcw)
|
||||||
|
fcw.close()
|
||||||
|
|
||||||
|
|
||||||
|
def query_region(region_name):
|
||||||
|
""" translate region name to region id """
|
||||||
|
grid_data = readcache()
|
||||||
|
|
||||||
|
all_regions = []
|
||||||
|
for element in grid_data:
|
||||||
|
try:
|
||||||
|
if str(element) == grid_data[element]['id']:
|
||||||
|
all_regions.append(element)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for region in all_regions:
|
||||||
|
if grid_data[region]['region'] == region_name:
|
||||||
|
logger.info('grid> region ' + region_name + ' found')
|
||||||
|
return grid_data[region]['id']
|
||||||
|
break
|
||||||
|
logger.error('grid> cant find region ' + region_name)
|
||||||
|
return "-1"
|
||||||
|
|
||||||
|
def query_happiness(region_id):
|
||||||
|
""" analyzes grid data for the reuqested region and returns proposed slave_id,
|
||||||
|
based on a "happiness" factor. happiness means alive and free :) """
|
||||||
|
grid_data = readcache()
|
||||||
|
grid_data = grid_data[str(region_id)]
|
||||||
|
|
||||||
|
all_slaves = []
|
||||||
|
for element in grid_data:
|
||||||
|
try:
|
||||||
|
if str(element) == grid_data[element]['id']:
|
||||||
|
all_slaves.append(element)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
all_slaves = [ int(x) for x in all_slaves ] #convert values from str to int
|
||||||
|
|
||||||
|
alive_slaves = []
|
||||||
|
for slaveid in all_slaves:
|
||||||
|
if str(grid_data[str(slaveid)]['alive']) == 'up':
|
||||||
|
alive_slaves.append(slaveid)
|
||||||
|
logger.info('grid> alive slaves ' + str(alive_slaves))
|
||||||
|
|
||||||
|
#happy_slave = random.choice(alive_slaves)
|
||||||
|
if len(alive_slaves) < 1:
|
||||||
|
logger.error('grid> grid is full. add more slaves')
|
||||||
|
else:
|
||||||
|
happy_slave = 1 #TODO: analyze slaves and make informed decision.
|
||||||
|
logger.info('grid> ' + str(happy_slave) + ' selected')
|
||||||
|
return happy_slave
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ipv4(region_id, how_many=1):
|
||||||
|
""" analyzes cached grid data and returns ip addresses for new machines. """
|
||||||
|
grid_data = readcache()
|
||||||
|
ip_range_min = grid_data[str(region_id)]['ipv4_min']
|
||||||
|
ip_range_max = grid_data[str(region_id)]['ipv4_max']
|
||||||
|
region_ipset = netaddr.IPSet(netaddr.IPRange(ip_range_min, ip_range_max))
|
||||||
|
region_ips = []
|
||||||
|
for ip in region_ipset:
|
||||||
|
region_ips.append(ip)
|
||||||
|
ip_min = 0
|
||||||
|
ip_max = len(region_ips) - 1
|
||||||
|
tested_ips = [] #initialize ip cache
|
||||||
|
requested_ips = []
|
||||||
|
all_ips = utils.get_rec(grid_data, 'ipaddr')
|
||||||
|
|
||||||
|
for ips in range(int(how_many)):
|
||||||
|
counter = 0
|
||||||
|
while True:
|
||||||
|
if counter == 50:
|
||||||
|
logger.error('grid> ip range full')
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
requested_ip_index = random.randint(ip_min, ip_max)
|
||||||
|
requested_ip = str(region_ips[requested_ip_index])
|
||||||
|
|
||||||
|
if requested_ip in tested_ips:
|
||||||
|
logger.warning('grid> ip address ' + str(requested_ip) + ' already tested. cache: ' + str(tested_ips))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if requested_ip in requested_ips:
|
||||||
|
logger.warning('grid> ip address ' + str(requested_ip) + ' already generated')
|
||||||
|
tested_ips.append(requested_ip)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if requested_ip in all_ips:
|
||||||
|
position = used_ips.index(requested_ip)
|
||||||
|
logger.warning('grid> ip address ' + str(requested_ip) + ' already exist. location:' + str(position))
|
||||||
|
tested_ips.append(requested_ip)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
tested_ips = [] #clear ip cache
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info('grid> ip address ' + requested_ip + ' selected')
|
||||||
|
requested_ips.append(requested_ip)
|
||||||
|
logger.info('grid> ip addresses ' + str(requested_ips) + ' selected')
|
||||||
|
return requested_ips
|
||||||
|
|
||||||
|
|
||||||
|
def generate_vmid():
|
||||||
|
""" analyzes cached grid data and return proposed vmid for new machines """
|
||||||
|
grid_data = readcache()
|
||||||
|
tested_vmids = [] #initialize id cache
|
||||||
|
id_min = grid_data['vmid_min']
|
||||||
|
id_max = grid_data['vmid_max']
|
||||||
|
all_vmid = utils.get_rec(grid_data, 'vmid') #get all vmid values from the nested grid
|
||||||
|
all_vmid = [ int(x) for x in all_vmid ] #convert values from str to int (its a vmid right?)
|
||||||
|
counter = 0
|
||||||
|
while True:
|
||||||
|
if counter == 50:
|
||||||
|
logger.error('grid> ip range full')
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
requested_vmid = random.randint(int(id_min), int(id_max)) #max 90k machines
|
||||||
|
|
||||||
|
if requested_vmid in tested_vmids:
|
||||||
|
logger.warning('grid> vmid ' + str(requested_vmid) + ' already tested. cache:' + str(tested_vmids))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if requested_vmid in all_vmid:
|
||||||
|
position = all_vmid.index(requested_vmid)
|
||||||
|
logger.warning('grid> vmid ' + str(requested_vmid) + ' already exist. location:' + str(position))
|
||||||
|
tested_vmids.append(requested_vmid)
|
||||||
|
else:
|
||||||
|
tested_vmids = [] #clear tested vmid cache
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info('grid> vmid ' + str(requested_vmid) + ' selected')
|
||||||
|
return requested_vmid
|
||||||
|
|
||||||
|
|
||||||
|
def query_slave_data(slave_id):
|
||||||
|
""" read the cache for the requested slave_id """
|
||||||
|
grid_data = readcache()
|
||||||
|
all_regions = []
|
||||||
|
for element in grid_data:
|
||||||
|
try:
|
||||||
|
if str(element) == grid_data[element]['id']:
|
||||||
|
all_regions.append(element)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
for region in all_regions:
|
||||||
|
cslave = grid_data[region]
|
||||||
|
result_slave = cslave[str(slave_id)] #select the slave from all regions
|
||||||
|
if result_slave != None:
|
||||||
|
return result_slave
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def query_vm(req_vmid):
|
||||||
|
""" returns slave_id and vm_type for the requested vmid """
|
||||||
|
#read current state (no cache)
|
||||||
|
sync(False)
|
||||||
|
grid_data = readreal()
|
||||||
|
|
||||||
|
#compare requested vmid to all vmid's from the grid
|
||||||
|
#TODO: maybe we should also check the owner somehow
|
||||||
|
all_vmid = utils.get_rec(grid_data, 'vmid')
|
||||||
|
target = int(0)
|
||||||
|
for running_vmid in all_vmid:
|
||||||
|
if str(req_vmid) == str(running_vmid):
|
||||||
|
target = req_vmid #=runn?
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if target == 0:
|
||||||
|
logger.error('grid> vmid {} cannot be found.' + str(req_vmid))
|
||||||
|
return "-1"
|
||||||
|
|
||||||
|
region_id, slave_id = journaldb.getjnode(target)
|
||||||
|
try:
|
||||||
|
vm_type = grid_data[str(region_id)][str(slave_id)][str(target)]['type']
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
|
#we should know them by now.
|
||||||
|
return slave_id, vm_type
|
||||||
|
|
||||||
|
|
||||||
|
def findDiff(d1, d2, path=""):
|
||||||
|
for k in d1.keys():
|
||||||
|
if not k in d2.keys():
|
||||||
|
logger.warning('cache> ' + str(k) + ' as key not in d2')
|
||||||
|
else:
|
||||||
|
if type(d1[k]) is dict:
|
||||||
|
if path == "":
|
||||||
|
path = k
|
||||||
|
else:
|
||||||
|
path = path + "->" + k
|
||||||
|
findDiff(d1[k],d2[k], path)
|
||||||
|
else:
|
||||||
|
if d1[k] != d2[k]:
|
||||||
|
logger.warning('cache> ' + str(k) + ' ' + str(d1[k]) + ' [-]')
|
||||||
|
logger.warning('cache> ' + str(k) + ' ' + str(d2[k]) + ' [+]')
|
||||||
|
|
||||||
|
def readcache():
|
||||||
|
""" read the last saved cache and return its contents """
|
||||||
|
try:
|
||||||
|
with open('grid-cache.json') as gridfile:
|
||||||
|
grid_data = json.load(gridfile)
|
||||||
|
gridfile.close()
|
||||||
|
except:
|
||||||
|
grid_data = {}
|
||||||
|
logger.error('cache> cannot read cache file')
|
||||||
|
return grid_data
|
||||||
|
|
||||||
|
|
||||||
|
def readreal():
|
||||||
|
""" read the current state and return its contents """
|
||||||
|
try:
|
||||||
|
with open('grid-real.json') as gridfile:
|
||||||
|
grid_data = json.load(gridfile)
|
||||||
|
gridfile.close()
|
||||||
|
resulttime = grid_data['synctime']
|
||||||
|
logger.info('grid> sync for ' + resulttime)
|
||||||
|
except:
|
||||||
|
grid_data = {}
|
||||||
|
logger.error('cache> cannot read temp file')
|
||||||
|
return grid_data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(sync())
|
||||||
|
#print(query_region('Plovdiv, Bulgaria'))
|
||||||
|
#print(query_happiness(0))
|
||||||
|
#print(generate_ipv4(0,3))
|
||||||
|
#print(generate_vmid())
|
||||||
|
#print(query_slave_data(0))
|
||||||
|
#print(query_vm(483039))
|
||||||
|
|
21
ioconfig.py
Normal file
21
ioconfig.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#. -*- coding: utf-8
|
||||||
|
#
|
||||||
|
# ioconfig.py - read/write config/log etc.
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import logging
|
||||||
|
|
||||||
|
""" config reader """
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
parser.read('config.ini')
|
||||||
|
|
||||||
|
""" log writer """
|
||||||
|
logger = logging.getLogger('proxmaster')
|
||||||
|
logging.captureWarnings(True)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
handler = logging.FileHandler(parser.get('general', 'logfile'))
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
#formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
59
journaldb.py
Normal file
59
journaldb.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- coding: utf-8
|
||||||
|
#
|
||||||
|
# manage journaldb.json which is a table of vmid's as indexes update on vmcreate and
|
||||||
|
# values of region_id and slave_id. should support deletion of unused id's and be
|
||||||
|
# properly updated on vm migrations
|
||||||
|
|
||||||
|
#site
|
||||||
|
import json
|
||||||
|
|
||||||
|
#local
|
||||||
|
import ioconfig
|
||||||
|
import utils
|
||||||
|
|
||||||
|
def createjnode(vmid, regionid, slaveid):
|
||||||
|
""" create new record into the journal. invoked on vm creation """
|
||||||
|
journaldb = readjournal()
|
||||||
|
if str(vmid) in journaldb:
|
||||||
|
ioconfig.logger.warning('journal> overwriting id[{}] !'.format(vmid))
|
||||||
|
else:
|
||||||
|
jnode = { str(vmid):{} }
|
||||||
|
journaldb.update(jnode)
|
||||||
|
|
||||||
|
ioconfig.logger.info ('journal> r[{}] s[{}] -> id[{}]'.format(regionid, slaveid, vmid))
|
||||||
|
jdata = { 'vmid':str(vmid), 'slaveid':str(slaveid), 'regionid':str(regionid) }
|
||||||
|
journaldb[str(vmid)] = jdata
|
||||||
|
writedb(journaldb)
|
||||||
|
|
||||||
|
|
||||||
|
def getjnode(vmid):
|
||||||
|
""" query the database for records with requested vmid. invoked on user commands """
|
||||||
|
journaldb = readjournal()
|
||||||
|
try:
|
||||||
|
regionid = journaldb[str(vmid)]['regionid']
|
||||||
|
slaveid = journaldb[str(vmid)]['slaveid']
|
||||||
|
ioconfig.logger.info('journal> read: id[{}] -> r[{}] s[{}]'.format(vmid, regionid, slaveid))
|
||||||
|
except:
|
||||||
|
ioconfig.logger.error('journal> invalid id[{}] !'.format(vmid))
|
||||||
|
else:
|
||||||
|
return regionid, slaveid
|
||||||
|
|
||||||
|
|
||||||
|
def readjournal():
|
||||||
|
""" read journal """
|
||||||
|
try:
|
||||||
|
with open('journal.json') as dbr:
|
||||||
|
journaldb = json.load(dbr)
|
||||||
|
dbr.close()
|
||||||
|
except:
|
||||||
|
journaldb = {}
|
||||||
|
ioconfig.logger.warning('journal> initializing...')
|
||||||
|
return journaldb
|
||||||
|
|
||||||
|
|
||||||
|
def writedb(journaldb):
|
||||||
|
""" write journal """
|
||||||
|
with open('journal.json', 'w') as dbw:
|
||||||
|
json.dump(journaldb, dbw)
|
||||||
|
dbw.close()
|
||||||
|
|
10
nginx_example_vhost.txt
Normal file
10
nginx_example_vhost.txt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name EXAMPLE.com;
|
||||||
|
location / {
|
||||||
|
uwsgi_pass 127.0.0.1:5117;
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_param UWSGI_SCRIPT proxmaster;
|
||||||
|
uwsgi_param UWSGI_PYHOME /home/USER/proxmaster;
|
||||||
|
}
|
||||||
|
}
|
44
novnc.py
Normal file
44
novnc.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#. -*- coding: utf-8
|
||||||
|
#
|
||||||
|
# novnc daemon spawner
|
||||||
|
|
||||||
|
#import site packages
|
||||||
|
import shlex, subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def spawn(target, options):
|
||||||
|
""" spawn """
|
||||||
|
vnctarget = '{}:{} {}:{} '.format(target['listen_host'], target['listen_port'], target['target_host'], target['target_port'])
|
||||||
|
|
||||||
|
a_options = ''
|
||||||
|
for key, value in options.items():
|
||||||
|
if value == True:
|
||||||
|
c_option = '--{} '.format(key)
|
||||||
|
else:
|
||||||
|
c_option = '--{} {} '.format(key, value)
|
||||||
|
a_options += c_option
|
||||||
|
|
||||||
|
command_line = 'python3 runwebsockify.py ' + a_options + vnctarget
|
||||||
|
args = shlex.split(command_line)
|
||||||
|
p = subprocess.Popen(args)
|
||||||
|
print('spawn!')
|
||||||
|
|
||||||
|
|
||||||
|
def spawn2(options):
|
||||||
|
""" spawn novnc daemon """
|
||||||
|
print('daemon spawned')
|
||||||
|
novncd = threading.Thread(name='novncd', target=start_websockify, args=(options,))
|
||||||
|
novncd.setDaemon(True) #daemonic ]:>
|
||||||
|
novncd.start()
|
||||||
|
print('stauts', novncd.isAlive())
|
||||||
|
|
||||||
|
|
||||||
|
def start_websockify(options):
|
||||||
|
""" spawn websockify process """
|
||||||
|
print(options)
|
||||||
|
server = websockify.WebSocketProxy(**options)
|
||||||
|
server.start_server()
|
||||||
|
print('daemon exited')
|
||||||
|
#while True:
|
||||||
|
# print('daemon')
|
||||||
|
|
301
plugin.py
Normal file
301
plugin.py
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
#. -*- coding: utf-8 -
|
||||||
|
# required proxmox permissions: PVESysAdmin, PVEVMAdmin
|
||||||
|
#
|
||||||
|
# afx 2015-2016
|
||||||
|
|
||||||
|
# site
|
||||||
|
from proxmoxer import ProxmoxAPI
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
|
||||||
|
#local
|
||||||
|
import grid
|
||||||
|
import clientsdb
|
||||||
|
import journaldb
|
||||||
|
import utils
|
||||||
|
import ioconfig
|
||||||
|
import novnc
|
||||||
|
|
||||||
|
def auth(slave_id, masterip=None, enc_password=None):
|
||||||
|
""" captures slave we want to auth from the cache and extract the credentials """
|
||||||
|
adminuser = ioconfig.parser.get('general', 'adminuser')
|
||||||
|
if masterip is None:
|
||||||
|
result_slave = grid.query_slave_data(slave_id)
|
||||||
|
masterip = result_slave['masterip']
|
||||||
|
enc_password = result_slave['password']
|
||||||
|
adminpassword = base64.b64decode(enc_password).decode('ascii')
|
||||||
|
|
||||||
|
#vendor specific
|
||||||
|
#connection = lib_proxmoxia.Connector(masterip)
|
||||||
|
#auth_token = connection.get_auth_token(adminuser, adminpassword)
|
||||||
|
#proxobject = lib_proxmoxia.Proxmox(connection)
|
||||||
|
proxobject = ProxmoxAPI(masterip, user=adminuser, password=adminpassword, verify_ssl=False)
|
||||||
|
|
||||||
|
return proxobject
|
||||||
|
|
||||||
|
|
||||||
|
def vmlist(proxobject):
|
||||||
|
""" get vmlist """
|
||||||
|
#we keep a single node proxmoxes so node id = 0
|
||||||
|
#slave_name = proxobject.get('cluster/status')#'name']
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
#query_kvm = proxobject.get('nodes/%s/qemu' % slave_name)
|
||||||
|
query_kvm = proxobject.nodes(slave_name).qemu.get()
|
||||||
|
query_lxc = proxobject.nodes(slave_name).lxc.get()
|
||||||
|
for kvm_dict in query_kvm:
|
||||||
|
kvm_dict['vmtype'] = 'kvm'
|
||||||
|
for lxc_dict in query_lxc:
|
||||||
|
lxc_dict['vmtype'] = 'lxc'
|
||||||
|
vmlist = query_kvm + query_lxc #merge machine list
|
||||||
|
return vmlist
|
||||||
|
|
||||||
|
|
||||||
|
def vmcreate(req):
|
||||||
|
""" create vm. returns JSON with data for whmcs """
|
||||||
|
grid.sync()
|
||||||
|
|
||||||
|
region_id = grid.query_region(req['region'])
|
||||||
|
if region_id == "-1":
|
||||||
|
logger.error('grid> no region found')
|
||||||
|
response = 'NO REGION FOUND'
|
||||||
|
return response
|
||||||
|
|
||||||
|
slave_id = str(grid.query_happiness(region_id))
|
||||||
|
vm_id = str(grid.generate_vmid())
|
||||||
|
vm_ipv4 = grid.generate_ipv4(region_id, req['vps_ipv4'])
|
||||||
|
ipv4_dict = {}
|
||||||
|
ipidx = 0
|
||||||
|
|
||||||
|
vm_name = req['hostname']
|
||||||
|
client_id = req['clientid']
|
||||||
|
client_name = req['clientname']
|
||||||
|
|
||||||
|
proxobject = auth(slave_id) #we dont know the ip of slave_id so we leave the auth function to find it itself.
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
|
||||||
|
#ioconfig.logger.info('grid[' + slave_name + ']> recieved data: %s, %s, %s, %s, %s', region_id, slave_id, vm_id, vm_ipv4, req)
|
||||||
|
for ip in vm_ipv4:
|
||||||
|
ipv4_dict[str(ipidx)] = str(ip)
|
||||||
|
ipidx += 1
|
||||||
|
response = { 'status':'CREATING', 'vmid':vm_id, 'name':vm_name, 'password':'TODO', 'ipv4_0':vm_ipv4[0] }
|
||||||
|
|
||||||
|
disk_filename = 'vm-' + vm_id + '-disk-1'
|
||||||
|
description = vm_name + ' (' + vm_id + ')\n'
|
||||||
|
description += 'owned by ' + client_name + ' (' + client_id + ')\n'
|
||||||
|
description += 'master ip: ' + vm_ipv4[0]
|
||||||
|
|
||||||
|
#create partition
|
||||||
|
image_name = 'vm-' + vm_id + '-disk-0'
|
||||||
|
local_storage = proxobject.nodes(slave_name).storage('lvm')
|
||||||
|
local_storage.content.post(vmid=vm_id,
|
||||||
|
filename=image_name,
|
||||||
|
size=req['vps_disk'] + 'G')
|
||||||
|
|
||||||
|
if req['vps_type'] == 'KVM':
|
||||||
|
create_result = proxobject.nodes(slave_name).qemu.post(vmid=vm_id,
|
||||||
|
name=vm_name,
|
||||||
|
sockets=1,
|
||||||
|
cores=req['vps_cpu'],
|
||||||
|
memory=req['vps_ram'],
|
||||||
|
virtio0='lvm:' + image_name,
|
||||||
|
ide1='skyblue:iso/' + req['vps_os'] + ',media=cdrom',
|
||||||
|
net0='e1000,bridge=pub',
|
||||||
|
onboot=1,
|
||||||
|
description=description)
|
||||||
|
if req['vps_type'] == 'LXC':
|
||||||
|
create_result = proxobject.nodes(slave_name).lxc.post(vmid=vm_id,
|
||||||
|
hostname=vm_name,
|
||||||
|
password=req['password'],
|
||||||
|
sockets=1,
|
||||||
|
cores=req['vps_cpu'],
|
||||||
|
memory=req['vps_ram'],
|
||||||
|
virtio0='lvm:' + image_name,
|
||||||
|
ip_address=vm_ipv4[0],
|
||||||
|
onboot=1,
|
||||||
|
description=description)
|
||||||
|
|
||||||
|
#populate the client db and vm journal
|
||||||
|
client_id = req['clientid']
|
||||||
|
client_name = req['clientname']
|
||||||
|
clientsdb.addclient(vm_id, vm_name, client_id, client_name)
|
||||||
|
journaldb.createjnode(vm_id, region_id, slave_id)
|
||||||
|
|
||||||
|
#start the machihe
|
||||||
|
time.sleep(7) #wait few seconds for the slave to prepare the machine for initial run
|
||||||
|
vmstart(vm_id)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def vmstatus(vm_id):
|
||||||
|
""" returns the status of the machine """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> get status of %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
result = proxobject.nodes(slave_name).qemu(vm_id).status.current.get()
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
result = proxobject.nodes(slave_name).lxc(vm_id).status.current.get()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def vmstart(vm_id):
|
||||||
|
""" starts a machine """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> starting %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
result = proxobject.nodes(slave_name).qemu(vm_id).status.start.post()
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
result = proxobject.nodes(slave_name).lxc(vm_id).status.start.post()
|
||||||
|
#ioconfig.logger.info('grid[{}]> {}'.format(slave_name, result))
|
||||||
|
response = { 'status':'STARTING' }
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def vmshutdown(vm_id):
|
||||||
|
""" acpi shutdown the machine.. """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> acpi shutdown %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
result = proxobject.nodes(slave_name).qemu(vm_id).status.stop.post()
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
result = proxobject.nodes(slave_name).lxc(vm_id).status.stop.post()
|
||||||
|
#ioconfig.logger.info('grid[{}]> {}'.format(slave_name, result))
|
||||||
|
response = { 'status':'SHUTDOWN', 'vmid':vm_id }
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def vmstop(vm_id):
|
||||||
|
""" poweroff the machine.. """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> power off %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
result = proxobject.nodes(slave_name).qemu(vm_id).status.stop.post()
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
result = proxobject.nodes(slave_name).lxc(vm_id).status.stop.post()
|
||||||
|
#ioconfig.logger.info('grid[{}]> {}'.format(slave_name, result))
|
||||||
|
response = { 'status':'STOPPING', 'vmid':vm_id }
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def vmshutdown(vm_id):
|
||||||
|
""" graceful stop """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> acpi shutdown sent to %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
result = proxobject.nodes(slave_name).qemu(vm_id).status.shutdown.post()
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
result = proxobject.nodes(slave_name).lxc(vm_id).status.shutdown.post()
|
||||||
|
#ioconfig.logger.info('grid[{}]> {}'.format(slave_name, result))
|
||||||
|
response = { 'status':'ACPI SHUTDOWN', 'vmid':vm_id }
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def vmsuspend(vm_id):
|
||||||
|
""" suspend machine """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> suspending %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
result = proxobject.nodes(slave_name).qemu(vm_id).status.suspend.post()
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
result = proxobject.nodes(slave_name).lxc(vm_id).status.suspend.post()
|
||||||
|
#ioconfig.logger.info('grid[{}]> {}'.format(slave_name, result))
|
||||||
|
response = { 'status':'SUSPEND', 'vmid':vm_id }
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def vmresume(vm_id):
|
||||||
|
""" resume machine """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> resuming %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
result = proxobject.nodes(slave_name).qemu(vm_id).status.resume.post()
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
result = proxobject.nodes(slave_name).lxc(vm_id).status.resume.post()
|
||||||
|
#ioconfig.logger.info('grid[{}]> {}'.format(slave_name, result))
|
||||||
|
response = { 'status':'RESUME', 'vmid':vm_id }
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def vmvnc(vm_id):
|
||||||
|
""" invoke vnc ticket """
|
||||||
|
slave_id, vm_type = grid.query_vm(vm_id)
|
||||||
|
proxobject = auth(slave_id)
|
||||||
|
vm_type = vm_type.lower()
|
||||||
|
slave_name = proxobject.cluster.status.get()[0]['name']
|
||||||
|
ioconfig.logger.info('grid[%s]> invoking vnc ticket for %s %s' % (slave_name, vm_type, vm_id))
|
||||||
|
|
||||||
|
if vm_type == 'kvm':
|
||||||
|
ticket = proxobject.nodes(slave_name).qemu(vm_id).vncproxy.post(websocket=1)
|
||||||
|
#socket = proxobject.nodes(slave_name).qemu(vm_id).vncwebsocket.get(port=ticket['port'],
|
||||||
|
# vncticket=ticket['ticket'])
|
||||||
|
if vm_type == 'lxc':
|
||||||
|
ticket = proxobject.nodes(slave_name).lxc(vm_id).vncproxy.post()
|
||||||
|
#socket = proxobject.nodes(slave_name).lxc(vm_id).vncwebsocket.get(port=ticket['port'],
|
||||||
|
# vncticket=ticket['ticket'])
|
||||||
|
|
||||||
|
slaveip = grid.query_slave_data(slave_id)['masterip']
|
||||||
|
#slaveport = socket['port']
|
||||||
|
slaveport = ticket['port']
|
||||||
|
listenport = str(int(slaveport) + 1000 + (int(slave_id) * 100)) #TODO: max 100 parallel connections/slave.
|
||||||
|
myip = getmyip()
|
||||||
|
|
||||||
|
vnc_target = { 'target_host': slaveip,
|
||||||
|
'target_port': slaveport,
|
||||||
|
'listen_host': myip,
|
||||||
|
'listen_port': listenport }
|
||||||
|
|
||||||
|
vnc_options = { 'idle-timeout': 20,
|
||||||
|
'verbose': True,
|
||||||
|
'run-once': True }
|
||||||
|
|
||||||
|
novnc.spawn(vnc_target, vnc_options)
|
||||||
|
|
||||||
|
external_url = ioconfig.parser.get('general', 'novnc_url')
|
||||||
|
prefix = external_url + "/?host=" + myip + "&port=" + listenport + "&encrypt=0&true_color=1&password="
|
||||||
|
ioconfig.logger.info('grid[{}]> {}'.format(slave_name, prefix + ticket['ticket']))
|
||||||
|
|
||||||
|
response = { 'status':'VNC', 'vmid':vm_id }
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def getmyip():
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(("gmail.com",80))
|
||||||
|
myip = s.getsockname()[0]
|
||||||
|
s.close
|
||||||
|
return myip
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
#internal module tests
|
||||||
|
time.sleep(30)
|
||||||
|
|
269
proxmaster.py
Normal file
269
proxmaster.py
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
#. -*- coding: utf-8 -
|
||||||
|
# required proxmox permissions: PVESysAdmin, PVEVMAdmin
|
||||||
|
#
|
||||||
|
# afx 2015-2016
|
||||||
|
|
||||||
|
# import site packages
|
||||||
|
import logging
|
||||||
|
import falcon
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
#import local packages
|
||||||
|
import ioconfig
|
||||||
|
import grid
|
||||||
|
import plugin
|
||||||
|
|
||||||
|
config = ioconfig.parser
|
||||||
|
logger = ioconfig.logger
|
||||||
|
|
||||||
|
def welcome():
|
||||||
|
"""displays motd in log as welcome message"""
|
||||||
|
logger.info('###################################')
|
||||||
|
logger.info('# proxmaster ][ (c) 2015-2016 afx #')
|
||||||
|
logger.info('###################################')
|
||||||
|
|
||||||
|
def apicheck(params):
|
||||||
|
""" compares request params for api key with the config file"""
|
||||||
|
try:
|
||||||
|
if params['apipass'] == config.get('general', 'apipass'):
|
||||||
|
status = True
|
||||||
|
response = 'OK'
|
||||||
|
else:
|
||||||
|
status = False
|
||||||
|
response = 'GET KEY DENIED'
|
||||||
|
logger.error('grid> read access denied. key mismatch')
|
||||||
|
except:
|
||||||
|
#raise
|
||||||
|
status = False
|
||||||
|
response = 'GET URL DENIED'
|
||||||
|
logger.error('grid> read access denied. url error?')
|
||||||
|
finally:
|
||||||
|
return (status, response)
|
||||||
|
|
||||||
|
#API methods
|
||||||
|
class ClusterResource(object):
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
"""TEST ONLY. List cluster nodes. TEST ONLY"""
|
||||||
|
logger.info('grid> cache status')
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
resp.body = str(grid.sync())
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
def on_post(self, req, resp):
|
||||||
|
"""Create a cluster node, returns array of: status, vmid, pass, ipv4, """
|
||||||
|
logger.info('grid> create ' + str(req.params))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmcreate(req.params))
|
||||||
|
except:
|
||||||
|
logger.error('grid> create function cancelled')
|
||||||
|
raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'CREATE ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
class StatusResource(object):
|
||||||
|
def on_get(self, req, resp, vmid):
|
||||||
|
""" check vm status """
|
||||||
|
logger.info('grid> status ' + str(vmid))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmstatus(vmid))
|
||||||
|
except:
|
||||||
|
logger.error('grid> status error')
|
||||||
|
raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'STATUS ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
class DeleteResource(object):
|
||||||
|
def on_post(self, req, resp, vmid):
|
||||||
|
""" delete machine completely"""
|
||||||
|
logger.info('grid> delete ' + str(vmid))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmdelete(vmid))
|
||||||
|
except:
|
||||||
|
logger.error('grid> delete error')
|
||||||
|
raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'DELETE ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
class ArchivateResource(object):
|
||||||
|
def on_post(self, req, resp, vmid):
|
||||||
|
""" Temporary suspend the instance """
|
||||||
|
logger.info('grid> suspend ' + str(vmid))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmsuspend(vmid))
|
||||||
|
except:
|
||||||
|
logger.error('grid> pause error')
|
||||||
|
raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'PAUSE ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
class UnArchiveResource(object):
|
||||||
|
def on_post(self, req, resp, vmid):
|
||||||
|
""" Unuspend the instance """
|
||||||
|
logger.info('grid> resume ' + str(vmid))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmresume(vmid))
|
||||||
|
except:
|
||||||
|
logger.error('grid> resume error')
|
||||||
|
raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'RESUME ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
class StartResource(object):
|
||||||
|
def on_post(self, req, resp, vmid):
|
||||||
|
""" Start the instance """
|
||||||
|
logger.info('grid> start ' + str(vmid))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmstart(vmid))
|
||||||
|
except:
|
||||||
|
logger.error('grid> start error')
|
||||||
|
#raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'START ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
|
||||||
|
class ShutdownResource(object):
|
||||||
|
def on_post(self, req, resp, vmid):
|
||||||
|
""" ACPI Shutdown the instance """
|
||||||
|
logger.info('grid> shutdown ' + str(vmid))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmshutdown(vmid))
|
||||||
|
#TODO: Try few times and then return proper status message
|
||||||
|
except:
|
||||||
|
logger.error('grid> shutdown error')
|
||||||
|
#raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'SHUTDOWN ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
|
||||||
|
class StopResource(object):
|
||||||
|
def on_post(self, req, resp, vmid):
|
||||||
|
""" Stop the instance """
|
||||||
|
logger.info('grid> stop ' + str(vmid))
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
if apicheck_stat:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
try:
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmstop(vmid))
|
||||||
|
except:
|
||||||
|
logger.error('grid> stop error')
|
||||||
|
#raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'STOP ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
class VNCResource(object):
|
||||||
|
def on_post(self, req, resp, vmid):
|
||||||
|
""" Create a VNC link to the instance """
|
||||||
|
apicheck_stat, apicheck_resp = apicheck(req.params)
|
||||||
|
logger.info('grid> vnc ' + str(vmid))
|
||||||
|
if apicheck_stat:
|
||||||
|
try:
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
resp.body = urllib.parse.urlencode(plugin.vmvnc(vmid))
|
||||||
|
except:
|
||||||
|
logger.error('grid> vnc error')
|
||||||
|
raise
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
response = 'VNC ERR'
|
||||||
|
resp.body = response
|
||||||
|
else:
|
||||||
|
resp.status = falcon.HTTP_403
|
||||||
|
resp.body = apicheck_resp
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit("invoke proxmaster via uwsgi. thanks. bye. o/")
|
||||||
|
|
||||||
|
#setup routes
|
||||||
|
wsgi_app = api = application = falcon.API()
|
||||||
|
|
||||||
|
#display motd
|
||||||
|
welcome()
|
||||||
|
#logger.info('grid> sync')
|
||||||
|
#grid.sync()
|
||||||
|
|
||||||
|
# setup routes
|
||||||
|
res_cluster = ClusterResource()
|
||||||
|
api.add_route('/instance', res_cluster)
|
||||||
|
|
||||||
|
res_status = StatusResource()
|
||||||
|
api.add_route('/instance/{vmid}', res_status)
|
||||||
|
|
||||||
|
res_delete = DeleteResource()
|
||||||
|
api.add_route('/instance/delete/{vmid}', res_delete)
|
||||||
|
|
||||||
|
res_archivate = ArchivateResource()
|
||||||
|
api.add_route('/instance/archivate/{vmid}', res_archivate)
|
||||||
|
|
||||||
|
res_unarchive = UnArchiveResource()
|
||||||
|
api.add_route('/instance/unarchive/{vmid}', res_unarchive)
|
||||||
|
|
||||||
|
res_start = StartResource()
|
||||||
|
api.add_route('/instance/start/{vmid}', res_start)
|
||||||
|
|
||||||
|
res_shutdown = ShutdownResource()
|
||||||
|
api.add_route('/instance/shutdown/{vmid}', res_shutdown)
|
||||||
|
|
||||||
|
res_stop = StopResource()
|
||||||
|
api.add_route('/instance/stop/{vmid}', res_stop)
|
||||||
|
|
||||||
|
res_vnc = VNCResource()
|
||||||
|
api.add_route('/instance/vnc/{vmid}', res_vnc)
|
||||||
|
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
uwsgi
|
||||||
|
pyOpenSSL
|
||||||
|
requests
|
||||||
|
falcon
|
||||||
|
urllib
|
||||||
|
netaddr
|
||||||
|
proxmoxer
|
||||||
|
websockify
|
5
runwebsockify.py
Normal file
5
runwebsockify.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import websockify
|
||||||
|
|
||||||
|
websockify.websocketproxy.websockify_init()
|
18
start.sh
Executable file
18
start.sh
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Log rotation
|
||||||
|
LOG_DIR=${HOME}/proxmaster/log
|
||||||
|
LOG_FILE="${LOG_DIR}/proxmaster.log"
|
||||||
|
|
||||||
|
mkdir -p $LOG_DIR
|
||||||
|
|
||||||
|
TIME=`date -u +%s`
|
||||||
|
|
||||||
|
if [ -e $LOG_FILE ] ; then
|
||||||
|
mv ${LOG_FILE} ${LOG_FILE}.${TIME} && touch ${LOG_FILE}
|
||||||
|
else
|
||||||
|
touch ${LOG_FILE}
|
||||||
|
fi
|
||||||
|
|
||||||
|
#startuwsgi instance
|
||||||
|
uwsgi config.ini
|
73
utils.py
Normal file
73
utils.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
#. -*- coding: utf-8
|
||||||
|
#
|
||||||
|
# helper functions
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
import functools
|
||||||
|
|
||||||
|
def dict_merge(target, *args):
|
||||||
|
""" Recursively merges mutiple dicts """
|
||||||
|
# Merge multiple dicts
|
||||||
|
if len(args) > 1:
|
||||||
|
for obj in args:
|
||||||
|
dict_merge(target, obj)
|
||||||
|
return target
|
||||||
|
|
||||||
|
# Recursively merge dicts and set non-dict values
|
||||||
|
obj = args[0]
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return obj
|
||||||
|
for k, v in obj.items():
|
||||||
|
if k in target and isinstance(target[k], dict):
|
||||||
|
dict_merge(target[k], v)
|
||||||
|
else:
|
||||||
|
target[k] = deepcopy(v)
|
||||||
|
return target
|
||||||
|
|
||||||
|
def get_rec(search_dict, field):
|
||||||
|
"""
|
||||||
|
Takes a dict with nested lists and dicts,
|
||||||
|
and searches all dicts for a key of the field
|
||||||
|
provided.
|
||||||
|
"""
|
||||||
|
fields_found = []
|
||||||
|
|
||||||
|
for key, value in search_dict.items():
|
||||||
|
if key == field:
|
||||||
|
fields_found.append(value)
|
||||||
|
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
results = get_rec(value, field)
|
||||||
|
for result in results:
|
||||||
|
fields_found.append(result)
|
||||||
|
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
more_results = get_recursively(item, field)
|
||||||
|
for another_result in more_results:
|
||||||
|
fields_found.append(another_result)
|
||||||
|
|
||||||
|
return fields_found
|
||||||
|
|
||||||
|
|
||||||
|
def gen_dict_extract(key, var):
|
||||||
|
if hasattr(var,'iteritems'):
|
||||||
|
for k, v in var.iteritems():
|
||||||
|
if k == key:
|
||||||
|
yield v
|
||||||
|
if isinstance(v, dict):
|
||||||
|
for result in gen_dict_extract(key, v):
|
||||||
|
yield result
|
||||||
|
elif isinstance(v, list):
|
||||||
|
for d in v:
|
||||||
|
for result in gen_dict_extract(key, d):
|
||||||
|
yield result
|
||||||
|
|
||||||
|
|
||||||
|
def chained_get(dct, *keys):
|
||||||
|
SENTRY = object()
|
||||||
|
def getter(level, key):
|
||||||
|
return 'NA' if level is SENTRY else level.get(key, SENTRY)
|
||||||
|
return functools.reduce(getter, keys, dct)
|
||||||
|
|
Loading…
Reference in a new issue