ansible相关

一、ansible的基本架构

二、playbook的构成
三、ansible api
四、ansible_playbook api
五、ansible执行原理
六、ansible任务执行性能优化
七、celery+ansible

 

ansible的基本架构

  • 核心:ansible
  • 核心模块(Core Modules):这些都是ansible自带的模块 
  • 扩展模块(Custom Modules):如果核心模块不足以完成某种功能,可以添加扩展模块
  • 插件(Plugins):完成模块功能的补充
  • 剧本(Playbooks):ansible的任务配置文件,将多个任务定义在剧本中,由ansible自动执行
  • 连接插件(Connectior Plugins):ansible基于连接插件连接到各个主机上,虽然ansible是使用ssh连接到各个主机的,但是它还支持其他的连接方法,所以需要有连接插件
  • 主机群(Host Inventory):定义ansible管理的主机

1、管理端支持local 、ssh、zeromq 三种方式连接被管理端,默认使用基于ssh的连接---这部分对应基本架构图中的连接模块;

2、可以按应用类型等方式进行Host Inventory(主机群)分类,管理节点通过各类模块实现相应的操作---单个模块,单条命令的批量执行,我们可以称之为ad-hoc;

3、管理节点可以通过playbooks 实现多个task的集合实现一类功能,如web服务的安装部署、数据库服务器的批量备份等。playbooks我们可以简单的理解为,系统通过组合多条ad-hoc操作的配置文件 。

Ansible Inventory实际上是包含静态Inventory和动态Inventory两部分,静态Inventory指的是在文件/etc/ansible/hosts中指定的主机和组,Dynamic Inventory指通过外部脚本获取主机列表,并按照ansible 所要求的格式返回给ansilbe命令的。这部分一般会结合CMDB资管系统、zabbix 监控系统、crobble安装系统、云计算平台等获取主机信息。由于主机资源一般会动态的进行增减,而这些系统一般会智能更新。我们可以通过这些工具提供的API 或者接入库查询等方式返回主机列表。

ansbile-playbook是一系统ansible命令的集合,其利用yaml 语言编写,运行过程,ansbile-playbook命令根据自上而下的顺序依次执行。同时,playbook开创了很多特性,它可以允许你传输某个命令的状态到后面的指令,如你可以从一台机器的文件中抓取内容并附为变量,然后在另一台机器中使用,这使得你可以实现一些复杂的部署机制,这是ansible命令无法实现的。

playbook通过ansible-playbook命令使用,它的参数和ansible命令类似,如参数-k(–ask-pass) 和 -K (–ask-sudo) 来询问ssh密码和sudo密码,-u指定用户,这些指令也可以通过规定的单元写在playbook 。ansible-playbook的简单使用方法: ansible-playbook example-play.yml 。

二、playbook的构成

playbook是由一个或多个“play”组成的列表。play的主要功能在于将事先归并为一组的主机装扮成事先通过ansible中的task定义好的角色。从根本上来讲所谓task无非是调用ansible的一个module。将多个play组织在一个playbook中即可以让它们联同起来按事先编排的机制同唱一台大戏。其主要有以下四部分构成

  1. playbooks组成:
  2. Target section: 定义将要执行 playbook 的远程主机组
  3. Variable section: 定义 playbook 运行时需要使用的变量
  4. Task section: 定义将要在远程主机上执行的任务列表
  5. Handler section: 定义 task 执行完成以后需要调用的任务

而其对应的目录层为五个,如下:

  1. 一般所需的目录层有:(视情况可变化)
  2. vars 变量层
  3. tasks 任务层
  4. handlers 触发条件
  5. files 文件
  6. template 模板

下面介绍下构成playbook 的四层结构。

1、Hosts和Users

playbook中的每一个play的目的都是为了让某个或某些主机以某个指定的用户身份执行任务。

hosts 用于指定要执行指定任务的主机其可以是一个或多个由冒号分隔主机组。

remote_user 则用于指定远程主机上的执行任务的用户。
不过remote_user也可用于各task中。也可以通过指定其通过sudo的方式在远程主机上执行任务其可用于play全局或某任务。
此外甚至可以在sudo时使用sudo_user指定sudo时切换的用户。

示例:

  1. - hosts: webnodes
  2. tasks:
  3. - name: test ping connection:
  4. remote_user: test
  5. sudo: yes
2、任务列表和action

play的主体部分是task list。

task list中的各任务按次序逐个在hosts中指定的所有主机上执行即在所有主机上完成第一个任务后再开始第二个。在运行自下而下某playbook时如果中途发生错误所有已执行任务都将回滚因此在更正playbook后重新执行一次即可。 

task的目的是使用指定的参数执行模块而在模块参数中可以使用变量。模块执行是幂等的这意味着多次执行是安全的因为其结果均一致。每个task都应该有其name用于playbook的执行结果输出建议其内容尽可能清晰地描述任务执行步骤。如果未提供name则action的结果将用于输出。 

定义task的可以使用“action: module options”或“module: options”的格式推荐使用后者以实现向后兼容。如果action一行的内容过多也中使用在行首使用几个空白字符进行换行。

  1. tasks:
  2. - name: make sure apache is running
  3. service: name=httpd state=running
  4. 在众多模块中只有command和shell模块仅需要给定一个列表而无需使用“key=value”格式例如
  5. tasks:
  6. - name: disable selinux
  7. command: /sbin/setenforce 0 如果命令或脚本的退出码不为零可以使用如下方式替代
  8. tasks:
  9. - name: run this command and ignore the result
  10. shell: /usr/bin/somecommand || /bin/true
  11. 或者使用ignore_errors来忽略错误信息
  12. tasks:
  13. - name: run this command and ignore the result
  14. shell: /usr/bin/somecommand
  15. ignore_errors: True
3、handlers 

用于当关注的资源发生变化时采取一定的操作。
“notify”这个action可用于在每个play的最后被触发这样可以避免多次有改变发生时每次都执行指定的操作取而代之仅在所有的变化发生完成后一次性地执行指定操作。
在notify中列出的操作称为handler也即notify中调用 handler中定义的操作。 

注意:在 notify 中定义内容一定要和tasks中定义的 - name 内容一样,这样才能达到触发的效果,否则会不生效。

  1. - name: template configuration file
  2. template: src=template.j2 dest=/etc/foo.conf
  3. notify:
  4. - restart memcached
  5. - restart apache
  6. handler是task列表这些task与前述的task并没有本质上的不同。
  7. handlers:
  8. - name: restart memcached
  9. service: name=memcached state=restarted
  10. - name: restart apache
  11. service: name=apache state=restarted
4、tags

tags用于让用户选择运行或略过playbook中的部分代码。ansible具有幂等性因此会自动跳过没有变化的部分即便如此有些代码为测试其确实没有发生变化的时间依然会非常地长。
此时如果确信其没有变化就可以通过tags跳过此些代码片断。

并发运行

ansible默认只会创建5个进程,所以一次任务只能同时控制5台机器执行.那如果你有大量的机器需要控制,或者你希望减少进程数,那你可以采取异步执行.ansible的模块可以把task放进后台,然后轮询它.这使得在一定进程数下能让大量需要的机器同时运作起来.

使用async和poll这两个关键字便可以并行运行一个任务. async这个关键字触发ansible并行运作任务,而async的值是ansible等待运行这个任务的最大超时值,而poll就是ansible检查这个任务是否完成的频率时间.

如果你希望在整个集群里面平行的执行一下updatedb这个命令.使用下面的配置

  1. - hosts: all
  2. tasks:
  3. - name: Install mlocate
  4. yum: name=mlocate state=installed
  5. - name: Run updatedb
  6. command: /usr/bin/updatedb
  7. async: 300
  8. poll: 10

你会发现当你使用上面的例子控制超过5台机器的时候,command.在上面yum模块会先在5台机器上跑,完成后再继续下面的机器.而上面command模块的任务会一次性在所有机器上都执行了,然后监听它的回调结果

如果你的command是控制机器开启一个进程放到后台,那就不需要检查这个任务是否完成了.你只需要继续其他的动作,最后再使用wait_for这个模块去检查之前的进程是否按预期中开启了便可.只需要把poll这个值设置为0,便可以按上面的要求配置ansible不等待job的完成.

最后,或者你还有一种需求是有一个task它是需要运行很长的时间,那你需要设置一直等待这个job完成.这个时候你把async的值设成0便可.

总结来说,大概有以下的一些场景你是需要使用到ansible的polling特性的

  1. 你有一个task需要运行很长的时间,这个task很可能会达到timeout.
  2. 你有一个任务需要在大量的机器上面运行
  3. 你有一个任务是不需要等待它完成的

当然也有一些场景是不适合使用polling特性的

  1. 你的这个任务是需要运行完后才能继续另外的任务的
  2. 你的这个任务能很快的完成

Looping

在ansible你能够通过不同的输入去重复的执行同一个模块,举个例子,你需要管理几个具有相同权限的文件.你能够用一个for循环迭代一个facts或者variables去减少你的重复劳动.

使用with_items这个关键字就可以完成迭代一个列表.列表里面的每个变量都叫做item.有一些模块譬如yum,它就支持使用with_items去安装一列表的包,而不需要写好多个yum的task

下面来一个with_items的例子

  1. tasks:
  2. - name: Secure config files
  3. file: path=/etc/{{ item }} mode=0600 owner=root group=root
  4. with_items:
  5. - my.cnf
  6. - shadow
  7. - fstab

除了使用items轮训,ansible还有一种方式是lookup插件.这些插件可以让ansible从外部取得数据,例如,你或许希望可以通过一种特定模式去上传你的文件.

在这个例子里面,我们会上传所有的public keys到一个目录,然后聚合它们到一个authorized_keys文件

  1. tasks: #1
  2. - name: Make key directory #2
  3. file: path=/root/.sshkeys ensure=directory mode=0700
  4. owner=root group=root #3
  5. - name: Upload public keys #4
  6. copy: src={{ item }} dest=/root/.sshkeys mode=0600
  7. owner=root group=root #5
  8. with_fileglob: #6
  9. - keys/*.pub #7
  10. - name: Assemble keys into authorized_keys file #8
  11. assemble: src=/root/.sshkeys dest=/root/.ssh/authorized_keys
  12. mode=0600 owner=root group=root #9

loop模块一般在下面的场景中使用

  1. 类似的配置模块重复了多遍
  2. fact是一个列表
  3. 创建多个文件,然后使用assemble聚合成一个大文件
  4. 使用with_fileglob匹配特定的文件管理

三、ansible api

ansible api 的使用非常强大,也非常简单,只不过把模块需要使用的参数写到了脚本中,这里先来看下官方给的示例,不过同于官方的是,我这里增我将结果进行了json美化输出。

 

  1. [root@361way api]# cat test_api.py
  2. #!/usr/bin/env python
  3. # coding=utf-8
  4. import ansible.runner
  5. import json
  6. runner = ansible.runner.Runner(
  7. module_name='ping',
  8. module_args='',
  9. pattern='all',
  10. forks=10
  11. )
  12. datastructure = runner.run()
  13. data = json.dumps(datastructure,indent=4)
  14. print data

其输出结果如下:

ansible-api

注:如果主机是不通或失败的,结果将会输出到dark部分里,一个含有失败主机的结果类似如下:

 

  1. {
  2. "dark" : {
  3. "web1.example.com" : "failure message"
  4. },
  5. "contacted" : {
  6. "web2.example.com" : 1
  7. }
  8. }

再为看下第二个示例:

 

  1. #!/usr/bin/python
  2. import ansible.runner
  3. import sys
  4. # construct the ansible runner and execute on all hosts
  5. results = ansible.runner.Runner(
  6. pattern='*', forks=10,
  7. module_name='command', module_args='/usr/bin/uptime',
  8. ).run()
  9. if results is None:
  10. print "No hosts found"
  11. sys.exit(1)
  12. print "UP ***********"
  13. for (hostname, result) in results['contacted'].items():
  14. if not 'failed' in result:
  15. print "%s >>> %s" % (hostname, result['stdout'])
  16. print "FAILED *******"
  17. for (hostname, result) in results['contacted'].items():
  18. if 'failed' in result:
  19. print "%s >>> %s" % (hostname, result['msg'])
  20. print "DOWN *********"
  21. for (hostname, result) in results['dark'].items():
  22. print "%s >>> %s" % (hostname, result)

上面的示例中对主机的输出结果进行了判断,并且结果的输出进行了定制化,上面执行的结果你可以和ansible all -m command -a 'uptime' 的结果进行下比对,看下有什么不同。

上面的示例基本上都是参照官方页面进行执行的,更多用法可以通过pydoc ansible或者通过python里的help(ansible)查看。另外在多主机执行时,可以使用async(异部)方式运行。

四、ansible_playbook api

ansible_playbook api 部分在官方文档上并没有提,不过通过查看ansible模块的帮助信息可以看到其是支持的。在ansible google论坛里(需FQ),有老外也给出里代码,其实它和执行ansible的api方式一样,只是多了个几个参数:

 

  1. import ansible.playbook
  2. from ansible import callbacks
  3. from ansible import utils
  4. stats = callbacks.AggregateStats()
  5. playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
  6. runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
  7. pb = ansible.playbook.PlayBook(
  8. playbook="nseries.yml",
  9. stats=stats,
  10. callbacks=playbook_cb,
  11. runner_callbacks=runner_cb,
  12. check=True
  13. )
  14. for (play_ds, play_basedir) in zip(pb.playbook, pb.play_basedirs):
  15. import ipdb
  16. ipdb.set_trace()
  17. # Can play around here to see what's going on.
  18. pb.run()

 

大致看了下代码,在用api的方式执行playbook的时候,playbook,stats,callbacks,runner_callbacks这几个参数是必须的。不使用的时候会报错。

 

  1. arguments = []
  2. if playbook is None:
  3. arguments.append('playbook')
  4. if callbacks is None:
  5. arguments.append('callbacks')
  6. if runner_callbacks is None:
  7. arguments.append('runner_callbacks')
  8. if stats is None:
  9. arguments.append('stats')
  10. if arguments:
  11. raise Exception('PlayBook missing required arguments: %s' % ', '.join(arguments))

playbook用来指定playbook的yaml文件

stats用来收集playbook执行期间的状态信息,最后会进行汇总

callbacks用来输出playbook执行的结果

runner_callbacks用来输出playbook执行期间的结果。但是它返回的结果太简单,我想让它详细点,如果用自定义callback的方法插入到mongo里面的话也行,或者是直接输出,但是我想所有task都执行完后,把每个task的详细信息输出到终端上,最后发现结果输出都是靠callbacks.py里的AggregateStats这个类,在每执行完一个task后,都会调用AggregateStats进行计算,汇总。

 

  1. [root@361way api]# cat playbook_api.py
  2. #!/usr/bin/env python
  3. # coding=utf-8
  4. import ansible.playbook
  5. from ansible import callbacks
  6. from ansible import utils
  7. import json
  8. stats = callbacks.AggregateStats()
  9. playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
  10. runner_cb = callbacks.PlaybookRunnerCallbacks(stats,verbose=utils.VERBOSITY)
  11. res=ansible.playbook.PlayBook(
  12. playbook='/etc/ansible/playbooks/user.yml',
  13. stats=stats,
  14. callbacks=playbook_cb,
  15. runner_callbacks=runner_cb
  16. ).run()
  17. data = json.dumps(res,indent=4)
  18. print data
  19. # 执行结果如下:
  20. [root@361way api]# python playbook_api.py
  21. PLAY [create user] ************************************************************
  22. TASK: [create test "{{ user }}"] **********************************************
  23. changed: [10.212.52.16]
  24. changed: [10.212.52.14]
  25. {
  26. "10.212.52.16": {
  27. "unreachable": 0,
  28. "skipped": 0,
  29. "ok": 1,
  30. "changed": 1,
  31. "failures": 0
  32. },
  33. "10.212.52.14": {
  34. "unreachable": 0,
  35. "skipped": 0,
  36. "ok": 1,
  37. "changed": 1,
  38. "failures": 0
  39. }
  40. }
  41. [root@361way api]#

三、总结

 从上面的例子来看,感觉作用似乎有点鸡肋。多条ansible shell 指令的执行可以写成playbook 来执行,ansbile-playbook 也可以通过include 调用子playbook ,似乎API 部分用处并不大 。咋一听深感有理,不过细究一下,

1、当需要先对前一次作任务执行的结果进行处理,并将相应的结果对应的作为输入再在一次任务传入时,这里使用api 更方便;

2、需要对结果输出进行整形时,也比较api 方便;

3、playbook 之间进行调用或都playbook比较复杂时,想要理清任务之间的关系势必累显麻烦,而通过api,从上一层任务到下一层任务之间的调用关系明子。而且playbook之间可以是平行的关系。方便小的功能模块的复用。

4、方便二次开发及和其他程序之间的耦合调用----目前感觉这条是最实用的。

 

五、ansible执行原理

首先ansbile-playbook接受到参数: playbook.yml,然后读取这个yml文件,根据这个yml文件生成Playbook对象,代码: class Playbook 。
在这个Playbook中加载yml文件,在执行时生成Play对象,在Play对象中又包含了Task对象,一个Task对象可以算是一个最小的执行单元。
到了Task这一步之后就应该调用runner接口了,这个接口的调用还是在Playbook这个类中: Playbook._run_task_internal 。


在runner这个类中,具体执行某一个task时是通过一个Action Module来完成的,这个action module是根据参数确定的,比如咱配置的是异步操作还是同步操作,这个不深究,默认是加载normal中的ActionModule,位置: normal 。在这个Action中对要执行的命令做了处理,对shell和command进行了处理,然后调用runner中的 self.runner._execute_module 来执行对应模块的操作,也就是playbook上写的git或者shell这样的模块。

首先,根据对应模块加载了library下面的对应的模块代码比如shell加载的是:library/commands/command这个代码,(这里要注意,上面加载的是normal的action模块,在那个模块里对shell进行了处理,变为command,因此这里就是command了)。找到这个具体的模块文件之后,ansible会加载一个module_common.py,对其进行渲染(把咱们定义的命令,比如:virtualenv ~demo,渲染到这个文件中)。
渲染完毕之后,会把这个文件copy到远程服务器的用户家目录下的.ansible/tmp/ansible-xxxxxx 这样的文件夹下(那个ansible-xxxx中xxx表示不知道是以什么方式生成的字符序列,可能是时间戳)。如果咱们定义的是一个shell,这里会多一个command的py文件,并且是可执行的。如果是git,这个文件名就是git。
传输完毕之后,就是执行了。ansible默认是以兼容的ssh来进行远程命令执行的,执行的方法就是,通过subprocess,来执行ssh和已经传输到远程服务器的可执行的python文件,通过PIPE的方式把执行结果输出回来,输出的CLI上。
大概就是这么个过程,只是大致的看了下整个的执行过程,很多地方复杂的逻辑都忽略了,最后的通过subprocess的方式执行ssh远程操作,并把结果通过PIPE输出回来不是特别理解其原理。

 

六.ansible任务执行性能优化

ansible默认只会创建5个进程并发执行任务,所以一次任务只能同时控制5台机器执行。如果有大量的机器需要控制,例如20台,ansible执行一个任务时会先在其中5台上执行,执行成功后再执行下一批5台,直到全部机器执行完毕。使用-f选项可以指定进程数,指定的进程数量多一些,不仅会实现全并发,对异步的轮训poll也会有正面影响。
ansible默认是同步阻塞模式,它会等待所有的机器都执行完毕才会在前台返回。可以采取异步执行模式。
异步模式下,ansible会将节点的任务丢在后台,每台被控制的机器都有一个job_id,ansible会根据这个job_id去轮训该机器上任务的执行情况,例如某机器上此任务中的某一个阶段是否完成,是否进入下一个阶段等。即使任务早就结束了,但只有轮训检查到任务结束后才认为该job结束。可以指定任务检查的时间间隔,默认是10秒。除非指定任务检查的间隔为0,否则会等待所有任务都完成后,ansible端才会释放占用的shell。

ansible性能优化
1.4.1 设置ansible开启ssh长连接
ansible天然支持openssh,默认连接方式下,它对ssh的依赖性非常强。所以优化ssh连接,在一定程度上也在优化ansible。其中一点是开启ssh的长连接,即长时间保持连接状态。
1.4.2 开启pipelining
pipeline也是openssh的一个特性。在ansible执行每个任务的流程中,有一个过程是将临时任务文件put到一个ansible端的一个临时文件中,然后sftp传输到远端,然后通过ssh连接过去远程执行这个任务。如果开启了pipelining,一个任务的所有动作都在一个ssh会话中完成,也会省去sftp到远端的过程,它会直接将要执行的任务在ssh会话中进行。
1.4.3 修改ansible执行策略
默认ansible在远程执行任务是按批并行执行的,一批控制多少台主机由命令行的"-f"或"--forks"选项控制。例如,默认的并行进程数是5,如果有20台被控主机,那么只有在每5台全部执行完一个任务才继续下一批的5台执行该任务,即使中间某台机器性能较好,完成速度较快,它也会空闲地等待在
在ansible 2.0中,添加了一个策略控制选项strategy,默认值为"linear",即上面按批并行处理的方式。还可以设置strategy的值为"free"。 在free模式下,ansible会尽可能快的切入到下一个主机。同样是上面的例子,首先每5台并行执行一个任务,当其中某一台机器由于性能较好提前完成了该任务,它不会等待其他4台完成,而是会跳出该任务让ansible切入到下一台机器来执行该任务。也就是说,这种模式下,一台主机完成一个任务后,另一台主机会立即执行任务,它是"前赴后继"的方式。
1.4.4 设置facts缓存
ansible或ansible-playbook默认总是先收集facts信息。在被控主机较少的情况下,收集信息还可以容忍,如果被控主机数量非常大,收集facts信息会消耗掉非常多时间。

 

七、celery+ansible

为什么要使用celery?我们使用之前的方法来调用命令不是很好吗?但是,你可以试试如果使用页面调用接口时,断网或者不小心刷新了一下网页,命令还会继续进行下去吗?答案是:将会全部终止,如果我们要执行一个很长的任务,比如编译安装一个模块,时间又比较长,如果这样的事情发生了,我们的命令就不知道进行到什么地方了,特别是一些不可逆的操作,将会给我们带来一些严重的后果。举个例子,在更新版本时突然接到撤销该更新的指令的时候,如果只是在做更新前的准备,我们本来可以终止的,但如果我们没有使用后台任务celery,就无法取消该操作。
14.2.4 使用celery取消正在进行的任务
这里要用到celery.task.control中的revoke方法,上面记录的task_id页面被要求作为参数传入这个revoke方法中,来确定是哪一个任务。

14.3 运行YML文件并实时读取日志
所以,我们要完成的任务名称就将是:同步实时读取Ansible日志。
我们需要完成几个目标:
1)任务开始时能显示任务状态;
2)可以在任意时间结束任务;
3)实时读取日志;
4)随着日志的增加,日志能够在浏览器自动往下移动,无需手动移动滚动条。

我们来解释要完成以上功能的意义:
当我们需要执行一个耗时很长的任务时,如果只是调用API的话,它只返回一串结果的JSON状态,如果出现错误,你就很难判断是在哪里出错。再则如果你使用插件来取得log,它也是要等待整个Ansible的API调用结束才会返回结果,中间我们可能根本不知道它运行到什么程度了,这样又违反了Ansible的设计初衷。若日志不能实时读取,那么使用命令行来执行更好。
另外我们还需要解决一个日志滚动显示的问题,当左侧滚动条移到最底部时日志自动向下移动,在其他位置则保持日志不动,
相当于完成一个Google浏览器的console界面的debug日志的功能,如图14-6所示。这样我们完成的同步实时读取Ansible日志的全部目标就达成了。虽然还不是很完善,但是却达到了易于排错的效果。

ansible定义好结果和日志输出到kafka, 前端js执行滚动刷新 5秒一次刷新

 

转载于:https://www.cnblogs.com/muzinan110/p/5287525.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值