add CloudService

This commit is contained in:
Max Erenberg 2021-11-19 22:16:05 -05:00
parent 798510511f
commit d7551eaf5c
8 changed files with 109 additions and 2 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ __pycache__/
.vscode/ .vscode/
*.o *.o
*.so *.so
*.swp
.idea/ .idea/
/docs/*.1 /docs/*.1
/docs/*.5 /docs/*.5

View File

@ -0,0 +1,12 @@
from zope.interface import Interface
from .IUser import IUser
class ICloudService(Interface):
"""Performs operations on the CSC Cloud."""
def create_account(user: IUser):
"""
Activate an LDAP account in CloudStack for the given user.
"""

View File

@ -1,3 +1,4 @@
from .ICloudService import ICloudService
from .IKerberosService import IKerberosService from .IKerberosService import IKerberosService
from .IConfig import IConfig from .IConfig import IConfig
from .IUser import IUser from .IUser import IUser

View File

@ -7,11 +7,12 @@ from zope import component
from .error_handlers import register_error_handlers from .error_handlers import register_error_handlers
from ceo_common.interfaces import IConfig, IKerberosService, ILDAPService, IFileService, \ from ceo_common.interfaces import IConfig, IKerberosService, ILDAPService, IFileService, \
IMailmanService, IMailService, IUWLDAPService, IHTTPClient, IDatabaseService IMailmanService, IMailService, IUWLDAPService, IHTTPClient, IDatabaseService, \
ICloudService
from ceo_common.model import Config, HTTPClient, RemoteMailmanService from ceo_common.model import Config, HTTPClient, RemoteMailmanService
from ceod.api.spnego import init_spnego from ceod.api.spnego import init_spnego
from ceod.model import KerberosService, LDAPService, FileService, \ from ceod.model import KerberosService, LDAPService, FileService, \
MailmanService, MailService, UWLDAPService MailmanService, MailService, UWLDAPService, CloudService
from ceod.db import MySQLService, PostgreSQLService from ceod.db import MySQLService, PostgreSQLService
@ -118,3 +119,8 @@ def register_services(app):
if hostname == cfg.get('ceod_database_host'): if hostname == cfg.get('ceod_database_host'):
psql_srv = PostgreSQLService() psql_srv = PostgreSQLService()
component.provideUtility(psql_srv, IDatabaseService, 'postgresql') component.provideUtility(psql_srv, IDatabaseService, 'postgresql')
# CloudService
if hostname == cfg.get('ceod_cloud_host'):
cloud_srv = CloudService()
component.provideUtility(cloud_srv, ICloudService)

View File

@ -0,0 +1,71 @@
from base64 import b64encode
import hashlib
import hmac
from typing import Dict
from urllib.parse import quote
import requests
from zope import component
from zope.interface import implementer
from ceo_common.interfaces import ICloudService, IConfig, IUser
from ceo_common.model import Term
@implementer(ICloudService)
class CloudService:
def __init__(self):
cfg = component.getUtility(IConfig)
self.api_key = cfg.get('cloudstack_api_key')
self.secret_key = cfg.get('cloudstack_secret_key')
self.base_url = cfg.get('cloudstack_base_url')
self.members_domain = cfg.get('cloudstack_members_domain')
def _create_url(self, params: Dict[str, str]) -> str:
# See https://docs.cloudstack.apache.org/en/latest/developersguide/dev.html#the-cloudstack-api
if 'apiKey' not in params and 'apikey' not in params:
params['apiKey'] = self.api_key
params['response'] = 'json'
request_str = '&'.join(
key + '=' + quote(val)
for key, val in params.items()
)
sig_str = '&'.join(
key.lower() + '=' + quote(val).lower()
for key, val in sorted(params.items())
)
sig = hmac.new(self.secret_key.encode(), sig_str.encode(), hashlib.sha1).digest()
encoded_sig = b64encode(sig).decode()
url = self.base_url + '?' + request_str + '&signature=' + quote(encoded_sig)
return url
def _get_domain_id(self, domain_name: str) -> str:
url = self._create_url({
'command': 'listDomains',
'details': 'min',
'name': domain_name,
})
resp = requests.get(url)
resp.raise_for_status()
d = resp.json()['listdomainsresponse']
assert d['count'] == 1, 'there should be one domain found'
return d['domain'][0]['id']
def create_account(self, user: IUser):
if not user.terms:
raise Exception('Only members may create cloud accounts')
most_recent_term = max(map(Term, user.terms))
if most_recent_term < Term.current():
raise Exception('Membership has expired for this user')
domain_id = self._get_domain_id(self.members_domain)
url = self._create_url({
'command': 'ldapCreateAccount',
'accounttype': '0',
'domainid': domain_id,
'username': user.uid,
})
resp = requests.post(url)
d = resp.json()['createaccountresponse']
if not resp.ok:
raise Exception(d['errortext'])

View File

@ -1,3 +1,4 @@
from .CloudService import CloudService
from .KerberosService import KerberosService from .KerberosService import KerberosService
from .LDAPService import LDAPService, UserNotFoundError, GroupNotFoundError from .LDAPService import LDAPService, UserNotFoundError, GroupNotFoundError
from .User import User from .User import User

View File

@ -10,6 +10,8 @@ fs_root_host = phosphoric-acid
database_host = caffeine database_host = caffeine
# this is the host which can make API requests to Mailman # this is the host which can make API requests to Mailman
mailman_host = mail mailman_host = mail
# this is the host which is running a CloudStack management server
cloud_host = biloba
use_https = true use_https = true
port = 9987 port = 9987
@ -72,3 +74,9 @@ host = localhost
username = REPLACE_ME username = REPLACE_ME
password = REPLACE_ME password = REPLACE_ME
host = localhost host = localhost
[cloudstack]
api_key = REPLACE_ME
secret_key = REPLACE_ME
base_url = http://localhost:8080/api/client
members_domain = Members

View File

@ -8,6 +8,7 @@ admin_host = phosphoric-acid
fs_root_host = phosphoric-acid fs_root_host = phosphoric-acid
mailman_host = mail mailman_host = mail
database_host = coffee database_host = coffee
cloud_host = biloba
use_https = false use_https = false
port = 9987 port = 9987
@ -67,3 +68,9 @@ host = localhost
username = postgres username = postgres
password = postgres password = postgres
host = localhost host = localhost
[cloudstack]
api_key = REPLACE_ME
secret_key = REPLACE_ME
base_url = http://localhost:8080/api/client
members_domain = Members