Ansible-playbook

1.1、什么是playbook?

  • playbook 是由一个或多个play组成的列表
  • play的主要功能在于将直线归并为一组的主机装扮实现通过ansible中的task定义好的角色。从根本来讲,所谓的task无非是调用ansible的一个module。将多个play组织在一个playbook内,即可以让它们联动起来按实现编排的机制唱一台大戏
  • playbook采用YAML语言编写

    其工作流程图如下:

1.2、playbook的核心组成

  • Hosts 执行的远程主机列表

  • Tasks 任务集

  • Varniables 内置变量或自定义变量在playbook中调用

  • Templates 模板,可替换模板中的变量并实现一些简单的逻辑的文件

  • Hanglers和notify结合使用,由特定条件触发的操作,满足条件方才执行,否则不执行

  • Tags 标签 制定某条任务执行,用户选择运行playbook中的部分代码,ansible具有幂等性,因此会自动跳过没有辩护的部分,即便如此,有的代码为测试其确实没有发生变化的时间依然会非常的长,此时确信其没有变化,就可以通过tags跳过这些代码片段

    ansible-playbook -t tagsname useradd.yml

先来一个例子:

[root@ansible ansible]# cat hello.yml
---
- hosts: web #指定执行剧本的主机列表
remote_user: root #指定以什么用户去执行playbook tasks: #任务列表
- name: create new file #任务名称
file: name=/data state=directory #任务模块
- name: create new user
user: name=lilei shell=/sbin/nologin
- name: install httpd
yum: name=httpd
- name: start service
service: name=httpd state=started enabled=yes
[root@ansible ansible]# ansible-playbook -C hello.yml #检测playbook语法
[root@ansible ansible]# ansible-playbook hello.yml #执行playbook
  • Hosts:playbook中的每一个play 的目的都是为了让某个或某些主机以某个特定身份执行任务,hosts用于制定要执行执行任务的主机,须事先定义在主机清单内。可以是两个组的并集,也可以是两个组的交集,也支持模糊匹配。
  • remote_user:可用于Host和task中,也可以通过指定其通过sudo的方式在远程主机上执行任务,其可用于play全局或某服务;次在,甚至可以在sudo时使用sudo_user制定sudo时切换的用户
  • tasks:play的主题部分是task list。task list中的个任务按次序诸葛在hosts制定的所有主机下执行,即在所有主机上完成第一个任务后开始第二个。
#运行playbook的方式
ansible-playbook <filename.yml> ...[options] #常见选项 -- check只检测可能发生的改变,不真正执行等于-C --list-hosts 列出运行任务的主机 --limit 主机列表指着对主机列表中的主机执行 -v 显示过程 -vv -vvv更详细 实例 ansible-playbook file.yml --check ansible-playbook file.yml ansible-playbook file.yml --limit web ansible-playbook file.yml --list-tasks

当我们对服务的配置文件更改时,如果还是用以上的方法,服务是不会根据配置文件的修改后进行自动重启而生效的,如下:

[root@ansible ansible]# cat hello.yml
---
- hosts: web
remote_user: root tasks:
- name: create new file
file: name=/data state=directory
- name: create new user
user: name=lilei shell=/sbin/nologin
- name: install httpd
yum: name=httpd
- name: copy conf file
copy: src=conf_files/httpd.conf dest=/etc/httpd/conf/httpd.conf
- name: start service
service: name=httpd state=started enabled=yes

当我们对上述的配置文件进行了修改监听端口为81时,httpd服务是不会自动重启而使配置生效的。此时就需要用到playbook的handers、notify结合来触发服务的重启。

1.3、playbook的handlers、notify触发

[root@ansible ansible]# cat hello.yml
---
- hosts: web
remote_user: root tasks:
#创建文件
- name: create new file
file: name=/data state=directory
#创建用户
- name: create new user
user: name=lilei shell=/sbin/nologin
#安装httpd服务
- name: install httpd
yum: name=httpd
#拷贝配置文件
- name: copy conf file
copy: src=conf_files/httpd.conf dest=/etc/httpd/conf/httpd.conf
notify: restart service #notify主要用于检测文件的变化,而通知handler对应的模块
#启动服务
- name: start service
service: name=httpd state=started enabled=yes
#如果配置文件发生变化则会调用handlers下面的模块
handlers:
- name: restart service
service: name=httpd state=restarted [root@ansible ansible]# ansible web -m shell -a "netstat -tulnp |grep 82"

1.4、playbook的变量和标签

  • 标签(tags)

在众多的playbook当中,我们为了更加方便地调用公共模块的tasks,通常会给一些tasks进行打定标签,在执行的过程中进行指定标签运行,以实现我们的目标需求,如下:

[root@ansible ansible]# cat hello.yml
---
- hosts: web
remote_user: root tasks:
- name: create new file
file: name=/data state=directory
- name: create new user
user: name=lilei shell=/sbin/nologin
- name: install httpd
yum: name=httpd
tags: install_httpd #安装httpd的标签
- name: copy conf file
copy: src=conf_files/httpd.conf dest=/etc/httpd/conf/httpd.conf
notify: restart service
tags: restart_httpd #重启httpd服务的标签
- name: start service
service: name=httpd state=started enabled=yes
tags: start_httpd #启动httpd的标签 handlers:
- name: restart service
service: name=httpd state=restarted # 直接调用了重启httpd服务的标签,-t为指定标签执行,也可以指定多个标签一起执行
[root@ansible ansible]# ansible-playbook -t restart_httpd hello.yml
  • 变量(vars)

在playbook当中,所有的任务都是固定的模式,在针对主机时,也是固定组别,服务端口等等也是固定的,写过shell脚本的大佬都知道在一个脚本当中,对于常用的量以变量代替,从而增加脚本的灵活性,那么在playbook当中也是可以引入变量的。

变量名:只能由字母、数字和下划线组成,且只能字母开头

变量的定义方式:

  • (1)ansible setup facts远程主机所有的变量可直接调用,支持通配符。

下面可以通过setup模块过滤出ip的变量名为:ansible_all_ipv4_addresses,那么在使用时,可以直接调用该变量名,以实现调用。

[root@ansible ~]# ansible web -m setup -a 'filter=*address*'
192.168.0.116 | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"192.168.0.116"
],
"ansible_all_ipv6_addresses": [
"fe80::20c:29ff:fef3:ce94"
]
},
"changed": false
}
192.168.0.135 | SUCCESS => {
"ansible_facts": {
"ansible_all_ipv4_addresses": [
"192.168.0.135"
],
"ansible_all_ipv6_addresses": [
"fe80::20c:29ff:fe4c:ef31"
]
},
"changed": false
}

(2)在/etc/ansible/hosts中进行定义,普通变量(主机组中主机单独定义,优先级高于公共变量),公共变量(是在主机组中对所有主机定义的统一变量)

[root@ansible ansible]# cat vars.yml
---
- hosts: web
remote_user: root tasks:
- name: set hostname
hostname: name={{ nodename }}.{{ domainname }} #playbook中的变量调用 [root@ansible ansible]# vim /etc/ansible/hosts
[web]
192.168.0.135 nodename=node01 #普通变量的定义
192.168.0.116 nodename=node02
[web:vars] #新增变量组,公共变量的定义
domainname=magedu.com
[root@ansible ansible]# ansible-playbook -C vars.yml
[root@ansible ansible]# ansible-playbook vars.yml
[root@ansible ansible]# ansible web -m shell -a "hostname"
192.168.0.135 | CHANGED | rc=0 >>
node01.magedu.com 192.168.0.116 | CHANGED | rc=0 >>
node02.magedu.com

(3)命令行指定变量,优先级是最高的。

# 在命令行用-e参数指定变量的值
[root@ansible ansible]# ansible-playbook -e 'domainname=baidu.com' vars.yml
[root@ansible ansible]# ansible web -m shell -a "hostname"
192.168.0.116 | CHANGED | rc=0 >>
node02.baidu.com 192.168.0.135 | CHANGED | rc=0 >>
node01.baidu.com

(4)playbook中定义变量

#变量定义模式
vars:
- var1: value1
- var2: value2 #修改playbook,增加变量使用
[root@ansible ansible]# vim vars.yml
---
- hosts: web
remote_user: root
#配置domainname变量
vars:
- domainname: magedu.com tasks:
- name: set hostname
hostname: name={{ nodename }}.{{ domainname }} #检测语法后执行,并查看执行后效果
[root@ansible ansible]# ansible-playbook -C vars.yml
[root@ansible ansible]# ansible-playbook vars.yml
[root@ansible ansible]# ansible web -m shell -a "hostname"
192.168.0.135 | CHANGED | rc=0 >>
node01.magedu.com 192.168.0.116 | CHANGED | rc=0 >>
node02.magedu.com

(5)独立的YAML文件中定义

对于变量管理,由于变量有多重方式可以定义,不同人习惯会导致变量混乱等,故可以考虑吧变量放在同一文件内,使用的时候在文件内修改,剧本中调用该文件。

# 新建var.yml增加变量定义
[root@ansible ansible]# vim var.yml
domainname: hao123.com #在playbook进行调用变量定义文件
[root@ansible ansible]# vim vars.yml
---
- hosts: web
remote_user: root
#导入变量文件,这里使用的相对路径,必须和playbook在同一目录下,如果不在同一目录,则需要写全路径
vars_files:
- var.yml
tasks:
- name: set hostname
hostname: name={{ nodename }}.{{ domainname }}
[root@ansible ansible]# ansible-playbook -C vars.yml
[root@ansible ansible]# ansible-playbook vars.yml
[root@ansible ansible]# ansible web -m shell -a "hostname"
192.168.0.135 | CHANGED | rc=0 >>
node01.hao123.com 192.168.0.116 | CHANGED | rc=0 >>
node02.hao123.com

1.5、playbook的模板

在生产服务器集群当中,每台服务器的配置都可能存在不同,在使用ansible进行自动化运维时,只是单纯的查询式操作,我们可以使用普通命令行ansible + hosts + -m + module + -a + "xxx"的模式进行进行批量查询,如查询负载,内存,cpu资源使用率等指标数据。而当我们需要批量化对目标主机进行批量任务操作时,如安装服务,启动服务,设置开机自启等批量化任务时,我们采用了playbook的方式进行任务的批量化执行。而在针对配置更改,实现服务自动重启,也采用了handlers+notify的方式进一步实现自动化的批量更改生效。与此同时,在使用ansible批量化自动运维时,还增加了变量和标签,以提高playbook的灵活性,以上的种种都说明了ansible模块化的强大功能。

而对于不同服务器的配置,以及不同的使用需求时,又改如何去更加灵活地去编写playbook来提高实用性呢?而ansible就提供了这样的一种模板(template)模块。假设有这样的一个需求,进行批量化部署httpd服务后,要求监听的服务端口分别为87、88端口。那么可以来一场这样的剧演:

# (1)创建模板配置目录,拷贝httpd服务的配置文件为j2后缀文件
[root@ansible ansible]# mkdir templates
[root@ansible ansible]# cp conf_files/httpd.conf templates/httpd.conf.j2
[root@ansible ansible]# cd templates/
[root@ansible templates]# ll
total 12
-rw-r--r-- 1 root root 12026 Nov 19 13:51 httpd.conf.j2 # (2)修改j2文件的配置,更改监听端口配置为http_port变量调用
[root@ansible templates]# vim httpd.conf.j2
Listen {{ http_port }} # (3)修改主机列表中的普通变量,增加每台主机分别监听的端口变量
[root@ansible templates]# vim /etc/ansible/hosts
[web]
192.168.0.135 nodename=node01 http_port=87
192.168.0.116 nodename=node02 http_port=88
[web:vars]
domainname=magedu.com # (4)编写playbook
[root@ansible ansible]# vim hello.yml
---
- hosts: web
remote_user: root tasks:
- name: create new file
file: name=/data state=directory
- name: create new user
user: name=lilei shell=/sbin/nologin
- name: install httpd
yum: name=httpd
tags: install_httpd
- name: copy conf template file
#这里要使用的是template模块,使用方式和copy模块类似
template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
notify: restart service
tags: restart_httpd
- name: start service
service: name=httpd state=started enabled=yes
tags: start_httpd handlers:
- name: restart service
service: name=httpd state=restarted # (5)playbook测试与执行和查看执行结果
[root@ansible ansible]# ansible-playbook -C hello.yml
[root@ansible ansible]# ansible-playbook hello.yml
[root@ansible ansible]# ansible web -m shell -a "netstat -tulnp |grep httpd"
192.168.0.135 | CHANGED | rc=0 >>
tcp6 0 0 :::87 :::* LISTEN 32274/httpd 192.168.0.116 | CHANGED | rc=0 >>
tcp6 0 0 :::88 :::* LISTEN 28307/httpd

1.6、playbook的条件语句--When

有时候我们希望对某些特定的主机执行某些特定的操作,比如对指定的系统版本,进行关机操作,如下:

tasks:
- name: "shut down Debian flavored systems"
command: /sbin/shutdown -t now
when: ansible_facts['os_family'] == "Debian" # 也可以进行分组多个条件组合进行判断
tasks:
- name: "shut down CentOS 6 and Debian 7 systems"
command: /sbin/shutdown -t now
when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6") or
(ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "7") # 当需要多个条件都必须具备时,可以使用列表的方式进行指定
tasks:
- name: "shut down CentOS 6 systems"
command: /sbin/shutdown -t now
when:
- ansible_facts['distribution'] == "CentOS"
- ansible_facts['distribution_major_version'] == "6"

使用举例:对判断被控端的主机名为node02.hao123.com的主机进行更改httpd服务的端口

# 使用setup获取目标主机的公共变量值
[root@ansible ansible]# ansible web -m setup -a "filter="*hostname*""
192.168.0.135 | SUCCESS => {
"ansible_facts": {
"ansible_hostname": "node01"
},
"changed": false
}
192.168.0.116 | SUCCESS => {
"ansible_facts": {
"ansible_hostname": "node02"
},
"changed": false
} # 编写playbook
[root@ansible ansible]# vim hello.yml
---
- hosts: web
remote_user: root
vars:
- http_port: 90 tasks:
- name: install httpd
yum: name=httpd
tags: install_httpd
- name: copy conf template file
template: src=templates/httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
when: ansible_hostname == "node02" #增加判断条件,当 hostname为node02才会更改配置文件,并重启服务
notify: restart service
tags: restart_httpd
- name: start service
service: name=httpd state=started enabled=yes
tags: start_httpd handlers:
- name: restart service
service: name=httpd state=restarted [root@ansible ansible]# ansible-playbook -C hello.yml
[root@ansible ansible]# ansible-playbook hello.yml
[root@ansible ansible]# ansible 192.168.0.116 -m shell -a "netstat -tulnp |grep httpd"
192.168.0.116 | CHANGED | rc=0 >>
tcp6 0 0 :::90 :::* LISTEN 30308/httpd

1.7、playbook的循环迭代--Item

Item主要用于循环迭代多个重复的操作,比如批量创建用户、批量创建文件等等。

# 编写playbook,进行批量创建文件
[root@ansible ansible]# cat item.yml
---
- hosts: web
remote_user: root tasks:
- name: create directory
file: name=/data state=directory
- name: create files
file: name=/data/{{ item }} state=touch
when: ansible_hostname == "node02"
with_items:
- file1
- file2
- file3 [root@ansible ansible]# ansible-playbook -C item.yml
[root@ansible ansible]# ansible-playbook item.yml
[root@ansible ansible]# ansible 192.168.0.116 -m shell -a "ls -l /data/file*"
192.168.0.116 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 0 Nov 19 15:34 /data/file1
-rw-r--r-- 1 root root 0 Nov 19 15:34 /data/file2
-rw-r--r-- 1 root root 0 Nov 19 15:34 /data/file3 # 创建3个组,3个用户,并且对应每一个组
[root@ansible ansible]# cat item_user_group.yml
---
- hosts: web
remote_user: root tasks:
- name: add some groups
group: name={{ item }}
with_items:
- g1
- g2
- g3
- name: add some users
user: name={{ item.name }} group={{ item.group }}
with_items:
- { name: 'user1' , group: 'g1'}
- { name: 'user2' , group: 'g2'}
- { name: 'user3' , group: 'g3'}
[root@ansible ansible]# ansible-playbook -C item_user_group.yml
[root@ansible ansible]# ansible-playbook item_user_group.yml

1.8、playbook的循环语句--For

重复性执行一段代码,生成一段配置信息。示例如下:

# for循环使用语法示例:
{% for vhost in nginx/-vhosts %} server { listen {{ vhost.listen| default('80 default_server') }} {% endfor %} # 通过jin2模板生成不同监听端口的server标签
[root@ansible ansible]# cat for.yml
---
- hosts: web
remote_user: root
vars:
ports:
- 83
- 84
tasks:
- name: copy conf file
template: src=templates/nginx.conf.j2 dest=/data/nginx.conf # 模板中通过定义循环,生成配置
[root@ansible ansible]# cat templates/nginx.conf.j2
{% for port in ports %}
server {
listen {{ port }}
}
{% endfor %} [root@ansible ansible]# ansible-playbook for.yml
[root@ansible ansible]# ansible web -m shell -a 'cat /data/nginx.conf'
192.168.0.135 | CHANGED | rc=0 >>
server {
listen 83
}
server {
listen 84
} 192.168.0.116 | CHANGED | rc=0 >>
server {
listen 83
}
server {
listen 84
}

1.9、playbook的判断语句--If

通过判断去执行,和shell的语法类似:

{% if vhost.server_name is defined %}

server_name {{vhost.server_name }};

{% endif %}

{% if vhost.root is defined 80 %}

root {{ vhost.root }};

{% endif %}

# 创建3个不同的web站点
[root@ansible ansible]# vim testif1.yml ---
- hosts: web
remote_user: root
vars:
ports:
- web1:
port: 86
#name: web1.hao123.com #注释
rootdir: /data/website1
- web2:
port: 87
name: web2.hao123.com
rootdir: /data/website2
- web3:
port: 88
#name: web3.hao123.com #注释
rootdir: /data/website3 tasks:
- name: copy conf
template: src=for3.conf.j2 dest=/data/for3.conf
#对p.name进行判断,没有则不生成servername
[root@ansible ansible]# vim templates/for3.conf.j2
#只有87端口的server有servername 别的由于被注释,if判断不存在 就不创建servername
{% for p in ports %}
server {
listen {{ p.port }}
{% if p.name is defined %}
servername {{ p.name }}
{% endif %}
documentroot {{ p.rootdir }}
}
{% endfor %} [root@ansible ansible]# ansible-playbook testif1.yml [root@ansible ansible]# ansible web -m shell -a 'less /data/for3.conf'

1.10、playbook的异常处理

默认Playbook会检查命令和模块的返回状态,如遇到错误就中断playbook的执行加入参数: ignore_errors: yes 忽略错误

[root@ansible ansible]# vim ignore.yml
---
- hosts: web
remote_user: root tasks:
- name: Ignore False
command: /bin/false #直接执行返回false
ignore_errors: yes #忽略错误 # 当上一个任务遇到错误中断时,后面的任务就不会执行了
- name: touch files
file: path=/tmp/bgx_ignore state=touch [root@ansible ansible]# ansible-playbook -C ignore.yml
[root@ansible ansible]# ansible-playbook ignore.yml
[root@ansible ansible]# ansible web -m shell -a 'ls -l /tmp/bgx_ignore'
192.168.0.116 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 0 Nov 20 14:35 /tmp/bgx_ignore 192.168.0.135 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 0 Nov 20 14:35 /tmp/bgx_ignore

Ansible入门笔记(3)之Playbook的更多相关文章

  1. ansible学习笔记三:playbook和roles

    参考博客: Ansible 系列之 Playbooks 剧本 -飞走不可(博客园) linux运维学习之ansible的playbook及roles的使用 - 51CTO博客 nginx 基于uwsg ...

  2. Ansible入门笔记(1)之工作架构和使用原理

    目录 Ansible入门笔记(1) 1.Ansible特性 2.ansible架构解析 3.ansible主要组成部分 1)命令执行来源: 2)利用ansible实现管理的方式 3)Ansile-pl ...

  3. Ansible入门笔记(2)之常用模块

    目录 Ansible常用模块 1.1.Ansible Ad-hoc 1.2.Ansible的基础命令 1.3.常用模块 Ansible常用模块 1.1.Ansible Ad-hoc 什么事ad-hoc ...

  4. Ansible 入门指南 - 安装及 Ad-Hoc 命令使用

    安装及配置 ansible Ansilbe 管理员节点和远程主机节点通过 SSH 协议进行通信.所以 Ansible 配置的时候只需要保证从 Ansible 管理节点通过 SSH 能够连接到被管理的远 ...

  5. ansible使用笔记

    ansible使用笔记 介绍 ansible 是一个模型驱动的配置管理器,支持多节点发布.远程任务执行.默认使用 SSH 进行远程连接.无需在被管理节点上安装附加软件,可使用各种编程语言进行扩展.an ...

  6. [转帖]Ansible 入门秘诀

    Ansible 入门秘诀 作者: Jose Delarosa 译者: LCTT jdh8383 | 2019-03-08 09:24   收藏: 2 用 Ansible 自动化你的数据中心的关键点. ...

  7. Ansible 入门指南 - 学习总结

    概述 这周在工作中需要去修改 nginx 的配置,发现了同事在使用 ansible 管理者系统几乎所有的配置,从数据库的安装.nginx 的安装及配置.于是这周研究起了 ansible 的基础用法.回 ...

  8. Ansible 入门指南 - ansible-playbook 命令

    上篇文章Ansible 入门指南 - 安装及 Ad-Hoc 命令使用介绍的额是 Ad-Hoc 命令方式,本文将介绍 Playbook 方式. Playbook 译为「剧本」,觉得还挺恰当的. play ...

  9. Centos7——docker入门(笔记)

    docker 入门(笔记) 一.Docker是什么? 官方原话: Docker provides a way to run applications securely isolated in a co ...

随机推荐

  1. 算法——回文(palindrome)

    回文(palindrome):指的是从头读到尾与从尾读到头一模一样的字符串. 分别在C.Java与Python实现回文检测: C: #include <stdio.h> #include ...

  2. 【CSP-S膜你考】不怕噩梦 (模拟)

    不怕噩梦 题面 蚊子最近经常做噩梦,然后就会被吓醒.这可不好.. 疯子一直在发愁,然后突然有一天,他发现蚊子其实就是害怕某些事. 如果那些事出现在她的梦里,就会害怕. 我们可以假定那个害怕的事其实是一 ...

  3. 【IntelliJ IDEA学习之三】IntelliJ IDEA常用快捷键

    版本:IntelliJIDEA2018.1.4 按场景列举一.打开设置CTRL + ALT + S:打开设置(File-->Settings...)Ctrl + Shift + Alt + S: ...

  4. 《Linux就该这么学》培训笔记_ch14_使用DHCP动态管理主机地址

    <Linux就该这么学>培训笔记_ch14_使用DHCP动态管理主机地址 文章最后会post上书本的笔记照片. 文章主要内容: 动态主机地址管理协议 部署dhcpd服务程序 自动管理IP地 ...

  5. 浅谈设计模式-visitor访问者模式

    先看一个和visitor无关的案例.假设你现在有一个书架,这个书架有两种操作,1添加书籍2阅读每一本书籍的简介. //书架public class Bookcase { List<Book> ...

  6. AKKA 常见异常

    一,scala 相关类找不到问题 AKKA 包的版本命名规则 compile("com.typesafe.akka:akka-remote_2.13:2.5.23") 注意: co ...

  7. js文件获取自身的URL路径

    我们做框架开发的时候,经常需要js文件获取的到自身的路径,在网上查了些资料,总结 了两种方式 浏览器支持docment.currentScript.src 直接用这个获取,不用支持的情况 try{ n ...

  8. 拦截器配置类使用继承写法导致jackson的全局配置失效

    问题描述 项目中需要一个拦截器用于拦截请求,在没有请求中生成requestId.然后写了一个配置类,这个类继承了 WebMvcConfigurationSupport类,重写了addIntercept ...

  9. 【学习笔记】Docker基础

    基本概念 Docker是什么? Docker是一种基于Golang开发的虚拟化技术,开发人员和系统管理员使用容器开发,部署和运行应用程序的平台. 使用Linux容器部署应用程序称为容器化. 容器不是新 ...

  10. 『7.3 NOIP模拟赛题解』

    T1 gift Description ​ 夏川的生日就要到了.作为夏川形式上的男朋友,季堂打算给夏川买一些生日礼物. ​ 商店里一共有种礼物.夏川每得到一种礼物,就会获得相应喜悦值Wi(每种礼物的喜 ...