0cc2103a90
- 新增'集群部署'分类(6个模板) - Kubernetes集群部署(kubeadm, Master+Worker, Calico/Flannel) - Ceph存储集群部署(cephadm, MON+OSD+MDS+RGW+Dashboard) - K8S + Rook-Ceph持久化存储(Operator+CephCluster+StorageClass) - ETCD集群部署(3/5节点, Systemd服务) - Harbor镜像仓库部署(HTTPS+Trivy扫描) - NFS共享存储集群(服务端+客户端自动挂载) - 总模板数: 24 -> 30
2654 lines
75 KiB
Go
2654 lines
75 KiB
Go
package services
|
||
|
||
// PlaybookTemplate Playbook模板定义
|
||
type PlaybookTemplate struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Category string `json:"category"`
|
||
Description string `json:"description"`
|
||
Icon string `json:"icon"`
|
||
Content string `json:"content"`
|
||
Tags []string `json:"tags"`
|
||
}
|
||
|
||
// TemplateCategory 模板分类
|
||
type TemplateCategory struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Icon string `json:"icon"`
|
||
Count int `json:"count"`
|
||
}
|
||
|
||
// GetTemplateCategories 获取所有模板分类
|
||
func GetTemplateCategories() []TemplateCategory {
|
||
cats := map[string]TemplateCategory{}
|
||
for _, t := range GetPlaybookTemplates() {
|
||
c, ok := cats[t.Category]
|
||
if !ok {
|
||
c = TemplateCategory{ID: t.Category, Name: categoryNames[t.Category], Icon: categoryIcons[t.Category]}
|
||
}
|
||
c.Count++
|
||
cats[t.Category] = c
|
||
}
|
||
var result []TemplateCategory
|
||
// 按固定顺序输出
|
||
order := []string{"system", "web", "database", "container", "cluster", "devops", "security", "monitoring"}
|
||
for _, id := range order {
|
||
if c, ok := cats[id]; ok {
|
||
result = append(result, c)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
var categoryNames = map[string]string{
|
||
"system": "系统管理",
|
||
"web": "Web服务",
|
||
"database": "数据库",
|
||
"container": "容器化",
|
||
"cluster": "集群部署",
|
||
"devops": "DevOps",
|
||
"security": "安全加固",
|
||
"monitoring": "监控告警",
|
||
}
|
||
|
||
var categoryIcons = map[string]string{
|
||
"system": "🖥️",
|
||
"web": "🌐",
|
||
"database": "🗄️",
|
||
"container": "🐳",
|
||
"cluster": "☸️",
|
||
"devops": "🔧",
|
||
"security": "🔒",
|
||
"monitoring": "📊",
|
||
}
|
||
|
||
// GetPlaybookTemplates 获取所有Playbook模板
|
||
func GetPlaybookTemplates() []PlaybookTemplate {
|
||
return []PlaybookTemplate{
|
||
// ===== 系统管理 =====
|
||
{
|
||
ID: "update-packages", Name: "系统包更新", Category: "system",
|
||
Description: "自动更新系统软件包,支持Debian/RedHat系列",
|
||
Icon: "📦", Tags: []string{"update", "apt", "yum"},
|
||
Content: `---
|
||
- name: 系统包更新
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
update_cache: yes
|
||
upgrade_type: dist # dist(完整升级) 或 safe(安全升级)
|
||
|
||
tasks:
|
||
- name: 更新apt缓存
|
||
apt:
|
||
update_cache: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 升级所有包(Debian)
|
||
apt:
|
||
upgrade: "{{ upgrade_type }}"
|
||
autoremove: yes
|
||
autoclean: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 更新yum缓存
|
||
yum:
|
||
update_cache: yes
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 升级所有包(RedHat)
|
||
yum:
|
||
name: "*"
|
||
state: latest
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 检查是否需要重启
|
||
stat:
|
||
path: /var/run/reboot-required
|
||
register: reboot_required
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 提示重启
|
||
debug:
|
||
msg: "⚠️ 系统需要重启以完成更新"
|
||
when: ansible_os_family == "Debian" and reboot_required.stat.exists
|
||
`,
|
||
},
|
||
{
|
||
ID: "check-system", Name: "系统信息采集", Category: "system",
|
||
Description: "采集操作系统、硬件、网络等详细信息",
|
||
Icon: "🔍", Tags: []string{"info", "facts", "inventory"},
|
||
Content: `---
|
||
- name: 系统信息采集
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
gather_facts: yes
|
||
|
||
tasks:
|
||
- name: 操作系统信息
|
||
debug:
|
||
msg: |
|
||
══════════ 系统信息 ══════════
|
||
主机名: {{ ansible_facts['hostname'] }}
|
||
系统: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
|
||
内核: {{ ansible_facts['kernel'] }}
|
||
架构: {{ ansible_facts['architecture'] }}
|
||
CPU: {{ ansible_facts['processor_vcpus'] }} vCPUs
|
||
内存: {{ (ansible_facts['memtotal_mb'] / 1024) | round(2) }} GB
|
||
IP: {{ ansible_facts['default_ipv4']['address'] | default('N/A') }}
|
||
═══════════════════════════════
|
||
|
||
- name: 磁盘使用
|
||
shell: df -h | grep -E '^/dev/'
|
||
register: disk_info
|
||
|
||
- name: 显示磁盘
|
||
debug:
|
||
msg: "{{ disk_info.stdout_lines }}"
|
||
|
||
- name: 运行时间
|
||
shell: uptime -p
|
||
register: uptime_info
|
||
|
||
- name: 显示运行时间
|
||
debug:
|
||
msg: "运行时间: {{ uptime_info.stdout }}"
|
||
`,
|
||
},
|
||
{
|
||
ID: "check-resources", Name: "资源监控检查", Category: "system",
|
||
Description: "检查CPU/内存/磁盘使用率,超阈值告警",
|
||
Icon: "📈", Tags: []string{"monitor", "cpu", "memory", "disk"},
|
||
Content: `---
|
||
- name: 资源监控检查
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
gather_facts: yes
|
||
vars:
|
||
warn_threshold: 80
|
||
crit_threshold: 90
|
||
|
||
tasks:
|
||
- name: 获取CPU使用率
|
||
shell: top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1
|
||
register: cpu_result
|
||
changed_when: false
|
||
|
||
- name: 获取内存使用率
|
||
shell: free | grep Mem | awk '{printf "%.0f", $3/$2*100}'
|
||
register: mem_result
|
||
changed_when: false
|
||
|
||
- name: 获取磁盘使用率
|
||
shell: df -h / | tail -1 | awk '{print $5}' | tr -d '%'
|
||
register: disk_result
|
||
changed_when: false
|
||
|
||
- name: 显示资源报告
|
||
debug:
|
||
msg: |
|
||
═══════ {{ inventory_hostname }} 资源报告 ═══════
|
||
CPU 使用率: {{ cpu_result.stdout }}%
|
||
内存使用率: {{ mem_result.stdout }}%
|
||
磁盘使用率: {{ disk_result.stdout }}%
|
||
告警阈值: {{ warn_threshold }}% / 严重: {{ crit_threshold }}%
|
||
═══════════════════════════════════════
|
||
|
||
- name: 严重告警
|
||
fail:
|
||
msg: "🚨 严重: {{ inventory_hostname }} 资源使用率超过 {{ crit_threshold }}%"
|
||
when: >
|
||
(cpu_result.stdout | int >= crit_threshold) or
|
||
(mem_result.stdout | int >= crit_threshold) or
|
||
(disk_result.stdout | int >= crit_threshold)
|
||
`,
|
||
},
|
||
{
|
||
ID: "log-cleanup", Name: "日志清理", Category: "system",
|
||
Description: "清理系统日志、临时文件,释放磁盘空间",
|
||
Icon: "🧹", Tags: []string{"cleanup", "log", "disk"},
|
||
Content: `---
|
||
- name: 日志清理
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
log_retention_days: 30
|
||
tmp_retention_days: 7
|
||
|
||
tasks:
|
||
- name: 清理journal日志
|
||
shell: journalctl --vacuum-time={{ log_retention_days }}d
|
||
register: journal_result
|
||
changed_when: "'Vacuuming' in journal_result.stdout"
|
||
|
||
- name: 清理旧日志文件
|
||
find:
|
||
paths: /var/log
|
||
patterns: "*.gz,*.old,*.[0-9]"
|
||
age: "{{ log_retention_days }}d"
|
||
register: old_logs
|
||
|
||
- name: 删除旧日志
|
||
file:
|
||
path: "{{ item.path }}"
|
||
state: absent
|
||
loop: "{{ old_logs.files }}"
|
||
|
||
- name: 清理临时文件
|
||
find:
|
||
paths: /tmp
|
||
age: "{{ tmp_retention_days }}d"
|
||
register: tmp_files
|
||
|
||
- name: 删除临时文件
|
||
file:
|
||
path: "{{ item.path }}"
|
||
state: absent
|
||
loop: "{{ tmp_files.files }}"
|
||
ignore_errors: yes
|
||
|
||
- name: 清理apt缓存
|
||
apt:
|
||
autoclean: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 显示磁盘空间
|
||
shell: df -h /
|
||
register: disk_after
|
||
|
||
- name: 清理完成
|
||
debug:
|
||
msg: "{{ disk_after.stdout_lines }}"
|
||
`,
|
||
},
|
||
{
|
||
ID: "time-sync", Name: "时间同步(NTP)", Category: "system",
|
||
Description: "配置NTP时间同步,确保集群时间一致",
|
||
Icon: "⏰", Tags: []string{"ntp", "time", "chrony"},
|
||
Content: `---
|
||
- name: 时间同步配置
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
ntp_servers:
|
||
- ntp.aliyun.com
|
||
- ntp1.aliyun.com
|
||
- pool.ntp.org
|
||
|
||
tasks:
|
||
- name: 安装chrony
|
||
package:
|
||
name: chrony
|
||
state: present
|
||
|
||
- name: 配置NTP服务器
|
||
lineinfile:
|
||
path: /etc/chrony.conf
|
||
regexp: "^server "
|
||
line: "server {{ item }} iburst"
|
||
create: yes
|
||
loop: "{{ ntp_servers }}"
|
||
|
||
- name: 启动chrony
|
||
service:
|
||
name: chronyd
|
||
state: started
|
||
enabled: yes
|
||
|
||
- name: 验证时间同步
|
||
shell: chronyc tracking
|
||
register: chrony_status
|
||
changed_when: false
|
||
|
||
- name: 显示同步状态
|
||
debug:
|
||
msg: "{{ chrony_status.stdout_lines }}"
|
||
`,
|
||
},
|
||
|
||
// ===== Web服务 =====
|
||
{
|
||
ID: "deploy-nginx", Name: "部署Nginx", Category: "web",
|
||
Description: "安装配置Nginx,支持自定义站点和反向代理",
|
||
Icon: "🌐", Tags: []string{"nginx", "web", "proxy"},
|
||
Content: `---
|
||
- name: 部署Nginx
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
nginx_worker_processes: auto
|
||
nginx_worker_connections: 1024
|
||
server_name: localhost
|
||
listen_port: 80
|
||
proxy_pass: ""
|
||
root_dir: /var/www/html
|
||
|
||
tasks:
|
||
- name: 安装Nginx
|
||
package:
|
||
name: nginx
|
||
state: present
|
||
|
||
- name: 创建网站目录
|
||
file:
|
||
path: "{{ root_dir }}"
|
||
state: directory
|
||
owner: www-data
|
||
group: www-data
|
||
mode: '0755'
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 创建网站目录(RedHat)
|
||
file:
|
||
path: "{{ root_dir }}"
|
||
state: directory
|
||
owner: nginx
|
||
group: nginx
|
||
mode: '0755'
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 部署默认页面
|
||
copy:
|
||
content: |
|
||
<!DOCTYPE html>
|
||
<html><head><title>{{ server_name }}</title></head>
|
||
<body><h1>Deployed by Ansible Deploy</h1>
|
||
<p>Server: {{ inventory_hostname }}</p></body></html>
|
||
dest: "{{ root_dir }}/index.html"
|
||
when: proxy_pass == ""
|
||
|
||
- name: 配置Nginx站点
|
||
copy:
|
||
content: |
|
||
server {
|
||
listen {{ listen_port }};
|
||
server_name {{ server_name }};
|
||
{% if proxy_pass != "" %}
|
||
location / {
|
||
proxy_pass {{ proxy_pass }};
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
}
|
||
{% else %}
|
||
root {{ root_dir }};
|
||
index index.html;
|
||
location / {
|
||
try_files $uri $uri/ =404;
|
||
}
|
||
{% endif %}
|
||
}
|
||
dest: /etc/nginx/conf.d/{{ server_name }}.conf
|
||
notify: Reload Nginx
|
||
|
||
- name: 启动Nginx
|
||
service:
|
||
name: nginx
|
||
state: started
|
||
enabled: yes
|
||
|
||
handlers:
|
||
- name: Reload Nginx
|
||
service:
|
||
name: nginx
|
||
state: reloaded
|
||
`,
|
||
},
|
||
{
|
||
ID: "deploy-apache", Name: "部署Apache", Category: "web",
|
||
Description: "安装配置Apache HTTP Server",
|
||
Icon: "🪶", Tags: []string{"apache", "httpd", "web"},
|
||
Content: `---
|
||
- name: 部署Apache
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
server_name: localhost
|
||
listen_port: 80
|
||
document_root: /var/www/html
|
||
|
||
tasks:
|
||
- name: 安装Apache(Debian)
|
||
apt:
|
||
name: apache2
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装Apache(RedHat)
|
||
yum:
|
||
name: httpd
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 创建网站目录
|
||
file:
|
||
path: "{{ document_root }}"
|
||
state: directory
|
||
mode: '0755'
|
||
|
||
- name: 部署测试页面
|
||
copy:
|
||
content: "<h1>Apache on {{ inventory_hostname }}</h1>"
|
||
dest: "{{ document_root }}/index.html"
|
||
|
||
- name: 启动Apache(Debian)
|
||
service:
|
||
name: apache2
|
||
state: started
|
||
enabled: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 启动Apache(RedHat)
|
||
service:
|
||
name: httpd
|
||
state: started
|
||
enabled: yes
|
||
when: ansible_os_family == "RedHat"
|
||
`,
|
||
},
|
||
|
||
// ===== 数据库 =====
|
||
{
|
||
ID: "deploy-mysql", Name: "部署MySQL", Category: "database",
|
||
Description: "安装MySQL/MariaDB,配置root密码和基础优化",
|
||
Icon: "🐬", Tags: []string{"mysql", "mariadb", "database"},
|
||
Content: `---
|
||
- name: 部署MySQL
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
mysql_root_password: "ChangeMe@2024"
|
||
mysql_port: 3306
|
||
mysql_max_connections: 500
|
||
mysql_datadir: /var/lib/mysql
|
||
|
||
tasks:
|
||
- name: 安装MySQL(Debian)
|
||
apt:
|
||
name:
|
||
- mysql-server
|
||
- mysql-client
|
||
- python3-pymysql
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装MariaDB(RedHat)
|
||
yum:
|
||
name:
|
||
- mariadb-server
|
||
- mariadb
|
||
- python3-PyMySQL
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 启动MySQL
|
||
service:
|
||
name: "{{ 'mysql' if ansible_os_family == 'Debian' else 'mariadb' }}"
|
||
state: started
|
||
enabled: yes
|
||
|
||
- name: 设置root密码
|
||
shell: |
|
||
mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ mysql_root_password }}'; FLUSH PRIVILEGES;"
|
||
ignore_errors: yes
|
||
no_log: true
|
||
|
||
- name: 优化配置
|
||
copy:
|
||
content: |
|
||
[mysqld]
|
||
port = {{ mysql_port }}
|
||
max_connections = {{ mysql_max_connections }}
|
||
datadir = {{ mysql_datadir }}
|
||
innodb_buffer_pool_size = 256M
|
||
character-set-server = utf8mb4
|
||
collation-server = utf8mb4_unicode_ci
|
||
slow_query_log = 1
|
||
slow_query_log_file = /var/log/mysql/slow.log
|
||
long_query_time = 2
|
||
dest: /etc/mysql/conf.d/optimize.cnf
|
||
when: ansible_os_family == "Debian"
|
||
notify: Restart MySQL
|
||
|
||
- name: 验证MySQL
|
||
shell: mysql -u root -p{{ mysql_root_password }} -e "SELECT VERSION();"
|
||
register: mysql_version
|
||
changed_when: false
|
||
no_log: true
|
||
|
||
- name: 显示版本
|
||
debug:
|
||
msg: "MySQL版本: {{ mysql_version.stdout_lines[-1] | default('unknown') }}"
|
||
|
||
handlers:
|
||
- name: Restart MySQL
|
||
service:
|
||
name: "{{ 'mysql' if ansible_os_family == 'Debian' else 'mariadb' }}"
|
||
state: restarted
|
||
`,
|
||
},
|
||
{
|
||
ID: "deploy-redis", Name: "部署Redis", Category: "database",
|
||
Description: "安装Redis,配置密码、内存限制和持久化",
|
||
Icon: "🔴", Tags: []string{"redis", "cache", "database"},
|
||
Content: `---
|
||
- name: 部署Redis
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
redis_port: 6379
|
||
redis_password: "ChangeMe@2024"
|
||
redis_maxmemory: "512mb"
|
||
redis_maxmemory_policy: allkeys-lru
|
||
redis_bind: "0.0.0.0"
|
||
|
||
tasks:
|
||
- name: 安装Redis
|
||
package:
|
||
name: redis-server
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装Redis(RedHat)
|
||
yum:
|
||
name: redis
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 配置Redis
|
||
lineinfile:
|
||
path: /etc/redis/redis.conf
|
||
regexp: "{{ item.regexp }}"
|
||
line: "{{ item.line }}"
|
||
loop:
|
||
- { regexp: '^bind ', line: 'bind {{ redis_bind }}' }
|
||
- { regexp: '^port ', line: 'port {{ redis_port }}' }
|
||
- { regexp: '^requirepass ', line: 'requirepass {{ redis_password }}' }
|
||
- { regexp: '^maxmemory ', line: 'maxmemory {{ redis_maxmemory }}' }
|
||
- { regexp: '^maxmemory-policy ', line: 'maxmemory-policy {{ redis_maxmemory_policy }}' }
|
||
notify: Restart Redis
|
||
|
||
- name: 启动Redis
|
||
service:
|
||
name: "{{ 'redis-server' if ansible_os_family == 'Debian' else 'redis' }}"
|
||
state: started
|
||
enabled: yes
|
||
|
||
- name: 验证Redis
|
||
shell: redis-cli -a {{ redis_password }} ping
|
||
register: redis_ping
|
||
changed_when: false
|
||
no_log: true
|
||
|
||
- name: 显示状态
|
||
debug:
|
||
msg: "Redis状态: {{ redis_ping.stdout }}"
|
||
|
||
handlers:
|
||
- name: Restart Redis
|
||
service:
|
||
name: "{{ 'redis-server' if ansible_os_family == 'Debian' else 'redis' }}"
|
||
state: restarted
|
||
`,
|
||
},
|
||
{
|
||
ID: "deploy-postgresql", Name: "部署PostgreSQL", Category: "database",
|
||
Description: "安装PostgreSQL,配置用户和基础优化",
|
||
Icon: "🐘", Tags: []string{"postgresql", "postgres", "database"},
|
||
Content: `---
|
||
- name: 部署PostgreSQL
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
postgres_version: "15"
|
||
postgres_password: "ChangeMe@2024"
|
||
postgres_port: 5432
|
||
postgres_max_connections: 200
|
||
|
||
tasks:
|
||
- name: 安装PostgreSQL(Debian)
|
||
apt:
|
||
name:
|
||
- postgresql
|
||
- postgresql-contrib
|
||
- python3-psycopg2
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装PostgreSQL(RedHat)
|
||
yum:
|
||
name:
|
||
- postgresql-server
|
||
- postgresql-contrib
|
||
- python3-psycopg2
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 初始化数据库(RedHat)
|
||
shell: postgresql-setup --initdb
|
||
when: ansible_os_family == "RedHat"
|
||
ignore_errors: yes
|
||
|
||
- name: 启动PostgreSQL
|
||
service:
|
||
name: postgresql
|
||
state: started
|
||
enabled: yes
|
||
|
||
- name: 设置postgres密码
|
||
become_user: postgres
|
||
shell: psql -c "ALTER USER postgres PASSWORD '{{ postgres_password }}';"
|
||
no_log: true
|
||
|
||
- name: 验证安装
|
||
become_user: postgres
|
||
shell: psql -c "SELECT version();"
|
||
register: pg_version
|
||
changed_when: false
|
||
|
||
- name: 显示版本
|
||
debug:
|
||
msg: "{{ pg_version.stdout_lines[0] | default('PostgreSQL installed') }}"
|
||
`,
|
||
},
|
||
|
||
// ===== 容器化 =====
|
||
{
|
||
ID: "deploy-docker", Name: "部署Docker", Category: "container",
|
||
Description: "安装Docker CE + Docker Compose,配置镜像加速",
|
||
Icon: "🐳", Tags: []string{"docker", "container", "compose"},
|
||
Content: `---
|
||
- name: 部署Docker
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
docker_mirror: "https://mirror.ccs.tencentyun.com"
|
||
docker_data_root: /var/lib/docker
|
||
docker_log_max_size: "100m"
|
||
docker_log_max_file: "3"
|
||
|
||
tasks:
|
||
- name: 安装依赖(Debian)
|
||
apt:
|
||
name:
|
||
- apt-transport-https
|
||
- ca-certificates
|
||
- curl
|
||
- gnupg
|
||
- lsb-release
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 添加Docker GPG密钥
|
||
shell: |
|
||
curl -fsSL https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||
when: ansible_os_family == "Debian"
|
||
ignore_errors: yes
|
||
|
||
- name: 添加Docker仓库
|
||
shell: |
|
||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable" > /etc/apt/sources.list.d/docker.list
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装Docker(Debian)
|
||
apt:
|
||
name:
|
||
- docker-ce
|
||
- docker-ce-cli
|
||
- containerd.io
|
||
- docker-compose-plugin
|
||
state: present
|
||
update_cache: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装Docker(RedHat)
|
||
yum:
|
||
name:
|
||
- docker
|
||
- docker-compose-plugin
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 配置Docker
|
||
copy:
|
||
content: |
|
||
{
|
||
"data-root": "{{ docker_data_root }}",
|
||
"registry-mirrors": ["{{ docker_mirror }}"],
|
||
"log-driver": "json-file",
|
||
"log-opts": {
|
||
"max-size": "{{ docker_log_max_size }}",
|
||
"max-file": "{{ docker_log_max_file }}"
|
||
},
|
||
"storage-driver": "overlay2",
|
||
"live-restore": true
|
||
}
|
||
dest: /etc/docker/daemon.json
|
||
notify: Restart Docker
|
||
|
||
- name: 启动Docker
|
||
service:
|
||
name: docker
|
||
state: started
|
||
enabled: yes
|
||
|
||
- name: 添加用户到docker组
|
||
user:
|
||
name: "{{ ansible_user }}"
|
||
groups: docker
|
||
append: yes
|
||
ignore_errors: yes
|
||
|
||
- name: 验证Docker
|
||
shell: docker version --format '{{.Server.Version}}'
|
||
register: docker_ver
|
||
changed_when: false
|
||
|
||
- name: 显示版本
|
||
debug:
|
||
msg: "Docker版本: {{ docker_ver.stdout }}"
|
||
|
||
handlers:
|
||
- name: Restart Docker
|
||
service:
|
||
name: docker
|
||
state: restarted
|
||
`,
|
||
},
|
||
{
|
||
ID: "deploy-portainer", Name: "部署Portainer", Category: "container",
|
||
Description: "部署Portainer CE容器管理面板",
|
||
Icon: "🎛️", Tags: []string{"portainer", "docker", "ui"},
|
||
Content: `---
|
||
- name: 部署Portainer
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
portainer_port: 9443
|
||
portainer_data: /opt/portainer
|
||
|
||
tasks:
|
||
- name: 创建数据目录
|
||
file:
|
||
path: "{{ portainer_data }}"
|
||
state: directory
|
||
|
||
- name: 拉取并启动Portainer
|
||
shell: |
|
||
docker volume create portainer_data
|
||
docker run -d \
|
||
-p 8000:8000 \
|
||
-p {{ portainer_port }}:9443 \
|
||
--name portainer \
|
||
--restart=always \
|
||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||
-v portainer_data:/data \
|
||
portainer/portainer-ce:latest
|
||
register: portainer_result
|
||
ignore_errors: yes
|
||
|
||
- name: 显示访问地址
|
||
debug:
|
||
msg: "Portainer已部署: https://{{ ansible_host | default(inventory_hostname) }}:{{ portainer_port }}"
|
||
`,
|
||
},
|
||
|
||
// ===== DevOps =====
|
||
{
|
||
ID: "deploy-nodejs", Name: "部署Node.js", Category: "devops",
|
||
Description: "安装Node.js + PM2进程管理器",
|
||
Icon: "💚", Tags: []string{"nodejs", "npm", "pm2"},
|
||
Content: `---
|
||
- name: 部署Node.js
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
node_version: "20"
|
||
install_pm2: true
|
||
|
||
tasks:
|
||
- name: 安装NodeSource仓库
|
||
shell: |
|
||
curl -fsSL https://deb.nodesource.com/setup_{{ node_version }}.x | bash -
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装Node.js(Debian)
|
||
apt:
|
||
name: nodejs
|
||
state: present
|
||
update_cache: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装Node.js(RedHat)
|
||
yum:
|
||
name: nodejs
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 安装PM2
|
||
npm:
|
||
name: pm2
|
||
global: yes
|
||
when: install_pm2
|
||
|
||
- name: 设置PM2开机启动
|
||
shell: pm2 startup systemd -u {{ ansible_user }} --hp /home/{{ ansible_user }}
|
||
when: install_pm2
|
||
ignore_errors: yes
|
||
|
||
- name: 验证安装
|
||
shell: node --version && npm --version
|
||
register: node_ver
|
||
changed_when: false
|
||
|
||
- name: 显示版本
|
||
debug:
|
||
msg: "{{ node_ver.stdout_lines }}"
|
||
`,
|
||
},
|
||
{
|
||
ID: "deploy-python-app", Name: "部署Python应用", Category: "devops",
|
||
Description: "配置Python环境 + Gunicorn + Systemd服务",
|
||
Icon: "🐍", Tags: []string{"python", "gunicorn", "flask", "django"},
|
||
Content: `---
|
||
- name: 部署Python应用
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
app_name: myapp
|
||
app_dir: /opt/myapp
|
||
python_version: python3
|
||
venv_dir: /opt/myapp/venv
|
||
gunicorn_port: 8000
|
||
gunicorn_workers: 4
|
||
app_user: www-data
|
||
|
||
tasks:
|
||
- name: 安装Python和依赖
|
||
package:
|
||
name:
|
||
- "{{ python_version }}"
|
||
- "{{ python_version }}-venv"
|
||
- python3-pip
|
||
state: present
|
||
|
||
- name: 创建应用目录
|
||
file:
|
||
path: "{{ app_dir }}"
|
||
state: directory
|
||
owner: "{{ app_user }}"
|
||
mode: '0755'
|
||
|
||
- name: 创建虚拟环境
|
||
shell: "{{ python_version }} -m venv {{ venv_dir }}"
|
||
args:
|
||
creates: "{{ venv_dir }}/bin/activate"
|
||
|
||
- name: 安装Gunicorn
|
||
pip:
|
||
name: gunicorn
|
||
virtualenv: "{{ venv_dir }}"
|
||
|
||
- name: 创建Systemd服务
|
||
copy:
|
||
content: |
|
||
[Unit]
|
||
Description={{ app_name }} Gunicorn Service
|
||
After=network.target
|
||
|
||
[Service]
|
||
User={{ app_user }}
|
||
WorkingDirectory={{ app_dir }}
|
||
ExecStart={{ venv_dir }}/bin/gunicorn --workers {{ gunicorn_workers }} --bind 0.0.0.0:{{ gunicorn_port }} app:app
|
||
Restart=always
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
dest: /etc/systemd/system/{{ app_name }}.service
|
||
notify: Restart App
|
||
|
||
- name: 启动服务
|
||
systemd:
|
||
name: "{{ app_name }}"
|
||
state: started
|
||
enabled: yes
|
||
daemon_reload: yes
|
||
|
||
handlers:
|
||
- name: Restart App
|
||
systemd:
|
||
name: "{{ app_name }}"
|
||
state: restarted
|
||
daemon_reload: yes
|
||
`,
|
||
},
|
||
{
|
||
ID: "backup-data", Name: "数据备份", Category: "devops",
|
||
Description: "定时备份指定目录到本地或远程",
|
||
Icon: "💾", Tags: []string{"backup", "cron", "archive"},
|
||
Content: `---
|
||
- name: 数据备份
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
backup_source: /data
|
||
backup_dest: /backup
|
||
backup_retention_days: 7
|
||
backup_hour: "2"
|
||
backup_minute: "0"
|
||
|
||
tasks:
|
||
- name: 创建备份目录
|
||
file:
|
||
path: "{{ backup_dest }}"
|
||
state: directory
|
||
mode: '0700'
|
||
|
||
- name: 创建备份脚本
|
||
copy:
|
||
content: |
|
||
#!/bin/bash
|
||
DATE=$(date +%Y%m%d_%H%M%S)
|
||
BACKUP_FILE="{{ backup_dest }}/backup_${DATE}.tar.gz"
|
||
tar -czf "$BACKUP_FILE" {{ backup_source }} 2>/dev/null
|
||
find {{ backup_dest }} -name "backup_*.tar.gz" -mtime +{{ backup_retention_days }} -delete
|
||
echo "Backup completed: $BACKUP_FILE"
|
||
dest: /usr/local/bin/backup.sh
|
||
mode: '0755'
|
||
|
||
- name: 配置定时任务
|
||
cron:
|
||
name: "daily-backup"
|
||
hour: "{{ backup_hour }}"
|
||
minute: "{{ backup_minute }}"
|
||
job: "/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
|
||
|
||
- name: 立即执行一次备份
|
||
shell: /usr/local/bin/backup.sh
|
||
register: backup_result
|
||
|
||
- name: 备份结果
|
||
debug:
|
||
msg: "{{ backup_result.stdout }}"
|
||
`,
|
||
},
|
||
|
||
// ===== 安全加固 =====
|
||
{
|
||
ID: "security-hardening", Name: "安全加固", Category: "security",
|
||
Description: "SSH加固、防火墙配置、禁用root远程登录",
|
||
Icon: "🔒", Tags: []string{"security", "ssh", "firewall", "hardening"},
|
||
Content: `---
|
||
- name: 安全加固
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
ssh_port: 22
|
||
disable_root_login: true
|
||
enable_firewall: true
|
||
allowed_ssh_users: []
|
||
|
||
tasks:
|
||
- name: 修改SSH端口
|
||
lineinfile:
|
||
path: /etc/ssh/sshd_config
|
||
regexp: "^#?Port "
|
||
line: "Port {{ ssh_port }}"
|
||
notify: Restart SSH
|
||
|
||
- name: 禁用root登录
|
||
lineinfile:
|
||
path: /etc/ssh/sshd_config
|
||
regexp: "^#?PermitRootLogin "
|
||
line: "PermitRootLogin no"
|
||
when: disable_root_login
|
||
notify: Restart SSH
|
||
|
||
- name: 禁用密码认证(仅密钥)
|
||
lineinfile:
|
||
path: /etc/ssh/sshd_config
|
||
regexp: "^#?PasswordAuthentication "
|
||
line: "PasswordAuthentication no"
|
||
notify: Restart SSH
|
||
|
||
- name: 设置SSH超时
|
||
lineinfile:
|
||
path: /etc/ssh/sshd_config
|
||
regexp: "^#?ClientAliveInterval "
|
||
line: "ClientAliveInterval 300"
|
||
notify: Restart SSH
|
||
|
||
- name: 安装UFW(Debian)
|
||
apt:
|
||
name: ufw
|
||
state: present
|
||
when: ansible_os_family == "Debian" and enable_firewall
|
||
|
||
- name: 配置UFW规则
|
||
shell: |
|
||
ufw allow {{ ssh_port }}/tcp
|
||
ufw allow 80/tcp
|
||
ufw allow 443/tcp
|
||
ufw --force enable
|
||
when: ansible_os_family == "Debian" and enable_firewall
|
||
|
||
- name: 安装fail2ban
|
||
package:
|
||
name: fail2ban
|
||
state: present
|
||
ignore_errors: yes
|
||
|
||
- name: 启动fail2ban
|
||
service:
|
||
name: fail2ban
|
||
state: started
|
||
enabled: yes
|
||
ignore_errors: yes
|
||
|
||
- name: 加固完成
|
||
debug:
|
||
msg: "✅ 安全加固完成 - SSH端口: {{ ssh_port }}, Root登录: {{ '禁用' if disable_root_login else '允许' }}"
|
||
|
||
handlers:
|
||
- name: Restart SSH
|
||
service:
|
||
name: "{{ 'ssh' if ansible_os_family == 'Debian' else 'sshd' }}"
|
||
state: restarted
|
||
`,
|
||
},
|
||
{
|
||
ID: "ssl-cert", Name: "SSL证书部署", Category: "security",
|
||
Description: "使用Certbot自动申请Let's Encrypt证书",
|
||
Icon: "📜", Tags: []string{"ssl", "https", "certbot", "letsencrypt"},
|
||
Content: `---
|
||
- name: SSL证书部署
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
domain: example.com
|
||
email: admin@example.com
|
||
webroot: /var/www/html
|
||
|
||
tasks:
|
||
- name: 安装Certbot
|
||
package:
|
||
name:
|
||
- certbot
|
||
- python3-certbot-nginx
|
||
state: present
|
||
|
||
- name: 申请证书
|
||
shell: |
|
||
certbot certonly --webroot -w {{ webroot }} \
|
||
-d {{ domain }} \
|
||
--email {{ email }} \
|
||
--agree-tos --non-interactive
|
||
args:
|
||
creates: /etc/letsencrypt/live/{{ domain }}/fullchain.pem
|
||
|
||
- name: 配置自动续期
|
||
cron:
|
||
name: "certbot-renew"
|
||
hour: "3"
|
||
minute: "30"
|
||
job: "certbot renew --quiet --post-hook 'systemctl reload nginx'"
|
||
|
||
- name: 证书信息
|
||
shell: openssl x509 -in /etc/letsencrypt/live/{{ domain }}/fullchain.pem -noout -dates
|
||
register: cert_info
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 显示证书
|
||
debug:
|
||
msg: "{{ cert_info.stdout_lines | default(['证书已申请']) }}"
|
||
`,
|
||
},
|
||
|
||
// ===== 监控告警 =====
|
||
{
|
||
ID: "deploy-node-exporter", Name: "部署Node Exporter", Category: "monitoring",
|
||
Description: "部署Prometheus Node Exporter采集主机指标",
|
||
Icon: "📡", Tags: []string{"prometheus", "monitoring", "exporter"},
|
||
Content: `---
|
||
- name: 部署Node Exporter
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
exporter_version: "1.7.0"
|
||
exporter_port: 9100
|
||
|
||
tasks:
|
||
- name: 创建用户
|
||
user:
|
||
name: node_exporter
|
||
system: yes
|
||
shell: /usr/sbin/nologin
|
||
create_home: no
|
||
|
||
- name: 下载Node Exporter
|
||
get_url:
|
||
url: "https://github.com/prometheus/node_exporter/releases/download/v{{ exporter_version }}/node_exporter-{{ exporter_version }}.linux-amd64.tar.gz"
|
||
dest: /tmp/node_exporter.tar.gz
|
||
|
||
- name: 解压
|
||
unarchive:
|
||
src: /tmp/node_exporter.tar.gz
|
||
dest: /tmp
|
||
remote_src: yes
|
||
|
||
- name: 安装二进制
|
||
copy:
|
||
src: "/tmp/node_exporter-{{ exporter_version }}.linux-amd64/node_exporter"
|
||
dest: /usr/local/bin/node_exporter
|
||
mode: '0755'
|
||
remote_src: yes
|
||
|
||
- name: 创建Systemd服务
|
||
copy:
|
||
content: |
|
||
[Unit]
|
||
Description=Node Exporter
|
||
After=network.target
|
||
|
||
[Service]
|
||
User=node_exporter
|
||
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:{{ exporter_port }}
|
||
Restart=always
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
dest: /etc/systemd/system/node_exporter.service
|
||
|
||
- name: 启动服务
|
||
systemd:
|
||
name: node_exporter
|
||
state: started
|
||
enabled: yes
|
||
daemon_reload: yes
|
||
|
||
- name: 验证
|
||
shell: curl -s http://localhost:{{ exporter_port }}/metrics | head -5
|
||
register: metrics
|
||
changed_when: false
|
||
|
||
- name: 显示状态
|
||
debug:
|
||
msg: "Node Exporter运行在端口 {{ exporter_port }}"
|
||
`,
|
||
},
|
||
{
|
||
ID: "deploy-grafana", Name: "部署Grafana", Category: "monitoring",
|
||
Description: "部署Grafana可视化监控面板",
|
||
Icon: "📊", Tags: []string{"grafana", "dashboard", "visualization"},
|
||
Content: `---
|
||
- name: 部署Grafana
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
grafana_port: 3000
|
||
grafana_admin_password: "admin123"
|
||
|
||
tasks:
|
||
- name: 安装依赖
|
||
apt:
|
||
name:
|
||
- apt-transport-https
|
||
- software-properties-common
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 添加Grafana仓库
|
||
shell: |
|
||
curl -fsSL https://apt.grafana.com/gpg.key | gpg --dearmor -o /usr/share/keyrings/grafana.gpg
|
||
echo "deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main" > /etc/apt/sources.list.d/grafana.list
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装Grafana
|
||
apt:
|
||
name: grafana
|
||
state: present
|
||
update_cache: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 配置端口
|
||
lineinfile:
|
||
path: /etc/grafana/grafana.ini
|
||
regexp: "^;?http_port"
|
||
line: "http_port = {{ grafana_port }}"
|
||
|
||
- name: 启动Grafana
|
||
service:
|
||
name: grafana-server
|
||
state: started
|
||
enabled: yes
|
||
|
||
- name: 显示访问信息
|
||
debug:
|
||
msg: "Grafana: http://{{ ansible_host | default(inventory_hostname) }}:{{ grafana_port }} (admin/{{ grafana_admin_password }})"
|
||
`,
|
||
},
|
||
|
||
// ===== 系统管理(新增) =====
|
||
{
|
||
ID: "host-connectivity", Name: "主机连通性测试", Category: "system",
|
||
Description: "批量检测主机SSH连通性、DNS解析、端口可达性",
|
||
Icon: "🔗", Tags: []string{"ping", "connectivity", "network", "check"},
|
||
Content: `---
|
||
- name: 主机连通性测试
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
gather_facts: no
|
||
|
||
tasks:
|
||
- name: Ansible Ping 测试
|
||
ping:
|
||
register: ping_result
|
||
ignore_errors: yes
|
||
|
||
- name: 检测SSH端口
|
||
wait_for:
|
||
host: "{{ ansible_host | default(inventory_hostname) }}"
|
||
port: "{{ ansible_port | default(22) }}"
|
||
timeout: 5
|
||
delegate_to: localhost
|
||
register: ssh_port
|
||
ignore_errors: yes
|
||
|
||
- name: 检测DNS解析
|
||
shell: nslookup {{ inventory_hostname }} 2>/dev/null || host {{ inventory_hostname }} 2>/dev/null || echo "DNS解析失败"
|
||
delegate_to: localhost
|
||
register: dns_result
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 检测常用端口
|
||
wait_for:
|
||
host: "{{ ansible_host | default(inventory_hostname) }}"
|
||
port: "{{ item }}"
|
||
timeout: 3
|
||
delegate_to: localhost
|
||
loop: [80, 443]
|
||
register: port_scan
|
||
ignore_errors: yes
|
||
|
||
- name: 输出连通性报告
|
||
debug:
|
||
msg: |
|
||
═══════ {{ inventory_hostname }} 连通性报告 ═══════
|
||
SSH Ping: {{ '✅ 成功' if ping_result is succeeded else '❌ 失败' }}
|
||
SSH 端口: {{ '✅ 开放' if ssh_port is succeeded else '❌ 不可达' }}
|
||
DNS 解析: {{ dns_result.stdout | default('N/A') }}
|
||
HTTP(80): {{ '✅ 开放' if port_scan.results[0] is succeeded else '❌ 关闭' }}
|
||
HTTPS(443): {{ '✅ 开放' if port_scan.results[1] is succeeded else '❌ 关闭' }}
|
||
═══════════════════════════════════════
|
||
`,
|
||
},
|
||
{
|
||
ID: "ubuntu-multi-ip", Name: "Ubuntu 22.04 多IP配置", Category: "system",
|
||
Description: "通过Netplan为Ubuntu 22.04配置多IP地址(主IP+辅助IP)",
|
||
Icon: "🌐", Tags: []string{"ubuntu", "netplan", "network", "multi-ip"},
|
||
Content: `---
|
||
- name: Ubuntu 22.04 多IP地址配置
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
# 主网卡名称(根据实际修改)
|
||
primary_nic: ens192
|
||
# 主IP配置
|
||
primary_ip: "192.168.1.100/24"
|
||
primary_gateway: "192.168.1.1"
|
||
primary_dns:
|
||
- 223.5.5.5
|
||
- 119.29.29.29
|
||
# 辅助IP列表(可添加多个)
|
||
secondary_ips:
|
||
- "192.168.1.101/24"
|
||
- "10.0.0.100/16"
|
||
|
||
tasks:
|
||
- name: 确认系统版本
|
||
assert:
|
||
that:
|
||
- ansible_distribution == "Ubuntu"
|
||
- ansible_distribution_version is version('22.04', '>=')
|
||
fail_msg: "此模板仅适用于 Ubuntu 22.04+"
|
||
success_msg: "系统版本: {{ ansible_distribution }} {{ ansible_distribution_version }}"
|
||
|
||
- name: 备份现有Netplan配置
|
||
shell: |
|
||
mkdir -p /etc/netplan/backup
|
||
cp /etc/netplan/*.yaml /etc/netplan/backup/ 2>/dev/null || true
|
||
args:
|
||
creates: /etc/netplan/backup
|
||
|
||
- name: 生成Netplan配置
|
||
copy:
|
||
content: |
|
||
network:
|
||
version: 2
|
||
ethernets:
|
||
{{ primary_nic }}:
|
||
addresses:
|
||
- {{ primary_ip }}
|
||
{% for ip in secondary_ips %}
|
||
- {{ ip }}
|
||
{% endfor %}
|
||
routes:
|
||
- to: default
|
||
via: {{ primary_gateway }}
|
||
nameservers:
|
||
addresses:
|
||
{% for dns in primary_dns %}
|
||
- {{ dns }}
|
||
{% endfor %}
|
||
dest: /etc/netplan/01-multi-ip.yaml
|
||
mode: '0600'
|
||
register: netplan_config
|
||
|
||
- name: 验证Netplan配置语法
|
||
shell: netplan try --timeout=10
|
||
environment:
|
||
NETPLAN_TRY_TIMEOUT: "10"
|
||
ignore_errors: yes
|
||
register: netplan_try
|
||
|
||
- name: 应用Netplan配置
|
||
shell: netplan apply
|
||
when: netplan_config is changed
|
||
|
||
- name: 等待网络恢复
|
||
wait_for:
|
||
host: "{{ primary_ip | ansible.utils.ipaddr('address') }}"
|
||
port: 22
|
||
timeout: 30
|
||
delegate_to: localhost
|
||
when: netplan_config is changed
|
||
ignore_errors: yes
|
||
|
||
- name: 验证IP配置
|
||
shell: ip addr show {{ primary_nic }} | grep "inet "
|
||
register: ip_verify
|
||
changed_when: false
|
||
|
||
- name: 显示配置结果
|
||
debug:
|
||
msg: |
|
||
═══════ 多IP配置完成 ═══════
|
||
网卡: {{ primary_nic }}
|
||
主IP: {{ primary_ip }}
|
||
网关: {{ primary_gateway }}
|
||
辅助IP:
|
||
{% for ip in secondary_ips %}
|
||
- {{ ip }}
|
||
{% endfor %}
|
||
当前IP列表:
|
||
{{ ip_verify.stdout }}
|
||
═══════════════════════════
|
||
`,
|
||
},
|
||
{
|
||
ID: "ipmitool-bmc", Name: "IPMI配置BMC地址", Category: "system",
|
||
Description: "使用ipmitool配置服务器BMC/IPMI管理口IP地址",
|
||
Icon: "🔧", Tags: []string{"ipmi", "bmc", "ipmitool", "hardware"},
|
||
Content: `---
|
||
- name: IPMI配置BMC地址
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
# BMC网络配置
|
||
bmc_ip: "10.10.10.100"
|
||
bmc_netmask: "255.255.255.0"
|
||
bmc_gateway: "10.10.10.1"
|
||
# BMC用户配置
|
||
bmc_user: "admin"
|
||
bmc_password: "Admin@123"
|
||
bmc_channel: 1
|
||
# 是否启用DHCP(设为true则忽略上面的静态IP配置)
|
||
bmc_dhcp: false
|
||
|
||
tasks:
|
||
- name: 安装ipmitool
|
||
package:
|
||
name: ipmitool
|
||
state: present
|
||
|
||
- name: 加载IPMI内核模块
|
||
modprobe:
|
||
name: "{{ item }}"
|
||
state: present
|
||
loop:
|
||
- ipmi_devintf
|
||
- ipmi_si
|
||
ignore_errors: yes
|
||
|
||
- name: 查看当前BMC配置
|
||
shell: ipmitool lan print {{ bmc_channel }}
|
||
register: bmc_current
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 显示当前BMC信息
|
||
debug:
|
||
msg: "{{ bmc_current.stdout_lines | default(['无法读取BMC信息']) }}"
|
||
|
||
- name: 设置BMC为静态IP
|
||
shell: ipmitool lan set {{ bmc_channel }} ipsrc static
|
||
when: not bmc_dhcp
|
||
|
||
- name: 配置BMC IP地址
|
||
shell: ipmitool lan set {{ bmc_channel }} ipaddr {{ bmc_ip }}
|
||
when: not bmc_dhcp
|
||
|
||
- name: 配置BMC子网掩码
|
||
shell: ipmitool lan set {{ bmc_channel }} netmask {{ bmc_netmask }}
|
||
when: not bmc_dhcp
|
||
|
||
- name: 配置BMC默认网关
|
||
shell: ipmitool lan set {{ bmc_channel }} defgw ipaddr {{ bmc_gateway }}
|
||
when: not bmc_dhcp
|
||
|
||
- name: 设置BMC为DHCP模式
|
||
shell: ipmitool lan set {{ bmc_channel }} ipsrc dhcp
|
||
when: bmc_dhcp
|
||
|
||
- name: 配置BMC用户密码
|
||
shell: |
|
||
ipmitool user set name {{ bmc_channel }} 2 {{ bmc_user }} 2>/dev/null || true
|
||
ipmitool user set password 2 {{ bmc_password }}
|
||
ipmitool user enable 2
|
||
ipmitool channel setaccess {{ bmc_channel }} 2 privilege=4
|
||
no_log: true
|
||
ignore_errors: yes
|
||
|
||
- name: 验证BMC配置
|
||
shell: ipmitool lan print {{ bmc_channel }}
|
||
register: bmc_verify
|
||
changed_when: false
|
||
|
||
- name: 显示最终配置
|
||
debug:
|
||
msg: |
|
||
═══════ BMC配置完成 ═══════
|
||
模式: {{ 'DHCP' if bmc_dhcp else '静态IP' }}
|
||
IP: {{ bmc_ip }}
|
||
掩码: {{ bmc_netmask }}
|
||
网关: {{ bmc_gateway }}
|
||
用户: {{ bmc_user }}
|
||
═══════════════════════════
|
||
{{ bmc_verify.stdout }}
|
||
`,
|
||
},
|
||
{
|
||
ID: "apt-install", Name: "APT软件安装", Category: "system",
|
||
Description: "通过apt批量安装/卸载软件包,支持指定版本和仓库",
|
||
Icon: "📦", Tags: []string{"apt", "install", "package", "debian"},
|
||
Content: `---
|
||
- name: APT软件安装
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
# 要安装的软件包列表
|
||
packages_install:
|
||
- vim
|
||
- curl
|
||
- wget
|
||
- htop
|
||
- net-tools
|
||
- tree
|
||
- unzip
|
||
- git
|
||
# 要卸载的软件包列表
|
||
packages_remove: []
|
||
# 是否更新缓存
|
||
update_cache: yes
|
||
# 是否自动清理
|
||
autoremove: yes
|
||
# 安装推荐包
|
||
install_recommends: no
|
||
# 指定版本(可选,格式: "package=version")
|
||
# 例: packages_install: ["nginx=1.18.0-0ubuntu1"]
|
||
|
||
tasks:
|
||
- name: 确认Debian系统
|
||
assert:
|
||
that: ansible_os_family == "Debian"
|
||
fail_msg: "此模板仅适用于 Debian/Ubuntu 系统"
|
||
|
||
- name: 更新APT缓存
|
||
apt:
|
||
update_cache: yes
|
||
cache_valid_time: 3600
|
||
when: update_cache
|
||
|
||
- name: 安装软件包
|
||
apt:
|
||
name: "{{ packages_install }}"
|
||
state: present
|
||
install_recommends: "{{ install_recommends }}"
|
||
register: install_result
|
||
when: packages_install | length > 0
|
||
|
||
- name: 卸载软件包
|
||
apt:
|
||
name: "{{ packages_remove }}"
|
||
state: absent
|
||
purge: yes
|
||
when: packages_remove | length > 0
|
||
|
||
- name: 自动清理
|
||
apt:
|
||
autoremove: yes
|
||
autoclean: yes
|
||
when: autoremove
|
||
|
||
- name: 验证安装结果
|
||
shell: dpkg -l {{ packages_install | join(' ') }} 2>/dev/null | grep "^ii" | awk '{print $2, $3}'
|
||
register: verify_result
|
||
changed_when: false
|
||
when: packages_install | length > 0
|
||
|
||
- name: 显示安装报告
|
||
debug:
|
||
msg: |
|
||
═══════ APT安装报告 ═══════
|
||
已安装: {{ packages_install | length }} 个包
|
||
已卸载: {{ packages_remove | length }} 个包
|
||
变更: {{ install_result.changed | default(false) }}
|
||
{% if verify_result is defined and verify_result.stdout_lines is defined %}
|
||
已确认:
|
||
{% for line in verify_result.stdout_lines %}
|
||
✓ {{ line }}
|
||
{% endfor %}
|
||
{% endif %}
|
||
═══════════════════════════
|
||
`,
|
||
},
|
||
{
|
||
ID: "dpkg-install", Name: "DPKG软件安装", Category: "system",
|
||
Description: "通过dpkg安装本地.deb包,支持URL下载和依赖修复",
|
||
Icon: "📥", Tags: []string{"dpkg", "deb", "install", "offline"},
|
||
Content: `---
|
||
- name: DPKG软件安装
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
# 本地deb包路径列表(服务器上的路径)
|
||
local_deb_files: []
|
||
# 远程URL列表(会自动下载后安装)
|
||
remote_deb_urls: []
|
||
# 下载临时目录
|
||
download_dir: /tmp/dpkg_install
|
||
# 安装后是否修复依赖
|
||
fix_dependencies: yes
|
||
# 是否保留下载的deb文件
|
||
keep_deb_files: no
|
||
|
||
tasks:
|
||
- name: 确认Debian系统
|
||
assert:
|
||
that: ansible_os_family == "Debian"
|
||
fail_msg: "此模板仅适用于 Debian/Ubuntu 系统"
|
||
|
||
- name: 创建下载目录
|
||
file:
|
||
path: "{{ download_dir }}"
|
||
state: directory
|
||
mode: '0755'
|
||
when: remote_deb_urls | length > 0
|
||
|
||
- name: 下载远程deb包
|
||
get_url:
|
||
url: "{{ item }}"
|
||
dest: "{{ download_dir }}/{{ item | basename }}"
|
||
mode: '0644'
|
||
loop: "{{ remote_deb_urls }}"
|
||
register: download_result
|
||
when: remote_deb_urls | length > 0
|
||
|
||
- name: 安装本地deb包
|
||
apt:
|
||
deb: "{{ item }}"
|
||
state: present
|
||
loop: "{{ local_deb_files }}"
|
||
register: local_install
|
||
when: local_deb_files | length > 0
|
||
|
||
- name: 安装下载的deb包
|
||
apt:
|
||
deb: "{{ download_dir }}/{{ item | basename }}"
|
||
state: present
|
||
loop: "{{ remote_deb_urls }}"
|
||
register: remote_install
|
||
when: remote_deb_urls | length > 0
|
||
|
||
- name: 修复依赖关系
|
||
apt:
|
||
update_cache: yes
|
||
force_apt_get: yes
|
||
shell: apt-get install -f -y
|
||
when: fix_dependencies
|
||
ignore_errors: yes
|
||
|
||
- name: 清理下载文件
|
||
file:
|
||
path: "{{ download_dir }}"
|
||
state: absent
|
||
when: not keep_deb_files and remote_deb_urls | length > 0
|
||
|
||
- name: 显示安装报告
|
||
debug:
|
||
msg: |
|
||
═══════ DPKG安装报告 ═══════
|
||
本地包: {{ local_deb_files | length }} 个
|
||
远程包: {{ remote_deb_urls | length }} 个
|
||
{% if local_deb_files | length > 0 %}
|
||
本地安装:
|
||
{% for f in local_deb_files %}
|
||
✓ {{ f }}
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% if remote_deb_urls | length > 0 %}
|
||
远程安装:
|
||
{% for u in remote_deb_urls %}
|
||
✓ {{ u | basename }}
|
||
{% endfor %}
|
||
{% endif %}
|
||
依赖修复: {{ '是' if fix_dependencies else '否' }}
|
||
═══════════════════════════
|
||
`,
|
||
},
|
||
|
||
// ===== 集群部署 =====
|
||
{
|
||
ID: "k8s-cluster", Name: "Kubernetes集群部署", Category: "cluster",
|
||
Description: "使用kubeadm部署K8S集群(Master+Worker),含容器运行时、网络插件、Dashboard",
|
||
Icon: "☸️", Tags: []string{"kubernetes", "k8s", "kubeadm", "cluster"},
|
||
Content: `---
|
||
# ============================================================
|
||
# Kubernetes 集群部署 (kubeadm)
|
||
# 使用方法:
|
||
# 1. 在inventory中定义 k8s_master 和 k8s_worker 组
|
||
# 2. 修改下方 vars 中的版本和网段
|
||
# 3. 执行本Playbook
|
||
# ============================================================
|
||
- name: K8S集群 - 基础环境准备
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
k8s_version: "1.29"
|
||
pod_network_cidr: "10.244.0.0/16"
|
||
service_cidr: "10.96.0.0/12"
|
||
cni_plugin: calico # calico 或 flannel
|
||
|
||
tasks:
|
||
- name: 关闭swap
|
||
shell: |
|
||
swapoff -a
|
||
sed -i '/swap/s/^/#/' /etc/fstab
|
||
|
||
- name: 加载内核模块
|
||
modprobe:
|
||
name: "{{ item }}"
|
||
state: present
|
||
loop:
|
||
- overlay
|
||
- br_netfilter
|
||
|
||
- name: 持久化内核模块
|
||
copy:
|
||
content: |
|
||
overlay
|
||
br_netfilter
|
||
dest: /etc/modules-load.d/k8s.conf
|
||
|
||
- name: 设置内核参数
|
||
sysctl:
|
||
name: "{{ item.key }}"
|
||
value: "{{ item.value }}"
|
||
sysctl_set: yes
|
||
reload: yes
|
||
loop:
|
||
- { key: "net.bridge.bridge-nf-call-iptables", value: "1" }
|
||
- { key: "net.bridge.bridge-nf-call-ip6tables", value: "1" }
|
||
- { key: "net.ipv4.ip_forward", value: "1" }
|
||
|
||
- name: 关闭防火墙
|
||
service:
|
||
name: firewalld
|
||
state: stopped
|
||
enabled: no
|
||
ignore_errors: yes
|
||
|
||
- name: 关闭SELinux
|
||
shell: setenforce 0 && sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
|
||
ignore_errors: yes
|
||
|
||
- name: 时间同步检查
|
||
shell: timedatectl set-ntp true
|
||
ignore_errors: yes
|
||
|
||
- name: K8S集群 - 安装容器运行时和K8S组件
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
k8s_version: "1.29"
|
||
|
||
tasks:
|
||
- name: 安装containerd依赖
|
||
apt:
|
||
name:
|
||
- apt-transport-https
|
||
- ca-certificates
|
||
- curl
|
||
- gnupg
|
||
state: present
|
||
update_cache: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装containerd
|
||
shell: |
|
||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
|
||
apt-get update && apt-get install -y containerd.io
|
||
when: ansible_os_family == "Debian"
|
||
ignore_errors: yes
|
||
|
||
- name: 配置containerd
|
||
shell: |
|
||
mkdir -p /etc/containerd
|
||
containerd config default > /etc/containerd/config.toml
|
||
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
|
||
systemctl restart containerd && systemctl enable containerd
|
||
ignore_errors: yes
|
||
|
||
- name: 添加K8S APT源
|
||
shell: |
|
||
curl -fsSL https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
|
||
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
|
||
apt-get update
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装kubeadm/kubelet/kubectl
|
||
apt:
|
||
name:
|
||
- kubelet
|
||
- kubeadm
|
||
- kubectl
|
||
state: present
|
||
update_cache: yes
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 锁定K8S包版本
|
||
shell: apt-mark hold kubelet kubeadm kubectl
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 启动kubelet
|
||
service:
|
||
name: kubelet
|
||
state: started
|
||
enabled: yes
|
||
|
||
- name: K8S集群 - 初始化Master
|
||
hosts: k8s_master
|
||
become: yes
|
||
vars:
|
||
pod_network_cidr: "10.244.0.0/16"
|
||
service_cidr: "10.96.0.0/12"
|
||
cni_plugin: calico
|
||
|
||
tasks:
|
||
- name: 初始化Master节点
|
||
shell: |
|
||
kubeadm init \
|
||
--pod-network-cidr={{ pod_network_cidr }} \
|
||
--service-cidr={{ service_cidr }} \
|
||
--upload-certs \
|
||
--ignore-preflight-errors=NumCPU
|
||
register: kubeadm_init
|
||
ignore_errors: yes
|
||
|
||
- name: 配置kubectl
|
||
shell: |
|
||
mkdir -p $HOME/.kube
|
||
cp -f /etc/kubernetes/admin.conf $HOME/.kube/config
|
||
chown $(id -u):$(id -g) $HOME/.kube/config
|
||
ignore_errors: yes
|
||
|
||
- name: 部署Calico网络
|
||
shell: kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
|
||
when: cni_plugin == "calico"
|
||
ignore_errors: yes
|
||
|
||
- name: 部署Flannel网络
|
||
shell: kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
|
||
when: cni_plugin == "flannel"
|
||
ignore_errors: yes
|
||
|
||
- name: 获取Worker加入命令
|
||
shell: kubeadm token create --print-join-command
|
||
register: join_command
|
||
changed_when: false
|
||
|
||
- name: 显示加入命令
|
||
debug:
|
||
msg: |
|
||
═══════ K8S Master 初始化完成 ═══════
|
||
Worker加入命令:
|
||
{{ join_command.stdout }}
|
||
═══════════════════════════════════
|
||
|
||
- name: K8S集群 - Worker加入集群
|
||
hosts: k8s_worker
|
||
become: yes
|
||
vars:
|
||
master_ip: "{{ hostvars[groups['k8s_master'][0]]['ansible_host'] | default(groups['k8s_master'][0]) }}"
|
||
|
||
tasks:
|
||
- name: 获取加入命令
|
||
shell: ssh -o StrictHostKeyChecking=no {{ master_ip }} "kubeadm token create --print-join-command"
|
||
register: join_cmd
|
||
delegate_to: "{{ groups['k8s_master'][0] }}"
|
||
ignore_errors: yes
|
||
|
||
- name: 加入集群
|
||
shell: "{{ join_cmd.stdout }}"
|
||
when: join_cmd is succeeded
|
||
ignore_errors: yes
|
||
|
||
- name: 验证节点状态
|
||
shell: kubectl get nodes
|
||
delegate_to: "{{ groups['k8s_master'][0] }}"
|
||
register: nodes_status
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 显示集群状态
|
||
debug:
|
||
msg: "{{ nodes_status.stdout_lines | default(['等待Master就绪']) }}"
|
||
`,
|
||
},
|
||
{
|
||
ID: "ceph-cluster", Name: "Ceph存储集群部署", Category: "cluster",
|
||
Description: "使用cephadm部署Ceph集群(MON+OSD+MDS+RGW),含Dashboard",
|
||
Icon: "🐙", Tags: []string{"ceph", "storage", "cephadm", "cluster"},
|
||
Content: `---
|
||
# ============================================================
|
||
# Ceph 存储集群部署 (cephadm)
|
||
# 使用方法:
|
||
# 1. 在inventory中定义 ceph_mon, ceph_osd, ceph_mds 组
|
||
# 2. 修改下方 vars 中的网络和磁盘配置
|
||
# 3. 执行本Playbook
|
||
# ============================================================
|
||
- name: Ceph集群 - 基础环境准备
|
||
hosts: "{{ target_hosts | default('all') }}"
|
||
become: yes
|
||
vars:
|
||
ceph_release: reef # quincy 或 reef
|
||
cluster_network: "10.0.0.0/24"
|
||
public_network: "192.168.1.0/24"
|
||
|
||
tasks:
|
||
- name: 安装基础依赖
|
||
package:
|
||
name:
|
||
- python3
|
||
- lvm2
|
||
- chrony
|
||
- podman
|
||
state: present
|
||
|
||
- name: 配置时间同步
|
||
service:
|
||
name: chronyd
|
||
state: started
|
||
enabled: yes
|
||
ignore_errors: yes
|
||
|
||
- name: 关闭防火墙
|
||
service:
|
||
name: firewalld
|
||
state: stopped
|
||
enabled: no
|
||
ignore_errors: yes
|
||
|
||
- name: 设置主机名解析
|
||
lineinfile:
|
||
path: /etc/hosts
|
||
line: "{{ hostvars[item]['ansible_host'] | default(item) }} {{ item }}"
|
||
create: yes
|
||
loop: "{{ groups['all'] }}"
|
||
when: hostvars[item]['ansible_host'] is defined
|
||
|
||
- name: Ceph集群 - 安装cephadm
|
||
hosts: ceph_mon
|
||
become: yes
|
||
vars:
|
||
ceph_release: reef
|
||
|
||
tasks:
|
||
- name: 安装cephadm
|
||
shell: |
|
||
curl --silent --remote-name --location https://github.com/ceph/ceph/raw/{{ ceph_release }}/src/cephadm/cephadm
|
||
chmod +x cephadm
|
||
./cephadm add-repo --release {{ ceph_release }}
|
||
./cephadm install
|
||
args:
|
||
creates: /usr/sbin/cephadm
|
||
ignore_errors: yes
|
||
|
||
- name: 验证cephadm
|
||
shell: cephadm version
|
||
register: cephadm_ver
|
||
changed_when: false
|
||
|
||
- name: 显示版本
|
||
debug:
|
||
msg: "{{ cephadm_ver.stdout }}"
|
||
|
||
- name: Ceph集群 - 引导集群
|
||
hosts: ceph_mon[0]
|
||
become: yes
|
||
vars:
|
||
cluster_network: "10.0.0.0/24"
|
||
public_network: "192.168.1.0/24"
|
||
mon_ip: "{{ ansible_host | default(ansible_default_ipv4.address) }}"
|
||
|
||
tasks:
|
||
- name: 引导Ceph集群
|
||
shell: |
|
||
cephadm bootstrap \
|
||
--mon-ip {{ mon_ip }} \
|
||
--cluster-network {{ cluster_network }} \
|
||
--allow-overwrite
|
||
args:
|
||
creates: /etc/ceph/ceph.conf
|
||
register: bootstrap_result
|
||
ignore_errors: yes
|
||
|
||
- name: 配置Dashboard
|
||
shell: |
|
||
ceph dashboard set-rgw-api-ssl-verify False
|
||
ceph mgr services
|
||
ignore_errors: yes
|
||
|
||
- name: 获取Dashboard地址
|
||
shell: ceph mgr services | grep dashboard
|
||
register: dashboard_url
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 显示集群信息
|
||
debug:
|
||
msg: |
|
||
═══════ Ceph集群引导完成 ═══════
|
||
MON IP: {{ mon_ip }}
|
||
集群网络: {{ cluster_network }}
|
||
公共网络: {{ public_network }}
|
||
Dashboard: {{ dashboard_url.stdout | default('http://' + mon_ip + ':8443') }}
|
||
═══════════════════════════════════
|
||
|
||
- name: Ceph集群 - 添加节点
|
||
hosts: ceph_osd
|
||
become: yes
|
||
vars:
|
||
mon_host: "{{ groups['ceph_mon'][0] }}"
|
||
|
||
tasks:
|
||
- name: 获取SSH公钥
|
||
shell: cat /etc/ceph/ceph.pub
|
||
delegate_to: "{{ mon_host }}"
|
||
register: ceph_pub_key
|
||
changed_when: false
|
||
|
||
- name: 分发SSH公钥
|
||
authorized_key:
|
||
user: root
|
||
key: "{{ ceph_pub_key.stdout }}"
|
||
|
||
- name: 添加节点到集群
|
||
shell: ceph orch host add {{ inventory_hostname }} {{ ansible_host | default(inventory_hostname) }}
|
||
delegate_to: "{{ mon_host }}"
|
||
ignore_errors: yes
|
||
|
||
- name: Ceph集群 - 部署OSD
|
||
hosts: ceph_mon[0]
|
||
become: yes
|
||
vars:
|
||
# OSD磁盘配置(根据实际修改)
|
||
osd_devices:
|
||
- /dev/sdb
|
||
- /dev/sdc
|
||
osd_all_available: false # 设为true则自动使用所有可用磁盘
|
||
|
||
tasks:
|
||
- name: 部署指定磁盘OSD
|
||
shell: ceph orch daemon add osd {{ item.split('/')[2] }}:{{ item }}
|
||
loop: "{{ osd_devices }}"
|
||
when: not osd_all_available
|
||
ignore_errors: yes
|
||
|
||
- name: 部署所有可用磁盘OSD
|
||
shell: ceph orch apply osd --all-available-devices
|
||
when: osd_all_available
|
||
ignore_errors: yes
|
||
|
||
- name: 等待OSD就绪
|
||
shell: ceph osd stat
|
||
register: osd_stat
|
||
changed_when: false
|
||
retries: 10
|
||
delay: 10
|
||
until: osd_stat.rc == 0
|
||
ignore_errors: yes
|
||
|
||
- name: 显示OSD状态
|
||
debug:
|
||
msg: "{{ osd_stat.stdout | default('等待OSD初始化') }}"
|
||
|
||
- name: Ceph集群 - 部署MDS和RGW
|
||
hosts: ceph_mon[0]
|
||
become: yes
|
||
vars:
|
||
deploy_mds: true
|
||
deploy_rgw: true
|
||
mds_placement: "ceph_mds"
|
||
rgw_placement: "ceph_rgw"
|
||
|
||
tasks:
|
||
- name: 部署MDS(CephFS元数据服务)
|
||
shell: ceph orch apply mds cephfs --placement="{{ mds_placement }}"
|
||
when: deploy_mds
|
||
ignore_errors: yes
|
||
|
||
- name: 部署RGW(对象存储网关)
|
||
shell: ceph orch apply rgw ceph-rgw --placement="{{ rgw_placement }}" --port=8080
|
||
when: deploy_rgw
|
||
ignore_errors: yes
|
||
|
||
- name: 显示集群状态
|
||
shell: ceph -s
|
||
register: ceph_status
|
||
changed_when: false
|
||
|
||
- name: 最终报告
|
||
debug:
|
||
msg: "{{ ceph_status.stdout_lines }}"
|
||
`,
|
||
},
|
||
{
|
||
ID: "k8s-rook-ceph", Name: "K8S + Rook-Ceph存储", Category: "cluster",
|
||
Description: "在K8S集群上部署Rook-Ceph operator,提供持久化存储",
|
||
Icon: "💎", Tags: []string{"kubernetes", "rook", "ceph", "storage", "csi"},
|
||
Content: `---
|
||
# ============================================================
|
||
# Rook-Ceph on Kubernetes
|
||
# 前提: K8S集群已部署完成
|
||
# ============================================================
|
||
- name: Rook-Ceph - 部署Operator
|
||
hosts: k8s_master[0]
|
||
become: yes
|
||
vars:
|
||
rook_version: "v1.13.3"
|
||
|
||
tasks:
|
||
- name: 克隆Rook仓库
|
||
git:
|
||
repo: https://github.com/rook/rook.git
|
||
dest: /tmp/rook
|
||
version: "{{ rook_version }}"
|
||
depth: 1
|
||
ignore_errors: yes
|
||
|
||
- name: 部署CRD和Operator
|
||
shell: |
|
||
cd /tmp/rook/deploy/examples
|
||
kubectl create -f crds.yaml -f common.yaml -f operator.yaml
|
||
ignore_errors: yes
|
||
|
||
- name: 等待Operator就绪
|
||
shell: kubectl -n rook-ceph get pod -l app=rook-ceph-operator -o jsonpath='{.items[0].status.phase}'
|
||
register: operator_status
|
||
retries: 30
|
||
delay: 10
|
||
until: operator_status.stdout == "Running"
|
||
ignore_errors: yes
|
||
|
||
- name: 显示Operator状态
|
||
debug:
|
||
msg: "Rook Operator: {{ operator_status.stdout | default('等待中') }}"
|
||
|
||
- name: Rook-Ceph - 创建CephCluster
|
||
hosts: k8s_master[0]
|
||
become: yes
|
||
vars:
|
||
# 存储设备配置
|
||
use_all_devices: false
|
||
devices:
|
||
- name: sdb
|
||
- name: sdc
|
||
# 或使用目录(测试环境)
|
||
use_directories: false
|
||
data_dir_host_path: /var/lib/rook
|
||
|
||
tasks:
|
||
- name: 创建CephCluster
|
||
shell: |
|
||
cat <<'EOF' | kubectl apply -f -
|
||
apiVersion: ceph.rook.io/v1
|
||
kind: CephCluster
|
||
metadata:
|
||
name: rook-ceph
|
||
namespace: rook-ceph
|
||
spec:
|
||
cephVersion:
|
||
image: quay.io/ceph/ceph:v18.2
|
||
dataDirHostPath: {{ data_dir_host_path }}
|
||
mon:
|
||
count: 3
|
||
allowMultiplePerNode: false
|
||
mgr:
|
||
count: 1
|
||
dashboard:
|
||
enabled: true
|
||
ssl: false
|
||
storage:
|
||
useAllNodes: true
|
||
{% if use_all_devices %}
|
||
useAllDevices: true
|
||
{% elif devices | length > 0 %}
|
||
nodes:
|
||
- name: "*"
|
||
devices:
|
||
{% for d in devices %}
|
||
- name: "{{ d.name }}"
|
||
{% endfor %}
|
||
{% elif use_directories %}
|
||
directories:
|
||
- path: {{ data_dir_host_path }}
|
||
{% endif %}
|
||
EOF
|
||
ignore_errors: yes
|
||
|
||
- name: 等待CephCluster就绪
|
||
shell: kubectl -n rook-ceph get cephcluster -o jsonpath='{.items[0].status.phase}'
|
||
register: cluster_phase
|
||
retries: 60
|
||
delay: 15
|
||
until: cluster_phase.stdout == "Ready"
|
||
ignore_errors: yes
|
||
|
||
- name: 显示集群状态
|
||
shell: kubectl -n rook-ceph get cephcluster
|
||
register: cluster_status
|
||
changed_when: false
|
||
|
||
- name: 报告
|
||
debug:
|
||
msg: "{{ cluster_status.stdout_lines }}"
|
||
|
||
- name: Rook-Ceph - 创建StorageClass
|
||
hosts: k8s_master[0]
|
||
become: yes
|
||
vars:
|
||
create_rbd_pool: true
|
||
create_cephfs_pool: true
|
||
rbd_pool_name: replicapool
|
||
rbd_pool_replicas: 3
|
||
|
||
tasks:
|
||
- name: 创建RBD StorageClass
|
||
shell: |
|
||
cat <<'EOF' | kubectl apply -f -
|
||
apiVersion: ceph.rook.io/v1
|
||
kind: CephBlockPool
|
||
metadata:
|
||
name: {{ rbd_pool_name }}
|
||
namespace: rook-ceph
|
||
spec:
|
||
failureDomain: host
|
||
replicated:
|
||
size: {{ rbd_pool_replicas }}
|
||
---
|
||
apiVersion: storage.k8s.io/v1
|
||
kind: StorageClass
|
||
metadata:
|
||
name: rook-ceph-block
|
||
provisioner: rook-ceph.rbd.csi.ceph.com
|
||
parameters:
|
||
clusterID: rook-ceph
|
||
pool: {{ rbd_pool_name }}
|
||
imageFormat: "2"
|
||
imageFeatures: layering
|
||
csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
|
||
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
|
||
csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
|
||
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
|
||
reclaimPolicy: Delete
|
||
allowVolumeExpansion: true
|
||
EOF
|
||
when: create_rbd_pool
|
||
ignore_errors: yes
|
||
|
||
- name: 创建CephFS StorageClass
|
||
shell: |
|
||
cat <<'EOF' | kubectl apply -f -
|
||
apiVersion: ceph.rook.io/v1
|
||
kind: CephFilesystem
|
||
metadata:
|
||
name: ceph-filesystem
|
||
namespace: rook-ceph
|
||
spec:
|
||
metadataPool:
|
||
replicated:
|
||
size: 3
|
||
dataPools:
|
||
- name: data0
|
||
replicated:
|
||
size: 3
|
||
metadataServer:
|
||
activeCount: 1
|
||
activeStandby: true
|
||
---
|
||
apiVersion: storage.k8s.io/v1
|
||
kind: StorageClass
|
||
metadata:
|
||
name: rook-cephfs
|
||
provisioner: rook-ceph.cephfs.csi.ceph.com
|
||
parameters:
|
||
clusterID: rook-ceph
|
||
fsName: ceph-filesystem
|
||
pool: ceph-filesystem-data0
|
||
csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
|
||
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
|
||
csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
|
||
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
|
||
reclaimPolicy: Delete
|
||
EOF
|
||
when: create_cephfs_pool
|
||
ignore_errors: yes
|
||
|
||
- name: 设置默认StorageClass
|
||
shell: kubectl patch storageclass rook-ceph-block -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
|
||
ignore_errors: yes
|
||
|
||
- name: 显示StorageClass
|
||
shell: kubectl get sc
|
||
register: sc_list
|
||
changed_when: false
|
||
|
||
- name: 最终报告
|
||
debug:
|
||
msg: |
|
||
═══════ Rook-Ceph 部署完成 ═══════
|
||
{{ sc_list.stdout }}
|
||
═══════════════════════════════════
|
||
`,
|
||
},
|
||
{
|
||
ID: "etcd-cluster", Name: "ETCD集群部署", Category: "cluster",
|
||
Description: "部署3/5节点ETCD集群,支持TLS加密和自动发现",
|
||
Icon: "🔐", Tags: []string{"etcd", "cluster", "kv", "consensus"},
|
||
Content: `---
|
||
# ============================================================
|
||
# ETCD 集群部署
|
||
# 使用方法: 在inventory中定义 etcd 组(3或5个节点)
|
||
# ============================================================
|
||
- name: ETCD集群 - 安装部署
|
||
hosts: etcd
|
||
become: yes
|
||
vars:
|
||
etcd_version: "3.5.12"
|
||
etcd_data_dir: /var/lib/etcd
|
||
etcd_initial_cluster_token: "etcd-cluster-prod"
|
||
# 节点间通信端口
|
||
client_port: 2379
|
||
peer_port: 2380
|
||
# 是否启用TLS
|
||
enable_tls: false
|
||
|
||
tasks:
|
||
- name: 创建etcd用户
|
||
user:
|
||
name: etcd
|
||
system: yes
|
||
shell: /usr/sbin/nologin
|
||
create_home: no
|
||
|
||
- name: 下载ETCD
|
||
get_url:
|
||
url: "https://github.com/etcd-io/etcd/releases/download/v{{ etcd_version }}/etcd-v{{ etcd_version }}-linux-amd64.tar.gz"
|
||
dest: /tmp/etcd.tar.gz
|
||
|
||
- name: 解压安装
|
||
unarchive:
|
||
src: /tmp/etcd.tar.gz
|
||
dest: /tmp
|
||
remote_src: yes
|
||
|
||
- name: 安装二进制
|
||
copy:
|
||
src: "/tmp/etcd-v{{ etcd_version }}-linux-amd64/{{ item }}"
|
||
dest: "/usr/local/bin/{{ item }}"
|
||
mode: '0755'
|
||
remote_src: yes
|
||
loop: [etcd, etcdctl]
|
||
|
||
- name: 创建数据目录
|
||
file:
|
||
path: "{{ etcd_data_dir }}"
|
||
state: directory
|
||
owner: etcd
|
||
group: etcd
|
||
mode: '0700'
|
||
|
||
- name: 构建集群节点列表
|
||
set_fact:
|
||
etcd_initial_cluster: "{{ groups['etcd'] | map('extract', hostvars, 'inventory_hostname') | zip(groups['etcd'] | map('extract', hostvars, ['ansible_host'])) | map('join', '=http://') | map('regex_replace', '^(.*)$', '\\1:' + (peer_port | string)) | join(',') }}"
|
||
|
||
- name: 创建Systemd服务
|
||
copy:
|
||
content: |
|
||
[Unit]
|
||
Description=ETCD Key-Value Store
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=notify
|
||
User=etcd
|
||
ExecStart=/usr/local/bin/etcd \
|
||
--name {{ inventory_hostname }} \
|
||
--data-dir {{ etcd_data_dir }} \
|
||
--listen-client-urls http://0.0.0.0:{{ client_port }} \
|
||
--advertise-client-urls http://{{ ansible_host | default(inventory_hostname) }}:{{ client_port }} \
|
||
--listen-peer-urls http://0.0.0.0:{{ peer_port }} \
|
||
--initial-advertise-peer-urls http://{{ ansible_host | default(inventory_hostname) }}:{{ peer_port }} \
|
||
--initial-cluster {{ etcd_initial_cluster }} \
|
||
--initial-cluster-token {{ etcd_initial_cluster_token }} \
|
||
--initial-cluster-state new
|
||
Restart=always
|
||
RestartSec=5
|
||
LimitNOFILE=65536
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
dest: /etc/systemd/system/etcd.service
|
||
|
||
- name: 启动ETCD
|
||
systemd:
|
||
name: etcd
|
||
state: started
|
||
enabled: yes
|
||
daemon_reload: yes
|
||
|
||
- name: ETCD集群 - 验证
|
||
hosts: etcd[0]
|
||
become: yes
|
||
vars:
|
||
client_port: 2379
|
||
|
||
tasks:
|
||
- name: 检查集群健康
|
||
shell: etcdctl endpoint health --cluster
|
||
register: health
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 检查成员列表
|
||
shell: etcdctl member list -w table
|
||
register: members
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 显示集群状态
|
||
debug:
|
||
msg: |
|
||
═══════ ETCD集群状态 ═══════
|
||
健康检查: {{ health.stdout | default('检查中...') }}
|
||
成员列表:
|
||
{{ members.stdout | default('等待集群就绪') }}
|
||
═══════════════════════════
|
||
`,
|
||
},
|
||
{
|
||
ID: "harbor-cluster", Name: "Harbor镜像仓库部署", Category: "cluster",
|
||
Description: "部署Harbor企业级容器镜像仓库,含HTTPS和持久化存储",
|
||
Icon: "⚓", Tags: []string{"harbor", "registry", "docker", "images"},
|
||
Content: `---
|
||
# ============================================================
|
||
# Harbor 容器镜像仓库部署
|
||
# ============================================================
|
||
- name: Harbor - 安装部署
|
||
hosts: "{{ target_hosts | default('harbor') }}"
|
||
become: yes
|
||
vars:
|
||
harbor_version: "2.10.0"
|
||
harbor_hostname: "harbor.example.com"
|
||
harbor_admin_password: "Harbor@12345"
|
||
harbor_data_dir: /data/harbor
|
||
# 协议: http 或 https
|
||
harbor_protocol: https
|
||
# HTTPS证书(自签名或已有证书)
|
||
ssl_cert: ""
|
||
ssl_cert_key: ""
|
||
|
||
tasks:
|
||
- name: 安装Docker(如未安装)
|
||
shell: |
|
||
if ! command -v docker &>/dev/null; then
|
||
curl -fsSL https://get.docker.com | sh
|
||
systemctl enable docker && systemctl start docker
|
||
fi
|
||
ignore_errors: yes
|
||
|
||
- name: 安装Docker Compose
|
||
shell: |
|
||
if ! command -v docker-compose &>/dev/null; then
|
||
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||
chmod +x /usr/local/bin/docker-compose
|
||
fi
|
||
ignore_errors: yes
|
||
|
||
- name: 创建数据目录
|
||
file:
|
||
path: "{{ harbor_data_dir }}"
|
||
state: directory
|
||
mode: '0755'
|
||
|
||
- name: 下载Harbor离线安装包
|
||
get_url:
|
||
url: "https://github.com/goharbor/harbor/releases/download/v{{ harbor_version }}/harbor-offline-installer-v{{ harbor_version }}.tgz"
|
||
dest: /tmp/harbor.tgz
|
||
timeout: 300
|
||
|
||
- name: 解压安装包
|
||
unarchive:
|
||
src: /tmp/harbor.tgz
|
||
dest: "{{ harbor_data_dir }}"
|
||
remote_src: yes
|
||
|
||
- name: 生成自签名证书
|
||
shell: |
|
||
mkdir -p /etc/harbor/ssl
|
||
openssl req -newkey rsa:4096 -nodes -sha256 \
|
||
-keyout /etc/harbor/ssl/harbor.key \
|
||
-x509 -days 3650 \
|
||
-subj "/CN={{ harbor_hostname }}" \
|
||
-addext "subjectAltName=DNS:{{ harbor_hostname }}" \
|
||
-out /etc/harbor/ssl/harbor.crt
|
||
when: harbor_protocol == "https" and ssl_cert == ""
|
||
args:
|
||
creates: /etc/harbor/ssl/harbor.crt
|
||
|
||
- name: 配置Harbor
|
||
shell: |
|
||
cd {{ harbor_data_dir }}/harbor
|
||
cp harbor.yml.tmpl harbor.yml
|
||
sed -i "s/^hostname: .*/hostname: {{ harbor_hostname }}/" harbor.yml
|
||
sed -i "s/^harbor_admin_password: .*/harbor_admin_password: {{ harbor_admin_password }}/" harbor.yml
|
||
sed -i "s|^data_volume: .*|data_volume: {{ harbor_data_dir }}/data|" harbor.yml
|
||
{% if harbor_protocol == "https" %}
|
||
sed -i "s|^ certificate: .*| certificate: {{ ssl_cert | default('/etc/harbor/ssl/harbor.crt') }}|" harbor.yml
|
||
sed -i "s|^ private_key: .*| private_key: {{ ssl_cert_key | default('/etc/harbor/ssl/harbor.key') }}|" harbor.yml
|
||
{% else %}
|
||
sed -i '/^https:/,/^[a-z]/{ /^https:/d; /^ port:/d; /^ certificate:/d; /^ private_key:/d }' harbor.yml
|
||
{% endif %}
|
||
|
||
- name: 运行Harbor安装脚本
|
||
shell: |
|
||
cd {{ harbor_data_dir }}/harbor
|
||
./install.sh --with-trivy
|
||
register: harbor_install
|
||
ignore_errors: yes
|
||
|
||
- name: 等待Harbor启动
|
||
wait_for:
|
||
port: "{{ 443 if harbor_protocol == 'https' else 80 }}"
|
||
timeout: 120
|
||
ignore_errors: yes
|
||
|
||
- name: 显示部署信息
|
||
debug:
|
||
msg: |
|
||
═══════ Harbor 部署完成 ═══════
|
||
地址: {{ harbor_protocol }}://{{ harbor_hostname }}
|
||
用户: admin
|
||
密码: {{ harbor_admin_password }}
|
||
数据目录: {{ harbor_data_dir }}
|
||
═══════════════════════════════════
|
||
`,
|
||
},
|
||
{
|
||
ID: "nfs-cluster", Name: "NFS共享存储集群", Category: "cluster",
|
||
Description: "部署NFS服务端+客户端,配置共享存储和自动挂载",
|
||
Icon: "📂", Tags: []string{"nfs", "storage", "share", "mount"},
|
||
Content: `---
|
||
# ============================================================
|
||
# NFS 共享存储部署
|
||
# 使用方法:
|
||
# 1. 在inventory中定义 nfs_server 和 nfs_client 组
|
||
# 2. 修改共享目录和客户端挂载配置
|
||
# ============================================================
|
||
- name: NFS - 服务端部署
|
||
hosts: nfs_server
|
||
become: yes
|
||
vars:
|
||
# 共享目录配置
|
||
nfs_exports:
|
||
- path: /data/nfs/share
|
||
clients: "192.168.1.0/24"
|
||
options: "rw,sync,no_root_squash,no_subtree_check"
|
||
- path: /data/nfs/backup
|
||
clients: "192.168.1.0/24"
|
||
options: "rw,sync,no_root_squash"
|
||
# NFS版本
|
||
nfs_versions: "4.2,4.1,4,3"
|
||
|
||
tasks:
|
||
- name: 安装NFS服务端
|
||
package:
|
||
name: nfs-kernel-server
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装NFS服务端(RedHat)
|
||
package:
|
||
name: nfs-utils
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 创建共享目录
|
||
file:
|
||
path: "{{ item.path }}"
|
||
state: directory
|
||
mode: '0777'
|
||
loop: "{{ nfs_exports }}"
|
||
|
||
- name: 配置exports
|
||
lineinfile:
|
||
path: /etc/exports
|
||
line: "{{ item.path }} {{ item.clients }}({{ item.options }})"
|
||
create: yes
|
||
loop: "{{ nfs_exports }}"
|
||
register: exports_config
|
||
|
||
- name: 重启NFS服务
|
||
service:
|
||
name: nfs-kernel-server
|
||
state: restarted
|
||
when: ansible_os_family == "Debian" and exports_config is changed
|
||
|
||
- name: 重启NFS服务(RedHat)
|
||
service:
|
||
name: nfs-server
|
||
state: restarted
|
||
when: ansible_os_family == "RedHat" and exports_config is changed
|
||
|
||
- name: 导出共享
|
||
shell: exportfs -ra
|
||
changed_when: false
|
||
|
||
- name: 验证导出
|
||
shell: exportfs -v
|
||
register: export_list
|
||
changed_when: false
|
||
|
||
- name: 显示共享信息
|
||
debug:
|
||
msg: "{{ export_list.stdout_lines }}"
|
||
|
||
- name: NFS - 客户端挂载
|
||
hosts: nfs_client
|
||
become: yes
|
||
vars:
|
||
nfs_server_ip: "{{ hostvars[groups['nfs_server'][0]]['ansible_host'] | default(groups['nfs_server'][0]) }}"
|
||
# 客户端挂载配置
|
||
nfs_mounts:
|
||
- server_path: "/data/nfs/share"
|
||
local_path: "/mnt/nfs/share"
|
||
options: "rw,soft,timeo=30"
|
||
- server_path: "/data/nfs/backup"
|
||
local_path: "/mnt/nfs/backup"
|
||
options: "rw,soft,timeo=30"
|
||
|
||
tasks:
|
||
- name: 安装NFS客户端
|
||
package:
|
||
name: nfs-common
|
||
state: present
|
||
when: ansible_os_family == "Debian"
|
||
|
||
- name: 安装NFS客户端(RedHat)
|
||
package:
|
||
name: nfs-utils
|
||
state: present
|
||
when: ansible_os_family == "RedHat"
|
||
|
||
- name: 创建挂载点
|
||
file:
|
||
path: "{{ item.local_path }}"
|
||
state: directory
|
||
mode: '0755'
|
||
loop: "{{ nfs_mounts }}"
|
||
|
||
- name: 挂载NFS共享
|
||
mount:
|
||
path: "{{ item.local_path }}"
|
||
src: "{{ nfs_server_ip }}:{{ item.server_path }}"
|
||
fstype: nfs
|
||
opts: "{{ item.options }}"
|
||
state: mounted
|
||
loop: "{{ nfs_mounts }}"
|
||
|
||
- name: 验证挂载
|
||
shell: df -h | grep nfs
|
||
register: mount_check
|
||
changed_when: false
|
||
ignore_errors: yes
|
||
|
||
- name: 显示挂载状态
|
||
debug:
|
||
msg: "{{ mount_check.stdout_lines | default(['等待挂载']) }}"
|
||
`,
|
||
},
|
||
}
|
||
}
|