Positions CLI #11

Merged
merenber merged 10 commits from positions-cli into v1 2021-09-08 09:32:35 -04:00
9 changed files with 132 additions and 12 deletions

View File

@ -13,6 +13,9 @@ from ceo_common.model import Config, HTTPClient
def register_services():
# Using base component directly so events get triggered
baseComponent = component.getGlobalSiteManager()
# Config
# This is a hack to determine if we're in the dev env or not
if socket.getfqdn().endswith('.csclub.internal'):
@ -21,11 +24,11 @@ def register_services():
else:
config_file = os.environ.get('CEO_CONFIG', '/etc/csc/ceo.ini')
cfg = Config(config_file)
component.provideUtility(cfg, IConfig)
baseComponent.registerUtility(cfg, IConfig)
# HTTPService
http_client = HTTPClient()
component.provideUtility(http_client, IHTTPClient)
baseComponent.registerUtility(http_client, IHTTPClient)
def main():

View File

@ -2,6 +2,7 @@ import click
from .members import members
from .groups import groups
from .positions import positions
from .updateprograms import updateprograms
@ -12,4 +13,5 @@ def cli():
cli.add_command(members)
cli.add_command(groups)
cli.add_command(positions)
cli.add_command(updateprograms)

44
ceo/cli/positions.py Normal file
View File

@ -0,0 +1,44 @@
import click
from zope import component
from ..utils import http_get, http_post
from .utils import handle_sync_response, handle_stream_response, print_colon_kv
from ceo_common.interfaces import IConfig
from ceod.transactions.members import UpdateMemberPositionsTransaction
@click.group(short_help='List or change exec positions')
def positions():
update_commands()
@positions.command(short_help='Get current positions')
def get():
resp = http_get('/api/positions')
result = handle_sync_response(resp)
print_colon_kv(result.items())
@positions.command(short_help='Update positions')
def set(**kwargs):
body = {k.replace('_', '-'): v for k, v in kwargs.items()}
print_body = {k: v or '' for k, v in body.items()}
click.echo('The positions will be updated:')
print_colon_kv(print_body.items())
click.confirm('Do you want to continue?', abort=True)
resp = http_post('/api/positions', json=body)
handle_stream_response(resp, UpdateMemberPositionsTransaction.operations)
# Provides dynamic parameters for `set' command using config file
def update_commands():
global set
cfg = component.getUtility(IConfig)
avail = cfg.get('positions_available')
required = cfg.get('positions_required')
for pos in avail:
Review

What is this doing?

What is this doing?
Review

zope's registerUtility emits an IUtilityRegistration containing the component that got registered. Here I'm just checking for that, and whether the registered component is an IConfig.

zope's `registerUtility` emits an IUtilityRegistration containing the component that got registered. Here I'm just checking for that, and whether the registered component is an IConfig.
Review

The reason why this whole thing is here is to make the parameters (--president, --syscom, etc) follow what is in the config.

The reason why this whole thing is here is to make the parameters (--president, --syscom, etc) follow what is in the config.
r = pos in required
set = click.option(f'--{pos}', metavar='USERNAME', required=r, prompt=r)(set)

View File

@ -24,4 +24,7 @@ descriptions = {
'remove_user_from_auxiliary_groups': 'Remove user from auxiliary groups',
'unsubscribe_user_from_auxiliary_mailing_lists': 'Unsubscribe user from auxiliary mailing lists',
'remove_sudo_role': 'Remove sudo role from LDAP',
'update_positions_ldap': 'Update positions in LDAP',
'update_exec_group_ldap': 'Update executive group in LDAP',
'subscribe_to_mailing_lists': 'Subscribe to mailing lists',
}

View File

@ -69,6 +69,8 @@ def space_colon_kv(pairs: List[Tuple[str, str]]) -> List[str]:
key1000: val3
val4
"""
if not pairs:
return []
lines = []
maxlen = max(len(key) for key, val in pairs)
for key, val in pairs:

View File

@ -29,6 +29,12 @@ def update_positions():
required = cfg.get('positions_required')
available = cfg.get('positions_available')
# remove falsy values
body = {
positions: username for positions, username in body.items()
if username
}
for position in body.keys():
if position not in available:
return {

View File

@ -0,0 +1,55 @@
from click.testing import CliRunner
from ceo.cli import cli
def test_positions(cli_setup):
runner = CliRunner()
# Setup test data
for i in range(5):
runner.invoke(cli, ['members', 'add', f'test_{i}', '--cn', f'Test {i}', '--program', 'Math', '--terms', '1'], input='y\n')
runner.invoke(cli, ['groups', 'add', 'exec', '--description', 'Test Group'], input='y\n')
result = runner.invoke(cli, [
'positions', 'set',
'--president', 'test_0',
'--vice-president', 'test_1',
'--sysadmin', 'test_2',
'--secretary', 'test_3',
'--webmaster', 'test_4',
], input='y\n')
assert result.exit_code == 0
assert result.output == '''
The positions will be updated:
president: test_0
vice-president: test_1
sysadmin: test_2
secretary: test_3
webmaster: test_4
treasurer:
cro:
librarian:
imapd:
offsck:
Do you want to continue? [y/N]: y
Update positions in LDAP... Done
Update executive group in LDAP... Done
Subscribe to mailing lists... Done
Transaction successfully completed.
'''[1:] # noqa: W291
result = runner.invoke(cli, ['positions', 'get'])
assert result.exit_code == 0
assert result.output == '''
president: test_0
secretary: test_3
sysadmin: test_2
vice-president: test_1
webmaster: test_4
'''[1:]
# Cleanup test data
for i in range(5):
runner.invoke(cli, ['members', 'delete', f'test_{i}'], input='y\n')
runner.invoke(cli, ['groups', 'delete', 'exec'], input='y\n')

View File

@ -7,3 +7,8 @@ uw_domain = uwaterloo.internal
admin_host = phosphoric-acid
use_https = false
port = 9987
[positions]
required = president,vice-president,sysadmin
available = president,vice-president,treasurer,secretary,
sysadmin,cro,librarian,imapd,webmaster,offsck

View File

@ -51,7 +51,7 @@ def cfg(_drone_hostname_mock):
with importlib.resources.path('tests', 'ceod_test_local.ini') as p:
config_file = p.__fspath__()
_cfg = Config(config_file)
component.provideUtility(_cfg, IConfig)
component.getGlobalSiteManager().registerUtility(_cfg, IConfig)
return _cfg
@ -75,7 +75,7 @@ def krb_srv(cfg):
else:
principal = 'ceod/' + socket.getfqdn()
krb = KerberosService(principal)
component.provideUtility(krb, IKerberosService)
component.getGlobalSiteManager().registerUtility(krb, IKerberosService)
delete_test_princs(krb)
yield krb
@ -160,7 +160,7 @@ def ldap_srv_session(cfg, krb_srv, ldap_conn):
conn.add(base_dn, 'organizationalUnit')
_ldap_srv = LDAPService()
component.provideUtility(_ldap_srv, ILDAPService)
component.getGlobalSiteManager().registerUtility(_ldap_srv, ILDAPService)
yield _ldap_srv
@ -180,7 +180,7 @@ def ldap_srv(ldap_srv_session, g_admin_ctx):
@pytest.fixture(scope='session')
def file_srv(cfg):
_file_srv = FileService()
component.provideUtility(_file_srv, IFileService)
component.getGlobalSiteManager().registerUtility(_file_srv, IFileService)
members_home = cfg.get('members_home')
clubs_home = cfg.get('clubs_home')
@ -194,7 +194,7 @@ def file_srv(cfg):
@pytest.fixture(scope='session')
def http_client(cfg):
_client = HTTPClient()
component.provideUtility(_client, IHTTPClient)
component.getGlobalSiteManager().registerUtility(_client, IHTTPClient)
return _client
@ -210,7 +210,7 @@ def mock_mailman_server():
def mailman_srv(mock_mailman_server, cfg, http_client):
# TODO: test the RemoteMailmanService as well
mailman = MailmanService()
component.provideUtility(mailman, IMailmanService)
component.getGlobalSiteManager().registerUtility(mailman, IMailmanService)
return mailman
@ -223,7 +223,7 @@ def uwldap_srv(cfg, ldap_conn):
conn.add(base_dn, 'organizationalUnit')
_uwldap_srv = UWLDAPService()
component.provideUtility(_uwldap_srv, IUWLDAPService)
component.getGlobalSiteManager().registerUtility(_uwldap_srv, IUWLDAPService)
yield _uwldap_srv
delete_subtree(conn, base_dn)
@ -240,21 +240,21 @@ def mock_mail_server():
@pytest.fixture(scope='session')
def mail_srv(cfg, mock_mail_server):
_mail_srv = MailService()
component.provideUtility(_mail_srv, IMailService)
component.getGlobalSiteManager().registerUtility(_mail_srv, IMailService)
return _mail_srv
@pytest.fixture(scope='session')
def mysql_srv(cfg):
mysql_srv = MySQLService()
component.provideUtility(mysql_srv, IDatabaseService, 'mysql')
component.getGlobalSiteManager().registerUtility(mysql_srv, IDatabaseService, 'mysql')
return mysql_srv
@pytest.fixture(scope='session')
def postgresql_srv(cfg):
psql_srv = PostgreSQLService()
component.provideUtility(psql_srv, IDatabaseService, 'postgresql')
component.getGlobalSiteManager().registerUtility(psql_srv, IDatabaseService, 'postgresql')
return psql_srv