Skip to content

Commit

Permalink
Merge 6af372b into 38b4519
Browse files Browse the repository at this point in the history
  • Loading branch information
nuwang committed Feb 22, 2020
2 parents 38b4519 + 6af372b commit 43dba1c
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 18 deletions.
16 changes: 14 additions & 2 deletions cloudman/clusterman/management/commands/create_autoscale_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand

from clusterman.models import GlobalSettings


class Command(BaseCommand):
help = 'Creates a user for managing autoscaling. This user has permissions to scale' \
Expand All @@ -16,12 +18,17 @@ def add_arguments(self, parser):
parser.add_argument(
'--password', required=False,
help='Password for this user, autogenerated if not specified')
parser.add_argument(
'--impersonate_account', required=False,
help='User account to impersonate when scaling. This account is assumed to have stored'
' cloud credentials or IAM access. Defaults to the first super admin found.')

def handle(self, *args, **options):
username = options['username']
password = options['password']
account = options['impersonate_account']

return self.create_autoscale_user(username, password)
return self.create_autoscale_user(username, password, account)

@staticmethod
def _add_permissions(user, perm_names):
Expand All @@ -31,7 +38,7 @@ def _add_permissions(user, perm_names):
return user

@staticmethod
def create_autoscale_user(username, password):
def create_autoscale_user(username, password, account):
try:
print("Creating autoscale user: {0}".format(username))
user, created = User.objects.get_or_create(username=username)
Expand All @@ -41,6 +48,11 @@ def create_autoscale_user(username, password):
user, ['view_cmcluster', 'add_cmclusternode',
'delete_cmclusternode'])
user.save()
if account:
impersonate_user = User.objects.get(username=account)
else:
impersonate_user = User.objects.filter(is_superuser=True).first()
GlobalSettings().settings.autoscale_impersonate = impersonate_user.username
return "Autoscale user created successfully."
else:
return "Autoscale user already exists."
Expand Down
10 changes: 9 additions & 1 deletion cloudman/clusterman/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 2.2.10 on 2020-02-17 18:49
# Generated by Django 2.2.10 on 2020-02-22 19:59

from django.db import migrations, models
import django.db.models.deletion
Expand Down Expand Up @@ -44,6 +44,14 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Clusters',
},
),
migrations.CreateModel(
name='GlobalSettings_SettingsStore',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=255)),
('value', models.TextField()),
],
),
migrations.CreateModel(
name='CMClusterNode',
fields=[
Expand Down
10 changes: 10 additions & 0 deletions cloudman/clusterman/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from django.db import models

from hierarkey.models import GlobalSettingsBase, Hierarkey

from cloudlaunch import models as cl_models
from djcloudbridge import models as cb_models
import yaml


hierarkey = Hierarkey(attribute_name='settings')


@hierarkey.set_global()
class GlobalSettings(GlobalSettingsBase):
pass


class CMCluster(models.Model):
"""CloudMan cluster details."""
# Automatically add timestamps when object is created
Expand Down
2 changes: 2 additions & 0 deletions cloudman/clusterman/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ def has_autoscale_permissions(user, obj):
rules.add_perm('clusternodes.add_clusternode', is_node_owner | has_autoscale_permissions | rules.is_staff)
rules.add_perm('clusternodes.change_clusternode', is_node_owner | has_autoscale_permissions | rules.is_staff)
rules.add_perm('clusternodes.delete_clusternode', is_node_owner | has_autoscale_permissions | rules.is_staff)

rules.add_perm('autoscalers.can_autoscale', has_autoscale_permissions | rules.is_staff)
50 changes: 41 additions & 9 deletions cloudman/clusterman/tests/test_cluster_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def setUp(self):
self.addCleanup(patcher_migrate_result.stop)

self.client.force_login(
User.objects.get_or_create(username='clusteradmin', is_staff=True)[0])
User.objects.get_or_create(username='clusteradmin', is_superuser=True, is_staff=True)[0])
responses.add(responses.POST, 'https://127.0.0.1:4430/v3/clusters/c-abcd1?action=generateKubeconfig',
json={'config': load_kube_config()}, status=200)

Expand Down Expand Up @@ -196,6 +196,9 @@ class LiveServerSingleThreadedTestCase(APILiveServerTestCase):
class CMClusterNodeTestBase(CMClusterServiceTestBase, LiveServerSingleThreadedTestCase):

def setUp(self):
self.client.force_login(
User.objects.get_or_create(username='clusteradmin', is_superuser=True, is_staff=True)[0])

cloudlaunch_url = f'{self.live_server_url}/cloudman/cloudlaunch/api/v1'
patcher1 = patch('clusterman.api.CMServiceContext.cloudlaunch_url',
new_callable=PropertyMock,
Expand Down Expand Up @@ -553,8 +556,6 @@ def _deactivate_autoscaling(self, cluster_id):
return self.client.put(url, cluster_data, format='json')

def _create_cluster_node(self, cluster_id):
responses.add(responses.POST, 'https://127.0.0.1:4430/v3/clusterregistrationtoken',
json={'nodeCommand': 'docker run rancher --worker'}, status=200)
url = reverse('clusterman:node-list', args=[cluster_id])
return self.client.post(url, self.NODE_DATA, format='json')

Expand All @@ -565,15 +566,11 @@ def _count_cluster_nodes(self, cluster_id):
return response.data['count']

def _signal_scaleup(self, cluster_id, data=SCALE_SIGNAL_DATA):
responses.add(responses.POST, 'https://127.0.0.1:4430/v3/clusterregistrationtoken',
json={'nodeCommand': 'docker run rancher --worker'}, status=200)
url = reverse('clusterman:scaleupsignal-list', args=[cluster_id])
response = self.client.post(url, data, format='json')
return response

def _signal_scaledown(self, cluster_id, data=SCALE_SIGNAL_DATA):
responses.add(responses.POST, 'https://127.0.0.1:4430/v3/clusterregistrationtoken',
json={'nodeCommand': 'docker run rancher --worker'}, status=200)
url = reverse('clusterman:scaledownsignal-list', args=[cluster_id])
response = self.client.post(url, data, format='json')
return response
Expand Down Expand Up @@ -771,8 +768,12 @@ def test_scaling_within_zone_group(self):
count = self._count_cluster_nodes(cluster_id)
self.assertEqual(count, 1)

def _login_as_autoscaling_user(self):
call_command('create_autoscale_user', "--username", "autoscaletestuser")
def _login_as_autoscaling_user(self, impersonate_user=None):
if impersonate_user:
call_command('create_autoscale_user', "--impersonate_account",
impersonate_user, "--username", "autoscaletestuser")
else:
call_command('create_autoscale_user', "--username", "autoscaletestuser")
self.client.force_login(
User.objects.get_or_create(username='autoscaletestuser')[0])

Expand Down Expand Up @@ -833,3 +834,34 @@ def test_autoscale_down_signal_unauthorized(self):
User.objects.get_or_create(username='notaclusteradmin', is_staff=False)[0])
response = self._signal_scaledown(cluster_id)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)

@responses.activate
def test_create_autoscale_user_impersonate(self):
# create a parent cluster
cluster_id = self._create_cluster()
self._login_as_autoscaling_user(impersonate_user='clusteradmin')
count = self._count_cluster_nodes(cluster_id)
self.assertEqual(count, 0)
response = self._signal_scaleup(cluster_id)
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)
count = self._count_cluster_nodes(cluster_id)
self.assertEqual(count, 1)

@responses.activate
def test_create_autoscale_user_impersonate_no_perms(self):
# create a parent cluster
cluster_id = self._create_cluster()
# create a non admin user
self.client.force_login(
User.objects.get_or_create(username='notaclusteradmin', is_staff=False)[0])
# log back in as admin
self.client.force_login(
User.objects.get(username='clusteradmin'))
# impersonate non admin user
self._login_as_autoscaling_user(impersonate_user='notaclusteradmin')
count = self._count_cluster_nodes(cluster_id)
self.assertEqual(count, 0)
response = self._signal_scaleup(cluster_id)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data)
count = self._count_cluster_nodes(cluster_id)
self.assertEqual(count, 0)
20 changes: 19 additions & 1 deletion cloudman/clusterman/tests/test_mgmt_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ def test_create_cluster(self):

class CreateAutoScaleUserCommandTestCase(TestCase):

def setUp(self):
self.client.force_login(
User.objects.get_or_create(username='admin', is_superuser=True)[0])

def test_create_autoscale_user_no_args(self):
call_command('create_autoscale_user')
self.assertTrue(User.objects.get(username='autoscaleuser'))
Expand All @@ -84,9 +88,23 @@ def test_create_autoscale_user_existing(self):
"--password", "hello", stdout=out)
self.assertIn("already exists", out.getvalue())

def test_create_autoscale_does_not_clobber_existing(self):
def test_create_autoscale_user_does_not_clobber_existing(self):
User.objects.create_user(username="hello", password="world")
call_command('create_autoscale_user', "--username", "hello",
"--password", "overwrite")
# Password should remain unchanged
self.assertTrue(self.client.login(username="hello", password="world"))

def test_create_autoscale_user_with_impersonate(self):
out = StringIO()
call_command('create_autoscale_user', "--username", "hello",
"--password", "overwrite", "--impersonate_account", "admin",
stdout=out)
self.assertIn("created successfully", out.getvalue())

def test_create_autoscale_user_with_non_existent_impersonate(self):
out = StringIO()
call_command('create_autoscale_user', "--username", "hello",
"--password", "overwrite", "--impersonate_account", "non_existent",
stdout=out)
self.assertNotIn("created successfully", out.getvalue())
30 changes: 26 additions & 4 deletions cloudman/clusterman/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"""CloudMan Create views."""
from django.contrib.auth.models import User

from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets, mixins

from djcloudbridge import drf_helpers
from . import serializers
from .api import CloudManAPI
from .api import CMServiceContext
from .models import GlobalSettings


class ClusterViewSet(drf_helpers.CustomModelViewSet):
Expand Down Expand Up @@ -91,10 +95,19 @@ class ClusterScaleUpSignalViewSet(CustomCreateOnlyModelViewSet):
authentication_classes = [SessionAuthentication, BasicAuthentication]

def perform_create(self, serializer):
# first, check whether the current user has permissions to
# autoscale
cmapi = CloudManAPI.from_request(self.request)
cmapi.check_permissions('autoscalers.can_autoscale')
# If so, the remaining actions must be carried out as an impersonated user
# whose profile contains the relevant cloud credentials, usually an admin
zone_name = serializer.validated_data.get(
'commonLabels', {}).get('availability_zone')
cluster = CloudManAPI.from_request(self.request).clusters.get(
self.kwargs["cluster_pk"])
impersonate = (User.objects.filter(
username=GlobalSettings().settings.autoscale_impersonate).first()
or User.objects.filter(is_superuser=True).first())
cmapi = CloudManAPI(CMServiceContext(user=impersonate))
cluster = cmapi.clusters.get(self.kwargs["cluster_pk"])
if cluster:
return cluster.scaleup(zone_name=zone_name)
else:
Expand All @@ -111,10 +124,19 @@ class ClusterScaleDownSignalViewSet(CustomCreateOnlyModelViewSet):
authentication_classes = [SessionAuthentication, BasicAuthentication]

def perform_create(self, serializer):
# first, check whether the current user has permissions to
# autoscale
cmapi = CloudManAPI.from_request(self.request)
cmapi.check_permissions('autoscalers.can_autoscale')
# If so, the remaining actions must be carried out as an impersonated user
# whose profile contains the relevant cloud credentials, usually an admin
zone_name = serializer.validated_data.get(
'commonLabels', {}).get('availability_zone')
cluster = CloudManAPI.from_request(self.request).clusters.get(
self.kwargs["cluster_pk"])
impersonate = (User.objects.filter(
username=GlobalSettings().settings.autoscale_impersonate).first()
or User.objects.filter(is_superuser=True).first())
cmapi = CloudManAPI(CMServiceContext(user=impersonate))
cluster = cmapi.clusters.get(self.kwargs["cluster_pk"])
if cluster:
return cluster.scaledown(zone_name=zone_name)
else:
Expand Down
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ def get_version(*file_paths):
'rules',
# ======== CloudLaunch =========
'cloudlaunch-server>=0.1.1',
'cloudlaunch-cli'
'cloudlaunch-cli',
# ===== CloudMan =====
# To store generic key-value pairs
'django-hierarkey'
]

REQS_PROD = ([
Expand Down

0 comments on commit 43dba1c

Please sign in to comment.