Author: gstein
Date: Tue Jun 11 21:17:44 2013
New Revision: 1491966
URL: http://svn.apache.org/r1491966
Log:
Begin conversion of the make_issue.pl script, over to Python.
This isn't 100% done, but most of the way. This commit enables the
team to review the current direction.
The steve.conf file doesn't belong here (maybe in trunk/conf/ and as
steve.conf.example). But it has to go somewhere for now/testing.
* cmdline/make_issue.py: new script, ported from make_issue.pl
* cmdline/steve.conf: temporary config file
Added:
steve/trunk/cmdline/make_issue.py (with props)
steve/trunk/cmdline/steve.conf (with props)
Added: steve/trunk/cmdline/make_issue.py
URL:
http://svn.apache.org/viewvc/steve/trunk/cmdline/make_issue.py?rev=1491966&view=auto
==============================================================================
--- steve/trunk/cmdline/make_issue.py (added)
+++ steve/trunk/cmdline/make_issue.py Tue Jun 11 21:17:44 2013
@@ -0,0 +1,307 @@
+#!/usr/bin/python
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+#
+# make_issue
+#
+# A program for creating issues to be voted upon by the named "group"
+#
+# o must be run by voter user (see wrapsuid.c for setuid wrapper)
+#
+# o creates an issue directory (/home/voter/issues/group/YYYYMMDD-name/),
+# and fills it with files for the issue info, election monitors,
+# and tally file;
+#
+# o creates tables of hash-ids and hashed-hash-ids for use in validating
+# votes, verifying that the values are unique;
+#
+# o mails to the vote monitors an alert message to start tallying;
+#
+# o mails to each voter their hash-id to be used, issue number, and
+# some text explaining the issue.
+#
+
+import sys
+import os
+import argparse
+import re
+import shutil
+
+### how do we want to "properly" adjust path?
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
'../lib')))
+import steve
+
+
+def main():
+ args = parse_argv()
+
+ ### where should we look for this config file?
+ config = steve.load_config('steve.conf')
+
+ # Expand the set of arguments (as a side-effect), if they were not provided
+ # on the cmdline.
+ augment_args(args, config)
+
+ issue_name = '%s-%s-%s' % (args.group, args.start, args.issue)
+
+ # Note: config.issue_dir updated as a side-effect
+ voters = get_voters(args, config)
+
+ # Note: config.issue_dir updated as a side-effect
+ create_issue_dir(issue_name, args, config)
+
+ info_fname = create_info_file(args, config)
+ monitors_hash, hash = build_hash(info_fname, voters, args)
+
+ monitors_fname = create_monitors_file(args, config)
+ type_fname = create_type_file(info_fname, args, config)
+ verify_email(info_fname, args, config)
+ verify_voters(voters, args, config)
+
+ email_monitors()
+ email_voters()
+
+ print "Issue %s with hashcode of %s\nhas been successfully created." \
+ % (issue_name, monitors_hash)
+
+
+def parse_argv():
+ parser = argparse.ArgumentParser(
+ prog=steve.PROG,
+ description='Make an issue for managing an on-line, '
+ 'anonymous voting process',
+ )
+ parser.add_argument('-b', '--batch', action='store_true',
+ help='batch processing: assume "ok" unless errors')
+ parser.add_argument('-g', '--group',
+ help='create an issue for an existing group of voters')
+ parser.add_argument('-s', '--start',
+ help='YYYYMMDD format of date that voting is allowed to
start')
+ parser.add_argument('-i', '--issue',
+ help='append this alphanumeric string to start date '
+ 'as issue name')
+ parser.add_argument('-f', '--file',
+ help='send the contents of this file to each voter '
+ 'as explanation')
+ parser.add_argument('-m', '--monitors',
+ help='e-mail address(es) for sending mail to vote
monitor(s)')
+ parser.add_argument('-v', '--votetype',
+ help='type of vote: yna = Yes/No/Abstain, '
+ 'stvN = single transferable vote for [1-9] slots, '
+ 'selectN = vote for [1-9] of the candidates')
+
+ return parser.parse_args()
+
+
+def augment_args(args, config):
+ "Update ARGS in-place with missing values."
+
+ if args.group is None:
+ args.group = steve.get_input_line('group name for voters on this issue',
True)
+ if not re.match(r'^\w+$', args.group):
+ die('group name must be an alphanumeric token')
+
+ if args.start is None:
+ args.start = steve.get_input_line('YYYYMMDD date that voting starts', True)
+ if not re.match(r'^[2-9]\d\d\d(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$',
+ args.start):
+ die('start date must be formatted as YYYYMMDD, like 20090930')
+
+ if args.issue is None:
+ args.issue = steve.get_input_line('short issue name to append to date',
True)
+ if not re.match(r'^\w+$', args.issue):
+ die('issue name must be an alphanumeric token')
+
+ if args.file is None:
+ args.file = steve.get_input_line('file pathname of issue info on %s'
+ % (config.hostname,),
+ True)
+ args.file = os.path.realpath(args.file)
+ if not os.path.exists(args.file):
+ die('info file does not exist: %s', args.file)
+ if args.file.startswith('/etc/'):
+ die('forbidden to read info files from: /etc')
+ issue_dir = os.path.realpath(config.issue_dir)
+ if args.file.startswith(issue_dir + '/'):
+ die('forbidden to read info files from: %s', issue_dir)
+
+ if args.monitors is None:
+ args.monitors = steve.get_input_line('e-mail address(es) for vote
monitors', True)
+ if '@' not in args.monitors:
+ die('vote monitor must be an Internet e-mail address')
+
+ if args.votetype is None:
+ args.votetype = steve.get_input_line('vote type; yna, stvN, or selectN
(N=1-9)',
+ True)
+ args.votetype = args.votetype.lower()
+ if args.votetype == 'yna':
+ args.selector = 0
+ args.style = 'yes, no, or abstain'
+ elif args.votetype.startswith('stv') and args.votetype[3:].isdigit():
+ args.selector = int(args.votetype[3:])
+ args.style = 'single transferable vote for %d slots' % (args.selector,)
+ elif args.votetype.startswith('select') and args.votetype[6:].isdigit():
+ args.selector = int(args.votetype[6:])
+ args.style = 'select %d of the candidates labeled [a-z0-9]' %
(args.selector,)
+ else:
+ die('vote type must be yna, stvN, or selectN (N=[1-9])')
+
+
+def get_voters(args, config):
+ if not os.path.isdir(config.issue_dir):
+ die('cannot find: %s', config.issue_dir)
+
+ config.issue_dir += '/' + args.group
+ if not os.path.isdir(config.issue_dir):
+ die('group "%s" has not been created yet, see votegroup', args.group)
+ if not os.access(config.issue_dir, os.R_OK | os.W_OK | os.X_OK):
+ die('you lack permissions on: %s', config.issue_dir)
+ uid = os.stat(config.issue_dir).st_uid
+ if uid != os.geteuid():
+ die('you are not the effective owner of: %s', config.issue_dir)
+
+ voters = steve.get_group(config.issue_dir + '/voters')
+ if not voters:
+ die('"%s" must be an existing voter group: see votegroup', args.group)
+
+ return voters
+
+
+def create_issue_dir(issue_name, args, config):
+ print 'Creating new issue:', issue_name
+
+ # Note that .issue_dir already has the group.
+ config.issue_dir += '/%s-%s' % (args.start, args.issue)
+ if os.path.exists(config.issue_dir):
+ die('already exists: %s', config.issue_dir)
+ os.mkdir(config.issue_dir, 0700)
+
+
+def create_info_file(args, config):
+ info_fname = config.issue_dir + '/issue'
+
+ ### generate the contents
+ contents = '### contents\n\n'
+ info = open(args.file).read()
+
+ open(info_fname, 'w').write(contents + info)
+
+ return info_fname
+
+
+def build_hash(info_fname, voters, args):
+ while True:
+ issue_id = steve.filestuff(info_fname)
+ monitors_hash = steve.get_hash_of('%s:%s' % (issue_id, args.monitors))
+
+ hash = { }
+ for voter in voters:
+ h1 = steve.get_hash_of('%s:%s' % (issue_id, voter))
+ h2 = steve.get_hash_of('%s:%s' % (issue_id, h1))
+
+ hash[voter] = (h1, h2)
+ hash[h1] = voter
+ hash[h2] = voter
+
+ if len(hash) == 3 * len(voters):
+ return monitors_hash, hash
+
+ _ = steve.get_input_line('anything to retry collision-free hash', True)
+ open(info_fname, 'a').write('')
+
+
+def create_monitors_file(args, config):
+ monitors_fname = config.issue_dir + '/monitors'
+
+ open(monitors_fname, 'w').write(args.monitors + '\n')
+
+ return monitors_fname
+
+
+def create_type_file(info_fname, args, config):
+ type_fname = config.issue_dir + '/vote_type'
+
+ f = open(type_fname, 'w')
+ f.write('%s\n' % (args.votetype,))
+
+ if args.selector == 0:
+ ballots = ('yes', 'no', 'abstain')
+ else:
+ ballots = steve.ballots(open(info_fname).readlines())
+
+ f.writelines(option + '\n' for option in ballots)
+
+ return type_fname
+
+
+def verify_email(info_fname, args, config):
+ print 'Here is the issue information to be sent to each voter:'
+
+ contents = open(info_fname).read() + _explain_vote('unique-hash-key')
+ _basic_verify(contents, args, config)
+
+
+def verify_voters(voters, args, config):
+ print 'Here is the list of voter e-mail addresses:'
+
+ contents = '\n'.join(voters) + '\n'
+ _basic_verify(contents, args, config)
+
+
+def _basic_verify(contents, args, config):
+ print '=============================================================='
+ print contents
+ print '=============================================================='
+
+ # No need to stop and verify.
+ if args.batch:
+ return
+
+ while True:
+ answer = steve.get_input_line('"ok" to accept, or "abort" to delete
issue', False)
+ answer = answer.lower()
+ if answer == 'abort':
+ shutil.rmtree(config.issue_dir)
+ sys.exit(1)
+ if answer == 'ok':
+ return
+
+
+def email_monitors():
+ pass
+
+
+def email_voters():
+ pass
+
+
+def _explain_vote(key):
+ return '### explain vote for: %s\n' % (key,)
+
+
+### keep this? not sure that exceptions would be helpful since there is likely
+### no intent to trap them.
+def die(msg, *args):
+ "Print an error message and exit with failure."
+
+ print '%s: %s' % (steve.PROG, msg % args)
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ os.umask(0077)
+ main()
Propchange: steve/trunk/cmdline/make_issue.py
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: steve/trunk/cmdline/make_issue.py
------------------------------------------------------------------------------
svn:executable = *
Added: steve/trunk/cmdline/steve.conf
URL:
http://svn.apache.org/viewvc/steve/trunk/cmdline/steve.conf?rev=1491966&view=auto
==============================================================================
--- steve/trunk/cmdline/steve.conf (added)
+++ steve/trunk/cmdline/steve.conf Tue Jun 11 21:17:44 2013
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+[general]
+UID_NAME = voter
+GID_NAME = voter
+HOME = /tmp
+ISSUE_DIR = %(HOME)s/issues
+HOSTNAME = people.apache.org
+EMAIL = [email protected]
Propchange: steve/trunk/cmdline/steve.conf
------------------------------------------------------------------------------
svn:eol-style = native