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 STeVeApache STeVe2026/10/12

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.pySTV 情景模拟:改席位数、剔除候选人、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 找到占用进程后停掉其一;同一台机器只保留一种启动形态。

七、参考链接