feat: Add ansible-test and refactor plugins

This commit is contained in:
Jadyn Emma Jaeger 2022-07-29 10:47:49 +02:00
parent 1a302fb74b
commit 52785ab768
No known key found for this signature in database
GPG key ID: 632410170049FDDA
19 changed files with 650 additions and 261 deletions

View file

@ -1,7 +1,31 @@
include:
- project: 'famedly/company/devops/templates/ci-cd'
ref: 'ansible-v1'
file: '/ansible.yml'
stages:
- test
- build
- publish
# Debian Stable
sanity 3.9:
variables: { PYTHON_VERSION: "3.9" }
extends: .ansible-test-sanity
# Latest stable Python
sanity 3.10:
variables: { PYTHON_VERSION: "3.10" }
extends: .ansible-test-sanity
unit:
variables: { PYTHON_VERSION: "3.10" }
extends: .ansible-test-units
integration:
variables: { PYTHON_VERSION: "3.10" }
extends: .ansible-test-integration
build:
image: docker.io/alpine
stage: build

View file

@ -1,93 +1,121 @@
#!/usr/bin/python
# coding: utf-8
# (c) 2021-2022, Famedly GmbH
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
import asyncio
#Check if all required libs can loded
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
# Check if all required libs can load
LIB_IMP_ERR = None
try:
from nio import *
HAS_LIB = True
from nio import AsyncClient, AsyncClientConfig, \
Api, \
LoginResponse, LoginError, \
LogoutResponse, LogoutError, \
RoomGetStateResponse, RoomGetStateError, \
RoomBanResponse, RoomBanError, \
RoomUnbanResponse, RoomUnbanError, \
RoomKickResponse, RoomKickError, \
RoomInviteResponse, RoomInviteError, \
RoomResolveAliasResponse, RoomResolveAliasError, \
JoinedRoomsResponse, JoinedRoomsError
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
class AnsibleNioModule():
def __init__(self,
custom_spec={},
class AnsibleNioModule:
def __init__(self,
custom_spec=None,
bypass_checks=False,
no_log=False,
mutually_exclusive=[['password', 'token']],
mutually_exclusive=None,
required_together=None,
required_one_of=[['password', 'token', 'key']],
required_by={'password': 'user_id', 'key': 'user_id'},
required_one_of=None,
required_by=None,
add_file_common_args=False,
supports_check_mode=True,
required_if=None,
user_logout=True):
#If a user/password login is provided, should we logout when exiting?
self.user_logout=user_logout
#Create the Ansible module
if required_by is None:
required_by = {'password': 'user_id'}
if required_one_of is None:
required_one_of = [['password', 'token']]
if mutually_exclusive is None:
mutually_exclusive = [['password', 'token']]
if custom_spec is None:
custom_spec = {}
# If a user/password login is provided, should we logout when exiting?
self.user_logout = user_logout
# Create the Ansible module
self.module = AnsibleModule(
argument_spec = AnsibleNioModule.__common_argument_spec(custom_spec),
argument_spec=AnsibleNioModule.__common_argument_spec(custom_spec),
bypass_checks=bypass_checks,
no_log=no_log,
mutually_exclusive=mutually_exclusive,
required_together=required_together,
required_one_of=required_one_of,
add_file_common_args=add_file_common_args,
supports_check_mode = supports_check_mode,
supports_check_mode=supports_check_mode,
required_if=required_if,
required_by=required_by
)
#Make some values from the module easly accessible
self.check_mode = self.module.check_mode
self.params = self.module.params
#Fail when matix-nio is not installed
#WARNING: We don't perform a version check!
# Make some values from the module easly accessible
self.check_mode = self.module.check_mode
self.params = self.module.params
# Fail when matix-nio is not installed
# WARNING: We don't perform a version check!
if not HAS_LIB:
self.module.fail_json(msg=missing_required_lib("matrix-nio"))
async def matrix_login(self):
#Login with token or supplied user account
# Login with token or supplied user account
if self.module.params['token'] is None:
self.client = AsyncClient(self.module.params['hs_url'], self.module.params['user_id'])
login_response = await self.client.login(self.module.params['password'])
if isinstance(login_response, LoginResponse):
self.access_token = login_response.access_token
self.device_id = login_response.device_id
else:
result['msg'] = login_response.message
result['http_status_code'] = login_response.status_code
module.fail_json(**result)
login_response = await self.client.login(password=self.module.params['password'])
else:
self.client = AsyncClient(self.module.params['hs_url'])
self.client.access_token = self.module.params['token']
login_response = await self.client.login(token=self.module.params['token'])
if isinstance(login_response, LoginResponse):
self.access_token = login_response.access_token
self.device_id = login_response.device_id
else:
result = {
'msg': login_response.message,
'http_status_code': login_response.status_code
}
self.module.fail_json(**result)
async def matrix_logout(self):
if self.client.logged_in:
await self.client.logout()
request = await self.client.logout()
if isinstance(request, LogoutError):
result = {'msg': request.message}
self.module.fail_json(**result)
async def exit_json(self, **result):
if self.module.params['token'] is None and self.user_logout == True:
if self.module.params['token'] is None and self.user_logout is True:
await self.matrix_logout()
await self.client.close()
self.module.exit_json(**result)
async def fail_json(self, **result):
if self.module.params['token'] is None and self.user_logout == True:
if self.module.params['token'] is None and self.user_logout is True:
await self.matrix_logout()
await self.client.close()
self.module.fail_json(**result)
@ -101,4 +129,3 @@ class AnsibleNioModule():
token=dict(type='str', required=False, no_log=True)
)
return {**argument_spec, **custom_spec}

View file

@ -1,4 +1,3 @@
#!/usr/bin/python3
# coding: utf-8
# (c) 2021, Famedly GmbH
@ -8,9 +7,19 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from typing import Union, Type
import requests
import traceback
import urllib.parse
from types import SimpleNamespace
# Check if all required libs can load
LIB_IMP_ERR = None
try:
import requests
HAS_REQUESTS = True
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
HAS_REQUESTS = False
requests = SimpleNamespace(Response=None)
class AdminApi:
@ -31,39 +40,39 @@ class AdminApi:
# Make API request
def get(self, path: str) -> requests.Response:
response = requests.get(url=urllib.parse.urljoin(self.api_url, path),
headers={"Authorization": "Bearer {}".format(self.access_token)})
headers={"Authorization": f"Bearer {self.access_token}"})
if response.status_code == 200:
return response
if response.status_code == 500:
raise Exceptions.MatrixException(
"Matrix Error\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text))
f"Matrix Error\nHTTP-Code: {response.status_code}\n Response: {response.text}")
else:
raise Exceptions.HTTPException(
"Unexpected return code\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text))
f"Unexpected return code\nHTTP-Code: {response.status_code}\n Response: {response.text}")
def post(self, path: str, **kwargs) -> requests.Response:
response = requests.post(url=urllib.parse.urljoin(self.api_url, path),
headers={"Authorization": "Bearer {}".format(self.access_token)}, **kwargs)
headers={"Authorization": f"Bearer {self.access_token}"}, **kwargs)
if response.status_code == 200:
return response
if response.status_code == 500:
raise Exceptions.MatrixException(
"Matrix Error\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text))
f"Matrix Error\nHTTP-Code: {response.status_code}\n Response: {response.text}")
else:
raise Exceptions.HTTPException(
"Unexpected return code\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text))
f"Unexpected return code\nHTTP-Code: {response.status_code}\n Response: {response.text}")
def delete(self, path: str) -> requests.Response:
response = requests.delete(url=urllib.parse.urljoin(self.api_url, path),
headers={"Authorization": "Bearer {}".format(self.access_token)})
headers={"Authorization": f"Bearer {self.access_token}"})
if response.status_code == 200:
return response
if response.status_code == 500:
raise Exceptions.MatrixException(
"Matrix Error\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text))
f"Matrix Error\nHTTP-Code: {response.status_code}\n Response: {response.text}")
else:
raise Exceptions.HTTPException(
"Unexpected return code\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text))
f"Unexpected return code\nHTTP-Code: {response.status_code}\n Response: {response.text}")
@staticmethod
def url_encode(string: str) -> str:
@ -83,8 +92,8 @@ class AdminApi:
def set(self, user_id: str, messages_per_second: int = 0, burst_count: int = 0) -> dict:
user_id = AdminApi.url_encode(user_id)
return self.__parent.post(
self.API_PATH.format(user_id=user_id), json={"messages_per_second": messages_per_second,
"burst_count": burst_count}).json()
self.API_PATH.format(user_id=user_id), json={"messages_per_second": messages_per_second,
"burst_count": burst_count}).json()
def delete(self, user_id: str) -> dict:
user_id = AdminApi.url_encode(user_id)

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -26,14 +27,22 @@ options:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
type: str
user_id:
description:
- The user id of the user
required: true
required: false
type: str
password:
description:
- The password to log in with
required: true
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
requirements:
- matrix-nio (Python library)
'''
@ -56,10 +65,10 @@ device_id:
returned: When login was successful
type: str
'''
import traceback
import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
async def run_module():
result = dict(

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -26,10 +27,22 @@ options:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
type: str
user_id:
description:
- The user id of the user
required: false
type: str
password:
description:
- The password to log in with
required: false
type: str
token:
description:
- Authentication token for the API call
required: true
required: false
type: str
requirements:
- matrix-nio (Python library)
'''
@ -43,10 +56,11 @@ EXAMPLES = '''
RETURN = '''
'''
import traceback
import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
async def run_module():
result = dict(
changed=False,
@ -61,5 +75,6 @@ async def run_module():
def main():
asyncio.run(run_module())
if __name__ == '__main__':
main()

View file

@ -6,13 +6,10 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'}
DOCUMENTATION = '''
---
@ -20,32 +17,53 @@ author: "Johanna Dorothea Reichmann (@transcaffeine)"
module: matrix_member
short_description: Manage matrix room membership
description:
- Manage the membership status of a given set of matrix users. Invitations (`state=member`), kicks and bans can be issued. With the `exclusive=True` flag, all other members in the room can be auto-kicked.
- Manage the membership status of a given set of matrix users. Invitations (`state=member`),
kicks and bans can be issued. With the `exclusive=True` flag, all other members in the room can be auto-kicked.
options:
room_id:
hs_url:
description:
- ID of the room to manage
user_ids:
description:
- List of matrix IDs to set their state
- URL of the homeserver, where the CS-API is reachable
required: true
state:
description:
- In which state all listed members should be: member|kicked|banned
required: true
exclusive:
description:
- If state=member, the module ensure only the specified user_ids are in the room by kicking every other user present in the room.
required: false
token:
description:
- Authentication token for the API call. If provided, user_id and password are not required
type: str
user_id:
description:
- The user id of the user
required: false
type: str
password:
description:
- The password to log in with
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
room_id:
description:
- ID of the room to edit
required: true
type: str
user_ids:
description:
- List of matrix IDs to set their state
type: list
elements: str
required: true
state:
description:
- In which state all listed members should be member|kicked|banned
choices: ['member', 'kicked', 'banned']
type: str
required: true
exclusive:
description:
- If state=member, the module ensure only the specified user_ids are in the room by
kicking every other user present in the room.
default: false
type: bool
required: false
requirements:
- matrix-nio (Python library)
'''
@ -58,83 +76,97 @@ EXAMPLES = '''
room_id: "{{ matrix_room_id }}"
state: member
user_ids:
- @user1:matrix.org
- @user2:homeserver.tld
- "@user1:matrix.org"
- "@user2:homeserver.tld"
'''
RETURN = '''
members:
description: Dictionary of all members in the given room who are either invited or joined
returned: When auth_token is valid
type: dict[str]
type: dict
'''
import traceback
import asyncio
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import RoomGetStateError, \
RoomBanError, RoomUnbanError, \
RoomKickError, RoomInviteError
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
class NioOperationError(Exception):
def __init__(self, *args):
super().__init__(*args)
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
async def get_room_members(client, room_id, res):
member_resp = await client.room_get_state(room_id)
if isinstance(member_resp, RoomGetStateError):
res['msg'] = "Could not get room state for roomId={0}".format(room_id)
raise Exception()
res['msg'] = f"Could not get room state for roomId={room_id}"
raise NioOperationError(res['msg'])
else:
return dict(list(map(lambda m: (m['state_key'],m['content']['membership']), filter(lambda e: e['type'] == 'm.room.member' and
e['content']['membership'] in ['invite', 'join', 'leave', 'ban'], member_resp.events))))
return dict(list(map(lambda m: (m['state_key'], m['content']['membership']), filter(
lambda e: e['type'] == 'm.room.member' and e['content']['membership'] in ['invite', 'join', 'leave', 'ban'],
member_resp.events))))
async def ban_from_room(client, room_id, user_id, res):
ban_resp = await client.room_ban(room_id, user_id)
if isinstance(ban_resp, RoomBanError):
res['msg'] = "Could not ban user={0} from roomId={1}".format(user_id, room_id)
raise Exception()
res['msg'] = f"Could not ban user={user_id} from roomId={room_id}"
raise NioOperationError(res['msg'])
res['changed'] = True
res['banned'].append(user_id)
async def unban_from_room(client, room_id, user_id, res):
ban_resp = await client.room_unban(room_id, user_id)
if isinstance(ban_resp, RoomUnbanError):
res['msg'] = "Could not unban user={0} from roomId={1}".format(user_id, room_id)
raise Exception()
res['msg'] = f"Could not unban user={user_id} from roomId={room_id}"
raise NioOperationError(res['msg'])
res['changed'] = True
res['unbanned'].append(user_id)
async def kick_from_room(client, room_id, user_id, res):
kick_resp = await client.room_kick(room_id, user_id)
if isinstance(kick_resp, RoomKickError):
res['msg'] = "Could not kick user={0} from roomId={1}".format(user_id, room_id)
raise Exception()
res['msg'] = f"Could not kick user={user_id} from roomId={room_id}"
raise NioOperationError(res['msg'])
res['changed'] = True
res['kicked'].append(user_id)
async def invite_to_room(client, room_id, user_id, res):
invite_resp = await client.room_invite(room_id, user_id)
if isinstance(invite_resp, RoomInviteError):
res['msg'] = "Could not invite user={0} to roomId={1}".format(user_id, room_id)
raise Exception()
res['msg'] = f"Could not invite user={user_id} to roomId={room_id}"
raise NioOperationError(res['msg'])
res['changed'] = True
res['invited'].append(user_id)
async def run_module():
module_args = dict(
state=dict(choices=['member', 'kicked', 'banned'], required=True),
room_id=dict(type='str', required=True),
user_ids=dict(type='list', required=True, elements='str'),
exclusive=dict(type='bool', required=False, default=False),
)
module_args = dict(state=dict(choices=['member', 'kicked', 'banned'], required=True),
room_id=dict(type='str', required=True),
user_ids=dict(type='list', required=True, elements='str'),
exclusive=dict(type='bool', required=False, default=False))
result = dict(
changed=False,
banned=[],
unbanned=[],
kicked=[],
invited=[],
members=[],
msg="",
)
result = dict(changed=False, banned=[], unbanned=[], kicked=[], invited=[], members=[], msg="", )
module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
await module.matrix_login()
if module.check_mode:
@ -151,10 +183,13 @@ async def run_module():
# Create client object
client = module.client
# Query all room members (invited users count as member, as they _can_ be in the room)
room_members = await get_room_members(client, room_id, result)
present_members = {m: s for m, s in room_members.items() if s in ['join', 'invite']}.keys()
banned_members = {m: s for m, s in room_members.items() if s == 'ban'}.keys()
try:
# Query all room members (invited users count as member, as they _can_ be in the room)
room_members = await get_room_members(client, room_id, result)
present_members = {m: s for m, s in room_members.items() if s in ['join', 'invite']}.keys()
banned_members = {m: s for m, s in room_members.items() if s == 'ban'}.keys()
except NioOperationError:
await module.fail_json(**result)
if not module.params['exclusive']:
# Handle non-exclusive invite|kick|ban
@ -170,7 +205,7 @@ async def run_module():
await unban_from_room(client, room_id, user_id, result)
elif action == 'banned' and user_id not in banned_members:
await ban_from_room(client, room_id, user_id, result)
except:
except NioOperationError:
await module.fail_json(**result)
else:
# Handle exclusive mode: get state and make lists of users to be kicked or invited
@ -182,18 +217,19 @@ async def run_module():
await invite_to_room(client, room_id, user_id, result)
for user_id in to_kick:
await kick_from_room(client, room_id, user_id, result)
except:
except NioOperationError:
await module.fail_json(**result)
# Get all current members from the room
try:
room_members_after = await get_room_members(client, room_id, result)
result['members'] = {m: s for m, s in room_members_after.items() if s in ['join', 'invite']}.keys()
except:
except NioOperationError:
pass
await module.exit_json(**result)
def main():
asyncio.run(run_module())

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -21,33 +22,43 @@ module: matrix_notification
short_description: Send notifications to matrix
description:
- This module sends html formatted notifications to matrix rooms.
version_added: "2.8"
version_added: "2.8.0"
options:
msg_plain:
description:
- Plain text form of the message to send to matrix, usually markdown
required: true
msg_html:
description:
- HTML form of the message to send to matrix
required: true
room_id:
description:
- ID of the room to send the notification to
required: true
hs_url:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
token:
description:
- Authentication token for the API call. If provided, user_id and password are not required
type: str
user_id:
description:
- The user id of the user
required: false
type: str
password:
description:
- The password to log in with
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
msg_plain:
description:
- Plain text form of the message to send to matrix, usually markdown
required: true
type: str
msg_html:
description:
- HTML form of the message to send to matrix
required: true
type: str
room_id:
description:
- ID of the room to send the notification to
required: true
type: str
requirements:
- matrix-nio (Python library)
'''
@ -73,16 +84,27 @@ EXAMPLES = '''
RETURN = '''
'''
import traceback
import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import RoomSendResponse, RoomSendError
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
async def run_module():
module_args = dict(
msg_plain=dict(type='str', required=True),
msg_html=dict(type='str', required=True),
room_id=dict(type='str', required=True),
)
)
result = dict(
changed=False,
@ -90,15 +112,17 @@ async def run_module():
)
module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
if module.check_mode:
return result
await module.matrix_login()
client = module.client
# send message
await client.room_send(
response = await client.room_send(
room_id=module.params['room_id'],
message_type="m.room.message",
content={
@ -108,9 +132,12 @@ async def run_module():
"formatted_body": module.params['msg_html'],
}
)
if isinstance(response, RoomSendError):
await module.fail_json(**result)
await module.exit_json(**result)
def main():
asyncio.run(run_module())

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -20,26 +21,37 @@ author: "Jan Christian Grünhage (@jcgruenhage)"
module: matrix_room
short_description: Join/Create matrix room
description:
- This module takes a room alias and makes sure that the user identified by the access token is in such a room. If that room does not exist, it is created, if it does exist but the user is not in it, it tries to join. If the alias is taken and the user can't join the room, the module will fail. Remote aliases are not supported for creating, but work for joining.
- This module takes a room alias and makes sure that the user identified by the access token is in such a room.
If that room does not exist, it is created, if it does exist but the user is not in it, it tries to join.
If the alias is taken and the user can't join the room, the module will fail.
Remote aliases are not supported for creating, but work for joining.
options:
alias:
description:
- Alias of the room to join/create
required: true
hs_url:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
token:
description:
- Authentication token for the API call. If provided, user_id and password are not required
type: str
user_id:
description:
- The user id of the user
required: false
type: str
password:
description:
- The password to log in with
equirements:
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
alias:
description:
- Alias of the room to join/create
required: true
type: str
requirements:
- matrix-nio (Python library)
'''
@ -55,12 +67,28 @@ RETURN = '''
room_id:
description: ID of the room
type: str
sample: !asdfbuiarbk213e479asf:server.tld
returned: success
sample: "!asdfbuiarbk213e479asf:server.tld"
'''
import traceback
import asyncio
import re
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import RoomCreateResponse, RoomCreateError, \
JoinedRoomsResponse, JoinedRoomsError, \
JoinResponse, JoinError, \
RoomResolveAliasResponse, RoomResolveAliasError
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
async def run_module():
module_args = dict(
@ -73,6 +101,9 @@ async def run_module():
)
module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
await module.matrix_login()
client = module.client
@ -87,7 +118,7 @@ async def run_module():
rooms_resp = await client.joined_rooms()
if isinstance(rooms_resp, JoinedRoomsError):
failed = True
result = {"msg":"Couldn't get joined rooms."}
result = {"msg": "Couldn't get joined rooms."}
elif room_id_resp.room_id in rooms_resp.rooms:
result = {"room_id": room_id_resp.room_id, "changed": False}
else:
@ -99,7 +130,7 @@ async def run_module():
result = {"room_id": join_resp.room_id, "changed": True}
else:
failed = True
result = {"msg": "Room exists, but couldn't join: {1}".format(join_resp)}
result = {"msg": f"Room exists, but couldn't join: {join_resp}"}
else:
# Get local part of alias
local_part_regex = re.search("#([^:]*):(.*)", module.params['alias'])
@ -113,13 +144,14 @@ async def run_module():
result = {"room_id": create_room_resp.room_id, "changed": True}
else:
failed = True
result = {"msg": "Room does not exist but couldn't be created either: {0}".format(create_room_resp)}
result = {"msg": f"Room does not exist but couldn't be created either: {create_room_resp}"}
if failed:
await module.fail_json(**result)
else:
await module.exit_json(**result)
def main():
asyncio.run(run_module())

View file

@ -1,18 +1,65 @@
#!/bin/python3
# Copyright: (c) 2018, Emmanouil Kampitakis <info@kampitakis.de>
#!/usr/bin/python
# Copyright: (c) 2018
# Apache 2.0
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
author: "Emmanouil Kampitakis (@madonius)"
module: matrix_signing_key
short_description: Create a signing key file if not exists
description:
- Create a signing key file if not exists
options:
path:
description:
- Path to the signing key file
required: true
type: str
requirements:
- signedjson (Python library)
'''
EXAMPLES = '''
- name: Create signing key file
matrix_signing_key:
changed: "/path/to/file"
'''
RETURN = '''
'''
import os
from ansible.module_utils.basic import AnsibleModule
from signedjson import key
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
# Check if all required libs can load
LIB_IMP_ERR = None
try:
from signedjson import key
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
def write_signing_key(path):
with open(path,'w') as file:
with open(path, 'w') as file:
key.write_signing_keys(
file,
[key.generate_signing_key('first')]
file,
[key.generate_signing_key('first')]
)
def run_module():
module_args = dict(
path=dict(type='str', required=True),
@ -29,6 +76,9 @@ def run_module():
supports_check_mode=True
)
if not HAS_LIB:
module.fail_json(msg=missing_required_lib("signedjson"))
signing_key_path = module.params['path']
signing_key_exists = os.path.isfile(signing_key_path)
@ -37,12 +87,17 @@ def run_module():
result['changed'] = True
if module.check_mode:
module.exit_json(**result)
write_signing_key(signing_key_path)
try:
write_signing_key(signing_key_path)
except OSError as e:
module.fail_json(msg=str(e))
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -22,36 +23,47 @@ short_description: Set matrix room state
description:
- This module sets matrix room state idempotently
options:
event_type:
description:
- Event type of the state to be set
required: true
state_key:
description:
- State key for the state event to be set
required: true
content:
description:
- The content to set the state to
required: true
room_id:
description:
- ID of the room to set the state for
required: true
hs_url:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
token:
description:
- Authentication token for the API call. If provided, user_id and password are not required
type: str
user_id:
description:
- The user id of the user
required: false
type: str
password:
description:
- The password to log in with
equirements:
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
event_type:
description:
- Event type of the state to be set
required: true
type: str
state_key:
description:
- State key for the state event to be set
required: true
type: str
content:
description:
- The content to set the state to
required: true
type: dict
room_id:
description:
- ID of the room to set the state for
required: true
type: str
requirements:
- matrix-client (Python library)
'''
@ -79,14 +91,27 @@ event_id:
type: str
sample: $Het2Dv7EEDFNJNgY-ehLSUrdqMo8JOxZDCMnuQPSNfo
'''
import traceback
import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import JoinedRoomsResponse, JoinedRoomsError, \
RoomGetStateEventResponse, RoomPutStateResponse
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
async def run_module():
module_args = dict(
event_type=dict(type='str', required=True),
state_key=dict(type='str', required=True),
state_key=dict(type='str', required=True, no_log=False),
content=dict(type='dict', required=True),
room_id=dict(type='str', required=True)
)
@ -97,6 +122,8 @@ async def run_module():
)
module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
await module.matrix_login()
client = module.client
@ -110,7 +137,7 @@ async def run_module():
rooms_resp = await client.joined_rooms()
if isinstance(rooms_resp, JoinedRoomsError):
failed = True
result = {"msg":"Couldn't get joined rooms."}
result = {"msg": "Couldn't get joined rooms."}
elif module.params['room_id'] not in rooms_resp.rooms:
failed = True
result = {"msg": "Not in the room you're trying to set state for."}
@ -138,13 +165,14 @@ async def run_module():
# Else, fail
else:
failed = True
result = {"msg": "Couldn't set state: {error}".format(error=send_resp)}
result = {"msg": f"Couldn't set state: {send_resp}"}
if failed:
await module.fail_json(**result)
else:
await module.exit_json(**result)
def main():
asyncio.run(run_module())

View file

@ -7,6 +7,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -17,7 +18,7 @@ ANSIBLE_METADATA = {
DOCUMENTATION = '''
---
author: "Jan Christian Grünhage"
author: "Jan Christian Grünhage (@jcgruenhage)"
module: matrix_token_login
short_description: Use com.famedly.token based logins to obtain an access token
description:
@ -27,17 +28,30 @@ options:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
type: str
user_id:
description:
- The user id of the user
required: true
key:
required: false
type: str
password:
description:
- The key to sign the log in token with
required: true
- The password to log in with
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
admin:
description:
- Whether to set the user as admin during login
type: bool
key:
description: Login key to use
type: str
required: true
requirements:
- matrix-nio (Python library)
- jwcrypto (Python library)
@ -62,18 +76,38 @@ device_id:
returned: When login was successful
type: str
'''
import traceback
import asyncio
import json
import base64
from jwcrypto import jwt, jwk
import time
import traceback
from ansible.module_utils.basic import missing_required_lib
# Check if all required libs can load
JWCRYPTO_IMP_ERR = None
try:
from jwcrypto import jwt, jwk
HAS_JWCRYPTO = True
except ImportError:
JWCRYPTO_IMP_ERR = traceback.format_exc()
HAS_JWCRYPTO = False
NIO_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import AsyncClient, Api
HAS_NIO = True
except ImportError:
NIO_IMP_ERR = traceback.format_exc()
HAS_NIO = False
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
async def run_module():
module_args = dict(
key=dict(type='str', required=True),
key=dict(type='str', required=True, no_log=True),
admin=dict(type='bool', required=False),
)
@ -83,6 +117,10 @@ async def run_module():
)
module = AnsibleNioModule(module_args, user_logout=False)
if not HAS_JWCRYPTO:
await module.fail_json(msg=missing_required_lib("jwcrypto"))
if not HAS_NIO:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
if module.check_mode:
return result
@ -112,9 +150,9 @@ async def run_module():
}
key = jwk.JWK(**key)
claims = {
"iss": "Matrix UIA Login Ansible Module",
"sub": client.user,
"exp": int(time.time()) + 60 * 30,
"iss": "Matrix UIA Login Ansible Module",
"sub": client.user,
"exp": int(time.time()) + 60 * 30,
}
if admin is not None:
@ -126,12 +164,12 @@ async def run_module():
token.make_signed_token(key)
auth = {
"type": "com.famedly.login.token",
"identifier" : {
"type": "m.id.user",
"user": client.user
},
"token": token.serialize()
"type": "com.famedly.login.token",
"identifier": {
"type": "m.id.user",
"user": client.user
},
"token": token.serialize()
}
payload = json.dumps(auth)

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -26,14 +27,22 @@ options:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
type: str
user_id:
description:
- The user id of the user
required: true
required: false
type: str
password:
description:
- The password to log in with
required: true
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
requirements:
- matrix-nio (Python library)
'''
@ -56,14 +65,24 @@ device_id:
returned: When login was successful
type: str
'''
import traceback
import asyncio
from functools import reduce
import json
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import AsyncClient, Api
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
log = []
def get_payload(data):
payload = json.dumps(data)
payload_length = len(payload)
@ -73,55 +92,62 @@ def get_payload(data):
}
return payload, headers
async def do_password_stage(client, session, method, path, password):
auth = {
"type": "m.login.password",
"identifier" : {
"type": "m.id.user",
"user": client.user
},
"session": session,
"password": password
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": client.user
},
"session": session,
"password": password
}
log.append("DEBUG: attempt stage=" + auth['type'] + " for session="+ auth['session'] + " with password="+auth['password'] + ", method=" + method)
payload, headers = get_payload({"auth" : auth })
raw_response = await client.send(method, path, payload, headers)
res = await client.parse_body(raw_response)
log.append("DEBUG: stage=" + auth['type'] + " resulted in status=" + str(raw_response.status))
return raw_response.status, res
async def do_dummy_stage(client, session, method, path, password):
auth = {
"type": "m.login.dummy",
"session": session
}
log.append("DEBUG: attempt stage=" + auth['type'] + " for session="+ auth['session'])
log.append(f"DEBUG: attempt stage={auth['type']} for session={auth['session']} with password={auth['password']}, method={method}")
payload, headers = get_payload({"auth": auth})
raw_response = await client.send(method, path, payload, headers)
res = await client.parse_body(raw_response)
log.append("DEBUG: stage=" + auth['type'] + " resulted in status=" + str(raw_response.status))
log.append(f"DEBUG: stage={auth['type']} resulted in status={raw_response.status}")
return raw_response.status, res
async def do_dummy_stage(client, session, method, path, password):
auth = {
"type": "m.login.dummy",
"session": session
}
log.append(f"DEBUG: attempt stage={auth['type']} for session={auth['session']}")
payload, headers = get_payload({"auth": auth})
raw_response = await client.send(method, path, payload, headers)
res = await client.parse_body(raw_response)
log.append(f"DEBUG: stage{auth['type']} resulted in status={raw_response.status}")
return raw_response.status, res
uia_stages = {
"m.login.password": do_password_stage,
"m.login.dummy": do_dummy_stage
}
# Picks the best compatible flow out of an array of flows
def pick_flow(flows):
supported_stages = uia_stages.keys()
# reduces each flow to a boolean telling filter if the flow consists only out of compatible stages
compatible_flows = [flow for flow in flows if all([stage in supported_stages for stage in flow['stages']])]
compatible_flows = [flow for flow in flows if all(stage in supported_stages for stage in flow['stages'])]
# the best flow is the one with the fewest stages, key= takes a function telling min() the weight of an entry
best = min(compatible_flows, key=(lambda flow: len(flow['stages'])))
return best
async def run_module():
result = dict(
changed=False,
)
module = AnsibleNioModule(user_logout=False)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
if module.check_mode:
return result
@ -134,6 +160,7 @@ async def run_module():
# Collect and check login information
password = module.params['password']
token = module.params['token']
if password is None and token is None:
await module.fail_json(msg="A PASSWORD has to be provided")
@ -149,21 +176,21 @@ async def run_module():
raw_response = await client.send(method, path, {})
res = await client.parse_body(raw_response)
uia_session = res['session']
log.append("DEBUG: begin UIA for session=" + uia_session)
log.append(f"DEBUG: begin UIA for session={uia_session}")
# Figure out best compatible UIA login flow
log.append("INFO: available flows: " + str(res['flows']))
log.append(f"INFO: available flows: {res['flows']}")
flow_to_attempt = pick_flow(res['flows'])
log.append("INFO: picking flow: " + (" -> ".join(flow_to_attempt['stages'])))
log.append(f"INFO: picking flow: {' -> '.join(flow_to_attempt['stages'])}")
# Attempt each stage in the flow
for stage in flow_to_attempt['stages']:
stage_status, stage_result = await uia_stages[stage](client, uia_session, method, path, password)
if int(stage_status) == 401 and stage != (flow_to_attempt['stages'])[-1]:
log.append("INFO: completed stage " + stage)
log.append(f"INFO: completed stage {stage}")
completed_stages = stage_result['completed']
elif int(stage_status) == 200 and stage == (flow_to_attempt['stages'])[-1]:
log.append("INFO: final stage completed " + stage)
log.append(f"INFO: final stage completed {stage}")
result['token'] = stage_result['access_token']
result['device_id'] = stage_result['device_id']
failed = False

View file

@ -16,7 +16,7 @@ ANSIBLE_METADATA = {
DOCUMENTATION = '''
---
author: "Jadyn Emma Jäger (@jadyn.dev)"
author: "Jadyn Emma Jäger (@jadyndev)"
module: synapse_ratelimit
short_description: Change a users rate-limits
description:
@ -26,31 +26,34 @@ options:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
type: str
access_token:
description:
- Shared secret to authenticate registration request
required: true
type: str
user_id:
description:
- The fully qualified MXID of a __local__ user
required: true
type: str
action:
description:
- Which (http) operation should be executed
required: True
required: false
type: str
choices: 'get', 'set', 'delete'
choices: ['get', 'set', 'delete']
default: 'get'
messages_per_second:
description:
- Set the maximum messages per second (0 = disabled)
required: False
required: false
type: int
default: 0
burst_count:
description:
- Set the maximum message burst (0 = disabled)
required: False
required: false
type: int
default: 0
requirements: []
@ -69,13 +72,21 @@ EXAMPLES = '''
RETURN = '''
ratelimit:
- burst_count: 5
- messages_per_second: 10
if a ratelimit is set, otherwise `ratelimit` is empty
description: if a ratelimit is set, otherwise `ratelimit` is empty
type: dict
returned: success
sample:
burst_count: 5
messages_per_second: 10
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.famedly.matrix.plugins.module_utils.synapse import *
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback
try:
from ansible_collections.famedly.matrix.plugins.module_utils.synapse import AdminApi, Exceptions, HAS_REQUESTS
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
def main():
@ -97,6 +108,9 @@ def main():
supports_check_mode=True
)
if not HAS_REQUESTS:
module.fail_json(msg=missing_required_lib("requests"))
if module.check_mode:
return result
@ -117,7 +131,7 @@ def main():
result['ratelimit'] = synapse.ratelimit.delete(module.params['user_id'])
result['changed'] = ratelimit != result['ratelimit']
module.exit_json(**result)
raise NotImplementedError("action {} is not implemented".format(action))
raise NotImplementedError(f"action {action} is not implemented")
except (Exceptions.HTTPException, Exceptions.MatrixException) as e:
result['msg'] = str(e)
module.fail_json(**result)

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
@ -26,14 +27,17 @@ options:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
type: str
user_id:
description:
- The user id of the user
required: true
type: str
password:
description:
- The password to register with
required: true
type: str
admin:
description:
- Whether or not the new user should be an admin
@ -44,6 +48,7 @@ options:
description:
- Shared secret to authenticate registration request
required: true
type: str
requirements: []
'''
@ -57,21 +62,28 @@ EXAMPLES = '''
shared_secret: "long secret string"
'''
RETURN = '''
'''
import traceback
import asyncio
import hmac
import hashlib
import requests
import traceback
# Check if all required libs can load
LIB_IMP_ERR = None
try:
import requests
HAS_REQUESTS = True
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
HAS_REQUESTS = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
def generate_mac(nonce, shared_secret, user, password, admin=False, user_type=None):
def generate_mac(nonce, shared_secret, user, password, admin=False, user_type=None):
mac = hmac.new(
key=shared_secret.encode('utf8'),
digestmod=hashlib.sha1,
key=shared_secret.encode('utf8'),
digestmod=hashlib.sha1,
)
mac.update(nonce.encode('utf8'))
@ -87,6 +99,7 @@ def generate_mac(nonce, shared_secret, user, password, admin=False, user_type=No
return mac.hexdigest()
async def run_module():
module_args = dict(
hs_url=dict(type='str', required=True),
@ -105,18 +118,22 @@ async def run_module():
supports_check_mode=True
)
if not HAS_REQUESTS:
module.fail_json(msg=missing_required_lib("requests"))
if module.check_mode:
return result
failed = False
url = "{}/_synapse/admin/v1/register".format(module.params["hs_url"])
url = f"{module.params['hs_url']}/_synapse/admin/v1/register"
response = requests.get(url)
if response.status_code != 200:
result["msg"] = response.json()["error"]
module.exit_json(**result)
nonce = response.json()["nonce"]
mac = generate_mac(nonce, module.params["shared_secret"], module.params["user_id"], module.params["password"], module.params["admin"])
mac = generate_mac(nonce, module.params["shared_secret"], module.params["user_id"], module.params["password"],
module.params["admin"])
data = {
"nonce": nonce,

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
ansible~=5.2.0
matrix-nio~=0.19.0
signedjson~=1.1.4

View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
cd ../../
while read -r line; do
find plugins/modules -name "*.py" | xargs -I {} -n 1 printf "{} $line\n"
done <"tests/sanity/ignore.template"

View file

@ -0,0 +1,11 @@
plugins/modules/matrix_signing_key.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_register.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_notification.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_state.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_room.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_uia_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_ratelimit.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_token_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_logout.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_member.py validate-modules:missing-gplv3-license # ignore license check

View file

@ -0,0 +1,11 @@
plugins/modules/matrix_signing_key.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_register.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_notification.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_state.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_room.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_uia_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_ratelimit.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_token_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_logout.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_member.py validate-modules:missing-gplv3-license # ignore license check

View file

@ -0,0 +1 @@
validate-modules:missing-gplv3-license # ignore license check