Added .lugitorc configuration and hooked the config files into the connectors. Updates to HISTORY.rst, README.rst and AUTHORS.rst

pull/1/head
Ben Johnston 6 years ago
parent f327136c34
commit e445368ef9
No known key found for this signature in database
GPG Key ID: 63E9F49BEDEC573D

@ -1,27 +0,0 @@
{
"hosts": {
"http://phab.lubuntu.me/": {
"token": ""
}
},
"config": {
"default": "http://phab.lubunutu.me/api/"
},
"HMAC": i{
"irc": "",
},
"irc": {
"host": "irc.freenode.net",
"port": "6697",
"username": "",
"password": "",
"channel": "#lubuntu-devel"
},
"launchpad": {
"application": "lugito",
"staging": "production",
"version": "devel",
"supported_versions": ["Cosmic", "Bionic", "Xenial", "Trusty"]
}
}

5
.gitignore vendored

@ -1,6 +1,11 @@
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
# Config file # Config file
.arcconfig .arcconfig
.lugitorc
# launchpad files
devel/*
# #
### Python template ### Python template
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files

@ -5,9 +5,9 @@ Credits
Development Lead Development Lead
---------------- ----------------
* Ben Johnston <bjohnston@neomailbox.net> * Simon Quigley
Contributors Contributors
------------ -----------
None yet. Why not be the first? * Ben Johnston (docEbrown) <bjohnston@neomailbox.net>

@ -2,7 +2,28 @@
History History
======= =======
0.1.0 (2018-11-07) 0.1.0 (20th May 2018)
------------------ ----------------------
* First release on PyPI. * Initial Release
0.2.0 (21st November 2018)
---------------------------
* added functionality for reporting creation, comments and edits on comments of
diffs
* Refactored into Python package
* irc and launchpad connections written into separate Python modules
* corrected issues with reporting using links with anchors via IRC
* added some unittests
* **Still TODO**
* Documentation - read the docs
* Add decorators to Lugito class to check that a request has been validated before other tasks can be completed
* Improve test coverage
* Pypi upload
* Add exceptions in the event that connector send method calls are not executed correctly
* CI and automated docs and Pypi udpate
* in-situ testing

@ -1,5 +1,65 @@
# Lugito lugito
======
This is Lubuntu's friendly IRC notifications bot, hooked up to our Phabricator instance at phab.lubuntu.me [![image](https://img.shields.io/pypi/v/lugito.svg)](https://pypi.python.org/pypi/lugito)
The code is licensed under the 3-clause BSD license, and is copyrighted by the Lubuntu team. More info available in LICENSE. [![image](https://img.shields.io/travis/doc-E-brown/lugito.svg)](https://travis-ci.org/doc-E-brown/lugito)
[![Documentation Status](https://readthedocs.org/projects/lugito/badge/?version=latest)](https://lugito.readthedocs.io/en/latest/?badge=latest)
Python Boilerplate contains all the boilerplate you need to create a
Python package.
- Free software: 3 Clause BSD license
- Documentation: <https://lugito.readthedocs.io>.
Temp - Example .lugitorc
------------------------
```
[phabricator]
host = http://127.0.0.1:9091/api/
token = api-nojs2ip33hmp4zn6u6cf72w7d6yh
[phabricator.hooks]
irc = cqg42zdcuqysff632kc6rnsu4m3hjg6c
commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi
[connector.irc]
host = irc.freenode.net
port = 6697
username = someusername
password = somepassword
channel = #somechannel
[connector.launchpad]
application = lugito
staging = production
version = devel
supported_versions =
Cosmic
Bionic
Xenial
Trusty
[connector.launchpad.package_names]
rDEFAULTSETTINGS = lubuntu-default-settings
rART = lubuntu-artwork
rCALASETTINGS = calamares-settings-ubuntu
rQTERMINALPACKAGING = qterminal
rLXQTCONFIGPACKAGING = lxqt-config
rNMTRAYPACKAGING = nm-tray
```
Features
--------
- TODO
Credits
-------
This package was created with
[Cookiecutter](https://github.com/audreyr/cookiecutter) and the
[audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage)
project template.

@ -2,25 +2,55 @@
lugito lugito
====== ======
lugito is a Python package that provides a webserver for connecting updates from Phabricator to communication tools such as irc and other services such as launchpad.
.. image:: https://img.shields.io/pypi/v/lugito.svg Installation
:target: https://pypi.python.org/pypi/lugito -------------
.. image:: https://img.shields.io/travis/doc-E-brown/lugito.svg lugito can be installed via Pyp
:target: https://travis-ci.org/doc-E-brown/lugito
.. image:: https://readthedocs.org/projects/lugito/badge/?version=latest
:target: https://lugito.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
* Free software: 3 Clause BSD license
Temp - Example .lugitorc
-------------------------
Python Boilerplate contains all the boilerplate you need to create a Python package. .. code::
[phabricator]
host = http://127.0.0.1:9091/api/
token = api-nojs2ip33hmp4zn6u6cf72w7d6yh
[phabricator.hooks]
irc = cqg42zdcuqysff632kc6rnsu4m3hjg6c
commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi
[connector.irc]
host = irc.freenode.net
port = 6697
username = someusername
password = somepassword
channel = #somechannel
[connector.launchpad]
application = lugito
staging = production
version = devel
supported_versions =
Cosmic
Bionic
Xenial
Trusty
[connector.launchpad.package_names]
rDEFAULTSETTINGS = lubuntu-default-settings
rART = lubuntu-artwork
rCALASETTINGS = calamares-settings-ubuntu
rQTERMINALPACKAGING = qterminal
rLXQTCONFIGPACKAGING = lxqt-config
rNMTRAYPACKAGING = nm-tray
* Free software: BSD license
* Documentation: https://lugito.readthedocs.io.
Features Features

@ -2,14 +2,7 @@ from lugito.lugito import (
Lugito, Lugito,
) )
from lugito.connectors.irc import ( import lugito.config
IRCConnector,
)
from lugito.connectors.launchpad import (
LPConnectook,
)
from ._version import get_versions from ._version import get_versions
__version__ = get_versions()['version'] __version__ = get_versions()['version']

@ -13,7 +13,6 @@
# Imports # Imports
from lugito.webhooks import run
if __name__ == "__main__": if __name__ == "__main__":
from lugito.webhooks import run
run() run()

@ -0,0 +1,135 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# S.D.G
"""
:mod:`lugito.config`
======================================
Module to manage lugito configuration
.. currentmodule:: lugito.config
"""
# Imports
import os
import logging
import configparser
DEFAULT_CONFIG_FILE = os.path.join(
os.getcwd(), '.lugitorc')
logger = logging.getLogger('lugito.config')
# Add log level
ch = logging.StreamHandler()
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
logger.setLevel(logging.DEBUG)
CONFIG = {}
def update_config(config_file=DEFAULT_CONFIG_FILE):
"""
Update the system config from a config file
Parameters
----------
config_file: str
The path of the lugito config file
Returns
-------
config: dictionary
A dictionary of config parameters
"""
config = configparser.ConfigParser()
config.read(config_file)
# Make some basic assertions for minimum functionality
if 'phabricator' not in config:
raise ValueError('phabricator section missing from config file: %s' %\
config_file)
if 'host' not in config['phabricator']:
raise ValueError('host value missing from phabricator section in'\
' config file: %s' % config_file)
if 'token' not in config['phabricator']:
raise ValueError('token value missing from phabricator section in'\
' config file: %s' % config_file)
CONFIG['phabricator'] = {}
CONFIG['phabricator']['host'] = config['phabricator']['host']
CONFIG['phabricator']['token'] = config['phabricator']['token']
CONFIG['phabricator']['hooks'] = {}
# Iterate through hooks for HMAC keys
if 'phabricator.hooks' in config:
for key, value in config['phabricator.hooks'].items():
CONFIG['phabricator']['hooks'][key] = value
CONFIG['connectors'] = {}
# Iterate through available connectors
for key in config.keys():
# Is a connector section
if ('connector.' in key) and (key.count('.') == 1) :
connector = key.split('.')[1]
if 'connectors' not in CONFIG:
CONFIG['connectors'] = {}
if connector not in CONFIG['connectors']:
CONFIG['connectors'][connector] = {}
for param, value in config[key].items():
# Check for multiple values for a parameter
if value.find('\n') >= 0:
value = value[1:].split('\n')
CONFIG['connectors'][connector][param] = value
# Is a connector sub-section
elif ('connector.' in key) and (key.count('.') > 1) :
sections = key.split('.')
connector = sections[1]
subsection = sections[-1]
if 'connectors' not in CONFIG:
CONFIG['connectors'] = {}
if connector not in CONFIG['connectors']:
CONFIG['connectors'][connector] = {}
CONFIG['connectors'][connector][subsection] = {}
for param, value in config[key].items():
# Check for multiple values for a parameter
if value.find('\n') >= 0:
value = value[1:].split('\n')
# configparser reads the parameters as lower case
# convert all but first character to upper case
param = 'r{}'.format(param[1:].upper())
CONFIG['connectors'][connector][subsection][param] = value
try:
update_config()
except ValueError:
# The config file is not present
logging.warning('Default config file: %s not found' % DEFAULT_CONFIG_FILE)

@ -1,3 +1,11 @@
#! /usr/bin/env python #! /usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# S.D.G # S.D.G
from lugito.connectors.irc import (
irc,
)
from lugito.connectors.launchpad import (
launchpad,
)

@ -18,25 +18,27 @@ import socket
import logging import logging
import threading import threading
import phabricator import phabricator
import lugito
from time import sleep from time import sleep
class IRCConnector(object): class irc(object):
def __init__(self, log_level=logging.DEBUG): def __init__(self, log_level=logging.DEBUG, sleep_delay=5):
# IRC info # IRC info
# Read the configuration out of the .arcconfig file # Read the configuration out of the .arcconfig file
self.host = phabricator.ARCRC['irc']['host'] self.host = lugito.config.CONFIG['connectors']['irc']['host']
self.port = int(phabricator.ARCRC['irc']['port']) self.port = int(lugito.config.CONFIG['connectors']['irc']['port'])
self.username = phabricator.ARCRC['irc']['username'] self.username = lugito.config.CONFIG['connectors']['irc']['username']
self.password = phabricator.ARCRC['irc']['password'] self.password = lugito.config.CONFIG['connectors']['irc']['password']
self.channel = phabricator.ARCRC['irc']['channel'] self.channel = lugito.config.CONFIG['connectors']['irc']['channel']
# Phabricator info # Phabricator info
self.phab = phabricator.Phabricator() self.phab = phabricator.Phabricator(
self.phab_host = phabricator.ARCRC['config']['default'].replace( host=lugito.config.CONFIG['phabricator']['host'],
'api/', '') token=lugito.config.CONFIG['phabricator']['token'],)
self.phab_host = self.phab.host.replace('api/', '')
self.logger = logging.getLogger('lugito.connector.IRCConnector') self.logger = logging.getLogger('lugito.connector.IRCConnector')
@ -50,14 +52,17 @@ class IRCConnector(object):
self.logger.addHandler(ch) self.logger.addHandler(ch)
self.logger.setLevel(log_level) self.logger.setLevel(log_level)
self.sleep_delay = sleep_delay
def _send_raw(self, message):
def _send_raw(self, message): # pragma: no cover
"""Low level send""" """Low level send"""
self.conn.send(message.encode('utf-8')) self.conn.send(message.encode('utf-8'))
def _socket_conn(self): def _setup_connection(self):
"""Setup connection"""
self.conn = ssl.wrap_socket( self.conn = ssl.wrap_socket(
socket.socket(socket.AF_INET, socket.SOCK_STREAM)) socket.socket(socket.AF_INET, socket.SOCK_STREAM))
self.conn.connect((self.host, self.port)) self.conn.connect((self.host, self.port))
@ -66,11 +71,11 @@ class IRCConnector(object):
def connect(self): def connect(self):
"""Connect""" """Connect"""
self._socket_conn() self.logger.info("Connecting to IRC.")
self._setup_connection()
setup = False setup = False
usersuffix = 0 usersuffix = 0
self.logger.info("Connecting to IRC.")
while not setup: while not setup:
response = self.conn.recv(512).decode("utf-8") response = self.conn.recv(512).decode("utf-8")
@ -84,11 +89,11 @@ class IRCConnector(object):
self.username, self.password)) self.username, self.password))
if "You are now identified" in response: if "You are now identified" in response:
sleep(5) sleep(self.sleep_delay)
self._send_raw("JOIN {}\r\n".format(self.channel)) self._send_raw("JOIN {}\r\n".format(self.channel))
if "477" in response: if "477" in response:
sleep(5) sleep(self.sleep_delay)
self._send_raw("JOIN {}\r\n".format(self.channel)) self._send_raw("JOIN {}\r\n".format(self.channel))
if "433" in response: if "433" in response:
@ -106,20 +111,32 @@ class IRCConnector(object):
self.logger.info("Successfully connected to the IRC server.") self.logger.info("Successfully connected to the IRC server.")
def send_notice(self, message): def send_notice(self, message): # pragma: no cover
self._send_raw("NOTICE {} :{}\r\n".format(self.channel, message)) self._send_raw("NOTICE {} :{}\r\n".format(self.channel, message))
def send(self, objectstr, who, body, link): def send(self, *args, **kwargs):
"""Send a formatted message""" """Send a formatted message"""
if len(args) == 4:
objectstr, who, body, link = args
elif len(kwargs) == 4:
objectstr = kwargs['objectstr']
who = kwargs['who']
body = kwargs['body']
link = kwargs['link']
# else
# raise exception
# e.g. [T31: Better IRC integration] # e.g. [T31: Better IRC integration]
message = "\x033[\x03\x0313" + objectstr + "\x03\x033]\x03 " message = "\x033[\x03\x0313" + objectstr + "\x03\x033]\x03 "
# e.g. tsimonq2 (Simon Quigley) # e.g. tsimonq2 (Simon Quigley)
message = message + "\x0315" + who + "\x03 " message += "\x0315" + who + "\x03 "
# e.g. commented on the task: # e.g. commented on the task:
message = message + body + ": " message += body + ": "
# e.g. https://phab.lubuntu.me/T40#779 # e.g. https://phab.lubuntu.me/T40#779
message = message + "\x032" + link + "\x03" message += "\x032" + link + "\x03"
# Make sure we can debug this if it goes haywire # Make sure we can debug this if it goes haywire
self.logger.debug(message) self.logger.debug(message)
# Sleep for a fifth of a second, so when we have a bunch of messages we have a buffer # Sleep for a fifth of a second, so when we have a bunch of messages we have a buffer
@ -127,7 +144,7 @@ class IRCConnector(object):
# Aaaaand, send it off! # Aaaaand, send it off!
self.send_notice(message) self.send_notice(message)
def gettaskinfo(self, task): def get_task_info(self, task):
sendmessage = "" sendmessage = ""
@ -170,7 +187,7 @@ class IRCConnector(object):
if color is not None: if color is not None:
sendmessage += ", " sendmessage += ", "
sendmessage += taskinfo["statusName"] + "\x03\x033]\x03 " sendmessage += taskinfo["statusName"] + "\x03\x033]\x03 "
# Put the title in there as well. # Put the title in there as well.
sendmessage += taskinfo["title"].strip() + ": " sendmessage += taskinfo["title"].strip() + ": "
@ -189,8 +206,14 @@ class IRCConnector(object):
# If someone wrote something like "Tblah", obviously that's not right. # If someone wrote something like "Tblah", obviously that's not right.
except ValueError: except ValueError:
self.send_notice("\x034Error: " + task.strip() + "is an invalid task reference.\x03")
return None if anchor is not None:
link = '{}#{}'.format(task.strip(), anchor)
else:
link = task.strip()
self.send_notice("\x034Error: " + link +\
" is an invalid task reference.\x03")
def bot(self, message, msgtype): def bot(self, message, msgtype):
@ -200,7 +223,7 @@ class IRCConnector(object):
for item in message.split(): for item in message.split():
if item.startswith("T") or item.startwith("D"): if item.startswith("T") or item.startwith("D"):
self.gettaskinfo(item.strip()) self.get_task_info(item.strip())
elif msgtype == "link": elif msgtype == "link":
@ -208,7 +231,7 @@ class IRCConnector(object):
if (item.split()[0].strip().startswith("T")) or \ if (item.split()[0].strip().startswith("T")) or \
(item.split()[0].strip().startswith("D")): (item.split()[0].strip().startswith("D")):
self.gettaskinfo(item.split()[0].strip()) self.get_task_info(item.split()[0].strip())
else: else:
self.sendnotice("\x034Error: unknown command.\x03") self.sendnotice("\x034Error: unknown command.\x03")

@ -11,29 +11,53 @@ Define a launchpad connector class
""" """
# Imports # Imports
import re
import logging import logging
import phabricator import phabricator
from launchpadlib.launchpad import Launchpad import lugito
from string import Template
from launchpadlib.launchpad import Launchpad as lp
class LPConnector(object): BUG_MESSAGE = Template(
"This bug has been marked as fixed in the Git repository: $link\n"
"The commit message is the following: $commit_message\n\n"
"(Note: I am only a bot. If this message was received in error, "
"please contact my owners on the Lubuntu Team.)")
RE_COMMIT_MSG = re.compile(r"lp:\s+\#\d+(?:,\s*\#\d+)*")
class launchpad(object):
def __init__(self, log_level=logging.DEBUG): def __init__(self, log_level=logging.DEBUG):
# Launchpad info # Launchpad info
# Read the configuration out of the .arcconfig file # Read the configuration out of the .lugitorc file
self.application = phabricator.ARCRC['launchpad']['application'] self.application = lugito.config.CONFIG['connectors']\
self.staging = phabricator.ARCRC['launchpad']['staging'] ['launchpad']['application']
self.version = phabricator.ARCRC['launchpad']['version'] self.staging = lugito.config.CONFIG['connectors']\
['launchpad']['staging']
self.version = lugito.config.CONFIG['connectors']\
['launchpad']['version']
self.supported_vers =\ self.supported_vers =\
phabricator.ARCRC['launchpad']['supported_versions'] lugito.config.CONFIG['connectors']\
['launchpad']['supported_versions']
self.package_names =\
lugito.config.CONFIG['connectors']\
['launchpad']['package_names']
# Phabricator info # Phabricator info
self.phab = phabricator.Phabricator() self.phab = phabricator.Phabricator(
self.phab_host = phabricator.ARCRC['config']['default'].replace( host=lugito.config.CONFIG['phabricator']['host'],
token=lugito.config.CONFIG['phabricator']['token'],
)
self.phab_host = lugito.config.CONFIG['phabricator']['host'].replace(
'api/', '') 'api/', '')
self.logger = logging.getLogger('lugito.connector.LPConnector') self.logger = logging.getLogger('lugito.connector.launchpad')
# Add log level # Add log level
ch = logging.StreamHandler() ch = logging.StreamHandler()
@ -50,17 +74,74 @@ class LPConnector(object):
"""Connect""" """Connect"""
self.logger.info("Connecting to Launchpad") self.logger.info("Connecting to Launchpad")
self.lp = lp.login_with(
self.application,
self.staging,
self.version)
def get_package_name(self, name):
"""Need to check"""
def send(self, objectstr, who, body, link): if name in self.package_names:
pass return self.package_names[name]
self.logger.debug('{} is an unsupported repository'.format(
name))
def listen(self): return None
pass
def get_bugs_list(self, link):
"""Get bugs list using a link"""
regex_search = RE_COMMIT_MSG.search(link.lower())
if not regex_search:
self.logger.debug('{} not a commit message'.format(link))
return []
return regex_search.group(0).strip("lp: ").replace("#", "").split(", ")
if __name__ == "__main__": def send(self, *args, **kwargs):
"""Send the commit message"""
obj = LPConnector() if len(args) == 2:
package_name, commit_msg = args
elif len(kwargs) == 2:
commit_msg = kwargs['commit_msg']
package_name = kwargs['package_name']
# else
# raise exception
package_name = self.get_package_name(package_name)
bug_list = self.get_bugs_list(commit_msg)
if package_name and bug_list:
for bug in bug_list:
goodtask = None
bug = self.lp.load("/bugs/" + str(bug).strip())
for task in bug.bug_tasks:
for rel in self.supported_vers:
if package_name + " (Ubuntu " + rel + ")" in task.bug_target_display_name:
goodtask = task
break
if not goodtask:
if package_name + " (Ubuntu)" in task.bug_target_display_name:
goodtask = task
if goodtask:
message = BUG_MESSAGE.substitute(
link=self.phab_host + package_name,
commit_message=commit_msg,
)
bug.newMessage(content=message)
goodtask.status = "Fix Committed"
goodtask.lp_save()
def listen(self):
pass

@ -15,6 +15,7 @@ import hmac
import http import http
import logging import logging
import phabricator import phabricator
import lugito
from hashlib import sha256 from hashlib import sha256
PHAB_WEBHOOK_SIG = "X-Phabricator-Webhook-Signature" PHAB_WEBHOOK_SIG = "X-Phabricator-Webhook-Signature"
@ -31,9 +32,12 @@ class Lugito(object):
Initialise Initialise
""" """
self.phab = phabricator.Phabricator() self.phab = phabricator.Phabricator(
self.HMAC = phabricator.ARCRC['HMAC'] host=lugito.config.CONFIG['phabricator']['host'],
self.host = phabricator.ARCRC['config']['default'] token=lugito.config.CONFIG['phabricator']['token'],
)
self.HMAC = lugito.config.CONFIG['phabricator']['hooks']
self.host = lugito.config.CONFIG['phabricator']['host']
self.logger = logging.getLogger('lugito.lugito') self.logger = logging.getLogger('lugito.lugito')
@ -51,15 +55,7 @@ class Lugito(object):
self.HMAC[key] = bytes(u'%s' % val, 'utf-8') self.HMAC[key] = bytes(u'%s' % val, 'utf-8')
def _transaction_search(self): def validate_request(self, hmac_key, request):
self.transaction = self.phab.transaction.search(objectIdentifier=
self.request_data["object"]["phid"])["data"]
def _request_data(self, request):
self.request_data = json.loads(request.data)
def validate_HMAC(self, hmac_key, request):
""" """
Check a request originated from Phabricator. This method must be called Check a request originated from Phabricator. This method must be called
first to validate a request is from Phabricator before any other method first to validate a request is from Phabricator before any other method
@ -86,8 +82,13 @@ class Lugito(object):
# check if from phabricator # check if from phabricator
if hash_.hexdigest() == request.headers[PHAB_WEBHOOK_SIG]: if hash_.hexdigest() == request.headers[PHAB_WEBHOOK_SIG]:
self._request_data(request)
self._transaction_search() # Store the request and transaction
self.request_data = json.loads(request.data)
self.transaction = self.phab.transaction.search(objectIdentifier=
self.request_data["object"]["phid"])["data"]
self.logger.info('received phid: %s' %\ self.logger.info('received phid: %s' %\
self.request_data["object"]["phid"]) self.request_data["object"]["phid"])
return True return True

@ -17,7 +17,7 @@ import logging
import threading import threading
from flask import Flask, request from flask import Flask, request
from lugito import Lugito from lugito import Lugito
from lugito.connectors.irc import IRCConnector from lugito.connectors import irc, launchpad
# Constants # Constants
GLOBAL_LOG_LEVEL = logging.DEBUG GLOBAL_LOG_LEVEL = logging.DEBUG
@ -26,7 +26,9 @@ GLOBAL_LOG_LEVEL = logging.DEBUG
lugito = Lugito(GLOBAL_LOG_LEVEL) lugito = Lugito(GLOBAL_LOG_LEVEL)
WEBSITE = lugito.host.replace('/api/', '') WEBSITE = lugito.host.replace('/api/', '')
irc_con = IRCConnector() # Connectors
irc_con = irc()
launchpad_con = launchpad()
# Logging # Logging
logger = logging.getLogger('lugito.webhooks') logger = logging.getLogger('lugito.webhooks')
@ -45,11 +47,41 @@ logger.setLevel(GLOBAL_LOG_LEVEL)
app = Flask('lugito') app = Flask('lugito')
@app.route("/commithook", methods=["POST"])
def commithook():
"""Commit hook"""
if lugito.validate_request('commithook', request):
author = lugito.get_author_fullname()
# Without the author we can't continue
if author is None:
return 'Ok'
object_type = lugito.request_data["object"]["type"]
if object_type == "CMIT":
logger.debug("Object is a commit.")
commit_msg = lugito.get_object_string("fullName").replace(
lugito.get_object_string("name") + ": ", "")
pkg_name = lugito.get_object_string("name")
launchpad_con.send(pkg_name, commit_msg)
return 'Ok'
@app.route("/irc", methods=["POST"]) @app.route("/irc", methods=["POST"])
def _main(): def _main():
"""Main route""" """Main route"""
if lugito.validate_HMAC('irc', request): if lugito.validate_request('irc', request):
author = lugito.get_author_fullname() author = lugito.get_author_fullname()
@ -136,6 +168,7 @@ def _main():
def run(): def run():
irc_con.connect() irc_con.connect()
launchpad_con.connect()
t = threading.Thread(target=irc_con.listen) t = threading.Thread(target=irc_con.listen)
t.daemon = True t.daemon = True
t.start() t.start()

@ -1,4 +1,5 @@
[pytest] [pytest]
timeout=100
python_files = tests.py test_*.py *_tests.py python_files = tests.py test_*.py *_tests.py
pytest_plugins = "pytest_cov", "pep8" pytest_plugins = "pytest_cov", "pep8"
addopts = --doctest-modules --cov-config=.coveragerc --cov=lugito --cov-report=term-missing addopts = --doctest-modules --cov-config=.coveragerc --cov=lugito --cov-report=term-missing

@ -17,7 +17,6 @@ keyring==16.0.2
launchpadlib==1.10.6 launchpadlib==1.10.6
lazr.restfulclient==0.14.0 lazr.restfulclient==0.14.0
lazr.uri==1.0.3 lazr.uri==1.0.3
-e git+ssh://git@phab.lubuntu.me:2222/source/lugito.git@165c866a5d5a184b0b36552334fa11aba95b5fb8#egg=lugito
MarkupSafe==1.1.0 MarkupSafe==1.1.0
more-itertools==4.3.0 more-itertools==4.3.0
oauthlib==2.1.0 oauthlib==2.1.0
@ -31,5 +30,7 @@ pytest-cov==2.6.0
SecretStorage==3.1.0 SecretStorage==3.1.0
six==1.11.0 six==1.11.0
testresources==2.0.1 testresources==2.0.1
versioneer==0.18
wadllib==1.3.3 wadllib==1.3.3
Werkzeug==0.14.1 Werkzeug==0.14.1
twine==1.12.1

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""The setup script.""" """The setup script."""
# import versioneer import versioneer
from setuptools import setup, find_packages from setuptools import setup, find_packages
with open('README.rst') as readme_file: with open('README.rst') as readme_file:
@ -29,8 +29,8 @@ test_requirements = [
] ]
setup( setup(
author="", author="Ben Johnston (docEbrown)",
author_email='', author_email='bjohnston@neomailbox.net',
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 2 - Pre-Alpha',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
@ -42,11 +42,11 @@ setup(
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
], ],
description="Python Boilerplate contains all the boilerplate "\ description="Python package to connect services such as irc and launchpad"\
"you need to create a Python package.", " to Phabricator and provide updates",
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'lugito=lugito.cli:run', 'lugito=lugito.webhooks:run',
], ],
}, },
install_requires=requirements, install_requires=requirements,
@ -60,8 +60,7 @@ setup(
test_suite='tests', test_suite='tests',
tests_require=test_requirements, tests_require=test_requirements,
url='', url='',
version='0.1.0',
zip_safe=False, zip_safe=False,
# version=versioneer.get_version(), version=versioneer.get_version(),
# cmdclass=versioneer.get_cmdclass(), cmdclass=versioneer.get_cmdclass(),
) )

@ -4,24 +4,4 @@
"token": "api-nojs2ip33hmp4zn6u6cf72w7d6yh" "token": "api-nojs2ip33hmp4zn6u6cf72w7d6yh"
} }
}, },
"config": {
"default": "http://127.0.0.1:9091/api/"
},
"HMAC": {
"diffhook": "vglzi6t4gsumnilv27r27no7rs3vgs75",
"commithook": "znkyfflbcia5gviqx5ybad7s6uyfywxi"
},
"irc": {
"host": "irc.freenode.net",
"port": "6697",
"username": "someusername",
"password": "somepassword",
"channel": "#somechannel"
},
"launchpad": {
"application": "lugito",
"staging": "production",
"version": "devel",
"supported_versions": ["Cosmic", "Bionic", "Xenial", "Trusty"]
}
} }

@ -0,0 +1,32 @@
[phabricator]
host = http://127.0.0.1:9091/api/
token = api-nojs2ip33hmp4zn6u6cf72w7d6yh
[phabricator.hooks]
diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75
commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi
[connector.irc]
host = irc.freenode.net
port = 6697
username = someusername
password = somepassword
channel = #somechannel
[connector.launchpad]
application = lugito
staging = production
version = devel
supported_versions =
Cosmic
Bionic
Xenial
Trusty
[connector.launchpad.package_names]
rDEFAULTSETTINGS = lubuntu-default-settings
rART = lubuntu-artwork
rCALASETTINGS = calamares-settings-ubuntu
rQTERMINALPACKAGING = qterminal
rLXQTCONFIGPACKAGING = lxqt-config
rNMTRAYPACKAGING = nm-tray

@ -0,0 +1,23 @@
[phabricator]
token = api-nojs2ip33hmp4zn6u6cf72w7d6yh
[phabricator.hooks]
diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75
commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi
[connector.irc]
host = irc.freenode.net
port = 6697
username = someusername
password = somepassword
channel = #somechannel
[connector.launchpad]
application = lugito
staging = production
version = devel
supported_versions =
Cosmic
Bionic
Xenial
Trusty

@ -0,0 +1,20 @@
[phabricator.hooks]
diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75
commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi
[connector.irc]
host = irc.freenode.net
port = 6697
username = someusername
password = somepassword
channel = #somechannel
[connector.launchpad]
application = lugito
staging = production
version = devel
supported_versions =
Cosmic
Bionic
Xenial
Trusty

@ -0,0 +1,23 @@
[phabricator]
host = http://127.0.0.1/api/
[phabricator.hooks]
diffhook = vglzi6t4gsumnilv27r27no7rs3vgs75
commithook = znkyfflbcia5gviqx5ybad7s6uyfywxi
[connector.irc]
host = irc.freenode.net
port = 6697
username = someusername
password = somepassword
channel = #somechannel
[connector.launchpad]
application = lugito
staging = production
version = devel
supported_versions =
Cosmic
Bionic
Xenial
Trusty

@ -0,0 +1 @@
[lugito]

@ -0,0 +1,114 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# S.D.G
"""Test config values
:author: Ben Johnston
"""
# Imports
import os
import pytest
import lugito.config
TEST_FILE = os.path.join(
os.path.dirname(__file__),
'.lugitorc')
TEST_FILE_NO_PHAB = os.path.join(
os.path.dirname(__file__),
'.lugitorc_no_phab')
TEST_FILE_NO_HOST = os.path.join(
os.path.dirname(__file__),
'.lugitorc_no_host')
TEST_FILE_NO_TOKEN = os.path.join(
os.path.dirname(__file__),
'.lugitorc_no_token')
def test_loading_config_hooks():
"""Test loading config"""
lugito.config.update_config(TEST_FILE)
CONFIG = lugito.config.CONFIG
assert(CONFIG['phabricator']['host'] == 'http://127.0.0.1:9091/api/')
assert(CONFIG['phabricator']['token'] == 'api-nojs2ip33hmp4zn6u6cf72w7d6yh')
# Hooks
assert(CONFIG['phabricator']['hooks']['diffhook'] ==\
'vglzi6t4gsumnilv27r27no7rs3vgs75')
assert(CONFIG['phabricator']['hooks']['commithook'] ==\
'znkyfflbcia5gviqx5ybad7s6uyfywxi')
def test_loading_config_connectors():
"""Test loading config connectors"""
lugito.config.update_config(TEST_FILE)
CONFIG = lugito.config.CONFIG
# Connectors
assert(CONFIG['connectors']['irc'] == {
'host': 'irc.freenode.net',
'port': '6697',
'username': 'someusername',
'password':'somepassword',
'channel': '#somechannel',
})
if not (CONFIG['connectors']['launchpad'] == {
'application': 'lugito',
'staging': 'production',
'version': 'devel',
'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'],
'package_names': {
'rDEFAULTSETTINGS': 'lubuntu-default-settings',
'rART': 'lubuntu-artwork',
'rCALASETTINGS': 'calamares-settings-ubuntu',
'rQTERMINALPACKAGING': 'qterminal',
'rLXQTCONFIGPACKAGING': 'lxqt-config',
'rNMTRAYPACKAGING': 'nm-tray',
},
}):
import pdb;pdb.set_trace()
def test_load_config_no_phab():
"""Test loading config to load phabricator"""
with pytest.raises(ValueError) as err:
lugito.config.update_config(TEST_FILE_NO_PHAB)
assert('phabricator section missing from config file' in str(err))
def test_load_config_no_host():
"""Test loading config to load phabricator"""
with pytest.raises(ValueError) as err:
lugito.config.update_config(TEST_FILE_NO_HOST)
assert('host value missing from phabricator section config file' in str(err))
def test_load_config_no_token():
"""Test loading config to load phabricator"""
with pytest.raises(ValueError) as err:
lugito.config.update_config(TEST_FILE_NO_TOKEN)
assert('host value missing from phabricator conffig file' in str(err))

@ -10,25 +10,49 @@ Test IRC connector
import os import os
import json import json
import phabricator import phabricator
from lugito import IRCConnector import lugito
from unittest.mock import MagicMock import pytest
from lugito.connectors import irc
# docEbrown - 20181120
# There is a bug in inspect.unwrap preventing the import of call directly
import unittest.mock
from unittest.mock import MagicMock, patch
# Setup ############################################################### # Setup ###############################################################
TEST_DIR = os.path.dirname(__file__) lugito.config.CONFIG = {
'phabricator': {
'host': 'http://127.0.0.1:9091/api/',
'token': 'api-nojs2ip33hmp4zn6u6cf72w7d6yh',
'hooks': {
'diffhook': 'vglzi6t4gsumnilv27r27no7rs3vgs75',
'commithook': 'znkyfflbcia5gviqx5ybad7s6uyfywxi',
},
},
'connectors': {
'irc': {
'host': 'irc.freenode.net',
'port': '6697',
'username': 'someusername',
'password': 'somepassword',
'channel': '#somechannel',
},
'launchpad': {
'application': 'lugito',
'staging': 'production',
'version': 'devel',
'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'],
},
},
}
# Force phabricator to use the ./tests/.arcconfig file
TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig')
with open(TEST_CONFIG, 'r') as f:
phabricator.ARCRC = json.load(f)
# Tests ############################################################### # Tests ###############################################################
def test_init(): def test_init():
"""Test initialise irc connector""" """Test initialise irc connector"""
obj = IRCConnector() obj = irc()
assert('irc.freenode.net' == obj.host) assert('irc.freenode.net' == obj.host)
assert(6697 == obj.port) assert(6697 == obj.port)
@ -38,12 +62,179 @@ def test_init():
assert('http://127.0.0.1:9091/' == obj.phab_host) assert('http://127.0.0.1:9091/' == obj.phab_host)
def test_connect(): @patch('phabricator.Phabricator')
def test_connect(phab_mock):
"""Test initial connection""" """Test initial connection"""
obj = IRCConnector() obj = irc()
assert(phab_mock.is_called())
@patch('phabricator.Phabricator')
def test_send(phab_mock):
"""Test sending a message"""
obj = irc()
obj.send_notice = MagicMock()
objectstr = "objectstr"
who = "who"
body = "body"
link = "link"
obj.send(objectstr, who, body, link)
obj.send_notice.assert_called_with(
'\x033[\x03\x0313objectstr\x03\x033]\x03 \x0315who\x03 body: \x032link\x03')
@patch('phabricator.Phabricator')
def test_send_kwargs(phab_mock):
"""Test sending a message - kwargs"""
obj = irc()
obj.send_notice = MagicMock()
objectstr = "objectstr"
who = "who"
body = "body"
link = "link"
obj.send(objectstr=objectstr, who=who, body=body, link=link)
obj.send_notice.assert_called_with(
'\x033[\x03\x0313objectstr\x03\x033]\x03 \x0315who\x03 body: \x032link\x03')
def test_connect():
"""Test connect"""
obj = irc(sleep_delay=0)
obj._send_raw = MagicMock()
obj._setup_connection = MagicMock()
obj.conn = MagicMock()
obj.conn.recv.side_effect = [
b'No Ident response',
b'You are now identified',
b'477',
b'433',
b'PING: something',
b'366',
]
obj.connect()
# No Ident response results
assert(unittest.mock.call('NICK someusername1\r\n') in\
obj._send_raw.call_args_list)
assert(unittest.mock.call('USER someusername * * :someusername\r\n') in\
obj._send_raw.call_args_list)
assert(unittest.mock.call('PRIVMSG nickserv :identify someusername'\
' somepassword\r\n') in obj._send_raw.call_args_list)
# Now identified / 477
assert(unittest.mock.call('JOIN #somechannel\r\n') in\
obj._send_raw.call_args_list)
# 433
assert(unittest.mock.call('NICK someusername1\r\n') in\
obj._send_raw.call_args_list)
assert(unittest.mock.call('USER someusername1 * * :someusername1\r\n') in\
obj._send_raw.call_args_list)
# Ping
assert(unittest.mock.call('PONG : something\r\n') in\
obj._send_raw.call_args_list)
# docEbrown - 20181120
# Address including anchors in reference
# https://phab.lubuntu.me/T88#3230
def test_get_task_info_with_anchor():
"""Test getting task info with anchor"""
obj = irc()
obj.send_notice = MagicMock()
obj.phab = MagicMock()
obj.phab.maniphest.info = MagicMock(
return_value={
'priorityColor': 'pink',
'statusName': 'Open',
'title': 'Fix shortcuts related to Super key',
'uri': 'https://phab.lubuntu.me/T154'
}
)
link_with_anchor = 'https://phab.lubuntu.me/T154#3228'
obj.get_task_info(link_with_anchor)
assert(unittest.mock.call(task_id=154) in\
obj.phab.maniphest.info.call_args_list)
assert(obj.send_notice.call_args == \
unittest.mock.call('\x033[\x03\x035Unbreak Now!, Open\x03\x033]\x03 '
'Fix shortcuts related to Super key: '\
'\x032https://phab.lubuntu.me/T154#3228\x03'))
def test_get_diff_info_with_anchor():
"""Test getting diff info with anchor"""
obj = irc()
obj.send_notice = MagicMock()
obj.phab = MagicMock()
obj.phab.differential.query = MagicMock(
return_value=[{
'statusName': 'Closed',
'title': 'Some diff title',
'uri': 'https://phab.lubuntu.me/D24'
},]
)
link_with_anchor = 'https://phab.lubuntu.me/D24#123'
obj.get_task_info(link_with_anchor)
assert(unittest.mock.call(ids=[24]) in\
obj.phab.differential.query.call_args_list)
assert(obj.send_notice.call_args == \
unittest.mock.call('\x033[\x03Closed\x03\x033]\x03 '
'Some diff title: '\
'\x032https://phab.lubuntu.me/D24#123\x03'))
def test_get_task_info_with_error_anchor():
"""Test getting task info with anchor"""
obj = irc()
obj.send_notice = MagicMock()
obj.phab = MagicMock()
obj.phab.maniphest.info = MagicMock(side_effect=ValueError(''))
link_with_anchor = 'https://phab.lubuntu.me/T154#3228'
obj.get_task_info(link_with_anchor)
assert(obj.send_notice.call_args ==
unittest.mock.call('\x034Error: https://phab.lubuntu.me/T154#3228'\
' is an invalid task reference.\x03'))
def test_get_task_info_with_error_no_anchor():
"""Test getting task info with no anchor"""
obj = irc()
obj.send_notice = MagicMock()
obj.phab = MagicMock()
obj.phab.maniphest.info = MagicMock(side_effect=ValueError(''))
obj._socket_conn = MagicMock() link_with_anchor = 'https://phab.lubuntu.me/T154'
# obj.conn.recv = MagicMock(side_effect=[ obj.get_task_info(link_with_anchor)
assert(obj.send_notice.call_args ==
unittest.mock.call('\x034Error: https://phab.lubuntu.me/T154'\
' is an invalid task reference.\x03'))

@ -9,19 +9,90 @@ Test launchpad connector
# Imports # Imports
import json import json
import phabricator import phabricator
from lugito.connectors.launchpad import LPConnector import lugito
from lugito.connectors import launchpad
# Setup ############################################################### # Setup ###############################################################
TEST_DIR = os.path.dirname(__file__) lugito.config.CONFIG = {
'phabricator': {
'host': 'http://127.0.0.1:9091/api/',
'token': 'api-nojs2ip33hmp4zn6u6cf72w7d6yh',
'hooks': {
'diffhook': 'vglzi6t4gsumnilv27r27no7rs3vgs75',
'commithook': 'znkyfflbcia5gviqx5ybad7s6uyfywxi',
},
},
'connectors': {
'irc': {
'host': 'irc.freenode.net',
'port': '6697',
'username': 'someusername',
'password': 'somepassword',
'channel': '#somechannel',
},
'launchpad': {
'application': 'lugito',
'staging': 'production',
'version': 'devel',
'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'],
'package_names': {
'rDEFAULTSETTINGS': 'lubuntu-default-settings',
'rART': 'lubuntu-artwork',
'rCALASETTINGS': 'calamares-settings-ubuntu',
'rQTERMINALPACKAGING': 'qterminal',
'rLXQTCONFIGPACKAGING': 'lxqt-config',
'rNMTRAYPACKAGING': 'nm-tray',
},
},
},
}
# Force phabricator to use the ./tests/.arcconfig file
TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig')
with open(TEST_CONFIG, 'r') as f:
phabricator.ARCRC = json.load(f)
# Tests ############################################################### # Tests ###############################################################
def test_init() def test_init():
"""Test initialising LPConnector"""
obj = launchpad()
assert(obj.application == "lugito")
assert(obj.staging == "production")
assert(obj.version == "devel")
assert(obj.supported_vers == ["Cosmic", "Bionic", "Xenial", "Trusty"])
assert(obj.package_names == {
'rDEFAULTSETTINGS': 'lubuntu-default-settings',
'rART': 'lubuntu-artwork',
'rCALASETTINGS': 'calamares-settings-ubuntu',
'rQTERMINALPACKAGING': 'qterminal',
'rLXQTCONFIGPACKAGING': 'lxqt-config',
'rNMTRAYPACKAGING': 'nm-tray',
})
def test_get_package_name():
"""Test get package name"""
obj = launchpad()
assert(obj.get_package_name('rART') == 'lubuntu-artwork')
assert(obj.get_package_name('rT') is None)
def test_get_package_name():
"""Test getting package name"""
obj = launchpad()
assert(obj.get_package_name('rNMTRAYPACKAGING') == 'nm-tray')
assert(obj.get_package_name('rNMTRKAGING') is None)
def test_get_bugs_list():
"""Test getting buglist"""
obj = launchpad()
assert(obj.get_bugs_list("lp: #1234") == ['1234'])
assert(obj.get_bugs_list("#1234") == [])

@ -12,6 +12,7 @@ import pytest
import phabricator import phabricator
import json import json
import http import http
import lugito
from lugito import Lugito from lugito import Lugito
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -19,11 +20,33 @@ from unittest.mock import MagicMock
TEST_DIR = os.path.dirname(__file__) TEST_DIR = os.path.dirname(__file__)
# Force phabricator to use the ./tests/.arcconfig file # Apply default values
TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig') lugito.config.CONFIG = {
'phabricator': {
'host': 'http://127.0.0.1:9091/api/',
'token': 'api-nojs2ip33hmp4zn6u6cf72w7d6yh',
'hooks': {
'diffhook': 'vglzi6t4gsumnilv27r27no7rs3vgs75',
'commithook': 'znkyfflbcia5gviqx5ybad7s6uyfywxi',
},
},
'connectors': {
'irc': {
'host': 'irc.freenode.net',
'port': '6697',
'username': 'someusername',
'password': 'somepassword',
'channel': '#somechannel',
},
'launchpad': {
'application': 'lugito',
'staging': 'production',
'version': 'devel',
'supported_versions': ['Cosmic', 'Bionic', 'Xenial', 'Trusty'],
},
},
}
with open(TEST_CONFIG, 'r') as f:
phabricator.ARCRC = json.load(f)
# Pre-prepared request # Pre-prepared request
FAKE_REQUEST = os.path.join(TEST_DIR, 'request.json') FAKE_REQUEST = os.path.join(TEST_DIR, 'request.json')
@ -56,10 +79,12 @@ def test_init():
'utf-8')) 'utf-8'))
def test_validate_HMAC(): def test_validate_request():
"""Test validating HMAC""" """Test validating HMAC"""
obj = Lugito() obj = Lugito()
obj.phab = MagicMock()
obj.phab.transaction.search = MagicMock()
request_mock = MagicMock() request_mock = MagicMock()
@ -71,7 +96,8 @@ def test_validate_HMAC():
"a8f636f03ed4464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f" "a8f636f03ed4464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f"
} }
assert(obj.validate_HMAC('diffhook', request_mock)) assert(obj.validate_request('diffhook', request_mock))
assert(obj.phab.transaction.search.is_called())
def test_invalid_HMAC(): def test_invalid_HMAC():
@ -89,7 +115,7 @@ def test_invalid_HMAC():
"a8f6364464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f" "a8f6364464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f"
} }
assert(not obj.validate_HMAC('diffhook', request_mock)) assert(not obj.validate_request('diffhook', request_mock))
def test_author_fullname(): def test_author_fullname():
@ -141,8 +167,6 @@ def test_get_object_type():
with open(FAKE_REQ_DATA, 'r') as f: with open(FAKE_REQ_DATA, 'r') as f:
obj.request_data = json.load(f) obj.request_data = json.load(f)
obj._transaction_search()
assert(obj.get_object_type() == 'DREV') assert(obj.get_object_type() == 'DREV')
def test_is_new_object_false(): def test_is_new_object_false():

Loading…
Cancel
Save