Added preliminar functional tests for systemusers

This commit is contained in:
Marc 2014-10-01 16:42:40 +00:00
parent f984d28709
commit 276c02c2fd
17 changed files with 311 additions and 181 deletions

12
TODO.md
View File

@ -142,3 +142,15 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* Redirect junk emails and delete every 30 days? * Redirect junk emails and delete every 30 days?
* Complitely decouples scripts execution, billing, service definition * Complitely decouples scripts execution, billing, service definition
* Create SystemUser on account creation. username=username, is_main=True,
* Exclude is_main=True from queryset filter default is_main=False
* self referencing group.
* Unify all users
* backend message with link
* test fucking user
* delete main user -> delete account or prevent delete main user

View File

@ -1,121 +0,0 @@
import textwrap
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from . import settings
# TODO create a base backend for SystemUsers!
class MainUserBackend(ServiceController):
verbose_name = _("Main user")
model = 'accounts.Account'
ignore_fields = ['last_login']
def save(self, user):
context = self.get_context(user)
self.append(textwrap.dedent("""
if [[ $( id %(username)s ) ]]; then
usermod --password '%(password)s' %(username)s
else
useradd %(username)s --password '%(password)s' --shell %(shell)s
fi
mkdir -p %(home)s
chown %(username)s.%(username)s %(home)s""" % context
))
def delete(self, user):
context = self.get_context(user)
self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context)
self.append("killall -u %(username)s" % context)
self.append("userdel %(username)s" % context)
def get_context(self, user):
context = {
'username': user.username,
'password': user.password if user.is_active else '*%s' % user.password,
'shell': getattr(user, 'shell', settings.ACCOUNTS_DEFAULT_SHELL)
}
context['home'] = settings.ACCOUNTS_HOME % context
return context
class MainUserDisk(ServiceMonitor):
model = 'accounts.Account'
resource = ServiceMonitor.DISK
verbose_name = _('Main user disk')
def monitor(self, user):
context = self.get_context(user)
self.append("du -s %(home)s | xargs echo %(object_id)s" % context)
def get_context(self, user):
context = SystemUserBackend().get_context(user)
context['object_id'] = user.pk
return context
class MainFTPTraffic(ServiceMonitor):
model = 'accounts.Account'
resource = ServiceMonitor.TRAFFIC
verbose_name = _('Main FTP traffic')
def prepare(self):
current_date = timezone.localtime(self.current_date)
current_date = current_date.strftime("%Y%m%d%H%M%S")
self.append(textwrap.dedent("""
function monitor () {
OBJECT_ID=$1
INI_DATE=$2
USERNAME="$3"
LOG_FILE="$4"
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \\
| grep " \\[${USERNAME}\\] " \\
| awk -v ini="${INI_DATE}" '
BEGIN {
end = "%s"
sum = 0
months["Jan"] = "01"
months["Feb"] = "02"
months["Mar"] = "03"
months["Apr"] = "04"
months["May"] = "05"
months["Jun"] = "06"
months["Jul"] = "07"
months["Aug"] = "08"
months["Sep"] = "09"
months["Oct"] = "10"
months["Nov"] = "11"
months["Dec"] = "12"
} {
# log: Fri Jul 11 13:23:17 2014
split($4, t, ":")
# line_date = year month day hour minute second
line_date = $5 months[$2] $3 t[1] t[2] t[3]
if ( line_date > ini && line_date < end)
split($0, l, "\\", ")
split(l[3], b, " ")
sum += b[1]
} END {
print sum
}
' | xargs echo ${OBJECT_ID}
}""" % current_date))
def monitor(self, user):
context = self.get_context(user)
self.append(
'monitor %(object_id)i %(last_date)s "%(username)s" "%(log_file)s"' % context)
def get_context(self, user):
last_date = timezone.localtime(self.get_last_date(user.pk))
return {
'log_file': settings.ACCOUNTS_FTP_LOG_PATH,
'last_date': last_date.strftime("%Y%m%d%H%M%S"),
'object_id': user.pk,
'username': user.username,
}

View File

@ -62,8 +62,8 @@ class Account(auth.AbstractBaseUser):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
created = not self.pk created = not self.pk
super(Account, self).save(*args, **kwargs) super(Account, self).save(*args, **kwargs)
if created and hasattr(self, 'systemgroups'): if created and hasattr(self, 'systemusers'):
self.systemgroups.create(name=self.username, account=self) self.systemusers.create_user(self.username, account=self, password=self.password, is_main=True)
def send_email(self, template, context, contacts=[], attachments=[], html=None): def send_email(self, template, context, contacts=[], attachments=[], html=None):
contacts = self.contacts.filter(email_usages=contacts) contacts = self.contacts.filter(email_usages=contacts)

View File

@ -13,5 +13,5 @@ class AccountSerializer(serializers.HyperlinkedModelSerializer):
class AccountSerializerMixin(object): class AccountSerializerMixin(object):
def save_object(self, obj, **kwargs): def save_object(self, obj, **kwargs):
obj.account = self.context['request'].user.account obj.account = self.context['request'].user
super(AccountSerializerMixin, self).save_object(obj, **kwargs) super(AccountSerializerMixin, self).save_object(obj, **kwargs)

View File

@ -22,12 +22,3 @@ ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en')
ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1) ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1)
ACCOUNTS_HOME = getattr(settings, 'ACCOUNTS_HOME', '/home/%(username)s')
ACCOUNTS_FTP_LOG_PATH = getattr(settings, 'ACCOUNTS_FTP_LOG_PATH', '/var/log/vsftpd.log')
ACCOUNTS_DEFAULT_SHELL = getattr(settings, 'ACCOUNTS_DEFAULT_SHELL', '/bin/false')

View File

@ -230,6 +230,7 @@ class AdminDomainMixin(DomainTestMixin):
return value_input return value_input
def add(self, domain_name, records): def add(self, domain_name, records):
# TODO use reverse
url = self.live_server_url + '/admin/domains/domain/add/' url = self.live_server_url + '/admin/domains/domain/add/'
self.selenium.get(url) self.selenium.get(url)
name = self.selenium.find_element_by_id('id_name') name = self.selenium.find_element_by_id('id_name')

View File

@ -1,3 +1,4 @@
import logging
import threading import threading
from django import db from django import db
@ -8,6 +9,9 @@ from . import settings
from .helpers import send_report from .helpers import send_report
logger = logging.getLogger(__name__)
def as_task(execute): def as_task(execute):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
with db.transaction.commit_manually(): with db.transaction.commit_manually():
@ -37,6 +41,7 @@ def execute(operations):
scripts = {} scripts = {}
cache = {} cache = {}
for operation in operations: for operation in operations:
logger.info("Queued %s" % str(operation))
servers = router.get_servers(operation, cache=cache) servers = router.get_servers(operation, cache=cache)
for server in servers: for server in servers:
key = (server, operation.backend) key = (server, operation.backend)
@ -64,7 +69,9 @@ def execute(operations):
logs = [] logs = []
for execution, operations in executions: for execution, operations in executions:
for operation in operations: for operation in operations:
logger.info("Executed %s" % str(operation))
operation.log = execution.log operation.log = execution.log
operation.save() operation.save()
logger.info(execution.log.stderr)
logs.append(execution.log) logs.append(execution.log)
return logs return logs

View File

@ -48,7 +48,7 @@ class SystemUserAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
# derived from monkeypatching formfield.widget.render on AccountAdminMinxin, # derived from monkeypatching formfield.widget.render on AccountAdminMinxin,
# don't ask. # don't ask.
formfield = form.base_fields['groups'] formfield = form.base_fields['groups']
formfield.queryset = formfield.queryset.exclude(name=obj.username) formfield.queryset = formfield.queryset.exclude(id=obj.id)
return form return form

View File

@ -10,6 +10,7 @@ from .serializers import SystemUserSerializer
class SystemUserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet): class SystemUserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
model = SystemUser model = SystemUser
serializer_class = SystemUserSerializer serializer_class = SystemUserSerializer
filter_fields = ('username',)
router.register(r'systemusers', SystemUserViewSet) router.register(r'systemusers', SystemUserViewSet)

View File

@ -1,17 +1,132 @@
import os
import textwrap import textwrap
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.backends import MainUserBackend, MainFTPTraffic from orchestra.apps.orchestration import ServiceController
from orchestra.apps.resources import ServiceMonitor
from . import settings
class SystemUserBackend(MainUserBackend): class SystemUserBackend(ServiceController):
verbose_name = _("System user") verbose_name = _("System user")
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
ignore_fields = []
def save(self, user):
context = self.get_context(user)
groups = ','.join(self.get_groups(user))
context['groups_arg'] = '--groups %s' % groups if groups else ''
self.append(textwrap.dedent("""
if [[ $( id %(username)s ) ]]; then
usermod %(username)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
else
useradd %(username)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
usermod -a -G %(username)s %(mainusername)s
fi
mkdir -p %(home)s
chown %(username)s.%(username)s %(home)s""" % context
))
def delete(self, user):
context = self.get_context(user)
self.append("{ sleep 2 && killall -u %(username)s -s KILL; } &" % context)
self.append("killall -u %(username)s || true" % context)
self.append("userdel %(username)s || true" % context)
self.append("groupdel %(username)s || true" % context)
def get_groups(self, user):
if user.is_main:
return user.account.systemusers.exclude(id=user.id).values_list('username', flat=True)
groups = list(user.groups.values_list('username', flat=True))
return groups
def get_context(self, user):
context = {
'username': user.username,
'password': user.password if user.active else '*%s' % user.password,
'shell': user.shell,
'mainusername': user.username if user.is_main else user.account.username,
}
basehome = settings.SYSTEMUSERS_HOME % context
context['home'] = os.path.join(basehome, user.home)
return context
class SystemUserFTPTraffic(MainFTPTraffic): class SystemUserDisk(ServiceMonitor):
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
verbose_name = _('System user FTP traffic') resource = ServiceMonitor.DISK
verbose_name = _('Main user disk')
def monitor(self, user):
context = self.get_context(user)
self.append("du -s %(home)s | xargs echo %(object_id)s" % context)
def get_context(self, user):
context = SystemUserBackend().get_context(user)
context['object_id'] = user.pk
return context
class FTPTraffic(ServiceMonitor):
model = 'systemusers.SystemUser'
resource = ServiceMonitor.TRAFFIC
verbose_name = _('Main FTP traffic')
def prepare(self):
current_date = timezone.localtime(self.current_date)
current_date = current_date.strftime("%Y%m%d%H%M%S")
self.append(textwrap.dedent("""
function monitor () {
OBJECT_ID=$1
INI_DATE=$2
USERNAME="$3"
LOG_FILE="$4"
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \\
| grep " \\[${USERNAME}\\] " \\
| awk -v ini="${INI_DATE}" '
BEGIN {
end = "%s"
sum = 0
months["Jan"] = "01"
months["Feb"] = "02"
months["Mar"] = "03"
months["Apr"] = "04"
months["May"] = "05"
months["Jun"] = "06"
months["Jul"] = "07"
months["Aug"] = "08"
months["Sep"] = "09"
months["Oct"] = "10"
months["Nov"] = "11"
months["Dec"] = "12"
} {
# log: Fri Jul 11 13:23:17 2014
split($4, t, ":")
# line_date = year month day hour minute second
line_date = $5 months[$2] $3 t[1] t[2] t[3]
if ( line_date > ini && line_date < end)
split($0, l, "\\", ")
split(l[3], b, " ")
sum += b[1]
} END {
print sum
}
' | xargs echo ${OBJECT_ID}
}""" % current_date))
def monitor(self, user):
context = self.get_context(user)
self.append(
'monitor %(object_id)i %(last_date)s "%(username)s" "%(log_file)s"' % context)
def get_context(self, user):
last_date = timezone.localtime(self.get_last_date(user.pk))
return {
'log_file': settings.SYSTEMUSERS_FTP_LOG_PATH,
'last_date': last_date.strftime("%Y%m%d%H%M%S"),
'object_id': user.pk,
'username': user.username,
}

View File

@ -2,6 +2,7 @@ from django.contrib.auth.hashers import make_password
from django.core import validators from django.core import validators
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db import models from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services from orchestra.core import services
@ -28,10 +29,11 @@ class SystemUser(models.Model):
home = models.CharField(_("home"), max_length=256, blank=True, home = models.CharField(_("home"), max_length=256, blank=True,
help_text=_("Home directory relative to account's ~main_user")) help_text=_("Home directory relative to account's ~main_user"))
shell = models.CharField(_("shell"), max_length=32, shell = models.CharField(_("shell"), max_length=32,
choices=settings.USERS_SHELLS, default=settings.USERS_DEFAULT_SHELL) choices=settings.SYSTEMUSERS_SHELLS, default=settings.SYSTEMUSERS_DEFAULT_SHELL)
groups = models.ManyToManyField('systemusers.SystemGroup', blank=True, groups = models.ManyToManyField('self', blank=True,
help_text=_("A new group will be created for the user. " help_text=_("A new group will be created for the user. "
"Which additional groups would you like them to be a member of?")) "Which additional groups would you like them to be a member of?"))
is_main = models.BooleanField(_("is main"), default=False)
is_active = models.BooleanField(_("active"), default=True, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this account should be treated as active. " help_text=_("Designates whether this account should be treated as active. "
"Unselect this instead of deleting accounts.")) "Unselect this instead of deleting accounts."))
@ -41,37 +43,25 @@ class SystemUser(models.Model):
def __unicode__(self): def __unicode__(self):
return self.username return self.username
def clean(self):
""" unique usernames between accounts and system users """
if not self.pk:
field = self._meta.get_field_by_name('account')[0]
account_model = field.rel.to
if account_model.objects.filter(username=self.username).exists():
raise validators.ValidationError(self.error_messages['duplicate_username'])
def save(self, *args, **kwargs):
created = not self.pk
super(SystemUser, self).save(*args, **kwargs)
if created:
self.groups.create(name=self.username, account=self.account)
def set_password(self, raw_password): def set_password(self, raw_password):
self.password = make_password(raw_password) self.password = make_password(raw_password)
def get_is_active(self): @cached_property
return self.account.is_active and self.is_active def active(self):
return self.is_active and self.account.is_active
class SystemGroup(models.Model): ## TODO user deletion and group handling.
name = models.CharField(_("name"), max_length=64, unique=True, #class SystemGroup(models.Model):
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."), # name = models.CharField(_("name"), max_length=64, unique=True,
validators=[validators.RegexValidator(r'^[\w.-]+$', # help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
_("Enter a valid group name."), 'invalid')]) # validators=[validators.RegexValidator(r'^[\w.-]+$',
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), # _("Enter a valid group name."), 'invalid')])
related_name='systemgroups') # account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
# related_name='systemgroups')
def __unicode__(self): #
return self.name # def __unicode__(self):
# return self.name
services.register(SystemUser) services.register(SystemUser)

View File

@ -23,7 +23,7 @@ class SystemUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelS
def validate_password(self, attrs, source): def validate_password(self, attrs, source):
""" POST only password """ """ POST only password """
if self.object.pk: if self.object:
if 'password' in attrs: if 'password' in attrs:
raise serializers.ValidationError(_("Can not set password")) raise serializers.ValidationError(_("Can not set password"))
elif 'password' not in attrs: elif 'password' not in attrs:

View File

@ -3,11 +3,17 @@ from django.conf import settings
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
USERS_SHELLS = getattr(settings, 'USERS_SHELLS', ( SYSTEMUSERS_SHELLS = getattr(settings, 'SYSTEMUSERS_SHELLS', (
('/bin/false', _("No shell, FTP only")), ('/bin/false', _("No shell, FTP only")),
('/bin/rsync', _("No shell, SFTP/RSYNC only")), ('/bin/rsync', _("No shell, SFTP/RSYNC only")),
('/bin/bash', "/bin/bash"), ('/bin/bash', "/bin/bash"),
('/bin/sh', "/bin/sh"), ('/bin/sh', "/bin/sh"),
)) ))
USERS_DEFAULT_SHELL = getattr(settings, 'USERS_DEFAULT_SHELL', '/bin/false') SYSTEMUSERS_DEFAULT_SHELL = getattr(settings, 'SYSTEMUSERS_DEFAULT_SHELL', '/bin/false')
SYSTEMUSERS_HOME = getattr(settings, 'SYSTEMUSERS_HOME', '/home/%(username)s')
SYSTEMUSERS_FTP_LOG_PATH = getattr(settings, 'SYSTEMUSERS_FTP_LOG_PATH', '/var/log/vsftpd.log')

View File

@ -0,0 +1,126 @@
from functools import partial
from django.conf import settings
from django.core.urlresolvers import reverse
from orchestra.apps.accounts.models import Account
from orchestra.apps.orchestration.models import Server, Route
from orchestra.utils.system import run
from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii
from ... import backends
r = partial(run, silent=True, display=False)
class SystemUserMixin(object):
MASTER_ADDR = 'localhost'
ACCOUNT_USERNAME = '%s_account' % random_ascii(10)
DEPENDENCIES = (
'orchestra.apps.orchestration',
'orcgestra.apps.systemusers',
)
def setUp(self):
super(SystemUserMixin, self).setUp()
self.add_route()
def add_route(self):
master = Server.objects.create(name=self.MASTER_ADDR)
backend = backends.SystemUserBackend.get_name()
Route.objects.create(backend=backend, match=True, host=master)
def add(self):
raise NotImplementedError
def delete(self):
raise NotImplementedError
def update(self):
raise NotImplementedError
def test_create_systemuser(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%spppP001' % random_ascii(5)
self.add(username, password)
self.addCleanup(partial(self.delete, username))
self.assertEqual(0, r("id %s" % username).return_code)
# TODO test group membership and everything
def test_delete_systemuser(self):
username = '%s_systemuser' % random_ascii(10)
password = '@!?%sppppP001' % random_ascii(5)
self.add(username, password)
self.assertEqual(0, r("id %s" % username).return_code)
self.delete(username)
self.assertEqual(1, r("id %s" % username, error_codes=[0,1]).return_code)
class RESTSystemUserMixin(SystemUserMixin):
def setUp(self):
super(RESTSystemUserMixin, self).setUp()
self.rest_login()
def add(self, username, password):
self.rest.systemusers.create(username=username, password=password)
def delete(self, username):
user = self.rest.systemusers.retrieve(username=username).get()
user.delete()
def update(self):
pass
# TODO
class AdminSystemUserMixin(SystemUserMixin):
def setUp(self):
super(AdminSystemUserMixin, self).setUp()
self.admin_login()
def add(self, username, password):
pass
def delete(self, username):
pass
def update(self):
pass
class RESTSystemUserTest(RESTSystemUserMixin, BaseLiveServerTestCase):
pass
class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase):
def test_create_account(self):
url = self.live_server_url + reverse('admin:accounts_account_add')
self.selenium.get(url)
account_username = '%s_account' % random_ascii(10)
username = self.selenium.find_element_by_id('id_username')
username.send_keys(account_username)
account_password = '@!?%spppP001' % random_ascii(5)
password = self.selenium.find_element_by_id('id_password1')
password.send_keys(account_password)
password = self.selenium.find_element_by_id('id_password2')
password.send_keys(account_password)
account_email = 'orchestra@orchestra.lan'
email = self.selenium.find_element_by_id('id_email')
email.send_keys(account_email)
contact_short_name = random_ascii(10)
short_name = self.selenium.find_element_by_id('id_contacts-0-short_name')
short_name.send_keys(contact_short_name)
email = self.selenium.find_element_by_id('id_contacts-0-email')
email.send_keys(account_email)
email.submit()
account = Account.objects.get(username=account_username)
self.addCleanup(account.delete)
self.assertNotEqual(url, self.selenium.current_url)
self.assertEqual(0, r("id %s" % account.username).return_code)

View File

@ -12,9 +12,6 @@ from xvfbwrapper import Xvfb
from orchestra.apps.accounts.models import Account from orchestra.apps.accounts.models import Account
User = get_user_model()
class AppDependencyMixin(object): class AppDependencyMixin(object):
DEPENDENCIES = () DEPENDENCIES = ()
@ -56,6 +53,9 @@ class BaseTestCase(TestCase, AppDependencyMixin):
class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase): class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
ACCOUNT_USERNAME = 'orchestra'
ACCOUNT_PASSWORD = 'orchestra'
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.vdisplay = Xvfb() cls.vdisplay = Xvfb()
@ -69,19 +69,21 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
cls.vdisplay.stop() cls.vdisplay.stop()
super(BaseLiveServerTestCase, cls).tearDownClass() super(BaseLiveServerTestCase, cls).tearDownClass()
def create_account(self, superuser=False):
if superuser:
return Account.objects.create_superuser(self.ACCOUNT_USERNAME,
password=self.ACCOUNT_PASSWORD, email='orchestra@orchestra.org')
return Account.objects.create_user(self.ACCOUNT_USERNAME,
password=self.ACCOUNT_PASSWORD, email='orchestra@orchestra.org')
def setUp(self): def setUp(self):
super(BaseLiveServerTestCase, self).setUp() super(BaseLiveServerTestCase, self).setUp()
self.rest = Api(self.live_server_url + '/api/') self.rest = Api(self.live_server_url + '/api/')
self.account = Account.objects.create(name='orchestra') self.account = self.create_account(superuser=True)
self.username = 'orchestra'
self.password = 'orchestra'
self.user = User.objects.create_superuser(username='orchestra',
password='orchestra', email='orchestra@orchestra.org',
account=self.account)
def admin_login(self): def admin_login(self):
session = SessionStore() session = SessionStore()
session[SESSION_KEY] = self.user.pk session[SESSION_KEY] = self.account.pk
session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0] session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0]
session.save() session.save()
## to set a cookie we need to first visit the domain. ## to set a cookie we need to first visit the domain.
@ -93,7 +95,7 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
)) ))
def rest_login(self): def rest_login(self):
self.rest.login(username=self.username, password=self.password) self.rest.login(username=self.ACCOUNT_USERNAME, password=self.ACCOUNT_PASSWORD)
def random_ascii(length): def random_ascii(length):