Linux之自动化运维工具ansible、ansible模块

文章目录

  • 1、什么是ansible?
  • 2、ansible的组成
  • 3、ansible环境安装配置以及实例测试
    • 1、实验环境
  • 2、ansible的使用
  • 3、ansible 体验
  • 4、ansible的模块
    • 1、copy模块
    • 2、fetch模块
    • 3、command模块
    • 4、shell模块
    • 5、file模块
    • 6、cron模块
    • 7、yum模块
    • 8、service模块
    • 9、script模块

1、什么是ansible?

ansible是一个自动化运维工具的名称,是基于Python开发,集合了众多运维工具的优点(puppet、fabric、slatstack),实现批量系统配置,程序的部署。
Linux运维:自动化(脚本)、智能化、平台化。由于Linux运维人员,人肉运维不可取–效率慢,如果敲错出事,于是就诞生了一系列的运维工具,ansible就是其中之一。

日常运维:
1、软件安装-查看依赖 漏铜 升级 Debian-apt-get
2、服务的配置-架构搭建-负载均衡(高可用)-等价路由-lvs
3、运行脚本
4、升级
5、备份
6、告警
ansible依赖于:paramiko、PyYam和jinja三个关键组件,基于ssh协议,只要ssh协议,只要管理员通过ssh登录到一台远程主机上能做的操作,Ansible都可以做到。

2、ansible的组成

1、host inventory --定义客户机,可以对客户机进行分类:db类、web类等
2、playbook 剧本 让主机按照我给定的剧本去完成一些事情。
3、module 模块 实现一个个功能的程序。
4、pluging 插件 实现一些额外的小功能。

3、ansible环境安装配置以及实例测试

1、实验环境

1、准备三台虚拟机:
A机器:192.168.2.152(ansible)
B机器:192.168.2.132
C机器:192.168.2.137

实验前提,做好免密登录认证,使用ssh服务,详情可见ssh服务免密登录
A---->B, A----->C A可以免密码登录到B机器和C机器上。
首先在A机器上操作(建立免密通道):
先连接到B机器(192.168.2.132)

[root@sc-master ~]# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa): 
/root/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:YgRssTONWSCv0/JgvN//oI54fJL8bTClAP4oqHjg1Ws root@sc-master
The key's randomart image is:
+---[RSA 2048]----+
|  ..+o.          |
|  .ooB           |
| . oB o          |
| ..o.+  .        |
|. Boo.ooS        |
|+..B.o+.         |
|+ooo...o.        |
|o..o*E.o..       |
| ...+==oo..      |
+----[SHA256]-----+
[root@sc-master ~]# cd /root/.ssh
[root@sc-master .ssh]# ls
authorized_keys  config  id_rsa  id_rsa.pub  known_hosts

登录测试:看能不能登录到B机器上,第一次登录需要输入密码,第二次登录就不需要密码了。

[root@sc-master .ssh]# ssh-copy-id -p 22 -i id_rsa.pub [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out an are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now to install the new keys
[email protected]'s password: 

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh -p '22' '[email protected]'"
and check to make sure that only the key(s) you wanted were added.

这里直接登录,就不需要输入密码。建立成功。

[root@sc-master .ssh]# ssh -p '22' '[email protected]'
Last login: Sat Aug 20 11:00:50 2022 from 192.168.2.116

然后查看B机器上~/.ssh目录下生成的A机器上生成的authorized_keys 文件。

[root@sc-slave .ssh]# ls
authorized_keys  id_rsa  id_rsa.pub  known_hosts
[root@sc-slave .ssh]# cat id_rsa.pub

然后就是建立和C机器之间的免密通信。方法同上,直接将A机器上生成的公钥上传到137机器上。

[root@sc-master .ssh]# ssh-copy-id -p 22 -i id_rsa.pub [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out an are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now to install the new keys
[email protected]'s password: 

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh -p '22' '[email protected]'"
and check to make sure that only the key(s) you wanted were added.

然后直接登录。

[root@sc-master .ssh]# ssh -p '22' '[email protected]'
Last login: Sat Aug 20 11:01:11 2022 from 192.168.2.116
[root@nginx-kafka03 ~]# exit
登出
Connection to 192.168.2.137 closed.
[root@sc-master .ssh]# ssh -p '22' '[email protected]'
Last login: Sat Aug 20 11:05:18 2022 from nginx-kafka01
[root@nginx-kafka03 ~]# exit
登出
Connection to 192.168.2.137 closed.

2、在A机器上安装ansible

[root@a .ssh]# yum install epel-release
[root@b ansible]# yum install ansible

3、配置:配置目录
/etc/ansible/ansible.cfg :ansible的主配置文件,此文件主要定义了roles_path的路径,主机清单路径,连接清单中的主机方式等等。
**/etc/ansible/hosts:**这个配置文件就是默认的主机清单配置文件, 可以通过ansible.cfg 重新定义。

备份/etc/ansible/hosts:

[root@sc-master ansible]# cp hosts hosts.bak

编辑 /etc/ansible/hosts文件:

[root@b ansible]# cat hosts
[webser]
192.168.2.132:22    
192.168.2.137:22

[webser]表示将需要管理的主机添加到webser组
如果通过ssh登陆的端口不是22号端口,就需要在配置文件中指明端口号
除了以上两个重要的配置文件还有三个重要的可执行文件分别是:
**ansible 主执行程序,**一般用于命令行下执行。
ansible-playbook 执行playbook中的任务。
ansible-doc 获取各模块的帮助信息。

2、ansible的使用

HOST-PATTERN: 匹配主机模式,如all表示所有主机
-m MOD_NAME: 模块名 如:ping、shell模块
-a MOD_ARGS : 模块执行的参数
-f FORKS : 生成几个子进程进行执行
-C :(不执行,模拟跑)
**-u Username :**某主机的用户名
-c CONNection: 连接方式(default smart)

3、ansible 体验

分组执行:
指定ansible管理的所有主机都执行命令(在tmp目录下创建sc目录)

[root@sc-master ansible]# ansible all -m shell -a "mkdir /tmp/sc"
[WARNING]: Consider using the file module with state=directory rather than running
'mkdir'.  If you need to use command because file is insufficient you can add
'warn: false' to this command task or set 'command_warnings=False' in ansible.cfg
to get rid of this message.
192.168.2.132 | CHANGED | rc=0 >>

192.168.2.137 | CHANGED | rc=0 >>

rc ==》 return code --为0表示执行成功。

使用pssh服务去批量处理。

[root@sc-master ansible]# pssh -h hosts "mkdir /tmp/sc2"
[1] 11:44:55 [FAILURE] [webser] Exited with error code 255
[2] 11:44:56 [SUCCESS] 192.168.2.132:22
[3] 11:44:56 [SUCCESS] 192.168.2.137:22

ansible不是一个守护进程(起来后一直在内存中运行,等待其他人访问),ansible就是一个命令脚本,使用python写的。

[root@sc-master ansible]# ps -ef|grep ansible
root       8411   7979  0 11:47 pts/0    00:00:00 grep --color=auto ansible

4、ansible的模块

1、copy模块:从本地copy文件分发到目录主机路径
2、fetch模块:从远程主机拉取文件到本地
3、command模块:在远程主机上执行命令,不进行shell解析。
4、shell模块:需要两台机器上也有能执行的命令
5、file模块:设置文件属性(创建文件)
6、cron模块:对目标主机生成计划任务
7、yum模块:yum安装软件包的模块
8、service模块:服务管理模块
9、script模块:把本地的脚本传到远端执行;前提是到远端可以执行

1、copy模块

从本地copy文件分发到目录主机路径
参数说明:

src=源文件路径
dest=目标路径
注意src=路径后面
带/表示里面的所有内容复制到目标目录下**,不带/是目录递归复制过去
content=自行填充的文件内容
owner 属主
group 属组
mode 权限

例如:1、将/lianxi/ansible-copy复制到主机的/lianxi/ansible下,并设置权限为777,属主为sanchuang,属组为sanchuang。

[root@b lianxi]# ansible all -m copy -a "src=/lianxi/ansible-copy dest=/lianxi/ansible mode=777 owner=sanchuang group=sanchuang"

2、指定webser组,将/etc/passwd 复制到主机/tmp目录下,指定权限777

[root@b copy_dir]# ansible webser -m copy -a "src=/etc/passwd  dest=/tmp mode=777"

3、指定webser组,将/lianxi下的aa文件拷贝到主机下的/tmp/sc下的aa.txt文件。

[root@sc-master ansible]# ansible webser -m copy -a "src=/lianxi/aa dest=/tmp/sc/aa.txt"
192.168.2.137 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "changed": true, 
    "checksum": "6dbdec21eba690a18d6fada6e33744c34d826b39", 
    "dest": "/tmp/sc/aa.txt", 
    "gid": 0, 
    "group": "root", 
    "md5sum": "dfd35885c2b6d0c25a7ba699b6ced4f7", 
    "mode": "0644", 
    "owner": "root", 
    "size": 452, 
    "src": "/root/.ansible/tmp/ansible-tmp-1660967660.93-8472-73428149734911/source", 
    "state": "file", 
    "uid": 0
}

src目录后面带/和不带/的区别:
① 带/ 表示拷贝目录下的子文件或者子文件夹
② 不带/ 表示拷贝整个目录

不带/的例题:/lianxi/copy_dir
将lianxi下的copy_dir整个目录都拷贝到主机的/lianxi/ansible目录下。

[root@b copy_dir]# ansible all -m copy -a "src=/lianxi/copy_dir dest=/lianxi/ansible"
192.168.0.48 | CHANGED => {
    "changed": true, 
    "dest": "/lianxi/ansible/", 
    "src": "/lianxi/copy_dir"
}

测试:将ansible机器上的/lianxi下的myproject的所有目录文件夹传入到主机下的lianxi下的ansible文件夹中。如果对方主机不存在的目录会自动帮助你新建,可以看下面的。

[root@sc-master lianxi]# ansible all -m copy -a "src=/lianxi/myproject dest=/lianxi/ansible"

执行拷贝目录的如果目录文件比较大,反应速度有点慢。需要等待。
被上传文件的主机:B机器
首先查看主机上的/lianxi/ansible文件夹是不存在的。

[root@sc-slave .ssh]# cd /lianxi
-bash: cd: /lianxi: 没有那个文件或目录
[root@sc-slave .ssh]# cd /
[root@sc-slave /]# cd /lianxi
-bash: cd: /lianxi: 没有那个文件或目录

C机器:也是如此。
在执行了上面那条命令之后,就有自动帮助创建了文件夹。
B机器上:

[root@sc-slave /]# ls
backup  boot  dev  home    lib    media  opt   root  sbin  sys  usr
bin     data  etc  lianxi  lib64  mnt    proc  run   srv   tmp  var
[root@sc-slave /]# cd /lianxi
[root@sc-slave lianxi]# ls
ansible
[root@sc-slave lianxi]# cd ansible
[root@sc-slave ansible]# ls
myproject

C机器上:

[root@nginx-kafka03 /]# cd /lianxi
[root@nginx-kafka03 lianxi]# ls
aa  ansible  bb  cc  file_num.sh  lianxi  tongle  xieshan

可以看到被执行了那条命令之后,B、C两机器上都有这个被上传的整个目录,那就说明命令执行成功。

带/:/lianxi/copy_dir/ :是将lianxi下的copy_dir下的所有子目录以及子文件都拷贝到/lianxi/ansible下面。

[root@b copy_dir]# ansible all -m copy -a "src=/lianxi/copy_dir/ dest=/lianxi/ansible"
192.168.2.132 | CHANGED => {
    "changed": true, 
    "dest": "/lianxi/ansible/", 
    "src": "/lianxi/copy_dir/"
}

2、fetch模块

从远程主机拉取文件到本地。
fetch会自动的在dest指定目录后加上远程主机命名的目录结构后面接src目录结构
fetch存储到本地的目录结构: dest + 远程主机名 + src
例如:将远程主机上的hostname整个目录拉取到ansible机器上的/tmp文件夹下面

[root@sc-master ~]# ansible webser -m fetch -a "src=/etc/hostname dest=/tmp/"
192.168.2.132 | CHANGED => {
    "changed": true, 
    "checksum": "6a407093b4b39fea35a63344f893529cf4ff26d2", 
    "dest": "/tmp/192.168.2.132/etc/hostname", 
    "md5sum": "c810e45b405a225acc2b9cced74d4e1f", 
    "remote_checksum": "6a407093b4b39fea35a63344f893529cf4ff26d2", 
    "remote_md5sum": null
}
192.168.2.137 | CHANGED => {
    "changed": true, 
    "checksum": "de9bcf635db0ce32c9975f2938b930d923576653", 
    "dest": "/tmp/192.168.2.137/etc/hostname", 
    "md5sum": "9fab83764b3e4608e27bd0c32ea881a9", 
    "remote_checksum": "de9bcf635db0ce32c9975f2938b930d923576653", 
    "remote_md5sum": null
}
[root@sc-master ~]# cd /tmp
[root@sc-master tmp]# ls
192.168.2.132
192.168.2.137
)

3、command模块

在远程主机上执行命令,属于裸执行,非键值对显示;不进行shell解析。

[root@b lianxi]# ansible all -m shell -a "ifconfig"
[root@b lianxi]# ansible all -m command -a "ifconfig|grep inet"
192.168.2.132 | FAILED | rc=2 >>
[Errno 2] 没有那个文件或目录
192.168.2.137 | FAILED | rc=2 >>
[Errno 2] 没有那个文件或目录

属于裸执行,不会解析它的管道符号 会认为ifconfig|grep 是一个命令。

4、shell模块

使用ansible中的shell命令的时候,需要两台机器上也有能执行的命令。比如执行ifconfig密码
跟command一样,只不过shell模块可以解析管道之类的功能。

[root@b lianxi]# ansible all -m shell -a "ifconfig|grep inet"
[root@sc-master 192.168.2.132]# ansible webser -m shell -a "ifconfig|grep inet"
192.168.2.132 | CHANGED | rc=0 >>
        inet 192.168.2.132  netmask 255.255.255.0  broadcast 192.168.2.255
        inet6 fe80::20c:29ff:fefd:d5db  prefixlen 64  scopeid 0x20<link>
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
192.168.2.137 | CHANGED | rc=0 >>
        inet 192.168.2.137  netmask 255.255.255.0  broadcast 192.168.2.255
        inet6 fe80::20c:29ff:fe3f:78b  prefixlen 64  scopeid 0x20<link>
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>

5、file模块

设置文件属性(创建文件)
常用参数:
path 目标路径
state directory为目录, link为软件链接
group 目录属组
owner 属主
mode 指定权限
等,其他参数通过ansible-doc -s file 获取

其中的state –
absent 删除文件和目录的
directory 目录
touch 新建空文件
link 软链接
hard 硬链接

例题:创建文件目录,并且设置权限为777。

[root@sc-master tmp]# ansible webser -m file -a "path=/tmp/sanchuang state=directory mode=777"
192.168.2.137 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "changed": true, 
    "gid": 0, 
    "group": "root", 
    "mode": "0777", 
    "owner": "root", 
    "path": "/tmp/sanchuang", 
    "size": 6, 
    "state": "directory", 
    "uid": 0
}
192.168.2.132 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "changed": true, 
    "gid": 0, 
    "group": "root", 
    "mode": "0777", 
    "owner": "root", 
    "path": "/tmp/sanchuang", 
    "size": 6, 
    "state": "directory", 
    "uid": 0
}

查看B机器:目录存在

[root@sc-slave tmp]# ls
ansible_stat_payload_fsj79S
sanchuang

C机器的也存在。
例如:删除那个sanchaung目录

[root@sc-master tmp]# ansible webser -m file -a "path=/tmp/sanchuang state=absent mode=777"

1、查看file模块帮助信息:ansible-doc

[root@b lianxi]# ansible-doc -s file

例题:设置修改文件属性

root@b lianxi]# ansible all -m file -a "path=/tmp/passwd owner=sanchuang"
192.168.2.132 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    }, 
    "changed": true, 
    "gid": 0, 
    "group": "root", 
    "mode": "0777", 
    "owner": "sanchuang", 
    "path": "/tmp/passwd", 
    "secontext": "unconfined_u:object_r:admin_home_t:s0", 
    "size": 1045, 
    "state": "file", 
    "uid": 1009
}

2、软连接 ,硬链接
创建一个硬链接 文件的链接数会+1。删除硬链接文件或者是源文件 只是把文件的链接数-1 文件不会被真正的删掉。
① 软连接:

[root@b lianxi]# vim ansible-copy
[root@b lianxi]# ln -s ansible-copy ansible-copy-link-s  #软链接
[root@b lianxi]# ls -al
总用量 4
drwxr-xr-x   4 root root  89 1125 11:37 .
dr-xr-xr-x. 18 root root 258 1125 10:10 ..
drwxr-xr-x   3 root root  17 1125 11:03 192.168.0.48
-rw-r--r--   1 root root   8 1125 11:36 ansible-copy
lrwxrwxrwx   1 root root  12 1125 11:37 ansible-copy-link-s -> ansible-copy
drwxr-xr-x   2 root root  36 1125 10:29 copy_dir

② 硬链接

[root@b lianxi]# ln ansible-copy ansible-copy-link   #硬链接
[root@b lianxi]# ls -al
总用量 8
drwxr-xr-x   4 root root 114 1125 11:38 .
dr-xr-xr-x. 18 root root 258 1125 10:10 ..
drwxr-xr-x   3 root root  17 1125 11:03 192.168.0.48
-rw-r--r--   2 root root   8 1125 11:36 ansible-copy
-rw-r--r--   2 root root   8 1125 11:36 ansible-copy-link
lrwxrwxrwx   1 root root  12 1125 11:37 ansible-copy-link-s -> ansible-copy
drwxr-xr-x   2 root root  36 1125 10:29 copy_dir

6、cron模块

通过cron模块对目标主机生成计划任务。

常用参数:
除了分(minute)时(hour)日(day)月(month)周(week)外
name: 本次计划任务的名称
state: present 生成(默认) | absent 删除 (基于name)

ntp服务,是一个时间管理服务器
在 centos 8 中, ntp 已经被 chrony 代替。
之前的版本:yum install -y ntp
centos8:yum install chrony

例题:每三分钟输出当前时间,到/tmp/time.txt文件。这个计划任务是设置为每三分钟生成一次,所以要记得及时删除。

[root@b ~]# ansible all -m cron -a "minute=*/3 job='date >>/tmp/time.txt' name=date_test  state=present"
192.168.2.132 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    }, 
    "changed": true, 
    "envs": [], 
    "jobs": [
        "date_test"
    ]
}

如果做测试的话,要及时删除计划任务。如果不记得设置的计划任务的名字,可以通过查看计划任务,上方会显示计划任务的名称,然后指定那个名字删除就可以了。
crantab -l:查看计划任务。
删除刚刚创建的计划任务。name是指刚刚创建的计划任务的名字。state=absent是表示删除。

[root@b ~]# ansible 192.168.0.48 -m cron -a "name=date_test  state=absent"
192.168.2.132 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    }, 
    "changed": true, 
    "envs": [], 
    "jobs": []
}

例题:每天凌晨1点 ,检查/etc/passwd 文件是否被修改,并且生成检查报告。
实现ansible node节点服务器备份,备份/var/log/messages 备份到/backup目录下,并且取名2020-11-25-01-log.tar.gz,每一个小时执行一次。

ansible webserver -m cron -a "minute=*/1 job='tar -czf /tmp/sc/$(date +%Y-%m-%d-%H)-log.tar.gz /var/log/messages' name=date_test state=present"

7、yum模块

故名思义就是yum安装软件包的模块;

常用参数说明:
enablerepo,disablerepo表示启用与禁用某repo库
name 安装包名
state (present’ or installed’, latest’)表示安装, (absent’ or `removed’) 表示删除

示例:通过安装epel扩展源并安装nginx。
1、安装wget:

[root@b ~]# ansible all -m yum -a "name=wget state=installed"

2、卸载wget

[root@b ~]# ansible all -m yum -a "name=wget state=absent"

8、service模块

服务管理模块。

常用参数:
name:服务名
state:服务状态 started(启动) stopped(关闭) restarted(重启) reloaded(重新加载)
enabled: 是否开机启动 true|false
runlevel: 启动级别 (systemed方式忽略)

安装文件传输服务vsftpd。

[root@b ~]# ansible all -m yum -a "name=vsftpd state=installed"
192.168.0.48 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    }, 
    "changed": true, 
    "msg": "", 
    "rc": 0, 
    "results": [
        "Installed: vsftpd-3.0.3-31.el8.x86_64"
    ]
}

关闭vsftpd服务:ansible all -m service -a “name=vsftpd state=stopped”

[root@b ~]# ansible all -m service -a "name=vsftpd state=stopped"

开启vsftpd服务:ansible all -m service -a “name=vsftpd state=started”

[root@b ~]# ansible all -m service -a "name=vsftpd state=started"

例题:使用ansible 部署web服务
安装nginx, 修改nginx的配置文件 /etc/nginx/conf.d/sc.conf
传递index.html文件 到 /opt/dist目录下
index里面的内容: this is index
启动服务
测试能不能访问

server {
        listen       80 ;
        server_name  www.sc.com;
        root         /opt/dist;
	access_log  /var/log/nginx/sc_access.log  main;
        location / {
	}
	location =/api {
	}
}
[root@scmysql opt]# curl -H "Host: www.sc.com" http://192.168.77.13
4
this is index

9、script模块

把本地的脚本传到远端执行;前提是到远端可以执行,不要把Linux下的脚本同步到windows下执行;只在远程服务器执行脚本,不上传脚本到远程服务器。
例如:测试将ansible本地机上的文件放在B、C机器上执行。

1、本地机器上的文件test.sh
[root@b ~]# cat test.sh
#!/bin/bash
echo "test ansible" >> /tmp/ansible.txt

2、使用script命令将本地机上的test.sh文件放在B、C两台远程机上执行。
[root@sc-master ~]# ansible all -m script -a "/root/test.sh"
192.168.2.132 | CHANGED => {
    "changed": true, 
    "rc": 0, 
    "stderr": "Shared connection to 192.168.2.132 closed.\r\n", 
    "stderr_lines": [
        "Shared connection to 192.168.2.132 closed."
    ], 
    "stdout": "", 
    "stdout_lines": []
}
192.168.2.137 | CHANGED => {
    "changed": true, 
    "rc": 0, 
    "stderr": "Shared connection to 192.168.2.137 closed.\r\n", 
    "stderr_lines": [
        "Shared connection to 192.168.2.137 closed."
    ], 
    "stdout": "", 
    "stdout_lines": []
}

执行慢,机器多 。可以使用多进程去执行
-f 6 执行6个进程去执行。

你可能感兴趣的:(linux,ansible,运维)