aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-19 16:21:53 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-19 16:21:53 +0100
commit98311fc2ea91cc2a5f00e9fa85a30f50fde77e79 (patch)
tree58ff0ffad8cbe0230eff1791360bc7cc45501c34
parentf4c9ed4c0a32082d8f7b60cee1eb33cb05c85a1c (diff)
parentd87a489f9585d10f0a185beb59ae16a10f27a7bd (diff)
downloadPuppet-98311fc2ea91cc2a5f00e9fa85a30f50fde77e79.tar.gz
Puppet-98311fc2ea91cc2a5f00e9fa85a30f50fde77e79.tar.zst
Puppet-98311fc2ea91cc2a5f00e9fa85a30f50fde77e79.zip
Merge branch 'backup' into dev
-rw-r--r--.gitmodules3
-rw-r--r--environments/global/common.yaml8
-rw-r--r--environments/global/roles/backup.yaml11
-rw-r--r--environments/hiera.yaml1
-rw-r--r--modules/base_installation/files/cronie/puppet-post-merge4
-rw-r--r--modules/base_installation/files/scripts/puppet_apply23
-rw-r--r--modules/base_installation/files/scripts/puppet_reset_and_apply2
-rw-r--r--modules/base_installation/manifests/cronie.pp4
-rw-r--r--modules/base_installation/manifests/puppet.pp6
-rw-r--r--modules/profile/manifests/known_hosts.pp11
-rw-r--r--modules/profile/manifests/mail.pp14
-rw-r--r--modules/profile/templates/mail/ssmtp.conf.erb14
-rw-r--r--modules/role/manifests/backup.pp122
-rw-r--r--modules/role/templates/backup/backup_dirname_head.sh.erb27
-rw-r--r--modules/role/templates/backup/backup_dirname_part.sh.erb26
-rw-r--r--modules/role/templates/backup/backup_dirname_tail.sh.erb4
-rw-r--r--modules/role/templates/backup/backup_head.sh.erb20
-rw-r--r--modules/role/templates/backup/backup_immae_eu.sh.erb79
-rw-r--r--modules/role/templates/backup/backup_tail.sh.erb0
-rw-r--r--modules/role/templates/backup/ssh_host_changed.info.erb4
-rw-r--r--modules/role/templates/backup/ssh_key_changed.info.erb5
m---------modules/ssh_keygen0
22 files changed, 383 insertions, 5 deletions
diff --git a/.gitmodules b/.gitmodules
index fa5163a..735ca8c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -46,6 +46,9 @@
46[submodule "python/ovh"] 46[submodule "python/ovh"]
47 path = python/ovh 47 path = python/ovh
48 url = git://git.immae.eu/github/ovh/python-ovh 48 url = git://git.immae.eu/github/ovh/python-ovh
49[submodule "modules/ssh_keygen"]
50 path = modules/ssh_keygen
51 url = git://git.immae.eu/github/voxpupuli/puppet-ssh_keygen
49[submodule "modules/ssl"] 52[submodule "modules/ssl"]
50 path = modules/ssl 53 path = modules/ssl
51 url = git://git.immae.eu/github/fnerdwq/puppet-ssl 54 url = git://git.immae.eu/github/fnerdwq/puppet-ssl
diff --git a/environments/global/common.yaml b/environments/global/common.yaml
index 5911194..4836f6e 100644
--- a/environments/global/common.yaml
+++ b/environments/global/common.yaml
@@ -8,6 +8,10 @@ lookup_options:
8 merge: unique 8 merge: unique
9 letsencrypt::hosts: 9 letsencrypt::hosts:
10 merge: unique 10 merge: unique
11 role::backup::backups:
12 merge: unique
13 profile::known_hosts::hosts:
14 merge: unique
11 15
12classes: 16classes:
13 stdlib: ~ 17 stdlib: ~
@@ -30,5 +34,9 @@ base_installation::system_timezone: "Europe/Paris"
30base_installation::system_users: [] # Fetched via ldap 34base_installation::system_users: [] # Fetched via ldap
31profile::xmr_stak::mining_pool: "" # Fetched via ldap 35profile::xmr_stak::mining_pool: "" # Fetched via ldap
32profile::xmr_stak::wallet: "" # Fetched via ldap 36profile::xmr_stak::wallet: "" # Fetched via ldap
37profile::mail::mailhub: "" # Fetched via ldap
38role::backup::mailto: "" # Fetched via ldap
39role::backup::backups: [] # Fetched via ldap
40profile::known_hosts::hosts: [] # Fetched via ldap
33letsencrypt::email: ~ # Fetched via ldap 41letsencrypt::email: ~ # Fetched via ldap
34letsencrypt::try_for_real_hostname: true 42letsencrypt::try_for_real_hostname: true
diff --git a/environments/global/roles/backup.yaml b/environments/global/roles/backup.yaml
new file mode 100644
index 0000000..52befe2
--- /dev/null
+++ b/environments/global/roles/backup.yaml
@@ -0,0 +1,11 @@
1---
2classes:
3 role::backup: ~
4role::backup::user: "backup"
5role::backup::group: "backup"
6base_installation::system_users:
7 - username: "%{lookup('role::backup::user')}"
8 userid: 976
9 system: true
10 password: "!!"
11
diff --git a/environments/hiera.yaml b/environments/hiera.yaml
index eda5eb3..61d40d8 100644
--- a/environments/hiera.yaml
+++ b/environments/hiera.yaml
@@ -8,6 +8,7 @@ defaults:
8hierarchy: 8hierarchy:
9 - name: "Initialization variables" 9 - name: "Initialization variables"
10 path: "/root/puppet_variables.json" 10 path: "/root/puppet_variables.json"
11 data_hash: json_data
11 12
12 - name: "Puppet ldap variables" 13 - name: "Puppet ldap variables"
13 data_hash: ldap_data 14 data_hash: ldap_data
diff --git a/modules/base_installation/files/cronie/puppet-post-merge b/modules/base_installation/files/cronie/puppet-post-merge
index ac5e3ff..f5c21a7 100644
--- a/modules/base_installation/files/cronie/puppet-post-merge
+++ b/modules/base_installation/files/cronie/puppet-post-merge
@@ -1,7 +1,7 @@
1#!/bin/bash 1#!/bin/bash
2## Run Puppet locally using puppet apply 2## Run Puppet locally using puppet_apply
3git submodule update --init 3git submodule update --init
4/usr/bin/puppet apply `pwd`/manifests/site.pp 4/usr/local/sbin/puppet_apply `pwd`/manifests/site.pp
5 5
6## Log status of the Puppet run 6## Log status of the Puppet run
7if [ $? -eq 0 ] 7if [ $? -eq 0 ]
diff --git a/modules/base_installation/files/scripts/puppet_apply b/modules/base_installation/files/scripts/puppet_apply
new file mode 100644
index 0000000..69673cc
--- /dev/null
+++ b/modules/base_installation/files/scripts/puppet_apply
@@ -0,0 +1,23 @@
1#!/bin/bash
2
3lockfile=/var/run/puppet-apply.lock
4path=`dirname $0`
5path=`cd $path/..; pwd`
6
7if [ $(id -u) -gt 0 ]; then
8 echo "You must be root to run this script." >&2
9 exit 2
10fi
11
12if (set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then
13 trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
14
15 puppet apply "$@"
16
17 rm -f "$lockfile"
18 trap - INT TERM EXIT
19else
20 echo "Failed to acquire lockfile: $lockfile." >&2
21 echo "Held by $(cat $lockfile 2>/dev/null)" >&2
22 exit 1
23fi
diff --git a/modules/base_installation/files/scripts/puppet_reset_and_apply b/modules/base_installation/files/scripts/puppet_reset_and_apply
index 6743044..0350e6e 100644
--- a/modules/base_installation/files/scripts/puppet_reset_and_apply
+++ b/modules/base_installation/files/scripts/puppet_reset_and_apply
@@ -11,4 +11,4 @@ fi
11git reset --hard origin/$branch 11git reset --hard origin/$branch
12 12
13git submodule update --init 13git submodule update --init
14puppet apply --test manifests/site.pp 14puppet_apply --test manifests/site.pp
diff --git a/modules/base_installation/manifests/cronie.pp b/modules/base_installation/manifests/cronie.pp
index e8d5dfd..e8f3e20 100644
--- a/modules/base_installation/manifests/cronie.pp
+++ b/modules/base_installation/manifests/cronie.pp
@@ -19,13 +19,13 @@ class base_installation::cronie inherits base_installation {
19 } 19 }
20 cron { 'puppet-apply': 20 cron { 'puppet-apply':
21 ensure => present, 21 ensure => present,
22 command => "cd $base_installation::puppet_code_path ; puppet apply $base_installation::puppet_code_path/manifests/site.pp", 22 command => "cd $base_installation::puppet_code_path ; puppet_apply $base_installation::puppet_code_path/manifests/site.pp",
23 user => root, 23 user => root,
24 minute => '*/20' 24 minute => '*/20'
25 } 25 }
26 cron { 'puppet-apply-reboot': 26 cron { 'puppet-apply-reboot':
27 ensure => present, 27 ensure => present,
28 command => "cd $base_installation::puppet_code_path ; puppet apply $base_installation::puppet_code_path/manifests/site.pp", 28 command => "cd $base_installation::puppet_code_path ; puppet_apply $base_installation::puppet_code_path/manifests/site.pp",
29 user => root, 29 user => root,
30 special => "reboot" 30 special => "reboot"
31 } 31 }
diff --git a/modules/base_installation/manifests/puppet.pp b/modules/base_installation/manifests/puppet.pp
index 6f7732d..0cb43bc 100644
--- a/modules/base_installation/manifests/puppet.pp
+++ b/modules/base_installation/manifests/puppet.pp
@@ -39,6 +39,12 @@ class base_installation::puppet (
39 source => "puppet:///modules/base_installation/scripts/report_print.rb" 39 source => "puppet:///modules/base_installation/scripts/report_print.rb"
40 } 40 }
41 41
42 file { '/usr/local/sbin/puppet_apply':
43 mode => "0755",
44 ensure => present,
45 source => "puppet:///modules/base_installation/scripts/puppet_apply",
46 }
47
42 unless empty(find_file($password_seed)) { 48 unless empty(find_file($password_seed)) {
43 $ldap_password = generate_password(24, $password_seed, "ldap") 49 $ldap_password = generate_password(24, $password_seed, "ldap")
44 $ssha_ldap_seed = generate_password(5, $password_seed, "ldap_seed") 50 $ssha_ldap_seed = generate_password(5, $password_seed, "ldap_seed")
diff --git a/modules/profile/manifests/known_hosts.pp b/modules/profile/manifests/known_hosts.pp
new file mode 100644
index 0000000..ed9ec8e
--- /dev/null
+++ b/modules/profile/manifests/known_hosts.pp
@@ -0,0 +1,11 @@
1class profile::known_hosts (
2 Optional[Array] $hosts = []
3) {
4 $hosts.each |$host| {
5 sshkey { $host["name"]:
6 ensure => "present",
7 key => $host["key"],
8 type => $host["type"],
9 }
10 }
11}
diff --git a/modules/profile/manifests/mail.pp b/modules/profile/manifests/mail.pp
new file mode 100644
index 0000000..cc47b77
--- /dev/null
+++ b/modules/profile/manifests/mail.pp
@@ -0,0 +1,14 @@
1class profile::mail (
2 String $mailhub,
3 Optional[Integer] $mailhub_port = 25,
4) {
5 ensure_packages(["s-nail", "ssmtp"])
6
7 $hostname = lookup("base_installation::real_hostname")
8
9 file { "/etc/ssmtp/ssmtp.conf":
10 ensure => "present",
11 content => template("profile/mail/ssmtp.conf.erb"),
12 }
13}
14
diff --git a/modules/profile/templates/mail/ssmtp.conf.erb b/modules/profile/templates/mail/ssmtp.conf.erb
new file mode 100644
index 0000000..e7a0410
--- /dev/null
+++ b/modules/profile/templates/mail/ssmtp.conf.erb
@@ -0,0 +1,14 @@
1#
2# /etc/ssmtp.conf -- a config file for sSMTP sendmail.
3#
4# The person who gets all mail for userids < 1000
5# Make this empty to disable rewriting.
6root=postmaster
7# The place where the mail goes. The actual machine name is required
8# no MX records are consulted. Commonly mailhosts are named mail.domain.com
9# The example will fit if you are in domain.com and you mailhub is so named.
10mailhub=<%= @mailhub %>:<%= @mailhub_port %>
11# Where will the mail seem to come from?
12#rewriteDomain=y
13# The full hostname
14hostname=<%= @hostname %>
diff --git a/modules/role/manifests/backup.pp b/modules/role/manifests/backup.pp
new file mode 100644
index 0000000..edfd5e0
--- /dev/null
+++ b/modules/role/manifests/backup.pp
@@ -0,0 +1,122 @@
1class role::backup (
2 String $user,
3 String $group,
4 String $mailto,
5 Optional[Array] $backups = [],
6 Optional[String] $mountpoint = "/backup1",
7 Optional[String] $backup_script = "/usr/local/bin/backup.sh",
8) {
9 include "base_installation"
10
11 include "profile::mail"
12 include "profile::tools"
13 include "profile::xmr_stak"
14 include "profile::known_hosts"
15
16 ssh_keygen { $user:
17 notify => Notify_refresh["notify-backup-sshkey-change"]
18 }
19
20 $hosts = $backups.map |$backup| { $backup["host"] }
21
22 notify_refresh { "notify-backup-sshkey-change":
23 message => template("role/backup/ssh_key_changed.info.erb"),
24 refreshonly => true
25 }
26
27 $hosts.each |$host| {
28 notify_refresh { "notify-backup-sshhost-$host-changed":
29 message => template("role/backup/ssh_host_changed.info.erb"),
30 refreshonly => true,
31 subscribe => Sshkey[$host],
32 }
33 }
34
35 concat { $backup_script:
36 ensure => "present",
37 ensure_newline => true,
38 mode => "0755",
39 }
40
41 cron { "backup":
42 ensure => present,
43 command => $backup_script,
44 user => $user,
45 minute => 25,
46 hour => 3,
47 require => Concat[$backup_script],
48 }
49
50 concat::fragment { "backup_head":
51 target => $backup_script,
52 content => template("role/backup/backup_head.sh.erb"),
53 order => "01-50",
54 }
55
56 concat::fragment { "backup_tail":
57 target => $backup_script,
58 content => template("role/backup/backup_tail.sh.erb"),
59 order => "99-50",
60 }
61
62 $backups.each |$infos| {
63 $dirname = $infos["name"]
64 $login = $infos["login"]
65 $host = $infos["host"]
66 $dest = "$login@$host"
67 $base = "$mountpoint/$dirname"
68 $nbr = $infos["nbr"]
69 $order_dirname = $infos["order"]
70
71 file { $base:
72 ensure => "directory",
73 owner => $user,
74 group => $group,
75 require => Mount[$mountpoint],
76 } ->
77 file { "$base/older":
78 ensure => "directory",
79 owner => $user,
80 group => $group,
81 } ->
82 file { "$base/rsync_output":
83 ensure => "directory",
84 owner => $user,
85 group => $group,
86 }
87
88 concat::fragment { "backup_${dirname}_head":
89 target => $backup_script,
90 content => template("role/backup/backup_dirname_head.sh.erb"),
91 order => "$order_dirname-01",
92 }
93
94 concat::fragment { "backup_${dirname}_tail":
95 target => $backup_script,
96 content => template("role/backup/backup_dirname_tail.sh.erb"),
97 order => "$order_dirname-99",
98 }
99
100 $infos["parts"].each |$part| {
101 $local_folder = $part["local_folder"]
102 $remote_folder = $part["remote_folder"]
103 $exclude_from = $part["exclude_from"]
104 $files_from = $part["files_from"]
105 $args = $part["args"]
106 $order_part = $part["order"]
107
108 file { "$base/$local_folder":
109 ensure => "directory",
110 owner => $user,
111 group => $group,
112 require => File[$base],
113 }
114
115 concat::fragment { "backup_${dirname}_${local_folder}":
116 target => $backup_script,
117 content => template("role/backup/backup_dirname_part.sh.erb"),
118 order => "$order_dirname-$order_part",
119 }
120 }
121 }
122}
diff --git a/modules/role/templates/backup/backup_dirname_head.sh.erb b/modules/role/templates/backup/backup_dirname_head.sh.erb
new file mode 100644
index 0000000..e20cfd3
--- /dev/null
+++ b/modules/role/templates/backup/backup_dirname_head.sh.erb
@@ -0,0 +1,27 @@
1##### <%= @dirname %> #####
2DEST="<%= @dest %>"
3BASE="<%= @base %>"
4OLD_BAK_BASE=$BASE/older/j
5BAK_BASE=${OLD_BAK_BASE}0
6RSYNC_OUTPUT=$BASE/rsync_output
7NBR=<%= @nbr %>
8
9if ! ssh \
10 -o PreferredAuthentications=publickey \
11 -o StrictHostKeyChecking=yes \
12 -o ClearAllForwardings=yes \
13 $DEST backup; then
14 echo "Fichier de verrouillage backup sur $DEST ou impossible de se connecter" >&2
15 skip=$DEST
16fi
17
18rm -rf ${OLD_BAK_BASE}${NBR}
19for j in `seq -w $(($NBR-1)) -1 0`; do
20 [ ! -d ${OLD_BAK_BASE}$j ] && continue
21 mv ${OLD_BAK_BASE}$j ${OLD_BAK_BASE}$(($j+1))
22done
23mkdir $BAK_BASE
24mv $RSYNC_OUTPUT $BAK_BASE
25mkdir $RSYNC_OUTPUT
26
27if [ "$skip" != "$DEST" ]; then
diff --git a/modules/role/templates/backup/backup_dirname_part.sh.erb b/modules/role/templates/backup/backup_dirname_part.sh.erb
new file mode 100644
index 0000000..ec662c4
--- /dev/null
+++ b/modules/role/templates/backup/backup_dirname_part.sh.erb
@@ -0,0 +1,26 @@
1### <%= @dirname %> <%= @local_folder %> ###
2LOCAL="<%= @local_folder %>"
3REMOTE="<%= @remote_folder %>"
4
5cd $BASE/$LOCAL
6cat > $EXCL_FROM <<EOF
7<%= @exclude_from.join("\n") %>
8EOF
9cat > $FILES_FROM <<EOF
10<%= @files_from.join("\n") %>
11EOF
12
13OUT=$RSYNC_OUTPUT/$LOCAL
14rsync -XAavbrz --fake-super -e ssh --numeric-ids --delete \
15 --backup-dir=$BAK_BASE/$LOCAL \
16<%- unless @args.empty? -%>
17 <%= @args %>\
18<% end -%>
19<%- unless @exclude_from.empty? -%>
20 --exclude-from=$EXCL_FROM \
21<% end -%>
22<%- unless @files_from.empty? -%>
23 --files-from=$FILES_FROM \
24<% end -%>
25 $DEST:$REMOTE . > $OUT || true
26### End <%= @dirname %> <%= @local_folder %> ###
diff --git a/modules/role/templates/backup/backup_dirname_tail.sh.erb b/modules/role/templates/backup/backup_dirname_tail.sh.erb
new file mode 100644
index 0000000..6b16c9d
--- /dev/null
+++ b/modules/role/templates/backup/backup_dirname_tail.sh.erb
@@ -0,0 +1,4 @@
1
2 ssh $DEST sh -c "date > .last_backup"
3fi # [ "$skip" != "$DEST" ]
4##### End <%= @dirname %> #####
diff --git a/modules/role/templates/backup/backup_head.sh.erb b/modules/role/templates/backup/backup_head.sh.erb
new file mode 100644
index 0000000..be9f5bf
--- /dev/null
+++ b/modules/role/templates/backup/backup_head.sh.erb
@@ -0,0 +1,20 @@
1#!/bin/bash
2MAILTO="<%= @mailto %>"
3
4EXCL_FROM=`mktemp`
5FILES_FROM=`mktemp`
6TMP_STDERR=`mktemp`
7
8on_exit() {
9 if [ -s "$TMP_STDERR" ]; then
10 cat "$TMP_STDERR" | mail -Ssendwait -s "save_distant rsync error" "$MAILTO"
11 fi
12 rm -f $TMP_STDERR $EXCL_FROM $FILES_FROM
13}
14
15trap "on_exit" EXIT
16
17exec 2> "$TMP_STDERR"
18exec < /dev/null
19
20set -e
diff --git a/modules/role/templates/backup/backup_immae_eu.sh.erb b/modules/role/templates/backup/backup_immae_eu.sh.erb
new file mode 100644
index 0000000..4fab30e
--- /dev/null
+++ b/modules/role/templates/backup/backup_immae_eu.sh.erb
@@ -0,0 +1,79 @@
1#!/bin/bash
2DEST="<%= @dest %>"
3MAILTO="<%= @mailto %>"
4BASE="<%= @base %>"
5OLD_BAK_BASE=$BASE/older/j
6BAK_BASE=${OLD_BAK_BASE}0
7RSYNC_OUTPUT=$BASE/rsync_output
8NBR=7
9
10TMP=`mktemp`
11TMP_STDERR=`mktemp`
12
13trap "rm -f $TMP $TMP_STDERR" EXIT
14
15exec 2> "$TMP_STDERR"
16
17set -e
18if ! `ssh -o ClearAllForwardings=yes $DEST backup`; then
19 echo "Fichier de verrouillage backup sur $DEST"
20 exit 1
21fi
22
23rm -rf ${OLD_BAK_BASE}${NBR}
24for j in `seq -w $(($NBR-1)) -1 0`; do
25 [ ! -d ${OLD_BAK_BASE}$j ] && continue
26 mv ${OLD_BAK_BASE}$j ${OLD_BAK_BASE}$(($j+1))
27done
28mkdir $BAK_BASE
29mv $RSYNC_OUTPUT $BAK_BASE
30mkdir $RSYNC_OUTPUT
31
32##############
33NAME="home"
34FOLDER="/home/immae"
35
36cd $BASE/$NAME
37cat > $TMP <<EOF
38/.no_backup/
39/hosts/florian/nobackup/
40/hosts/connexionswing.com/
41/hosts/connexionswing.immae.eu/
42/hosts/ludivine.immae.eu/
43/hosts/ludivinecassal.com/
44/hosts/piedsjaloux.fr/
45/hosts/piedsjaloux.immae.eu/
46/hosts/spip/sites/*/
47/hosts/spip/spip*
48EOF
49OUT=$RSYNC_OUTPUT/$NAME
50rsync -XAavbrz --fake-super -e ssh --numeric-ids --delete \
51 --backup-dir=$BAK_BASE/$NAME --exclude-from=$TMP \
52 $DEST:$FOLDER . > $OUT || true
53
54##############
55NAME="system"
56FOLDER="/"
57
58cd $BASE/$NAME
59cat > $TMP <<EOF
60/etc/
61/srv/
62/var/lib/
63/var/spool/
64/var/named/
65/usr/local/
66EOF
67OUT=$RSYNC_OUTPUT/$NAME
68rsync -XAavbrz -R --fake-super -e ssh --numeric-ids --delete \
69 --rsync-path='sudo rsync' \
70 --backup-dir=$BAK_BASE/$NAME \
71 --files-from=$TMP \
72 $DEST:$FOLDER . > $OUT || true
73
74##############
75ssh $DEST sh -c "date > .last_backup"
76
77if [ -s "$TMP_STDERR" ]; then
78 cat "$TMP_STDERR" | mail -Ssendwait -s "save_distant rsync error" "$MAILTO"
79fi
diff --git a/modules/role/templates/backup/backup_tail.sh.erb b/modules/role/templates/backup/backup_tail.sh.erb
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/role/templates/backup/backup_tail.sh.erb
diff --git a/modules/role/templates/backup/ssh_host_changed.info.erb b/modules/role/templates/backup/ssh_host_changed.info.erb
new file mode 100644
index 0000000..ebf202e
--- /dev/null
+++ b/modules/role/templates/backup/ssh_host_changed.info.erb
@@ -0,0 +1,4 @@
1Host <%= @host %> added, please send <%= @user %> key if necessary.
2<%- if File.exist?("/home/#{@user}/.ssh/id_rsa.pub") %>
3 <%= File.read("/home/#{@user}/.ssh/id_rsa.pub") %>
4<% end -%>
diff --git a/modules/role/templates/backup/ssh_key_changed.info.erb b/modules/role/templates/backup/ssh_key_changed.info.erb
new file mode 100644
index 0000000..43fd2ec
--- /dev/null
+++ b/modules/role/templates/backup/ssh_key_changed.info.erb
@@ -0,0 +1,5 @@
1ssh key of <%= @user %> changed,
2please update hosts:
3<%- @hosts.each do |host| %>
4 - <%= host %>
5<% end -%>
diff --git a/modules/ssh_keygen b/modules/ssh_keygen
new file mode 160000
Subproject ca53363249b58af96f90cb810c7c51dda8ba803