Fixes on resources, mailer and vps0
This commit is contained in:
parent
99071f01b1
commit
4593fcc278
48
TODO.md
48
TODO.md
|
@ -36,8 +36,6 @@
|
||||||
|
|
||||||
* What fields we really need on contacts? name email phone and what more?
|
* What fields we really need on contacts? name email phone and what more?
|
||||||
|
|
||||||
* Redirect junk emails and delete every 30 days?
|
|
||||||
|
|
||||||
* DOC: Complitely decouples scripts execution, billing, service definition
|
* DOC: Complitely decouples scripts execution, billing, service definition
|
||||||
|
|
||||||
* init.d celery scripts
|
* init.d celery scripts
|
||||||
|
@ -58,21 +56,11 @@
|
||||||
* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python3 manage.py test orchestra.contrib.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest --nologcapture --keepdb
|
* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python3 manage.py test orchestra.contrib.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest --nologcapture --keepdb
|
||||||
|
|
||||||
* ForeignKey.swappable
|
* ForeignKey.swappable
|
||||||
* Field.editable
|
|
||||||
* ManyToManyField.symmetrical = False (user group)
|
|
||||||
|
|
||||||
* REST PERMISSIONS
|
* REST PERMISSIONS
|
||||||
|
|
||||||
* caching based on "def text2int(textnum, numwords={}):"
|
|
||||||
|
|
||||||
* sync() ServiceController method that synchronizes orchestra and servers (delete or import)
|
|
||||||
|
|
||||||
* consider removing mailbox support on forward (user@pangea.org instead)
|
|
||||||
|
|
||||||
* Databases.User add reverse M2M databases widget (like mailbox.addresses)
|
* Databases.User add reverse M2M databases widget (like mailbox.addresses)
|
||||||
|
|
||||||
* Grant permissions to systemusers
|
|
||||||
|
|
||||||
* Make one dedicated CGI user for each account only for CGI execution (fpm/fcgid). Different from the files owner, and without W permissions, so attackers can not inject backdors and malware.
|
* Make one dedicated CGI user for each account only for CGI execution (fpm/fcgid). Different from the files owner, and without W permissions, so attackers can not inject backdors and malware.
|
||||||
|
|
||||||
* resource min max allocation with validation
|
* resource min max allocation with validation
|
||||||
|
@ -81,24 +69,14 @@
|
||||||
|
|
||||||
* Directory Protection on webapp and use webapp path as base path (validate)
|
* Directory Protection on webapp and use webapp path as base path (validate)
|
||||||
|
|
||||||
* validate systemuser.home on server-side
|
|
||||||
|
|
||||||
* webapp backend option compatibility check? raise exception, missconfigured error
|
* webapp backend option compatibility check? raise exception, missconfigured error
|
||||||
|
|
||||||
* admin systemuser home/directory, add default home and empty directory with has_shell on admin
|
|
||||||
|
|
||||||
* Resource used_list_display=True, allocated_list_displat=True, allow resources to show up on list_display
|
* Resource used_list_display=True, allocated_list_displat=True, allow resources to show up on list_display
|
||||||
|
|
||||||
* BackendLog.updated_at (tasks that run over several minutes when finished they do not appear first on the changelist) (like celery tasks.when)
|
* BackendLog.updated_at (tasks that run over several minutes when finished they do not appear first on the changelist) (like celery tasks.when)
|
||||||
|
|
||||||
* Periodic task for cleaning old monitoring data
|
|
||||||
|
|
||||||
* Create an admin service_view with icons (like SaaS app)
|
* Create an admin service_view with icons (like SaaS app)
|
||||||
|
|
||||||
* Resource graph for each related object
|
|
||||||
|
|
||||||
* SaaS model splitted into SaaSUser and SaaSSite? inherit from SaaS, proxy model?
|
|
||||||
|
|
||||||
* prevent @pangea.org email addresses on contacts, enforce at least one email without @pangea.org
|
* prevent @pangea.org email addresses on contacts, enforce at least one email without @pangea.org
|
||||||
|
|
||||||
* forms autocomplete="off", doesn't work in chrome
|
* forms autocomplete="off", doesn't work in chrome
|
||||||
|
@ -125,8 +103,6 @@ Php binaries should have this format: /usr/bin/php5.2-cgi
|
||||||
|
|
||||||
* contact.alternative_phone on a phone.tooltip, email:to
|
* contact.alternative_phone on a phone.tooltip, email:to
|
||||||
|
|
||||||
* better validate options and directives (url locations, filesystem paths, etc..)
|
|
||||||
|
|
||||||
* make sure that you understand the risks
|
* make sure that you understand the risks
|
||||||
|
|
||||||
* full support for deactivation of services/accounts
|
* full support for deactivation of services/accounts
|
||||||
|
@ -295,7 +271,7 @@ https://code.djangoproject.com/ticket/24576
|
||||||
# Settings.parser.changes: if setting.value == default. remove
|
# Settings.parser.changes: if setting.value == default. remove
|
||||||
# reload generic admin view ?redirect=http...
|
# reload generic admin view ?redirect=http...
|
||||||
# inspecting django db connection for asserting db readines? or performing a query
|
# inspecting django db connection for asserting db readines? or performing a query
|
||||||
# wake up django mailer on send_mail
|
* wake up django mailer on send_mail
|
||||||
|
|
||||||
from orchestra.contrib.tasks import task
|
from orchestra.contrib.tasks import task
|
||||||
import time, sys
|
import time, sys
|
||||||
|
@ -308,14 +284,14 @@ https://code.djangoproject.com/ticket/24576
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
counter.apply_async(10, '/tmp/kakas')
|
counter.apply_async(10, '/tmp/kakas')
|
||||||
|
|
||||||
# Provide some fixtures with mocked data
|
* Provide some fixtures with mocked data
|
||||||
|
|
||||||
|
|
||||||
TODO http://wiki2.dovecot.org/HowTo/SimpleVirtualInstall
|
TODO http://wiki2.dovecot.org/HowTo/SimpleVirtualInstall
|
||||||
TODO http://wiki2.dovecot.org/HowTo/VirtualUserFlatFilesPostfix
|
TODO http://wiki2.dovecot.org/HowTo/VirtualUserFlatFilesPostfix
|
||||||
TODO mount the filesystem with "nosuid" option
|
TODO mount the filesystem with "nosuid" option
|
||||||
|
|
||||||
# uwse uwsgi cron: decorator or config cron = 59 2 -1 -1 -1 %(virtualenv)/bin/python manage.py runmyfunnytask
|
* uwse uwsgi cron: decorator or config cron = 59 2 -1 -1 -1 %(virtualenv)/bin/python manage.py runmyfunnytask
|
||||||
|
|
||||||
# mailboxes.address settings multiple local domains, not only one?
|
# mailboxes.address settings multiple local domains, not only one?
|
||||||
# backend.context = self.get_context() or save(obj, context=None) ?? more like form.cleaned_data
|
# backend.context = self.get_context() or save(obj, context=None) ?? more like form.cleaned_data
|
||||||
|
@ -323,7 +299,7 @@ TODO mount the filesystem with "nosuid" option
|
||||||
# smtplib.SMTPConnectError: (421, b'4.7.0 mail.pangea.org Error: too many connections from 77.246.181.209')
|
# smtplib.SMTPConnectError: (421, b'4.7.0 mail.pangea.org Error: too many connections from 77.246.181.209')
|
||||||
|
|
||||||
# rename virtual_maps to virtual_alias_maps and remove virtual_alias_domains ?
|
# rename virtual_maps to virtual_alias_maps and remove virtual_alias_domains ?
|
||||||
# virtdomains file is not ideal, prevent fake/error on domains there! and make sure to chekc if this file is required!
|
# virtdomains file is not ideal, prevent user provided fake/error domains there! and make sure to chekc if this file is required!
|
||||||
|
|
||||||
# Deprecate restart/start/stop services (do touch wsgi.py and fuck celery)
|
# Deprecate restart/start/stop services (do touch wsgi.py and fuck celery)
|
||||||
orchestra-beat support for uwsgi cron
|
orchestra-beat support for uwsgi cron
|
||||||
|
@ -338,11 +314,9 @@ resorce monitoring more efficient, less mem an better queries for calc current d
|
||||||
|
|
||||||
# bill this https://orchestra.pangea.org/admin/orders/order/8236/ should be already billed, <= vs <
|
# bill this https://orchestra.pangea.org/admin/orders/order/8236/ should be already billed, <= vs <
|
||||||
# Convert rating method from function to PluginClass
|
# Convert rating method from function to PluginClass
|
||||||
# Tests can not run because django.db.utils.ProgrammingError: relation "accounts_account" does not exist
|
|
||||||
|
|
||||||
# autoresponses on mailboxes, not addresses or remove them
|
# autoresponses on mailboxes, not addresses or remove them
|
||||||
|
|
||||||
# ACL don't give exec permissions to files!
|
|
||||||
# force save and continue on routes (and others?)
|
# force save and continue on routes (and others?)
|
||||||
# gevent for python3
|
# gevent for python3
|
||||||
apt-get install cython3
|
apt-get install cython3
|
||||||
|
@ -384,8 +358,6 @@ uwsgi --reload /tmp/project-master.pid
|
||||||
# or if uwsgi was started with touch-reload=/tmp/somefile
|
# or if uwsgi was started with touch-reload=/tmp/somefile
|
||||||
touch /tmp/somefile
|
touch /tmp/somefile
|
||||||
|
|
||||||
# datetime metric storage granularity: otherwise innacurate detection of billed metric on order.billed_on
|
|
||||||
|
|
||||||
# Serializers.validation migration to DRF3: grep -r 'attrs, source' *|grep -v '~'
|
# Serializers.validation migration to DRF3: grep -r 'attrs, source' *|grep -v '~'
|
||||||
serailzer self.instance on create.
|
serailzer self.instance on create.
|
||||||
|
|
||||||
|
@ -427,14 +399,14 @@ Case
|
||||||
|
|
||||||
# round decimals on every billing operation
|
# round decimals on every billing operation
|
||||||
|
|
||||||
# Add SPF record type
|
|
||||||
|
|
||||||
# OVZ TRAFFIC ACCOUNTING!!
|
|
||||||
|
|
||||||
# PHPlist cron, bounces and traffic (maybe specific mail script with sitename)
|
# PHPlist cron, bounces and traffic (maybe specific mail script with sitename)
|
||||||
|
|
||||||
|
|
||||||
# use "su $user --shell /bin/bash" on backends for security : MKDIR -p...
|
# use "su $user --shell /bin/bash" on backends for security : MKDIR -p...
|
||||||
|
|
||||||
|
|
||||||
# model.field.flatchoices
|
# model.field.flatchoices
|
||||||
|
|
||||||
|
* This is beta software, please test thoroughly before putting into production and report back any issues.
|
||||||
|
|
||||||
|
# messages SMTP errors: temporary->deferre else Failed
|
||||||
|
|
||||||
|
# Don't enforce one contact per account? remove account.email in favour of contacts?
|
||||||
|
|
|
@ -191,7 +191,7 @@ def fire_pending_messages(settings, db):
|
||||||
for num, seconds in enumerate(MAILER_DEFERE_SECONDS):
|
for num, seconds in enumerate(MAILER_DEFERE_SECONDS):
|
||||||
delta = timedelta(seconds=seconds)
|
delta = timedelta(seconds=seconds)
|
||||||
epoch = now-delta
|
epoch = now-delta
|
||||||
query_or.append("""(mailer_message.retries = %i AND mailer_message.last_retry <= '%s')"""
|
query_or.append("""(mailer_message.retries = %i AND mailer_message.last_try <= '%s')"""
|
||||||
% (num, epoch.isoformat().replace('T', ' ')))
|
% (num, epoch.isoformat().replace('T', ' ')))
|
||||||
query = """\
|
query = """\
|
||||||
SELECT 1 FROM mailer_message
|
SELECT 1 FROM mailer_message
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core.validators import validate_password
|
from orchestra.forms import UserCreationForm, NonStoredUserChangeForm
|
||||||
from orchestra.forms.widgets import SpanWidget
|
|
||||||
|
|
||||||
|
|
||||||
class CleanAddressMixin(object):
|
class CleanAddressMixin(object):
|
||||||
|
@ -15,25 +13,9 @@ class CleanAddressMixin(object):
|
||||||
return domain
|
return domain
|
||||||
|
|
||||||
|
|
||||||
class ListCreationForm(CleanAddressMixin, forms.ModelForm):
|
class ListCreationForm(CleanAddressMixin, UserCreationForm):
|
||||||
password1 = forms.CharField(label=_("Password"), validators=[validate_password],
|
pass
|
||||||
widget=forms.PasswordInput)
|
|
||||||
password2 = forms.CharField(label=_("Password confirmation"),
|
|
||||||
widget=forms.PasswordInput,
|
|
||||||
help_text=_("Enter the same password as above, for verification."))
|
|
||||||
|
|
||||||
def clean_password2(self):
|
|
||||||
password1 = self.cleaned_data.get("password1")
|
|
||||||
password2 = self.cleaned_data.get("password2")
|
|
||||||
if password1 and password2 and password1 != password2:
|
|
||||||
msg = _("The two password fields didn't match.")
|
|
||||||
raise forms.ValidationError(msg)
|
|
||||||
return password2
|
|
||||||
|
|
||||||
|
|
||||||
class ListChangeForm(CleanAddressMixin, forms.ModelForm):
|
class ListChangeForm(CleanAddressMixin, NonStoredUserChangeForm):
|
||||||
password = forms.CharField(label=_("Password"), required=False,
|
pass
|
||||||
widget=SpanWidget(display='<strong>Unknown password</strong>'),
|
|
||||||
help_text=_("List passwords are not stored, so there is no way to see this "
|
|
||||||
"list's password, but you can change the password using "
|
|
||||||
"<a href=\"password/\">this form</a>."))
|
|
||||||
|
|
|
@ -28,13 +28,13 @@ COLORS = {
|
||||||
class MessageAdmin(admin.ModelAdmin):
|
class MessageAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'display_subject', 'colored_state', 'priority', 'to_address', 'from_address',
|
'display_subject', 'colored_state', 'priority', 'to_address', 'from_address',
|
||||||
'created_at_delta', 'retries', 'last_retry_delta', 'num_logs',
|
'created_at_delta', 'display_retries', 'last_try_delta',
|
||||||
)
|
)
|
||||||
list_filter = ('state', 'priority', 'retries')
|
list_filter = ('state', 'priority', 'retries')
|
||||||
list_prefetch_related = ('logs__id')
|
list_prefetch_related = ('logs__id')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'fields': ('state', 'priority', ('retries', 'last_retry_delta', 'created_at_delta'),
|
'fields': ('state', 'priority', ('retries', 'last_try_delta', 'created_at_delta'),
|
||||||
'display_full_subject', 'display_from', 'display_to',
|
'display_full_subject', 'display_from', 'display_to',
|
||||||
'display_content'),
|
'display_content'),
|
||||||
}),
|
}),
|
||||||
|
@ -44,36 +44,36 @@ class MessageAdmin(admin.ModelAdmin):
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
'retries', 'last_retry_delta', 'created_at_delta', 'display_full_subject',
|
'retries', 'last_try_delta', 'created_at_delta', 'display_full_subject',
|
||||||
'display_to', 'display_from', 'display_content',
|
'display_to', 'display_from', 'display_content',
|
||||||
)
|
)
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
colored_state = admin_colored('state', colors=COLORS)
|
colored_state = admin_colored('state', colors=COLORS)
|
||||||
created_at_delta = admin_date('created_at')
|
created_at_delta = admin_date('created_at')
|
||||||
last_retry_delta = admin_date('last_retry')
|
last_try_delta = admin_date('last_try')
|
||||||
|
|
||||||
def display_subject(self, instance):
|
def display_subject(self, instance):
|
||||||
subject = instance.subject
|
subject = instance.subject
|
||||||
if len(subject) > 32:
|
if len(subject) > 64:
|
||||||
return subject[:32] + '…'
|
return subject[:64] + '…'
|
||||||
return subject
|
return subject
|
||||||
display_subject.short_description = _("Subject")
|
display_subject.short_description = _("Subject")
|
||||||
display_subject.admin_order_field = 'subject'
|
display_subject.admin_order_field = 'subject'
|
||||||
display_subject.allow_tags = True
|
display_subject.allow_tags = True
|
||||||
|
|
||||||
def num_logs(self, instance):
|
def display_retries(self, instance):
|
||||||
num = instance.logs__count
|
num_logs = instance.logs__count
|
||||||
if num == 1:
|
if num_logs == 1:
|
||||||
pk = instance.logs.all()[0].id
|
pk = instance.logs.all()[0].id
|
||||||
url = reverse('admin:mailer_smtplog_change', args=(pk,))
|
url = reverse('admin:mailer_smtplog_change', args=(pk,))
|
||||||
else:
|
else:
|
||||||
url = reverse('admin:mailer_smtplog_changelist')
|
url = reverse('admin:mailer_smtplog_changelist')
|
||||||
url += '?&message=%i' % instance.pk
|
url += '?&message=%i' % instance.pk
|
||||||
return '<a href="%s" onclick="return showAddAnotherPopup(this);">%d</a>' % (url, num)
|
return '<a href="%s" onclick="return showAddAnotherPopup(this);">%d</a>' % (url, instance.retries)
|
||||||
num_logs.short_description = _("Logs")
|
display_retries.short_description = _("Retries")
|
||||||
num_logs.admin_order_field = 'logs__count'
|
display_retries.admin_order_field = 'retries'
|
||||||
num_logs.allow_tags = True
|
display_retries.allow_tags = True
|
||||||
|
|
||||||
def display_content(self, instance):
|
def display_content(self, instance):
|
||||||
part = email.message_from_string(instance.content)
|
part = email.message_from_string(instance.content)
|
||||||
|
|
|
@ -23,6 +23,12 @@ def send_message(message, num=0, connection=None, bulk=settings.MAILER_BULK_MESS
|
||||||
connection = get_connection(backend='django.core.mail.backends.smtp.EmailBackend')
|
connection = get_connection(backend='django.core.mail.backends.smtp.EmailBackend')
|
||||||
connection.open()
|
connection.open()
|
||||||
error = None
|
error = None
|
||||||
|
message.last_try = timezone.now()
|
||||||
|
update_fields = ['last_try']
|
||||||
|
if message.state != message.QUEUED:
|
||||||
|
message.retries += 1
|
||||||
|
update_fields.append('retries')
|
||||||
|
message.save(update_fields=update_fields)
|
||||||
try:
|
try:
|
||||||
connection.connection.sendmail(message.from_address, [message.to_address], smart_str(message.content))
|
connection.connection.sendmail(message.from_address, [message.to_address], smart_str(message.content))
|
||||||
except (SocketError,
|
except (SocketError,
|
||||||
|
@ -49,7 +55,7 @@ def send_pending(bulk=settings.MAILER_BULK_MESSAGES):
|
||||||
qs = Q()
|
qs = Q()
|
||||||
for retries, seconds in enumerate(settings.MAILER_DEFERE_SECONDS):
|
for retries, seconds in enumerate(settings.MAILER_DEFERE_SECONDS):
|
||||||
delta = timedelta(seconds=seconds)
|
delta = timedelta(seconds=seconds)
|
||||||
qs = qs | Q(retries=retries, last_retry__lte=now-delta)
|
qs = qs | Q(retries=retries, last_try__lte=now-delta)
|
||||||
for message in Message.objects.filter(state=Message.DEFERRED).filter(qs).order_by('priority'):
|
for message in Message.objects.filter(state=Message.DEFERRED).filter(qs).order_by('priority'):
|
||||||
connection = send_message(message, num, connection, bulk)
|
connection = send_message(message, num, connection, bulk)
|
||||||
num += 1
|
num += 1
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mailer', '0003_auto_20150617_1024'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='message',
|
||||||
|
old_name='last_retry',
|
||||||
|
new_name='last_try',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='message',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(verbose_name='State', max_length=16, choices=[('QUEUED', 'Queued'), ('SENT', 'Sent'), ('DEFERRED', 'Deferred'), ('FAILED', 'Failed')], default='QUEUED'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,4 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
@ -14,7 +13,7 @@ class Message(models.Model):
|
||||||
(QUEUED, _("Queued")),
|
(QUEUED, _("Queued")),
|
||||||
(SENT, _("Sent")),
|
(SENT, _("Sent")),
|
||||||
(DEFERRED, _("Deferred")),
|
(DEFERRED, _("Deferred")),
|
||||||
(FAILED, _("Failes")),
|
(FAILED, _("Failed")),
|
||||||
)
|
)
|
||||||
|
|
||||||
CRITICAL = 0
|
CRITICAL = 0
|
||||||
|
@ -36,8 +35,7 @@ class Message(models.Model):
|
||||||
content = models.TextField(_("content"))
|
content = models.TextField(_("content"))
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||||
retries = models.PositiveIntegerField(_("retries"), default=0)
|
retries = models.PositiveIntegerField(_("retries"), default=0)
|
||||||
# TODO rename to last_try
|
last_try = models.DateTimeField(_("last try"), null=True)
|
||||||
last_retry = models.DateTimeField(_("last try"), null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s to %s' % (self.subject, self.to_address)
|
return '%s to %s' % (self.subject, self.to_address)
|
||||||
|
@ -47,9 +45,7 @@ class Message(models.Model):
|
||||||
# Max tries
|
# Max tries
|
||||||
if self.retries >= len(settings.MAILER_DEFERE_SECONDS):
|
if self.retries >= len(settings.MAILER_DEFERE_SECONDS):
|
||||||
self.state = self.FAILED
|
self.state = self.FAILED
|
||||||
self.retries += 1
|
self.save(update_fields=('state',))
|
||||||
self.last_retry = timezone.now()
|
|
||||||
self.save(update_fields=('state', 'retries', 'last_retry'))
|
|
||||||
|
|
||||||
def sent(self):
|
def sent(self):
|
||||||
self.state = self.SENT
|
self.state = self.SENT
|
||||||
|
|
|
@ -198,7 +198,7 @@ class MonitorDataAdmin(ExtendedModelAdmin):
|
||||||
list_display = ('id', 'monitor', content_object_link, 'display_created', 'value')
|
list_display = ('id', 'monitor', content_object_link, 'display_created', 'value')
|
||||||
list_filter = ('monitor', ResourceDataListFilter)
|
list_filter = ('monitor', ResourceDataListFilter)
|
||||||
add_fields = ('monitor', 'content_type', 'object_id', 'created_at', 'value')
|
add_fields = ('monitor', 'content_type', 'object_id', 'created_at', 'value')
|
||||||
fields = ('monitor', 'content_type', content_object_link, 'display_created', 'value')
|
fields = ('monitor', 'content_type', content_object_link, 'display_created', 'value', 'state')
|
||||||
change_readonly_fields = fields
|
change_readonly_fields = fields
|
||||||
list_select_related = ('content_type',)
|
list_select_related = ('content_type',)
|
||||||
search_fields = ('content_object_repr',)
|
search_fields = ('content_object_repr',)
|
||||||
|
@ -258,6 +258,7 @@ def resource_inline_factory(resources):
|
||||||
if resource not in queryset_resources:
|
if resource not in queryset_resources:
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'content_object': self.instance,
|
'content_object': self.instance,
|
||||||
|
'content_object_repr': str(self.instance),
|
||||||
}
|
}
|
||||||
if resource.default_allocation:
|
if resource.default_allocation:
|
||||||
kwargs['allocated'] = resource.default_allocation
|
kwargs['allocated'] = resource.default_allocation
|
||||||
|
@ -323,7 +324,9 @@ def resource_inline_factory(resources):
|
||||||
used_url = reverse('admin:resources_resourcedata_used_monitordata', args=(rdata.pk,))
|
used_url = reverse('admin:resources_resourcedata_used_monitordata', args=(rdata.pk,))
|
||||||
used = '<a href="%s">%s %s</a>' % (used_url, rdata.used, rdata.unit)
|
used = '<a href="%s">%s %s</a>' % (used_url, rdata.used, rdata.unit)
|
||||||
return ' '.join(map(str, (used, update, history)))
|
return ' '.join(map(str, (used, update, history)))
|
||||||
|
if rdata.resource.monitors:
|
||||||
return _("Unknonw %s %s") % (update, history)
|
return _("Unknonw %s %s") % (update, history)
|
||||||
|
return _("No monitor")
|
||||||
display_used.short_description = _("Used")
|
display_used.short_description = _("Used")
|
||||||
display_used.allow_tags = True
|
display_used.allow_tags = True
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,14 @@ class ServiceMonitor(ServiceBackend):
|
||||||
return data.created_at
|
return data.created_at
|
||||||
|
|
||||||
def process(self, line):
|
def process(self, line):
|
||||||
""" line -> object_id, value """
|
""" line -> object_id, value, state"""
|
||||||
return line.split()
|
result = line.split()
|
||||||
|
if len(result) != 2:
|
||||||
|
cls_name = self.__class__.__name__
|
||||||
|
raise ValueError("%s expected '<id> <value>' got '%s'" % (cls_name, line))
|
||||||
|
# State is None, unless your monitor needs to keep track of it
|
||||||
|
result.append(None)
|
||||||
|
return result
|
||||||
|
|
||||||
def store(self, log):
|
def store(self, log):
|
||||||
""" stores monitored values from stdout """
|
""" stores monitored values from stdout """
|
||||||
|
@ -68,16 +74,14 @@ class ServiceMonitor(ServiceBackend):
|
||||||
ct = ContentType.objects.get_by_natural_key(app_label, model_name.lower())
|
ct = ContentType.objects.get_by_natural_key(app_label, model_name.lower())
|
||||||
for line in log.stdout.splitlines():
|
for line in log.stdout.splitlines():
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
try:
|
object_id, value, state = self.process(line)
|
||||||
object_id, value = self.process(line)
|
|
||||||
except ValueError:
|
|
||||||
cls_name = self.__class__.__name__
|
|
||||||
raise ValueError("%s expected '<id> <value>' got '%s'" % (cls_name, line))
|
|
||||||
if isinstance(value, bytes):
|
if isinstance(value, bytes):
|
||||||
value = value.decode('ascii')
|
value = value.decode('ascii')
|
||||||
|
if isinstance(state, bytes):
|
||||||
|
state = state.decode('ascii')
|
||||||
content_object = ct.get_object_for_this_type(pk=object_id)
|
content_object = ct.get_object_for_this_type(pk=object_id)
|
||||||
MonitorData.objects.create(
|
MonitorData.objects.create(
|
||||||
monitor=name, object_id=object_id, content_type=ct, value=value,
|
monitor=name, object_id=object_id, content_type=ct, value=value, state=state,
|
||||||
created_at=self.current_date, content_object_repr=str(content_object),
|
created_at=self.current_date, content_object_repr=str(content_object),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('resources', '0007_auto_20150723_1251'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='monitordata',
|
||||||
|
name='state',
|
||||||
|
field=models.DecimalField(verbose_name='state', null=True, max_digits=16, help_text='Optional field used to store current state needed for diff-based monitoring.', decimal_places=2),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('resources', '0008_monitordata_state'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='resourcedata',
|
||||||
|
name='allocated',
|
||||||
|
field=models.PositiveIntegerField(verbose_name='allocated', null=True, blank=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -172,8 +172,7 @@ class ResourceData(models.Model):
|
||||||
used = models.DecimalField(_("used"), max_digits=16, decimal_places=3, null=True,
|
used = models.DecimalField(_("used"), max_digits=16, decimal_places=3, null=True,
|
||||||
editable=False)
|
editable=False)
|
||||||
updated_at = models.DateTimeField(_("updated"), null=True, editable=False)
|
updated_at = models.DateTimeField(_("updated"), null=True, editable=False)
|
||||||
allocated = models.DecimalField(_("allocated"), max_digits=8, decimal_places=2,
|
allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True)
|
||||||
null=True, blank=True)
|
|
||||||
content_object_repr = models.CharField(_("content object representation"), max_length=256,
|
content_object_repr = models.CharField(_("content object representation"), max_length=256,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
|
||||||
|
@ -268,6 +267,8 @@ class MonitorData(models.Model):
|
||||||
object_id = models.PositiveIntegerField(_("object id"), db_index=True)
|
object_id = models.PositiveIntegerField(_("object id"), db_index=True)
|
||||||
created_at = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
|
created_at = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
|
||||||
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
|
||||||
|
state = models.DecimalField(_("state"), max_digits=16, decimal_places=2, null=True,
|
||||||
|
help_text=_("Optional field used to store current state needed for diff-based monitoring."))
|
||||||
content_object_repr = models.CharField(_("content object representation"), max_length=256,
|
content_object_repr = models.CharField(_("content object representation"), max_length=256,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
|
||||||
|
@ -308,6 +309,7 @@ def create_resource_relation():
|
||||||
)
|
)
|
||||||
rdata = ResourceData(
|
rdata = ResourceData(
|
||||||
content_object=self.obj,
|
content_object=self.obj,
|
||||||
|
content_object_repr=str(self.obj),
|
||||||
resource=resource,
|
resource=resource,
|
||||||
allocated=resource.default_allocation
|
allocated=resource.default_allocation
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,7 +33,12 @@ class ResourceSerializer(serializers.ModelSerializer):
|
||||||
def insert_resource_serializers():
|
def insert_resource_serializers():
|
||||||
# clean previous state
|
# clean previous state
|
||||||
for related in Resource._related:
|
for related in Resource._related:
|
||||||
|
try:
|
||||||
viewset = router.get_viewset(related)
|
viewset = router.get_viewset(related)
|
||||||
|
except KeyError:
|
||||||
|
# API viewset not registered
|
||||||
|
pass
|
||||||
|
else:
|
||||||
fields = list(viewset.serializer_class.Meta.fields)
|
fields = list(viewset.serializer_class.Meta.fields)
|
||||||
try:
|
try:
|
||||||
fields.remove('resources')
|
fields.remove('resources')
|
||||||
|
|
|
@ -63,7 +63,7 @@ def monitor(resource_id, ids=None):
|
||||||
def cleanup_old_monitors(queryset=None):
|
def cleanup_old_monitors(queryset=None):
|
||||||
if queryset is None:
|
if queryset is None:
|
||||||
from .models import MonitorData
|
from .models import MonitorData
|
||||||
queryset = MonitorData.objects.filter()
|
queryset = MonitorData.objects.all()
|
||||||
delta = datetime.timedelta(days=settings.RESOURCES_OLD_MONITOR_DATA_DAYS)
|
delta = datetime.timedelta(days=settings.RESOURCES_OLD_MONITOR_DATA_DAYS)
|
||||||
threshold = timezone.now() - delta
|
threshold = timezone.now() - delta
|
||||||
queryset = queryset.filter(created_at__lt=threshold)
|
queryset = queryset.filter(created_at__lt=threshold)
|
||||||
|
|
|
@ -34,7 +34,8 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
sys.stderr.write(msg + '\n')
|
sys.stderr.write(msg + '\n')
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
def _save(self, saas, server):
|
def _install_or_change_password(self, saas, server):
|
||||||
|
""" configures the database for the new site through HTTP to /admin/ """
|
||||||
admin_link = 'https://%s/admin/' % saas.get_site_domain()
|
admin_link = 'https://%s/admin/' % saas.get_site_domain()
|
||||||
sys.stdout.write('admin_link: %s\n' % admin_link)
|
sys.stdout.write('admin_link: %s\n' % admin_link)
|
||||||
admin_content = requests.get(admin_link, verify=settings.SAAS_PHPLIST_VERIFY_SSL)
|
admin_content = requests.get(admin_link, verify=settings.SAAS_PHPLIST_VERIFY_SSL)
|
||||||
|
@ -75,7 +76,7 @@ class PhpListSaaSBackend(ServiceController):
|
||||||
|
|
||||||
def save(self, saas):
|
def save(self, saas):
|
||||||
if hasattr(saas, 'password'):
|
if hasattr(saas, 'password'):
|
||||||
self.append(self._save, saas)
|
self.append(self._install_or_change_password, saas)
|
||||||
context = self.get_context(saas)
|
context = self.get_context(saas)
|
||||||
if context['crontab']:
|
if context['crontab']:
|
||||||
context['escaped_crontab'] = context['crontab'].replace('$', '\\$')
|
context['escaped_crontab'] = context['crontab'].replace('$', '\\$')
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
is_fee=&
|
is_fee=&
|
||||||
order_description=&
|
order_description=&
|
||||||
ignore_period=TEN_DAYS&
|
ignore_period=TEN_DAYS&
|
||||||
metric=max(logsteps(mailbox.resources.disk.allocated%20or%200)%20-2,%200)&
|
metric=max((mailbox.resources.disk.allocated%20or%200)%20-2,%200)&
|
||||||
nominal_price=20.00&
|
nominal_price=20.00&
|
||||||
tax=21&
|
tax=21&
|
||||||
pricing_period=&
|
pricing_period=&
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import fnmatch
|
||||||
import os
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
@ -97,6 +98,51 @@ class UNIXUserBackend(ServiceController):
|
||||||
else:
|
else:
|
||||||
self.append("rm -fr %(base_home)s" % context)
|
self.append("rm -fr %(base_home)s" % context)
|
||||||
|
|
||||||
|
def grant_permissions(self, user, context):
|
||||||
|
context['perms'] = user.set_perm_perms
|
||||||
|
# Capital X adds execution permissions for directories, not files
|
||||||
|
context['perms_X'] = context['perms'] + 'X'
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
# Grant execution permissions to every parent directory
|
||||||
|
for access_path in %(access_paths)s; do
|
||||||
|
# Preserve existing ACLs
|
||||||
|
acl=$(getfacl -a "$access_path" | grep '^user:%(user)s:') && {
|
||||||
|
perms=$(echo "$acl" | cut -d':' -f3)
|
||||||
|
perms=$(echo "$perms" | cut -c 1,2)x
|
||||||
|
setfacl -m u:%(user)s:$perms "$access_path"
|
||||||
|
} || setfacl -m u:%(user)s:--x "$access_path"
|
||||||
|
done
|
||||||
|
# Grant perms to existing files, excluding execution
|
||||||
|
find '%(perm_to)s' -type f %(exclude_acl)s \\
|
||||||
|
-exec setfacl -m u:%(user)s:%(perms)s {} \\;
|
||||||
|
# Grant perms to extisting directories and set defaults for future content
|
||||||
|
find '%(perm_to)s' -type d %(exclude_acl)s \\
|
||||||
|
-exec setfacl -m u:%(user)s:%(perms_X)s -m d:u:%(user)s:%(perms_X)s {} \\;
|
||||||
|
# Account group as the owner of new files
|
||||||
|
chmod g+s '%(perm_to)s'""") % context
|
||||||
|
)
|
||||||
|
if not user.is_main:
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
# Grant access to main user
|
||||||
|
find '%(perm_to)s' -type d %(exclude_acl)s \\
|
||||||
|
-exec setfacl -m d:u:%(mainuser)s:rwx {} \\;\
|
||||||
|
""") % context
|
||||||
|
)
|
||||||
|
|
||||||
|
def revoke_permissions(self, context):
|
||||||
|
revoke_perms = {
|
||||||
|
'rw': '',
|
||||||
|
'r': 'w',
|
||||||
|
'w': 'r',
|
||||||
|
}
|
||||||
|
context['perms'] = revoke_perms[user.set_perm_perms]
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
# Revoke permissions
|
||||||
|
find '%(perm_to)s' %(exclude_acl)s \\
|
||||||
|
-exec setfacl -m u:%(user)s:%(perms)s {} \\;\
|
||||||
|
""") % context
|
||||||
|
)
|
||||||
|
|
||||||
def set_permission(self, user):
|
def set_permission(self, user):
|
||||||
context = self.get_context(user)
|
context = self.get_context(user)
|
||||||
context.update({
|
context.update({
|
||||||
|
@ -108,17 +154,10 @@ class UNIXUserBackend(ServiceController):
|
||||||
context['exclude_acl'] = os.path.join(user.set_perm_base_home, exclude)
|
context['exclude_acl'] = os.path.join(user.set_perm_base_home, exclude)
|
||||||
exclude_acl.append('-not -path "%(exclude_acl)s"' % context)
|
exclude_acl.append('-not -path "%(exclude_acl)s"' % context)
|
||||||
context['exclude_acl'] = ' \\\n -a '.join(exclude_acl) if exclude_acl else ''
|
context['exclude_acl'] = ' \\\n -a '.join(exclude_acl) if exclude_acl else ''
|
||||||
if user.set_perm_perms == 'rw':
|
|
||||||
context['perm_perms'] = 'rwx' if user.set_perm_action == 'grant' else '---'
|
|
||||||
elif user.set_perm_perms == 'r':
|
|
||||||
context['perm_perms'] = 'r-x' if user.set_perm_action == 'grant' else '-wx'
|
|
||||||
elif user.set_perm_perms == 'w':
|
|
||||||
context['perm_perms'] = '-wx' if user.set_perm_action == 'grant' else 'r-x'
|
|
||||||
# Access paths
|
# Access paths
|
||||||
head = user.set_perm_base_home
|
head = user.set_perm_base_home
|
||||||
relative = ''
|
relative = ''
|
||||||
access_paths = ["'%s'" % head]
|
access_paths = ["'%s'" % head]
|
||||||
import fnmatch
|
|
||||||
for tail in user.set_perm_home_extension.split(os.sep)[:-1]:
|
for tail in user.set_perm_home_extension.split(os.sep)[:-1]:
|
||||||
relative = os.path.join(relative, tail)
|
relative = os.path.join(relative, tail)
|
||||||
for exclude in settings.SYSTEMUSERS_FORBIDDEN_PATHS:
|
for exclude in settings.SYSTEMUSERS_FORBIDDEN_PATHS:
|
||||||
|
@ -129,39 +168,11 @@ class UNIXUserBackend(ServiceController):
|
||||||
head = os.path.join(head, tail)
|
head = os.path.join(head, tail)
|
||||||
access_paths.append("'%s'" % head)
|
access_paths.append("'%s'" % head)
|
||||||
context['access_paths'] = ' '.join(access_paths)
|
context['access_paths'] = ' '.join(access_paths)
|
||||||
|
|
||||||
if user.set_perm_action == 'grant':
|
if user.set_perm_action == 'grant':
|
||||||
self.append(textwrap.dedent("""\
|
self.grant_permissions(user, context)
|
||||||
# Grant execution permissions to every parent directory
|
|
||||||
for access_path in %(access_paths)s; do
|
|
||||||
# Preserve existing ACLs
|
|
||||||
acl=$(getfacl -a "$access_path" | grep '^user:%(user)s:') && {
|
|
||||||
perms=$(echo "$acl" | cut -d':' -f3)
|
|
||||||
perms=$(echo "$perms" | cut -c 1,2)x
|
|
||||||
setfacl -m u:%(user)s:$perms "$access_path"
|
|
||||||
} || setfacl -m u:%(user)s:--x "$access_path"
|
|
||||||
done
|
|
||||||
# Grant perms to existing and future files
|
|
||||||
find '%(perm_to)s' %(exclude_acl)s \\
|
|
||||||
-exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;
|
|
||||||
find '%(perm_to)s' -type d %(exclude_acl)s \\
|
|
||||||
-exec setfacl -m d:u:%(user)s:%(perm_perms)s {} \\;
|
|
||||||
# Account group as the owner of new files
|
|
||||||
chmod g+s '%(perm_to)s'""") % context
|
|
||||||
)
|
|
||||||
if not user.is_main:
|
|
||||||
self.append(textwrap.dedent("""\
|
|
||||||
# Grant access to main user
|
|
||||||
find '%(perm_to)s' -type d %(exclude_acl)s \\
|
|
||||||
-exec setfacl -m d:u:%(mainuser)s:rwx {} \\;\
|
|
||||||
""") % context
|
|
||||||
)
|
|
||||||
elif user.set_perm_action == 'revoke':
|
elif user.set_perm_action == 'revoke':
|
||||||
self.append(textwrap.dedent("""\
|
self.revoke_permissions(user, context)
|
||||||
# Revoke permissions
|
|
||||||
find '%(perm_to)s' %(exclude_acl)s \\
|
|
||||||
-exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;\
|
|
||||||
""") % context
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,18 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.contrib.accounts.actions import list_accounts
|
from orchestra.contrib.accounts.actions import list_accounts
|
||||||
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
from orchestra.contrib.accounts.admin import AccountAdminMixin
|
||||||
|
from orchestra.forms import UserCreationForm, NonStoredUserChangeForm
|
||||||
|
|
||||||
from .forms import VPSChangeForm, VPSCreationForm
|
|
||||||
from .models import VPS
|
from .models import VPS
|
||||||
|
|
||||||
|
|
||||||
class VPSAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
class VPSAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = ('hostname', 'type', 'template', 'account_link')
|
list_display = ('hostname', 'type', 'template', 'account_link')
|
||||||
list_filter = ('type', 'template')
|
list_filter = ('type', 'template')
|
||||||
form = VPSChangeForm
|
form = NonStoredUserChangeForm
|
||||||
add_form = VPSCreationForm
|
add_form = UserCreationForm
|
||||||
readonly_fields = ('account_link',)
|
readonly_fields = ('account_link',)
|
||||||
change_readonly_fields = ('account', 'name', 'type', 'template')
|
change_readonly_fields = ('account', 'hostname', 'type', 'template')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
'classes': ('wide',),
|
'classes': ('wide',),
|
||||||
|
|
|
@ -1,38 +1,114 @@
|
||||||
from orchestra.contrib.orchestration import replace
|
import decimal
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from orchestra.contrib.orchestration import ServiceController
|
||||||
from orchestra.contrib.resources import ServiceMonitor
|
from orchestra.contrib.resources import ServiceMonitor
|
||||||
|
|
||||||
|
|
||||||
|
class ProxmoxOVZ(ServiceController):
|
||||||
|
model = 'vps.VPS'
|
||||||
|
|
||||||
|
RESOURCES = (
|
||||||
|
('memory', 'mem'),
|
||||||
|
('swap', 'swap'),
|
||||||
|
('disk', 'disk')
|
||||||
|
)
|
||||||
|
GET_PROXMOX_INFO = textwrap.dedent("""
|
||||||
|
function get_vz_info () {
|
||||||
|
hostname=$1
|
||||||
|
version=$(pveversion | cut -d '/' -f2 | cut -d'.' -f1)
|
||||||
|
if [[ $version -lt 2 ]]; then
|
||||||
|
conf=$(grep "CID\\|:$hostname:" /var/lib/pve-manager/vzlist | grep -B1 ":$hostname:")
|
||||||
|
CID=$(echo "$conf" | head -n1 | cut -d':' -f2)
|
||||||
|
CTID=$(echo "$conf" | tail -n1 | cut -d':' -f1)
|
||||||
|
node=$(pveca -l | grep "^\\s*$CID\\s*:" | awk {'print $3'})
|
||||||
|
else
|
||||||
|
conf=$(grep -r "HOSTNAME=\\"$hostname\\"" /etc/pve/nodes/*/openvz/*.conf)
|
||||||
|
node=$(echo "${conf}" | cut -d"/" -f5)
|
||||||
|
CTID=$(echo "${conf}" | cut -d"/" -f7 | cut -d"\\." -f1)
|
||||||
|
fi
|
||||||
|
echo $CTID $node
|
||||||
|
}""")
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
super(ProxmoxOVZ, self).prepare()
|
||||||
|
self.append(self.GET_PROXMOX_INFO)
|
||||||
|
|
||||||
|
def get_vzset_args(self, vps):
|
||||||
|
args = []
|
||||||
|
for resource, arg_name in self.RESOURCES:
|
||||||
|
try:
|
||||||
|
allocation = getattr(vps.resources, resource).allocated
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
args.append('--%s %i' % (arg_name, allocation))
|
||||||
|
return ' '.join(args)
|
||||||
|
|
||||||
|
def save(self, vps):
|
||||||
|
context = self.get_context(vps)
|
||||||
|
self.append('info=( $(get_vz_info %(hostname)s) )' % context)
|
||||||
|
vzset_args = self.get_vzset_args(vps)
|
||||||
|
if vzset_args:
|
||||||
|
context['vzset_args'] = vzset_args
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
cat << EOF | ssh root@${info[1]}
|
||||||
|
pvectl vzset ${info[0]} %(vzset_args)s
|
||||||
|
EOF""") % context
|
||||||
|
)
|
||||||
|
if hasattr(vps, 'password'):
|
||||||
|
context['password'] = vps.password.replace('$', '\\$')
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
cat << EOF | ssh root@${info[1]}
|
||||||
|
echo 'root:%(password)s' | vzctl exec ${info[0]} chpasswd -e
|
||||||
|
EOF""") % context
|
||||||
|
)
|
||||||
|
def get_context(self, vps):
|
||||||
|
return {
|
||||||
|
'hostname': vps.hostname,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO rename to proxmox
|
||||||
class OpenVZTraffic(ServiceMonitor):
|
class OpenVZTraffic(ServiceMonitor):
|
||||||
"""
|
|
||||||
WARNING: Not fully implemeted
|
|
||||||
"""
|
|
||||||
model = 'vps.VPS'
|
model = 'vps.VPS'
|
||||||
resource = ServiceMonitor.TRAFFIC
|
resource = ServiceMonitor.TRAFFIC
|
||||||
monthly_sum_old_values = True
|
monthly_sum_old_values = True
|
||||||
|
|
||||||
def process(self, line):
|
def prepare(self):
|
||||||
""" diff with last stored value """
|
super(OpenVZTraffic, self).prepare()
|
||||||
object_id, value = line.split()
|
self.append(ProxmoxOVZ.GET_PROXMOX_INFO)
|
||||||
last = self.get_last_data(object_id)
|
self.append(textwrap.dedent("""
|
||||||
if not last or last.value > value:
|
function monitor () {
|
||||||
return object_id, value
|
object_id=$1
|
||||||
return object_id, value-last.value
|
hostname=$2
|
||||||
|
info=( $(get_vz_info $hostname) )
|
||||||
def monitor(self, container):
|
cat << EOF | ssh root@${info[1]}
|
||||||
""" Get OpenVZ container traffic on a Proxmox +2.0 cluster """
|
vzctl exec ${info[0]} cat /proc/net/dev \\
|
||||||
context = self.get_context(container)
|
| grep venet0 \\
|
||||||
self.append(
|
| tr ':' ' ' \\
|
||||||
"CONF=$(grep -r 'HOSTNAME=\"%(hostname)s\"' /etc/pve/nodes/*/openvz/*.conf)" % context)
|
| awk '{sum=\\$2+\\$10} END {printf ("$object_id %0.0f\\n", sum)}'
|
||||||
self.append('NODE=$(echo "${CONF}" | cut -d"/" -f5)')
|
EOF
|
||||||
self.append('CTID=$(echo "${CONF}" | cut -d"/" -f7 | cur -d"\." -f1)')
|
}
|
||||||
self.append(
|
""")
|
||||||
"ssh root@${NODE} vzctl exec ${CTID} cat /proc/net/dev \\\n"
|
)
|
||||||
" | grep venet0 \\\n"
|
|
||||||
" | awk -F: '{print $2}' \\\n"
|
def process(self, line):
|
||||||
" | awk '{print $1+$9}'")
|
""" diff with last stored state """
|
||||||
|
object_id, value, state = super(OpenVZTraffic, self).process(line)
|
||||||
def get_context(self, container):
|
value = decimal.Decimal(value)
|
||||||
context = {
|
last = self.get_last_data(object_id)
|
||||||
'hostname': container.hostname,
|
if not last or last.state > value:
|
||||||
|
return object_id, value, value
|
||||||
|
return object_id, value-last.state, value
|
||||||
|
|
||||||
|
def monitor(self, vps):
|
||||||
|
""" Get OpenVZ container traffic on a Proxmox cluster """
|
||||||
|
context = self.get_context(vps)
|
||||||
|
self.append('monitor %(object_id)s %(hostname)s' % context)
|
||||||
|
|
||||||
|
def get_context(self, vps):
|
||||||
|
return {
|
||||||
|
'object_id': vps.id,
|
||||||
|
'hostname': vps.hostname,
|
||||||
}
|
}
|
||||||
return replace(context, "'", '"')
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class VPSCreationForm(forms.ModelForm):
|
|
||||||
password1 = forms.CharField(label=_("Password"),
|
|
||||||
widget=forms.PasswordInput)
|
|
||||||
password2 = forms.CharField(label=_("Password confirmation"),
|
|
||||||
widget=forms.PasswordInput,
|
|
||||||
help_text=_("Enter the same password as above, for verification."))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
fields = ('username', 'account', 'type', 'template')
|
|
||||||
|
|
||||||
def clean_password2(self):
|
|
||||||
password1 = self.cleaned_data.get("password1")
|
|
||||||
password2 = self.cleaned_data.get("password2")
|
|
||||||
if password1 and password2 and password1 != password2:
|
|
||||||
msg = _("The two password fields didn't match.")
|
|
||||||
raise forms.ValidationError(msg)
|
|
||||||
return password2
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
vps = super(VPSCreationForm, self).save(commit=False)
|
|
||||||
vps.set_password(self.cleaned_data["password1"])
|
|
||||||
if commit:
|
|
||||||
vps.save()
|
|
||||||
return vps
|
|
||||||
|
|
||||||
|
|
||||||
class VPSChangeForm(forms.ModelForm):
|
|
||||||
password = ReadOnlyPasswordHashField(label=_("Password"),
|
|
||||||
help_text=_("Raw passwords are not stored, so there is no way to see "
|
|
||||||
"this user's password, but you can change the password "
|
|
||||||
"using <a href=\"password/\">this form</a>."))
|
|
||||||
|
|
||||||
def clean_password(self):
|
|
||||||
return self.initial["password"]
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('vps', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='vps',
|
||||||
|
name='password',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vps',
|
||||||
|
name='template',
|
||||||
|
field=models.CharField(default='debian7', verbose_name='template', choices=[('debian7', 'Debian 7 - Wheezy')], max_length=64, help_text='Initial template.'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -13,9 +13,8 @@ class VPS(models.Model):
|
||||||
type = models.CharField(_("type"), max_length=64, choices=settings.VPS_TYPES,
|
type = models.CharField(_("type"), max_length=64, choices=settings.VPS_TYPES,
|
||||||
default=settings.VPS_DEFAULT_TYPE)
|
default=settings.VPS_DEFAULT_TYPE)
|
||||||
template = models.CharField(_("template"), max_length=64,
|
template = models.CharField(_("template"), max_length=64,
|
||||||
choices=settings.VPS_TEMPLATES, default=settings.VPS_DEFAULT_TEMPLATE)
|
choices=settings.VPS_TEMPLATES, default=settings.VPS_DEFAULT_TEMPLATE,
|
||||||
password = models.CharField(_('password'), max_length=128,
|
help_text=_("Initial template."))
|
||||||
help_text=_("<TT>root</TT> password of this virtual machine"))
|
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
related_name='vpss')
|
related_name='vpss')
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ class UserCreationForm(forms.ModelForm):
|
||||||
class UserChangeForm(forms.ModelForm):
|
class UserChangeForm(forms.ModelForm):
|
||||||
password = auth_forms.ReadOnlyPasswordHashField(label=_("Password"),
|
password = auth_forms.ReadOnlyPasswordHashField(label=_("Password"),
|
||||||
help_text=_("Raw passwords are not stored, so there is no way to see "
|
help_text=_("Raw passwords are not stored, so there is no way to see "
|
||||||
"this user's password, but you can change the password "
|
"this user's password, but you can change it by "
|
||||||
"using <a href=\"password/\">this form</a>."))
|
"using <a href=\"password/\">this form</a>."))
|
||||||
|
|
||||||
def clean_password(self):
|
def clean_password(self):
|
||||||
|
@ -70,6 +70,13 @@ class UserChangeForm(forms.ModelForm):
|
||||||
return self.initial["password"]
|
return self.initial["password"]
|
||||||
|
|
||||||
|
|
||||||
|
class NonStoredUserChangeForm(forms.ModelForm):
|
||||||
|
password = forms.CharField(label=_("Password"), required=False,
|
||||||
|
widget=SpanWidget(display='<strong>Unknown password</strong>'),
|
||||||
|
help_text=_("This service's password is not stored, so there is no way to see it, "
|
||||||
|
"but you can change it using <a href=\"password/\">this form</a>."))
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyFormMixin(object):
|
class ReadOnlyFormMixin(object):
|
||||||
"""
|
"""
|
||||||
Mixin class for ModelForm or Form that provides support for SpanField on readonly fields
|
Mixin class for ModelForm or Form that provides support for SpanField on readonly fields
|
||||||
|
|
|
@ -142,7 +142,11 @@ def run(command, display=False, valid_codes=(0,), silent=False, stdin=b'', async
|
||||||
|
|
||||||
def sshrun(addr, command, *args, executable='bash', persist=False, **kwargs):
|
def sshrun(addr, command, *args, executable='bash', persist=False, **kwargs):
|
||||||
from .. import settings
|
from .. import settings
|
||||||
options = ['stricthostkeychecking=no']
|
options = [
|
||||||
|
'stricthostkeychecking=no',
|
||||||
|
'BatchMode=yes',
|
||||||
|
'EscapeChar=none',
|
||||||
|
]
|
||||||
if persist:
|
if persist:
|
||||||
options.extend((
|
options.extend((
|
||||||
'ControlMaster=auto',
|
'ControlMaster=auto',
|
||||||
|
|
Loading…
Reference in New Issue