Commit 1ab77c1f by Szeberényi Imre

Merge branch 'master' into 'smallville_fix'

# Conflicts:
#   circle/vm/tasks/local_periodic_tasks.py
parents fa743214 76a2a4a6
Pipeline #1391 failed with stage
in 0 seconds
......@@ -8,6 +8,8 @@
*.swp
*.swo
*~
.vscode
.idea
# Sphinx docs:
build
......
......@@ -495,6 +495,7 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
},
'required_attributes': required_attrs,
'optional_attributes': optional_attrs,
'want_response_signed': False,
},
},
'metadata': {'local': [remote_metadata], },
......@@ -576,7 +577,7 @@ SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^
MAX_NODE_RAM = get_env_variable("MAX_NODE_RAM", 1024)
MAX_NODE_CPU_CORE = get_env_variable("MAX_NODE_CPU_CORE", 10)
SCHEDULER_METHOD = get_env_variable("SCHEDULER_METHOD", 'random')
SCHEDULER_METHOD = get_env_variable("SCHEDULER_METHOD", 'advanced')
# Url to download the client: (e.g. http://circlecloud.org/client/download/)
CLIENT_DOWNLOAD_URL = get_env_variable('CLIENT_DOWNLOAD_URL', 'http://circlecloud.org/client/download/')
......@@ -590,3 +591,12 @@ REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
SSHKEY_EMAIL_ADD_KEY = False
TWO_FACTOR_ISSUER = get_env_variable("TWO_FACTOR_ISSUER", "CIRCLE")
# Default value is every day at midnight
AUTO_MIGRATION_CRONTAB = get_env_variable("AUTO_MIGRATION_CRONTAB", "0 0 * * *")
AUTO_MIGRATION_TIME_LIMIT_IN_HOURS = (
get_env_variable("AUTO_MIGRATION_TIME_LIMIT_IN_HOURS", "2"))
# Maximum time difference until the monitor's values get valid
SCHEDULER_TIME_SENSITIVITY_IN_SECONDS = (
get_env_variable("SCHEDULER_TIME_SENSITIVITY_IN_SECONDS", "60"))
......@@ -65,7 +65,10 @@
"modified": "2014-02-19T21:11:34.671Z",
"priority": 1,
"traits": [],
"host": 1
"host": 1,
"ram_weight": 1.0,
"cpu_weight": 1.0,
"time_stamp": "2017-12-13T21:08:08.819Z"
}
}
]
......@@ -17,56 +17,51 @@
from __future__ import absolute_import
from datetime import timedelta
from urlparse import urlparse
import os
import pyotp
from django.forms import ModelForm
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
PasswordChangeForm,
)
from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
from dal import autocomplete
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
Layout, Div, BaseInput, Field, HTML, Submit, TEMPLATE_PACK, Fieldset
)
from crispy_forms.utils import render_field
from crispy_forms.bootstrap import FormActions
from dal import autocomplete
from datetime import timedelta
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
PasswordChangeForm,
)
from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.urlresolvers import reverse_lazy
from django.core.validators import URLValidator
from django.forms import ModelForm
from django.forms.widgets import TextInput, HiddenInput
from django.template.loader import render_to_string
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
from django_sshkey.models import UserKey
from circle.settings.base import LANGUAGES, MAX_NODE_RAM, MAX_NODE_CPU_CORE
from dashboard.models import ConnectCommand, create_profile
from dashboard.store_api import Store
from firewall.models import Vlan, Host
from storage.models import DataStore, Disk
from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
)
from storage.models import DataStore, Disk
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission
from .models import Profile, GroupProfile, Message
from circle.settings.base import LANGUAGES, MAX_NODE_RAM, MAX_NODE_CPU_CORE
from django.utils.translation import string_concat
from .validators import domain_validator
from dashboard.models import ConnectCommand, create_profile
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES)
......@@ -189,7 +184,7 @@ class VmCustomizeForm(forms.Form):
self.initial['ram_size'] = self.template.ram_size
else:
self.allowed_fields = ("name", "template", "customized", )
self.allowed_fields = ("name", "template", "customized",)
# initial name and template pk
self.initial['name'] = self.template.name
......@@ -214,7 +209,6 @@ class VmCustomizeForm(forms.Form):
class GroupCreateForm(NoFormTagMixin, forms.ModelForm):
description = forms.CharField(label=_("Description"), required=False,
widget=forms.Textarea(attrs={'rows': 3}))
......@@ -258,7 +252,7 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm):
class Meta:
model = Group
fields = ('name', )
fields = ('name',)
class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm):
......@@ -276,6 +270,7 @@ class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm):
label=_('Directory identifier'))
if not new_groups:
self.fields['org_id'].widget = HiddenInput()
self.fields['disk_quota'].widget = HiddenInput()
self.fields['description'].widget = forms.Textarea(attrs={'rows': 3})
@property
......@@ -293,7 +288,7 @@ class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm):
class Meta:
model = GroupProfile
fields = ('description', 'org_id')
fields = ('description', 'org_id', 'disk_quota')
class HostForm(NoFormTagMixin, forms.ModelForm):
......@@ -513,7 +508,7 @@ class TemplateForm(forms.ModelForm):
self.allowed_fields += tuple(set(self.fields.keys()) -
set(['raw_data']))
if self.user.is_superuser:
self.allowed_fields += ('raw_data', )
self.allowed_fields += ('raw_data',)
for name, field in self.fields.items():
if name not in self.allowed_fields:
field.widget.attrs['disabled'] = 'disabled'
......@@ -525,8 +520,8 @@ class TemplateForm(forms.ModelForm):
self.initial['max_ram_size'] = 512
lease_queryset = (
Lease.get_objects_with_level("operator", self.user).distinct() |
Lease.objects.filter(pk=self.instance.lease_id).distinct())
Lease.get_objects_with_level("operator", self.user).distinct() |
Lease.objects.filter(pk=self.instance.lease_id).distinct())
self.fields["lease"].queryset = lease_queryset
......@@ -602,7 +597,7 @@ class TemplateForm(forms.ModelForm):
class Meta:
model = InstanceTemplate
exclude = ('state', 'disks', )
exclude = ('state', 'disks',)
widgets = {
'system': forms.TextInput,
'max_ram_size': forms.HiddenInput,
......@@ -745,7 +740,6 @@ class LeaseForm(forms.ModelForm):
class VmRenewForm(OperationForm):
force = forms.BooleanField(required=False, label=_(
"Set expiration times even if they are shorter than "
"the current value."))
......@@ -785,11 +779,10 @@ class VmMigrateForm(forms.Form):
class VmStateChangeForm(OperationForm):
interrupt = forms.BooleanField(required=False, label=_(
"Forcibly interrupt all running activities."),
help_text=_("Set all activities to finished state, "
"but don't interrupt any tasks."))
help_text=_("Set all activities to finished state, "
"but don't interrupt any tasks."))
new_state = forms.ChoiceField(Instance.STATUS, label=_(
"New status"))
reset_node = forms.BooleanField(required=False, label=_("Reset node"))
......@@ -830,6 +823,41 @@ class VmCreateDiskForm(OperationForm):
return size_in_bytes
class VmDiskExportForm(OperationForm):
exported_name = forms.CharField(max_length=100, label=_('Filename'))
disk_format = forms.ChoiceField(
choices=Disk.EXPORT_FORMATS,
label=_('Format'))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.disk = kwargs.pop('default')
super(VmDiskExportForm, self).__init__(*args, **kwargs)
self.fields['disk'] = forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk'))
if self.disk:
self.fields['disk'].widget = HiddenInput()
@property
def helper(self):
helper = super(VmDiskExportForm, self).helper
if self.disk:
helper.layout = Layout(
AnyTag(
"div",
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
css_class="form-group",
),
Field('disk'),
Field('exported_name'),
Field('disk_format')
)
return helper
class VmDiskResizeForm(OperationForm):
size = forms.CharField(
widget=FileSizeWidget, initial=(10 << 30), label=_('Size'),
......@@ -858,7 +886,7 @@ class VmDiskResizeForm(OperationForm):
" GB or MB!"))
if int(size_in_bytes) < int(disk.size):
raise forms.ValidationError(_("Disk size must be greater than the "
"actual size."))
"actual size."))
return cleaned_data
@property
......@@ -899,6 +927,20 @@ class VmDiskRemoveForm(OperationForm):
return helper
class VmImportDiskForm(OperationForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(VmImportDiskForm, self).__init__(*args, **kwargs)
disk_paths = Store(self.user).get_disk_images()
disk_filenames = [os.path.basename(item) for item in disk_paths]
self.choices = zip(disk_paths, disk_filenames)
self.fields['name'] = forms.CharField(max_length=100, label=_('Name'))
self.fields['disk_path'] = forms.ChoiceField(label=_('Disk image'),
choices=self.choices)
class VmDownloadDiskForm(OperationForm):
name = forms.CharField(max_length=100, label=_("Name"), required=False)
url = forms.CharField(label=_('URL'), validators=[URLValidator(), ])
......@@ -1138,7 +1180,6 @@ class CircleSetPasswordForm(SetPasswordForm):
class LinkButton(BaseInput):
"""
Used to create a link button descriptor for the {% crispy %} template tag::
......@@ -1224,7 +1265,7 @@ class MyProfileForm(forms.ModelForm):
class Meta:
fields = ('preferred_language', 'email_notifications',
'desktop_notifications', 'use_gravatar', )
'desktop_notifications', 'use_gravatar',)
model = Profile
@property
......@@ -1239,9 +1280,8 @@ class MyProfileForm(forms.ModelForm):
class UnsubscribeForm(forms.ModelForm):
class Meta:
fields = ('email_notifications', )
fields = ('email_notifications',)
model = Profile
@property
......@@ -1299,6 +1339,9 @@ class UserEditForm(forms.ModelForm):
instance_limit = forms.IntegerField(
label=_('Instance limit'),
min_value=0, widget=NumberInput)
template_instance_limit = forms.IntegerField(
label=_('Template instance limit'),
min_value=0, widget=NumberInput)
two_factor_secret = forms.CharField(
label=_('Two-factor authentication secret'),
help_text=_("Remove the secret key to disable two-factor "
......@@ -1308,20 +1351,25 @@ class UserEditForm(forms.ModelForm):
super(UserEditForm, self).__init__(*args, **kwargs)
self.fields["instance_limit"].initial = (
self.instance.profile.instance_limit)
self.fields["template_instance_limit"].initial = (
self.instance.profile.template_instance_limit)
self.fields["two_factor_secret"].initial = (
self.instance.profile.two_factor_secret)
class Meta:
model = User
fields = ('email', 'first_name', 'last_name', 'instance_limit',
'is_active', "two_factor_secret", )
fields = ('email', 'first_name', 'last_name',
'instance_limit', 'template_instance_limit',
'is_active', 'two_factor_secret',)
def save(self, commit=True):
user = super(UserEditForm, self).save()
user.profile.instance_limit = (
self.cleaned_data['instance_limit'] or None)
self.cleaned_data['instance_limit'] or None)
user.profile.template_instance_limit = (
self.cleaned_data['template_instance_limit'] or None)
user.profile.two_factor_secret = (
self.cleaned_data['two_factor_secret'] or None)
self.cleaned_data['two_factor_secret'] or None)
user.profile.save()
return user
......@@ -1405,10 +1453,9 @@ class ConnectCommandForm(forms.ModelForm):
class TraitsForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('req_traits', )
fields = ('req_traits',)
@property
def helper(self):
......@@ -1428,7 +1475,7 @@ class RawDataForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('raw_data', )
fields = ('raw_data',)
@property
def helper(self):
......@@ -1490,7 +1537,7 @@ class GroupPermissionForm(forms.ModelForm):
class Meta:
model = Group
fields = ('permissions', )
fields = ('permissions',)
@property
def helper(self):
......@@ -1538,7 +1585,7 @@ class VmResourcesForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('num_cores', 'priority', 'ram_size', )
fields = ('num_cores', 'priority', 'ram_size',)
class VmRenameForm(forms.Form):
......@@ -1629,7 +1676,7 @@ class DataStoreForm(ModelForm):
class Meta:
model = DataStore
fields = ("name", "path", "hostname", )
fields = ("name", "path", "hostname",)
class DiskForm(ModelForm):
......@@ -1647,7 +1694,7 @@ class DiskForm(ModelForm):
class Meta:
model = Disk
fields = ("name", "filename", "datastore", "type", "bus", "size",
"base", "dev_num", "destroyed", "is_ready", )
"base", "dev_num", "destroyed", "is_ready",)
class MessageForm(ModelForm):
......@@ -1697,3 +1744,11 @@ class TwoFactorConfirmationForm(forms.Form):
totp = pyotp.TOTP(self.user.profile.two_factor_secret)
if not totp.verify(self.cleaned_data.get('confirmation_code')):
raise ValidationError(_("Invalid confirmation code."))
class AutoMigrationForm(forms.Form):
minute = forms.CharField()
hour = forms.CharField()
day_of_month = forms.CharField()
month_of_year = forms.CharField()
day_of_week = forms.CharField()
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2020-04-24 20:00
from __future__ import unicode_literals
from django.db import migrations
import sizefield.models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0006_auto_20170707_1909'),
]
operations = [
migrations.AddField(
model_name='groupprofile',
name='disk_quota',
field=sizefield.models.FileSizeField(default=2147483648, help_text='Disk quota in mebibytes.', verbose_name='disk quota'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2020-11-06 13:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0007_groupprofile_disk_quota'),
]
operations = [
migrations.AddField(
model_name='profile',
name='template_instance_limit',
field=models.IntegerField(default=1),
),
]
......@@ -50,7 +50,7 @@ from common.models import HumanReadableObject, create_readable, Encoder
from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException, Timeout
from .store_api import Store, NoStoreException, NotOkException
from .validators import connect_command_template_validator
logger = getLogger(__name__)
......@@ -162,7 +162,7 @@ class ConnectCommand(Model):
validators=[connect_command_template_validator])
class Meta:
ordering = ('id', )
ordering = ('id',)
def __unicode__(self):
return self.template
......@@ -178,6 +178,7 @@ class Profile(Model):
unique=True, blank=True, null=True, max_length=64,
help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5)
template_instance_limit = IntegerField(default=1)
use_gravatar = BooleanField(
verbose_name=_("Use Gravatar"), default=True,
help_text=_("Whether to use email address as Gravatar profile image"))
......@@ -218,7 +219,7 @@ class Profile(Model):
'id': command.id,
'cmd': command.template % {
'port': instance.get_connect_port(use_ipv6=use_ipv6),
'host': instance.get_connect_host(use_ipv6=use_ipv6),
'host': instance.get_connect_host(use_ipv6=use_ipv6),
'password': instance.pw,
'username': 'cloud',
}} for command in commands]
......@@ -263,7 +264,7 @@ class Profile(Model):
super(Profile, self).save(*args, **kwargs)
class Meta:
ordering = ('id', )
ordering = ('id',)
permissions = (
('use_autocomplete', _('Can use autocomplete.')),
)
......@@ -275,7 +276,7 @@ class FutureMember(Model):
group = ForeignKey(Group)
class Meta:
ordering = ('id', )
ordering = ('id',)
unique_together = ('org_id', 'group')
def __unicode__(self):
......@@ -293,9 +294,13 @@ class GroupProfile(AclBase):
unique=True, blank=True, null=True, max_length=64,
help_text=_('Unique identifier of the group at the organization.'))
description = TextField(blank=True)
disk_quota = FileSizeField(
verbose_name=_('disk quota'),
default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.'))
class Meta:
ordering = ('id', )
ordering = ('id',)
def __unicode__(self):
return self.group.name
......@@ -331,7 +336,11 @@ def create_profile(user):
profile, created = Profile.objects.get_or_create(user=user)
try:
Store(user).create_user(profile.smb_password, None, profile.disk_quota)
store = Store(user)
quotas = [profile.disk_quota]
quotas += [group.profile.disk_quota for group in user.groups.all()]
max_quota = max(quotas)
store.create_user(profile.smb_password, None, max_quota)
except:
logger.exception("Can't create user %s", unicode(user))
return created
......@@ -347,6 +356,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
logger.debug("Register save_org_id to djangosaml2 pre_user_save")
from djangosaml2.signals import pre_user_save
def save_org_id(sender, instance, attributes, **kwargs):
logger.debug("save_org_id called by %s", instance.username)
atr = settings.SAML_ORG_ID_ATTRIBUTE
......@@ -399,6 +409,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
return False # User did not change
pre_user_save.connect(save_org_id)
......@@ -411,7 +422,7 @@ def update_store_profile(sender, **kwargs):
profile.disk_quota)
except NoStoreException:
logger.debug("Store is not available.")
except (NotOkException, Timeout):
except NotOkException:
logger.critical("Store is not accepting connections.")
......
......@@ -41,12 +41,14 @@ $(function() {
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary');
}
});
e.preventDefault();
});
/* if the operation fails show the modal again */
$("body").on("click", "#confirmation-modal #op-form-send", function() {
var url = $(this).closest("form").prop("action");
......@@ -237,4 +239,3 @@ String.prototype.hashCode = function() {
}
return hash;
};
......@@ -558,3 +558,5 @@ $(function() {
inputs.prop("checked", !inputs.prop("checked"));
});
});
$.fn.modal.Constructor.prototype.enforceFocus = function() {};
......@@ -1079,6 +1079,10 @@ textarea[name="new_members"] {
max-width: 100%;
}
#node-list-auto-migration-body {
padding: 20px;
}
#vm-list-table td.state,
#vm-list-table td.memory {
white-space: nowrap;
......@@ -1088,7 +1092,7 @@ textarea[name="new_members"] {
vertical-align: middle;
}
.disk-resize-btn {
.disk-resize-btn, .disk-export-btn {
margin-right: 5px;
}
......
......@@ -3,4 +3,11 @@ $(function() {
// find disabled nodes, set danger (red) on the rows
$('.node-disabled').closest("tr").addClass('danger');
});
$('#reschedule-now').click(function() {
$.get($(this).attr('href'), function(data){
highlight = data.result === 'ok' ? 'success' : 'danger';
addMessage(data.message, highlight);
});
return false;
});
});
......@@ -14,19 +14,20 @@
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from os.path import splitext
import json
import logging
from urlparse import urljoin
from datetime import datetime
from django.http import Http404
import os
from datetime import datetime
from django.conf import settings
from django.http import Http404
from os.path import splitext
from requests import get, post, codes
from requests.exceptions import Timeout # noqa
from sizefield.utils import filesizeformat
from storage.models import Disk
logger = logging.getLogger(__name__)
......@@ -47,6 +48,17 @@ class NoStoreException(StoreApiException):
class Store(object):
def __init__(self, user, default_timeout=0.5):
self.store_url = settings.STORE_URL
if not self.store_url:
raise NoStoreException
if user.is_superuser and not user.profile.org_id:
self.username = 'u-admin'
elif not user.profile.org_id:
raise NoStoreException
else:
self.username = 'u-%s' % user.profile.org_id
self.request_args = {'verify': settings.STORE_VERIFY_SSL}
if settings.STORE_SSL_AUTH:
self.request_args['cert'] = (settings.STORE_CLIENT_CERT,
......@@ -54,18 +66,15 @@ class Store(object):
if settings.STORE_BASIC_AUTH:
self.request_args['auth'] = (settings.STORE_CLIENT_USER,
settings.STORE_CLIENT_PASSWORD)
self.username = "u-%d" % user.pk
self.default_timeout = default_timeout
self.store_url = settings.STORE_URL
if not self.store_url:
raise NoStoreException
def _request(self, url, method=get, timeout=None,
raise_status_code=True, **kwargs):
url = urljoin(self.store_url, url)
if timeout is None:
timeout = self.default_timeout
payload = json.dumps(kwargs) if kwargs else None
kwargs['USER'] = self.username
payload = json.dumps(kwargs)
try:
headers = {'content-type': 'application/json'}
response = method(url, data=payload, headers=headers,
......@@ -83,7 +92,7 @@ class Store(object):
return response
def _request_cmd(self, cmd, **kwargs):
return self._request(self.username, post, CMD=cmd, **kwargs)
return self._request("/user/", post, CMD=cmd, **kwargs)
def list(self, path, process=True):
r = self._request_cmd("LIST", PATH=path)
......@@ -101,13 +110,22 @@ class Store(object):
else:
return result
def get_disk_images(self, path='/'):
images = []
file_list = self.list(path, process=False)
export_formats = [item[0] for item in Disk.EXPORT_FORMATS]
for item in file_list:
if os.path.splitext(item['NAME'])[1].strip('.') in export_formats:
images.append(os.path.join(path, item['NAME']))
return images
def request_download(self, path):
r = self._request_cmd("DOWNLOAD", PATH=path, timeout=10)
return r.json()['LINK']
def request_upload(self, path):
r = self._request_cmd("UPLOAD", PATH=path)
return r.json()['LINK']
r = self._request_cmd("UPLOAD", PATH=path)
return r.json()['LINK']
def remove(self, path):
self._request_cmd("REMOVE", PATH=path)
......@@ -119,7 +137,7 @@ class Store(object):
self._request_cmd("RENAME", PATH=old_path, NEW_NAME=new_name)
def get_quota(self): # no CMD? :o
r = self._request(self.username)
r = self._request("/user/")
quota = r.json()
quota.update({
'readable_used': filesizeformat(float(quota['used'])),
......@@ -129,17 +147,17 @@ class Store(object):
return quota
def set_quota(self, quota):
self._request("/quota/" + self.username, post, QUOTA=quota)
self._request("/quota/", post, QUOTA=quota)