Tim Starling has submitted this change and it was merged.
Change subject: Add 'tcpircbot' Puppet class
......................................................................
Add 'tcpircbot' Puppet class
This Puppet class configures a Python script that listens on a TCP socket and
forwards incoming data to an IRC channel. It allows setting up automatic
announcements to IRC from machines in the cluster that do not have a public
interface. The script runs as an upstart service. By default, it connects to
freenode via SSL, and listens on port 9200 for messages to forward.
Sample configuration:
include tcpircbot
tcpircbot::instance { 'announcebot':
password => 'nickserv_secret123',
channel => '#wikimedia-operations',
}
If a "cidr" key is set, it is interpreted as an IPv6 CIDR range. Inbound
connections from addresses outside this range are rejected. If "cidr" is not
set, only private and loopback IP addresses are allowed to connect.
Change-Id: I6d4f661b70e6c4d4111672f5a7f8018389986a18
---
A modules/tcpircbot/files/tcpircbot.py
A modules/tcpircbot/manifests/init.pp
A modules/tcpircbot/manifests/instance.pp
A modules/tcpircbot/templates/tcpircbot.conf.erb
A modules/tcpircbot/templates/tcpircbot.json.erb
5 files changed, 269 insertions(+), 0 deletions(-)
Approvals:
Tim Starling: Verified; Looks good to me, approved
jenkins-bot: Verified
diff --git a/modules/tcpircbot/files/tcpircbot.py
b/modules/tcpircbot/files/tcpircbot.py
new file mode 100755
index 0000000..68129f7
--- /dev/null
+++ b/modules/tcpircbot/files/tcpircbot.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+# -*- coding: utf8 -*-
+"""
+TCP -> IRC forwarder bot
+Forward data from a TCP socket to an IRC channel
+
+Usage: tcpircbot.py CONFIGFILE
+
+CONFIGFILE should be a JSON file with the following structure:
+
+ {
+ "irc": {
+ "channel": "#wikimedia-operations",
+ "network": ["irc.freenode.net", 7000, "serverpassword"],
+ "nickname": "tcpircbot",
+ "ssl": true
+ },
+ "tcp": {
+ "max_clients": 5,
+ "cidr": "::/0",
+ "port": 9125
+ }
+ }
+
+Requires irclib >=0.4.8 <http://bitbucket.org/jaraco/irc>
+Available in Ubuntu as 'python-irclib'
+
+"""
+import sys
+reload(sys)
+sys.setdefaultencoding('utf8')
+
+import atexit
+import codecs
+import json
+import logging
+import os
+import select
+import socket
+
+import netaddr
+
+try:
+ # irclib 0.7+
+ import irc.bot as ircbot
+except ImportError:
+ import ircbot
+
+
+BUFSIZE = 460 # Read from socket in IRC-message-sized chunks.
+
+logging.basicConfig(level=logging.INFO, stream=sys.stderr,
+ format='%(asctime)-15s %(message)s')
+
+
+class ForwarderBot(ircbot.SingleServerIRCBot):
+ """Minimal IRC bot; joins a channel."""
+
+ def __init__(self, network, nickname, channel, **options):
+ ircbot.SingleServerIRCBot.__init__(self, [network], nickname, nickname)
+ self.channel = channel
+ self.options = options
+ for event in ['disconnect', 'join', 'part', 'welcome']:
+ self.connection.add_global_handler(event, self.log_event)
+
+ def connect(self, *args, **kwargs):
+ """Intercepts call to ircbot.SingleServerIRCBot.connect to add support
+ for ssl and ipv6 params."""
+ kwargs.update(self.options)
+ ircbot.SingleServerIRCBot.connect(self, *args, **kwargs)
+
+ def on_privnotice(self, connection, event):
+ logging.info('%s %s', event.source(), event.arguments())
+
+ def log_event(self, connection, event):
+ if connection.real_nickname in [event._source, event._target]:
+ logging.info('%(_eventtype)s [%(_source)s -> %(_target)s]'
+ % vars(event))
+
+ def on_welcome(self, connection, event):
+ connection.join(self.channel)
+
+
+if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help'):
+ sys.exit(__doc__.lstrip())
+
+with open(sys.argv[1]) as f:
+ config = json.load(f)
+
+# Create a TCP server socket
+server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+server.setblocking(0)
+server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+server.bind((config['tcp'].get('iface', ''), config['tcp']['port']))
+server.listen(config['tcp']['max_clients'])
+
+# Create a bot and connect to IRC
+bot = ForwarderBot(**config['irc'])
+bot._connect()
+
+sockets = [server]
+
+def close_sockets():
+ for sock in sockets:
+ try:
+ sock.close()
+ except socket.error:
+ pass
+
+atexit.register(close_sockets)
+
+
+def is_ip_allowed(ip):
+ """Check if we should accept a connection from remote IP `ip`. If
+ the config specifies a CIDR, test against that; otherwise allow only
+ private and loopback IPs.
+ """
+ ip = netaddr.IPAddress(ip)
+ if 'cidr' in config['tcp']:
+ return ip in netaddr.IPNetwork(config['tcp']['cidr'])
+ try:
+ ip = ip.ipv4()
+ except netaddr.core.AddrConversionError:
+ pass
+ return ip.is_private() or ip.is_loopback()
+
+
+while 1:
+ readable, _, _ = select.select([bot.connection.socket] + sockets, [], [])
+ for sock in readable:
+ if sock is server:
+ conn, addr = server.accept()
+ if not is_ip_allowed(addr[0]):
+ conn.close()
+ continue
+ conn.setblocking(0)
+ logging.info('Connection from %s', addr)
+ sockets.append(conn)
+ elif sock is bot.connection.socket:
+ bot.connection.process_data()
+ else:
+ data = sock.recv(BUFSIZE)
+ data = codecs.decode(data, 'utf8', 'replace').strip()
+ if data:
+ logging.info('TCP %s: "%s"', sock.getpeername(), data)
+ bot.connection.privmsg(bot.channel, data)
+ else:
+ sock.close()
+ sockets.remove(sock)
diff --git a/modules/tcpircbot/manifests/init.pp
b/modules/tcpircbot/manifests/init.pp
new file mode 100644
index 0000000..96a6bc5
--- /dev/null
+++ b/modules/tcpircbot/manifests/init.pp
@@ -0,0 +1,52 @@
+# Listens on a TCP port and forwards messages to an IRC channel.
+# Connects to freenode via SSL and listens on TCP port 9200 by default.
+#
+# Example:
+#
+# include tcpircbot
+#
+# tcpircbot::instance { 'announcebot':
+# password => 'nickserv_secret123',
+# channel => '#wikimedia-operations',
+# }
+#
+# You can test it like this:
+#
+# nc localhost 9200 <<<"Hello, IRC!"
+#
+# Logs to /var/log/upstart/tcpircbot-*.log
+#
+class tcpircbot (
+ $user = 'tcpircbot',
+ $group = 'tcpircbot',
+ $dir = '/srv/tcpircbot',
+) {
+ package { [ 'python-irclib', 'python-netaddr' ]:
+ ensure => present,
+ }
+
+ if ! defined(Group[$group]) {
+ group { $group:
+ ensure => present,
+ }
+ }
+
+ if ! defined(User[$user]) {
+ user { $user:
+ ensure => present,
+ gid => $group,
+ shell => '/bin/false',
+ home => $dir,
+ managehome => true,
+ system => true,
+ }
+ }
+
+ file { "${dir}/tcpircbot.py":
+ ensure => present,
+ source => 'puppet:///modules/tcpircbot/tcpircbot.py',
+ owner => $user,
+ group => $group,
+ mode => '0555',
+ }
+}
diff --git a/modules/tcpircbot/manifests/instance.pp
b/modules/tcpircbot/manifests/instance.pp
new file mode 100644
index 0000000..1433f93
--- /dev/null
+++ b/modules/tcpircbot/manifests/instance.pp
@@ -0,0 +1,38 @@
+define tcpircbot::instance(
+ $channel,
+ $password,
+ $nickname = $title,
+ $server_host = 'chat.freenode.net',
+ $server_port = 7000,
+ $ssl = true,
+ $max_clients = 5,
+ $listen_port = 9200,
+) {
+ include tcpircbot
+
+ file { "${tcpircbot::dir}/${title}.json":
+ ensure => present,
+ content => template('tcpircbot/tcpircbot.json.erb'),
+ require => User[$tcpircbot::user],
+ }
+
+ file { "/etc/init/tcpircbot-${title}.conf":
+ ensure => present,
+ content => template('tcpircbot/tcpircbot.conf.erb'),
+ }
+
+ file { "/etc/init.d/tcpircbot-${title}":
+ ensure => link,
+ target => '/lib/init/upstart-job',
+ }
+
+ service { "tcpircbot-${title}":
+ ensure => running,
+ provider => 'upstart',
+ require => [
+ Package['python-irclib'],
+ File["${tcpircbot::dir}/${title}.json"],
+ File["/etc/init/tcpircbot-${title}.conf"]
+ ],
+ }
+}
diff --git a/modules/tcpircbot/templates/tcpircbot.conf.erb
b/modules/tcpircbot/templates/tcpircbot.conf.erb
new file mode 100755
index 0000000..50e25dd
--- /dev/null
+++ b/modules/tcpircbot/templates/tcpircbot.conf.erb
@@ -0,0 +1,17 @@
+# vim: set ft=upstart:
+
+# Upstart job configuration for tcpircbot
+# This file is managed by Puppet
+
+description "TCP socket to IRC bot: <%= @title %>"
+
+start on (local-filesystems and net-device-up IFACE!=lo)
+stop on runlevel [!2345]
+
+setuid <%= scope.lookupvar('tcpircbot::user') %>
+setgid <%= scope.lookupvar('tcpircbot::group') %>
+
+chdir "<%= scope.lookupvar('tcpircbot::dir') %>"
+exec python tcpircbot.py "<%= @title %>.json"
+
+respawn
diff --git a/modules/tcpircbot/templates/tcpircbot.json.erb
b/modules/tcpircbot/templates/tcpircbot.json.erb
new file mode 100755
index 0000000..8c4a82a
--- /dev/null
+++ b/modules/tcpircbot/templates/tcpircbot.json.erb
@@ -0,0 +1,12 @@
+{
+ "irc": {
+ "channel": "<%= @channel %>",
+ "network": ["<%= @server_host %>", <%= @server_port %>, "<%= @nickname
%>:<%= @password %>"],
+ <% if @ssl %>"ssl": true,<% end %>
+ "nickname": "<%= @nickname %>"
+ },
+ "tcp": {
+ "max_clients": <%= @max_clients %>,
+ "port": <%= @listen_port %>
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/61078
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I6d4f661b70e6c4d4111672f5a7f8018389986a18
Gerrit-PatchSet: 3
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Ori.livneh <[email protected]>
Gerrit-Reviewer: Ori.livneh <[email protected]>
Gerrit-Reviewer: Ryan Lane <[email protected]>
Gerrit-Reviewer: Tim Starling <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits