Apache STeVe 常用 CLI 命令与工具详解
详解 Apache STeVe v3 的常用命令行工具与运维手段:uv 依赖环境管理、create-election.py 从 YAML 创建选举、tally.py 计票并输出 STV 当选结果、Election Python API 开启/关闭选举与写入加密选票、sqlite3 检查数据库状态、load-fakedata/mail-voters/whatif 辅助工具与 systemd 服务托管,全部命令与回显均实测
Apache STeVe 常用 CLI 命令与工具详解
目标:详解 Apache STeVe v3 的常用命令行工具——uv 依赖管理、bin/create-election.py 建选举、bin/tally.py 计票、Election Python API 开闭选举与投票、sqlite3 数据库检查,以及 load-fakedata/mail-voters/whatif 辅助工具和 systemctl 服务管理。所有命令与回显均为 Ubuntu 22.04.5 真实环境实测。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- Apache STeVe:v3(pyproject 版本 3.0.0,trunk 提交 3222df7)
- Python:3.12.15(venv,SQLite 3.53.1);uv 0.13.0;系统 SQLite 3.37.2
- 源码目录:
/opt/steve;工具目录:/opt/steve/v3/server/bin/ - 数据库:
/opt/steve/v3/server/steve.db - 服务端口:58383;systemd 服务:steve.service
- 测试选举:E:0948e72d25(yna 议题 I:c7e4b8e7ba,stv 议题 I:c156373ef0,6 名选民)
- 示例管理账号:admin(演示口令 q1w2e3r4,仅作团队演示记忆;STeVe v3 无本地口令登录,认证经 asfquart 对接 Apache OAuth)
二、工具概览
| 工具 | 位置 | 用途 |
|---|---|---|
uv | /root/.local/bin/uv(或 pip 装 /usr/local/bin/uv) | 依赖环境管理与运行器(uv run) |
bin/create-election.py | /opt/steve/v3/server/bin/ | 按 YAML 定义创建选举、议题与选民名单 |
bin/tally.py | 同上 | 对已关闭选举计票(yna 计数 + STV 计票),支持文本/JSON 输出 |
| Python API(steve.election) | venv 内 steve 包 | 开启/关闭选举、写入选票、防篡改检查 |
sqlite3 | 系统包 | 检查/维护 SQLite 数据库(表结构、人员、选举状态) |
bin/load-fakedata.py | 同上 | 生成随机测试选举与议题 |
bin/mail-voters.py | 同上 | 按选举向选民发送邮件(.ezt 模板正文) |
whatif.py | /opt/steve/whatif.py | STV 情景模拟:改席位数、剔除候选人、run-off |
systemctl | 系统自带 | steve.service 服务托管 |
三、前置条件
- 已按《Apache STeVe 安装指南》完成安装:venv 就绪、steve.db 已初始化、person 表已写入人员(含 admin)。
- 所有
uv run命令默认在/opt/steve/v3/server/目录执行(脚本按相对路径定位 steve.db)。 PATH含 uv:export PATH=/root/.local/bin:$PATH(pip 安装的 uv 无需此步)。
四、工具详解
4.1 uv:依赖环境管理
uv --version
回显:
uv 0.13.0 (x86_64-unknown-linux-gnu)
| 命令 | 说明 |
|---|---|
uv venv --python 3.12 | 在 /opt/steve/v3 创建 .venv(自带 SQLite 3.53.1 的独立 Python) |
uv sync --python 3.12 | 按 pyproject/uv.lock 安装全部依赖 |
uv sync --no-dev | 仅装运行依赖(跳过 python-ldap/faker 等开发组) |
uv run <脚本> | 在项目环境内运行脚本(如 uv run main.py、uv run bin/tally.py) |
💡 bin/ 下的脚本首行是
#!/usr/bin/env -S uv run --script,因此既可uv run bin/tally.py也可直接./bin/tally.py执行。
4.2 create-election.py:创建选举
uv run bin/create-election.py --help
回显:
usage: create-election.py [-h] yaml_file
positional arguments:
yaml_file Path to the YAML file defining the election.
options:
-h, --help show this help message and exit
以实测的 election.yaml 创建选举:
uv run bin/create-election.py election.yaml
回显:
INFO:steve.election:Created election[E:0948e72d25]
INFO:__main__:Created election[E:0948e72d25]: "Demo Election 2026" by owner "admin"
INFO:steve.election:Created issue[I:c7e4b8e7ba] in election[E:0948e72d25]
INFO:__main__:Added issue[I:c7e4b8e7ba] to election[E:0948e72d25]
INFO:steve.election:Created issue[I:c156373ef0] in election[E:0948e72d25]
INFO:__main__:Added issue[I:c156373ef0] to election[E:0948e72d25]
INFO:__main__:Added 6 voters to election[E:0948e72d25]
INFO:__main__:Election[E:0948e72d25] fully created from election.yaml
创建后的选举处于 editable 状态;YAML 字段规范见《Apache STeVe 配置参数详解》。
4.3 tally.py:计票
uv run bin/tally.py --help
回显:
usage: tally.py [-h] [--spy-on-open-elections] [--election-id ELECTION_ID]
[--issue-id ISSUE_ID] [--db-path DB_PATH]
[--output {text,json}]
Tally votes for all issues in a closed election (or open if --spy-on-open-elections is used).
options:
-h, --help show this help message and exit
--spy-on-open-elections
Allow tallying of open elections (use with caution).
--election-id ELECTION_ID
Specify Election ID to tally directly (skips interactive selection).
--issue-id ISSUE_ID Specify an Issue ID to tally directly (skips interactive selection).
--db-path DB_PATH Path to the database file.
--output {text,json} Output format for results.
对已关闭选举计票(指定选举 ID、文本输出):
uv run bin/tally.py --election-id 0948e72d25 --output text
回显:
INFO:__main__:Tallying 2 issues ...
Issue c156373ef0: Elect Board Members (stv)
Result: Alice elected
Bob elected
Carlos not elected
David not elected
Details: {'candidates': {'a': True, 'b': True, 'c': False, 'd': False}, 'version': 2, 'labelmap': {'a': ['alice', 'Alice'], 'b': ['bob', 'Bob'], 'c': ['carlos', 'Carlos'], 'd': ['david', 'David']}, 'seats': 2, 'votestrings': ['b,c,a', 'b,a,d', 'd,c,b', 'a,c,b', 'a,b,c']}
----------------------------------------
Issue c7e4b8e7ba: Approve Budget (yna)
Result: Yes: 3
No: 1
Abstain: 1
Details: {'y': 3, 'n': 1, 'a': 1}
----------------------------------------
💡 不带
--election-id时进入交互式选择:列出可计票选举(按 close_at 倒序),输入序号选择,或q退出。默认只计已关闭选举;--spy-on-open-elections允许窥视开启中的选举(调试用)。计票前会执行is_tampered防篡改检查,发现选举被篡改即报错终止。
4.4 Election Python API:开启选举、写入选票与关闭
选举开闭与选票写入无独立 CLI,官方路径是 Web 端点或 Python API。API 等价于 Web 端 do-vote 流程,实测脚本(/opt/steve/v3/server/ 下执行):
import logging
import steve.election
import steve.persondb
logging.basicConfig(level=logging.INFO)
DB = 'steve.db'
EID = '0948e72d25'
pdb = steve.persondb.PersonDB.open(DB)
e = steve.election.Election(DB, EID)
print('state before open:', e.get_state())
e.open(pdb) # editable -> open:生成选举级 salt 与 opened_key,并为每条 mayvote 写盐
print('state after open:', e.get_state())
yna_iid = 'c7e4b8e7ba' # yna 议题:取值 yes/no/abstain
stv_iid = 'c156373ef0' # stv 议题:逗号分隔的偏好序(label 按 kv.labelmap)
yna_ballots = {'alice': 'yes', 'bob': 'no', 'carlos': 'yes', 'david': 'yes', 'eve': 'abstain'}
stv_ballots = {'alice': 'a,b,c', 'bob': 'b,a,d', 'carlos': 'a,c,b', 'david': 'd,c,b', 'eve': 'b,c,a'}
for pid, vs in yna_ballots.items():
e.add_vote(pid, yna_iid, vs) # 同一人重复投票时仅最新一票计入计票
for pid, vs in stv_ballots.items():
e.add_vote(pid, stv_iid, vs)
print('tampered check:', e.is_tampered(pdb))
e.close() # open -> closed:此后元数据/议题/选民均被锁定
print('state after close:', e.get_state())
实测回显:
state before open: editable
state after open: open
vote recorded: alice on c7e4b8e7ba = yes
vote recorded: bob on c7e4b8e7ba = no
vote recorded: carlos on c7e4b8e7ba = yes
vote recorded: david on c7e4b8e7ba = yes
vote recorded: eve on c7e4b8e7ba = abstain
vote recorded: alice on c156373ef0 = a,b,c
vote recorded: bob on c156373ef0 = b,a,d
vote recorded: carlos on c156373ef0 = a,c,b
vote recorded: david on c156373ef0 = d,c,b
vote recorded: eve on c156373ef0 = b,c,a
tampered check: False
state after close: closed
| API | 作用 |
|---|---|
Election.open_database(db_fname) | 打开数据库句柄 |
steve.persondb.PersonDB.open(db_fname) | 打开人员库(open() 的入参) |
Election(db, eid) | 定位选举实例 |
e.get_state() | 返回 editable/open/closed |
e.open(pdb) | 开放投票:生成 salt、opened_key |
e.add_vote(pid, iid, votestring) | 记录选票(加密落库;yna 取值 yes/no/abstain,stv 为逗号分隔标签序) |
e.is_tampered(pdb) | 防篡改校验(True 表示定义被改动,opened_key 失配) |
e.close() | 关闭选举并锁定定义 |
e.tally_issue(iid) | 单议题计票(tally.py 内部即调用它) |
4.5 sqlite3:数据库检查
cd /opt/steve/v3/server
sqlite3 steve.db ".tables"
回显:
election issue mayvote person vote
选举状态(closed=1 表示已关闭):
sqlite3 steve.db "SELECT eid,title,owner_pid,authz,closed FROM election;"
回显:
0948e72d25|Demo Election 2026|admin||1
议题与选票量:
sqlite3 steve.db "SELECT iid,eid,type FROM issue;"
sqlite3 steve.db "SELECT count(*) FROM vote;"
回显:
c7e4b8e7ba|0948e72d25|yna
c156373ef0|0948e72d25|stv
10
⚠️ vote 表中的选票以 vote_token 匿名化并加密存储,直接 SELECT 只能看到密文;人工核对应使用 tally.py。person 表维护:
sqlite3 steve.db "INSERT INTO person (pid, name, email) VALUES ('frank', 'Frank', 'frank@steve.apache.org');"
sqlite3 steve.db "SELECT count(*) FROM person;"
4.6 load-fakedata.py:生成测试数据
uv run bin/load-fakedata.py --help
回显:
options:
-h, --help show this help message and exit
--elections ELECTIONS
The number of elections to create. (default: 10)
--issues ISSUES The number of issues per election to create. (default:
10)
--owner-pid OWNER_PID
The owner's Apache ID to use for created elections. If
not set, pick a random existing person. (default:
None)
💡 每场选举随机生成标题与若干 yna 议题;
--owner-pid须存在于 person 表(不传则随机挑一个已有人员)。该脚本依赖 dev 依赖组的 faker(uv sync全量安装时可用)。
4.7 mail-voters.py:向选民发送邮件
uv run bin/mail-voters.py --help
回显:
options:
-h, --help show this help message and exit
--eid EID Election ID to send emails for. (default: None)
--template TEMPLATE Path to the .ezt template file for email body.
(default: None)
💡 按
--eid取该选举选民名单(person 表 email 字段),用--template指定的 .ezt 模板渲染邮件正文。发送前先在 person 表落实真实邮箱。
4.8 whatif.py:STV 情景模拟
/opt/steve/whatif.py 用于对既有原始选票文件做「如果…会怎样」分析(源码头注释原文):调整席位数、剔除候选人、或仅以指定候选人做 run-off;候选人可按名/姓/无空格全名(不区分大小写)指定。用法示例(源自源码注释):
whatif.py ../Meetings/20110712/raw_board_votes.txt 10
whatif.py ../Meetings/20110712/raw_board_votes.txt -LawrenceRosen
whatif.py ../Meetings/20110712/raw_board_votes.txt 1 kulp noirin geir chris
第一个参数是原始选票文本;后续可跟席位 数、-候选人名(剔除)、或候选人名单(run-off)。
4.9 systemctl:服务管理
systemctl is-active steve.service
回显:
active
systemctl --no-pager -l status steve.service
回显:
● steve.service - Apache STeVe v3 voting server
Loaded: loaded (/etc/systemd/system/steve.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-10-12 05:16:50 UTC; 8s ago
Docs: https://github.com/apache/steve
Main PID: 6997 (python)
Tasks: 5 (limit: 5753)
Memory: 53.7M
CPU: 1.274s
CGroup: /system.slice/steve.service
└─6997 /opt/steve/v3/.venv/bin/python main.py
systemctl restart steve.service # 改 config.yaml 后重启
journalctl -u steve.service -n 20 # 查看最近 20 行日志
五、注意事项
- 计票只认「已关闭」选举:
tally.py默认列出 closed 状态选举,开启中的选举需--spy-on-open-elections显式指定,属调试行为。 - 选举一旦
close(),元数据/议题/选民定义被数据库触发器锁定;开选举前完成全部定义修改。 - 选票以最新一票为准:同一 pid 对同一 iid 重复
add_vote不算多票,计票取最新。 - 数据层操作(开闭选举、投票、计票)不依赖 Web 登录;Web 端
/voter、/admin页面的登录走 asfquart 对接的 Apache OAuth,自建环境不可用时以上述 CLI/API 为准。 uv run前确认当前目录为/opt/steve/v3/server/(脚本按相对路径找 steve.db);换库用--db-path(tally.py)。
六、常见问题
6.1 开选举报 no such function: unixepoch
定位:Python 链接的 SQLite <3.38(Ubuntu 22.04 系统库为 3.37.2),schema.sql 的触发器用到 unixepoch()。
解决:用 uv 独立 Python 3.12 重建 venv:rm -rf /opt/steve/v3/.venv && uv venv --python 3.12 && uv sync --python 3.12,核对该报错消失。
6.2 create-election.py 报 PID frank from record not found in person database
解决:先把该 pid 写入 person 表(见 4.5 的 INSERT 示例),再重跑创建命令。
6.3 tally.py 提示选举被篡改无法计票
定位:is_tampered 检测到选举定义与 opened_key 不再匹配(开选举后改过议题/选民等)。
解决:防篡改机制的设计行为;如确认是误操作导致,修复定义差异后用新选举重走流程,历史选票不可恢复匹配。
6.4 uv run 找不到 uv 命令
解决:方案 A 安装的 uv 在 /root/.local/bin/,先 export PATH=/root/.local/bin:$PATH;或改用 pip 方案(pip3 install uv)装入 /usr/local/bin/。
6.5 端口被占用导致服务起不来
定位:OSError: [Errno 98] Address already in use(systemd 服务与手工进程重复启动时实测复现)。
解决:ss -tlnp | grep 58383 找到占用进程后停掉其一;同一台机器只保留一种启动形态。
七、参考链接
- Apache STeVe 官网:https://steve.apache.org/
- 源码仓库:https://github.com/apache/steve(工具源码:`v3/server/bin/`)
- 数据库 schema 说明:https://github.com/apache/steve/blob/trunk/v3/docs/schema.md
- uv 官方文档:https://docs.astral.sh/uv/
- 专栏其他文档:《Apache STeVe 安装指南》《Apache STeVe 配置参数详解》