diff --git a/manifests/site.pp b/manifests/site.pp index 562b0bd619..29e537ef86 100644 --- a/manifests/site.pp +++ b/manifests/site.pp @@ -279,6 +279,8 @@ node 'eavesdrop.openstack.org' { statusbot_wiki_password => hiera('statusbot_wiki_password'), statusbot_wiki_url => 'https://wiki.openstack.org/w/api.php', statusbot_wiki_pageid => '1781', + accessbot_nick => hiera('accessbot_nick'), + accessbot_password => hiera('accessbot_nick_password'), } } diff --git a/modules/accessbot/files/accessbot.py b/modules/accessbot/files/accessbot.py new file mode 100755 index 0000000000..6f657a59fb --- /dev/null +++ b/modules/accessbot/files/accessbot.py @@ -0,0 +1,214 @@ +#! /usr/bin/env python + +# Copyright 2011, 2013-2014 OpenStack Foundation +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ConfigParser +import argparse +import irc.client +import logging +import ssl +import sys +import time +import yaml + +logging.basicConfig(level=logging.DEBUG) + + +class SetAccess(irc.client.SimpleIRCClient): + log = logging.getLogger("setaccess") + + def __init__(self, config, noop, nick, password, server, port): + irc.client.SimpleIRCClient.__init__(self) + self.identify_msg_cap = False + self.config = config + self.nick = nick + self.password = password + self.server = server + self.port = int(port) + self.noop = noop + self.channels = [x['name'] for x in self.config['channels']] + self.current_channel = None + self.current_list = [] + self.changes = [] + self.identified = False + if self.port == 6697: + factory = irc.connection.Factory(wrapper=ssl.wrap_socket) + self.connect(self.server, self.port, self.nick, + connect_factory=factory) + else: + self.connect(self.server, self.port, self.nick) + + def on_disconnect(self, connection, event): + sys.exit(0) + + def on_welcome(self, c, e): + self.identify_msg_cap = False + self.log.debug("Requesting identify-msg capability") + c.cap('REQ', 'identify-msg') + c.cap('END') + + def on_cap(self, c, e): + self.log.debug("Received cap response %s" % repr(e.arguments)) + if e.arguments[0] == 'ACK' and 'identify-msg' in e.arguments[1]: + self.log.debug("identify-msg cap acked") + self.identify_msg_cap = True + self.log.debug("Identifying to nickserv") + c.privmsg("nickserv", "identify %s " % self.password) + + def on_privnotice(self, c, e): + if not self.identify_msg_cap: + self.log.debug("Ignoring message because identify-msg " + "cap not enabled") + return + nick = e.source.split('!')[0] + auth = e.arguments[0][0] + msg = e.arguments[0][1:] + if auth == '+' and nick == 'NickServ' and not self.identified: + if msg.startswith('You are now identified'): + self.identified = True + self.advance() + return + if auth != '+' or nick != 'ChanServ': + self.log.debug("Ignoring message from unauthenticated " + "user %s" % nick) + return + self.failed = False + self.advance(msg) + + def _get_access_list(self, channel_name): + ret = {} + channel = None + for c in self.config['channels']: + if c['name'] == channel_name: + channel = c + if channel is None: + raise Exception("Unknown channel %s" % (channel_name,)) + mask = '' + for access, nicks in (self.config['global'].items() + + channel.items()): + if access == 'mask': + mask = self.config['access'].get(nicks) + continue + flags = self.config['access'].get(access) + if flags is None: + continue + for nick in nicks: + ret[nick] = flags + return mask, ret + + def _get_access_change(self, current, target, mask): + remove = '' + add = '' + change = '' + for x in current: + if x in '+-': + continue + if target: + if x not in target: + remove += x + else: + if x not in mask: + remove += x + for x in target: + if x in '+-': + continue + if x not in current: + add += x + if remove: + change += '-' + remove + if add: + change += '+' + add + return change + + def _get_access_changes(self): + mask, target = self._get_access_list(self.current_channel) + self.log.debug("Mask for %s: %s" % (self.current_channel, mask)) + self.log.debug("Target for %s: %s" % (self.current_channel, target)) + all_nicks = set() + current = {} + changes = [] + for nick, flags, msg in self.current_list: + all_nicks.add(nick) + current[nick] = flags + for nick in target.keys(): + all_nicks.add(nick) + for nick in all_nicks: + change = self._get_access_change(current.get(nick, ''), + target.get(nick, ''), mask) + if change: + changes.append('access #%s add %s %s' % (self.current_channel, + nick, change)) + return changes + + def advance(self, msg=None): + if self.changes: + if self.noop: + for change in self.changes: + self.log.info('NOOP: ' + change) + self.changes = [] + else: + change = self.changes.pop() + self.log.info(change) + self.connection.privmsg('chanserv', change) + time.sleep(1) + return + if not self.current_channel: + if not self.channels: + self.connection.quit() + return + self.current_channel = self.channels.pop() + self.current_list = [] + self.connection.privmsg('chanserv', 'access list #%s' % + self.current_channel) + time.sleep(1) + return + if msg.startswith('End of'): + self.changes = self._get_access_changes() + self.current_channel = None + self.advance() + return + parts = msg.split() + if parts[2].startswith('+'): + self.current_list.append((parts[1], parts[2], msg)) + + +def main(): + parser = argparse.ArgumentParser(description='IRC channel access check') + parser.add_argument('-c', dest='config', nargs=1, + help='specify the config file') + parser.add_argument('-l', dest='channels', + default='/etc/irc/channels.yaml', + help='path to the channel config') + parser.add_argument('--noop', dest='noop', + action='store_true', + help="Don't make any changes") + args = parser.parse_args() + + config = ConfigParser.ConfigParser() + config.read(args.config) + + channels = yaml.load(open(args.channels)) + + a = SetAccess(channels, args.noop, + config.get('ircbot', 'nick'), + config.get('ircbot', 'pass'), + config.get('ircbot', 'server'), + config.get('ircbot', 'port')) + a.start() + + +if __name__ == "__main__": + main() diff --git a/modules/openstack_project/files/irc/checkaccess.py b/modules/accessbot/files/checkaccess.py old mode 100644 new mode 100755 similarity index 91% rename from modules/openstack_project/files/irc/checkaccess.py rename to modules/accessbot/files/checkaccess.py index 8e5575c0ea..40384043fa --- a/modules/openstack_project/files/irc/checkaccess.py +++ b/modules/accessbot/files/checkaccess.py @@ -20,9 +20,12 @@ import irc.client import logging import random import string +import ssl import sys +import time import yaml + logging.basicConfig(level=logging.INFO) @@ -82,6 +85,7 @@ class CheckAccess(irc.client.SimpleIRCClient): self.current_list = [] self.connection.privmsg('chanserv', 'access list %s' % self.current_channel) + time.sleep(1) return if msg.startswith('End of'): found = False @@ -108,13 +112,13 @@ class CheckAccess(irc.client.SimpleIRCClient): def main(): parser = argparse.ArgumentParser(description='IRC channel access check') parser.add_argument('-l', dest='config', - default='/etc/irc/channels.yaml', + default='/etc/accessbot/channels.yaml', help='path to the config file') parser.add_argument('-s', dest='server', default='chat.freenode.net', help='IRC server') parser.add_argument('-p', dest='port', - default=6667, + default=6697, help='IRC port') parser.add_argument('nick', help='the nick for which access should be validated') @@ -137,7 +141,13 @@ def main(): a = CheckAccess(channels, args.nick, flags) mynick = ''.join(random.choice(string.ascii_uppercase) for x in range(16)) - a.connect(args.server, int(args.port), mynick) + port = int(args.port) + if port == 6697: + factory = irc.connection.Factory(wrapper=ssl.wrap_socket) + a.connect(args.server, int(args.port), mynick, + connect_factory=factory) + else: + a.connect(args.server, int(args.port), mynick) a.start() if __name__ == "__main__": diff --git a/modules/accessbot/manifests/init.pp b/modules/accessbot/manifests/init.pp new file mode 100644 index 0000000000..e49cf8bbf7 --- /dev/null +++ b/modules/accessbot/manifests/init.pp @@ -0,0 +1,74 @@ +# == Class: accessbot +# +class accessbot( + $nick = '', + $password = '', + $server = '', + $channel_file = '', +) { + + user { 'accessbot': + ensure => present, + home => '/home/accessbot', + shell => '/bin/bash', + gid => 'accessbot', + managehome => true, + require => Group['accessbot'], + } + + group { 'accessbot': + ensure => present, + } + + exec { 'run_accessbot' : + command => '/usr/local/bin/accessbot -c /etc/accessbot/accessbot.config -l /etc/accessbot/channels.yaml >> /var/log/accessbot/accessbot.log 2>&1', + path => '/usr/local/bin:/usr/bin:/bin/', + refreshonly => true, + subscribe => File['/etc/accessbot/channels.yaml'], + require => [File['/etc/accessbot/channels.yaml'], + File['/etc/accessbot/accessbot.config'], + File['/usr/local/bin/accessbot']], + } + + file { '/etc/accessbot': + ensure => directory, + } + + file { '/var/log/accessbot': + ensure => directory, + owner => 'accessbot', + group => 'accessbot', + mode => '0775', + require => User['accessbot'], + } + + file { '/etc/accessbot/accessbot.config': + ensure => present, + content => template('accessbot/accessbot.config.erb'), + group => 'accessbot', + mode => '0440', + owner => 'root', + replace => true, + require => User['accessbot'], + } + + file { '/etc/accessbot/channels.yaml': + ensure => present, + source => $channel_file, + group => 'accessbot', + mode => '0440', + owner => 'root', + replace => true, + require => User['accessbot'], + } + + file { '/usr/local/bin/accessbot': + ensure => present, + source => 'puppet:///modules/accessbot/files/accessbot.py', + group => 'accessbot', + mode => '0440', + owner => 'root', + replace => true, + require => User['accessbot'], + } +} diff --git a/modules/accessbot/templates/accessbot.conf.erb b/modules/accessbot/templates/accessbot.conf.erb new file mode 100644 index 0000000000..a11ea48a5d --- /dev/null +++ b/modules/accessbot/templates/accessbot.conf.erb @@ -0,0 +1,5 @@ +[ircbot] +nick=<%= nick %> +pass=<%= password %> +server=<%= server %> +port=6697 diff --git a/modules/openstack_project/files/accessbot/channels.yaml b/modules/openstack_project/files/accessbot/channels.yaml new file mode 100644 index 0000000000..679bcbee04 --- /dev/null +++ b/modules/openstack_project/files/accessbot/channels.yaml @@ -0,0 +1,117 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# In general, to add a new channel for an official OpenStack project +# to this file, just add the name to the list in "channels" without +# anything else. Stackforge projects can optionally set "mask" to +# "full_mask" to keep full permissions. + +# Global definitions +# First set up the access levels (map names in this file to chanserv flags): +access: + masters: +AFRfiorstv + topics: +t + meetbots: +O + operators: +Afortv + channel_op_mask: +AVOfortv + full_mask: +AFORVfiorstv + +# Define access that should apply to all channels. The label 'mask' +# is special: anyone with perms on a channel that isn't otherwise +# listed for the channel or in the global list will have their access +# limited to the mask but otherwise left alone. +global: + masters: + - openstackinfra + operators: + - SergeyLukjanov + - clarkb + - fungi + - jeblair + - lifeless + - maffulli + - mtaylor + - ttx + topics: + - openstackstatus + mask: + channel_op_mask + +# Individual channel configuration: +channels: + - name: edeploy + mask: full_mask + - name: fuel-dev + mask: full_mask + - name: heat + - name: magnetodb + mask: full_mask + - name: murano + mask: full_mask + - name: openstack + - name: openstack-101 + - name: openstack-anvil + - name: openstack-bacon + - name: openstack-barbican + - name: openstack-board + - name: openstack-ceilometer + - name: openstack-chef + - name: openstack-cinder + - name: openstack-climate + - name: openstack-cloudkeep + - name: openstack-community + - name: openstack-dev + - name: openstack-dns + - name: openstack-doc + - name: openstack-entropy + - name: openstack-foundation + - name: openstack-gantt + - name: openstack-gate + - name: openstack-hyper-v + - name: openstack-infra + - name: openstack-ironic + - name: openstack-keystone + - name: openstack-manila + - name: openstack-marconi + - name: openstack-meeting + meetbots: + - open_stack + - name: openstack-meeting-3 + meetbots: + - open_stack + - name: openstack-meeting-alt + meetbots: + - open_stack + - name: openstack-meniscus + - name: openstack-merges + - name: openstack-metering + - name: openstack-neutron + - name: openstack-nova + - name: openstack-opw + - name: openstack-oslo + - name: openstack-packaging + - name: openstack-qa + - name: openstack-raksha + - name: openstack-relmgr-office + - name: openstack-sdks + - name: openstack-state-management + - name: openstack-swift + - name: openstack-translation + - name: openstack-trove + - name: packstack-dev + - name: refstack + - name: storyboard + - name: syscompass + mask: full_mask + - name: tripleo diff --git a/modules/openstack_project/files/irc/channels.yaml b/modules/openstack_project/files/irc/channels.yaml deleted file mode 100644 index 0492e420da..0000000000 --- a/modules/openstack_project/files/irc/channels.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Global definitions -# First set up the access levels (map names in this file to chanserv flags): -access: - masters: +AFRfiorstv - operators: +Aiortv - topics: +t - -# Define access that should apply to all channels: -global: - masters: - - openstackinfra - operators: - - jeblair - - mtaylor - - clarkb - - fungi - - SergeyLukjanov - - ttx - - reed - topics: - - openstackstatus - -# Individual channel configuration: -channels: - - name: openstack-infra - - name: openstack-meeting - topics: - - open_stack - - name: openstack-nova - operators: - - russellb diff --git a/modules/openstack_project/files/zuul/layout.yaml b/modules/openstack_project/files/zuul/layout.yaml index 9f88e36cc0..6e74675b34 100644 --- a/modules/openstack_project/files/zuul/layout.yaml +++ b/modules/openstack_project/files/zuul/layout.yaml @@ -413,7 +413,7 @@ jobs: - name: gate-config-irc-access voting: false files: - - 'modules/openstack_project/files/irc/channels.yaml' + - 'modules/openstack_project/files/accessbot/channels.yaml' # Continous publishing from master of the following documentation targets: - name: openstack-admin-guide-cloud branch: ^master$ diff --git a/modules/openstack_project/manifests/eavesdrop.pp b/modules/openstack_project/manifests/eavesdrop.pp index fe97fb4354..b060edbdfc 100644 --- a/modules/openstack_project/manifests/eavesdrop.pp +++ b/modules/openstack_project/manifests/eavesdrop.pp @@ -12,6 +12,8 @@ class openstack_project::eavesdrop ( $statusbot_wiki_password = '', $statusbot_wiki_url = '', $statusbot_wiki_pageid = '', + $accessbot_nick = '', + $accessbot_password = '', ) { class { 'openstack_project::server': iptables_public_tcp_ports => [80], @@ -80,4 +82,11 @@ class openstack_project::eavesdrop ( a2mod { 'headers': ensure => present, } + + class { 'accessbot': + nick => $accessbot_nick, + password => $accessbot_password, + server => $statusbot_server, + channel_file => 'puppet:///modules/openstack_project/files/accessbot/channels.yaml', + } } diff --git a/tox.ini b/tox.ini index 23b8faef26..0e815475bc 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ commands = {posargs} [testenv:irc] deps = PyYAML irc -commands = python modules/openstack_project/files/irc/checkaccess.py -l modules/openstack_project/files/irc/channels.yaml openstackinfra +commands = python modules/accessbot/files/checkaccess.py -l modules/openstack_project/files/accessbot/channels.yaml openstackinfra [flake8] show-source = True