Bootstrap Actions#
本文将详细介绍我们是如何实现 bootstrap 的业务逻辑的.
Important
每个 bootstrap action (小步骤) 都对应着一个 Python 函数. 每一步的详细说明都写在了 Python 函数的 doc string 中, 你可以直接点击链接跳转.
1. Configure Ubuntu#
这一步主要是对 Ubuntu 系统进行一些配置, 下面有几个小步骤. 下面是业务逻辑的源代码.
s0_configure_ubuntu/impl.py
1# -*- coding: utf-8 -*-
2
3"""
4todo: add docstring
5"""
6
7import subprocess
8
9from light_emoji import common
10
11from ...logger import logger
12from ...logger_root import get_logger
13
14from .paths import (
15 path_20auto_upgrade_source,
16 path_20auto_upgrade_target,
17 path_wserver_run_on_restart_sh_source,
18 path_wserver_run_on_restart_sh_target,
19)
20
21
22@logger.start_and_end(msg="{func_name}")
23def disable_ubuntu_auto_upgrade():
24 """
25 关闭 Ubuntu 的自动更新. 主要是为了防止 Ubuntu 更新 MySQL Client 的小版本. 因为游戏服务器
26 要求 MySQL Client 的版本跟服务器编译时的版本一摸一样 (数据库的版本可以比核心和 MySQL client 高).
27
28 **如何判断 Auto Upgrade 是否已经被禁用**
29
30 .. code-block:: bash
31
32 cat /etc/apt/apt.conf.d/20auto-upgrades
33
34 如果你看到了如下内容, 说明 **自动升级没有被禁用**
35
36 .. code-block::
37
38 APT::Periodic::Update-Package-Lists "1";
39 APT::Periodic::Unattended-Upgrade "1";
40
41 如果你看到了如下内容, 说明 **自动升级已经被禁用**
42
43 .. code-block::
44
45 APT::Periodic::Update-Package-Lists "0";
46 APT::Periodic::Unattended-Upgrade "0";
47
48 Reference:
49
50 - https://askubuntu.com/questions/1322292/how-do-i-turn-off-automatic-updates-completely-and-for-real
51 """
52 file_logger = get_logger()
53 file_logger.debug(
54 f"{common.play_or_pause} disable ubuntu auto upgrade, this step requires sudo"
55 )
56 file_logger.debug(
57 f"copy {path_20auto_upgrade_source} to {path_20auto_upgrade_target}"
58 )
59 logger.info(f"Apply changes to {path_20auto_upgrade_target}")
60
61 args = [
62 "sudo",
63 "cp",
64 f"{path_20auto_upgrade_source}",
65 f"{path_20auto_upgrade_target}",
66 ]
67 subprocess.run(args, check=True)
68
69
70@logger.start_and_end(msg="{func_name}")
71def setup_ec2_run_on_restart_script():
72 """
73 EC2 可以用 User Data 来指定在第一次 Launch EC2 的时候运行一些自动化脚本来配置机器.
74 但是这只限于第一次 Launch. 作为游戏服务器, 我们是有一些自动化脚本需要在每次重启时运行的.
75 这里我们使用了 AWS 官方提议的方法, 将我们需要运行的自动化脚本放到
76 ``/var/lib/cloud/scripts/per-boot/`` 目录下. 这样每次重启的时候, 这个目录下的脚本都会被
77 ``cloud-init`` 这个每个 EC2 都会自动运行的启动程序所运行. 值得注意的是, 默认该目录下
78 的脚本会以 root 用户的身份运行, 而如果你的脚本需要创建一些给 ubuntu 用户使用的文件, 那么
79 就要注意用 ``sudo -H -u ubuntu ...`` 命令来切换用户了.
80
81 这里有三个文件比较重要:
82
83 - ``wserver-run-on-restart.sh``: 这个脚本要被放到 ``/var/lib/cloud/scripts/per-boot/``
84 目录下, 也是每次启动时要运行的自动化脚本. 注, 该脚本在 EC2 第一次 Launch 的时候不存在,
85 而是会由 User Data 中的脚本来创建.
86 - ``wserver_run_on_restart.py``: 这个脚本会被 ``wserver-run-on-restart.sh`` 所调用,
87 用来执行不需要 sudo 权限的任务.
88 - ``wserver_run_on_restart_as_sudo.py``: 这个脚本也会被 ``wserver-run-on-restart.sh``
89 所调用,用来执行不需要 sudo 权限的任务.
90
91 Reference:
92
93 - Run commands on your Linux instance at launch: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
94 - How can I utilize user data to automatically run a script with every restart of my Amazon EC2 Linux instance?: https://repost.aws/knowledge-center/execute-user-data-ec2
95 """
96 file_logger = get_logger()
97 file_logger.debug(
98 f"{common.play_or_pause} setup ec2 run on restart script, this step requires sudo"
99 )
100 file_logger.debug(
101 f"copy {path_wserver_run_on_restart_sh_source} to {path_wserver_run_on_restart_sh_target}"
102 )
103 logger.info(f"Create / update {path_wserver_run_on_restart_sh_target}")
104 args = [
105 f"sudo",
106 "cp",
107 f"{path_wserver_run_on_restart_sh_source}",
108 f"{path_wserver_run_on_restart_sh_target}",
109 ]
110 subprocess.run(args)
111
112 file_logger.debug(
113 f"Change mode to executable for {path_wserver_run_on_restart_sh_target}"
114 )
115 logger.info(f"Change mode to executable")
116 args = [
117 f"sudo",
118 "chmod",
119 "777", # +x is just for the file owner, +777 is for everyone
120 f"{path_wserver_run_on_restart_sh_target}",
121 ]
122 subprocess.run(args)
1.1 Disable Ubuntu Auto Upgrade#
1.2 Setup EC2 Run On Restart Script#
2. Configure DB#
这一步主要是对游戏数据库进行一些配置. 下面是业务逻辑的源代码.
s1_configure_db/impl.py
1# -*- coding: utf-8 -*-
2
3"""
4todo: add docstring
5"""
6
7import typing as T
8import subprocess
9from jinja2 import Template
10from pathlib_mate import Path
11
12from light_emoji import common
13from acore_server.api import Server
14
15from ...logger import logger
16from ...logger_ubuntu import get_logger
17
18from .paths import (
19 path_create_mysql_database_aws_rds_sql_template,
20 path_create_mysql_user_aws_rds_sql_template,
21 path_update_realmlist_address_sql_template,
22)
23
24
25# ------------------------------------------------------------------------------
26# low level api
27# ------------------------------------------------------------------------------
28def run_sql(
29 sql: str,
30 host: str,
31 database: T.Optional[str] = None,
32 username: T.Optional[str] = None,
33 password: T.Optional[str] = None,
34 timeout: T.Optional[int] = None,
35 path: Path = Path.home().joinpath("tmp.sql"),
36):
37 """
38 Run a SQL statement via MySQL cli.
39
40 一个调用 MySQL cli 来运行大段 SQL 的函数. 本质上是 CLI 命令的封装
41 """
42 args = [
43 "sudo",
44 "mysql",
45 f"--host={host}",
46 ]
47 if database:
48 args.append(f"--database={database}")
49 if username:
50 args.append(f"--user={username}")
51 args.append(f"--password={password}")
52 if timeout:
53 args.append(f"--connect-timeout={timeout}")
54 path.write_text(sql)
55 with path.open("r") as f:
56 subprocess.run(args, stdin=f, text=True)
57
58
59def render_create_mysql_database_sql_in_rds_mode(
60 database_username: str,
61 database_password: str,
62) -> str:
63 """
64 Return the SQL statement that can create Azerothcore Database user and
65 initial databases, and grant new user Database permission.
66 """
67 template = Template(path_create_mysql_database_aws_rds_sql_template.read_text())
68 return template.render(
69 database_username=database_username,
70 database_password=database_password,
71 )
72
73
74def render_create_mysql_user_sql_in_rds_mode(
75 database_username: str,
76 database_password: str,
77) -> str:
78 """
79 Return the SQL statement that can create Azerothcore Database user and
80 grant new Database permission (without creating the database).
81 """
82 template = Template(path_create_mysql_user_aws_rds_sql_template.read_text())
83 return template.render(
84 database_username=database_username,
85 database_password=database_password,
86 )
87
88
89def run_create_mysql_database_sql_in_rds_mode(
90 database_username: str,
91 database_password: str,
92 database_host: str,
93 database_admin_username: str = None,
94 database_admin_password: str = None,
95):
96 """
97 为游戏服务器创建数据库 User 账号密码. 并创建三个空数据库.
98
99 :param database_username: 给游戏服务器用的 DB User 账号, 默认是 acore
100 :param database_password: 给游戏服务器用的 DB User 密码
101 :param database_host: RDS Instance 的 Endpoint (不包括 port)
102 :param database_admin_username: RDS Instance 的 master username 默认是 admin
103 这是你创建 RDS 的时候的 admin 账号, 不是给游戏服务器用的 acore 那个.
104 :param database_admin_password: RDS Instance 的 master password
105 这是你创建 RDS 的时候的 admin 密码, 不是给游戏服务器用的 acore 那个.
106 """
107 sql = render_create_mysql_database_sql_in_rds_mode(
108 database_username=database_username,
109 database_password=database_password,
110 )
111 run_sql(
112 sql=sql,
113 host=database_host,
114 username=database_admin_username,
115 password=database_admin_password,
116 timeout=3,
117 )
118
119
120def run_create_mysql_user_sql_in_rds_mode(
121 database_username: str,
122 database_password: str,
123 database_host: str,
124 database_admin_username: str = None,
125 database_admin_password: str = None,
126):
127 """
128 更新给游戏服务器用的数据库 User 账号密码. 常用于密码更改时对已经存在的 EC2 进行重新配置.
129
130 :param database_username: 给游戏服务器用的 DB User 账号, 默认是 acore
131 :param database_password: 给游戏服务器用的 DB User 密码
132 :param database_host: RDS Instance 的 Endpoint (不包括 port)
133 :param database_admin_username: RDS Instance 的 master username 默认是 admin
134 这是你创建 RDS 的时候的 admin 账号, 不是给游戏服务器用的 acore 那个.
135 :param database_admin_password: RDS Instance 的 master password
136 这是你创建 RDS 的时候的 admin 密码, 不是给游戏服务器用的 acore 那个.
137 """
138 sql = render_create_mysql_user_sql_in_rds_mode(
139 database_username=database_username,
140 database_password=database_password,
141 )
142 run_sql(
143 sql=sql,
144 host=database_host,
145 username=database_admin_username,
146 password=database_admin_password,
147 timeout=3,
148 )
149
150
151def render_update_realmlist_address_sql(server_public_ip: str) -> str:
152 """
153 Return the SQL statement that can update the realmlist address.
154 """
155 template = Template(path_update_realmlist_address_sql_template.read_text())
156 return template.render(
157 server_public_ip=server_public_ip,
158 )
159
160
161def run_update_realmlist_address_sql(
162 server_public_ip: str,
163 database_host: str,
164 database_admin_username: str = None,
165 database_admin_password: str = None,
166):
167 """
168 更新 acore_auth.realmlist 表里面的 address 字段. 从而让登录服务器知道如何路由到游戏服务器.
169
170 :param server_public_ip: EC2 的公网 IP 地址
171 :param database_host: RDS Instance 的 Endpoint (不包括 port)
172 :param database_admin_username: RDS Instance 的 master username 默认是 admin
173 这是你创建 RDS 的时候的 admin 账号, 不是给游戏服务器用的 acore 那个.
174 :param database_admin_password: RDS Instance 的 master password
175 这是你创建 RDS 的时候的 admin 密码, 不是给游戏服务器用的 acore 那个.
176 """
177 sql = render_update_realmlist_address_sql(
178 server_public_ip=server_public_ip,
179 )
180 run_sql(
181 sql=sql,
182 host=database_host,
183 database="acore_auth",
184 username=database_admin_username,
185 password=database_admin_password,
186 timeout=3,
187 )
188
189
190# ------------------------------------------------------------------------------
191# high level api
192# ------------------------------------------------------------------------------
193@logger.start_and_end(msg="{func_name}")
194def create_database(server: Server):
195 """
196 在第一次开服的时候, 游戏数据库中是没有 ``acore_auth``, ``acore_characters``,
197 ``acore_world`` 三个数据库的, 我们需要创建他们. 这个函数是幂等的, 也就是说如果某一个数据库已经
198 存在了, 那么这个函数会跳过这个数据库的创建.
199 """
200 file_logger = get_logger()
201 file_logger.debug(
202 f"{common.play_or_pause} Create database user for game server ..."
203 )
204 file_logger.debug(
205 "Create three database acore_auth, acore_characters, acore_world ..."
206 )
207 logger.info("Create database user for game server ...")
208 logger.info("Create three database acore_auth, acore_characters, acore_world ...")
209 run_create_mysql_database_sql_in_rds_mode(
210 database_username=server.config.db_username,
211 database_password=server.config.db_password,
212 database_host=server.metadata.rds_inst.endpoint,
213 database_admin_username="admin",
214 database_admin_password=server.config.db_admin_password,
215 )
216
217
218@logger.start_and_end(msg="{func_name}")
219def create_user(server: Server):
220 """
221 游戏服务器连接数据库不是用的 Admin User (这样安全隐患太大了), 而是用我们创建的 Acore DB User.
222 在第一次开服的时候我们需要创建这些 User 并且给它们对应的 database 的访问权限.
223 并且, 如果我们修改了 configuration, 其中就包含了数据库用户名和密码, 我们同样要删掉
224 旧的 DB User 并重新配置. 这个任务就是做这件事的.
225 """
226 logger.info("Create database user for game server ...")
227 run_create_mysql_user_sql_in_rds_mode(
228 database_username=server.config.db_username,
229 database_password=server.config.db_password,
230 database_host=server.metadata.rds_inst.endpoint,
231 database_admin_username="admin",
232 database_admin_password=server.config.db_admin_password,
233 )
234
235
236@logger.start_and_end(msg="{func_name}")
237def update_realmlist(server: Server):
238 """
239 在 ``acore_auth.realmlist`` 表中我们需要设定我们的游戏服务器的 IP. 这样登录服务器鉴权成功后
240 才能将游戏客户端的连接导向到我们的游戏服务器. 而由于我们的 IP 地址可能在 EC2 重启后发生变化,
241 所以我们需要在每次重启 EC2 后更新这个表.
242 """
243 file_logger = get_logger()
244 file_logger.debug(f"{common.play_or_pause} Update acore_auth.realmlist.address ...")
245 logger.info("Update acore_auth.realmlist.address ...")
246 run_update_realmlist_address_sql(
247 server_public_ip=server.metadata.ec2_inst.public_ip,
248 database_host=server.metadata.rds_inst.endpoint,
249 database_admin_username="admin",
250 database_admin_password=server.config.db_admin_password,
251 )
252
253
254@logger.start_and_end(msg="{func_name}")
255def configure_db(server: Server):
256 """
257 你需要为游戏服务器创建数据库用户才能让游戏服务器和数据库互相认识. 每次启动 EC2 游戏服务器时,
258 如果不是在生产环境, IP 地址还可能会变, 导致我们需要更新 realmlist.address 字段的值.
259 这一步可以自动化配置跟数据库相关的操作.
260
261 See:
262
263 - :func:`create_database`
264 - :func:`update_realmlist`
265 """
266 file_logger = get_logger()
267 file_logger.debug(f"{common.play_or_pause} configure database ...")
268 with logger.nested():
269 create_database(server)
270 update_realmlist(server)
2.1 Create Database#
s1_configure_db/create/create_mysql_database.sql.aws_rds_mode.jinja2
1/*
2这段代码是对 AzerothCore 官方 SQL
3https://github.com/azerothcore/azerothcore-wotlk/blob/master/data/sql/create/create_mysql.sql
4的修改, 以适用 AWS RDS 的使用场景. 我们并没有改变原 SQL 的逻辑, 仅仅是将一些 hardcode 的值参数化了,
5让使用起来更加灵活.
6
7该段代码只适用于于配置一台刚创建的 EC2, 还从未连接到数据库的情况. 如果你要配置一台曾经已经连接过数据库
8的 EC2, 进行例如更新密码等操作, 而无需重新创建数据库, 则你需要用 create_mysql_user.sql.aws_rds_mode.jinja2
9这个 SQL.
10
11该段 SQL 的逻辑为:
12
13- 删除可能已经存在的和我们要创建的 User 一摸一样的 User
14- 创建 User
15- 允许这个 User 能进行各种数据库操作
16- 创建 acore 中需要的三个数据库
17- 允许这个 User 对这三个数据库进行各种操作
18
19可以看出, 该段 SQL 只适用于面对一个空数据库的场景.
20*/
21
22/*
23注意这里不是 @'localhost' 了, 而我们是用的 @'%.%.%.%'. 这个设计本身是用来防止密码泄露之后
24从别的机器上访问数据库. 而因为我们使用的是 EC2 + RDS, 两者必须都有同一个特定的 security group
25才能由 EC2 访问 RDS. 不同环境下的 (blue / green) 的 EC2 和 RDS 是无法互相访问的. 由于
26security group 的防火墙机制发生在先, 而 DB User 的验证机制发生在后, 所以这里就没有必要设置
27复杂的安全策略了. 并且由于 EC2 的 IP 地址会变动, 而且 VPC 的 CIDR block 也可能会改变,
28所以为了方便起见就直接设为允许任意 IP 地址访问了.
29*/
30
31/*
32先删除可能已经存在的同名 User, 这样是为了防止在重复执行这段 SQL 时出错, 以达到幂等性的目的
33*/
34DROP USER IF EXISTS '{{ database_username }}'@'%.%.%.%';
35
36/*
37注意这里 'your_password' 是 acore 用户的密码. 不用默认密码而用你自己的密码有助于增加数据库安全性.
38但你要知道如果黑客能成功的黑进你的 EC2, 就意味着能看到 authserver.conf 文件, 里面可是明文记录了
39你的密码的. 所以你还需要保证 EC2 的安全. 当然在 Key Pair + Security Group 的双重保护下一般是
40没问题的, 除非你自己泄露了 Key Pair, 又不小心让黑客黑进了你的白名单网络.
41*/
42CREATE USER '{{ database_username }}'@'%.%.%.%' IDENTIFIED BY '{{ database_password }}' WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0;
43
44/*
45RDS 不允许用 ALL PRIVILEGES, 下面是我们需要给 user 的权限
46*/
47GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, PROCESS, REFERENCES, INDEX, ALTER, SHOW DATABASES, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER ON * . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
48
49/*
50然后创建三个空数据库. 游戏服务器核心会自动往里面填充数据的.
51*/
52CREATE DATABASE `acore_world` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci;
53CREATE DATABASE `acore_characters` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci;
54CREATE DATABASE `acore_auth` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci;
55
56/*
57RDS 不允许用 ALL PRIVILEGES, 而且这一步是将对特定数据库的权限给 user, 所以这些权限要比前面的全局权限小.
58*/
59GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `acore_world` . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
60GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `acore_characters` . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
61GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `acore_auth` . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
2.2 Create User#
s1_configure_db/create/create_mysql_user.sql.aws_rds_mode.jinja2
1/*
2这段代码是 create_mysql.sql.aws_rds_mode.jinja2 的逻辑子集, 不管创建数据库, 只管创建 User
3并给予权限. 该段代码只适用于配置一台曾经已经连接过数据库的 EC2, 进行例如更新密码等操作, 而不需重新
4创建数据库.
5
6注意这里不是 @'localhost' 了, 而我们是用的 @'%.%.%.%'. 这个设计本身是用来防止密码泄露之后
7从别的机器上访问数据库. 而因为我们使用的是 EC2 + RDS, 两者必须都有同一个特定的 security group
8才能由 EC2 访问 RDS. 不同环境下的 (blue / green) 的 EC2 和 RDS 是无法互相访问的. 由于
9security group 的防火墙机制发生在先, 而 DB User 的验证机制发生在后, 所以这里就没有必要设置
10复杂的安全策略了. 并且由于 EC2 的 IP 地址会变动, 而且 VPC 的 CIDR block 也可能会改变,
11所以为了方便起见就直接设为允许任意 IP 地址访问了.
12*/
13
14/*
15先删除可能已经存在的同名 User, 这样是为了防止在重复执行这段 SQL 时出错, 以达到幂等性的目的
16*/
17DROP USER IF EXISTS '{{ database_username }}'@'%.%.%.%';
18
19/*
20注意这里 'your_password' 是 acore 用户的密码. 不用默认密码而用你自己的密码有助于增加数据库安全性.
21但你要知道如果黑客能成功的黑进你的 EC2, 就意味着能看到 authserver.conf 文件, 里面可是明文记录了
22你的密码的. 所以你还需要保证 EC2 的安全. 当然在 Key Pair + Security Group 的双重保护下一般是
23没问题的, 除非你自己泄露了 Key Pair, 又不小心让黑客黑进了你的白名单网络.
24*/
25CREATE USER '{{ database_username }}'@'%.%.%.%' IDENTIFIED BY '{{ database_password }}' WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0;
26
27/*
28RDS 不允许用 ALL PRIVILEGES, 下面是我们需要给 user 的权限
29*/
30GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, PROCESS, REFERENCES, INDEX, ALTER, SHOW DATABASES, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER ON * . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
31
32/*
33注意, 这里没有创建三个空数据库的步骤.
34*/
35
36/*
37RDS 不允许用 ALL PRIVILEGES, 而且这一步是将对特定数据库的权限给 user, 所以这些权限要比前面的全局权限小.
38*/
39GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `acore_world` . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
40GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `acore_characters` . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
41GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, EVENT, TRIGGER ON `acore_auth` . * TO '{{ database_username }}'@'%.%.%.%' WITH GRANT OPTION;
2.3 Update Realmlist#
s1_configure_db/create/update_realmlist_address.sql.jinja2
1/*
2这段代码更新 realmlist 的 address 字段. 这是因为每次重启开发测试服务器后, 得到的 Public IP
3地址都是不同的 (我们只给生产服务器配备宝贵的 IP). 所以重启之后需要运行这个脚本来更新配置.
4*/
5UPDATE realmlist SET address = '{{ server_public_ip }}' WHERE id = 1;
2.4 Configure DB#
3. Apply Server Config#
这一步主要是对游戏服务器进行一些配置. 下面是业务逻辑的源代码.
s2_apply_server_config/impl.py
1# -*- coding: utf-8 -*-
2
3"""
4todo: add docstring
5"""
6
7import subprocess
8from pathlib_mate import Path
9
10import acore_paths.api as acore_paths
11from acore_conf.api import apply_changes
12from acore_server.api import Server
13
14from light_emoji import common
15
16from ...logger import logger
17from ...logger_ubuntu import get_logger
18
19
20@logger.start_and_end(msg="{func_name}")
21def apply_authserver_conf(server: Server):
22 """
23 从 S3 上拉取 configuration 数据, 并把
24 `acore_server_config.api.Server.authserver_conf <https://acore-server-config.readthedocs.io/en/latest/acore_server_config/config/define/server.html#acore_server_config.config.define.server.Server>`_
25 中的数据应用到 ``authserver.conf`` 文件中.
26 """
27 file_logger = get_logger()
28 file_logger.debug(f"{common.play_or_pause} apply authserver config ...")
29 data = server.config.authserver_conf.copy()
30 data.update(
31 {
32 "LoginDatabaseInfo": f"{server.metadata.rds_inst.endpoint};3306;{server.config.db_username};{server.config.db_password};acore_auth",
33 }
34 )
35 logger.info(
36 f"copy from template: {acore_paths.path_azeroth_server_authserver_conf_dist}"
37 )
38 logger.info(f"create: {acore_paths.path_azeroth_server_authserver_conf}")
39 apply_changes(
40 path_input=acore_paths.path_azeroth_server_authserver_conf_dist,
41 path_output=acore_paths.path_azeroth_server_authserver_conf,
42 data={"authserver": data},
43 )
44
45
46@logger.start_and_end(msg="{func_name}")
47def apply_worldserver_conf(server: Server):
48 """
49 从 S3 上拉取 configuration 数据, 并把
50 `acore_server_config.api.Server.worldserver_conf <https://acore-server-config.readthedocs.io/en/latest/acore_server_config/config/define/server.html#acore_server_config.config.define.server.Server>`_
51 中的数据应用到 ``worldserver.conf`` 文件中.
52 """
53 file_logger = get_logger()
54 file_logger.debug(f"{common.play_or_pause} apply worldserver config ...")
55 data = server.config.worldserver_conf.copy()
56 data.update(
57 {
58 "DataDir": f"{acore_paths.dir_azeroth_server_data}",
59 "LogsDir": f"{acore_paths.dir_azeroth_server_logs}",
60 "LoginDatabaseInfo": f"{server.metadata.rds_inst.endpoint};3306;{server.config.db_username};{server.config.db_password};acore_auth",
61 "WorldDatabaseInfo": f"{server.metadata.rds_inst.endpoint};3306;{server.config.db_username};{server.config.db_password};acore_world",
62 "CharacterDatabaseInfo": f"{server.metadata.rds_inst.endpoint};3306;{server.config.db_username};{server.config.db_password};acore_characters",
63 }
64 )
65 logger.info(
66 f"copy from template: {acore_paths.path_azeroth_server_worldserver_conf_dist}"
67 )
68 logger.info(f"create: {acore_paths.path_azeroth_server_worldserver_conf}")
69 apply_changes(
70 path_input=acore_paths.path_azeroth_server_worldserver_conf_dist,
71 path_output=acore_paths.path_azeroth_server_worldserver_conf,
72 data={"worldserver": data},
73 )
74
75
76@logger.start_and_end(msg="{func_name}")
77def apply_mod_lua_engine_conf(server: Server):
78 """
79 从 S3 上拉取 configuration 数据, 并把
80 `acore_server_config.api.Server.mod_lua_engine_conf <https://acore-server-config.readthedocs.io/en/latest/acore_server_config/config/define/server.html#acore_server_config.config.define.server.Server>`_
81 中的数据应用到 ``mod_LuaEngine.conf`` 文件中.
82 """
83 file_logger = get_logger()
84 file_logger.debug(f"{common.play_or_pause} apply mod lua engine config ...")
85 data = server.config.mod_lua_engine_conf.copy()
86 data.update(
87 {
88 "Eluna.ScriptPath": f"{acore_paths.dir_server_lua_scripts}",
89 }
90 )
91 logger.info(f"copy from template: {acore_paths.path_mod_eluna_conf_dist}")
92 logger.info(f"create: {acore_paths.path_mod_eluna_conf}")
93 apply_changes(
94 path_input=acore_paths.path_mod_eluna_conf_dist,
95 path_output=acore_paths.path_mod_eluna_conf,
96 data={"worldserver": data},
97 )
98
99
100@logger.start_and_end(msg="{func_name}")
101def apply_server_config(server: Server):
102 """
103 Run the following functions:
104
105 - :func:`apply_authserver_conf`
106 - :func:`apply_worldserver_conf`
107 - :func:`apply_mod_lua_engine_conf`
108 """
109 file_logger = get_logger()
110 file_logger.debug(f"{common.play_or_pause} apply server config ...")
111 with logger.nested():
112 apply_authserver_conf(server)
113 apply_worldserver_conf(server)
114 apply_mod_lua_engine_conf(server)
115
116
117@logger.start_and_end(msg="{func_name}")
118def sync_lua_scripts(
119 s3dir_uri: str,
120):
121 """
122 清空本地的 lua_scripts 中的所有 lua 文件, 然后从指定的 S3 dir 中下载所有的 lua 文件到本地.
123 """
124 dir_server_lua_scripts = Path(acore_paths.dir_server_lua_scripts)
125 for path in dir_server_lua_scripts.select_by_ext(".lua"):
126 path.remove()
127 args = [
128 "/home/ubuntu/.pyenv/shims/aws",
129 "s3",
130 "sync",
131 s3dir_uri,
132 str(dir_server_lua_scripts),
133 ]
134 subprocess.run(args, check=True)
3.1 Apply authserver Config#
3.2 Apply worldserver Config#
3.3 Apply mod_lua_engine Config#
3.4 Apply Server Config#
3.5 Sync Lua Scripts#
这一步可以从 S3 上下载 lua 脚本到服务器上的 lua_scripts 目录.
4. Check Server Status#
这一步主要是运行检查游戏服务器健康状态的定时任务. 下面是业务逻辑的源代码.
s3_check_server_status/impl.py
1# -*- coding: utf-8 -*-
2
3"""
4todo: add docstring
5"""
6
7from light_emoji import common
8
9from ...logger import logger
10from ...logger_ubuntu import get_logger
11from ...vendor.screen_session_manager import (
12 run_script,
13 stop_script,
14)
15from .paths import path_server_monitor_sh
16
17
18@logger.start_and_end(msg="{func_name}")
19def run_check_server_status_cron_job():
20 """
21 运行服务器状态健康检查定时脚本. 这个 cron job 的详细信息请参考
22 `measure_server_status <https://acore-soap-app.readthedocs.io/en/latest/search.html?q=measure_server_status&check_keywords=yes&area=default>`_.
23 """
24 file_logger = get_logger()
25 file_logger.debug(
26 f"{common.play_or_pause} run {path_server_monitor_sh} in screen session"
27 )
28 logger.info(f"run {path_server_monitor_sh} in screen session")
29 run_script(path_server_monitor_sh, name="servermonitor", print_func=logger.info)
30
31
32@logger.start_and_end(msg="{func_name}")
33def stop_check_server_status_cron_job():
34 """
35 停止服务器状态健康检查定时脚本. 关闭由 :func:`run_check_server_status_cron_job` 启动的 cron job.
36 """
37 stop_script(name="servermonitor", print_func=logger.info)
4.1 Run Check Server Status Cron Job#
4.2 Run Check Server Status Cron Job#
5. Run Server#
这一步主要运行游戏服务器. 下面是业务逻辑的源代码.
s4_run_server/impl.py
1# -*- coding: utf-8 -*-
2
3"""
4todo: add docstring
5"""
6
7from light_emoji import common
8
9from ...logger import logger
10from ...logger_ubuntu import get_logger
11
12from ...vendor.screen_session_manager import (
13 run_script,
14 list_session as _list_session,
15 enter_session,
16 stop_script,
17)
18from .paths import path_auth_sh, path_world_sh
19
20
21@logger.start_and_end(msg="{func_name}")
22def run_server():
23 """
24 用 screen session 运行 auth 和 world 服务器.
25 """
26 file_logger = get_logger()
27 file_logger.debug(f"{common.play_or_pause} run server ...")
28 logger.info(f"run {path_auth_sh} and {path_auth_sh} in screen session")
29 run_script(path_auth_sh, name="auth", print_func=logger.info)
30 run_script(path_world_sh, name="world", print_func=logger.info)
31
32
33@logger.start_and_end(msg="{func_name}")
34def list_session():
35 """
36 列出所有在运行中的 screen session.
37 """
38 _list_session()
39
40
41@logger.start_and_end(msg="{func_name}")
42def enter_worldserver():
43 """
44 通过 screen session 进入进入 worldserver 的交互式命令行.
45 """
46 enter_session(name="world", print_func=logger.info)
47
48
49@logger.start_and_end(msg="{func_name}")
50def stop_server():
51 """
52 用 screen session 杀死 authserver 和 worldserver.
53 """
54 stop_script(name="auth", print_func=logger.info)
55 stop_script(name="world", print_func=logger.info)
Note
在服务器上长期运行一个进程的核心技术是 GNU Screen Session, 建议仔细阅读 Keep Long Live Session Using GNU Screen 做进一步了解.