From f87ded62cfe10296eaf58a2d2b214e2decf9414d Mon Sep 17 00:00:00 2001 From: Paul Czarkowski Date: Tue, 20 Jun 2017 03:32:14 -0500 Subject: [PATCH] =?UTF-8?q?(=E2=95=AF=C2=B0=E2=96=A1=C2=B0=EF=BC=89?= =?UTF-8?q?=E2=95=AF=EF=B8=B5=20=E2=94=BB=E2=94=81=E2=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 12 + CONTRIBUTORS.md | 34 + LICENSE.md | 13 + README.md | 127 ++ Vagrantfile | 85 ++ ansible.cfg | 27 + docs/architecture.md | 302 ++++ docs/bastion_user_admin.md | 142 ++ docs/contract_elk_cluster.md | 31 + docs/deploy_sc.md | 192 +++ docs/disk_replacement.md | 132 ++ docs/elk_shard_recovery.md | 34 + docs/flapjack.md | 118 ++ docs/images/1.png | Bin 0 -> 19354 bytes docs/images/2.png | Bin 0 -> 31590 bytes docs/images/3.png | Bin 0 -> 25786 bytes docs/images/4.png | Bin 0 -> 35035 bytes docs/images/5.png | Bin 0 -> 79904 bytes docs/images/grafana_sla_dashboard.png | Bin 0 -> 574801 bytes docs/logging.dot | 54 + docs/logging.png | Bin 0 -> 188325 bytes docs/monitoring.dot | 98 ++ docs/monitoring.png | Bin 0 -> 269935 bytes docs/post_deploy_validation.md | 48 + docs/sitecontroller_backup_strategies.md | 37 + envs/example/bastion/bastion-users.yml | 74 + envs/example/bastion/group_vars/all.yml | 377 +++++ envs/example/bastion/hosts | 6 + envs/example/bastion/vagrant.yml | 17 + .../centralcontroller/group_vars/all.yml | 98 ++ envs/example/centralcontroller/hosts | 14 + envs/example/centralcontroller/vagrant.yml | 23 + envs/example/ci/group_vars/all.yml | 93 ++ envs/example/ci/group_vars/cd-masters.yml | 24 + envs/example/ci/group_vars/imagebuilder.yml | 10 + .../example/ci/group_vars/jenkins-masters.yml | 5 + envs/example/ci/heat_stack.yml | 90 ++ envs/example/ci/hosts | 18 + envs/example/ci/vagrant.yml | 23 + envs/example/ci/vars_heat.yml | 14 + envs/example/consul/group_vars/all | 4 + envs/example/consul/hosts | 4 + envs/example/consul/vagrant.yml | 11 + envs/example/defaults.yml | 324 +++++ envs/example/elk/group_vars/all.yml | 97 ++ envs/example/elk/heat_stack.yml | 117 ++ envs/example/elk/hosts | 20 + envs/example/elk/vagrant.yml | 18 + envs/example/elk/vars_heat.yml | 22 + envs/example/ipmi-proxy/group_vars/all.yml | 59 + envs/example/ipmi-proxy/hosts | 5 + envs/example/ipmi-proxy/vagrant.yml | 9 + envs/example/ipsec/README.md | 122 ++ envs/example/ipsec/group_vars/all | 1 + .../example/ipsec/host_vars/ipsec-client.yaml | 38 + envs/example/ipsec/host_vars/ipsec-server.yml | 36 + envs/example/ipsec/hosts | 3 + envs/example/ipsec/vagrant.yml | 17 + envs/example/jenkins/group_vars/all | 9 + envs/example/jenkins/hosts | 2 + envs/example/jenkins/vagrant.yml | 9 + envs/example/minibootstrapper/README.md | 9 + envs/example/minibootstrapper/group_vars/all | 142 ++ envs/example/minibootstrapper/hosts | 5 + envs/example/minibootstrapper/vagrant.yml | 9 + envs/example/mirror/group_vars/all.yml | 138 ++ envs/example/mirror/heat_stack.yml | 81 ++ envs/example/mirror/hosts | 2 + envs/example/mirror/vagrant.yml | 9 + envs/example/mirror/vars_heat.yml | 127 ++ envs/example/monitor/group_vars/all.yml | 169 +++ envs/example/monitor/hosts | 23 + envs/example/monitor/vagrant.yml | 9 + envs/example/netdata/group_vars/all.yml | 19 + envs/example/netdata/heat_stack.yml | 81 ++ envs/example/netdata/hosts | 2 + envs/example/netdata/vagrant.yml | 9 + envs/example/netdata/vars_heat.yml | 127 ++ envs/example/netman/group_vars/all | 1 + envs/example/netman/hosts | 11 + envs/example/netman/vagrant.yml | 9 + .../example/sitecontroller/group_vars/all.yml | 218 +++ .../sitecontroller/group_vars/bastion.yml | 326 +++++ .../sitecontroller/group_vars/bootstrap.yml | 211 +++ .../example/sitecontroller/group_vars/elk.yml | 120 ++ .../sitecontroller/group_vars/monitor.yml | 70 + envs/example/sitecontroller/heat_stack.yml | 133 ++ .../sitecontroller/host_vars/bastion01.yml | 73 + envs/example/sitecontroller/hosts | 86 ++ envs/example/sitecontroller/vagrant.yml | 26 + envs/example/sitecontroller/vars_heat.yml | 14 + envs/example/swiftbrowser/group_vars/all.yml | 19 + envs/example/swiftbrowser/hosts | 2 + envs/example/swiftbrowser/vagrant.yml | 10 + envs/example/vagrant.yml | 34 + hosts | 11 + library/apache2_site.py | 89 ++ library/binary.py | 91 ++ library/jenkins.py | 210 +++ library/logrotate.py | 183 +++ library/rabbitmq_user.py | 301 ++++ library/sensu_check.py | 110 ++ library/sensu_check_dict.py | 80 + library/sensu_metrics_check.py | 96 ++ library/sensu_process_check.py | 96 ++ library/systemd_service.py | 191 +++ library/upstart_service.py | 142 ++ library/xml_configuration.py | 90 ++ playbooks/add-bastion-users.yml | 26 + playbooks/apt-mirror-debmirror-conversion.yml | 32 + playbooks/central-cutover.yml | 64 + playbooks/delete-bastion-users.yml | 54 + playbooks/elk-cluster-upgrade.yml | 182 +++ playbooks/elk-curator.yml | 41 + playbooks/elk-rolling-restart.yml | 139 ++ playbooks/elk-stats.yml | 48 + playbooks/full-remote-restart.yml | 50 + playbooks/healthcheck.yml | 22 + playbooks/pxe-config.yml | 21 + playbooks/remove-grafana-datasources.yml | 33 + playbooks/sensu-server-force-restart.yml | 24 + playbooks/sensu-server-health.yml | 33 + playbooks/stats-to-spreadsheet.py | 114 ++ playbooks/templates/authorized_keys | 5 + playbooks/templates/delete_indices.yml | 17 + playbooks/ucs-bmc-verify.py | 194 +++ playbooks/ucs-sanity.yml | 48 + playbooks/update-apt-mirror.yml | 38 + playbooks/update-yum-mirror.yml | 34 + playbooks/upgrades/elasticsearch.yml | 27 + playbooks/validation.yml | 37 + playbooks/vyatta-add-ssh-keys.sh | 34 + playbooks/vyatta-set-authorized-keys.yml | 19 + playbooks/whisper-resize.yml | 9 + plugins/callbacks/timestamp.py | 105 ++ plugins/filters/filters.py | 40 + plugins/filters/jenkins_filters.py | 54 + plugins/vars/default_vars.py | 67 + requirements-es-stats.txt | 5 + requirements.txt | 7 + roles/_blank/defaults/main.yml | 1 + roles/_blank/handlers/main.yml | 1 + roles/_blank/meta/main.yml | 1 + roles/_blank/tasks/checks.yml | 0 roles/_blank/tasks/main.yml | 14 + roles/_blank/tasks/metrics.yml | 0 roles/_blank/tasks/serverspec.yml | 0 roles/_sitecontroller/defaults/main.yml | 10 + roles/_sitecontroller/tasks/checks.yml | 0 roles/_sitecontroller/tasks/main.yml | 54 + roles/_sitecontroller/tasks/metrics.yml | 0 roles/_sitecontroller/tasks/serverspec.yml | 0 .../templates/etc/apt/sources.list | 44 + .../_sitecontroller/templates/python/pip.conf | 5 + .../templates/python/pydistutils.cfg | 4 + roles/_sitecontroller/templates/ruby/gemrc | 12 + roles/apache/defaults/main.yml | 26 + roles/apache/handlers/main.yml | 9 + roles/apache/meta/main.yml | 8 + roles/apache/tasks/checks.yml | 4 + roles/apache/tasks/main.yml | 77 + roles/apache/tasks/metrics.yml | 0 roles/apache/tasks/serverspec.yml | 6 + roles/apache/templates/etc/apache2/ports.conf | 5 + .../templates/serverspec/apache_spec.rb | 31 + roles/apt-mirror/defaults/main.yml | 41 + roles/apt-mirror/meta/main.yml | 17 + roles/apt-mirror/tasks/checks.yml | 1 + roles/apt-mirror/tasks/debmirror.yml | 79 + roles/apt-mirror/tasks/main.yml | 67 + roles/apt-mirror/tasks/metrics.yml | 0 roles/apt-mirror/tasks/serverspec.yml | 6 + roles/apt-mirror/templates/debmirror.sh | 25 + .../apt-mirror/templates/etc/apache2/htaccess | 6 + .../etc/apache2/sites-available/apt_mirror | 77 + .../apt-mirror/templates/etc/cron.d/debmirror | 5 + .../templates/serverspec/apt_mirror_spec.rb | 35 + roles/apt-repos/defaults/main.yml | 5 + roles/apt-repos/tasks/checks.yml | 0 roles/apt-repos/tasks/main.yml | 42 + roles/apt-repos/tasks/metrics.yml | 0 roles/apt-repos/tasks/serverspec.yml | 0 roles/bastion/defaults/main.yml | 30 + roles/bastion/files/utils/ssh-ip-check | 382 +++++ roles/bastion/handlers/main.yml | 5 + roles/bastion/meta/main.yml | 3 + roles/bastion/tasks/checks.yml | 6 + roles/bastion/tasks/main.yml | 24 + roles/bastion/tasks/metrics.yml | 0 roles/bastion/tasks/serverspec.yml | 14 + roles/bastion/tasks/utils.yml | 19 + roles/bastion/tasks/yubiauthd.yml | 60 + .../bastion/templates/etc/init/yubiauthd.conf | 15 + roles/bastion/templates/etc/ssh-ip-check.conf | 6 + roles/bastion/templates/etc/yubiauthd.conf | 22 + .../templates/serverspec/yama-utils_spec.rb | 13 + .../templates/serverspec/yubiauthd_spec.rb | 44 + roles/bbg-ssl/defaults/main.yml | 6 + roles/bbg-ssl/handlers/main.yml | 3 + roles/bbg-ssl/tasks/checks.yml | 0 roles/bbg-ssl/tasks/main.yml | 49 + roles/bbg-ssl/tasks/metrics.yml | 0 roles/bbg-ssl/tasks/serverspec.yml | 0 .../templates/etc/ssl/certs/intermediate.crt | 3 + .../etc/ssl/certs/sitecontroller.crt | 3 + .../etc/ssl/private/sitecontroller.key | 3 + .../local/share/ca-certificates/ca_cert.crt | 3 + roles/collectd/defaults/main.yml | 21 + roles/collectd/handlers/main.yml | 3 + roles/collectd/meta/main.yml | 3 + roles/collectd/tasks/checks.yml | 4 + roles/collectd/tasks/main.yml | 44 + roles/collectd/tasks/metrics.yml | 0 roles/collectd/tasks/serverspec.yml | 6 + .../templates/etc/collectd/collectd.conf | 18 + .../templates/etc/collectd/plugins/amqp.conf | 18 + .../etc/collectd/plugins/logfile.conf | 9 + .../etc/collectd/plugins/system.conf | 49 + .../templates/serverspec/collectd_spec.rb | 11 + roles/common/defaults/main.yml | 167 +++ roles/common/handlers/main.yml | 21 + roles/common/meta/main.yml | 25 + roles/common/tasks/checks.yml | 58 + roles/common/tasks/main.yml | 96 ++ roles/common/tasks/metrics.yml | 30 + roles/common/tasks/ntpd.yml | 36 + roles/common/tasks/serverspec.yml | 6 + roles/common/tasks/shell_customization.yml | 31 + roles/common/tasks/ssh.yml | 54 + roles/common/tasks/sudoers.yml | 32 + roles/common/tasks/ufw.yml | 22 + .../common/templates/admin_user/bash_profile | 3 + roles/common/templates/admin_user/bashrc | 89 ++ roles/common/templates/admin_user/gitconfig | 5 + roles/common/templates/admin_user/tmux.conf | 1 + .../templates/bin/ghe_authorized_keys.py | 30 + roles/common/templates/etc/hosts | 19 + roles/common/templates/etc/ntp.conf | 48 + .../common/templates/etc/profile.d/prompt.sh | 21 + .../templates/etc/profile.d/tmux_fix_ssh.sh | 10 + .../common/templates/etc/ssh/ssh_host_rsa_key | 3 + .../templates/etc/ssh/ssh_host_rsa_key.pub | 3 + roles/common/templates/etc/ssh/sshd_config | 126 ++ roles/common/templates/etc/sudoers | 35 + .../common/templates/etc/sudoers.d/admin_user | 3 + .../templates/etc/sudoers.d/blueboxcloud | 7 + roles/common/templates/etc/timezone | 1 + .../templates/serverspec/common_spec.rb | 288 ++++ roles/common/templates/ssh-private-key | 3 + roles/consul/defaults/main.yml | 29 + roles/consul/meta/main.yml | 3 + roles/consul/tasks/checks.yml | 4 + roles/consul/tasks/main.yml | 79 + roles/consul/tasks/metrics.yml | 0 roles/consul/tasks/serverspec.yml | 6 + roles/consul/templates/etc/consul.json | 36 + .../templates/serverspec/consul-server_rb.yml | 11 + roles/dnsmasq/defaults/main.yml | 19 + roles/dnsmasq/handlers/main.yml | 3 + roles/dnsmasq/meta/main.yml | 3 + roles/dnsmasq/tasks/checks.yml | 4 + roles/dnsmasq/tasks/main.yml | 59 + roles/dnsmasq/tasks/metrics.yml | 0 roles/dnsmasq/tasks/serverspec.yml | 6 + roles/dnsmasq/templates/etc/default/dnsmasq | 34 + .../templates/etc/dnsmasq.d/server.conf | 22 + roles/dnsmasq/templates/etc/hosts.dnsmasq | 5 + roles/dnsmasq/templates/etc/resolv.conf | 3 + .../templates/serverspec/dnsmasq_spec.rb | 11 + roles/docker/defaults/main.yml | 3 + roles/docker/meta/main.yml | 6 + roles/docker/tasks/checks.yml | 4 + roles/docker/tasks/main.yml | 25 + roles/docker/tasks/metrics.yml | 0 roles/docker/tasks/serverspec.yml | 0 roles/elasticsearch/defaults/main.yml | 75 + roles/elasticsearch/handlers/main.yml | 17 + roles/elasticsearch/meta/main.yml | 13 + roles/elasticsearch/tasks/checks.yml | 31 + roles/elasticsearch/tasks/curator.yml | 35 + roles/elasticsearch/tasks/main.yml | 142 ++ roles/elasticsearch/tasks/metrics.yml | 54 + roles/elasticsearch/tasks/serverspec.yml | 6 + .../templates/etc/cron.daily/elasticsearch | 5 + .../templates/etc/default/elasticsearch | 35 + .../templates/etc/elasticsearch/action.yml | 27 + .../templates/etc/elasticsearch/curator.yml | 20 + .../etc/elasticsearch/elasticsearch.yml | 10 + .../templates/etc/init.d/elasticsearch | 224 +++ .../templates/etc/logrotate.d/elasticsearch | 2 + .../scripts/elk-stats-collection.py | 181 +++ .../opt/sitecontroller/scripts/elk-stats.py | 257 ++++ .../sensu-plugins/elk-stats-metrics.rb | 54 + .../serverspec/elasticsearch_spec.rb | 59 + roles/file-mirror/defaults/main.yml | 61 + roles/file-mirror/meta/main.yml | 17 + roles/file-mirror/tasks/apache.yml | 23 + roles/file-mirror/tasks/checks.yml | 1 + roles/file-mirror/tasks/main.yml | 99 ++ roles/file-mirror/tasks/metrics.yml | 0 roles/file-mirror/tasks/serverspec.yml | 6 + .../templates/etc/apache2/htaccess | 6 + .../etc/apache2/sites-available/file_mirror | 48 + .../templates/serverspec/file_mirror_spec.rb | 42 + roles/flapjack/defaults/main.yml | 69 + roles/flapjack/handlers/main.yml | 12 + roles/flapjack/meta/main.yml | 7 + roles/flapjack/tasks/checks.yml | 9 + roles/flapjack/tasks/email.yml | 6 + roles/flapjack/tasks/main.yml | 87 ++ roles/flapjack/tasks/metrics.yml | 0 roles/flapjack/tasks/serverspec.yml | 6 + .../etc/flapjack/flapjack_config.yaml | 207 +++ .../etc/flapjack/templates/pagerduty.erb | 14 + .../templates/serverspec/flapjack_spec.rb | 39 + roles/gem-mirror/defaults/main.yml | 58 + roles/gem-mirror/handlers/main.yml | 6 + roles/gem-mirror/meta/main.yml | 20 + roles/gem-mirror/tasks/apache.yml | 23 + roles/gem-mirror/tasks/checks.yml | 4 + roles/gem-mirror/tasks/main.yml | 80 + roles/gem-mirror/tasks/metrics.yml | 0 roles/gem-mirror/tasks/serverspec.yml | 6 + .../etc/apache2/sites-available/gem_mirror | 48 + .../templates/opt/gem_mirror/config.ru | 11 + .../templates/opt/gem_mirror/unicorn.rb | 11 + .../templates/serverspec/gem_mirror_spec.rb | 59 + roles/git-repos/defaults/main.yml | 4 + roles/gpg/defaults/main.yml | 4 + roles/gpg/files/blueboxcloud.asc | 59 + roles/gpg/files/blueboxcloud.key | 30 + roles/gpg/tasks/checks.yml | 0 roles/gpg/tasks/main.yml | 35 + roles/gpg/tasks/metrics.yml | 0 roles/gpg/tasks/serverspec.yml | 0 roles/grafana/defaults/main.yml | 46 + roles/grafana/handlers/main.yml | 3 + roles/grafana/meta/main.yml | 12 + roles/grafana/tasks/checks.yml | 4 + roles/grafana/tasks/datasources.yml | 43 + roles/grafana/tasks/main.yml | 81 ++ roles/grafana/tasks/metrics.yml | 0 roles/grafana/tasks/serverspec.yml | 6 + .../templates/dashboards/bbc-basic-host.json | 882 +++++++++++ .../templates/dashboards/bbc-ceph-usage.json | 699 +++++++++ .../dashboards/bbc-standard-sla.json | 389 +++++ .../dashboards/cleversafe-standard-sla.json | 368 +++++ .../templates/dashboards/elk-stats.json | 550 +++++++ roles/grafana/templates/datasources.json | 7 + .../templates/etc/default/grafana-server | 19 + .../grafana/templates/etc/grafana/grafana.ini | 36 + .../templates/etc/init.d/grafana-server | 149 ++ .../templates/serverspec/grafana_spec.rb | 39 + roles/graphite/defaults/main.yml | 96 ++ roles/graphite/handlers/main.yml | 10 + roles/graphite/meta/main.yml | 20 + roles/graphite/tasks/checks.yml | 4 + roles/graphite/tasks/main.yml | 171 +++ roles/graphite/tasks/metrics.yml | 0 roles/graphite/tasks/serverspec.yml | 6 + .../etc/apache2/sites-available/graphite | 64 + .../templates/etc/init/carbon-cache.conf | 23 + .../templates/opt/graphite/conf/carbon.conf | 364 +++++ .../templates/opt/graphite/conf/graphite.wsgi | 25 + .../opt/graphite/conf/storage-schemas.conf | 24 + .../webapp/graphite/local_settings.py | 245 ++++ .../opt/graphite/webapp/graphite/settings.py | 259 ++++ .../templates/serverspec/graphite_spec.rb | 89 ++ roles/harden/tasks/checks.yml | 0 roles/harden/tasks/main.yml | 18 + roles/harden/tasks/metrics.yml | 0 roles/harden/tasks/serverspec.yml | 0 roles/imagebuilder/defaults/main.yml | 15 + .../files/usr/local/bin/image-refresh.sh | 36 + roles/imagebuilder/meta/main.yml | 3 + roles/imagebuilder/tasks/checks.yml | 0 roles/imagebuilder/tasks/main.yml | 43 + roles/imagebuilder/tasks/metrics.yml | 0 roles/imagebuilder/tasks/serverspec.yml | 0 .../templates/etc/cron.d/dib-image-refresh | 1 + .../imagebuilder/templates/etc/sudoers.d/dib | 3 + roles/ipmi-proxy/README.md | 10 + roles/ipmi-proxy/defaults/main.yml | 52 + roles/ipmi-proxy/meta/main.yml | 19 + roles/ipmi-proxy/tasks/checks.yml | 1 + roles/ipmi-proxy/tasks/main.yml | 64 + roles/ipmi-proxy/tasks/metrics.yml | 0 roles/ipmi-proxy/tasks/serverspec.yml | 6 + .../apache2/sites-available/ipmi-proxy.conf | 25 + .../templates/etc/bluebox/ipmi-proxy.conf | 35 + .../templates/etc/sudoers.d/www-data | 4 + .../templates/serverspec/ipmi-proxy_spec.rb | 62 + roles/ipsec/defaults/main.yml | 11 + roles/ipsec/handlers/main.yml | 6 + roles/ipsec/meta/main.yml | 3 + roles/ipsec/tasks/checks.yml | 4 + roles/ipsec/tasks/main.yml | 74 + roles/ipsec/tasks/serverspec.yml | 6 + roles/ipsec/tasks/strongswan.yml | 24 + roles/ipsec/templates/etc/ipsec.conf | 11 + .../templates/etc/ipsec.d/connections.conf | 10 + .../templates/etc/ipsec.d/ipsec-notify.sh | 14 + roles/ipsec/templates/etc/ipsec.secrets | 4 + .../etc/network/interfaces.d/vti0.cfg | 2 + .../ipsec/templates/serverspec/ipsec_spec.rb | 68 + roles/jenkins-common/defaults/main.yml | 237 +++ roles/jenkins-common/handlers/main.yml | 17 + roles/jenkins-common/meta/main.yml | 3 + roles/jenkins-common/tasks/checks.yml | 0 roles/jenkins-common/tasks/main.yml | 70 + roles/jenkins-common/tasks/metrics.yml | 0 roles/jenkins-common/tasks/serverspec.yml | 0 roles/jenkins-master/files/rules/failure.prop | 1 + .../files/usr/local/bin/load-jenkins-jobs.sh | 19 + roles/jenkins-master/handlers/main.yml | 14 + roles/jenkins-master/meta/main.yml | 14 + roles/jenkins-master/tasks/apache.yml | 53 + roles/jenkins-master/tasks/checks.yml | 5 + roles/jenkins-master/tasks/jjb.yml | 33 + roles/jenkins-master/tasks/main.yml | 92 ++ roles/jenkins-master/tasks/metrics.yml | 0 roles/jenkins-master/tasks/serverspec.yml | 6 + .../etc/apache2/sites-available/jenkins | 95 ++ .../templates/etc/default/jenkins | 70 + .../etc/jenkins_jobs/jenkins_jobs.ini | 9 + .../templates/serverspec/jenkins_spec.rb | 104 ++ roles/jenkins-master/templates/ssh-key | 3 + ...nkins.plugins.multijob.PhaseJobsConfig.xml | 11 + .../templates/var/lib/jenkins/config.xml | 61 + .../templates/var/lib/jenkins/credentials.xml | 72 + .../var/lib/jenkins/gerrit-trigger.xml | 77 + .../jenkins/github-plugin-configuration.xml | 12 + ...ins.model.JenkinsLocationConfiguration.xml | 5 + .../jenkins.plugins.slack.SlackNotifier.xml | 7 + roles/jenkins-slave/defaults/main.yml | 7 + roles/jenkins-slave/meta/main.yml | 8 + roles/jenkins-slave/tasks/checks.yml | 0 roles/jenkins-slave/tasks/main.yml | 60 + roles/jenkins-slave/tasks/metrics.yml | 0 roles/jenkins-slave/tasks/serverspec.yml | 0 roles/jenkins-slave/templates/gitconfig | 5 + .../templates/home/jenkins/jenkins.stackrc | 1 + .../templates/private_vars/rhel/default.yml | 37 + .../templates/private_vars/ubuntu/default.yml | 4 + .../templates/var/lib/jenkins/packagecloud | 1 + roles/kibana/defaults/main.yml | 29 + roles/kibana/handlers/main.yml | 3 + roles/kibana/meta/main.yml | 12 + roles/kibana/tasks/checks.yml | 4 + roles/kibana/tasks/config.yml | 37 + roles/kibana/tasks/main.yml | 97 ++ roles/kibana/tasks/metrics.yml | 0 roles/kibana/tasks/serverspec.yml | 6 + roles/kibana/templates/etc/init/kibana.conf | 25 + .../templates/opt/kibana/config/config.json | 3 + .../opt/kibana/config/index-pattern.json | 9 + .../templates/opt/kibana/config/kibana.yml | 9 + .../templates/serverspec/kibana_spec.rb | 28 + roles/logging-config/defaults/main.yml | 28 + roles/logging-config/handlers/main.yml | 10 + roles/logging-config/meta/main.yml | 1 + roles/logging-config/tasks/checks.yml | 0 roles/logging-config/tasks/filebeat.yml | 18 + roles/logging-config/tasks/main.yml | 18 + roles/logging-config/tasks/metrics.yml | 0 roles/logging-config/tasks/serverspec.yml | 0 .../etc/filebeat/filebeat.d/template.yml | 5 + roles/logging/defaults/main.yml | 21 + .../files/etc/init/logstash-forwarder.conf | 15 + .../files/etc/rsyslog.d/49-haproxy.conf | 7 + roles/logging/handlers/main.yml | 21 + roles/logging/meta/main.yml | 15 + roles/logging/tasks/checks.yml | 6 + roles/logging/tasks/filebeat.yml | 30 + roles/logging/tasks/main.yml | 22 + roles/logging/tasks/metrics.yml | 0 roles/logging/tasks/serverspec.yml | 6 + .../templates/etc/default/logstash-forwarder | 8 + .../templates/etc/filebeat/filebeat.yml | 18 + .../etc/init/logstash-forwarder.conf | 21 + .../etc/logstash-forwarder.d/main.conf | 31 + .../templates/etc/rsyslog.d/50-default.conf | 44 + .../templates/serverspec/logging_spec.rb | 11 + roles/logstash/README.md | 13 + roles/logstash/defaults/main.yml | 122 ++ roles/logstash/handlers/main.yml | 3 + roles/logstash/meta/main.yml | 20 + roles/logstash/tasks/checks.yml | 4 + roles/logstash/tasks/main.yml | 73 + roles/logstash/tasks/metrics.yml | 0 roles/logstash/tasks/serverspec.yml | 6 + roles/logstash/templates/etc/default/logstash | 37 + .../filter-add-missing-customer_id.conf | 7 + .../logstash/conf.d/filter-drop-empty.conf | 5 + .../etc/logstash/conf.d/filter-json.conf | 7 + .../conf.d/filter-offset-integer.conf | 5 + .../etc/logstash/conf.d/filter-openstack.conf | 86 ++ .../etc/logstash/conf.d/filter-syslog.conf | 27 + .../etc/logstash/conf.d/filter-tags.conf | 9 + .../etc/logstash/conf.d/pipeline.conf | 51 + .../templates/etc/logstash/patterns/openstack | 3 + .../templates/serverspec/logstash_spec.rb | 90 ++ roles/manage-disks/defaults/main.yml | 27 + roles/manage-disks/handlers/main.yml | 9 + roles/manage-disks/meta/main.yml | 1 + roles/manage-disks/tasks/checks.yml | 0 roles/manage-disks/tasks/main.yml | 159 ++ roles/manage-disks/tasks/metrics.yml | 0 roles/manage-disks/tasks/serverspec.yml | 0 .../templates/mount-loopback.conf | 9 + roles/manage-disks/templates/passphrase | 1 + roles/netdata-dashboard/defaults/main.yml | 35 + roles/netdata-dashboard/meta/main.yml | 16 + roles/netdata-dashboard/tasks/apache.yml | 24 + roles/netdata-dashboard/tasks/checks.yml | 1 + roles/netdata-dashboard/tasks/main.yml | 52 + roles/netdata-dashboard/tasks/metrics.yml | 0 roles/netdata-dashboard/tasks/serverspec.yml | 6 + .../templates/etc/apache2/htaccess | 6 + .../apache2/sites-available/netdata_dashboard | 59 + .../templates/serverspec/file_mirror_spec.rb | 7 + .../templates/var/www/html/health_check | 1 + .../templates/var/www/html/index.html | 195 +++ roles/netdata/defaults/main.yml | 6 + roles/netdata/handlers/main.yml | 1 + roles/netdata/meta/main.yml | 1 + roles/netdata/tasks/checks.yml | 0 roles/netdata/tasks/main.yml | 34 + roles/netdata/tasks/metrics.yml | 0 roles/netdata/tasks/serverspec.yml | 0 roles/oauth2_proxy/README.md | 53 + roles/oauth2_proxy/defaults/main.yml | 76 + roles/oauth2_proxy/handlers/main.yml | 3 + roles/oauth2_proxy/meta/main.yml | 17 + roles/oauth2_proxy/tasks/apache.yml | 48 + roles/oauth2_proxy/tasks/checks.yml | 1 + roles/oauth2_proxy/tasks/main.yml | 27 + roles/oauth2_proxy/tasks/metrics.yml | 0 roles/oauth2_proxy/tasks/serverspec.yml | 6 + .../apache2/sites-available/oauth2_proxy.conf | 75 + .../etc/oauth2_proxy/oauth2_proxy.cfg.j2 | 74 + .../templates/serverspec/oauth2-proxy_spec.rb | 51 + .../templates/var/www/html/index.html | 172 +++ roles/openid_proxy/defaults/main.yml | 92 ++ roles/openid_proxy/handlers/main.yml | 1 + roles/openid_proxy/meta/main.yml | 17 + roles/openid_proxy/tasks/checks.yml | 1 + roles/openid_proxy/tasks/main.yml | 124 ++ roles/openid_proxy/tasks/metrics.yml | 0 roles/openid_proxy/tasks/serverspec.yml | 6 + .../apache2/sites-available/admin_proxy.conf | 94 ++ .../apache2/sites-available/openid_proxy.conf | 126 ++ .../etc/apache2/sites-available/redirect.conf | 27 + .../templates/serverspec/openid-proxy_spec.rb | 81 ++ .../templates/var/www/html/health_check | 1 + .../templates/var/www/html/index.html | 194 +++ roles/percona/defaults/main.yml | 35 + roles/percona/handlers/main.yml | 3 + roles/percona/meta/main.yml | 12 + roles/percona/tasks/arbiter.yml | 19 + roles/percona/tasks/backup.yml | 6 + roles/percona/tasks/checks.yml | 10 + roles/percona/tasks/main.yml | 25 + roles/percona/tasks/metrics.yml | 0 roles/percona/tasks/replication.yml | 45 + roles/percona/tasks/server.yml | 90 ++ roles/percona/tasks/serverspec.yml | 7 + roles/percona/tasks/setup.yml | 19 + roles/percona/templates/etc/default/garbd | 8 + roles/percona/templates/etc/my.cnf | 11 + .../etc/mysql/conf.d/bind-inaddr-any.cnf | 4 + .../etc/mysql/conf.d/replication.cnf | 42 + .../templates/etc/mysql/conf.d/tuning.cnf | 7 + .../templates/etc/mysql/conf.d/utf8.cnf | 11 + roles/percona/templates/percona-xtrabackup.sh | 52 + roles/percona/templates/root/.my.cnf | 5 + .../templates/serverspec/percona_spec.rb | 103 ++ roles/postfix-simple/handlers/main.yml | 6 + roles/postfix-simple/meta/main.yml | 3 + roles/postfix-simple/tasks/checks.yml | 4 + roles/postfix-simple/tasks/main.yml | 26 + roles/postfix-simple/tasks/metrics.yml | 0 roles/postfix-simple/tasks/serverspec.yml | 6 + .../templates/etc/postfix/main.cf | 22 + .../serverspec/postfix-simple_spec.rb | 11 + roles/pxe/defaults/main.yml | 56 + roles/pxe/handlers/main.yml | 10 + roles/pxe/meta/main.yml | 5 + roles/pxe/tasks/checks.yml | 1 + roles/pxe/tasks/install.yml | 50 + roles/pxe/tasks/main.yml | 30 + roles/pxe/tasks/metrics.yml | 0 roles/pxe/tasks/remove.yml | 18 + roles/pxe/tasks/server.yml | 105 ++ roles/pxe/tasks/serverspec.yml | 6 + .../pxe/templates/etc/dnsmasq.d/pxeboot.conf | 18 + roles/pxe/templates/serverspec/pxe_spec.yml | 11 + .../tftpboot/os/block_monitor_preseed.cfg | 133 ++ .../templates/tftpboot/os/default_preseed.cfg | 124 ++ .../templates/tftpboot/os/swift_preseed.cfg | 132 ++ .../templates/tftpboot/os/vagrant_preseed.cfg | 124 ++ roles/pxe/templates/tftpboot/post_install.sh | 46 + .../templates/tftpboot/pxelinux.cfg/default | 57 + .../templates/tftpboot/pxelinux.cfg/server | 19 + roles/pypi-mirror/README.md | 28 + roles/pypi-mirror/defaults/main.yml | 70 + roles/pypi-mirror/handlers/main.yml | 13 + roles/pypi-mirror/meta/main.yml | 17 + roles/pypi-mirror/tasks/checks.yml | 4 + roles/pypi-mirror/tasks/main.yml | 97 ++ roles/pypi-mirror/tasks/metrics.yml | 0 roles/pypi-mirror/tasks/serverspec.yml | 6 + roles/pypi-mirror/tasks/users.yml | 29 + .../etc/apache2/sites-available/pypi_mirror | 53 + .../templates/pypi_mirror/devpi_users.sh | 36 + .../templates/serverspec/pypi_mirror_spec.rb | 56 + roles/rabbitmq/defaults/main.yml | 112 ++ roles/rabbitmq/handlers/main.yml | 8 + roles/rabbitmq/meta/main.yml | 14 + roles/rabbitmq/tasks/checks.yml | 22 + roles/rabbitmq/tasks/cluster.yml | 56 + roles/rabbitmq/tasks/main.yml | 164 +++ roles/rabbitmq/tasks/metrics.yml | 12 + roles/rabbitmq/tasks/serverspec.yml | 6 + .../templates/etc/default/rabbitmq-server | 11 + .../templates/etc/rabbitmq/rabbitmq-env.conf | 5 + .../templates/etc/rabbitmq/rabbitmq.config | 40 + .../templates/etc/rabbitmq/ssl/cacert.pem | 3 + .../templates/etc/rabbitmq/ssl/cert.pem | 3 + .../templates/etc/rabbitmq/ssl/key.pem | 3 + .../etc/security/limits.d/10-rabbitmq.conf | 3 + .../templates/serverspec/rabbitmq_spec.rb | 54 + .../templates/var/lib/rabbitmq/erlang.cookie | 1 + roles/rally/defaults/main.yml | 5 + roles/rally/handlers/main.yml | 3 + roles/rally/meta/main.yml | 4 + roles/rally/tasks/checks.yml | 0 roles/rally/tasks/main.yml | 66 + roles/rally/tasks/metrics.yml | 0 roles/rally/tasks/serverspec.yml | 0 .../etc/apache2/sites-available/rally.conf | 13 + .../etc/rally/rally-deployment-example.conf | 11 + roles/rally/templates/etc/rally/rally.conf | 590 ++++++++ .../bbc/rally-tests/bbc-cloud-validate.yml | 451 ++++++ roles/redis/defaults/main.yml | 10 + roles/redis/handlers/main.yml | 6 + roles/redis/meta/main.yml | 8 + roles/redis/tasks/checks.yml | 4 + roles/redis/tasks/main.yml | 45 + roles/redis/tasks/metrics.yml | 0 roles/redis/tasks/serverspec.yml | 6 + roles/redis/templates/etc/init.d/redis-server | 91 ++ .../redis/templates/serverspec/redis_spec.rb | 27 + roles/runtime/java/defaults/main.yml | 1 + roles/runtime/java/handlers/main.yml | 1 + roles/runtime/java/meta/main.yml | 1 + roles/runtime/java/tasks/checks.yml | 0 roles/runtime/java/tasks/main.yml | 50 + roles/runtime/java/tasks/metrics.yml | 0 roles/runtime/java/tasks/serverspec.yml | 0 roles/runtime/python/defaults/main.yml | 4 + roles/runtime/python/handlers/main.yml | 1 + roles/runtime/python/meta/main.yml | 1 + roles/runtime/python/tasks/main.yml | 21 + roles/runtime/ruby/defaults/main.yml | 1 + roles/runtime/ruby/handlers/main.yml | 1 + roles/runtime/ruby/meta/main.yml | 1 + roles/runtime/ruby/tasks/main.yml | 25 + roles/security/defaults/main.yml | 11 + roles/security/handlers/main.yml | 1 + roles/security/meta/main.yml | 1 + roles/security/tasks/checks.yml | 0 roles/security/tasks/main.yml | 19 + roles/security/tasks/metrics.yml | 0 roles/security/tasks/serverspec.yml | 0 roles/sensu-check/defaults/main.yml | 368 +++++ roles/sensu-check/handlers/main.yml | 10 + roles/sensu-check/tasks/main.yml | 9 + .../files/plugins/check-arping.sh | 55 + roles/sensu-client/files/plugins/check-cpu.rb | 106 ++ .../files/plugins/check-dir-new-files.rb | 103 ++ .../sensu-client/files/plugins/check-disk.rb | 225 +++ .../files/plugins/check-es-cluster-status.rb | 108 ++ .../plugins/check-es-file-descriptors.rb | 110 ++ .../files/plugins/check-es-heap.rb | 134 ++ .../files/plugins/check-es-insert-rate.py | 166 +++ .../files/plugins/check-fstab-mounts.rb | 68 + .../files/plugins/check-hostname.sh | 25 + .../sensu-client/files/plugins/check-http.rb | 138 ++ .../files/plugins/check-inspec.rb | 90 ++ .../files/plugins/check-ipmi-sensors.py | 72 + .../files/plugins/check-kernel-options.rb | 34 + .../plugins/check-large-receive-offload.py | 54 + roles/sensu-client/files/plugins/check-log.rb | 140 ++ .../files/plugins/check-mcelog.sh | 18 + roles/sensu-client/files/plugins/check-mem.sh | 72 + .../sensu-client/files/plugins/check-netif.rb | 75 + .../files/plugins/check-network-traffic.rb | 63 + roles/sensu-client/files/plugins/check-ntp.rb | 33 + .../files/plugins/check-percona-xtrabackup.py | 88 ++ .../files/plugins/check-port-listening.sh | 58 + .../sensu-client/files/plugins/check-procs.rb | 132 ++ .../files/plugins/check-rabbitmq-cluster.rb | 45 + .../files/plugins/check-rabbitmq-queues.rb | 96 ++ .../sensu-client/files/plugins/check-raid.sh | 21 + .../files/plugins/check-serverspec.rb | 90 ++ .../files/plugins/check-storcli.pl | 1287 +++++++++++++++++ .../files/plugins/check-syslog-socket.rb | 87 ++ .../files/plugins/check-ucarp-procs.sh | 37 + .../files/plugins/check_3ware_raid.py | 486 +++++++ .../files/plugins/check_adaptec_raid.py | 134 ++ .../files/plugins/check_megaraid_sas.pl | 263 ++++ .../files/plugins/es-cluster-metrics.rb | 108 ++ .../files/plugins/es-node-graphite.rb | 238 +++ .../files/plugins/es-node-metrics.rb | 75 + roles/sensu-client/files/plugins/flapper.rb | 35 + .../files/plugins/load-metrics.rb | 43 + .../files/plugins/memcached-graphite.rb | 54 + .../files/plugins/memory-metrics.rb | 45 + .../files/plugins/metrics-disk-capacity.rb | 113 ++ .../files/plugins/metrics-disk-usage.rb | 121 ++ .../files/plugins/metrics-disk.rb | 87 ++ .../files/plugins/metrics-filesize.rb | 76 + .../sensu-client/files/plugins/metrics-net.rb | 86 ++ .../files/plugins/metrics-netif.rb | 64 + .../files/plugins/metrics-network-queues.py | 109 ++ .../files/plugins/metrics-power-temp.py | 64 + .../files/plugins/metrics-process-usage.py | 122 ++ .../files/plugins/percona-cluster-size.rb | 68 + .../files/plugins/vmstat-metrics.rb | 79 + roles/sensu-client/meta/main.yml | 8 + roles/sensu-client/tasks/checks.yml | 6 + roles/sensu-client/tasks/main.yml | 99 ++ roles/sensu-client/tasks/metrics.yml | 1 + roles/sensu-client/tasks/serverspec.yml | 43 + .../templates/etc/sensu/conf.d/client.json | 23 + .../templates/etc/sensu/conf.d/rabbitmq.json | 13 + .../templates/etc/sensu/ssl/cert.pem | 3 + .../templates/etc/sensu/ssl/key.pem | 3 + .../templates/serverspec/sensu-client_spec.rb | 11 + roles/sensu-common/defaults/main.yml | 218 +++ roles/sensu-common/handlers/main.yml | 5 + roles/sensu-common/meta/main.yml | 11 + roles/sensu-common/tasks/checks.yml | 0 roles/sensu-common/tasks/main.yml | 30 + roles/sensu-common/tasks/metrics.yml | 0 roles/sensu-common/tasks/serverspec.yml | 6 + .../templates/etc/init.d/sensu-service | 349 +++++ .../templates/etc/sudoers.d/sensu | 20 + .../templates/serverspec/sensu-common_spec.rb | 7 + roles/sensu-server/defaults/main.yml | 3 + .../etc/sensu/extensions/handlers/flapjack.rb | 98 ++ .../extensions/handlers/flapjack_http.rb | 352 +++++ .../sensu/extensions/handlers/sensu_api.rb | 226 +++ .../files/etc/sensu/handlers/pagerduty.rb | 55 + .../files/etc/sensu/handlers/remedy.rb | 191 +++ .../files/etc/sensu/scripts/clear-stashes.rb | 84 ++ .../sensu-plugins/check-sensu-api-health.rb | 125 ++ roles/sensu-server/handlers/main.yml | 39 + roles/sensu-server/meta/main.yml | 6 + roles/sensu-server/tasks/checks.yml | 42 + roles/sensu-server/tasks/dashboard.yml | 59 + roles/sensu-server/tasks/main.yml | 18 + roles/sensu-server/tasks/metrics.yml | 0 roles/sensu-server/tasks/server.yml | 201 +++ roles/sensu-server/tasks/serverspec.yml | 6 + .../apache2/sites-available/sensu-api.conf | 23 + .../templates/etc/default/sensu-api | 7 + .../templates/etc/default/sensu-server | 9 + .../sensu-server/templates/etc/default/uchiwa | 3 + .../templates/etc/init.d/sensu-server | 53 + .../templates/etc/logrotate.d/sensu | 50 + .../templates/etc/sensu/conf.d/api.json | 9 + .../etc/sensu/conf.d/handlers/default.json | 8 + .../etc/sensu/conf.d/handlers/flapjack.json | 3 + .../sensu/conf.d/handlers/flapjack_http.json | 3 + .../etc/sensu/conf.d/handlers/graphite.json | 12 + .../etc/sensu/conf.d/handlers/hijack.json | 8 + .../etc/sensu/conf.d/handlers/metrics.json | 8 + .../etc/sensu/conf.d/handlers/pagerduty.json | 24 + .../etc/sensu/conf.d/handlers/remedy.json | 20 + .../etc/sensu/conf.d/handlers/sensu_api.json | 3 + .../templates/etc/sensu/conf.d/rabbitmq.json | 13 + .../templates/etc/sensu/conf.d/redis.json | 6 + .../templates/etc/sensu/ssl/cert.pem | 3 + .../templates/etc/sensu/ssl/key.pem | 3 + .../templates/etc/sensu/uchiwa.json | 32 + .../serverspec/sensu-server-handler_spec.rb | 26 + .../templates/serverspec/sensu-server_spec.rb | 27 + roles/serverspec/defaults/main.yml | 11 + roles/serverspec/handlers/main.yml | 1 + roles/serverspec/meta/main.yml | 1 + roles/serverspec/tasks/checks.yml | 0 roles/serverspec/tasks/main.yml | 44 + roles/serverspec/tasks/metrics.yml | 0 roles/serverspec/tasks/serverspec.yml | 0 .../templates/etc/serverspec/Rakefile | 29 + .../etc/serverspec/spec/spec_helper.rb | 6 + roles/source-install/defaults/main.yml | 13 + roles/source-install/tasks/checks.yml | 0 roles/source-install/tasks/main.yml | 84 ++ roles/source-install/tasks/metrics.yml | 0 roles/source-install/tasks/serverspec.yml | 0 roles/squid/README.md | 7 + roles/squid/defaults/main.yml | 32 + roles/squid/handlers/main.yml | 3 + roles/squid/meta/main.yml | 3 + roles/squid/tasks/checks.yml | 5 + roles/squid/tasks/main.yml | 110 ++ roles/squid/tasks/metrics.yml | 0 roles/squid/tasks/serverspec.yml | 5 + .../etc/squid3/allowed-domains-dst.acl | 29 + .../etc/squid3/allowed-networks-src.acl | 13 + .../etc/squid3/pkg-blacklist-regexp.acl | 11 + roles/squid/templates/etc/squid3/squid.conf | 127 ++ .../squid/templates/serverspec/squid_spec.rb | 52 + roles/sshagentmux/defaults/main.yml | 16 + roles/sshagentmux/handlers/main.yml | 6 + roles/sshagentmux/meta/main.yml | 8 + roles/sshagentmux/tasks/authorizations.yml | 25 + roles/sshagentmux/tasks/checks.yml | 5 + roles/sshagentmux/tasks/main.yml | 47 + roles/sshagentmux/tasks/metrics.yml | 0 roles/sshagentmux/tasks/serverspec.yml | 6 + roles/sshagentmux/tasks/ssh_agent.yml | 14 + roles/sshagentmux/tasks/ssh_keys.yml | 52 + .../etc/init/authorization_proxy.conf | 15 + .../etc/profile.d/authorization_proxy.sh | 7 + .../templates/serverspec/sshagentmux_spec.rb | 19 + roles/support-tools/defaults/main.yml | 45 + roles/support-tools/meta/main.yml | 12 + roles/support-tools/tasks/checks.yml | 0 roles/support-tools/tasks/main.yml | 39 + roles/support-tools/tasks/metrics.yml | 0 roles/support-tools/tasks/serverspec.yml | 7 + roles/support-tools/templates/git_pull | 37 + .../support-tools/templates/git_pull.sudoers | 3 + .../serverspec/support-tools_spec.rb | 31 + roles/ttyspy-client/handlers/main.yml | 5 + roles/ttyspy-client/meta/main.yml | 4 + roles/ttyspy-client/tasks/checks.yml | 5 + roles/ttyspy-client/tasks/main.yml | 62 + roles/ttyspy-client/tasks/metrics.yml | 0 roles/ttyspy-client/tasks/serverspec.yml | 7 + .../templates/etc/init/ttyspyd.conf | 12 + roles/ttyspy-client/templates/etc/ttyspy.conf | 8 + .../templates/etc/ttyspy/client/ca.pem | 3 + .../templates/etc/ttyspy/client/cert.pem | 3 + .../templates/etc/ttyspy/client/key.pem | 3 + .../serverspec/ttyspy-client_spec.rb | 31 + roles/ttyspy-common/defaults/main.yml | 29 + roles/ttyspy-common/meta/main.yml | 3 + roles/ttyspy-common/tasks/checks.yml | 0 roles/ttyspy-common/tasks/main.yml | 39 + roles/ttyspy-common/tasks/metrics.yml | 0 roles/ttyspy-common/tasks/serverspec.yml | 7 + .../serverspec/ttyspy-common_spec.rb | 16 + roles/ttyspy-server/handlers/main.yml | 5 + roles/ttyspy-server/meta/main.yml | 4 + roles/ttyspy-server/tasks/checks.yml | 5 + roles/ttyspy-server/tasks/compression.yml | 16 + roles/ttyspy-server/tasks/main.yml | 73 + roles/ttyspy-server/tasks/metrics.yml | 0 roles/ttyspy-server/tasks/serverspec.yml | 7 + .../etc/cron.daily/ttyspy_compression | 5 + .../templates/etc/ttyspy/compression.py | 10 + .../templates/etc/ttyspy/server/ca.pem | 3 + .../templates/etc/ttyspy/server/cert.pem | 3 + .../templates/etc/ttyspy/server/key.pem | 3 + .../serverspec/ttyspy-server_spec.rb | 43 + roles/users/defaults/main.yml | 54 + roles/users/handlers/main.yml | 5 + roles/users/tasks/main.yml | 74 + roles/users/tasks/serverspec.yml | 6 + roles/users/templates/authorized_keys | 5 + .../users/templates/serverspec/users_spec.rb | 20 + roles/varnish/defaults/main.yml | 9 + roles/varnish/handlers/main.yml | 3 + roles/varnish/meta/main.yml | 3 + roles/varnish/tasks/checks.yml | 4 + roles/varnish/tasks/main.yml | 32 + roles/varnish/tasks/metrics.yml | 0 roles/varnish/tasks/serverspec.yml | 6 + roles/varnish/templates/etc/default/varnish | 8 + .../varnish/templates/etc/varnish/default.vcl | 21 + .../templates/serverspec/varnish_spec.yml | 11 + roles/vault/defaults/main.yml | 24 + roles/vault/handlers/main.yml | 6 + roles/vault/meta/main.yml | 3 + roles/vault/tasks/main.yml | 36 + roles/vault/templates/etc/vault.hcl | 21 + roles/yum_mirror/defaults/main.yml | 58 + roles/yum_mirror/meta/main.yml | 17 + roles/yum_mirror/tasks/checks.yml | 1 + roles/yum_mirror/tasks/main.yml | 145 ++ roles/yum_mirror/tasks/metrics.yml | 1 + roles/yum_mirror/tasks/serverspec.yml | 6 + .../etc/apache2/sites-available/yum_mirror | 67 + .../templates/etc/cron.d/yum_mirror | 9 + .../yum/repo-manager/repo-manager-template.sh | 11 + .../templates/etc/yum/repos.d/mirror-template | 19 + roles/yum_mirror/templates/etc/yum/yum.conf | 10 + .../templates/serverspec/yum_mirror_spec.rb | 63 + site.yml | 240 +++ tmp/.gitignore | 2 + vagrant.yml | 50 + 906 files changed, 40236 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTORS.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 Vagrantfile create mode 100644 ansible.cfg create mode 100644 docs/architecture.md create mode 100644 docs/bastion_user_admin.md create mode 100644 docs/contract_elk_cluster.md create mode 100644 docs/deploy_sc.md create mode 100644 docs/disk_replacement.md create mode 100644 docs/elk_shard_recovery.md create mode 100644 docs/flapjack.md create mode 100644 docs/images/1.png create mode 100644 docs/images/2.png create mode 100644 docs/images/3.png create mode 100644 docs/images/4.png create mode 100644 docs/images/5.png create mode 100644 docs/images/grafana_sla_dashboard.png create mode 100644 docs/logging.dot create mode 100644 docs/logging.png create mode 100644 docs/monitoring.dot create mode 100644 docs/monitoring.png create mode 100644 docs/post_deploy_validation.md create mode 100644 docs/sitecontroller_backup_strategies.md create mode 100644 envs/example/bastion/bastion-users.yml create mode 100644 envs/example/bastion/group_vars/all.yml create mode 100644 envs/example/bastion/hosts create mode 100644 envs/example/bastion/vagrant.yml create mode 100644 envs/example/centralcontroller/group_vars/all.yml create mode 100644 envs/example/centralcontroller/hosts create mode 100644 envs/example/centralcontroller/vagrant.yml create mode 100644 envs/example/ci/group_vars/all.yml create mode 100644 envs/example/ci/group_vars/cd-masters.yml create mode 100644 envs/example/ci/group_vars/imagebuilder.yml create mode 100644 envs/example/ci/group_vars/jenkins-masters.yml create mode 100644 envs/example/ci/heat_stack.yml create mode 100644 envs/example/ci/hosts create mode 100644 envs/example/ci/vagrant.yml create mode 100644 envs/example/ci/vars_heat.yml create mode 100644 envs/example/consul/group_vars/all create mode 100644 envs/example/consul/hosts create mode 100644 envs/example/consul/vagrant.yml create mode 100644 envs/example/defaults.yml create mode 100644 envs/example/elk/group_vars/all.yml create mode 100644 envs/example/elk/heat_stack.yml create mode 100644 envs/example/elk/hosts create mode 100644 envs/example/elk/vagrant.yml create mode 100644 envs/example/elk/vars_heat.yml create mode 100644 envs/example/ipmi-proxy/group_vars/all.yml create mode 100644 envs/example/ipmi-proxy/hosts create mode 100644 envs/example/ipmi-proxy/vagrant.yml create mode 100644 envs/example/ipsec/README.md create mode 100644 envs/example/ipsec/group_vars/all create mode 100644 envs/example/ipsec/host_vars/ipsec-client.yaml create mode 100644 envs/example/ipsec/host_vars/ipsec-server.yml create mode 100644 envs/example/ipsec/hosts create mode 100644 envs/example/ipsec/vagrant.yml create mode 100644 envs/example/jenkins/group_vars/all create mode 100644 envs/example/jenkins/hosts create mode 100644 envs/example/jenkins/vagrant.yml create mode 100644 envs/example/minibootstrapper/README.md create mode 100644 envs/example/minibootstrapper/group_vars/all create mode 100644 envs/example/minibootstrapper/hosts create mode 100644 envs/example/minibootstrapper/vagrant.yml create mode 100644 envs/example/mirror/group_vars/all.yml create mode 100644 envs/example/mirror/heat_stack.yml create mode 100644 envs/example/mirror/hosts create mode 100644 envs/example/mirror/vagrant.yml create mode 100644 envs/example/mirror/vars_heat.yml create mode 100644 envs/example/monitor/group_vars/all.yml create mode 100644 envs/example/monitor/hosts create mode 100644 envs/example/monitor/vagrant.yml create mode 100644 envs/example/netdata/group_vars/all.yml create mode 100644 envs/example/netdata/heat_stack.yml create mode 100644 envs/example/netdata/hosts create mode 100644 envs/example/netdata/vagrant.yml create mode 100644 envs/example/netdata/vars_heat.yml create mode 100644 envs/example/netman/group_vars/all create mode 100644 envs/example/netman/hosts create mode 100644 envs/example/netman/vagrant.yml create mode 100644 envs/example/sitecontroller/group_vars/all.yml create mode 100644 envs/example/sitecontroller/group_vars/bastion.yml create mode 100644 envs/example/sitecontroller/group_vars/bootstrap.yml create mode 100644 envs/example/sitecontroller/group_vars/elk.yml create mode 100644 envs/example/sitecontroller/group_vars/monitor.yml create mode 100644 envs/example/sitecontroller/heat_stack.yml create mode 100644 envs/example/sitecontroller/host_vars/bastion01.yml create mode 100644 envs/example/sitecontroller/hosts create mode 100644 envs/example/sitecontroller/vagrant.yml create mode 100644 envs/example/sitecontroller/vars_heat.yml create mode 100644 envs/example/swiftbrowser/group_vars/all.yml create mode 100644 envs/example/swiftbrowser/hosts create mode 100644 envs/example/swiftbrowser/vagrant.yml create mode 100644 envs/example/vagrant.yml create mode 100644 hosts create mode 100644 library/apache2_site.py create mode 100644 library/binary.py create mode 100644 library/jenkins.py create mode 100644 library/logrotate.py create mode 100644 library/rabbitmq_user.py create mode 100644 library/sensu_check.py create mode 100644 library/sensu_check_dict.py create mode 100644 library/sensu_metrics_check.py create mode 100644 library/sensu_process_check.py create mode 100755 library/systemd_service.py create mode 100644 library/upstart_service.py create mode 100644 library/xml_configuration.py create mode 100644 playbooks/add-bastion-users.yml create mode 100644 playbooks/apt-mirror-debmirror-conversion.yml create mode 100644 playbooks/central-cutover.yml create mode 100644 playbooks/delete-bastion-users.yml create mode 100644 playbooks/elk-cluster-upgrade.yml create mode 100644 playbooks/elk-curator.yml create mode 100644 playbooks/elk-rolling-restart.yml create mode 100644 playbooks/elk-stats.yml create mode 100644 playbooks/full-remote-restart.yml create mode 100644 playbooks/healthcheck.yml create mode 100644 playbooks/pxe-config.yml create mode 100644 playbooks/remove-grafana-datasources.yml create mode 100644 playbooks/sensu-server-force-restart.yml create mode 100644 playbooks/sensu-server-health.yml create mode 100644 playbooks/stats-to-spreadsheet.py create mode 100644 playbooks/templates/authorized_keys create mode 100644 playbooks/templates/delete_indices.yml create mode 100755 playbooks/ucs-bmc-verify.py create mode 100644 playbooks/ucs-sanity.yml create mode 100644 playbooks/update-apt-mirror.yml create mode 100644 playbooks/update-yum-mirror.yml create mode 100644 playbooks/upgrades/elasticsearch.yml create mode 100644 playbooks/validation.yml create mode 100644 playbooks/vyatta-add-ssh-keys.sh create mode 100644 playbooks/vyatta-set-authorized-keys.yml create mode 100644 playbooks/whisper-resize.yml create mode 100644 plugins/callbacks/timestamp.py create mode 100644 plugins/filters/filters.py create mode 100644 plugins/filters/jenkins_filters.py create mode 100644 plugins/vars/default_vars.py create mode 100644 requirements-es-stats.txt create mode 100644 requirements.txt create mode 100644 roles/_blank/defaults/main.yml create mode 100644 roles/_blank/handlers/main.yml create mode 100644 roles/_blank/meta/main.yml create mode 100644 roles/_blank/tasks/checks.yml create mode 100644 roles/_blank/tasks/main.yml create mode 100644 roles/_blank/tasks/metrics.yml create mode 100644 roles/_blank/tasks/serverspec.yml create mode 100644 roles/_sitecontroller/defaults/main.yml create mode 100644 roles/_sitecontroller/tasks/checks.yml create mode 100644 roles/_sitecontroller/tasks/main.yml create mode 100644 roles/_sitecontroller/tasks/metrics.yml create mode 100644 roles/_sitecontroller/tasks/serverspec.yml create mode 100644 roles/_sitecontroller/templates/etc/apt/sources.list create mode 100644 roles/_sitecontroller/templates/python/pip.conf create mode 100644 roles/_sitecontroller/templates/python/pydistutils.cfg create mode 100644 roles/_sitecontroller/templates/ruby/gemrc create mode 100644 roles/apache/defaults/main.yml create mode 100644 roles/apache/handlers/main.yml create mode 100644 roles/apache/meta/main.yml create mode 100644 roles/apache/tasks/checks.yml create mode 100644 roles/apache/tasks/main.yml create mode 100644 roles/apache/tasks/metrics.yml create mode 100644 roles/apache/tasks/serverspec.yml create mode 100644 roles/apache/templates/etc/apache2/ports.conf create mode 100644 roles/apache/templates/serverspec/apache_spec.rb create mode 100644 roles/apt-mirror/defaults/main.yml create mode 100644 roles/apt-mirror/meta/main.yml create mode 100644 roles/apt-mirror/tasks/checks.yml create mode 100644 roles/apt-mirror/tasks/debmirror.yml create mode 100644 roles/apt-mirror/tasks/main.yml create mode 100644 roles/apt-mirror/tasks/metrics.yml create mode 100644 roles/apt-mirror/tasks/serverspec.yml create mode 100644 roles/apt-mirror/templates/debmirror.sh create mode 100644 roles/apt-mirror/templates/etc/apache2/htaccess create mode 100644 roles/apt-mirror/templates/etc/apache2/sites-available/apt_mirror create mode 100644 roles/apt-mirror/templates/etc/cron.d/debmirror create mode 100644 roles/apt-mirror/templates/serverspec/apt_mirror_spec.rb create mode 100644 roles/apt-repos/defaults/main.yml create mode 100644 roles/apt-repos/tasks/checks.yml create mode 100644 roles/apt-repos/tasks/main.yml create mode 100644 roles/apt-repos/tasks/metrics.yml create mode 100644 roles/apt-repos/tasks/serverspec.yml create mode 100644 roles/bastion/defaults/main.yml create mode 100755 roles/bastion/files/utils/ssh-ip-check create mode 100644 roles/bastion/handlers/main.yml create mode 100644 roles/bastion/meta/main.yml create mode 100644 roles/bastion/tasks/checks.yml create mode 100644 roles/bastion/tasks/main.yml create mode 100644 roles/bastion/tasks/metrics.yml create mode 100644 roles/bastion/tasks/serverspec.yml create mode 100644 roles/bastion/tasks/utils.yml create mode 100644 roles/bastion/tasks/yubiauthd.yml create mode 100644 roles/bastion/templates/etc/init/yubiauthd.conf create mode 100644 roles/bastion/templates/etc/ssh-ip-check.conf create mode 100644 roles/bastion/templates/etc/yubiauthd.conf create mode 100644 roles/bastion/templates/serverspec/yama-utils_spec.rb create mode 100644 roles/bastion/templates/serverspec/yubiauthd_spec.rb create mode 100644 roles/bbg-ssl/defaults/main.yml create mode 100644 roles/bbg-ssl/handlers/main.yml create mode 100644 roles/bbg-ssl/tasks/checks.yml create mode 100644 roles/bbg-ssl/tasks/main.yml create mode 100644 roles/bbg-ssl/tasks/metrics.yml create mode 100644 roles/bbg-ssl/tasks/serverspec.yml create mode 100644 roles/bbg-ssl/templates/etc/ssl/certs/intermediate.crt create mode 100644 roles/bbg-ssl/templates/etc/ssl/certs/sitecontroller.crt create mode 100644 roles/bbg-ssl/templates/etc/ssl/private/sitecontroller.key create mode 100644 roles/bbg-ssl/templates/usr/local/share/ca-certificates/ca_cert.crt create mode 100644 roles/collectd/defaults/main.yml create mode 100644 roles/collectd/handlers/main.yml create mode 100644 roles/collectd/meta/main.yml create mode 100644 roles/collectd/tasks/checks.yml create mode 100644 roles/collectd/tasks/main.yml create mode 100644 roles/collectd/tasks/metrics.yml create mode 100644 roles/collectd/tasks/serverspec.yml create mode 100644 roles/collectd/templates/etc/collectd/collectd.conf create mode 100644 roles/collectd/templates/etc/collectd/plugins/amqp.conf create mode 100644 roles/collectd/templates/etc/collectd/plugins/logfile.conf create mode 100644 roles/collectd/templates/etc/collectd/plugins/system.conf create mode 100644 roles/collectd/templates/serverspec/collectd_spec.rb create mode 100644 roles/common/defaults/main.yml create mode 100644 roles/common/handlers/main.yml create mode 100644 roles/common/meta/main.yml create mode 100644 roles/common/tasks/checks.yml create mode 100644 roles/common/tasks/main.yml create mode 100644 roles/common/tasks/metrics.yml create mode 100644 roles/common/tasks/ntpd.yml create mode 100644 roles/common/tasks/serverspec.yml create mode 100644 roles/common/tasks/shell_customization.yml create mode 100644 roles/common/tasks/ssh.yml create mode 100644 roles/common/tasks/sudoers.yml create mode 100644 roles/common/tasks/ufw.yml create mode 100644 roles/common/templates/admin_user/bash_profile create mode 100644 roles/common/templates/admin_user/bashrc create mode 100644 roles/common/templates/admin_user/gitconfig create mode 100644 roles/common/templates/admin_user/tmux.conf create mode 100644 roles/common/templates/bin/ghe_authorized_keys.py create mode 100644 roles/common/templates/etc/hosts create mode 100644 roles/common/templates/etc/ntp.conf create mode 100644 roles/common/templates/etc/profile.d/prompt.sh create mode 100644 roles/common/templates/etc/profile.d/tmux_fix_ssh.sh create mode 100644 roles/common/templates/etc/ssh/ssh_host_rsa_key create mode 100644 roles/common/templates/etc/ssh/ssh_host_rsa_key.pub create mode 100644 roles/common/templates/etc/ssh/sshd_config create mode 100644 roles/common/templates/etc/sudoers create mode 100644 roles/common/templates/etc/sudoers.d/admin_user create mode 100644 roles/common/templates/etc/sudoers.d/blueboxcloud create mode 100644 roles/common/templates/etc/timezone create mode 100644 roles/common/templates/serverspec/common_spec.rb create mode 100644 roles/common/templates/ssh-private-key create mode 100644 roles/consul/defaults/main.yml create mode 100644 roles/consul/meta/main.yml create mode 100644 roles/consul/tasks/checks.yml create mode 100644 roles/consul/tasks/main.yml create mode 100644 roles/consul/tasks/metrics.yml create mode 100644 roles/consul/tasks/serverspec.yml create mode 100644 roles/consul/templates/etc/consul.json create mode 100644 roles/consul/templates/serverspec/consul-server_rb.yml create mode 100644 roles/dnsmasq/defaults/main.yml create mode 100644 roles/dnsmasq/handlers/main.yml create mode 100644 roles/dnsmasq/meta/main.yml create mode 100644 roles/dnsmasq/tasks/checks.yml create mode 100644 roles/dnsmasq/tasks/main.yml create mode 100644 roles/dnsmasq/tasks/metrics.yml create mode 100644 roles/dnsmasq/tasks/serverspec.yml create mode 100644 roles/dnsmasq/templates/etc/default/dnsmasq create mode 100644 roles/dnsmasq/templates/etc/dnsmasq.d/server.conf create mode 100644 roles/dnsmasq/templates/etc/hosts.dnsmasq create mode 100644 roles/dnsmasq/templates/etc/resolv.conf create mode 100644 roles/dnsmasq/templates/serverspec/dnsmasq_spec.rb create mode 100644 roles/docker/defaults/main.yml create mode 100644 roles/docker/meta/main.yml create mode 100644 roles/docker/tasks/checks.yml create mode 100644 roles/docker/tasks/main.yml create mode 100644 roles/docker/tasks/metrics.yml create mode 100644 roles/docker/tasks/serverspec.yml create mode 100644 roles/elasticsearch/defaults/main.yml create mode 100644 roles/elasticsearch/handlers/main.yml create mode 100644 roles/elasticsearch/meta/main.yml create mode 100644 roles/elasticsearch/tasks/checks.yml create mode 100644 roles/elasticsearch/tasks/curator.yml create mode 100644 roles/elasticsearch/tasks/main.yml create mode 100644 roles/elasticsearch/tasks/metrics.yml create mode 100644 roles/elasticsearch/tasks/serverspec.yml create mode 100644 roles/elasticsearch/templates/etc/cron.daily/elasticsearch create mode 100644 roles/elasticsearch/templates/etc/default/elasticsearch create mode 100644 roles/elasticsearch/templates/etc/elasticsearch/action.yml create mode 100644 roles/elasticsearch/templates/etc/elasticsearch/curator.yml create mode 100644 roles/elasticsearch/templates/etc/elasticsearch/elasticsearch.yml create mode 100644 roles/elasticsearch/templates/etc/init.d/elasticsearch create mode 100644 roles/elasticsearch/templates/etc/logrotate.d/elasticsearch create mode 100644 roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats-collection.py create mode 100644 roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats.py create mode 100644 roles/elasticsearch/templates/opt/sitecontroller/sensu-plugins/elk-stats-metrics.rb create mode 100644 roles/elasticsearch/templates/serverspec/elasticsearch_spec.rb create mode 100644 roles/file-mirror/defaults/main.yml create mode 100644 roles/file-mirror/meta/main.yml create mode 100644 roles/file-mirror/tasks/apache.yml create mode 100644 roles/file-mirror/tasks/checks.yml create mode 100644 roles/file-mirror/tasks/main.yml create mode 100644 roles/file-mirror/tasks/metrics.yml create mode 100644 roles/file-mirror/tasks/serverspec.yml create mode 100644 roles/file-mirror/templates/etc/apache2/htaccess create mode 100644 roles/file-mirror/templates/etc/apache2/sites-available/file_mirror create mode 100644 roles/file-mirror/templates/serverspec/file_mirror_spec.rb create mode 100644 roles/flapjack/defaults/main.yml create mode 100644 roles/flapjack/handlers/main.yml create mode 100644 roles/flapjack/meta/main.yml create mode 100644 roles/flapjack/tasks/checks.yml create mode 100644 roles/flapjack/tasks/email.yml create mode 100644 roles/flapjack/tasks/main.yml create mode 100644 roles/flapjack/tasks/metrics.yml create mode 100644 roles/flapjack/tasks/serverspec.yml create mode 100644 roles/flapjack/templates/etc/flapjack/flapjack_config.yaml create mode 100644 roles/flapjack/templates/etc/flapjack/templates/pagerduty.erb create mode 100644 roles/flapjack/templates/serverspec/flapjack_spec.rb create mode 100644 roles/gem-mirror/defaults/main.yml create mode 100644 roles/gem-mirror/handlers/main.yml create mode 100644 roles/gem-mirror/meta/main.yml create mode 100644 roles/gem-mirror/tasks/apache.yml create mode 100644 roles/gem-mirror/tasks/checks.yml create mode 100644 roles/gem-mirror/tasks/main.yml create mode 100644 roles/gem-mirror/tasks/metrics.yml create mode 100644 roles/gem-mirror/tasks/serverspec.yml create mode 100644 roles/gem-mirror/templates/etc/apache2/sites-available/gem_mirror create mode 100644 roles/gem-mirror/templates/opt/gem_mirror/config.ru create mode 100644 roles/gem-mirror/templates/opt/gem_mirror/unicorn.rb create mode 100644 roles/gem-mirror/templates/serverspec/gem_mirror_spec.rb create mode 100644 roles/git-repos/defaults/main.yml create mode 100644 roles/gpg/defaults/main.yml create mode 100644 roles/gpg/files/blueboxcloud.asc create mode 100644 roles/gpg/files/blueboxcloud.key create mode 100644 roles/gpg/tasks/checks.yml create mode 100644 roles/gpg/tasks/main.yml create mode 100644 roles/gpg/tasks/metrics.yml create mode 100644 roles/gpg/tasks/serverspec.yml create mode 100644 roles/grafana/defaults/main.yml create mode 100644 roles/grafana/handlers/main.yml create mode 100644 roles/grafana/meta/main.yml create mode 100644 roles/grafana/tasks/checks.yml create mode 100644 roles/grafana/tasks/datasources.yml create mode 100644 roles/grafana/tasks/main.yml create mode 100644 roles/grafana/tasks/metrics.yml create mode 100644 roles/grafana/tasks/serverspec.yml create mode 100644 roles/grafana/templates/dashboards/bbc-basic-host.json create mode 100644 roles/grafana/templates/dashboards/bbc-ceph-usage.json create mode 100644 roles/grafana/templates/dashboards/bbc-standard-sla.json create mode 100644 roles/grafana/templates/dashboards/cleversafe-standard-sla.json create mode 100644 roles/grafana/templates/dashboards/elk-stats.json create mode 100644 roles/grafana/templates/datasources.json create mode 100644 roles/grafana/templates/etc/default/grafana-server create mode 100644 roles/grafana/templates/etc/grafana/grafana.ini create mode 100644 roles/grafana/templates/etc/init.d/grafana-server create mode 100644 roles/grafana/templates/serverspec/grafana_spec.rb create mode 100644 roles/graphite/defaults/main.yml create mode 100644 roles/graphite/handlers/main.yml create mode 100644 roles/graphite/meta/main.yml create mode 100644 roles/graphite/tasks/checks.yml create mode 100644 roles/graphite/tasks/main.yml create mode 100644 roles/graphite/tasks/metrics.yml create mode 100644 roles/graphite/tasks/serverspec.yml create mode 100644 roles/graphite/templates/etc/apache2/sites-available/graphite create mode 100644 roles/graphite/templates/etc/init/carbon-cache.conf create mode 100644 roles/graphite/templates/opt/graphite/conf/carbon.conf create mode 100644 roles/graphite/templates/opt/graphite/conf/graphite.wsgi create mode 100644 roles/graphite/templates/opt/graphite/conf/storage-schemas.conf create mode 100644 roles/graphite/templates/opt/graphite/webapp/graphite/local_settings.py create mode 100644 roles/graphite/templates/opt/graphite/webapp/graphite/settings.py create mode 100644 roles/graphite/templates/serverspec/graphite_spec.rb create mode 100644 roles/harden/tasks/checks.yml create mode 100644 roles/harden/tasks/main.yml create mode 100644 roles/harden/tasks/metrics.yml create mode 100644 roles/harden/tasks/serverspec.yml create mode 100644 roles/imagebuilder/defaults/main.yml create mode 100755 roles/imagebuilder/files/usr/local/bin/image-refresh.sh create mode 100644 roles/imagebuilder/meta/main.yml create mode 100644 roles/imagebuilder/tasks/checks.yml create mode 100644 roles/imagebuilder/tasks/main.yml create mode 100644 roles/imagebuilder/tasks/metrics.yml create mode 100644 roles/imagebuilder/tasks/serverspec.yml create mode 100644 roles/imagebuilder/templates/etc/cron.d/dib-image-refresh create mode 100644 roles/imagebuilder/templates/etc/sudoers.d/dib create mode 100644 roles/ipmi-proxy/README.md create mode 100644 roles/ipmi-proxy/defaults/main.yml create mode 100644 roles/ipmi-proxy/meta/main.yml create mode 100644 roles/ipmi-proxy/tasks/checks.yml create mode 100644 roles/ipmi-proxy/tasks/main.yml create mode 100644 roles/ipmi-proxy/tasks/metrics.yml create mode 100644 roles/ipmi-proxy/tasks/serverspec.yml create mode 100644 roles/ipmi-proxy/templates/etc/apache2/sites-available/ipmi-proxy.conf create mode 100644 roles/ipmi-proxy/templates/etc/bluebox/ipmi-proxy.conf create mode 100644 roles/ipmi-proxy/templates/etc/sudoers.d/www-data create mode 100644 roles/ipmi-proxy/templates/serverspec/ipmi-proxy_spec.rb create mode 100644 roles/ipsec/defaults/main.yml create mode 100644 roles/ipsec/handlers/main.yml create mode 100644 roles/ipsec/meta/main.yml create mode 100644 roles/ipsec/tasks/checks.yml create mode 100644 roles/ipsec/tasks/main.yml create mode 100644 roles/ipsec/tasks/serverspec.yml create mode 100644 roles/ipsec/tasks/strongswan.yml create mode 100644 roles/ipsec/templates/etc/ipsec.conf create mode 100644 roles/ipsec/templates/etc/ipsec.d/connections.conf create mode 100644 roles/ipsec/templates/etc/ipsec.d/ipsec-notify.sh create mode 100644 roles/ipsec/templates/etc/ipsec.secrets create mode 100644 roles/ipsec/templates/etc/network/interfaces.d/vti0.cfg create mode 100644 roles/ipsec/templates/serverspec/ipsec_spec.rb create mode 100644 roles/jenkins-common/defaults/main.yml create mode 100644 roles/jenkins-common/handlers/main.yml create mode 100644 roles/jenkins-common/meta/main.yml create mode 100644 roles/jenkins-common/tasks/checks.yml create mode 100644 roles/jenkins-common/tasks/main.yml create mode 100644 roles/jenkins-common/tasks/metrics.yml create mode 100644 roles/jenkins-common/tasks/serverspec.yml create mode 100644 roles/jenkins-master/files/rules/failure.prop create mode 100644 roles/jenkins-master/files/usr/local/bin/load-jenkins-jobs.sh create mode 100644 roles/jenkins-master/handlers/main.yml create mode 100644 roles/jenkins-master/meta/main.yml create mode 100644 roles/jenkins-master/tasks/apache.yml create mode 100644 roles/jenkins-master/tasks/checks.yml create mode 100644 roles/jenkins-master/tasks/jjb.yml create mode 100644 roles/jenkins-master/tasks/main.yml create mode 100644 roles/jenkins-master/tasks/metrics.yml create mode 100644 roles/jenkins-master/tasks/serverspec.yml create mode 100644 roles/jenkins-master/templates/etc/apache2/sites-available/jenkins create mode 100644 roles/jenkins-master/templates/etc/default/jenkins create mode 100644 roles/jenkins-master/templates/etc/jenkins_jobs/jenkins_jobs.ini create mode 100644 roles/jenkins-master/templates/serverspec/jenkins_spec.rb create mode 100644 roles/jenkins-master/templates/ssh-key create mode 100644 roles/jenkins-master/templates/var/lib/jenkins/com.tikal.jenkins.plugins.multijob.PhaseJobsConfig.xml create mode 100644 roles/jenkins-master/templates/var/lib/jenkins/config.xml create mode 100644 roles/jenkins-master/templates/var/lib/jenkins/credentials.xml create mode 100644 roles/jenkins-master/templates/var/lib/jenkins/gerrit-trigger.xml create mode 100644 roles/jenkins-master/templates/var/lib/jenkins/github-plugin-configuration.xml create mode 100644 roles/jenkins-master/templates/var/lib/jenkins/jenkins.model.JenkinsLocationConfiguration.xml create mode 100644 roles/jenkins-master/templates/var/lib/jenkins/jenkins.plugins.slack.SlackNotifier.xml create mode 100644 roles/jenkins-slave/defaults/main.yml create mode 100644 roles/jenkins-slave/meta/main.yml create mode 100644 roles/jenkins-slave/tasks/checks.yml create mode 100644 roles/jenkins-slave/tasks/main.yml create mode 100644 roles/jenkins-slave/tasks/metrics.yml create mode 100644 roles/jenkins-slave/tasks/serverspec.yml create mode 100644 roles/jenkins-slave/templates/gitconfig create mode 100644 roles/jenkins-slave/templates/home/jenkins/jenkins.stackrc create mode 100644 roles/jenkins-slave/templates/private_vars/rhel/default.yml create mode 100644 roles/jenkins-slave/templates/private_vars/ubuntu/default.yml create mode 100644 roles/jenkins-slave/templates/var/lib/jenkins/packagecloud create mode 100644 roles/kibana/defaults/main.yml create mode 100644 roles/kibana/handlers/main.yml create mode 100644 roles/kibana/meta/main.yml create mode 100644 roles/kibana/tasks/checks.yml create mode 100644 roles/kibana/tasks/config.yml create mode 100644 roles/kibana/tasks/main.yml create mode 100644 roles/kibana/tasks/metrics.yml create mode 100644 roles/kibana/tasks/serverspec.yml create mode 100644 roles/kibana/templates/etc/init/kibana.conf create mode 100644 roles/kibana/templates/opt/kibana/config/config.json create mode 100644 roles/kibana/templates/opt/kibana/config/index-pattern.json create mode 100644 roles/kibana/templates/opt/kibana/config/kibana.yml create mode 100644 roles/kibana/templates/serverspec/kibana_spec.rb create mode 100644 roles/logging-config/defaults/main.yml create mode 100644 roles/logging-config/handlers/main.yml create mode 100644 roles/logging-config/meta/main.yml create mode 100644 roles/logging-config/tasks/checks.yml create mode 100644 roles/logging-config/tasks/filebeat.yml create mode 100644 roles/logging-config/tasks/main.yml create mode 100644 roles/logging-config/tasks/metrics.yml create mode 100644 roles/logging-config/tasks/serverspec.yml create mode 100644 roles/logging-config/templates/etc/filebeat/filebeat.d/template.yml create mode 100644 roles/logging/defaults/main.yml create mode 100644 roles/logging/files/etc/init/logstash-forwarder.conf create mode 100644 roles/logging/files/etc/rsyslog.d/49-haproxy.conf create mode 100644 roles/logging/handlers/main.yml create mode 100644 roles/logging/meta/main.yml create mode 100644 roles/logging/tasks/checks.yml create mode 100644 roles/logging/tasks/filebeat.yml create mode 100644 roles/logging/tasks/main.yml create mode 100644 roles/logging/tasks/metrics.yml create mode 100644 roles/logging/tasks/serverspec.yml create mode 100644 roles/logging/templates/etc/default/logstash-forwarder create mode 100644 roles/logging/templates/etc/filebeat/filebeat.yml create mode 100644 roles/logging/templates/etc/init/logstash-forwarder.conf create mode 100644 roles/logging/templates/etc/logstash-forwarder.d/main.conf create mode 100644 roles/logging/templates/etc/rsyslog.d/50-default.conf create mode 100644 roles/logging/templates/serverspec/logging_spec.rb create mode 100644 roles/logstash/README.md create mode 100644 roles/logstash/defaults/main.yml create mode 100644 roles/logstash/handlers/main.yml create mode 100644 roles/logstash/meta/main.yml create mode 100644 roles/logstash/tasks/checks.yml create mode 100644 roles/logstash/tasks/main.yml create mode 100644 roles/logstash/tasks/metrics.yml create mode 100644 roles/logstash/tasks/serverspec.yml create mode 100644 roles/logstash/templates/etc/default/logstash create mode 100644 roles/logstash/templates/etc/logstash/conf.d/filter-add-missing-customer_id.conf create mode 100644 roles/logstash/templates/etc/logstash/conf.d/filter-drop-empty.conf create mode 100644 roles/logstash/templates/etc/logstash/conf.d/filter-json.conf create mode 100644 roles/logstash/templates/etc/logstash/conf.d/filter-offset-integer.conf create mode 100644 roles/logstash/templates/etc/logstash/conf.d/filter-openstack.conf create mode 100644 roles/logstash/templates/etc/logstash/conf.d/filter-syslog.conf create mode 100644 roles/logstash/templates/etc/logstash/conf.d/filter-tags.conf create mode 100644 roles/logstash/templates/etc/logstash/conf.d/pipeline.conf create mode 100644 roles/logstash/templates/etc/logstash/patterns/openstack create mode 100644 roles/logstash/templates/serverspec/logstash_spec.rb create mode 100644 roles/manage-disks/defaults/main.yml create mode 100644 roles/manage-disks/handlers/main.yml create mode 100644 roles/manage-disks/meta/main.yml create mode 100644 roles/manage-disks/tasks/checks.yml create mode 100644 roles/manage-disks/tasks/main.yml create mode 100644 roles/manage-disks/tasks/metrics.yml create mode 100644 roles/manage-disks/tasks/serverspec.yml create mode 100644 roles/manage-disks/templates/mount-loopback.conf create mode 100644 roles/manage-disks/templates/passphrase create mode 100644 roles/netdata-dashboard/defaults/main.yml create mode 100644 roles/netdata-dashboard/meta/main.yml create mode 100644 roles/netdata-dashboard/tasks/apache.yml create mode 100644 roles/netdata-dashboard/tasks/checks.yml create mode 100644 roles/netdata-dashboard/tasks/main.yml create mode 100644 roles/netdata-dashboard/tasks/metrics.yml create mode 100644 roles/netdata-dashboard/tasks/serverspec.yml create mode 100644 roles/netdata-dashboard/templates/etc/apache2/htaccess create mode 100644 roles/netdata-dashboard/templates/etc/apache2/sites-available/netdata_dashboard create mode 100644 roles/netdata-dashboard/templates/serverspec/file_mirror_spec.rb create mode 100644 roles/netdata-dashboard/templates/var/www/html/health_check create mode 100644 roles/netdata-dashboard/templates/var/www/html/index.html create mode 100644 roles/netdata/defaults/main.yml create mode 100644 roles/netdata/handlers/main.yml create mode 100644 roles/netdata/meta/main.yml create mode 100644 roles/netdata/tasks/checks.yml create mode 100644 roles/netdata/tasks/main.yml create mode 100644 roles/netdata/tasks/metrics.yml create mode 100644 roles/netdata/tasks/serverspec.yml create mode 100644 roles/oauth2_proxy/README.md create mode 100644 roles/oauth2_proxy/defaults/main.yml create mode 100644 roles/oauth2_proxy/handlers/main.yml create mode 100644 roles/oauth2_proxy/meta/main.yml create mode 100644 roles/oauth2_proxy/tasks/apache.yml create mode 100644 roles/oauth2_proxy/tasks/checks.yml create mode 100644 roles/oauth2_proxy/tasks/main.yml create mode 100644 roles/oauth2_proxy/tasks/metrics.yml create mode 100644 roles/oauth2_proxy/tasks/serverspec.yml create mode 100644 roles/oauth2_proxy/templates/etc/apache2/sites-available/oauth2_proxy.conf create mode 100644 roles/oauth2_proxy/templates/etc/oauth2_proxy/oauth2_proxy.cfg.j2 create mode 100644 roles/oauth2_proxy/templates/serverspec/oauth2-proxy_spec.rb create mode 100644 roles/oauth2_proxy/templates/var/www/html/index.html create mode 100644 roles/openid_proxy/defaults/main.yml create mode 100644 roles/openid_proxy/handlers/main.yml create mode 100644 roles/openid_proxy/meta/main.yml create mode 100644 roles/openid_proxy/tasks/checks.yml create mode 100644 roles/openid_proxy/tasks/main.yml create mode 100644 roles/openid_proxy/tasks/metrics.yml create mode 100644 roles/openid_proxy/tasks/serverspec.yml create mode 100644 roles/openid_proxy/templates/etc/apache2/sites-available/admin_proxy.conf create mode 100644 roles/openid_proxy/templates/etc/apache2/sites-available/openid_proxy.conf create mode 100644 roles/openid_proxy/templates/etc/apache2/sites-available/redirect.conf create mode 100644 roles/openid_proxy/templates/serverspec/openid-proxy_spec.rb create mode 100644 roles/openid_proxy/templates/var/www/html/health_check create mode 100644 roles/openid_proxy/templates/var/www/html/index.html create mode 100644 roles/percona/defaults/main.yml create mode 100644 roles/percona/handlers/main.yml create mode 100644 roles/percona/meta/main.yml create mode 100644 roles/percona/tasks/arbiter.yml create mode 100644 roles/percona/tasks/backup.yml create mode 100644 roles/percona/tasks/checks.yml create mode 100644 roles/percona/tasks/main.yml create mode 100644 roles/percona/tasks/metrics.yml create mode 100644 roles/percona/tasks/replication.yml create mode 100644 roles/percona/tasks/server.yml create mode 100644 roles/percona/tasks/serverspec.yml create mode 100644 roles/percona/tasks/setup.yml create mode 100644 roles/percona/templates/etc/default/garbd create mode 100644 roles/percona/templates/etc/my.cnf create mode 100644 roles/percona/templates/etc/mysql/conf.d/bind-inaddr-any.cnf create mode 100644 roles/percona/templates/etc/mysql/conf.d/replication.cnf create mode 100644 roles/percona/templates/etc/mysql/conf.d/tuning.cnf create mode 100644 roles/percona/templates/etc/mysql/conf.d/utf8.cnf create mode 100755 roles/percona/templates/percona-xtrabackup.sh create mode 100644 roles/percona/templates/root/.my.cnf create mode 100644 roles/percona/templates/serverspec/percona_spec.rb create mode 100644 roles/postfix-simple/handlers/main.yml create mode 100644 roles/postfix-simple/meta/main.yml create mode 100644 roles/postfix-simple/tasks/checks.yml create mode 100644 roles/postfix-simple/tasks/main.yml create mode 100644 roles/postfix-simple/tasks/metrics.yml create mode 100644 roles/postfix-simple/tasks/serverspec.yml create mode 100644 roles/postfix-simple/templates/etc/postfix/main.cf create mode 100644 roles/postfix-simple/templates/serverspec/postfix-simple_spec.rb create mode 100644 roles/pxe/defaults/main.yml create mode 100644 roles/pxe/handlers/main.yml create mode 100644 roles/pxe/meta/main.yml create mode 100644 roles/pxe/tasks/checks.yml create mode 100644 roles/pxe/tasks/install.yml create mode 100644 roles/pxe/tasks/main.yml create mode 100644 roles/pxe/tasks/metrics.yml create mode 100644 roles/pxe/tasks/remove.yml create mode 100644 roles/pxe/tasks/server.yml create mode 100644 roles/pxe/tasks/serverspec.yml create mode 100644 roles/pxe/templates/etc/dnsmasq.d/pxeboot.conf create mode 100644 roles/pxe/templates/serverspec/pxe_spec.yml create mode 100644 roles/pxe/templates/tftpboot/os/block_monitor_preseed.cfg create mode 100644 roles/pxe/templates/tftpboot/os/default_preseed.cfg create mode 100644 roles/pxe/templates/tftpboot/os/swift_preseed.cfg create mode 100644 roles/pxe/templates/tftpboot/os/vagrant_preseed.cfg create mode 100644 roles/pxe/templates/tftpboot/post_install.sh create mode 100644 roles/pxe/templates/tftpboot/pxelinux.cfg/default create mode 100644 roles/pxe/templates/tftpboot/pxelinux.cfg/server create mode 100644 roles/pypi-mirror/README.md create mode 100644 roles/pypi-mirror/defaults/main.yml create mode 100644 roles/pypi-mirror/handlers/main.yml create mode 100644 roles/pypi-mirror/meta/main.yml create mode 100644 roles/pypi-mirror/tasks/checks.yml create mode 100644 roles/pypi-mirror/tasks/main.yml create mode 100644 roles/pypi-mirror/tasks/metrics.yml create mode 100644 roles/pypi-mirror/tasks/serverspec.yml create mode 100644 roles/pypi-mirror/tasks/users.yml create mode 100644 roles/pypi-mirror/templates/etc/apache2/sites-available/pypi_mirror create mode 100644 roles/pypi-mirror/templates/pypi_mirror/devpi_users.sh create mode 100644 roles/pypi-mirror/templates/serverspec/pypi_mirror_spec.rb create mode 100644 roles/rabbitmq/defaults/main.yml create mode 100644 roles/rabbitmq/handlers/main.yml create mode 100644 roles/rabbitmq/meta/main.yml create mode 100644 roles/rabbitmq/tasks/checks.yml create mode 100644 roles/rabbitmq/tasks/cluster.yml create mode 100644 roles/rabbitmq/tasks/main.yml create mode 100644 roles/rabbitmq/tasks/metrics.yml create mode 100644 roles/rabbitmq/tasks/serverspec.yml create mode 100644 roles/rabbitmq/templates/etc/default/rabbitmq-server create mode 100644 roles/rabbitmq/templates/etc/rabbitmq/rabbitmq-env.conf create mode 100644 roles/rabbitmq/templates/etc/rabbitmq/rabbitmq.config create mode 100644 roles/rabbitmq/templates/etc/rabbitmq/ssl/cacert.pem create mode 100644 roles/rabbitmq/templates/etc/rabbitmq/ssl/cert.pem create mode 100644 roles/rabbitmq/templates/etc/rabbitmq/ssl/key.pem create mode 100644 roles/rabbitmq/templates/etc/security/limits.d/10-rabbitmq.conf create mode 100644 roles/rabbitmq/templates/serverspec/rabbitmq_spec.rb create mode 100644 roles/rabbitmq/templates/var/lib/rabbitmq/erlang.cookie create mode 100644 roles/rally/defaults/main.yml create mode 100644 roles/rally/handlers/main.yml create mode 100644 roles/rally/meta/main.yml create mode 100644 roles/rally/tasks/checks.yml create mode 100644 roles/rally/tasks/main.yml create mode 100644 roles/rally/tasks/metrics.yml create mode 100644 roles/rally/tasks/serverspec.yml create mode 100644 roles/rally/templates/etc/apache2/sites-available/rally.conf create mode 100644 roles/rally/templates/etc/rally/rally-deployment-example.conf create mode 100644 roles/rally/templates/etc/rally/rally.conf create mode 100644 roles/rally/templates/opt/bbc/rally-tests/bbc-cloud-validate.yml create mode 100644 roles/redis/defaults/main.yml create mode 100644 roles/redis/handlers/main.yml create mode 100644 roles/redis/meta/main.yml create mode 100644 roles/redis/tasks/checks.yml create mode 100644 roles/redis/tasks/main.yml create mode 100644 roles/redis/tasks/metrics.yml create mode 100644 roles/redis/tasks/serverspec.yml create mode 100644 roles/redis/templates/etc/init.d/redis-server create mode 100644 roles/redis/templates/serverspec/redis_spec.rb create mode 100644 roles/runtime/java/defaults/main.yml create mode 100644 roles/runtime/java/handlers/main.yml create mode 100644 roles/runtime/java/meta/main.yml create mode 100644 roles/runtime/java/tasks/checks.yml create mode 100644 roles/runtime/java/tasks/main.yml create mode 100644 roles/runtime/java/tasks/metrics.yml create mode 100644 roles/runtime/java/tasks/serverspec.yml create mode 100644 roles/runtime/python/defaults/main.yml create mode 100644 roles/runtime/python/handlers/main.yml create mode 100644 roles/runtime/python/meta/main.yml create mode 100644 roles/runtime/python/tasks/main.yml create mode 100644 roles/runtime/ruby/defaults/main.yml create mode 100644 roles/runtime/ruby/handlers/main.yml create mode 100644 roles/runtime/ruby/meta/main.yml create mode 100644 roles/runtime/ruby/tasks/main.yml create mode 100644 roles/security/defaults/main.yml create mode 100644 roles/security/handlers/main.yml create mode 100644 roles/security/meta/main.yml create mode 100644 roles/security/tasks/checks.yml create mode 100644 roles/security/tasks/main.yml create mode 100644 roles/security/tasks/metrics.yml create mode 100644 roles/security/tasks/serverspec.yml create mode 100644 roles/sensu-check/defaults/main.yml create mode 100644 roles/sensu-check/handlers/main.yml create mode 100644 roles/sensu-check/tasks/main.yml create mode 100755 roles/sensu-client/files/plugins/check-arping.sh create mode 100755 roles/sensu-client/files/plugins/check-cpu.rb create mode 100755 roles/sensu-client/files/plugins/check-dir-new-files.rb create mode 100755 roles/sensu-client/files/plugins/check-disk.rb create mode 100755 roles/sensu-client/files/plugins/check-es-cluster-status.rb create mode 100755 roles/sensu-client/files/plugins/check-es-file-descriptors.rb create mode 100755 roles/sensu-client/files/plugins/check-es-heap.rb create mode 100755 roles/sensu-client/files/plugins/check-es-insert-rate.py create mode 100755 roles/sensu-client/files/plugins/check-fstab-mounts.rb create mode 100755 roles/sensu-client/files/plugins/check-hostname.sh create mode 100755 roles/sensu-client/files/plugins/check-http.rb create mode 100755 roles/sensu-client/files/plugins/check-inspec.rb create mode 100755 roles/sensu-client/files/plugins/check-ipmi-sensors.py create mode 100755 roles/sensu-client/files/plugins/check-kernel-options.rb create mode 100755 roles/sensu-client/files/plugins/check-large-receive-offload.py create mode 100755 roles/sensu-client/files/plugins/check-log.rb create mode 100755 roles/sensu-client/files/plugins/check-mcelog.sh create mode 100755 roles/sensu-client/files/plugins/check-mem.sh create mode 100755 roles/sensu-client/files/plugins/check-netif.rb create mode 100755 roles/sensu-client/files/plugins/check-network-traffic.rb create mode 100755 roles/sensu-client/files/plugins/check-ntp.rb create mode 100755 roles/sensu-client/files/plugins/check-percona-xtrabackup.py create mode 100755 roles/sensu-client/files/plugins/check-port-listening.sh create mode 100755 roles/sensu-client/files/plugins/check-procs.rb create mode 100755 roles/sensu-client/files/plugins/check-rabbitmq-cluster.rb create mode 100755 roles/sensu-client/files/plugins/check-rabbitmq-queues.rb create mode 100755 roles/sensu-client/files/plugins/check-raid.sh create mode 100755 roles/sensu-client/files/plugins/check-serverspec.rb create mode 100755 roles/sensu-client/files/plugins/check-storcli.pl create mode 100755 roles/sensu-client/files/plugins/check-syslog-socket.rb create mode 100755 roles/sensu-client/files/plugins/check-ucarp-procs.sh create mode 100755 roles/sensu-client/files/plugins/check_3ware_raid.py create mode 100755 roles/sensu-client/files/plugins/check_adaptec_raid.py create mode 100755 roles/sensu-client/files/plugins/check_megaraid_sas.pl create mode 100755 roles/sensu-client/files/plugins/es-cluster-metrics.rb create mode 100755 roles/sensu-client/files/plugins/es-node-graphite.rb create mode 100755 roles/sensu-client/files/plugins/es-node-metrics.rb create mode 100755 roles/sensu-client/files/plugins/flapper.rb create mode 100755 roles/sensu-client/files/plugins/load-metrics.rb create mode 100755 roles/sensu-client/files/plugins/memcached-graphite.rb create mode 100755 roles/sensu-client/files/plugins/memory-metrics.rb create mode 100755 roles/sensu-client/files/plugins/metrics-disk-capacity.rb create mode 100755 roles/sensu-client/files/plugins/metrics-disk-usage.rb create mode 100755 roles/sensu-client/files/plugins/metrics-disk.rb create mode 100755 roles/sensu-client/files/plugins/metrics-filesize.rb create mode 100755 roles/sensu-client/files/plugins/metrics-net.rb create mode 100755 roles/sensu-client/files/plugins/metrics-netif.rb create mode 100755 roles/sensu-client/files/plugins/metrics-network-queues.py create mode 100755 roles/sensu-client/files/plugins/metrics-power-temp.py create mode 100755 roles/sensu-client/files/plugins/metrics-process-usage.py create mode 100755 roles/sensu-client/files/plugins/percona-cluster-size.rb create mode 100755 roles/sensu-client/files/plugins/vmstat-metrics.rb create mode 100644 roles/sensu-client/meta/main.yml create mode 100644 roles/sensu-client/tasks/checks.yml create mode 100644 roles/sensu-client/tasks/main.yml create mode 100644 roles/sensu-client/tasks/metrics.yml create mode 100644 roles/sensu-client/tasks/serverspec.yml create mode 100644 roles/sensu-client/templates/etc/sensu/conf.d/client.json create mode 100644 roles/sensu-client/templates/etc/sensu/conf.d/rabbitmq.json create mode 100644 roles/sensu-client/templates/etc/sensu/ssl/cert.pem create mode 100644 roles/sensu-client/templates/etc/sensu/ssl/key.pem create mode 100644 roles/sensu-client/templates/serverspec/sensu-client_spec.rb create mode 100644 roles/sensu-common/defaults/main.yml create mode 100644 roles/sensu-common/handlers/main.yml create mode 100644 roles/sensu-common/meta/main.yml create mode 100644 roles/sensu-common/tasks/checks.yml create mode 100644 roles/sensu-common/tasks/main.yml create mode 100644 roles/sensu-common/tasks/metrics.yml create mode 100644 roles/sensu-common/tasks/serverspec.yml create mode 100644 roles/sensu-common/templates/etc/init.d/sensu-service create mode 100644 roles/sensu-common/templates/etc/sudoers.d/sensu create mode 100644 roles/sensu-common/templates/serverspec/sensu-common_spec.rb create mode 100644 roles/sensu-server/defaults/main.yml create mode 100644 roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack.rb create mode 100755 roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack_http.rb create mode 100755 roles/sensu-server/files/etc/sensu/extensions/handlers/sensu_api.rb create mode 100644 roles/sensu-server/files/etc/sensu/handlers/pagerduty.rb create mode 100644 roles/sensu-server/files/etc/sensu/handlers/remedy.rb create mode 100644 roles/sensu-server/files/etc/sensu/scripts/clear-stashes.rb create mode 100644 roles/sensu-server/files/opt/sitecontroller/sensu-plugins/check-sensu-api-health.rb create mode 100644 roles/sensu-server/handlers/main.yml create mode 100644 roles/sensu-server/meta/main.yml create mode 100644 roles/sensu-server/tasks/checks.yml create mode 100644 roles/sensu-server/tasks/dashboard.yml create mode 100644 roles/sensu-server/tasks/main.yml create mode 100644 roles/sensu-server/tasks/metrics.yml create mode 100644 roles/sensu-server/tasks/server.yml create mode 100644 roles/sensu-server/tasks/serverspec.yml create mode 100644 roles/sensu-server/templates/etc/apache2/sites-available/sensu-api.conf create mode 100644 roles/sensu-server/templates/etc/default/sensu-api create mode 100644 roles/sensu-server/templates/etc/default/sensu-server create mode 100644 roles/sensu-server/templates/etc/default/uchiwa create mode 100644 roles/sensu-server/templates/etc/init.d/sensu-server create mode 100644 roles/sensu-server/templates/etc/logrotate.d/sensu create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/api.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/default.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack_http.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/graphite.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/hijack.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/metrics.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/pagerduty.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/remedy.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/handlers/sensu_api.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/rabbitmq.json create mode 100644 roles/sensu-server/templates/etc/sensu/conf.d/redis.json create mode 100644 roles/sensu-server/templates/etc/sensu/ssl/cert.pem create mode 100644 roles/sensu-server/templates/etc/sensu/ssl/key.pem create mode 100644 roles/sensu-server/templates/etc/sensu/uchiwa.json create mode 100644 roles/sensu-server/templates/serverspec/sensu-server-handler_spec.rb create mode 100644 roles/sensu-server/templates/serverspec/sensu-server_spec.rb create mode 100644 roles/serverspec/defaults/main.yml create mode 100644 roles/serverspec/handlers/main.yml create mode 100644 roles/serverspec/meta/main.yml create mode 100644 roles/serverspec/tasks/checks.yml create mode 100644 roles/serverspec/tasks/main.yml create mode 100644 roles/serverspec/tasks/metrics.yml create mode 100644 roles/serverspec/tasks/serverspec.yml create mode 100644 roles/serverspec/templates/etc/serverspec/Rakefile create mode 100644 roles/serverspec/templates/etc/serverspec/spec/spec_helper.rb create mode 100644 roles/source-install/defaults/main.yml create mode 100644 roles/source-install/tasks/checks.yml create mode 100644 roles/source-install/tasks/main.yml create mode 100644 roles/source-install/tasks/metrics.yml create mode 100644 roles/source-install/tasks/serverspec.yml create mode 100644 roles/squid/README.md create mode 100644 roles/squid/defaults/main.yml create mode 100644 roles/squid/handlers/main.yml create mode 100644 roles/squid/meta/main.yml create mode 100644 roles/squid/tasks/checks.yml create mode 100644 roles/squid/tasks/main.yml create mode 100644 roles/squid/tasks/metrics.yml create mode 100644 roles/squid/tasks/serverspec.yml create mode 100644 roles/squid/templates/etc/squid3/allowed-domains-dst.acl create mode 100644 roles/squid/templates/etc/squid3/allowed-networks-src.acl create mode 100644 roles/squid/templates/etc/squid3/pkg-blacklist-regexp.acl create mode 100644 roles/squid/templates/etc/squid3/squid.conf create mode 100644 roles/squid/templates/serverspec/squid_spec.rb create mode 100644 roles/sshagentmux/defaults/main.yml create mode 100644 roles/sshagentmux/handlers/main.yml create mode 100644 roles/sshagentmux/meta/main.yml create mode 100644 roles/sshagentmux/tasks/authorizations.yml create mode 100644 roles/sshagentmux/tasks/checks.yml create mode 100644 roles/sshagentmux/tasks/main.yml create mode 100644 roles/sshagentmux/tasks/metrics.yml create mode 100644 roles/sshagentmux/tasks/serverspec.yml create mode 100644 roles/sshagentmux/tasks/ssh_agent.yml create mode 100644 roles/sshagentmux/tasks/ssh_keys.yml create mode 100644 roles/sshagentmux/templates/etc/init/authorization_proxy.conf create mode 100644 roles/sshagentmux/templates/etc/profile.d/authorization_proxy.sh create mode 100644 roles/sshagentmux/templates/serverspec/sshagentmux_spec.rb create mode 100644 roles/support-tools/defaults/main.yml create mode 100644 roles/support-tools/meta/main.yml create mode 100644 roles/support-tools/tasks/checks.yml create mode 100644 roles/support-tools/tasks/main.yml create mode 100644 roles/support-tools/tasks/metrics.yml create mode 100644 roles/support-tools/tasks/serverspec.yml create mode 100644 roles/support-tools/templates/git_pull create mode 100644 roles/support-tools/templates/git_pull.sudoers create mode 100644 roles/support-tools/templates/serverspec/support-tools_spec.rb create mode 100644 roles/ttyspy-client/handlers/main.yml create mode 100644 roles/ttyspy-client/meta/main.yml create mode 100644 roles/ttyspy-client/tasks/checks.yml create mode 100644 roles/ttyspy-client/tasks/main.yml create mode 100644 roles/ttyspy-client/tasks/metrics.yml create mode 100644 roles/ttyspy-client/tasks/serverspec.yml create mode 100644 roles/ttyspy-client/templates/etc/init/ttyspyd.conf create mode 100644 roles/ttyspy-client/templates/etc/ttyspy.conf create mode 100644 roles/ttyspy-client/templates/etc/ttyspy/client/ca.pem create mode 100644 roles/ttyspy-client/templates/etc/ttyspy/client/cert.pem create mode 100644 roles/ttyspy-client/templates/etc/ttyspy/client/key.pem create mode 100644 roles/ttyspy-client/templates/serverspec/ttyspy-client_spec.rb create mode 100644 roles/ttyspy-common/defaults/main.yml create mode 100644 roles/ttyspy-common/meta/main.yml create mode 100644 roles/ttyspy-common/tasks/checks.yml create mode 100644 roles/ttyspy-common/tasks/main.yml create mode 100644 roles/ttyspy-common/tasks/metrics.yml create mode 100644 roles/ttyspy-common/tasks/serverspec.yml create mode 100644 roles/ttyspy-common/templates/serverspec/ttyspy-common_spec.rb create mode 100644 roles/ttyspy-server/handlers/main.yml create mode 100644 roles/ttyspy-server/meta/main.yml create mode 100644 roles/ttyspy-server/tasks/checks.yml create mode 100644 roles/ttyspy-server/tasks/compression.yml create mode 100644 roles/ttyspy-server/tasks/main.yml create mode 100644 roles/ttyspy-server/tasks/metrics.yml create mode 100644 roles/ttyspy-server/tasks/serverspec.yml create mode 100644 roles/ttyspy-server/templates/etc/cron.daily/ttyspy_compression create mode 100644 roles/ttyspy-server/templates/etc/ttyspy/compression.py create mode 100644 roles/ttyspy-server/templates/etc/ttyspy/server/ca.pem create mode 100644 roles/ttyspy-server/templates/etc/ttyspy/server/cert.pem create mode 100644 roles/ttyspy-server/templates/etc/ttyspy/server/key.pem create mode 100644 roles/ttyspy-server/templates/serverspec/ttyspy-server_spec.rb create mode 100644 roles/users/defaults/main.yml create mode 100644 roles/users/handlers/main.yml create mode 100644 roles/users/tasks/main.yml create mode 100644 roles/users/tasks/serverspec.yml create mode 100644 roles/users/templates/authorized_keys create mode 100644 roles/users/templates/serverspec/users_spec.rb create mode 100644 roles/varnish/defaults/main.yml create mode 100644 roles/varnish/handlers/main.yml create mode 100644 roles/varnish/meta/main.yml create mode 100644 roles/varnish/tasks/checks.yml create mode 100644 roles/varnish/tasks/main.yml create mode 100644 roles/varnish/tasks/metrics.yml create mode 100644 roles/varnish/tasks/serverspec.yml create mode 100644 roles/varnish/templates/etc/default/varnish create mode 100644 roles/varnish/templates/etc/varnish/default.vcl create mode 100644 roles/varnish/templates/serverspec/varnish_spec.yml create mode 100644 roles/vault/defaults/main.yml create mode 100644 roles/vault/handlers/main.yml create mode 100644 roles/vault/meta/main.yml create mode 100644 roles/vault/tasks/main.yml create mode 100644 roles/vault/templates/etc/vault.hcl create mode 100644 roles/yum_mirror/defaults/main.yml create mode 100644 roles/yum_mirror/meta/main.yml create mode 100644 roles/yum_mirror/tasks/checks.yml create mode 100644 roles/yum_mirror/tasks/main.yml create mode 100644 roles/yum_mirror/tasks/metrics.yml create mode 100644 roles/yum_mirror/tasks/serverspec.yml create mode 100644 roles/yum_mirror/templates/etc/apache2/sites-available/yum_mirror create mode 100644 roles/yum_mirror/templates/etc/cron.d/yum_mirror create mode 100644 roles/yum_mirror/templates/etc/yum/repo-manager/repo-manager-template.sh create mode 100644 roles/yum_mirror/templates/etc/yum/repos.d/mirror-template create mode 100644 roles/yum_mirror/templates/etc/yum/yum.conf create mode 100644 roles/yum_mirror/templates/serverspec/yum_mirror_spec.rb create mode 100644 site.yml create mode 100644 tmp/.gitignore create mode 100644 vagrant.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b22c465 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.pyc +*.vdi +.vagrant +.tox +build +*.DS_Store +*-openrc.sh +*.retry +ursula.log +elk-stats +.ssh_config +*.log diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..e312b60 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,34 @@ +# CONTRIBUTORS + +The following people contributed to this project before it was opensourced +and history was removed to protect the innocent. If you feel you should be +added to this list, please PR it. + +## Leads + +* Paul Czarkowski +* Myles Steinhauser + +## Core Team + +* Craig Tracey +* Tom Spoonemoore +* Josh Yotty +* Michael Sambol +* Zach Sais +* Brian Richardson + +## Extended Team + +* Jesse Keating +* Tim Chavez +* Nicola Heald + +## Others + +* Ryan Miller +* Terry Penner +* Priya Ingle +* Leslie Lundquist +* Dustin Lundquist +* Jesse Keating diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fc94c23 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,13 @@ +Copyright 2017 IBM + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa59570 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Cuttle + +_Originally called Site Controller (sitectl) and is Pronounced "Cuddle"._ + +_insert logo of a squid/cuttlefish cuddling a server_ + +A Monolithic Repository of Composable Ansible Roles for building an SRE Operations Platform. + +Originally built by the BlueBox Cloud team to install the infrastructure required to build and +support Openstack Clouds using [Ursula](http://github.com/blueboxgroup/ursula) it quickly grew into +a larger project for enabling SRE Operations both in the Datacenter and in the Cloud. + +Like Ursula, [Ursula](http://github.com/blueboxgroup/ursula) Cuttle uses the +[ursula-cli](https://github.com/blueboxgroup/ursula-cli) ( installed via requirements.txt ) +for running Ansible on specific environments. + +For a rough idea of how Blue Box uses Cuttle by building Central and Remote sites +tethered together with IPSEC VPNs check out [architecture.md](architecture.md). + +You will see a number of example Ansible Inventories in `envs/example/` that +show Cuttle being used to build infrastructure to solve a number of problems. +`envs/example/sitecontroller` shows close to a full deployment, whereas +`envs/example/mirror` or `envs/example/elk` to build just specific components. +All of these environments can easily be deployed in Vagrant by using the `ursula-cli` + (see [Example Usage](#example-usage) ). + +How to Contribute +----------------- + +See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the original team. + +The official git repository of Site Controller is https://github.com/IBM/cuttle. +If you have cloned this from somewhere else, may god have mercy on your soul. + +### Workflow + +We follow the standard github workflow of Fork -> Branch -> PR -> Test -> Review -> Merge. + +The Site Controller Core team is working to put together guidance on contributing and +governance now that it is an opensource proect. + +Development and Testing +----------------------- + +### Build Development Environment + +``` +# clone this repo +$ git clone git@github.com:ibm/cuttle.git + +# install pip, hopefully your system has it already +# install virtualenv +$ pip install virtualenv + +# create a new virtualenv so python is happy +$ virtualenv --no-site-packages --no-wheel ~//venv + +# activate your new venv like normal +$ source ~//venv/bin/activate + +# install ursula-cli, the correct version of ansible, and all other deps +$ cd cuttle +$ pip install -r requirements.txt + +# run ansible using ursula-cli; or ansible-playbook, if that's how you roll +$ ursula envs/example/ site.yml + +# decactivate your virtualenv when you are done +$ deactivate +``` + +[Vagrant](https://www.vagrantup.com/) is our preferred Development/Testing framework. + +### Example Usage + +ursula-cli understands how to interact with vagrant using the `--provisioner` flag: + +``` +$ ursula --provisioner=vagrant envs/example/sitecontroller bastion.yml +$ ursula --provisioner=vagrant envs/example/sitecontroller site.yml +``` + +### Tardis and Heat + +You can also test in Tardis with Heat Orchestration. First, grab your stackrc file from Tardis: + +`Project > Compute > Access & Security > Download OpenStack RC File` + +Ensure your `ssh-agent` is running, then source your stackrc and run the play: +``` +$ source -openrc.sh +$ ursula --ursula-forward --provisioner=heat envs/example/sitecontroller site.yml +``` + +Add argument `--ursula-debug` for verbose output. + +## Run behind a docker proxy for local dev + +``` +$ docker run \ + --name proxy -p 3128:3128 \ + -v $(pwd)/tmp/cache:/var/cache/squid3 \ + -d jpetazzo/squid-in-a-can +``` + +then set the following in your inventory (`vagrant.yml` in `envs/example/*/`) + +``` +env_vars: + http_proxy: "http://10.0.2.2:3128" + https_proxy: "http://10.0.2.2:3128" + no_proxy: localhost,127.0.0.0/8,10.0.0.0/8,172.0.0.0/8 + +``` + +Deploying +--------- + +To actually deploy an environment you would use ursula-cli like so: + +``` +$ ursula ../sitecontroller-envs/sjc01 bastion.yml +$ ursula ../sitecontroller-envs/sjc01 site.yml + +# targetted runs using any ansible-playbook option +$ ursula ../ursula-infra-envs/sjc01 site.yml --tags openid_proxy +``` diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..8f52188 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,85 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : +require 'yaml' + +# INFRA_PLAYBOOK = ENV['INFRA_PLAYBOOK'] || abort("Please specify INFRA_PLAYBOOK env variable") + +ANSIBLE_ARGS = ENV['ANSIBLE_ARGS'] ? ENV['ANSIBLE_ARGS'].split() : [] + +if File.file?('.vagrant/vagrant.yml') + SETTINGS_FILE = ENV['SETTINGS_FILE'] || '.vagrant/vagrant.yml' +else + SETTINGS_FILE = ENV['SETTINGS_FILE'] || 'vagrant.yml' +end + +SETTINGS = YAML.load_file SETTINGS_FILE + +BOX_URL = SETTINGS['default']['box_url'] || 'http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_ubuntu-14.04_chef-provisionerless.box' +BOX_NAME = SETTINGS['default']['box_name'] || 'ubuntu-trusty' + + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + # All Vagrant configuration is done here. The most common configuration + # options are documented and commented below. For a complete reference, + # please see the online documentation at vagrantup.com. + + # Every Vagrant virtual environment requires a box to build off of. + + # default is a small vm + config.vm.box = BOX_NAME + config.vm.box_url = BOX_URL + config.vm.provider "virtualbox" do |v| + v.memory = SETTINGS['default']['memory'] + v.cpus = SETTINGS['default']['cpus'] + v.gui = SETTINGS['default']['gui'] + end + config.ssh.insert_key = false + config.ssh.forward_agent = true + + SETTINGS['vms'].each do |name,vm| + config.vm.define name do |c| + c.vm.hostname = name + if vm['ip_address'].is_a? String + ip_addresses = [vm['ip_address']] + else + ip_addresses = vm['ip_address'] + end + ip_addresses.each do |ip| + c.vm.network :private_network, ip: ip + end + if vm.has_key?('memory') || vm.has_key?('cpus') + c.vm.provider "virtualbox" do |v| + v.memory = vm['memory'] if vm.has_key?('memory') + v.cpus = vm['cpus'] if vm.has_key?('cpus') + if vm.has_key?('gui') + v.gui = vm['gui'] + end + end + end + + # allow vagrant provision to run + config.vm.provision "fix-no-tty", type: "shell" do |s| + s.privileged = false + s.inline = "sudo sed -i '/tty/!s/mesg n/tty -s \\&\\& mesg n/' /root/.profile" + end + # performance booster for VMs running on SSDs + c.vm.provision "shell", inline: "echo noop > /sys/block/sda/queue/scheduler" + end + end + + if SETTINGS.has_key?('ansible') + config.vm.provision "ansible" do |ansible| + ansible.playbook = 'site.yml' + ansible.extra_vars = 'envs/example/defaults.yml' + ansible.verbose = 'vvvv' if ENV['DEBUG'] + ansible.limit = 'all' + ansible.raw_arguments = ANSIBLE_ARGS + ansible.sudo = true + ansible.groups = SETTINGS['ansible']['groups'] + end + end + +end diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..e6ada56 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,27 @@ +[defaults] +roles_path = roles +hash_behaviour = merge +nocows = 1 +nocolor = 0 + +timeout = 60 +forks = 25 +transport = ssh +host_key_checking = False + +vars_plugins = plugins/vars +connection_plugins = plugins/connection +callback_plugins = plugins/callbacks +filter_plugins = plugins/filters + +log_path = ursula.log + +var_defaults_file = ../defaults.yml +ansible_managed = This file managed by ursula. Any changes made will be overwritten. +retry_files_enabled = False + +# Required so `sudo: yes` does not lose the environment variables, which hold the ssh-agent socket +sudo_flags=-HE + +[ssh_connection] +pipelining=true diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..a9ad87b --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,302 @@ +Overall Architecture +==================== + +Central Site Controller +----------------------- + +### Access +Authorized users can SSH to the Central Site Controller Bastion. Following this, +any Remote SiteController, or any Openstack Deployment based on any Remote Site +Controller can be accessed via SSH from the Central Bastion as long as the +user is in the correct group for that server. + +#### Control Portal +An Apache web portal, `control.XXXX.com` is being hosted by the +Central Controller to allow authorized users to monitor all deployments beneath +it. The portal can be accessed though the multi-factor authentication of +boxpanel. Once logged into the portal, sites for each Remote Site Controller can +be reached without further authentication, using OpenID. + +### Included +The Central Site Controller has: + * [Bastion](#bastion) + * [IPMI Proxy](#ipmi-proxy) + * [OpenID Proxy](#openid-proxy) + * [Monitoring](#monitoring) + * [Mirror & Squid Proxy](#mirror--squid-proxy) + + +#### Bastion +It houses: + * Support Tools (git_pull cronjobs, update info) + * SSHAuthMux (shared ssh authentication) + * ttyspy (sends all input/output to a remote server over TLS) + +The Bastion is merely a secure location post-connection. It also maintains the +state of the Central Site Controller to ensure it is always up-to-date. + +#### IPMI Proxy +Allows connection to IPMI of servers. This enables the Central Site Controller +to control the remote Site Controllers, even if powered off. + +#### OpenID Proxy +Allows connection to OpenID of servers. This enables the Central Control Pod to +use OpenID, maintaining a single identity with a given set of authentication. + +#### Monitoring +It houses: + * Sensu (system monitoring framework) + * RabbitMQ (AMQP - advanced message queuing protocol) + * Flapjack (alert-routing, event processing) + +Ensures that the Central Controller and everything directly controlled by it is +running properly with chronological checks. Checks are done via the Sensu client +within the Central Site Controller, and their results are passed (using +RabbitMQ) to the Sensu server, which are passed the the Sensu Redis server. +Redis servers allow the checks to be key-mapped, which allows higher +availability (retaining more events without loss). + +Apart from self-checking, the Central Site Controller also monitors all Remote +Site Controller deployments beneath it. All checks done by Remote Site +Controllers are passed to the Central Site Controller Sensu host. + +Pagerduty, an incident resolution service, is also enabled. Alerts from the +Sensu host within the Central Site Controller are passed to a Redis server. +Flapjack retrieves data from the Redis server, then from there, alerts are +passed to Pagerduty. + +The Uchiwa dashboard allows users to view Sensu checks by calling the +Sensu API which calls the Sensu Redis server. + +To gain a better understanding of how the overall monitoring works, view the +[Monitoring Diagram](#monitoring-diagram) + +#### Mirror & Squid Proxy +It houses _four_ mirrors: + * Apt + * PyPi + * Gem + * File + +A detailed listing of mirror contents can be found in your ansible inventory +The mirror is used by all Site Controller and OpenStack hosts and is accessed +via each Site Controller's Squid proxy (installed on the Bootstrapper). + +The Central Site Controller also houses a [Squid caching proxy](http://www.squid-cache.org/) +that is used to proxy domains such as [github.com](github.com). The proxy can be used as an +upstream/parent proxy for each Remote Site Controller's Squid. + +Remote Site Controller +---------------------- + +### Deploying +To deploy a Remote Site Controller, a working environment is required. +The [Site Controller Generator](#site-controller-generator) creates this for the +user. +To further understand how to deploy, read the docs: + * write docs + +### Access +From the Bastion of the Central Site Controller, authorized users can access the +remote Site Controllers deployed via SSH through an IPSec tunnel connected to +each VPN. To keep remote site security and hardware managable, reverse proxying +is used. Reverse proxy servers act as a gateway between the Central Site +Controller and each Remote Site Controller. A virtual router, known as a Vyatta +handles this proxy service, by executing DNS lookups, then rerouting original +request. In other words, rather than accessing the remote site directly from the +Central, the Central Site Controller sends a request to access the remote site +via the Vyatta, which then finds the correct private address of the Remote Site +Controller so it may send the request from the Central to it. + +### Included +Each Remote Site Controller has: + * [Bootstrapper](#bootstrapper) + * [ELK](#elk) + * [Monitoring](#monitoring-1) + +#### Bootstrapper +The Bootstrapper host plays a vital role in the deployment and upgrade of Site +Controller and OpenStack hosts. The Bootstrapper is the first host installed +and converged in a deployment. For a local environment the Bootstrapper is the +only host that gets Ubuntu installed manually (not via pxe). The +Bootstrapper serves two primary functions: + * Squid + * PXE (Local deployments only) + +It is important to note that no mirror roles are run on the Bootstrapper. For more +detailed information on the Bootstrapper's role in deployments, please read the +aforementioned deployment guides. + +###### Squid +The Bootstrapper serves as a [Squid caching proxy](http://www.squid-cache.org/) +for Site Controller and OpenStack hosts. This is especially useful when installing +and upgrading packages. Some important commands involving Squid: +``` +# check squid status +$ service squid3 status + +# view access log +$ view /var/log/squid3/access.log + +# view cache log +$ view /var/log/squid3/cache.log +``` + +###### PXE +For local deployments the Bootstrapper acts as a PXE server. This allows the +Site Controller team to install Ubuntu on SC and OpenStack hosts in an automated +fashion. The PXE files that are installed on the Bootstrapper are specified in the +environment's `group_vars/bootstrap.yml` and can be found on the server in +`/data/pxe/tftpboot`. +PXE files are installed when the Bootstrapper is converged, however, you must +specify `-e 'pxe_files=true'`, as the default is to skip file +installation. There is also a [playbook](https://github.com/IBM/cuttle/blob/master/playbooks/pxe-config.yml) +that only does PXE file installation. We have a practice of removing PXE files after deployment so +no host is accidentally wiped. + +#### ELK +It houses: + * Elasticsearch + * Logstash + * Kibana + * OpenID Proxy + +The ELK (Elasticsearch, Logstash, and Kibana) host manages logging. The logging +flow is very similar to that of monitoring. The Logstash Forwarder on an +Openstack Deployment or within the Remote Site Controller itself ships +designated logs to Logstash. Logstash then stores it in Elasticsearch, a search +engine that evaluates the collective of logs stored. The Kibana service allows +the user to search and create visualizations, all through the use of a web user +interface by pulling data from Elasticsearch. + +To gain a better understanding of how the logging flow works, view the +[Logging Diagram](#logging-diagram) + +#### Monitoring +It houses: + * Sensu + * RabbitMQ + * IPMI Proxy + * Grafana (with Graphite) + +The automated Sensu checks done within Remote Site Controller notify the Sensu +host from the Central Site Controller, as stated. +A Remote Site Controller mimics the Central Site Controller by also monitoring +all deployments beneath it, which for a Remote Site Controller are Openstack +Deployments. Checks done by the Sensu client on each associated Openstack +Deployment are passed to the Remote Site Controller Sensu host. + +Graphite, a monitoring tool that stores and passes data to Sensu, is implemented +into Grafana, a graph and dashboard builder for visualizing the time-series +metrics passed. + +The two major components of Graphite used for monitoring are: + * Carbon, a Twisted daemon that listens for time-series data, + * Whisper, a simple database library for storing time-series data, and the + +Monitoring Diagram +------------------ +``` +┌────── Central ────────┐ ┌────── Remote ────────┐ ┌──── Openstack ─────┐ +│ │ │ │ │ Deployment │ +│ Auth Proxy ────────────────> Apache │ │ │ +│ │ │ │ │ │ │ Sensu Client │ +│ V │ │ │ │ │ │ │ +│ ┌─── Apache │ │ │ │ │ │ │ +│ │ │ │ │ │ │ └──────── │ ─────────┘ +│ │ V │ │ V │ │ +│ │ Uchiwa │ │ Uchiwa │ │ +│ │ │ │ │ │ │ │ +│ │ V │ │ V │ │ +│ │ Sensu API │ │ Sensu API │ │ +│ │ │ │ │ │ │ │ +│ │ V │ │ V │ │ +│ │ Sensu │ │ Sensu │ │ +│ │ Redis Server │ │ Redis Server │ │ +│ │ ^ │ │ ^ │ │ +│ │ │ │ │ │ │ │ +│ │ Sensu Server <──────┐ │ Sensu Server <─────────────────┘ +│ │ ^ │ │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ │ │ └─────────────┐ +│ │ (RabbitMQ) │ └─(RabbitMQ)───┐ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ │ Sensu Client │ │ +│ │ Sensu Client │ │ │ │ +│ │ │ └──────────────────────┘ │ +│ │ │ │ +│ │ HTTP Broker <─────────(Flapjack HTTP Handler)───────┘ +│ │ │ │ +│ │ V │ +│ │ Flapjack │ +│ │ Redis Server │ +│ │ ^ │ +│ │ │ │ +│ └──> Flapjack │ +│ │ │ +└─────── │ ─────────────┘ + │ + V + PagerDuty (not hosted) +``` +A more detailed, graphical image can be found [here](https://github.com/IBM/cuttle/blob/master/docs/monitoring.png). + + +Logging Diagram +--------------- +``` +┌── Central ────┐ ┌─────── Remote ────────┐ ┌───── Openstack ──────┐ +│ │ │ │ │ Deployment │ +│ Auth Proxy ─────────> Apache │ │ │ +│ (Apache) │ │ │ │ │ Log Occurs │ +│ │ │ V │ │ │ │ +└───────────────┘ │ Kibana │ │ V │ + │ │ │ │ Logstash Forwarder │ + │ V │ │ (Shipper) │ + │ Elasticsearch │ │ │ │ + │ ^ │ │ │ │ + │ │ │ │ │ │ + │ Logstash <────────────────────┘ │ + │ ^ │ └──────────────────────┘ + │ │ │ + │ Logstash Forwarder │ + │ (Shipper) │ + │ ^ │ + │ │ │ + │ Log Occurs │ + └───────────────────────┘ +``` +A more detailed, graphical image can be found [here](https://github.com/IBM/cuttle/blob/master/docs/logging.png). + +Site Controller Generator +------------------------- + +[Site Controller Generator](https://github.com/IBM/cuttle-generator) + +### Function + +This tool generates working production environments based on a single input file +which contains variables specific to a desired site controller deployment. The +generated environment creates configurations, as well as documentation of what +configurations are established. + + +Future Development & Operations +------------------------------- + + * [Issue Triaging](#issue-triaging) + + +#### Issue Triaging +Whether issues are found by users or admins, there should be a portal accessible +and integrated into the control portal (limited access for users=reporters, full +access for admins = resolvers & reporters) where issues can be labeled with any +combination of: + Priority: + * _critical_, + * _moderate_, or + * _low_ + Status: + * _unresolved_, + * _in-progress_, or + * _resolved_ diff --git a/docs/bastion_user_admin.md b/docs/bastion_user_admin.md new file mode 100644 index 0000000..1026050 --- /dev/null +++ b/docs/bastion_user_admin.md @@ -0,0 +1,142 @@ +# Bastion User Admin + +This document provides a comprehensive guide to managing access +requests for Bastion systems. + +## Workflow + +1. edit your ansible inventory and modify your bastion users ( `users:` namespace ). +2. run `site.yml`, `playbooks/add-bastion-users.yml`, or `playbooks/delete-bastion-users.yml` + +## Setup + +User administation requires the following two repos. Clone both (on a Bastion host), and create a virtualenv +per the instructions [here](https://github.com/IBM/cuttle#build-development-environment). + +1. [sitecontroller](https://github.com/IBM/cuttle) contains the code to +add/update/delete users. Important files: `site.yml`, `playbooks/add-bastion-users.yml`, & `playbooks/delete-bastion-users.yml`. + +2. Your ansible inventory contains a representation +of all users that currently have access, as well as their access levels. + +## bastion-users.yml + +There are two data structures of importance in your ansible inventory. + +1. `user_groups` + + This is a dictionary of groups. Consider a group to be an access level. + Users belong to groups, and groups have ssh keys. Certain systems + permit access to specific ssh keys. Here's a taste: + ``` + blueboxadmin: + system: yes + ssh_keys: + enable_passphrase: no + fingerprint: ~ + public: ~ + private: ~ + ``` + +2. `users` + + This is a dictionary of Bastion users. A user inherits the keys of the groups he belongs to. + In other words, when a user logs in, his ssh agent is loaded with the keys of the groups of which he is a member. + This allows granular, explicit, and auditable control of permissions on Bastion systems. Users also have YubiKey + data associated with them, which allows for two-factor authentication. `uid` needs to be unique per user. Here's a flavor: + ``` + bobsmith: + comment: "Bob Smith; bobsmith@example.com" + primary_group: default + groups: + - internal_restricted + - OpenStack_Operations + - SiteController_Operations + public_keys: + - ssh-rsa AAAAB3... + uid: 1002 + yubikey: + aes_key: ~ + private_id: ~ + public_id: ~ + serial_number: ~ + ``` + +## Add & Update Bastion Users + +There are two steps that must be taken to add & update Bastion users: + +1. Update `bastion-users.yml` +2. Run `playbooks/add-bastion-users.yml` OR `site.yml` + +#### Update ansible inventory + +Gather following user info: +``` + : + comment: ... + primary_group: + groups: + - #if applicable + public_keys: + - #ask user for this + uid: + yubikey: + aes_key: + private_id: + public_id: + serial_number: +``` +Determine username and confirm it doesn't exist on either bastion. +The uid can be determined by running this one-liner on both bastions (disregard the 65534 uid): +``` +$ awk -F":" '{ print $3 }' /etc/passwd | sort -n | tail +1113 +1114 +1115 +1116 +1117 +1118 +1119 +1120 +1121 +65534 +``` +In this example, 1121 is the highest uid in use so the new user will be 1122. + +Update `users` and `user_groups` as applicable. Because of the sensitivity of the +data in the `bastion-users.yml`, it is encrypted by [Ansible Vault](http://docs.ansible.com/ansible/playbooks_vault.html). +``` +ansible-vault edit bastion-users.yml + +``` +For information on diffing encrypted files, please see [here](https://github.com/IBM/cuttle-envs#encrypted-files). + +#### Run playbooks/add-bastion-users.yml OR site.yml + +This must be done against all datacenters with Bastion hosts. The most efficient way is to use the `add-bastion-users` playbook: +``` +ursula ../sitecontroller-envs/control-dc01 playbooks/add-bastion-users.yml --ask-vault-pass --ask-su-pass -e "@../inventory/bastion-users.yml" -e "usernames=bobsmith,pdiddy" +``` +If you decide you want to run `site.yml` here are some options: +``` +# if only updating users +ursula ../sitecontroller-envs/control-dc01 site.yml --limit=bastion -e "@../inventory/bastion-users.yml" --ask-vault-pass --ask-su-pass --skip-tags support-tools,ssh-agent + + + +# if updating both users and groups +ursula ../sitecontroller-envs/control-dc01 site.yml --limit=bastion -e "@../inventory/bastion-users.yml" --ask-vault-pass --ask-su-pass --skip-tags support-tools +... +``` + +## Delete Bastion Users + +There are also two steps for deleting Bastion users: + +1. Update `bastion-users.yml` +2. Run `playbooks/delete-bastion-users.yml` + +``` +ursula ../sitecontroller-envs/control-dc01 playbooks/delete-bastion-users.yml --ask-su-pass -e "username=bobsmith" +``` diff --git a/docs/contract_elk_cluster.md b/docs/contract_elk_cluster.md new file mode 100644 index 0000000..44007d7 --- /dev/null +++ b/docs/contract_elk_cluster.md @@ -0,0 +1,31 @@ +# Contract ELK Cluster + +## Migrate Shards Off Nodes + +1. To reallocate all shards off the elk nodes you want to decommission, ssh onto any one of them, then run this command: + ``` + curl -XPUT localhost:9200/_cluster/settings -d '{ + "transient" :{ + "cluster.routing.allocation.exclude._ip": ",,...", + "cluster.routing.allocation.cluster_concurrent_rebalance": "25" + } + }';echo + ``` + +2. You **SHOULD NOT CONTINUE** unless all shards are migrated. + + +## Stop Services + +1. **NOTE:** If one of the nodes you want to decommission is a **master node** of the ELK cluster, **refrain from shutting down** that node until you have stopped all others, if any. This is to ensure that none of the nodes that you are decommissioning will be elected as the new master node, possibly reallocating shards back onto this node. + +2. **In this order**, stop these services on the non-master nodes, followed finally by the master node: + - Logstash + - Kibana + - Elasticsearch + + + +## Cancel Nodes + +1. Once you have [Migrated All Shards Off the Node(s)](https://github.com/IBM/cuttle/blob/master/docs/contract_elk_cluster.md#migrate-shards-off-nodes) and you have [Stopped ELK Services](https://github.com/IBM/cuttle/blob/master/docs/contract_elk_cluster.md#stop-services) diff --git a/docs/deploy_sc.md b/docs/deploy_sc.md new file mode 100644 index 0000000..a256c4c --- /dev/null +++ b/docs/deploy_sc.md @@ -0,0 +1,192 @@ +# Deploying a Site Controller + +## Table of Contents + +* [Get Hosts](#order-hosts) +* [Prepare template-vars.yml & ansible-inventory](#prepare-template-varsyml-and-ansible-inventory) +* [Prepare Bastion](#prepare-bastion) +* [Special Considerations for CDL](#special-considerations-for-cdl) +* [Run CIMC validation](#run-cimc-validation-local-and-cisco-bom-only) +* [Converge Bootstrapper](#converge-bootstrapper) +* [PXE SC nodes (Local ONLY)](#pxe-sc-nodes-local-only) +* [Ensure SC nodes came up after PXE boot (Local ONLY)](#ensure-sc-nodes-came-up-after-pxe-boot-local-only) +* [Bootstrap other SC nodes (Local ONLY)](#bootstrap-other-sc-nodes-local-only) +* [Converge Site Controller](#converge-site-controller) +* [Converge Control (Dedicated and Local)](#converge-control-dedicated-and-local) +* [Run Flotsam](#run-flotsam) +* [PXE OpenStack nodes (Local ONLY)](#pxe-openstack-nodes-local-only) +* [Ensure OpenStack nodes came up after PXE boot (Local ONLY)](#ensure-openstack-nodes-came-up-after-pxe-boot-local-only) +* [Remove PXE files from Bootstrapper (Local ONLY)](#remove-pxe-files-from-bootstrapper-local-only) +* [Validate deployment](#validate-deployment) +* [Troubleshooting](#troubleshooting) +* [Legacy PureApp: Bootstrap VPN Node from Localhost](#legacy-pureapp-bootstrap-vpn-node-from-localhost) + +## Get Hosts + +Somehow you need to get a bunch of servers. do that. + +## Prepare template-vars.yml and ansible-inventory + +Create environment and variables in ansible-inventory repo using [Site Controller Generator](https://github.com/IBM/cuttle-generator). + +Update the following in `central.yml`: + 1. sensu.dashboard.datacenters + 2. grafana.remote_graphite.datasources + 3. openid_proxy.remote_locations + +## 1 to N Site Controller (Local ONLY) + +Special considerations need to be made for a Local Site Controller that is used to support >1 OpenStack, as +sc-gen was written to support a 1:1 relationship between SC and OpenStack. After running sc-gen with the workbook, +three items need to be added to the respective environment: + 1. `pxe.servers` + 2. `pxe.dhcp_ranges` + 3. Static route in `host_vars/bootstrap01.yml`. The static route can be added manually on the machine but needs to be reflected in the env. The subnet is for the PXE network. For example: `ip route add 10.10.36.128/27 via 10.10.0.129 dev bond0`. + +## Bootstrap nodes + +Local (bootstrap01 ONLY): +``` +cd ../ursula-flotsam + +# trust host (you may need to change user) +ursula ../ansible-inventory/remote-$DC playbooks.keyprime.yml --limit bootstrap01 + +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=admin' --ask-pass --ask-sudo-pass --sudo --limit bootstrap01 +``` + +Central & Dedicated: +``` +cd ../ursula-flotsam + +# trust hosts +ursula ../ansible-inventory/remote-$DC playbooks.keyprime.yml + +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=root' --ask-pass --limit bootstrap01 -e '{"ansible_ssh_user": "root", "env_vars": {"http_proxy": "", "https_proxy": "", "no_proxy": "", "PERL_LWP_ENV_PROXY": 1}}' +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=root' --ask-pass --limit monitor01 -e '{"ansible_ssh_user": "root", "env_vars": {"http_proxy": "", "https_proxy": "", "no_proxy": "", "PERL_LWP_ENV_PROXY": 1}}' +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=root' --ask-pass --limit elk01 -e '{"ansible_ssh_user": "root", "env_vars": {"http_proxy": "", "https_proxy": "", "no_proxy": "", "PERL_LWP_ENV_PROXY": 1}}' +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=root' --ask-pass --limit elk02 -e '{"ansible_ssh_user": "root", "env_vars": {"http_proxy": "", "https_proxy": "", "no_proxy": "", "PERL_LWP_ENV_PROXY": 1}}' +``` + +## Converge Bootstrapper / Bastion + +Local: +``` +ursula ../ansible-inventory/remote-$DC site.yml --limit bootstrap* -e 'pxe_files=true' +``` + +Dedicated: +``` +ursula ../ansible-inventory/remote-$DC site.yml --limit bootstrap* -e '{"env_vars": {"http_proxy": "", "https_proxy": "", "no_proxy": "", "PERL_LWP_ENV_PROXY": 1}}' +``` + +Central: +``` +ursula ../ansible-inventory/control-$DC site.yml --limit=bastion -e "@../ansible-inventory/bastion-users.yml" --ask-vault-pass --ask-su-pass +``` + +## PXE SC nodes (Local ONLY) + +``` +export IPMISUBNET=10.11.9 +export IPMISTART=12 +export IPMIEND=14 +export IPMIUSER=admin +export IPMIPASS=admin + +for ip in {$IPMISTART..$IPMIEND}; do + echo $IPMISUBNET.$ip + ipmitool -I lanplus -U $IPMIUSER -P $IPMIPASS -e - -H $IPMISUBNET.$ip chassis bootdev pxe + ipmitool -I lanplus -U $IPMIUSER -P $IPMIPASS -e - -H $IPMISUBNET.$ip chassis power reset + echo +done +``` + +If you're having trouble with pxe, run the following command on the bootstrap host to monitor dhcp traffic: +``` +tcpdump -i bond0 -vvv -s 1500 '((port 67 or port 68) and (udp[8:1] = 0x1))' +``` + +## Ensure SC nodes came up after PXE boot (Local ONLY) + +``` +for i in {12..14}; do echo -n "10.11.10.$i "; ping -c 1 10.11.10.$i > /dev/null && echo OK || echo NOT DONE; done +``` + +## Bootstrap other SC nodes (Local ONLY) + +``` +cd ../ursula-flotsam + +# trust hosts (you may need to change user) +ursula ../ansible-inventory/remote-$DC playbooks.keyprime.yml + +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=admin' --ask-pass --limit monitor01 +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=admin' --ask-pass --limit elk01 +ursula ../ansible-inventory/remote-$DC playbooks/bootstrap.yml -e 'ansible_ssh_user=admin' --ask-pass --limit elk02 +``` + +## Converge Site Controller + +``` +cd ../sitecontroller +ursula ../ansible-inventory/remote-$DC site.yml +``` + +## Converge Control (Dedicated and Local) + +``` +cd ../sitecontroller +ursula ../ansible-inventory/control-iad01 site.yml --limit tools --tags uchiwa,openid-proxy +``` + +## PXE OpenStack nodes (Local ONLY) + +``` +export IPMISUBNET=10.11.9 +export IPMISTART=12 +export IPMIEND=14 +export IPMIUSER=admin +export IPMIPASS=admin + +for ip in {$IPMISTART..$IPMIEND}; do + echo $IPMISUBNET.$ip + ipmitool -I lanplus -U $IPMIUSER -P $IPMIPASS -e - -H $IPMISUBNET.$ip chassis bootdev pxe + ipmitool -I lanplus -U $IPMIUSER -P $IPMIPASS -e - -H $IPMISUBNET.$ip chassis power reset + echo +done +``` + +## Ensure OpenStack nodes came up after PXE boot (Local ONLY) + +``` +export IPMISUBNET=10.11.9 +export IPMISTART=12 +export IPMIEND=14 +export IPMIUSER=admin +export IPMIPASS=admin + +for ip in {$IPMISTART..$IPMIEND}; do + echo $IPMISUBNET.$ip + ping -c 1 $IPMISUBNET.$ip > /dev/null && echo OK || echo NOT DONE + echo +done +``` + +## Remove PXE files from Bootstrapper (Local ONLY) + +``` +ursula ../ansible-inventory/remote-$DC playbooks/pxe-config.yml -e 'pxe_files=false' +``` + +## Validate deployment + +See [Validating a Remote Site Controller](https://github.com/IBM/cuttle/blob/master/docs/post_deploy_validation.md). + +## Troubleshooting + +Lost connection? You'll likely have problems with ssh agent forwarding. +``` +fixssh +ssh-add -l +``` diff --git a/docs/disk_replacement.md b/docs/disk_replacement.md new file mode 100644 index 0000000..ff95972 --- /dev/null +++ b/docs/disk_replacement.md @@ -0,0 +1,132 @@ +# Disk Replacement + +### MegaCLI Cheatsheets + +1. http://erikimh.com/megacli-cheatsheet/ +2. https://things.maths.cam.ac.uk/computing/docs/public/megacli_raid_lsi.html + +### Disable Shard allocation (BOMv2 ELK Hosts ONLY) +``` +curl -XPUT localhost:9200/_cluster/settings -d '{ + "transient" : { + "cluster.routing.allocation.enable": "none" + } +}' +``` + +### Shutdown services (BOMv2 ELK Hosts ONLY) +``` +sudo su - +service apache2 stop +service kibana stop +service logstash stop +service elasticsearch stop +``` + +### Find the bad disk +``` +megacli -PDList -aALL | less -> Media & Other Error Count > 0 +megacli -ldinfo -Lall -aall | less -> Bad Blocks Exist = Yes +``` +Note the `Enclosure Device ID` & `Slot Number` for later. For example: +``` +Enclosure Device ID: 9 –> E +Slot Number: 2 –> S +``` + +### Map the physical disk to the logical disk +``` +lshw -class disk +``` +You should see something like `physical id: 2.2.0` or `physical id: 2.3.0`. +These correspond to slot number 2 and 3, respectively. Use the slot number +from above to find the correct disk, then note the logical name. For example, +`logical name: /dev/sdc` (Note for later: c -> Z). Once your know this, run `lsblk`. +``` +lsblk + +sdc 8:32 0 1.8T 0 disk +└─sdc1 8:33 0 1.8T 0 part + └─vgdata2-lvdata2 (dm-1) 252:1 0 1.8T 0 lvm + └─luks-vgdata2-lvdata2 (dm-7) 252:7 0 1.8T 0 crypt /data2 +``` +Save the number next to `vgdata` and `lvdata`: 2 -> X. + +### Unmount the partitions from the failed disk +``` +umount /dev/mapper/vgdataX-lvdataX +``` + +### Remove the disk from the device mapper +``` +dmsetup remove /dev/vgdataX/lvdataX +``` + +### Mark the disk as offline +``` +megacli -pdoffline -physdrv[E:S] -a0 +``` + +### Mark the drive as missing +``` +megacli -pdmarkmissing -physdrv[E:S] -a0 +``` + +### Prepare the device for removal +``` +megacli -PdPrpRmv -PhysDrv[E:S] -a0 +``` + +### Have SL replace the disk during their next maintenance window + + +### Clear the new disk +Some combo of the following commands: +``` +megacli -PDClear -Start -PhysDrv [E:S] -a0 +megacli -PDClear -ShowProg -PhysDrv [E:S] -a0 +# wait for clear to finish, but if you need to shortcut: + megacli -PDClear -Stop -PhysDrv [E:S] -a0 +megacli -CfgForeign -Scan -a0 +``` + +### Create new Virtual Disk and replace the missing drive +Some combo of the following commands: +``` +# Create a new RAID0 using the disk in Enclosure E, Slot S. +megacli -CfgLdAdd -r0[E:S] -a0 +# If the virtual disk doesn't initiate a rebuild automatically, the following command may be needed: +megacli -PdReplaceMissing -PhysDrv[E:S] -Array0 -row0 -a0 +megacli -PDMakeGood -PhysDrv [E:S] -a0 +megacli -PDOnline -PhysDrv [E:S] -a0 +``` + +### Repartition disk and remount using flotsam (BOMv2 ELK Hosts ONLY) +Use Z from above: +``` +ursula ../sitecontroller-envs/remote-dc01 playbooks/bootstrap.yml --tags purge_disks -e '{ "disk_list": [ "/dev/sdZ1","sdZ"] }' --limit elk07 +``` + +### Recreate the LVM disks and remount the disk (BOMv2 ELK Hosts ONLY) +This also starts elasticsearch and creates any missing directories in the new LVM disk. + +**NOTE: This must be run against all ELK hosts in the cluster or the generated Elasticsearch config is wrong.** +``` +ursula ../sitecontroller-envs/remote-dc01 site.yml -t manage-disks,elasticsearch --limit elk +``` + +### Start services (BOMv2 ELK Hosts ONLY) +``` +service logstash stop +service kibana stop +service apache2 stop +``` + +### Re-Enable shard allocation (BOMv2 ELK Hosts ONLY) +``` +curl -XPUT localhost:9200/_cluster/settings -d '{ + "transient" : { + "cluster.routing.allocation.enable": "all" + } +}' +``` diff --git a/docs/elk_shard_recovery.md b/docs/elk_shard_recovery.md new file mode 100644 index 0000000..d2d04b1 --- /dev/null +++ b/docs/elk_shard_recovery.md @@ -0,0 +1,34 @@ +# ELK Shard Recovery + +Try the following two commands if ES cluster status is red and there are +unallocated shards. **Be sure to fill in `node` in the first command.** + +``` +for index in $(curl -XGET http://localhost:9200/_cat/indices | awk '{print $3}'); do +for shard in $(curl -XGET http://localhost:9200/_cat/shards/$index | grep UNASSIGNED | awk '{print $2}'); do + curl -XPOST 'localhost:9200/_cluster/reroute' -d "{ + \"commands\" : [ { + \"allocate\" : { + \"index\" : \"$index\", + \"shard\" : $shard, + \"node\" : \"\", + \"allow_primary\" : true + } + } + ] + }" + sleep 5 +done +done +``` + +``` +curl -XPUT localhost:9200/_cluster/settings -d '{ +"persistent" : { +"cluster.routing.allocation.enable": "ALL", +"cluster.routing.allocation.node_concurrent_recoveries" : "25", +"indices.recovery.max_bytes_per_sec": "500mb", +"indices.recovery.concurrent_streams": 8 +} +}' +``` diff --git a/docs/flapjack.md b/docs/flapjack.md new file mode 100644 index 0000000..c7ab954 --- /dev/null +++ b/docs/flapjack.md @@ -0,0 +1,118 @@ +# Monitoring and alerting with Flapjack + +Flapjack is a event processor that takes messages from multiple sources, +then filters and relays messages to PagerDuty, email or SMS. + +For Blue Box Cloud, Sensu clients on OpenStack nodes and site controller +nodes send messages to the Sensu server in the datacenter it is deployed +in. The Sensu server sends messages to a Flapjack server in the central +Site Controller, which applies rules to determine if, and where, to send +an alert. + +```text + OpenStack or SC node +┌───────────────────────────┐ +│ Sensu Client │ +└───────────┬───────────────┘ + │ + │ Dedicated or Local SC monitor + │ ┌─────────────────────┐ + └────▶ Sensu Server │ + └────╦────────────────┘ + ║ +Sensu Flapjack handler║ Central controller + (HTTP req over VPN) ║ ┌──────────────┐ + ╚════════▶ │ + ╔════════▶ Flapjack ├────────▶ PagerDuty, email, or SMS + ║ ╔══════▶ │ + ║ ║ └──────────────┘ + ║ ║ + ║ ║ + Other SC Sensu servers + +``` + +Sensu client checks are created during Ursula playbook runs with the `sensu_check` +Ansible module. A custom field, `service_owner`, a recipient for the check, +can optionally be added with the module. This field is only used by the +[`flapjack_http`][fh] Sensu handler; if omitted, it uses the value `'default'`. +When processing an event, the handler adds a tag `service_owner:value` before sending +to Flapjack. + +[fh]: https://github.com/IBM/cuttle/blob/master/roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack_http.rb + +In Flapjack, [notification rules][n] are associated with contacts. These notification rules +can match on tags. When an event is received, contacts with matching rules will be notified. +On the central site controller monitoring node, a simple helper script `flapjackadm` allows +you to see how the notification rules are set up. Use `flapjackadm contact list` to see +contacts, and `flapjackadmin contact show --id [ID]` to view notification rules for that +contact. Currently, both `service_owner:default` and `service_owner:openstack` are routed to OpenStack team in PagerDuty. + +[n]: http://flapjack.io/docs/1.0/usage/Notification-Routing/ + +# Add a new contact + +``` +# ssh to monitor01 host in control-* +ssh -F ... + +# list current contacts +flapjackadm contact list + +# add new contact +flapjackadm contact add --name 'nope Ops' --email 'nope@example.com' --tags 'service_owner:nope' + +# find id of new contact +flapjackadm contact list + +# use id of new contact to show it +flapjackadm contact show --id e6c8b9ee-255b-4591-cfc4-71a84691a4b3 + +# edit default notification rule to disable all alerts +flapjackadm rule edit --rule-id cc1cb568-4f4f-438a-ac12-1cfb95fea889 --rule-warning-media '-' --rule-critical-media '-' + +# add new contact method of pagerduty +flapjackadm pagerduty add --id e6c8b9ee-255b-4591-cfc4-71a84691a4b3 --pd-key XXXXXXXX --pd-subdomain example --pd-token XXXXXX + +# add notifiction rule for pagerduty matching on the tags 'service_owner:cleversafe' but DO NOT alert on WARNING alerts +flapjackadm rule add --id e6c8b9ee-255b-4591-cfc4-71a84691a4b3 --rule-tags 'service_owner:nope' --rule-critical-media pagerduty --rule-warning-blackhole + +# modify following script to use new contact id, adds the magic ALL entity. +ruby add-flapjack-ALL-entity-to-contact-id.rb + +cat add-flapjack-ALL-entity-to-contact-id.rb +#!/usr/bin/env ruby +require 'flapjack-diner' +Flapjack::Diner.base_uri('127.0.0.1:3081') + +entity_all_data = { + :id => 'ALL', + :name => 'ALL' +} + +ada_data = { + :id => 'e6c8b9ee-255b-4591-cfc4-71a84691a4b3' +} + +entity_all = Flapjack::Diner.entities(entity_all_data[:id]).first +unless entity_all[:links][:contacts].include?(ada_data[:id]) + puts "Adding Contact ID #{ada_data[:id]} to the ALL entity" + Flapjack::Diner.update_entities(entity_all_data[:id], :add_contact => ada_data[:id]) +end +``` + +# Update a contact's notification_rules + +``` +# ssh to monitor01 host in control-* +ssh -F ... + +# list current contacts and find id of desired contact to modify +flapjackadm contact list + +# use id of new contact to show it +flapjackadm contact show --id e6c8b9ee-255b-4591-cfc4-71a84691a4b3 + +# add notifiction rule for email matching on the tag 'service_owner:openstack' AND tag 'ticket' but DO NOT email on CRITICAL alerts +flapjackadm rule add --id e6c8b9ee-255b-4591-cfc4-71a84691a4b3 --rule-tags 'service_owner:openstack,ticket' --rule-warning-media email --rule-critical-blackhole +``` diff --git a/docs/images/1.png b/docs/images/1.png new file mode 100644 index 0000000000000000000000000000000000000000..661373e874d3722b66ad354fa369ba20b9cd1a69 GIT binary patch literal 19354 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 literal 0 HcmV?d00001 diff --git a/docs/images/2.png b/docs/images/2.png new file mode 100644 index 0000000000000000000000000000000000000000..7515025c7e4ed36dd0eab300064b7976a3f4c26e GIT binary patch literal 31590 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 F{vQogp3DFM literal 0 HcmV?d00001 diff --git a/docs/images/3.png b/docs/images/3.png new file mode 100644 index 0000000000000000000000000000000000000000..f8d3b2186fe3e6ad763b7dc76cb75748181a81b9 GIT binary patch literal 25786 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 RW&L>Ir>(B9R;*$Z@;~pJ%f|o! literal 0 HcmV?d00001 diff --git a/docs/images/4.png b/docs/images/4.png new file mode 100644 index 0000000000000000000000000000000000000000..4792f6b95f101f44c16f4f445632f3d5027a35a6 GIT binary patch literal 35035 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%0AjPzvNeSeqAS&R@NCNdoc$*|HC|7&}WCRu^2V3 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 literal 0 HcmV?d00001 diff --git a/docs/images/5.png b/docs/images/5.png new file mode 100644 index 0000000000000000000000000000000000000000..bbf37a9a7029c772891d8a36372f58d770520d3d GIT binary patch literal 79904 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 literal 0 HcmV?d00001 diff --git a/docs/images/grafana_sla_dashboard.png b/docs/images/grafana_sla_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..88890471bc8f75bfab76214c277acaad47273372 GIT binary patch literal 574801 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Ƴ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

ja7!Qhse@Y*{tkrnb-?kO5% zg$NDokN7ODB?;{= z0=f|%GA&%a1^pgF{!7X%cwC57Z^4I+wXxG;(B63ZK+y7|VyzKY0mHomOG!~aaVSsJ zcU<9P2s7~_MXUNUpnpDB9eWoxF}c4_kP<)f)6&O~q(NA05lP_OmP}<(tI zm(5Wt%-lwga<1mX5;B#ohZNsx`kyC=Dju1fj=UT_ zF!*R{0U>t_rm;ZcuDH)WkouB~B8i({eQPV#7&|3O6bd{dv92M#_4&rsQ#Xm+sJ}nx|NL7ye=PUyw4vkxJ~#vBTTJ}z82Or+Hsih| zKzA6A3UgKfaWWNk6l~wljK}4kG1C+!#5ps0iL7NNT>xz*mFUW^%yL!B3P}1wlu%VC zS9abwqF?*eh2+bGXlBE=nHg>I0cVmwK33=<$t^-*VjqMHRZhl_;&4*7CXSzVaf;f7 zQn<(jaHshWu)OD80BI;)ZF>8u$ zFwRAe;DU2;`1tZ}Ro20|^dAM4*aqI|r?W9MwUuNguMDH&>V^)Vsfw({dV?`XTg3vUVlA_) zG9n6_1*zGjd!+&SfMVb?V#O(F->I_pu4iz$V`pJrm#j&A5JXA_8{oYvtg3PxuK{08 zrc(k*iPw}omm6RMLN;Xp-X<4@c3|#MCT^BNCa+!E-G#7Cgxw$G@jPng(a|K7+5i2bFR45plc>Q!jR}Ot-Y@az)fwJI^kN2h;RHpH`>E z`9y1@2A+bL2mEPW6WE-oSmPHsqcNCz&L>0xdO9hv@+5s?_^d~Xf9+H&aki{B z|0Qtwg{J(8&Hc}1Nh4i8nE@ls{QB-T(H0&_8d7|khF`4ReRaFCQ^vRy=lKfK8j0p{ ztX-t;#hqR0`j2Y`>%|*aUtV5Im8%0~h8?n>25GWzwON30q-nk-Tmgq|`CXh4tU)5hUaRS(9zkL4|fPEi}ja!{zs%#p$#)RvGGqQfU#u2vT*i$P%hyWH;@7?eh= zdS6$Lw?JF5&dg&4b65jn_SvP~a&^76^~k`~5_zL<8`F-uJ#E)#;ns}0thGr*KsvT( zfh{Kp!~H$Y?;`-GGdN*U<1uk>oqW7UHOOlRj;NTyFfzlm$6xi{7+8FoPkN!mlMS67Rx%bY&EnBM_U}rs^Sf&f?Pf+ z`+|P9DmFgw`TW&By}!yhKz#b`jFPA`*-NG_iqk z{*!OH{TqU)W z2d7EYE|)ZtF4?h|bd4;Duo`QR@e2hCHXXc8LWT$URLDUSV2eI-BfHCjzkvU;NwoNa z)2jaIaRs=GpADnKKMV8!dDZ%XXdQMdw@`Y6zwY0^+w7GL$#9x4jSx&Dxd4>h6eaVkv?J~RQXD#AyoQ)QF|&&VVD(rZ%uVnj z20WIgCtgrMV*NX$e5Ig-AYl3*Ho7WCRN@j0x@ODlB4S7)Q~D-v4wV8IxhPWtmWLTs zEr{|$`K~fRiW}^IrJs5#-`!N}EbgJfzSu!{NJ)u!P5Z(;@3%ikpG`FG+;T&r3^y)| z{Q69P4O0K@gA9xCvvclj975tpAXbFQ=iLPxF(}{!ncrp!0bJ+|yFjWhX9-|$%{7N~ zCR;GC8&fmjuy$=#Vs&3^M<$~?yAG>}_ocNlTT)>qt=NtGwt%fL2CirQvUZ_azuy5} zQ6cxL4?bCQDRABnJ-+a{kbyg7>qbt zpSsca>^kdfV$j)7B6FmxU`i230Vvx~vv^bjk}5s70tt7|l!ip5ubE%WX@^ukOb#(K z$R0Al-vKnm>Kk?mLXKzPnhcid0Ig*%;s>*#U}hB%M`u z#u=!#`pP~g>h+E%O%K#xCso4V1tawpKupiO&(}$^81KMcyL#+>-9rd_SbPTL!3&Uo_g>^$~=v!Q;Mm5xMF)L3U^2N*lb!^ymne}wiLzv zBk|&^vr}#Mg|Rf28Mj+BFMo+tOT|yeV1!WNs?|A>e0PN%Cl|C5)F`(pMIhHVNvjQ6 zNobsK6gEhVyvQf9nIlR&j@Op=6A9ZQIeVrL_S!4ZqgXhhKMk6f`&BsZAPCuUW{Afp zY8#2eSXZ{Q^_1cZ3S{iL>8->5w>W$5+fHyXFBAP{5CYKE>*4SM81BhPgudvyQ*KI< ziP!<;KLO^r_c?9(=ck8bKN5RKwq8YUt`-hWv<0)wQm-ldjGIP zPU8~>Wh98h^rM|oqsXkpDHZCfaWGaJVk9op>e!?m8@5dc<+LhCScaDupV>?HqXby z+L;%wCRtw>1K8YK=QiXpi_pSxp#=~E#<|+9=0DOWjA+`DGFL$L^gCzR^CMjoK>y}# zSI4=CmJ_kL25U(uISLG`$$z0C@P@ zJsZNc{O_Uo|k>#7Qme1u_ib3!?-vo-@q)Zh{MS-@y?SB;TG(bGLp5 z;JrM^zC3&>;lgkSysoB&$`+p^#oB4rAc?Nj??fIi|6tH+yhl|Ph@gTRz>ZK|y(N(Q z)+QauNk9LQep{P@DSWSr0bBmQJOvZGD6abH=LD!;ZmoIR&#zV# zT8_>BK5t%s!|WpA1oW+a9{W+Uti@4$B)LgDibLE;XR3i+Ci0%#SLT+R{H0d$*{KVR zyJYSwjfH9V!oqotruK&?@_U422fey&t&4lEvuOboC*OkJR=_f|K9uZBoie%G z*f8NzmQ7}Pn+fUYCMG{Iro1v89g3~Y8FN6^)Q+2ML9C5ByFy)WHe89N`Pp9;fq02w zAUWEA=621D6Ug!$^_!qf_juL~V$eQkDtP-fB0S*eMJc5}27l|#*6w7@9&rLxu;`@V zD(Ha3g3P*E)m>eV6Z0Xtm>kt2SB~0S}jaxbuuYlbI zLL`EcRoA^&Rdi6!c`Jj&%+?DN+-KZ*7EPQVUn#Pk-8Idxl`i9B z$r~SEo=Gb5-B>RIG~n2%77H&%gTj1N3gWqDVD4JtE$H=ePxl6$9kB-FM$$tOWaa^S z)u}f6AJ~gO59p602(cc%1v_AWT@_45=FZPwI*{6@Ui;dH^2$S+y?9=T^8OpQ1*^g3 zRi#1@(!Ru3fa0sSgoEt9pLC(j%za_rL@x0><{iref%F<{5$Amz4`^?iFZ+CuE|syn zSR_BsofK}TiW458iC{}}c*J>#aX|fxvSL*0u#k~mNJGfy^hq8B)4t7+gSttk{Be&< zU5OAin`@?K0PP+#h~$#XSRb$g_FRqi;mCwbW16)NK}NB?P8%?pG0Kz0q`;D(z2m)N zA_?tIjtS6ufZOm?^X=o2WA6f7O#+1w+TR*L(Rrv7i1@Wk3~5rRFHV)Y-YudPD5=TG zU@=7sQj@7-2)jJiy2XikD`jyBj{G%yEXFbo%1C?-##f9Oa$BdSJEPjIsZa(WG?*Q} zC>})fZe{h*N=-sKQ{7(&kdl^ZobUPw4|RPk!;+JB3H!It(VuWbo1C2Z4-}22eNaoB zcdQ_UQ>K^Cns@7WfF8%3U5SDUG>~WxcD|f4(vTHg=vSBRKxyp?B|Sa-#UHL9{~CKA zF8M-4ymp?p_~?}R9O$K9Hyt=j!bXh#=C9sJey58E$;%vXx>;|c)Wwiv3bj0 zXB+G}Udz%WW)1u?7S;+WH>QFOj6XQHzU!HAi|;mvFAw7frh`Sq8Rg<>!Wqfw_HtQiI zZ%m{m>I1*&J%|XkRykL*ZrP&EzdnzljjfPqZV$E=!|c<0L$EnswSSEdfA^EW7xP{S z&CQtsA#wAY!snz~?j(Ew2-@E{wmXk_!3kASs|C}Fs*&SQP=EOLw7%m|K1iTBtD5Yp zD!fd@*Z6GW@C_3SAP5n5DfoCe$%#x_4t)30Ab$+2diGX$>oC})_PCuBDU&(4uy3Vx zIi>E#EZ^QgoM_wwG#(On%0A%B#3*(C&_QGlwM(~jy&AKwkG*l!EL8(qt#1Z~6E$h) zqXOKSLA~4`NW{UR7UoB|D9FQ#;b6jLhgieTyA1F|$!C_AC6FiGossrrl0je}^nRIj z1xW13Wcu@+;xG@A=6|6qD{Pft>C+F1h1S%?p7)AUX|G8W2T2Mj0EZ2PwyoH268C4JO8G zNF_M&;^gH*RFMn8$3>Y(hb!;Y05LHQ7N@Uab3nJ6K~D4iiUzJ` zXGAAKL5{@-&sHBRp46iacS)rN8`6tTnd9HmyHoo)t#K*cPY_W$HTVfZNkLg+GV zF0{)J&J@{!31l4%LAZx`F$|;5EAOLTO1+YYU13{Q9LcK_><}Mw8>SEc@e&D|N$_+m zL3Ottl88d9v{0q&bZ#B?bJ3`CDmtot+aXfFX0j`1_JxX38a!gXN7drTyT&2FE)NCq z!_47W_WjB!I2xv?_MD|0AT4nPzXQv~?XG_%`_yD^-SO8^_4hyhI0oJt$?9E_8=Y@s z+b$1*q+y=DO8Nd?Gvuj2UX9bIv6Ta$!R7ShkZ?%CrE4U&%#OmgrNZ*Sg{iz3%nlR+&oU@t%OswX}fNfjUL$sU~ z`8T;!SjHw6HpdF&b5qTR_UOxB$?*J4F7o@t9Rw$?iST~cy^z|cgvo0rxEmq zQL+rWZe?SuiCU|0(%1mzhKvwL#0;Q*26;cO818*Lv0x^<^2)#zuCSe9I}gr%^nTjy ziC%=iGgAqSD#%v^63MJ?-ZijH*w^COuiiWkRQ7!&i??UQs4Oac6lf+7?GDOI zY5)AJjvbt+A{Y-VgQG$>KAvGH=N0m*{q0yc2*?wc67TYdn+p8;4E@tVK%2hp-)`T( zd!*$!X(34V1=z=I;9GNWF7{0)vWMPF^lQq(OLqOK4ClJJ6^c8oA;&HM>&P36vKpW( z`@lV4(cql2QF#Km$5o)vr-6bhOmOYxDX(|0-ho|!2w3F%K}y$tNcQCn9Doq6dqYp2 zU;OJOodQHKR_g@uA94;w3PYeDFGP9sP>9NcT=d0P&MVn>!e@^12qtlf=llh~qKhi#w3D6k%IIalz&8&KpSS zjuc6W$96QHxxV-8&+*Q53Ii^?=ZmyIEPey4Unay8H8{_pnsGE8*uciqR48PdZSy;A zy~}2qXjB1`kJjvN+`%rSo!e19Dk)0)OS{l!3=*<>66QT(4E#?0Fz5EAk~p+ zt5G{?O>#~B;Sy{hKb|NV!)fIrL+#R#9Hf`e<3+9V4ZURny1EXQS~zEjJ>cOA6YFaV z{39VSY$pvWO$C!dcck&4Hi)n};Z~#bRh;k_L*>#zCX2cEP(OrS<)RLx3)o49<%aNJ zE8v87rQmdXz|T63Gh*?ggf`8%@v<1VuR6Wx{H4YF>v{a&KgqFS z_XE&uT&Ydjp&Y^%5$GA_8JEeMj4C0(GnuBGw8-dTQz{r^Plx}GxbjB zo=+JCEt4&=0iMQu6x|b3WfDJtY;@i?=gVr-YX^S@ntP>$S=_LP`+GiV3Yurmbr+WS z?(O#dc~=;+LUKpyX_aT7TsA1MF`7~bCPJ|rN86^TupkbyZSQOPOAtt&1#cp{^K!i21#bCnf=`6ULFGA^7I%O}9APqPrZ4YKP(UH$vG6?N;GTeX*ns zv=v93WjZN5#9}-jn+%;%XnpMq5g&-|BJ*}MB!c~X5sIKu00gQfP9D$^`@>7^@64!C z^V1j&PtQu7D0pDIUAF3c!FBfDC1oFlG|5IWdJ>c1qMB4O;u)IS;=EIHzq< zLmdj=fU2vg9-I4uPia6jmkexzPv^6Xtn4Sv?{hf5J&4*dJ)yp-qSpFgzl|8}ulFG- zY{%o{zxTZ`|BF8U+n-)tFa>YfSH@4kI#?m2BkF34#9r!o zYKPXT_VG@1M(}q%9>nBT6>w)6z_5YxyY4Dw2$P7=FIEOkd9NYi%^KocZcd9e#hiRo zP?akPQp=@9l!%>I@d<_8xVv0dMI_K`0de;y@fDnq7lp=19Z0XI%ZGf^0kqJ~!nkFz z^8T1QlQ{~CM<9bMsJ>UyQQ}KA@DQjaDrQZg{j1io{7=M9=>eaB-xaz{wn~72jl>c~;9dUA9$FT7aKymNP zL3|fqwhe0w{6OyE2NJb8MqeNtS4gsfkom1@^MFwG)>FyI7;&(*^&E z(TXP70hy4Sy|gL+sK&V=bjsY`2bY;Q0zPekO^~f#j}bBp0Mo7-bebpjz$k!5J!gjg z@tjR21sRf^fX`n8|3Wd0{asz;3zw0-7NDctCHV;5I1#IlFZ@AR--8sI5Kh0XAf)WB zOlkd@DI89SBM3mdX7Z_eC%9Sf)+6aBgALU1J#bxURqlhYG)lYv!fSvDUEKGu(JY9= zJg4H@g%sY$3Mn9Z`1TcH%0U_|k2SQe3zb@OHXhLD9e{L=g`rQ-USb2eSxcrfxn^}L z$wnL-U>M*(-ozI<5j`>g%xXKZkTRZ2Diw*+Nl9ddik1CdU=!mEL=gRf1|eJCqh|Vx ziIsU+I?$0E0O$Y+|CVj=`)mT&Qp*RGq}_O+{wIlThjXlV&{-Y29h znt=4lTuWkqOVOUeL$wZZ$ktKot@c3;0*Pf0Zd`6PJ2y)q1$%z90J1hnk@!AnKc7zQ zgJ@QBy~hgK*dku(%$kf|c$O`r3?1)oH)Ro9hZU(JB@YC6Yh=ePIygxkwZ4O=C~yh< zCT@3dIHyFQQE94>U|UC>HBVM+Rfa^%cxjW8vBg@OuW77F`)6Z+3|M*dDJn}yN z)Q=}U{dJY|GA1*rfdhgZ8*sib>Hnbh-B3;xoC%G!x)o^ReYAsAK??u3{c2+fahQg=!3g*iuFvZ z`!fNX)ZmwvzPFsHslA&{9>}kO15k#ETdMiC|`c^w1+`H`;`m%}U4R6?~ z^}&Z%DQ2hs(E}?tw3YVLyNHImJNQB`=)ji#zK5^IE#T(iSGv{qgR)S$7!%}0(Tx3| zXd8u3@qlmhi1%h=+t^s*T&hAAwh^Of(iBz6W_;@sOxd*hcNdgAyz^wg(^S(u#53T+ zt%aDcX|HN$?z*jXtBd&xCF+&S+QS@-31i|(3pF9KxV4pme7VhBKy4umDfCYQmt%>7 z$;b#W(Bc9OrnhIJ9!}A-o~zwL5Cz4>2RCs{;o)T?RYP=6*<#GPOex=RicwX}Id9mo z_rSs0eqickvW_k$m~x-Yl}nS&hLy5R|19Rny+1SYpy;P;lWN&-N9k5LtlE9`$ahL(Koy7qntgltb;T`wqvq>&qx1lezILlCIMS7u?T>5s?Glk7;`1$+$ z(cO{OYNqAgWAZ{1GYj>Z90DnFwwb&3(NU)GSr8|jEsldwy*{6~v2Y%Lkv>o%_k~81L}yF$S-`Yy0MZxpU0-w9Loi}-(k4rs*yK#MuNp$Xy*TGi}cVc`U{b91D57Y#_h8aC%Y(S7-N$-%_{T;^5vF zMT^o0!%dC*Az->C5ihmR`#mh43nzI5F@1}d?-ewSziIyZuk6V%)ax|^N|E{CpvT}} z>HA_2YU=78_w=V+)RLW|0UnNC&q@wX9f!M{e5~MIbUP)T?G&uFAE!|7(V=6BVQ&ys zjG}A=ZBBxR7ReW>0iu%IDFm$rds-uDrQ!ae{ZqH6&mGmvX!`2npNO(eQ0xt0f_XrL z^=!;If{Xh5YLf4P3>V$8?<>nJf1Y3nFZ;y!_Q`%evww5B{r!0L=2)1Z3A$XHGf_P) z=nUK&(yP)rsDy7LeHVH!OYVq*N}1z5H!qlc)O9G3^QX~))U&$I9KWafqLiIK0B{P* z;X4C~J`zaGG!0fTy0k=S>{{2$(t9`p!8YIa9LAiR>dLW4J0O_zQC0UL3)%y3u%9vX z#1(}qg);1MsIsby`b{CrBSkKgO`+UnYScs$X&q3n*xXQntkn5^FQQrwcf~#aygN?W z#4eOBrwMXCEMNq=^8OWb6TJc!T1J*N>=>6QG}u?K+3-ss>x#>S0Fx!;k^)58s|YCJ z<_>P6tFGw1dS%5>$~s%AovJ#ic$7Ypet=!GsyeI9v^8@52 zZWP}ZXmbRmjOdpt0>@LYykZVmwhONb2(a{)xNYU;W4RA&D4ql*G3jOa$s z@z+dzcI#39b#3?;@W|C!_j;Lh^Zoj>zdI)0-loA8p`z}eacF?6P#b#Cl6|z}{_9H> zZZ}TxS;L9^8Y&)TzOz|V@w61Za#!2$->yd)nd8r*WTN8gZG!1N=j7|z?Me_70U^vaa9;O;v3pd^H{vYhiMf2L@qZ zTUYY2y$Km(RsE1(KwKFcfem!$&6Bx<(32y<|4n`<`*k`@<1*i#9hTK8xpew)?~?(8 ztPvPm797Yl@3Df&e>vzxxFU7)i&wW~=gz|SLm}DG5_!8#> z0f@~lrjqh>S-VEP_?(mdsvH1(y{@cqz zj;pFhw99bBL`7HeSyg71AepZwZBOfcVUSHs1f>X$9UF_5_4fFF#6~n`vY`i#c^Mlf zydOnlPG~RQuZ{JSl=HYLzE(5Z8DOlUYxTSBCEg{T_7$>r%P}VdLf_Ky8ue@R!nv3Q zJ2dB}bLpQ&^z=+}yF97D=6js9xMG|9LKrKYu7vB;6w;p|Nveq&JnOt8qTLt1q;8jY zb||~E^pwRPfl_31k;ItIfoWg&qGBIq4uLWsw=_b&n$o_pFNb1WStWj?SL9D z{0vjyA8G$wN49o!v-J#-&UTqH!!4osq|EeQlL6%1s%TAFT?i>yDc0MDKSV*v%SFIP zR%-;ZIk;S6Cm6cO?unNI5U+2>`RijyHgq4#hpw@GlZJ|8HIfz-i~&Lu%c4V@;troy zU-|Hu`%*1LAzR?Vm`z!hI+$tpHwXb`=16X3#MGvsL?toR+wtu5IIv4YK2Z1Sk&CNq{r%0I8nUM z8=C-a#fO3VPYwlDF=h+8k$;>IMeLLPt$Xpu+}1j@)Gsf8>8ED&hN8q)hEEyZjsSN+ zbm^>VXY?{&TQfQb7*4xgPQ;Bk2`C!3PnCf$`}EUrwQq|i5EuC~__(eJxi|}LpDk&0 zEBJRn!kAk-ej)19mSrTKXZDcky#ZZRt1B#;8g4qL+1YfBTp}_y--Ky;{WBGU?Kp}i zQ@_gE45$Bw2DF;n1JzK##WzW*OT_zxAaJj48&>lSL*b(YxRwT{_fvZxKH2WyHDYyF zrcF$9#f2dBrE1mAUQ{R7u(7Ia$EH87Q~&!vLHU?~82Ps@02K`iIAs9uFPg1F6)3VRVS4~>EzOy@P=rW9Hu&5Idbdj>15PkX5w z#3)VieTSo86!X-j9plzfL-r%ZcXAe`^(zbHqN) z%AffpE!_FflCF0=b=r)fK}jSB-=TtXlvc?g$-C7Pv=Gl$1xxul$gWzV>94VyCTD`z z9bFadCtyX9dD@$Aq0XSWG)ZYd5^DQ3G|P_g7Xim=_d6hcW*h^JKh;)g#2Yf@^cX?`1O+!odxgA){*# z0*Lp(@cn@UAvZtO`Wt)_p{`|6ZbIZEO2F0wjd5MnEr)y4&RR~5Zc@6HTs>+l(1N}1z7LVefjs<&pJA*X5X8yuurkgl8Z{9OfzeO8D{J4rXkpy|z?MTg#63swd&8v= zG7>9OaXaJ`SD!}WDv>Ci(7>n1NLLvJq!sNGPS?aE7x$Rt3=9Gua(ZA2?~5D4^@Vtu z+=_Yd+Q7fZwE7A`gGKhzklrj6hNn^0e2x0Tzk-ncY`Q<@ zKH&hr^{S8qr)6y%D5W?d;Ohx8&JU8~nl^3)CvTm1@V&AXyXW}$SYqo~QrXabHcc1V z%uen80Kbeci}rgHx}J?{2ZBe zx@o?DgEanpaQ+*j8%qP{{&O%HypjDrw51&KyR^J?neS^&krfDd>Lu%I5REnsAeGuz z@AD7nL0bM};A6TD`R!Mb7I+Hb8lxVX*k8)kHhZ1aD&n}0aDvjrOotgq;}FAkzQ^fY zQ>eyKEi4kYfr`Ny8V%{N)v=&2o0Z*Z5?lA&U0vk}; z8e&(VvwY5!)VWI}U8{xg;3JunMw*E;{CiKgm>-Yk6t$zOQ`wDHMcuqPSRlbjZgXpxVxfd8?d@B*}g?R zq#^lNS4roLOm=3)`MS$?IznS}0lH{?bDVRMa(gZNpm}xn{M;)~f}E()Y;a(}tCi#A z&0@*kT5&l6sSh`|nwEQnB8*xqOr-PNJtTvkO2$#|ldUc$ihKK9BQ{$Lb-aDnl2tn< z67j+9M1LK@TpFE4>)OzDJ{8amy7Ja{sA`WE_ZXVX(r^1fmG2ZB*Z2Z>Le(_z@c*+*TsWXEs8P1$^EDJoE- z=ZCgMqDIn#XRt2>@iUcr>AM||Ds>w3h}N2QVqB9R@b0T{&!1>mZ#|qm@o@ggD=EkI z!!n(oX-k5Y%lIuia&GWRah_GPyi6c1p3L0S4w}oOB7M8ktPRqhc#wi_L&-wV;ikk$ z^X?+sIPo%BAeYx;I#)9W&3>*SS@-`a5|#5{YWWe<#L8kvQDt4&A+SGX?h2rmwcEN7 zh`uFcVrFlT$$zYhT#$OfcFrO6zkGH7dV7EU^fX0I|F?>5Td_?tklZowD1&&0@Z}3g zC!r6-X>TOms496Ge*h=s2xD7DQ7|aRmdTu{8Hq)9d&EracnTWXoJw$O6r^fK6M)r?Mn9F}CfLs376bMjiVV;s= z!G{C~&xet#c04g;U6(uXAFJ;7x)SmqG}8nD$HSgD~4=gkBf^y|5^g8 zt$JRv2dnJRa+la4QmR#YWk;)IPuf{bSohqPNus5mU5L_yDDsDQKw z-pR@bSL#2-6>20aaO#!7IX?sP?jpES`T!16 zv3-2Nd*LCfjHz?Sr(Wj@>!m?6S0(&=1JkjQik9#e7bDyVgg2Q$(JeuGrvW^=HbCd1 zvsbGnKGz!W8{6&U_#2e=y@J*-i@x%kIXR9XLIEvK(cx_4dDLdhx%KqN_ZdQUkO%&e z^|nN7T<58K>!aI-?o6{yfO+8>GW*o}cVxjEo1lz5#b$4!ZP&Fh%DR=_zV+)O#vNK4h(weH= z2duj7w@>vxRZ!M+u{L|x1LNZO`i_i-F!_Olu2E%-`5=hC4%a?}V z7Dxu1RU#OJ@n*xrEgzaGz)`UsHle9ZcyJ;`t$WLLRsd|ugs31Qqs@h#nD7pf3Y*XgxJ;&?wg}A7Q~Q*yS=m+Qir6JO}7w3D7jG-kk|<*6&QWi|c+Hc4Vy zRF3X^i_i&$9lC}$2pf%pT11rqsf;iKP-@zM%Wq&3*80=ENMie<3xK9Ra7-{e^#E|# z4f?L}VF|)y$NtMna{kft#MdtooF{lTxy1@FVU<;~*b*EZoy^%@K$KDq&#fl#=?b85 z8^GTi_`#%Q0jAwNfLr1O-ac&UhTa3zf-ZLb9W%pr9T2V7%}EBemih(MfVOGMELae7 z4+X}PWFLXb9z83JZm1cKC!}tlAU@yUSUw-NTR3r)q)d2TA!)kuviyMCF{fjr3 z{t+m!i6-WvoPR4yC$|SpNFK`Pw#REyROJ(`vrAvwgM#V8NeQ4z&P8}%GBrb#mD;7N zT_A%)Tl;e$pgkOTaIW`44u$#2>C=|;9+xny-RcQubeZGn#ET)YM0(1p9n30V{&dkV zqJE!-oSBZ%CwbL6JX7a9Oz)SW9o!b=>pm~B&Xcpr0+vaeCP4~Sk##At4@4l}?(kwM zy!%m!Cy1I7O+@5bV_UvqTaJo|(# zNS31Fj}v`+6YdBUeS3N8~p7J`;vyP%fX)Jv;~ph_ILHm`z#w*#(Vg zoc|E=%=$*1tf}8ydN?)h1ntQ0rFPg+vBs}y;nTQ-M<$K14{g24Okd8*+=2+zXE6N zGNxl|##~%1*J3%rUk*>pT>#Y1m#sGy8*`AURGi8BDB*V~a+z8FO<=)Sko$OAB2qKB zcjLlY9+iK3bWtT_?k>jomJ2JtzKC12`Iqa&tM}-ZCk`>f?^*b^b5CIz{!CNUF?G)J zNfKE)mz0!56SqQ5YB3jL=8b%)5GyHo*RCF++iNl!hYQ%m4|g5NJe*;7K0#1#_Ui!O z(^XJ1xXp=lmO*UocKbS9iDX`Krw$5az8Hq>>U?~?g+r^aP=uo)uz%1o)G4|RI-kPxjrRMJ^+{K$Or05(+*o>5g>0^QcOfEpO? z<1D*HyOoWm4}Sl!ZTW@u-F`mj^r0a{KPNXq&}3jf`Ld4Ls|88s+uFg_pIS~}=$m4n zMB^UpKUGnx6Vid*Fb1C171S%MlSme*HK$?LJ)UcwTM61K4@<4&k9z)Hrd0*bIYM(l z-HyER1drdF527)40~I~ZrGSE6&0{O4?8y`&%SH*`6J3zfs}6^Iuhva45irJeFF*m8 z!1a4Ohv#KKa2fW#lrx22CYRmM9)KkAJT%GEXM1E?v8}A}YG*k>ZR$53&8O$i9vlAt z^D_MNAO0eOH5-Q?jt(AjW&DrH)aF#);PmTLtOMDMm`x9!Dt0*2nBqG6o3QwqdG$g)CMma zpzmt2uYN?YLtg#8F9^)z~w`@35PNDWS zxV<|eu`iV>n(A{%BK!UET<=v^`&zB}L>jH&J^a{H9kH{HBZA}72dJw2CH7UUr`27-YKWi@eF=Q^uJZG{=5GM_ZbkR>wCh<;JuSzgXDm@ z93uX&uBc_>$dT$@d6wKY8Obh3JHAYw1ng?}kqcWHm@0fh_BIV2ImM6x`Qw!o^B1EE zw>-e6`3GjxpT3V`ZiY#x!S)l=Ddr*^R{^~F55q@f1b`NUz=hT=F)*9up__KVXVrEU zW^P-c-Obp)LUg)!>2FL&-MVq+*b)@FEW7^wDj%d1gM8CoRy|pP@X=OLeW6FIL~JiM zk3?{Tw0ha|iTvK2%)2{R97o?zw4)H{pW&QN3aT9z236H---5TBidIMBJRKnqOJ>Z)h7!i&uq(>QT^J8bPPpNrx4JW>R? z6dHNQ4-ox}aa&!SvOv~m9uYQ??$?GwuV2Drw9*0i z=J(2ga5j9B>Vss)bec*wylzCIKzH(JD~!QGaT0A_=NGk83-#3x*OkHZ^=(;xcS)Dl}TE;i!h8o2Qz2Ot@h__N*HE|PK>fh2aPAlmfe$d!U$mxhhXp%(ScH=zqNo^|GQ&EE^=q;bxI3)D4nLvdu1UHC6ITec~b_r z6SCBuhj7V;gj=_^b=8L->wCZ5!NjB+(K5Oh>rzEi#|zJUzFI;W6Y6aOeP!ZO^Wi{{dB$fj#6o?6kyQ z*?co-fYUPtUPlZ<p`biwQtHJ-^;wfnIyBe#A* zdd6jK{`i&4Q1?lIVfr0=d#H}`=|pgCJ_n!MD+6u+SAd9k&OcX}z^-3!jHL*w?0TQ> zF2a0`A&^b7*sgza_{>b|x-3cze9>@sfN!q1)X5^z7v54Ie_9M(sJZ1Sda7M3H_*X@ zQ}KsR1=3VJ96>3mX?W`W{mQX|8Bg>ru65&@DXyf*_VEc1{=DSZx`ZgF&+k5OR%>K0 zz>C8Z36dxew5TjELXygw3|&Bc@9cQ;COar=t`(Ysc9|Jq0z;lOCMp|eo{`u0HeM*j z1}bF3W{%stpPr^fl7TKzp`wgCV4xjNO-y+B=XJP028KvA#Y?KEt)G{|GDx*ZhAg1< zeK;T=$^gJ8naCZ!pNUw9}8Ndd={w(p~SBe~)*S5TD}a8gT~ zNbBpQ1f+!Fq&p7B9wu2mD;*&5fi@r1oP-vOtXv2vk>C6ONc--%9{2x!rATQ|iAu6n zl+ltTAsj`tcPXQH6D_4QR94D3Mu}7>?Wv(XP!1{WJ&d%AmiF&@IR_oy=X-n}zyER$ zz2EnJzwYO_p4W9frM(-Sqdu^19q>(g*pW)S>0Li-^gs0r4Yl_UTqMqLF9we3WN>6y ztnp%Tr>NLt$~gY{g_vayGW3!a8fRA&xBcE>*MUqc-Nk)(rU{>i2?!&nmh5iy;aU3Y@HP7 z`;UuxdGq&07=-5qG5*f?f?;U4VcpwBwN2>Ry_jR9^yrh_AIK70p`Np`11Um%jCvn_ z%)JxXA*wjlyL9*0Qul{Q{No=(F3JMGQIHVJqTr@g&t#-rP)Y3pXf|iZ21XU^@6OMU zMc27LsYDK~a z$Qg6i>ZFzsJ=&KJ#Zo?5S1{)@umu0fN`Zlh83y;HPhZP$<(CZ0{(~`0{t6PLJ=-TT zg?^5f^TOFlQZyX;z4X ze2g-UV&)QL81K7gQnvICV!GZL7nn7L;-Vh#Cu*r+vyg4|veB1EitjaWP-2M5bIhy; z}GZ*?jWTyd|cYKomX(#tR1q6+0PgXm*-5Wo7E$-cF-VpStLWr?f~LDwk)@XE#gz zT3qFSN{6u=C5>49XDN#96FmNa7*lRN|8V^)vI{C!$SkPua$anjK*j@?i*BT+-J>Jh zq9(RCLRX1Ky#baz#*F&(J94+sw{Tdg)@@XL;W~Htj;d^nly~!_25)(W!;5t()tE1n zjFO0?T!*y5ZmG(6Q)v9*{oLR=>6Deol*#&Y<#Myz9n_ z85XuZa3X%P0(4_!Xo*rAoB^ADr3~lQJAGh9@+C)MaC>H8AD{SIwWw;tG9yLbGEQ|J zL)`L2c=S}f0@dTb;@L6c!Ia(>bSYy3Q>SM8psG!%u9efzu+d&|-dcIerH)Z__Kvem#!*#b@+hobUpEgn%P?#ay3BL#Y$t4Kne&KUYyjZ!+$d+hhfW8X z?B#XBb%0a8fC3oq0~Z(S;es(b%%0Py}wQazn0IgFIQT9o=Us= z_{#m@zeq0so;$zBY02%h6lvHoy%-Nw>}t%c0|C^Ws4DH0*^jI*{~S>g>9&C}f0jhj zoegY-QM42j)tUF4ovma2<&3XB*y<6)!L9yTFtz>I-e@PBgU|Y|fTc%gitXw6u=;q) z<$;6E$~%gpUK`=rK=!TvrC>G-*X)P5Vu;NIdhfg%FV-bJXZyiR$hFdvV|9yb0sx=+ zUfKI5kigijSYp7(sM2=82nbWCY%6M(=3-6@^)TfYS&vy2Y!PuGH+{_Rs+P5Zi1gus z`noW-jLAwtjfIzq2^<{l39jlPM^QoV-T zpD!=ErE-wSO*1*4V3dofbZ30>-y@uD! z2-}G!7nL}MKk$v4`#!Sxl3Ok}qu7gZ%!Mb%!H-D-`Ze;laEh*X3Ta|Ih=CwD&kpPI zZ(nMhMAONuaG%K=a3=d|hJE^(f-;o(}>M8KQ zRc2P+W|7P{e+u`f=D>bYr&nV4*Ug|mM2;W-7~ScrJhu9!;&3?Ztn=tyt7Z6964&@*U{SMQ^(=JwU}Asln7 zuvx@`PvN2&?d6mi2iRnD^e~7dYkPI%wkcC`%6Bco0OCX0K>skx=~~!^brqA6n2{+W zp0pF8Y^|W3O^8MB3nuOBRZql|rhI3{03o*Uqy0#S)u2`O_CFm(J>=L0LdX|YnZ8<% zQtmFXI7;2{IqAP3LZq^h|sH{KqK-puh&A}N- zIoOB&BtPjSll-9%`yw7*(XCUSF?bx8KW>NuRy=bFWk<#4%YFug0`o*$6|~#&Jbq#f zF|}3cMSI)p;H|Pc*k5SXxTPmG-@S@arZaRwCB{u@0Y^(8|NiPehoCnfqv$rs75OYw zXV_u+PpGE>y*csmW@YZQyy#X>1Reu#8g(N366?#QqvQ`e*@~8n#}GUxO+bNXZg;V& zy6%w^Q&m_Hv|<`d`AbM~-Q&9Uto|QeO7H0Sz7ZFHJV!s&{VUc6%H|M2irJJ=X#F*P z*`@p*rW{$X*D=)XOirZNR()|VPc%(HbY4A&)j{{gGhH983go{fr%8r47%4vO9n_#% zN}sdbE6<2AO@NMUU1CiVTo_vXSWccKCtaF}%+lg$+KUvfisE>AR4i*z_7!BFqTEpp zPOR0gsOB9x6)Rtg^LW}yee3~$XlvEv8NemB5XEWYtYsclIm$a{zg9+IXOyn*}9W!`XXc%-kAgKZWIhX8ee#Iowm60 za=+SPj=OVcMIEBr8Cf^5VfIK?C1VO$kPZmurF#tVV=>gHRgT-&eQxyH?Y!u;@NOmZ zXUJ*W4yHeMm?zsTNk1?w$iNh_Sb1B@^UWM<=5p%>S4G+$@R&E~xXeNHcu9`Gj@Z`J z&pWOz+ZA+->3*l=VGms~_etjn^P!zAi@p3Jd_?Eh9`F#nzHz=k`lQ-d@t(R#QChk_ zp#gfm+J}1sD#Pb3#?r%p-WB2lS*r#bI}S5lU{2!hbX&prxT9BVJ%^NC z@Oc`#EAlQmov)8?_{HI!i)_T4H7X%9JS?v6P&B3dIk$GB`xvnewTSnu{Dc2I?P7{z znbW)OJ?l=yS3ZCAa%tcK*`)sP3E@%VS;+c0;-E;S((%399|c~OJic0uC&bO1d?o&H zTA*g1vb8bGjKj-3dU(`P(&*V-9=JThqCq4)@;B=&BH(*nwdv1G0`KqeWtMaBh^9J2 zV&cp|3f?B*_T?m?q7&G*1--=K$Id#AQ^zf}8*Y-|S+Kf|ja1g4c+(eVxH ze?Acvi37Ek7)L~7OvBSzuJus+-KnjO*!lX>=wM=;pB85R77Nt&9X_NrdN}6x1w<0r z8#$0VMgiS~Fke-*7Q73?47ATd6oUshhj@~;R%58`D|ks)L>|Kr?wb%RO_*j%QZyQ)3=0;~jBsuNgBpqTF+3UsnPE9-`B0F!0KUrC{^f zPU{D=JoFar7V*8a6|2(OJ+(QlRfA2}g9vsU5oK_ex~{>_v;A%^rmnQ~c?G%SKSl|D zs|-M|KI*>je>f9g>&ve-<2RVT^bZ&AkD%OlrJQ3i=&X!3Oh0D);-S_y_mU8|>lAOr zKb79DScNH$KeUE2c%zek4WAg z0zT=GqdKfreAUx!i-X93rjos;ghz3iq*b^g;JqKwf6pv#nanWMa za?7=w>?Z*Mf256psFS(rP{66MBr##(S36>WWxSVTdBDQ@!<8J8!(snzP2w*)1{i5P+BvLW6u5nc+Hl$~iA(Y_;PPEq9Ihz(fLXB1Pr_1w$z_{Z zlg%gJF<3Iz%ga<5iCq&M2=}jC+!+^L$OWo z#Z!t!#iPLIs#_LDj)*02s(D+z?vB4&Tp=836`3c#9S4%~``=&JB`1DP*mmdf&mRrl zI{{6N_oh^aW+wu|4)yIsEIcym__v7Y`&Z_Vh6j;iWbfRuU!-~i0XfKE4CNi}^M{mR zjmw&Uzn1)q=a^w&H!}v*C?tJQqlDMK#`;=h|AWEu&hZzlt?j#hz{+J;ap6`y-!_By z`)a&jgX9o!VN0EGDU#CL{rO(O0wPt9|;h@vBOW?=UnDLnG9^Q&4nbK zm_6bBkMi$_YVgY+?^?NHZbm*rx|ysM^A|H!TWD2ub-7pg-|sYID!4bp=X%+BzJ;T| z-){at=om6WZ-`FQSp4``zCV^9e&Uxi0KdpE#|Cc^NE-@UteRbluWog;HASIZ-HZ{b zG8hc{PfCB_`5;9bk!DJxclB=fU%j#4{+_Qqy=_;u;q}!A#67etnnwv;2C#^eW#W++ zM-jZ3d2~3f)+q(ba&Mh)``w%4ukY^qjy&4&(5PQjjQ-xbu*Dc40Pqxv^9_1u zCw7Qc<58agsbowA3q*3WL~+^=;hJqv6`9lz))l<^p}?hfgnsyMAvbf7oxvN3H!5%_ zR5esrEK|xMTZ346bjUOaED*(L-6QEgUQftr7yz0~t~)&YSF_^xZ;h!wxTZM4&{48J z7Hm#KP`9dr_-c`Os$jELhW*Dru^)#Zc;DXOD%poyezgSqo|at|1PLM6e}j>Hb+Z(> zfpr)%gdK!5J1&=G!ryMLU6l1~=8q4PO;QTixckG?`X4X;hkHonkBd}b41t9 z7M*pF^VvUw|H@xS7M0#-?jN%*BEx681m00c-X=kf@BVy0yovw(W1;jnTKUBV>nPy9 z6-m#2b@dxL3XCNE;D!{-``tB(t21^vCy4|6#o9697Xe#RnmRZC)TX`^j&q)HAdJFY zlRQ$J6)>h?*;V-$tM=P_`7bE=9r-{QDyRVgm|C_wbn|f*j<1iPke_xhb1K}xwTHUS zREWE04DwDV)Z_NbZe+Z%RqJEJEG40jmU&pYf8KgBGo-MXf zz}#Lv^I2j^Mh2;-fHdNO!D@hI)&wB@d5fv^w14<_(6~YT*b*AezOM@V=MDeqUwpkp z*b~fI)0}3IOUp_w)TI;L??o2TP(m4|!n6yb4UgC#3k>|~L+Yfj7)`YK>G1vgC;k0Z z-?DuMb0L40dQvKiiVZs2cQzh*BbKTLdxJCHvwu3WzR4u~M-RJae%ILj*JpgaxIH@! zUI*<8HHhTau>puOYSjaR_fu1=Xy|OH9b|rUGBBy@Wp(_u?XuIptTDwEW`E~e)I4;m zqP4jkh)i&1%!v~x8s9n8nmnX>6H9L)>JAJ#$EQsDMO0qOpZ~9{=K_snGtG=yRj@ko zEYOBG7jg0Q(&jt~9@ z$8nbPsm)&vZoYl*@6nt3Py9*rmGJe9&9y-v(;|%QSVDAZOx!wv@^|EjmABCRNK?48 z4mYpv#d3lXq%F|{f~*#)c$t9+CDY^daqR`PD2ZgRZB z`}JJ^>eWeFxMC`ok66;XE(T(`B*1Gcb&h478HqxJYv}%06EW1)()rW#!^=vGEaFP{ zQ&0Wh2Xr?Fdbk}6tro*oZvxzwmW^og;z2q^H9-8!csHLs7u!!IK<(LsJ1e(Qf$Hhx zXJ+69Y2IO=%@ExD{WBf&|2EkDA*}m;q$fFr$W=XtFj(%(ro~+UlS^b?G{s669)86A zUmtt)QlLaFoEZZr={O><0O@7U;vb(iLN1DPO=+lKdw(sUj|NLE!&}tA(JF~l!FTbxQ9SnBni!@8Y_LsD- zOPoPGBi$Ovob2mfj{dJToT~~)H2?}lKhfu;M8+EBO>qV1UEyqviM8I3W52Q{_WjA zUy&>~Os4(h8yReWthD|!1ns$)15toEjy!p&tdBcBl78Sd+-o#|0Z2DTET}>}*97=` zEkUc5N=`qdiXag2De=UQkKnJvth#Xq=T|QPDo5k*6VHEw#zPZ>`c@is4>VhArboARtn4A_KpraGISjHE z<914U*gWynh^I;RAKy?b+~9u}Ns55cSMg21^fBLqo$pe*q24A-1bCzG-GRLsG;vH{ z`b%_R)M}g=u9ugAe;1r_o%j3ZHY(Ti{e!;!6v70B4@)(SILNjqzLNpKE6tm-w$=~B z*Yt_0)jF^d|K%eE$wy`-@^bxj!2g*}+EtoM9_$c@>CV)rVW1%dekHhYBq)^N=9NS8 z>DzO~3?>KwnAQVMx(cu5TAvm-CgHd8Ok*dR-%!qjZ61zP{F5$Z@ zR|<(A4j;jIqlv{7!F%EH{IR4Huhj%oclc?3aA4vegx5mbZ1?swV$1tgFiC2x%cugFQN!U5degl?5)Cz7F*`uW3&}=$=h3Ef%+g(Lh ztp`huC+2yimcDz zjDBn|gGQH-QyMkKAeehvKZrwSpB|mXaCVs&Z4B|#Ces8@mcC3xkDmwD_0= zbdRTVRHs;!a#Xo)%<-g+hI!uJg;rZ%e;CQNeN3g6D7{WzxdDs#SnAL0+krP$awU%0 zMAwJ6IRRtY<$Bd(pM1r9xcAP629|O*04|5P%ylPkiVkWNg^#{T_w#oHMc*Cd(iGS= z=VTA?DO}3vg`1^nj82Wurjw%=9CUybuqRO}r;X`lJ=sf6XHA+14Z*jf0KFed7Dk^$ zNae5wyl#E77FLc^0~hefpDZZkcHF2u?Y>YCi>^1=Q=x=52kVZ!QN*x$euEXp>FW;M zlNNUxwI&8;>;V!<`uKkFy>8s5M&4(~-!8Gmx-ZC9>)vFJW9(iHKNoIGZVdRpCS#NY zHuS_Jg@_EXI-njs;}NX#^CY@=@sscNV21Nh*J-eVnBd=@TpS0lG(xr2ul9;F?Z~n; z`^$rS`cW(OnZX>}{vq@5f&aLwLSI@iJqAV2cQ#`~;HnpXVjnw)@V?c4QElw*ZpzMv zW^uXRi%yhAhi9|!#=)h_-lz*23{#4)Ia(%(&}VWgTgrfD6Ty=jH2jhJ*;((4?@z^P z4tLcUF~&xO@AZx*GV>2m=6pO7q^*N>4l0%oe&XhSTVdNmj8yXd^oS25$|!@>Xna&@ zwrs4^C}@J+nDh_0SVE1Ee|o5=!PM`-7|bIrBB4+SfF^iOZ8u0d;A|Tu5R$lVPEmo0Z!K~aM*~T z+G_%(sivDhb@rlSho%Jw?*)7 zpJW-QQCNFDCHntl>}vE6-3DQ^S2S+-z7D*^kHB|+l^+T|Cd-agEd$hfwnbu98sIoc zo23)dK1GVyyTTG-a7K7u+UvJue{@&P)Uec(+WSx|JsrE2(OQ+v#3}gh64A6^d>^o0 ziQLvP=S&CyTO!VavQl=deYL`E11>%L2QaZWA$Hkh$ZwAA6)k6S-HFp|ejH;1Ws1-} zg_J`tZ-vq%ckP_UUT6-!pHSBhIM3C*56LlmG|#I49+)J|M}Fqib5GCR+J^yweUYO;GG=? zunui;y-C9J-D>y3kw+O?`-`{qO4k9k7a@Wj`%dDTKt}g`zo~Xc=J|uswZmHx{o{a0 zbT&3Ih#MEOv0ftr5t7IJXzy`D{1z(l!~#r&`$7g0?>QbVh9x3tb~%0{Sv;F>{2^}m z5J*lVlP0yGHI484vLPZoSImJ!;LsBypkS)i=}qet9)oZsKQPA60A6lHow#9faF$E# zL@GqAUp5Iqxc5Z+kDj`6?BTKb;2-ac)&+3R)6vV@~MN1}(amQqc;I137wNHs$ zO7O9_o2T8H_Cp4QOwAYD-wr;ICXp3m4Cs6%+pbq{+c4zA9~7rrmC_?$6d*Kb+LjVwn3r6IA0}kl`k~0xEeT|?BOoOY8s*HuXXUe)x$Uo;z{Aq1k(Qp3 zY(W{1pA5E%{HVHJ-9sOvkJebk10i@b1&)NGP{bVY(_UbGw^DWrgr(sHHb}lbjLdjJ z!U?qYswUaM&Oj(3@y{- zN;|Pcc?;!DYhI4xjMo8}${Sg}1jnhM?(lthC@G(}#j=(!{l_&yrCRU{3TyHUu0Bvx zeCaO{+k4$D7G6>PBjuwJ&(ys!DHzF|3^ot{NQ@PkAJVgyFB959?_uOkUhlfY6>}ra z$&mhQg4*Lli};7f@@(XB%B$?<#Z?GU#w7eC5gq?uFjYI-UZ37gW7Eu`{tn-Z2qELy zX_3wIskb-ZKBx2#S&}s-D!EEwOTa60pv|?F38s>F=SeFJ$lUHFME)r5bDFWKZy1TR z>98=*VoC4KWlikvp(-8+*tz{4VojJW#Ame#n3m{2lhOHTb(rZ-yFW09B^I7;dq4KDGp1*uXc)pY-+|$FoT>E2d*5H4kP`&_ z@B~R&_trY?1wALgL>SM|r>RO!l`Pw3;&!R<+$Jj6F$xA%f*}ZQ@r zRLyrhiCj}d-X_MZh>2uGo(n~V6Xe7NF_% zaWM_c|9Lhe%WqBw$Ns%cj{lK`Wkcs3FAcfyJAh=KeXFr93JQDaH}S+c@x$v|m6tdK zPCgkC%SUA<-4!uX5vqly>G~EZV)klH9zW)QyD-5th}J^N>>!ar4njB6uU1L?5X28e z&0$Q}gLZ{9M-GXmh_psUt!2gtK7#PrA3aE~_kQD<&sIdI*Z}Pf-(25K(|`ci@&y{% z&YAkss~A#+wH~dh{{x*`EsWAlx4WAHcT@P#@DuS0kJWWxxv$tnm!RW+t_NRqFc7o> zaqQ&WCYp3HMl*5M>wJ}J<)sy{4)NpOe_d(;jI$FX-tPsAirYC>ysS{<&#@=)3YLq6 zPrUiNkQLk1uA4$mX%621hFb^w#EwunL94G|9i7a0-wxA;w4{{_Y%5L@t*6Lj zwUJK^XgfjbM~DarP1;0UyB`}D*D+9s<1mFP5Y8Mp#JarT?o~os`dW+Kl_dme!e%vt zlr;$e11Z$I@H#!hR*&bHX_(-1A7S|GFMPKjzqJPZ-!4GFDR>(Gah-F{=xc~g%X)wR zYO5g3JnpU0oZcaP{mAbhPJY_EfNWkO`-Kz1TCc?l^M#uN!+#f$t|ccumi7*k%f-ju z5OpsW*Y%D-*h@rs9W^}`U~`k~d}861Y>rMK2KE_8$bM%>=-x8hRYaaJde344+`8U= zA;e7)Hk55f%pQ9|(&`ooR}?!*7o9|jyU0s1ttfGNw;`hCw;j&8ZJ_THLlIQ7Zwt6K zXIBy>q&CLy?lr9G>Pms2Lp9E?!#%d|lrcNHYT9-9&ZPxe{-G244_gq1M;|+5e$oB4 zzydDb%5}6I7A|neYMdF9Ch0vnQ4+G3{RJc_8@p&CzlF?cp<|DtYghQ=6VrQQ>Nk*q zj+P?a4M(SNRU7ni1XXwee6lYg8ov1$U?0P_&4Jc($P~`S?%IY8R$N`4JQfVvN)>M) zY*JFLGXda|UvuC7%^VVi&7NIeH0d#lm(njwWeWY$XRbDPm+%E8w(cdJ5d0~)RvHis zgC#^CeA^f~IS~kO4T5>*QdRv@$eU8KV?;io^C&|-c7%WhCh1qdc&7~z?=+FYv!MUk zVnp%z3}Xq!k!(PakA%|mv%{kt9909Ta;znias?7)8I&KW zC5aN{HBoc0-h6d$YaK^n`K|gU)Sslt3tC25MRr^w@E61HkivK(i+KD%t8V`y$*iVS z-iObea$A)rl3Q1d^nmxX(Lzt^xE>I@H|s4~g^?4{s@!J_SDMLd+@T!Cs%um+vOCNv zU6IvkPc|wE!TsACZom3yEgp756T+&-)7S(UgRD$)a8X^VO0SQ5}t3TYqhF-FJBWt9U z=WL9xDROQ7PVabP6(#YSZ25RdwtP-bettPV`)k#K@EOcj;$d!W@kNO*RlE(5ee8DT zXsO+*IfM+bnA3ocf33fb;d+PrM{+Vk|BzKxqpIRr{~MGOeW4)<>> z&baQ|P?RTZW}97X^Hky5bK}ZI$T!c{YddBdf-VKGB0oK1|WH(08e(&8C`f zo6q<|UGi>HR8=)pqoXKWi@-g2d3qv%vQM?TAGXr#Jr2S1+d#@n??F-dGq5dnPDSKB zYH2Mizjv$jEhS9q;}iCVwNk!Q=;#wcqtgZ{Q)i)CwCZzjtjS0nN$P%la@V~V_QYif z8h+(HlF}SK>vU4BJ_}1kPpylRjp9Q~_>#uw)ORp6XKGG8po>bdR_)%*3o!FIahw*( zBOVpsV;8XIvEj)&+bBt8;u*-DhTs{qXTAeoP@?C1j2e$+#Ut5wQ=E!ziiZD5#N-#6 zvu3UXT<0IZ$Td?Z;}}5YC$(~B5#Hd9!!rA{y3N3=DKNuqA&xSP*+5i zR7Y5_j)CO8sv~^Us!A3$1K^opx-rK9l}UJ7 zO(oQ+3(hFlteEy2rv+GQL6`iujar0pC2p8^E-=>yLjZjI>NDZ+0WE2Ez#F9v<@rX3 z-!-Vw)?{hP^u1ZaNA*9;!ac5-Bc^V#Zr(brECH6TZh(aU<%)?g9!rPQxen$GLPN-i zD<-U+pyp(2`C?7q5yIfjN1dMT%JjpRJY|ME7muV3Mc z3n7V-Iskmm)Dv&b&~fGZsX)YX+)6bk3E4i^Na5R|n|{ZvQgMlV7AN#r|M6^pyri!m z^!2g2&Sm=y4^!g&fKP-y6`2X-3aN%4dI~Q)j&nLbn_F);Z}`VC`1U&_OUZP5U`tLX zwa`WNYknzyt=MQ8QztRokIA*cvd|&Yx8o+etprB;ycWO^#ZvLN-xAQ7&+^&uj}!3q zie-EyDlV#$th4l&t{GxD`QcB&pM?Rrj#q3+UypXn*_M%-|nz) z4es#KA-nT~|I;Zdyorl_g-r%xsU$mdSm|O@nZb#d8w#p{NPeJ=p`5^IlhU`4{p-d3 zqt;yEhjGJB#{!LiS~q@s2Gk$0>sq1p;D;cJ;{GNhgcxX}J~c&KVS-Q!HNotEyRkte zj1JiO4*pjd{m0$jqv48tKu1CnX}GFy0ut8-cBRlAy?5_kGGqF;GgL^y8A`0(^5@XM zpOcWN73zs|vT0{@<=mjgVt2nhcZwr+1|J{2a`m zR|MtSKz5p&#v!6tkJ6q?G!=zo!4+iB<`dLkkHY=r(#q0{lK*4O_s{Dy4kJrQV3a|E zERaOEDj|4IB$?NVgE57QWlriWDXKBR*p3AJ3HF?!|LJw)edNNe&1tq6@}YFK;K(IN zbMfNlwW%(qI5I?+c@thFQ|qg!9#4L@`S1|UGnoI6<3RmTe*Y`3H|T(Pa-M@KMfcJ1 zgXbThJ>sDpWF5QpHB@>)38y02}8EKoYmYnb3x+&_8^^H$@Yd>EWF1lR%8c) z8#A^XW(U|*Y69wj447{>cA%T)*6X-UY5$ zBv@x%1Nln&m3x)lWJ~kOcZKtGR!cZ(sNLq;-poc+9R@Ikx^8Nz`o+h!{)7G{lmsLpjVHwZhi@e{>Id~Z(!8EwjddD`Q)56O>j$AH z$86j@v{U02y5Cwo0%BcrlHKN>!}>{jOci*BM*dgRCNj z{nV;eA5;x2r2J97l2=Z(L{!Sx4@j^|opzGU>!R2ed5x{1eP&Ts(Rr!0!dpX4bRQO! zIB*FZT9LYjF0u}RE)jKHi2c<$%R3p{`T>Sl;~D@D_Vh=ttNbv9y@-i;akx%GJQvPn z;OI@pmIqEwBN0qbVqTg!$%eB&yr=$%e$Hj{m1PZ!@Osw3GTIyywq#6w?TEDr>+MEZ z3D@(4D@C~ZK}wkn>Q%alheU<3-=VT_K4llN$H^*UF-M=pTQ+4G^O(F}&|r*JH4QLi z-6>l=+XF~*D{ab0i`a}8Hw50kwCNUcU=`KPI&<1i0Ipa98ZQd#X-8kKY3|_Bfr*#l z=v|Yx31Acl(Wk+5_}q_f8h;;5RI8t>s$UM@i<6&rRR7P%=cOt8;5Uy|a7FEi5$n#6 zo^-%uJv`?3+Fig9n&qy84y+J=M5SNQ9{ql!$}Rujj~3!pVd4$7Yg6idpmnn19*;$p zcUC4$!?=_x$H2jM<1jh(uF#MO3yrR9enxL#6TJ@)(*07SCAX2XvBL1|K|LQ7v0|WV zOX0ck_eB*~VW&n}iYP|c#$H{g0KcCsO8^e3eMeYQEPyY#+KK2Be zEU{2Me*ziB8}5n>I{)WX2%v z(fLfM!`qqS{9Gm=Bp1N$-FGt=1Q*|yQI?_8&UCN=Zspmo^*|#pZnkd?#^mOJ+qSz$ z>L5sUA63a2#6)*4$c|PZqPDp*wM=i}r@d`^BvBg78NL~xH|-++kBgNZT7cD0(TL6k z*Z^Wm+CyShN3X-V^D-wzwVwj)-0hiSQ%1OHlB=L^p)CJO8YlSy&DZa-ZuP?ENI5^o z>``Bvfh2CvV6VKGQC`6rk(b;E-#i%wnkLeH(6Qo#6u7wl-P?*qkBM@K$3p#E(p(#~ ziwW9hjhjgZ`O~i9dr?vNiWFA;>BY&vSm@l*FhE-~gaA^94pKK21XX%nFzxMfv=H;2 z(9f&zYZqZ;i%{LPg?=`xOUsq~Rla!bniIJ?cMU#P?Tn+Xl?jRNM8%m_2Nv{^CuQY~ zk$rG(u&GayyPp<-&iwqy{EwcsS{3KFfYiiwG5|DPQ=5w9HL%OFJap(<`kH2~pq&?X z)+A z+)C7t52t>M-$_?`FpwAqyfAqfupnN=>Dk~2AnGh8+hTtSkyH>NVimczl`}*xs3tD? zx32G(5-6K3N7yUyFhW7bY1IJIYtz~kLJ_d0QP++9{SMWhYzA%30W`fsj>R>HslR96 zpWt6VHJ@iha(w2|#NpC^tF5vUyaZzVtzanQF;;#uymGf6pHG{1!KGs^F5HS0tr7cR z_Pk8b5J63NJmTitDXe1X2{MSXVC5EFa~p=jf5xD5S6SnJFyy?WVi3bx5OZCGWWkI|^zsXoqu4vE8Z~qxCn! za;0{=iwYRMCE&cMTkaWUWiEdiGnmIV3sIsoJ-z~g4whKaO#Z63Vu#+Fc?56GVU&sS^b1=r^2aa2_2XA&710ACj48sivryq zD8e(Lr)U24TrFA^2iAr^mThjaAHr^=*rMAT@-r)usm)dgSYHdUe{-QHumPR{N2;ab z45q(n=MTu5M>B#P)txc~x6#|q`C~4fuXisoC&K~4FMt}oCv;@-lKVoZmiji*j>-s@ zMC^lkfBhTvD2*1{I3OY$bg@rUz7@15;sv*@Q*##$o>zsB4TOz{M`6KWC96UR9 zW-AJn^J6wMqVr;r4}pV4>SdLLw$dYzIalz?BbE7`+yh^!DmTCcEHKkBS%m_0Ted2)VUuFNMZpyj@9l1$jmCS#>m0-- z4${GP!4mUg<;&&C*_a{kXs9K)wLk)wA9@Gw(}bi!F6g*+dEOlRN(k8bvsvxbCZ^g z$Z&{lHPEz^W8qVdjOy@(d`kbQcmfQvYK$#S*jFj6_h=CV-`to>$I8{Ftt2<#iJ(xo zDdzrPCN}<(CxQsGNppPh(f(F|hH(gqmsbp^Pbym?lDtUQX7+H2RYeHjN_ z^)i>A;p*!m*C#U`VY=j^h<0)mQt3nf`kAZKIiJoFWdx%{i_JC-)yW z{pk>fjA#qf@pNEa%zeZ#U>O~b)KT>1wp?PF9leDh^EWhIC7#&U0{U4Qx+kBnB$UK7 zr>^t#R{s`p_Uv-;tWt5YUr4RYa$Sz4shwBLi)CULTbi1JOsZfWCd1Yd%O2fs0@l}T zy3?2enF56Ju`e@m_ik?f&V!R(z@`BK7}engx~>Xz#lg8yUm=r^?Vj?75SOog_Phj&;I=ms+n1K z6*0KD&~%qhC&^x08?RNnUqGJuL#e(@V48Y}sS_mHQ6#h6VT+MJWFAJ&u_2D_lxuWjCYBB zYOC}i?PhIl&f0;cuTVME0Fzm!_B`HB|f;^S?$)Qb?0(jFKZ>xWQ`KSHFzZrS$`E#o;t7Ct< zwmY(@R94xuZyXT}OR@nUMYz0U=AE=kr`Z$!>Odzmi7 zSvYYg+jOc4X~YGReLE=Wk3a&xXK=*p)GQ=oW&f%lojJb53;sj+!XdL$sK-1N67wqR zPCQX>MX*W$JgbqKq`LhD_?Ww+v_HOi8jYYn&pepI3zDGalQZ>u^j+~5rwy+fpKO}| zgL20cCXnK{EwhdqmhV+RYMy*S5%(spAxoa2TSk^7Jt2e}A7s7Bk zQagC`DW->xU$*~{{Y)a61J|}_R*~8UAC!z8I4`<+j?H4GjlI*NYp|HwWh6w-nA*KO zJ*(eLf;zN_Z((I>Dk=RKufus8mXu^c(e^rGvbAu-5(yR`7uu&7{GGhjCp0QvZ8fv^ zhJZ4mzO>XtL9#G!>5*E4<@TxH8zbM#IUquJxz6UZ$`&EG3~rX)_NrBiek=Ot_K;+D z<*qD&y}_DJLbXYFrd=o~$sVLy?6Qu6;&uqUGEIEwm3qcce^Rd(t#;BMx8E8xo*_x~ zq6C;qpzJ@OX0H-WRwasgEX{W<)QFBoBhDfh&6rwR>z8`prA=HTb9j_Dq;ggyTa?V~ zN4cwd9E<(F_15hx*Tc<$%X$I4;rbSc zhD_y-I~`fbWi?pna~x%s&Dc_eniRZWHAJikN5Lpv$L=IznUL~|?215*S<}~&Z;ovQ z;)kD{PfdPLh%ZQoY43;NXBP8ez48#p!c9jn6Tw8N>BLqNs2aL~q%4`ohFHq5MT9CT z(1ks)<#$$Esy7DG+=X;3el>9h^fmLZffrDb1LC@5Hl3p=0;XeA)}J>biD>Tdec80o=M|?523y?H{u8LD8bB~(+Z2x3Z4>OomUNob|BLGYxEP*Q zYd?o3?4@>sG6c~aDdU#Ika@X^V|8> zGl~A%GJ7If8}6M|ByzAH-bbhdexjUyYMbJgkl`%ZW$U5LDV_mt*2Iob`IfT%TZ)pZ zgf-&$Q@8zbfq?aB$J%9r9TaELeq;<&)S&qU#$|<|N|*umw&}1vyJEMT28$1W?bLUx3$^bZBt|bl*0>KzA-S**O;EiyhElblu>z5lu zY>LQA^r?~ZB)NHnpqBo55fjH6Ho7>c^tf72)>eMoY=xNbPoPxa0Kn`JL`QY(2c%mT zIAg^?4H{K5#`znsWVp}GNZ?< zB%?xGNX$fm7>!#ugpKZ6fYMopJNg``U%u6CkYf2(9oL&htt_OWo4>;V@NLY43e+8N z9@KU^?ps;;#QuI_hnAp512%@(&9Qt7NNu@X+^Ej#s#%@d1`{^KJaiuGJ66kW-)_3> zGR82sEN0CJ7V7Qn&YrltBz@-@;NnPZEcb;1g<&w|!|_0^yOjwxWq z`~;Qv5nxwerb`lIG{ZO`VyUqp&JH0nXcOManCOG+9q)hxzsb^&fXb&wE>Y_Am7yhv zqf>_BIio5rJSZ3B;UmVO#6`~BYky-utZ(bk^_dW>0wVA_W>aPopnssWQv{CcM_=7F zA#)S}CC>&*9nJtcAV#N;s~~l|k-q=8-%eJIe<+-TWcN&F1Z&ROZUFLlv_k-Ot;6TwXt`b4T2xT!+Qq4wmcXc_cf4F!tPI$^zcC zSUCIP4I}^2m$h-_m)S6Rb0-N$`0{DcRMUsGXvzUJ)Dd8aTI&YZYQ(dR5s3 z<7if29+O2DSu*{|C+I3)(tv*Qj#9~>pJ~jA7R`12D3NSl^x!y=If`xOoco--_GPwS8d7rZ3n*zodc^X0>d& z=^W(H=ShR$Y*&i;j2C_n%Aoseyy;y%SS6rt>#c4qg)-Vo_l+5&tMM*(6?{LvZ z0Tg1&n|0u~qi@jbKY~lz<5txGh7K&aNFwgfMA$tVaKBVZNquP-@j~`EoISqJ_381i zUI3r=l{sXO^`rw7k*InX@xk=$4)Y|&bkE&T(?t`WK=ETww`hraEBE$yTgYZe^bYkz z2#_)kE=anIkydL}2KT2!RyIIcLf;=>qZBo+P-q-12s;=4aW+2PhDoRX;0x; z-p{z^%qYCm(mBUbjl7i^xkP)d@92bR-a~UF?!f-snqi!Nko6&q5Of z4tGuDQ&YypS)cwCszeTIcJ97EHmIH-d9&9G_uC~Cs`99_Qr z%$&aBUi}wJ??bYxq2cBWw`s}q+mR+}W>gl+5{Fr+Ke@tlv@MltOY)RWBmH#pKa40V z?}q|lxtu-YyVtdTbDC7t1j-oKFf^u~FtVEy8&c~R5M##*<38lA@0M|2_ybMj; z2jz6W73Wi{1DiwgVklkXc$uPs1e6$WcSYHl+8iOSnC%nnRl6HKt{Ru1YU&{P2iA#e zfwb!GmI#fgIpkV}ebNnE`FhV{cJjVf1!zw^L&bo6ZNpmapdj>!!$5c>O znkrs`O}Toqv*}Qmu3o0`&|SHL4F#Jy)YyxnqYXJi=M~D*&==SobiB6Yc~WkNfs^id z$@WiKnl)aA6%irN)_g;Ed>iP9N>y3AZO-bu2Ef5j6y2yP;!ih=t=);t)07E%_%?&Y zit`ai%P^-%^HksFp65?w%;;RxWTUe6S6_P_1{HR;kYW|ZyuvgS>jq+$+5$}yhqq|e z#&Ha4pCqkaB0s1Ye`2eE#~myJ@8lycb)?1z4Czs`&cS)f(Qb4E0a&oN;eG^r|L1uu zLxK!1_wc#`8?QrGWJ#p)0y84B@1soEbC=mb@q2YO{XjO)pqc!AXvX}WX-D%rFgU|? z`AW9PB-Pmw!g?8!6_slAA4q}=Q46TrMX*)$L6Oe{obFdJbjBJb<&sWq*`t{CYt7@% zWc>Xp{6b7vNBkNyEyyjzY;Fpw3JO_TBsV4F1>HQ8ejn!<_;VJ}tjcPzgG?pY2x`dq z!S1>$`q4*oo%BS@-2ncn67L7M(`8Z*Y*OFf)F!irN{oJq4Q1x%H(ltv_wA~=tco4YN9CCDeD9b5^ddHddG5? z=5p%i={#}Z(ouN%FKdX}*Rffc54K3?7T_FVfFLjssuUSh(Rgj!*~tODKjvyS0;KFB z10kAh@JRjhT(T-Y!9fHr0u6IDgg@9(SIv(}YJGg1E5&v72$X#-}U7{zR0UbbRAHs6wQiQ|=8 zB_h7^Qwin@;*XM-jM4-JwQPzuj$L@mpm_hZ- z&sbfDId9l_q6 zMAG)cI@1K&X{%cVYdANDop`i7LRD8qc1LWP8t>mU>AlBG-JVQz0-U%u3$1pBYf2lcP%4bu4aQh{*my0&_y#o6-^$)BTlZsKlaIeYBx?(i^x|qp* zX@?9Hv!PC27>)>yF11)4mIM82+obm!AfjKiMF9%@+a)|IzPGN^A_l)Ufp046;er~>GTMywM60?{x*GDi|VZIk?rH@G1O{^XWwRFKD zv8%?d8wgXvHW~yMCyW6?-S3ONLiJah{Kd9hrz4gg@(38>Ysl9duZRxsL;+k%DmZyisT>yXBYNfuRL6~0AG*zU)77-(yU zFCB}*oX|rBjuaNdrw^8lkj?LC?503#Bd@Vs2?kNtW0#yq7;VUC#(JYbfMlz|ZpbHz z9Z%0Y8B|LzX)bA|T0KaQ-R?dMUB>I?6HQW)6X|VN>y27Y=a;p7DA;(WGdg80yd;JS zm;0PUoA&W#tdj-39bux8ZQ25#wP3zyph*@4G4c5<-Xfk_eBih?XqGLOCj$&<8BbxC z#IP+ekW}$^15%r}yKRU|qxw!kRn)co)O?5Q8vo)7S9sjDtx~P2(ND*;6GE!vEmo0k zW}2thScgM)iUir4J>~q;op{%nqSsBrM6DVtc#R3d6|O3p$RhTz!%2Nlm6z^BTEx0z zP;J>l`#74uECz&F!?Nuaj2?Vf4njq4?RWyXIPZvvwhWzCJpfXYF#N+O=A_nb#f z$|e(N%eWb{I(Ev97L9_CT!U2H-cxn<%K}Wy!x!+>^gjTxnmE_yGWf#s{FB1p| zdx+7i8pv8zDum<%OjJ=NTC0NaT^S!yw(Y;Ky{%ks32bRcb5$ZWoJ&Q3XoHPvUZVk@=SMFdb}r50kjMqQmQYj!6;di%x%o6Rlf*Kj1r&?#ZZ5kA4XeZp zU=3K%+;Km)B(nBkMNfzJp?sQs+w_m74$VI415bz~nq9HiS<=+(mF_bwzT54^vg)Rn z-=+B{c6vEpubZ{FI>u$w+dif>va$Olt)kg3PVOI>5jB-Qh>c!;!p?1p5%Oa6hE5CB zQaHH{zV$sH8P-UCMutkeRJ)N>u)>sE3MOrK{h&@*aL(7mh}Zb(z8J?Tk=X~tgFpynMJ4ZPR4W`p!OJDHSf^T42pi`YziTY!Ff!rFN3VH zr)RJ!0J23E(1x<}gnOuUim-YE3a#v}Bi(=Khcy|Uy9p1PwEEtWlQO#S1bW<)1rv~J z6o2OMv=Hth+vL5DF@?Sj&|m#~>8Ldvz6g&#DtN!>Ac$Q4mMN<1ZDCHH`zbbaNXrx^ zU|SK{yvjp(*7{z^7Y@wnB>OKZjH~(_sz*+5r<#sg2w8V2d%_H=|2M%?HX}N^R*Rko zJI(I$^M}r~X0{$wj%K+zWQ2x0ru)yc$>0~Ya-T;A?>^Yqc2V)o)kjqK6;qK;~cxsY1G zNgZk67!b;Uv7XPTl^yG|vub^TIJ~EQ%4po-#F(;mWW;)L@5cP%=ajOK$+?0Wfokh& z4H9ctyLA60erqmdh|+mnnq(6ZuUvEk_SYwFwKWG-?%%$5BDq5Pb#|uD%i8`3lX|F!p?VNq>Mw=hutYK(qEH zB)?@@=U;=jlh>~2<3tk(4g;Ib7+}8R_ze?2i?-+s6yVbyVXj+G{1MbsNeGLIedd*b z#-P*<0fwk0E_RbtQuVDxoJSs0Ez*XZ0=aE3-cZ4W{dACI0TttDsd3NfUh~bY&Sm5P zu$(*4g77?C@dC(9?$z~o6PM`gfnrT>pXX3GIqvwKd zTw_fCrL{{mfuJ?lG~r){Y>gm2xlCXR?ogx>oR?xXs} zos$(NoTXdwff(;4r!4E>_6Ia>U?JW_ZmR04Co7ru>>Hm2>+~hUx~OTW(4z^SsNNhB z9H5PMTRWb(XTK8M#upQCky&!?ASU_s$s47H|wdhg1esg|uwpvdVmMhu2N;DnQ5O=vH>KrNab99tFG|i{( zY9Spx-OMMiU6C2D5V(*@@kZ~?wQ7BVYGb-Q-7IDmAzo3Rzb5%D&Q;M~01cfo1bekT zpy_)RFVy!z8wC)<1cdyrFfqZ9DrcO?*jE&P)mH^E;L3Tt8q{+6T*ndBculzet_><0 zz7;If!LKVb#xrqs_q>a!0;O!i7RXSWq^0ES;kqc9R=(8Ij z9ZEp6TRXGI3PKtNT((e6S#ck<@r|FskJiK#!=QA@3{i(9N~E!MuB+?5J-R2*43*TguuG3+E_DrR6yfNp96EE@|hnX7?HM(P%#ee6Z5$)4)H*ZlH;)M?(&)@Q!+aZRVhT3p=z-3Ob|^HGD)EY@5mITsCsjij zXVx~`jg5b1Uw?a`z&u>v;<>fQyy%~Jg%2)pu^n8<9QpjBkvxOpLj!S3{_zGm>C||P z1vHDV#k-`^Mck*k*|HWJy)1WOI_SJpUo;&7;GM~-2Co`WUy1l$XUql5743IMkR4b_ z`p&H@nWt=7BA1cLzczp;|P31fpAfJk}kR*QZ}A^;*gU!_0#C zsQXy95O>1qW|MhOTeImk)+VXvcFAttWEpje^wm16l@kid=*6Pt?o0$dUMI%Y&%blk z_8J7LpkOZFgXB6x+i2OIxekzPmx`@x9g#@;(2~>bQeBg#n!8swiL$jMhP z%-gt|kgRTwx*qY+U3Xfl3#rQdB>}&rC%uLOCBex_sR+pU5O>Y$XS_kXipFo9p(-7Z z(-19FzRgL6G2gNW-&`xSL;0buZh)pgqpuOxPcWB9{Ot++dJOO6DSc|Ob6!0oX4@Z$ zQ;0&CMBsRsM;AxKv1oZOzvbN9{FUfZ$SLukbBb5Z@v=6`v%nql!E%a=I3*XDq2~ z^}b!}Ags&|AQqzWJJfv78chPz0=<`fCn-CghR8 z{xyfD(Jqc_rZ38_UVV3H26Fg|>s?Jf%GZ+}&WbnHxxr8cne`12AKbnuVC{_gL6GCh zAYWh=7Ua#@X`PDMn*(P(Ox2iB>^upKzK+7Xrvz~~4#pc&F@W_UQV}}c?TNRR{ zQeK+GJjIn8YTi@EM!J4!mAk}oRw`SU>`jF;Jh+Gul5+3Il%d)Aa; zjVDSJmVOobS!J7%gTRg)B*6hRa!S{YuUFJ~+di8`xBYXpK< zP5e@pkIOooUcx9pJvfdi#Pwn3xG^&IKv#eOK1noTye;fh1&(74?`0qD>kHR~gNx2w z*hwE07_WvPhi!mwZEOqqi(|`mYQBVrT zRhC=dYCW#5vBR7L#bpHy7!jsG+HBf>O>sH+W85y_cE$l@QWI{}fVzeMhm7(+QZ&C_ zcx^+L3(R0xsgHHJHT8&!rViy+u5ikTL*({H!8yNIPQX5h>~3Ibh~$1}n^e z{ywJ;5P`=eR4+L~oS8a+e=+XsynRZ-E6yG0=fmmA*l{? zf#~}Ai#^CA=_(^2#K!04X)do>-0eMQ0Pi)06X5M=b{j$oINS;pUAdA+h$R&W2v4+f zB$M_x7bg>0%I-$lAIXaHnWJ?WEqHcJUlK+v`MHBr8k2d)VZ~RYda{DCk1i3T2C)0v zD3(>tEU9xe8{1jNJ_`PsmR)-3iP=SPD)LTC+K_Ghgv!Fjx%++J<2|{iu0tuOaxj&! z3f!lbahPR&VEIgmxFL49z-qI)@GGnBx^YG0ohwKXbOlECRo>mAP0t42%}v+G%DsCS z!H|wvTOpR^y1+mUo+QPI9(dkOL4F@)ACvfK->P;a)Kvx0Z*N?f-nl$+r_*hhK`2D?q2 ze*##9!wn?~Ws0U+APJ{|TPE{n)5#5`7NA$%_HSj6CY z<$y?$d8<&PsOd!tGsgSPGl5xCAEdVG9nD9zC@ygVgK%mI#xr*%r(ns!i3z%@PK+i{6O9UD|$^4teU^1FSdU2&!TmL1D1Wx{M}sZ<%sgNgg)jx zXom_G9%IHF6;mMGy0R)lOs?k;!>@bt=IAX&(OEP!*%drgf#Ud-{@62{>ZTsL4Ke>s zL9f3EwSV!(;nhQ4&{@oOpK95bSLDSbL}%qQ30>-RNyLy5LT$c53~dFtFZ94nd-l(< zK>U~21l^i0k$Rv%Rpb%pkbg-y7iAH%e<=P=u}>uXJffiGuz;qW-n(J(Ce>q~wLIAC)&c61V-t^PKe;i#cUG&+47G1fDS7k5=rovaR$QPMS}PIAZ;|n^ zhH*PAuLFOqMCslrvL>d)9#3Xn`=_S*&{0<}`=r^BFFi>+>V(Bin|qXPeYr}oeOtcX z9$9DY_QIs+xXZoe(DKImd84Hc#BZ}>tD#s`_Av+`5(xbj!$;f|@zx0&+V}I^ub;i8 z*4E4_u-*kQK>?tyUBxPn)~B04<-;*rVmR8P2orz)w$A?i>l0^0sW2^J7`ScD_A!fu z>N$uZ@&fcPqZIh#hDp;mMS&qo-VBQF0p?((Rx?p_-7*|4wq+XRB8(Wc0s4j_`@xj# zCa_mI)bTi$TD>tr%aO57)mg0!%}ykVS$778&@F?Q(ov{w2`o?TJrh`TQB4eQl;YY2 zQ4Gau14*2)B!hOPm`SE4m*=s#crfIu@uAC4Lsd3ve7&+te-bF5L*+G2ATt3PkMjr0*bW zuta)1fSk6$LbjsXXZ~>wVOb%>hYl**Q3HMa#U!IdpcbYnTt1-E<$-h?9Qj1-nmh&* z7F7gNIL(3^W+}N@V#C--=!kU#0HcWwk+AwjsLS}+ny3;UlOJY^R z6(k&O;JI#w(9GAmLM0ezJF5^YX8V)u_-7wjL?0@|&_+BOg{D?I_^|PI#=j$YAR{NB z4AUDUpmsk;*jbnk6OD2+$(^X zZnbKSYqRdnpjH=DYM1rB095h4^_N(>^`ti70d;`KqcJ2$!YJ+WUhA#|OKAF(L<$Zs z0+OI(how2K-f_iccWObnGs@m`3;GPV1DKCxLv+eJp3-tEFFHi4Mjt{MX?YQ}&qT=O zn$=l2fB>AWnR;=R*OL6^qGrtf=!%a0(^>7ei~;nPq*#?qOg${{2-v17vXgr)Rt`mvC9u%@CWS@TUrOq*9;l z7#lo|_@077q`;kXtH2%L|L-EQjV?%IK1PJ`fq)Xx7jD@*{lQU_u)#hlGJf%#sHsYd zL(_;1ZX`!$Eesgfw!BvwNF3ho6V&Ea(1K*nH4sceJCGxJGf~nmBZM{OYSTsW%~g>A zpu)>YG+tfv(c;v#=RUj#@8l$Pm3NCPbNpu1qXd#gqiDDTmSHFjGX-8zPqp}PZ8>(eY9WqIMGVIW{(FFy)i;ujet)R zc3YXb&^`zxj;>W8+;^*z=lz3QiOmWbNJiXR3ioVJ|GYRu~8{PV`s}LK9850;FZVr)Yf@@_=I0S z$lM(WQ@G5>N*&poUUl84aYGEI&s%3(TcQ)uJ?6R$wmk(-*6$pH5zAJXWz*}LI-Tp? zZF-hV>ZaFUMA4HOu%a*^x!V|K0jNRU?0xYU!Fo|lFNNYY84=AX)OWI*rt8PKYWiBTSu#MhueQ zZD5N)C4vdLP~2j@>Ic1XXuTo6;cr_oTSpRJ^-B_{UVtHN>X&QL`qYy=A20Rz0?$|U z6uCT1d70*jgy9QokL;&9zmm(~^2hlR-gBp6yT%_8-ictPmr>?JL7}&bQ2_2S!%206 zmLTWC<~`WOEkFr$b8~^2AS9l`bko-e4tMsWs?(Q&oIjb}VP&^gjA%5D7`*EIlX}b> zR0ZZUuI#!3>0q$$x}4>Fl_6zm0O4NnCgcMx;;Lo&S4+OnskPyon8P;%Y#m)o+I|p} zeyy-!_9s$e|3MS9ZTR#oy}?*_u7XN-Jcji8hSD>Ct!r*dimg;th?tn2fiR+4;IC!l zMx=i3tnbO@#BxvqSMWr&v;ZRlMTs9@!%pL9q-kl{JJY7ITgBsAJw3oK@HIC4DbD^l zM`AYX2Kn^_NCq5JpCT53Aj>rRHo3%_NR%im`>O3Wlz3Aj@rsghJ#m`{`Kf}j3OT@G z5wbw+n^&9flo&OCz^XU0S{K%L;l44Lfc`8E8MTDfz;R?f=g-2rtD>Hb-;$zwhs{ur zR-xV~Mn6#%d?Wdsxx1(NVI%`ATC-pY=;8~dB(+RQxAjS@qJ%)B)fI(y}<>r+~>M zLN3mzMoo)1n_Hy-vq2zB~m{K$A-_lC|svvJ1>9kYnUfk6HnE%w7RY z<;1_9TpQrFu`_z1TO0s{siKDUzK~m}q46FvhSz9mG>@J&eq{U`RVCI4nTehKqZ6?f zGn4rN$r{R&T{TY{?N?c8 z3QD=0vT@HwxKD?;$zJNPrjTUyDMip6todx2?qcX*@b*57vwi)!lHFt{Um`8a@>Hnf zui)|M9y#`RIG>JRBIj+h6iA<>bU&&POZxI6-OwS^EL^5ZJvZ}Q)6Pc?u8EVj;L<0= zH&&765(r7qS6~+ywdy)W?2^YGz+ZaGJvGE=(0o~^>Gimu7Ed$T8r0W$btx<|8%kU% z)Obo94wh1mUSxHomC&U`w_Ll

WgH5{R~kM~RjdCqQ(}d`mYpw!+*P@#;SCVOk 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

8sJA!x=pC&sse zdjEF*;2&2S5Oi$`)j^cKv&?U81(Pa4kDWPexR)*izo}2TXC3;RrJ^;hnLcmJKq^A z&`K;Q(d5d0bbzUuNzNONgcd=HwpeewdpfZ(=CI)=4`iL>_G(r46u;aV?<)VeBnAP5|BOAqB(gOm?r_b{m94-ouqD^ z1GZ3G!yjbOfSu@X5Wa+kS(; zhq^>!FEC__L(1}FCte{(yd1~fgA@j_<%5s*pK5@z{8{MDTzDW=#BGo%OCW>1?|!u^ zm_!0=MgWSKWJ{k*PZcb`6zNSiT1z&A_4R?SZ-UIP|HOwCK(H!P=<+ir@<4o35*7ItmSTZr9@ux%ut6d>~E0zhXwr0F)1Cm6UjTrAlLqKG*(^+ z(`hoF$ z8O(-0RfEbeq7_1hg$r!W@P#9Kv_OloKGKE|t41clfED$%m3`ZtpGXd2Oe;*raJz^8 z?^`QM8WbQ6AViA{o$Et1A!mtL(Z`X5MU9LCaP1dRk|`IPcXOOJQEG?m;H7SMr)_l; zFfQtxagwz1UKP#Vh_6eQ}fA+D!5Yvze-|MtP( zK|3!j1cB7?vBtaq#htC(yHDKN#@}dtWtMu!7C113@SDg|3S&)WXJ+z7URR%M197BC zo0QeXHirPg?2pYxe!KVo(JF)gnws6tFT>>sBsRb{6%`&k$W_v7f*cX}k>^}M8y|*Vf+m%qBw)TrLWZ^6-QC+E#k4yvOp;FjB+v-S z!yuP5rRn(~%Kzy@vDPBx#Y>{zSo+(7Eub2`2J8hh(}z#Y#hId8hqdlS8*WxU+W*Wa z6$mXg=UOOpWTQ~tP3Kc)^jKYTe{@Oy(~?p0A$e)>_A<*^+gp0! ztldy`%J(5;fg>8TkD?6e-5yQmmf)A32D!yCc(R|dF-W6fWxV-P`(_UudT(IC;vKbu za6ZTk&_<|IgON-c?1EyLLO~W)#9$M6S6@3hIl1~AVr+0$V=XYs?%wOzo94>&&XB+| z+3T^!RQ|Z?zpeZ9$4K%_X(aMLFCrqZcE(XS6WZY{nQv&|iK!@R@#+nB`#lLQTOHcpY;#_?YAH1+F9tbXS;sMzhBOO z_?fI^z(NY<3Sjcy_V@O``(2);$s@mA-;X=~-~9^d0!VhU1xf<8OWyvwU-A74C<|R^ z{=*mkZ{GU<-%YVUx7{6-9@=$OD~k_Fx#q;Me%mYntfm3XJ3x23>hk{n3zWKG+cisg zxcr_DC=HN674e1V6{Jb+V6|uFmUUn5_w^8UlFYw$h(A`uWt*fj$`C>QAKev}|7~e{ z@iVr;fz$Y&S^pJGZ_<9lQ}+|#`DGzDAF3-ZacTX3 dZP|EjQQ+4-qOEI3KM}#7%QA`=)6N@s{~w}E@TUL( literal 0 HcmV?d00001 diff --git a/docs/monitoring.dot b/docs/monitoring.dot new file mode 100644 index 0000000..5a1e71e --- /dev/null +++ b/docs/monitoring.dot @@ -0,0 +1,98 @@ +digraph G { + label="Site Controller Monitoring" + labelloc="t"; + rankdir="LR"; + clusterrank="local"; + overlap=scale; + normalize=true; + edge [fontsize=10]; + nodesep=0.5 + ranksep=2; + dpi=128; + + + user [label="Support / Engineering User", shape=Mdiamond]; + pagerduty [label="PagerDuty", shape=Msquare]; + + subgraph cluster_legend { + label="Legend"; + node [shape=plaintext] + key [label=< + + + + + +
Encrypted (TLS, HTTPS)
VPN
Encrypted over VPN
Unencrpted
Socket / In-Process
>] + key2 [label=< + + + + + +
 
 
 
 
 
>] + key:i1:e -> key2:i1:w [style=solid , color=green] + key:i2:e -> key2:i2:w [style=dashed ] + key:i3:e -> key2:i3:w [style=dashed, color=green] + key:i4:e -> key2:i4:w [style=solid , color=black] + key:i5:e -> key2:i5:w [style=dotted, color=black] + } + + subgraph cluster_central_controller { + label="Central Site Controller"; + + cc_apache [label="BBG Auth Proxy\n(Apache)"]; + cc_sensu [label="Sensu"]; + cc_sensu_client [label="Sensu Client"]; + cc_uchiwa [label="Uchiwa"]; + cc_alert [label="Central Controller Alert", shape=Mdiamond]; + cc_flapjack [label="Flapjack"]; + + cc_apache -> cc_uchiwa [label="HTTP localhost:3000/tcp"]; + cc_uchiwa -> cc_sensu [label="HTTP localhost:4567/tcp"]; + cc_alert -> cc_sensu_client [style=dotted] + } + + subgraph cluster_site_controller { + label="Remote Site Controller"; + + sc_apache [label="Apache"]; + sc_uchiwa [label="Uchiwa"]; + sc_sensu [label="Sensu"]; + sc_alert [label="Remote Controller Alert", shape=Mdiamond]; + sc_sensu_client [label="Sensu Client"]; + + sc_apache -> sc_uchiwa [label="HTTP localhost:3000/tcp"]; + sc_uchiwa -> sc_sensu [label="HTTP localhost:4567/tcp"]; + sc_alert -> sc_sensu_client [style=dotted] + } + + subgraph cluster_openstack { + label="OpenStack"; + + os_alert [label="OpenStack Alert", shape=Mdiamond]; + os_sensu_client [label="Sensu Client"]; + + os_alert -> os_sensu_client [style=dotted] + } + + user -> cc_apache [label="HTTPS control.openstack.blueboxgrid.com:443/tcp", color="green"]; + + cc_apache -> sc_apache [xlabel="HTTP 80/tcp", style="dashed"]; + cc_uchiwa -> sc_sensu [labeldistance=6, headlabel="sensu-api:4567/tcp", style="dashed"]; + + sc_sensu -> cc_flapjack [labeldistance=7, headlabel="flapjack_http 3090/tcp", style="dashed"]; + cc_sensu -> cc_flapjack + cc_flapjack -> pagerduty [label="HTTPS 443/tcp", color="green"]; + + os_sensu_client -> sc_sensu [label="RabbitMQ 5671/tcp", style="dashed", color="green"]; + sc_sensu_client -> cc_sensu [label="RabbitMQ 5671/tcp", style="dashed", color="green"]; + cc_sensu_client -> cc_sensu [label="RabbitMQ 5671/tcp", color="green"]; + + // Legend placement hacks + user_hidden [style=invis] + internet_hidden [style=invis] + user_hidden -> internet_hidden -> cc_alert [style=invis] + { rank=same user_hidden user} + key2:i5:w -> os_alert [style=invis] +} diff --git a/docs/monitoring.png b/docs/monitoring.png new file mode 100644 index 0000000000000000000000000000000000000000..1b78b5fe80eee386771a292bc0a9f1d0b3de3ac2 GIT binary patch literal 269935 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

Cluster Capacity Used%

\n\n

if > 90% will be shown as Red;

\n

if > 70% will be shown as Yellow;

", + "editable": true, + "error": false, + "id": 14, + "isNew": true, + "links": [], + "mode": "html", + "span": 3, + "style": {}, + "title": "", + "type": "text" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": null, + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "id": 1, + "interval": null, + "isNew": true, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "refId": "A", + "target": "stats.bbc.$cloud_name.openstack.ceph.cluster.used_percentage" + } + ], + "thresholds": "0,70,90", + "title": "Cluster Capacity Used%", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": null, + "decimals": null, + "editable": true, + "error": false, + "format": "none", + "id": 6, + "interval": null, + "isNew": true, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "targets": [ + { + "refId": "A", + "target": "countSeries(stats.bbc.$cloud_name.openstack.ceph.pool.*.total_bytes)" + } + ], + "thresholds": "0,70,90", + "title": "Pools", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": null, + "decimals": 2, + "editable": true, + "error": false, + "format": "bytes", + "id": 4, + "interval": null, + "isNew": true, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "refId": "A", + "target": "stats.bbc.$cloud_name.openstack.ceph.cluster.total_avail_bytes" + } + ], + "thresholds": "0,70,90", + "title": "Cluster Available Raw Storage", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + } + ], + "showTitle": false, + "title": "Cluster Capacity" + }, + { + "collapse": false, + "editable": true, + "height": "200px", + "panels": [ + { + "content": "

Pool $pool_name status:

\n\n

used%: used_bytes/ (used_bytes + max_avail_bytes).

\n

allocated%: cinder_allocated_bytes/ (used_bytes + max_avail_bytes).

\n

available storage: min(max_avail_bytes,cinder_avail_bytes)

", + "editable": true, + "error": false, + "id": 15, + "isNew": true, + "links": [], + "mode": "html", + "scopedVars": { + "pool_name": { + "selected": true, + "text": "rbd_ssd", + "value": "rbd_ssd" + } + }, + "span": 3, + "style": {}, + "title": "", + "type": "text" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": null, + "editable": true, + "error": false, + "format": "percent", + "id": 12, + "interval": null, + "isNew": true, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "scopedVars": { + "pool_name": { + "selected": true, + "text": "rbd_ssd", + "value": "rbd_ssd" + } + }, + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "refId": "A", + "target": "stats.bbc.$cloud_name.openstack.ceph.pool.$pool_name.used_percentage" + } + ], + "thresholds": "0,70,90", + "title": "Pool Used% ($pool_name)", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": null, + "editable": true, + "error": false, + "format": "percent", + "id": 11, + "interval": null, + "isNew": true, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "scopedVars": { + "pool_name": { + "selected": true, + "text": "rbd_ssd", + "value": "rbd_ssd" + } + }, + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "refId": "A", + "target": "stats.bbc.$cloud_name.openstack.ceph.pool.$pool_name.allocated_percentage" + } + ], + "thresholds": "0,70,90", + "title": "Pool Allocated% ($pool_name)", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": null, + "editable": true, + "error": false, + "format": "bytes", + "id": 13, + "interval": null, + "isNew": true, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "scopedVars": { + "pool_name": { + "selected": true, + "text": "rbd_ssd", + "value": "rbd_ssd" + } + }, + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "hide": false, + "refId": "A", + "target": "minSeries(stats.bbc.$cloud_name.openstack.ceph.pool.$pool_name.*avail_bytes)" + } + ], + "thresholds": "0,70,90", + "title": "Pool Available Storage($pool_name)", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 7, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "scopedVars": { + "pool_name": { + "selected": true, + "text": "rbd_ssd", + "value": "rbd_ssd" + } + }, + "seriesOverrides": [], + "span": 6, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.bbc.$cloud_name.openstack.ceph.pool.$pool_name.used_bytes)" + }, + { + "refId": "B", + "target": "aliasByMetric(stats.bbc.$cloud_name.openstack.ceph.pool.$pool_name.max_avail_bytes)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Pool Usage : Real", + "tooltip": { + "shared": true, + "value_type": "individual" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "bytes", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 16, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "scopedVars": { + "pool_name": { + "selected": true, + "text": "rbd_ssd", + "value": "rbd_ssd" + } + }, + "seriesOverrides": [], + "span": 6, + "stack": true, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.bbc.$cloud_name.openstack.ceph.pool.$pool_name.cinder_allocated_bytes)" + }, + { + "refId": "B", + "target": "aliasByMetric(stats.bbc.$cloud_name.openstack.ceph.pool.$pool_name.cinder_avail_bytes)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Pool Usage : Allocated", + "tooltip": { + "shared": true, + "value_type": "individual" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "bytes", + "short" + ] + } + ], + "repeat": "pool_name", + "scopedVars": { + "pool_name": { + "selected": true, + "text": "rbd_ssd", + "value": "rbd_ssd" + } + }, + "title": "Pool Capacity ($pool_name)" + }, + { + "collapse": false, + "editable": true, + "height": "200px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "grid": { + "leftLogBase": 1, + "leftMax": 100, + "leftMin": 0, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": 70, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)", + "thresholdLine": false + }, + "id": 17, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(currentAbove(stats.bbc.$cloud_name.openstack.ceph.osd.*.utilization, 0), 6)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "OSD utilization % (>70% as alert threshold)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "percent", + "short" + ] + } + ], + "title": "OSD utilization" + } + ], + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [ + { + "allFormat": "wildcard", + "current": { + "text": "prius", + "value": "prius" + }, + "datasource": null, + "includeAll": false, + "multi": false, + "multiFormat": "wildcard", + "name": "cloud_name", + "options": [ + { + "text": "prius", + "value": "prius" + } + ], + "query": "stats.bbc.*", + "refresh": true, + "type": "query" + }, + { + "allFormat": "wildcard", + "current": { + "text": "rbd_ssd", + "value": "rbd_ssd" + }, + "datasource": null, + "includeAll": false, + "multi": true, + "multiFormat": "wildcard", + "name": "pool_name", + "options": [ + { + "text": "rbd_ssd", + "value": "rbd_ssd" + } + ], + "query": "stats.bbc.$cloud_name.openstack.ceph.pool.*", + "refresh": true, + "type": "query" + } + ] + }, + "annotations": { + "list": [] + }, + "schemaVersion": 8, + "version": 30, + "links": [] +} diff --git a/roles/grafana/templates/dashboards/bbc-standard-sla.json b/roles/grafana/templates/dashboards/bbc-standard-sla.json new file mode 100644 index 0000000..67b5e41 --- /dev/null +++ b/roles/grafana/templates/dashboards/bbc-standard-sla.json @@ -0,0 +1,389 @@ +{ + "id": 21, + "title": "API SLA Dashboard", + "originalTitle": "API SLA Dashboard", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftYAxisLabel": "percent", + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 22, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": true, + "decimals": 0, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "one", + "yaxis": 1 + } + ], + "span": 12, + "stack": false, + "steppedLine": true, + "targets": [ + { + "refId": "A", + "target": "aliasSub(summarize(scale(stats.bbc.$cloud_instance.$node_name.*.status, 100), '$interval', 'avg', false), '.*\\.(\\w+)\\.(\\w+)\\.status,.*', '\\1.\\2')" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Uptime Percent for $cloud_instance.$node_name (interval: $interval)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "percent", + "short" + ] + } + ], + "title": "New Row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftYAxisLabel": "seconds", + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 23, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "decimals": 3, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "one", + "yaxis": 1 + } + ], + "span": 12, + "stack": false, + "steppedLine": true, + "targets": [ + { + "refId": "A", + "target": "aliasSub(stats.bbc.$cloud_instance.$node_name.*.time, '.*\\.(\\w+)\\.(\\w+)\\.time$', '\\1.\\2')" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Response time for $cloud_instance.$node_name", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "s", + "short" + ] + } + ], + "title": "New Row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": 600, + "leftMin": 110, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 24, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "decimals": 0, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "one", + "yaxis": 1 + } + ], + "span": 12, + "stack": false, + "steppedLine": true, + "targets": [ + { + "refId": "A", + "target": "aliasSub(stats.bbc.$cloud_instance.$node_name.*.code, '.*\\.(\\w+)\\.(\\w+)\\.code$', '\\1.\\2')" + + } + ], + "timeFrom": null, + "timeShift": null, + "title": "HTTP status code (200: OK, Greater than 3xx: Error)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "New Row" + } + ], + "time": { + "from": "now-2d", + "to": "now" + }, + "nav": [ + { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + } + ], + "time": { + "from": "now-2d", + "to": "now" + }, + "templating": { + "list": [ + { + "allFormat": "wildcard", + "current": { + "text": "stack", + "value": "stack" + }, + "datasource": null, + "includeAll": false, + "multi": false, + "multiFormat": "wildcard", + "name": "cloud_instance", + "options": [ + { + "text": "stack", + "value": "stack" + } + ], + "query": "stats.bbc.*", + "refresh": true, + "type": "query" + }, + { + "allFormat": "wildcard", + "current": { + "text": "node", + "value": "node" + }, + "datasource": null, + "includeAll": true, + "multi": false, + "multiFormat": "wildcard", + "name": "node_name", + "options": [ + { + "text": "node", + "value": "node" + } + ], + "query": "stats.bbc.$cloud_instance.*", + "refresh": true, + "type": "query" + }, + { + "type": "interval", + "name": "interval", + "options": [ + { + "text": "5 minutes", + "value": "5m" + }, + { + "text": "15 minutes", + "value": "15m" + }, + { + "text": "30 minutes", + "value": "30m" + }, + { + "text": "1 hour", + "value": "1h" + }, + { + "text": "6 hours", + "value": "6h" + }, + { + "text": "12 hours", + "value": "12h" + }, + { + "text": "1 day", + "value": "1d" + }, + { + "text": "1 week", + "value": "1w" + }, + { + "text": "1 month", + "value": "1month" + } + ], + "includeAll": false, + "auto": true, + "auto_count": 50, + "current": { + "text": "auto", + "value": "$__auto_interval" + } + } + ] + }, + "annotations": { + "list": [] + }, + "refresh": false, + "schemaVersion": 6, + "version": 1 +} diff --git a/roles/grafana/templates/dashboards/cleversafe-standard-sla.json b/roles/grafana/templates/dashboards/cleversafe-standard-sla.json new file mode 100644 index 0000000..1695513 --- /dev/null +++ b/roles/grafana/templates/dashboards/cleversafe-standard-sla.json @@ -0,0 +1,368 @@ +{ + "id": null, + "title": "Cleversafe SLA Dashboard", + "originalTitle": "Cleversafe SLA Dashboard", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "decimals": 0, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "leftYAxisLabel": "percent", + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 22, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "one", + "yaxis": 1 + } + ], + "span": 12, + "stack": false, + "steppedLine": true, + "targets": [ + { + "refId": "A", + "target": "aliasSub(summarize(scale(stats.cleversafe.$cleversafe_instance.status, 100), '$interval', 'avg', false), '.*\\.(\\w+)\\.(\\w+)\\.status,.*', '\\1.\\2')" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Uptime Percent for $cleversafe_instance (interval: $interval)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "percent", + "short" + ] + } + ], + "title": "Uptime Status" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "decimals": 3, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "leftYAxisLabel": "seconds", + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 23, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "one", + "yaxis": 1 + } + ], + "span": 12, + "stack": false, + "steppedLine": true, + "targets": [ + { + "refId": "A", + "target": "aliasSub(stats.cleversafe.$cleversafe_instance.time, '.*\\.(\\w+)\\.(\\w+)\\.time$', '\\1.\\2')" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Response Time for $cleversafe_instance", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "s", + "short" + ] + } + ], + "title": "Response Time" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "decimals": 0, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": 600, + "leftMin": 110, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 24, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "one", + "yaxis": 1 + } + ], + "span": 12, + "stack": false, + "steppedLine": true, + "targets": [ + { + "refId": "A", + "target": "aliasSub(stats.cleversafe.$cleversafe_instance.code, '.*\\.(\\w+)\\.(\\w+)\\.code$', '\\1.\\2')" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "HTTP Status Code (200: OK, Greater than 3xx: Error)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "HTTP Code" + } + ], + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "templating": { + "list": [ + { + "allFormat": "wildcard", + "current": { + "text": "cleversafe-company1", + "value": "cleversafe-company1" + }, + "datasource": null, + "includeAll": false, + "multi": false, + "multiFormat": "wildcard", + "name": "cleversafe_instance", + "options": [ + { + "text": "cleversafe", + "value": "cleversafe", + "selected": false + }, + { + "text": "cleversafe-company1", + "value": "cleversafe-company1", + "selected": true + } + ], + "query": "stats.cleversafe.*", + "refresh": true, + "type": "query" + }, + { + "allFormat": "glob", + "auto": true, + "auto_count": 50, + "current": { + "text": "auto", + "value": "$__auto_interval" + }, + "datasource": null, + "includeAll": false, + "name": "interval", + "options": [ + { + "text": "auto", + "value": "$__auto_interval" + }, + { + "text": "5 minutes", + "value": "5m" + }, + { + "text": "15 minutes", + "value": "15m" + }, + { + "text": "30 minutes", + "value": "30m" + }, + { + "text": "1 hour", + "value": "1h" + }, + { + "text": "6 hours", + "value": "6h" + }, + { + "text": "12 hours", + "value": "12h" + }, + { + "text": "1 day", + "value": "1d" + }, + { + "text": "1 week", + "value": "1w" + }, + { + "text": "1 month", + "value": "1month" + } + ], + "type": "interval" + } + ] + }, + "annotations": { + "list": [] + }, + "refresh": false, + "schemaVersion": 8, + "version": 7, + "links": [] +} diff --git a/roles/grafana/templates/dashboards/elk-stats.json b/roles/grafana/templates/dashboards/elk-stats.json new file mode 100644 index 0000000..c55fcce --- /dev/null +++ b/roles/grafana/templates/dashboards/elk-stats.json @@ -0,0 +1,550 @@ +{ + "title": "elk-stats", + "originalTitle": "elk-stats", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "max": null, + "min": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 1, + "isNew": true, + "legend": { + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true, + "decimals": 3 + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(stats.sc.$cloud_instance.*.elasticsearch.last_index_size,3)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Current Index Size (GBs)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "Current Index Size (GBs)" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "max": null, + "min": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 2, + "isNew": true, + "legend": { + "avg": true, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true, + "decimals": 3 + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(stats.sc.$cloud_instance.*.elasticsearch.projection,3)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Projection 90 days from now (TBs)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "Projection 270 days from now" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "max": null, + "min": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 3, + "isNew": true, + "legend": { + "avg": true, + "current": false, + "max": false, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(stats.sc.$cloud_instance.*.elasticsearch.days_remaining,3)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Days Remaining until Max Capacity Reached", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "Days Remaining until Max Capacity Reached" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 4, + "isNew": true, + "legend": { + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(stats.sc.$cloud_instance.*.elasticsearch.hosts_sustainable,3)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Hosts Sustainable in 90 days time", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "Hosts Sustainable in 270 days time" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "max": null, + "min": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 5, + "isNew": true, + "legend": { + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true, + "decimals": 3 + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(stats.sc.$cloud_instance.*.elasticsearch.last_host_count,3)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Last Host Count", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "Last Host Count" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 6, + "isNew": true, + "legend": { + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(stats.sc.$cloud_instance.*.elasticsearch.last_num_all_msg,3)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Current Number of All Messages", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "Current Number of All Messages" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 7, + "isNew": true, + "legend": { + "avg": true, + "current": false, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true, + "decimals": 3 + }, + "lines": true, + "linewidth": 2, + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(stats.sc.$cloud_instance.*.elasticsearch.last_debug_ratio,3)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Current (Debug : Total) Message Ratio", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y_formats": [ + "short", + "short" + ] + } + ], + "title": "Current (Debug : Total) Message Ratio" + } + ], + "time": { + "from": "now/w", + "to": "now/w" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [ + { + "allFormat": "glob", + "current": { + "text": "dal09", + "value": "dal09" + }, + "datasource": null, + "includeAll": true, + "multi": true, + "multiFormat": "glob", + "name": "cloud_instance", + "options": [ + { + "text": "dal09", + "value": "dal09" + } + ], + "query": "stats.sc.*", + "refresh": true, + "type": "query" + } + ] + }, + "annotations": { + "list": [] + }, + "refresh": false, + "schemaVersion": 6, + "links": [] +} diff --git a/roles/grafana/templates/datasources.json b/roles/grafana/templates/datasources.json new file mode 100644 index 0000000..feec4a4 --- /dev/null +++ b/roles/grafana/templates/datasources.json @@ -0,0 +1,7 @@ +{ + "name": "{{ item.name }}", + "type": "graphite", + "url": "http://{{ item.hostname }}:{{ item.port|default('8081') }}", + "access": "proxy", + "database": "" +} diff --git a/roles/grafana/templates/etc/default/grafana-server b/roles/grafana/templates/etc/default/grafana-server new file mode 100644 index 0000000..05e42f5 --- /dev/null +++ b/roles/grafana/templates/etc/default/grafana-server @@ -0,0 +1,19 @@ +# {{ ansible_managed }} + +GRAFANA_USER=grafana + +GRAFANA_GROUP=adm + +GRAFANA_HOME=/usr/share/grafana + +LOG_DIR=/var/log/grafana + +DATA_DIR=/var/lib/grafana + +MAX_OPEN_FILES=10000 + +CONF_DIR=/etc/grafana + +CONF_FILE=/etc/grafana/grafana.ini + +RESTART_ON_UPGRADE=false diff --git a/roles/grafana/templates/etc/grafana/grafana.ini b/roles/grafana/templates/etc/grafana/grafana.ini new file mode 100644 index 0000000..b996df5 --- /dev/null +++ b/roles/grafana/templates/etc/grafana/grafana.ini @@ -0,0 +1,36 @@ +# {{ ansible_managed }} + +[server] +http_addr = {{ grafana.server.http_addr }} +http_port = {{ grafana.server.http_port }} +protocol = http +root_url = {{ grafana.server.root_url|default("")}} + +[database] +type = {{ grafana.database.type }} +path = {{ grafana.database.path }} +host = {{ grafana.database.host }} +name = {{ grafana.database.name }} +user = {{ grafana.database.user }} +password = {{ grafana.database.password }} + +{% if grafana.security.enabled|bool %} +[security] +admin_user = {{ grafana.security.admin_user }} +admin_password = {{ grafana.security.admin_password }} +secret_key = {{ grafana.security.secret_key }} +{% endif %} + +{% if grafana.security.anonymous|bool %} +[auth.anonymous] +enabled = true +{% endif %} + +[auth.basic] +enabled = {{ grafana.security.basic|default("true")|bool }} + +{% if grafana.dashboards.path %} +[dashboards.json] +enabled = true +path = {{ grafana.dashboards.path }} +{% endif %} diff --git a/roles/grafana/templates/etc/init.d/grafana-server b/roles/grafana/templates/etc/init.d/grafana-server new file mode 100644 index 0000000..1ba6540 --- /dev/null +++ b/roles/grafana/templates/etc/init.d/grafana-server @@ -0,0 +1,149 @@ +#! /usr/bin/env bash +# +# {{ ansible_managed }} + +# chkconfig: 2345 80 05 +# description: Grafana web server & backend +# processname: grafana +# config: /etc/grafana/grafana.ini +# pidfile: /var/run/grafana.pid + +### BEGIN INIT INFO +# Provides: grafana +# Required-Start: $all +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start grafana at boot time +### END INIT INFO + +# tested on +# 1. New lsb that define start-stop-daemon +# 3. Centos with initscripts package installed + +PATH=/bin:/usr/bin:/sbin:/usr/sbin +NAME=grafana-server +DESC="Grafana Server" +DEFAULT=/etc/default/$NAME + +GRAFANA_USER=grafana +GRAFANA_GROUP=grafana +GRAFANA_HOME=/usr/share/grafana +CONF_DIR=/etc/grafana +WORK_DIR=$GRAFANA_HOME +DATA_DIR=/var/lib/grafana +LOG_DIR=/var/log/grafana +CONF_FILE=$CONF_DIR/grafana.ini +MAX_OPEN_FILES=10000 +PID_FILE=/var/run/$NAME.pid +DAEMON=/usr/sbin/$NAME + +if [ `id -u` -ne 0 ]; then + echo "You need root privileges to run this script" + exit 1 +fi + +. /lib/lsb/init-functions + +if [ -r /etc/default/rcS ]; then + . /etc/default/rcS +fi + +# overwrite settings from default file +if [ -f "$DEFAULT" ]; then + . "$DEFAULT" +fi + +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR}" + +# Check DAEMON exists +test -x $DAEMON || exit 0 + +case "$1" in + start) + + log_daemon_msg "Starting $DESC" + + pid=`pidofproc -p $PID_FILE grafana` + if [ -n "$pid" ] ; then + log_begin_msg "Already running." + log_end_msg 0 + exit 0 + fi + + # Prepare environment + mkdir -p "$LOG_DIR" "$DATA_DIR" && chown "$GRAFANA_USER":"$GRAFANA_GROUP" "$LOG_DIR" "$DATA_DIR" + touch "$PID_FILE" && chown "$GRAFANA_USER":"$GRAFANA_GROUP" "$PID_FILE" + + if [ -n "$MAX_OPEN_FILES" ]; then + ulimit -n $MAX_OPEN_FILES + fi + + # Start Daemon + start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$GRAFANA_USER" -c "$GRAFANA_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS + sleep 10 + return=$? + if [ $return -eq 0 ] + then + sleep 1 + + # check if pid file has been written two + if ! [[ -s $PID_FILE ]]; then + log_end_msg 1 + exit 1 + fi + + i=0 + timeout=10 + # Wait for the process to be properly started before exiting + until { cat "$PID_FILE" | xargs kill -0; } >/dev/null 2>&1 + do + sleep 1 + i=$(($i + 1)) + if [ $i -gt $timeout ]; then + log_end_msg 1 + exit 1 + fi + done + + chown -R "$GRAFANA_USER":"$GRAFANA_GROUP" "$LOG_DIR" + fi + log_end_msg $return + ;; + stop) + log_daemon_msg "Stopping $DESC" + + if [ -f "$PID_FILE" ]; then + start-stop-daemon --stop --pidfile "$PID_FILE" \ + --user "$GRAFANA_USER" \ + --retry=TERM/20/KILL/5 >/dev/null + if [ $? -eq 1 ]; then + log_progress_msg "$DESC is not running but pid file exists, cleaning up" + elif [ $? -eq 3 ]; then + PID="`cat $PID_FILE`" + log_failure_msg "Failed to stop $DESC (pid $PID)" + exit 1 + fi + rm -f "$PID_FILE" + else + log_progress_msg "(not running)" + fi + log_end_msg 0 + ;; + status) + status_of_proc -p $PID_FILE grafana grafana && exit 0 || exit $? + ;; + restart|force-reload) + if [ -f "$PID_FILE" ]; then + $0 stop + sleep 1 + fi + $0 start + ;; + *) + log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}" + exit 1 + ;; +esac + +exit 0 diff --git a/roles/grafana/templates/serverspec/grafana_spec.rb b/roles/grafana/templates/serverspec/grafana_spec.rb new file mode 100644 index 0000000..4f63c27 --- /dev/null +++ b/roles/grafana/templates/serverspec/grafana_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe user('grafana') do + it { should exist } #GFN001 + it { should belong_to_group 'adm' } #GFN002 + it { should have_home_directory '/usr/share/grafana' } #GFN003 + it { should have_login_shell '/bin/false' } #GFN004 +end + +describe file('/var/log/grafana') do + it { should be_owned_by 'grafana' } #GFN005 + it { should be_grouped_into 'adm' } #GFN006 + it { should be_directory } #GFN007 +end + +describe file('/usr/share/grafana/packages') do + it { should be_owned_by 'grafana' } #GFN008 + it { should be_directory } #GFN009 +end + +describe file('/etc/init.d/grafana-server') do + it { should be_mode 755 } #GFN010 + it { should be_file } #GFN011 +end + +describe file('/etc/grafana/grafana.ini') do + it { should be_file } #GFN012 + its(:content) { should_not contain /(password = \w{0,15})$/ } #GFN013 +end + +{% for item in grafana.firewall %} +describe port('{{ item.port }}') do + it { should be_listening } #GFN014 +end +{% endfor %} + +describe service('grafana-server') do + it { should be_enabled } +end diff --git a/roles/graphite/defaults/main.yml b/roles/graphite/defaults/main.yml new file mode 100644 index 0000000..00155c5 --- /dev/null +++ b/roles/graphite/defaults/main.yml @@ -0,0 +1,96 @@ +--- +graphite: + path: + home: /opt/graphite + data: /data/graphite + virtualenv: /opt/graphite + install_root: /opt/graphite + system_deps: + - python-cairo + - sqlite3 + - libmysqlclient-dev + pip_packages: + - 'pyparsing<2.1.10' + - 'graphite-web<1.0' + - 'carbon<1.0' + - 'whisper<1.0' + - mysql-python + - 'Twisted<12.0' + - 'django<1.6' + - 'django-tagging<0.4' + alternatives: [] + # - name: graphite + # path: /opt/git/ipmi-proxy/current/src/ipmi-proxy-tool.py + # link: /usr/local/bin/ipmi-proxy-tool.py + # - name: sync-proxy-cache.py + # path: /opt/git/ipmi-proxy/current/src/sync-proxy-cache.py + # link: /usr/local/bin/sync-proxy-cache.py + cleanup: [] + + remote_poll: + enabled: True + port: 8081 + allowed_networks: + - 127.0.0.1/32 + mysql: + enabled: True + host: "{{ database.host }}" + port: "{{ database.port }}" + database: "{{ database.users.graphite.database|default('graphite') }}" + username: "{{ database.users.graphite.username|default('graphite') }}" + password: "{{ database.users.graphite.password|default('graphite') }}" + secret_key: SECRETKEY + amqp: + enabled: False + verbose: False + host: localhost + port: 5672 + vhost: /graphite + user: graphite + password: graphite + exchange: metrics + metric_name_in_body: True + storage_schemas: + tap: + name: tap + comment: "1min for 7days" + pattern: 'tap.*' + retentions: "60s:7d" + carbon: + name: carbon + comment: internal carbon stats + pattern: '^carbon\.' + retentions: "60s:90d" + default: + name: default + comment: "1min for 90days, 5min for1year" + pattern: '.*' + retentions: "60s:90d,300s:365d" + + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/graphite/carbon-cache-a.log + fields: + tags: graphite,carbon-cache + - paths: + - /var/log/graphite/webapp/info.log + - /var/log/graphite/webapp/exception.log + fields: + tags: graphite,webapp + - paths: + - /var/log/apache2/graphite_access.log + fields: + tags: apache_access,graphite + - paths: + - /var/log/apache2/graphite_error.log + fields: + tags: apache_error,graphite + + logging: + forwarder: filebeat + + logrotate: + frequency: 'daily' + rotations: 7 + size: '1G' diff --git a/roles/graphite/handlers/main.yml b/roles/graphite/handlers/main.yml new file mode 100644 index 0000000..3e23a46 --- /dev/null +++ b/roles/graphite/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: setup graphite database + command: "{{ graphite.path.virtualenv }}/bin/python {{ graphite.path.install_root }}/webapp/graphite/manage.py syncdb --noinput" + run_once: true + notify: restart apache + +- name: restart carbon-cache + service: + name: carbon-cache + state: restarted diff --git a/roles/graphite/meta/main.yml b/roles/graphite/meta/main.yml new file mode 100644 index 0000000..062dd2e --- /dev/null +++ b/roles/graphite/meta/main.yml @@ -0,0 +1,20 @@ +--- +dependencies: + - role: apache + - role: source-install + source_install: + name: graphite + virtualenvs: + - path: "{{ graphite.path.virtualenv }}" + owner: root + system_deps: "{{ graphite.system_deps }}" + alternatives: "{{ graphite.alternatives }}" + pip_virtualenv: "{{ graphite.path.virtualenv }}" + pip_packages: "{{ graphite.pip_packages }}" + cleanup: "{{ graphite.cleanup }}" + - role: logging-config + service: graphite + logdata: "{{ graphite.logs }}" + forward_type: "{{ graphite.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/graphite/tasks/checks.yml b/roles/graphite/tasks/checks.yml new file mode 100644 index 0000000..6bf1cc7 --- /dev/null +++ b/roles/graphite/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install carbon-cache process check + sensu_check_dict: name="check-carbon-cache-process" check="{{ sensu_checks.graphite.check_carbon_cache_process }}" + notify: restart sensu-client missing ok diff --git a/roles/graphite/tasks/main.yml b/roles/graphite/tasks/main.yml new file mode 100644 index 0000000..a10cc96 --- /dev/null +++ b/roles/graphite/tasks/main.yml @@ -0,0 +1,171 @@ +--- +- name: create graphite user + user: + name: graphite + comment: graphite + shell: /bin/false + system: yes + home: /nonexistent + +- name: graphite dirs owned by graphite user + file: + path: "{{ item }}" + state: directory + owner: graphite + group: graphite + with_items: + - "{{ graphite.path.home }}" + - "{{ graphite.path.virtualenv }}" + - "{{ graphite.path.virtualenv }}/conf" + - "{{ graphite.path.data }}" + - "{{ graphite.path.data }}/whisper" + - "{{ graphite.path.data }}/rrd" + - /var/run/graphite + - /var/log/graphite + - /var/log/graphite/webapp + +# pip install carbon isn't idempotent. lets make it so. +- name: check if carbon is installed + stat: + path: "{{ graphite.path.virtualenv }}/bin/carbon-cache.py" + register: carbon + +- name: pip install carbon + pip: + name: carbon + virtualenv: "{{ graphite.path.virtualenv }}" + when: not carbon.stat.exists + +- name: install carbon-cache service + template: + src: etc/init/carbon-cache.conf + dest: /etc/init/carbon-cache.conf + when: ansible_distribution_version == "14.04" + +- name: install carbon-cache service + systemd_service: + name: carbon-cache + description: Graphite carbon-cache-a instance. + cmd: "{{ graphite.path.virtualenv }}/bin/twistd" + args: "--nodaemon --reactor=epoll --no_save carbon-cache --config {{ graphite.path.home }}/conf/carbon.conf" + env_vars: + - GRAPHITE_ROOT={{ graphite.path.home }} + - PYTHONPATH={{ graphite.path.install_root }}/opt/graphite/lib + user: graphite + group: graphite + restart: always + service_type: simple + notify: restart carbon-cache + when: ansible_distribution_version != "14.04" + +- name: configure carbon + template: + src: opt/graphite/conf/{{ item }} + dest: "{{ graphite.path.home }}/conf/{{ item }}" + owner: graphite + with_items: + - carbon.conf + - storage-schemas.conf + - graphite.wsgi + notify: + - restart carbon-cache + +# pip install graphite-web isn't idempotent. lets make it so. +- name: check if graphite-web is installed + stat: + path: "{{ graphite.path.install_root }}/webapp/graphite/storage.py" + register: graphite_web + +- name: pip install graphite-web + pip: + name: graphite-web + virtualenv: "{{ graphite.path.virtualenv }}" + when: not graphite_web.stat.exists + notify: setup graphite database + +- name: graphite webapp configuration + template: + src: opt/graphite/webapp/graphite/local_settings.py + dest: "{{ graphite.path.install_root }}/webapp/graphite/local_settings.py" + notify: + - restart apache + +# GPH034 +- name: start and enable graphite carbon + service: + name: carbon-cache + state: started + enabled: yes + +- meta: flush_handlers + +- name: set up log rotation for graphite carbon-cache + logrotate: + name: graphite-carbon + path: /var/log/graphite/*.log + args: + options: + - "{{ graphite.logrotate.frequency }}" + - "size {{ graphite.logrotate.size }}" + - "rotate {{ graphite.logrotate.rotations }}" + - missingok + - copytruncate + - compress + - notifempty + +- name: set up log rotation for graphite webapp + logrotate: + name: graphite-webapp + path: /var/log/graphite/webapp/*.log + args: + options: + - "{{ graphite.logrotate.frequency }}" + - "size {{ graphite.logrotate.size }}" + - "rotate {{ graphite.logrotate.rotations }}" + - missingok + - copytruncate + - compress + - notifempty + +- name: graphite vhosts configuration + template: + src: etc/apache2/sites-available/graphite + dest: /etc/apache2/sites-available/graphite.conf + +- name: enable graphite vhost + apache2_site: + state: enabled + name: graphite + notify: + - restart apache + +- name: allow graphite to be polled + ufw: + rule: allow + proto: tcp + to_port: "{{ graphite.remote_poll.port }}" + src: "{{ item }}" + when: graphite.remote_poll.enabled|default(False)|bool + with_items: "{{ graphite.remote_poll.allowed_networks }}" + tags: + - firewall + +- meta: flush_handlers + +- name: ensure apache is running + service: + name: apache2 + state: started + enabled: yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/graphite/tasks/metrics.yml b/roles/graphite/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/graphite/tasks/serverspec.yml b/roles/graphite/tasks/serverspec.yml new file mode 100644 index 0000000..3ed1877 --- /dev/null +++ b/roles/graphite/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests graphite tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/graphite/templates/etc/apache2/sites-available/graphite b/roles/graphite/templates/etc/apache2/sites-available/graphite new file mode 100644 index 0000000..fa238ca --- /dev/null +++ b/roles/graphite/templates/etc/apache2/sites-available/graphite @@ -0,0 +1,64 @@ +# {{ ansible_managed }} + +# This line also needs to be in your server's config. +# LoadModule wsgi_module modules/mod_wsgi.so + +# You need to manually edit this file to fit your needs. +# This configuration assumes the default installation prefix +# of {{ graphite.path.virtualenv }}/, if you installed graphite somewhere else +# you will need to change all the occurances of {{ graphite.path.virtualenv }}/ +# in this file to your chosen install location. + + + LoadModule wsgi_module modules/mod_wsgi.so + + +# XXX You need to set this up! +# Read http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGISocketPrefix +WSGISocketPrefix /var/run/wsgi + + + ServerName graphite + DocumentRoot "{{ graphite.path.virtualenv }}/webapp" + ErrorLog /var/log/apache2/graphite_error.log + CustomLog /var/log/apache2/graphite_access.log common + + # I've found that an equal number of processes & threads tends + # to show the best performance for Graphite (ymmv). + WSGIDaemonProcess graphite processes=5 threads=5 display-name='%{GROUP}' inactivity-timeout=120 user='graphite' + WSGIProcessGroup graphite + WSGIApplicationGroup %{GLOBAL} + WSGIImportScript {{ graphite.path.home }}/conf/graphite.wsgi process-group=graphite application-group=%{GLOBAL} + + # XXX You will need to create this file! There is a graphite.wsgi.example + # file in this directory that you can safely use, just copy it to graphite.wgsi + WSGIScriptAlias / {{ graphite.path.home }}/conf/graphite.wsgi + + Alias /content/ {{ graphite.path.virtualenv }}/webapp/content/ + + SetHandler None + + + # XXX In order for the django admin site media to work you + # must change @DJANGO_ROOT@ to be the path to your django + # installation, which is probably something like: + # /usr/lib/python2.6/site-packages/django + Alias /media/ "@DJANGO_ROOT@/contrib/admin/media/" + + SetHandler None + + + # The graphite.wsgi file has to be accessible by apache. It won't + # be visible to clients because of the DocumentRoot though. + + Order deny,allow + Allow from all + Require all granted + + + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + diff --git a/roles/graphite/templates/etc/init/carbon-cache.conf b/roles/graphite/templates/etc/init/carbon-cache.conf new file mode 100644 index 0000000..e88f596 --- /dev/null +++ b/roles/graphite/templates/etc/init/carbon-cache.conf @@ -0,0 +1,23 @@ +# {{ ansible_managed }} + +description "Graphite carbon-cache-a instance." +author "Puppet" + +start on runlevel [2345] +stop on runlevel [016] + +env GRAPHITE_ROOT={{ graphite.path.home }} +env PYTHONPATH={{ graphite.path.install_root }}/lib + +setuid graphite +setgid graphite + +script + exec twistd \ + --nodaemon \ + --reactor=epoll \ + --no_save \ + carbon-cache \ + --config $GRAPHITE_ROOT/conf/carbon.conf \ + 2>&1 >> /var/log/graphite/carbon-cache-a.log +end script diff --git a/roles/graphite/templates/opt/graphite/conf/carbon.conf b/roles/graphite/templates/opt/graphite/conf/carbon.conf new file mode 100644 index 0000000..5d2a700 --- /dev/null +++ b/roles/graphite/templates/opt/graphite/conf/carbon.conf @@ -0,0 +1,364 @@ +# {{ ansible_managed }} + +[cache] +# Configure carbon directories. +# +# OS environment variables can be used to tell carbon where graphite is +# installed, where to read configuration from and where to write data. +# +# GRAPHITE_ROOT - Root directory of the graphite installation. +# Defaults to ../ +# GRAPHITE_CONF_DIR - Configuration directory (where this file lives). +# Defaults to $GRAPHITE_ROOT/conf/ +# GRAPHITE_STORAGE_DIR - Storage directory for whipser/rrd/log/pid files. +# Defaults to $GRAPHITE_ROOT/storage/ +GRAPHITE_STORAGE_DIR = {{ graphite.path.data }} +# +# To change other directory paths, add settings to this file. The following +# configuration variables are available with these default values: +# +STORAGE_DIR = {{ graphite.path.data }} +LOCAL_DATA_DIR = {{ graphite.path.data }}/whisper/ +# WHITELISTS_DIR = STORAGE_DIR/lists/ +CONF_DIR = {{ graphite.path.home }}/conf/ +# LOG_DIR = STORAGE_DIR/log/ +# PID_DIR = STORAGE_DIR/ +# +# For FHS style directory structures, use: +# +# STORAGE_DIR = /var/lib/carbon/ +# CONF_DIR = /etc/carbon/ +# LOG_DIR = /var/log/carbon/ +PID_DIR = /var/run/graphite +# +#LOCAL_DATA_DIR = /opt/graphite/storage/whisper/ + +# Enable daily log rotation. If disabled, a kill -HUP can be used after a manual rotate +ENABLE_LOGROTATION = True + +# Specify the user to drop privileges to +# If this is blank carbon runs as the user that invokes it +# This user must have write access to the local data directory +USER = +# +# NOTE: The above settings must be set under [relay] and [aggregator] +# to take effect for those daemons as well + +# Limit the size of the cache to avoid swapping or becoming CPU bound. +# Sorts and serving cache queries gets more expensive as the cache grows. +# Use the value "inf" (infinity) for an unlimited cache size. +MAX_CACHE_SIZE = inf + +# Limits the number of whisper update_many() calls per second, which effectively +# means the number of write requests sent to the disk. This is intended to +# prevent over-utilizing the disk and thus starving the rest of the system. +# When the rate of required updates exceeds this, then carbon's caching will +# take effect and increase the overall throughput accordingly. +MAX_UPDATES_PER_SECOND = 500 + +# If defined, this changes the MAX_UPDATES_PER_SECOND in Carbon when a +# stop/shutdown is initiated. This helps when MAX_UPDATES_PER_SECOND is +# relatively low and carbon has cached a lot of updates; it enables the carbon +# daemon to shutdown more quickly. +# MAX_UPDATES_PER_SECOND_ON_SHUTDOWN = 1000 + +# Softly limits the number of whisper files that get created each minute. +# Setting this value low (like at 50) is a good way to ensure your graphite +# system will not be adversely impacted when a bunch of new metrics are +# sent to it. The trade off is that it will take much longer for those metrics' +# database files to all get created and thus longer until the data becomes usable. +# Setting this value high (like "inf" for infinity) will cause graphite to create +# the files quickly but at the risk of slowing I/O down considerably for a while. +MAX_CREATES_PER_MINUTE = 50 + +LINE_RECEIVER_INTERFACE = 0.0.0.0 +LINE_RECEIVER_PORT = 2003 + +# Set this to True to enable the UDP listener. By default this is off +# because it is very common to run multiple carbon daemons and managing +# another (rarely used) port for every carbon instance is not fun. +ENABLE_UDP_LISTENER = False +UDP_RECEIVER_INTERFACE = 0.0.0.0 +UDP_RECEIVER_PORT = 2003 + +PICKLE_RECEIVER_INTERFACE = 0.0.0.0 +PICKLE_RECEIVER_PORT = 2004 + +# Set to false to disable logging of successful connections +LOG_LISTENER_CONNECTIONS = False + +# Per security concerns outlined in Bug #817247 the pickle receiver +# will use a more secure and slightly less efficient unpickler. +# Set this to True to revert to the old-fashioned insecure unpickler. +USE_INSECURE_UNPICKLER = False + +CACHE_QUERY_INTERFACE = 0.0.0.0 +CACHE_QUERY_PORT = 7002 + +# Set this to False to drop datapoints received after the cache +# reaches MAX_CACHE_SIZE. If this is True (the default) then sockets +# over which metrics are received will temporarily stop accepting +# data until the cache size falls below 95% MAX_CACHE_SIZE. +USE_FLOW_CONTROL = True + +# By default, carbon-cache will log every whisper update and cache hit. This can be excessive and +# degrade performance if logging on the same volume as the whisper data is stored. +LOG_UPDATES = False +LOG_CACHE_HITS = False +LOG_CACHE_QUEUE_SORTS = True + +# The thread that writes metrics to disk can use on of the following strategies +# determining the order in which metrics are removed from cache and flushed to +# disk. The default option preserves the same behavior as has been historically +# available in version 0.9.10. +# +# sorted - All metrics in the cache will be counted and an ordered list of +# them will be sorted according to the number of datapoints in the cache at the +# moment of the list's creation. Metrics will then be flushed from the cache to +# disk in that order. +# +# max - The writer thread will always pop and flush the metric from cache +# that has the most datapoints. This will give a strong flush preference to +# frequently updated metrics and will also reduce random file-io. Infrequently +# updated metrics may only ever be persisted to disk at daemon shutdown if +# there are a large number of metrics which receive very frequent updates OR if +# disk i/o is very slow. +# +# naive - Metrics will be flushed from the cache to disk in an unordered +# fashion. This strategy may be desirable in situations where the storage for +# whisper files is solid state, CPU resources are very limited or deference to +# the OS's i/o scheduler is expected to compensate for the random write +# pattern. +# +CACHE_WRITE_STRATEGY = sorted + +# On some systems it is desirable for whisper to write synchronously. +# Set this option to True if you'd like to try this. Basically it will +# shift the onus of buffering writes from the kernel into carbon's cache. +WHISPER_AUTOFLUSH = False + +# By default new Whisper files are created pre-allocated with the data region +# filled with zeros to prevent fragmentation and speed up contiguous reads and +# writes (which are common). Enabling this option will cause Whisper to create +# the file sparsely instead. Enabling this option may allow a large increase of +# MAX_CREATES_PER_MINUTE but may have longer term performance implications +# depending on the underlying storage configuration. +# WHISPER_SPARSE_CREATE = False + +# Only beneficial on linux filesystems that support the fallocate system call. +# It maintains the benefits of contiguous reads/writes, but with a potentially +# much faster creation speed, by allowing the kernel to handle the block +# allocation and zero-ing. Enabling this option may allow a large increase of +# MAX_CREATES_PER_MINUTE. If enabled on an OS or filesystem that is unsupported +# this option will gracefully fallback to standard POSIX file access methods. +WHISPER_FALLOCATE_CREATE = True + +# Enabling this option will cause Whisper to lock each Whisper file it writes +# to with an exclusive lock (LOCK_EX, see: man 2 flock). This is useful when +# multiple carbon-cache daemons are writing to the same files +# WHISPER_LOCK_WRITES = False + +# Set this to True to enable whitelisting and blacklisting of metrics in +# CONF_DIR/whitelist and CONF_DIR/blacklist. If the whitelist is missing or +# empty, all metrics will pass through +# USE_WHITELIST = False + +# By default, carbon itself will log statistics (such as a count, +# metricsReceived) with the top level prefix of 'carbon' at an interval of 60 +# seconds. Set CARBON_METRIC_INTERVAL to 0 to disable instrumentation +# CARBON_METRIC_PREFIX = carbon +# CARBON_METRIC_INTERVAL = 60 + +# Enable AMQP if you want to receve metrics using an amqp broker +ENABLE_AMQP = {{ graphite.amqp.enabled }} + +# Verbose means a line will be logged for every metric received +# useful for testing +{% if graphite.amqp.enabled %} +AMQP_VERBOSE = {{ graphite.amqp.verbose }} + +AMQP_HOST = {{ graphite.amqp.host }} +AMQP_PORT = {{ graphite.amqp.port }} +AMQP_VHOST = {{ graphite.amqp.vhost }} +AMQP_USER = {{ graphite.amqp.user }} +AMQP_PASSWORD = {{ graphite.amqp.password }} +AMQP_EXCHANGE = {{ graphite.amqp.exchange }} +AMQP_METRIC_NAME_IN_BODY = {{ graphite.amqp.metric_name_in_body }} +{% endif %} + +# The manhole interface allows you to SSH into the carbon daemon +# and get a python interpreter. BE CAREFUL WITH THIS! If you do +# something like time.sleep() in the interpreter, the whole process +# will sleep! This is *extremely* helpful in debugging, assuming +# you are familiar with the code. If you are not, please don't +# mess with this, you are asking for trouble :) +# +# ENABLE_MANHOLE = False +# MANHOLE_INTERFACE = 127.0.0.1 +# MANHOLE_PORT = 7222 +# MANHOLE_USER = admin +# MANHOLE_PUBLIC_KEY = ssh-rsa AAAAB3NzaC1yc2EAAAABiwAaAIEAoxN0sv/e4eZCPpi3N3KYvyzRaBaMeS2RsOQ/cDuKv11dlNzVeiyc3RFmCv5Rjwn/lQ79y0zyHxw67qLyhQ/kDzINc4cY41ivuQXm2tPmgvexdrBv5nsfEpjs3gLZfJnyvlcVyWK/lId8WUvEWSWHTzsbtmXAF2raJMdgLTbQ8wE= + +# Patterns for all of the metrics this machine will store. Read more at +# http://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol#Bindings +# +# Example: store all sales, linux servers, and utilization metrics +# BIND_PATTERNS = sales.#, servers.linux.#, #.utilization +# +# Example: store everything +# BIND_PATTERNS = # + +# To configure special settings for the carbon-cache instance 'b', uncomment this: +#[cache:b] +#LINE_RECEIVER_PORT = 2103 +#PICKLE_RECEIVER_PORT = 2104 +#CACHE_QUERY_PORT = 7102 +# and any other settings you want to customize, defaults are inherited +# from [carbon] section. +# You can then specify the --instance=b option to manage this instance + + + +[relay] +LINE_RECEIVER_INTERFACE = 0.0.0.0 +LINE_RECEIVER_PORT = 2013 +PICKLE_RECEIVER_INTERFACE = 0.0.0.0 +PICKLE_RECEIVER_PORT = 2014 + +# Set to false to disable logging of successful connections +LOG_LISTENER_CONNECTIONS = False + +# Carbon-relay has several options for metric routing controlled by RELAY_METHOD +# +# Use relay-rules.conf to route metrics to destinations based on pattern rules +#RELAY_METHOD = rules +# +# Use consistent-hashing for even distribution of metrics between destinations +#RELAY_METHOD = consistent-hashing +# +# Use consistent-hashing but take into account an aggregation-rules.conf shared +# by downstream carbon-aggregator daemons. This will ensure that all metrics +# that map to a given aggregation rule are sent to the same carbon-aggregator +# instance. +# Enable this for carbon-relays that send to a group of carbon-aggregators +#RELAY_METHOD = aggregated-consistent-hashing +RELAY_METHOD = rules + +# If you use consistent-hashing you can add redundancy by replicating every +# datapoint to more than one machine. +REPLICATION_FACTOR = 1 + +# This is a list of carbon daemons we will send any relayed or +# generated metrics to. The default provided would send to a single +# carbon-cache instance on the default port. However if you +# use multiple carbon-cache instances then it would look like this: +# +# DESTINATIONS = 127.0.0.1:2004:a, 127.0.0.1:2104:b +# +# The general form is IP:PORT:INSTANCE where the :INSTANCE part is +# optional and refers to the "None" instance if omitted. +# +# Note that if the destinations are all carbon-caches then this should +# exactly match the webapp's CARBONLINK_HOSTS setting in terms of +# instances listed (order matters!). +# +# If using RELAY_METHOD = rules, all destinations used in relay-rules.conf +# must be defined in this list +DESTINATIONS = 127.0.0.1:2004 + +# This defines the maximum "message size" between carbon daemons. +# You shouldn't need to tune this unless you really know what you're doing. +MAX_DATAPOINTS_PER_MESSAGE = 500 +MAX_QUEUE_SIZE = 10000 + +# Set this to False to drop datapoints when any send queue (sending datapoints +# to a downstream carbon daemon) hits MAX_QUEUE_SIZE. If this is True (the +# default) then sockets over which metrics are received will temporarily stop accepting +# data until the send queues fall below 80% MAX_QUEUE_SIZE. +USE_FLOW_CONTROL = True + +# Set this to True to enable whitelisting and blacklisting of metrics in +# CONF_DIR/whitelist and CONF_DIR/blacklist. If the whitelist is missing or +# empty, all metrics will pass through +# USE_WHITELIST = False + +# By default, carbon itself will log statistics (such as a count, +# metricsReceived) with the top level prefix of 'carbon' at an interval of 60 +# seconds. Set CARBON_METRIC_INTERVAL to 0 to disable instrumentation +# CARBON_METRIC_PREFIX = carbon +# CARBON_METRIC_INTERVAL = 60 + + +[aggregator] +LINE_RECEIVER_INTERFACE = 0.0.0.0 +LINE_RECEIVER_PORT = 2023 + +PICKLE_RECEIVER_INTERFACE = 0.0.0.0 +PICKLE_RECEIVER_PORT = 2024 + +# Set to false to disable logging of successful connections +LOG_LISTENER_CONNECTIONS = False + +# If set true, metric received will be forwarded to DESTINATIONS in addition to +# the output of the aggregation rules. If set false the carbon-aggregator will +# only ever send the output of aggregation. +FORWARD_ALL = True + +# This is a list of carbon daemons we will send any relayed or +# generated metrics to. The default provided would send to a single +# carbon-cache instance on the default port. However if you +# use multiple carbon-cache instances then it would look like this: +# +# DESTINATIONS = 127.0.0.1:2004:a, 127.0.0.1:2104:b +# +# The format is comma-delimited IP:PORT:INSTANCE where the :INSTANCE part is +# optional and refers to the "None" instance if omitted. +# +# Note that if the destinations are all carbon-caches then this should +# exactly match the webapp's CARBONLINK_HOSTS setting in terms of +# instances listed (order matters!). +DESTINATIONS = 127.0.0.1:2004 + +# If you want to add redundancy to your data by replicating every +# datapoint to more than one machine, increase this. +REPLICATION_FACTOR = 1 + +# This is the maximum number of datapoints that can be queued up +# for a single destination. Once this limit is hit, we will +# stop accepting new data if USE_FLOW_CONTROL is True, otherwise +# we will drop any subsequently received datapoints. +MAX_QUEUE_SIZE = 10000 + +# Set this to False to drop datapoints when any send queue (sending datapoints +# to a downstream carbon daemon) hits MAX_QUEUE_SIZE. If this is True (the +# default) then sockets over which metrics are received will temporarily stop accepting +# data until the send queues fall below 80% MAX_QUEUE_SIZE. +USE_FLOW_CONTROL = True + +# This defines the maximum "message size" between carbon daemons. +# You shouldn't need to tune this unless you really know what you're doing. +MAX_DATAPOINTS_PER_MESSAGE = 500 + +# This defines how many datapoints the aggregator remembers for +# each metric. Aggregation only happens for datapoints that fall in +# the past MAX_AGGREGATION_INTERVALS * intervalSize seconds. +MAX_AGGREGATION_INTERVALS = 5 + +# By default (WRITE_BACK_FREQUENCY = 0), carbon-aggregator will write back +# aggregated data points once every rule.frequency seconds, on a per-rule basis. +# Set this (WRITE_BACK_FREQUENCY = N) to write back all aggregated data points +# every N seconds, independent of rule frequency. This is useful, for example, +# to be able to query partially aggregated metrics from carbon-cache without +# having to first wait rule.frequency seconds. +# WRITE_BACK_FREQUENCY = 0 + +# Set this to True to enable whitelisting and blacklisting of metrics in +# CONF_DIR/whitelist and CONF_DIR/blacklist. If the whitelist is missing or +# empty, all metrics will pass through +# USE_WHITELIST = False + +# By default, carbon itself will log statistics (such as a count, +# metricsReceived) with the top level prefix of 'carbon' at an interval of 60 +# seconds. Set CARBON_METRIC_INTERVAL to 0 to disable instrumentation +# CARBON_METRIC_PREFIX = carbon +# CARBON_METRIC_INTERVAL = 60 diff --git a/roles/graphite/templates/opt/graphite/conf/graphite.wsgi b/roles/graphite/templates/opt/graphite/conf/graphite.wsgi new file mode 100644 index 0000000..8c70f26 --- /dev/null +++ b/roles/graphite/templates/opt/graphite/conf/graphite.wsgi @@ -0,0 +1,25 @@ +# {{ ansible_managed }} + +import os +import sys +import site + +site.addsitedir('{{ graphite.path.install_root }}') + +sys.path.append('{{ graphite.path.virtualenv }}/webapp') +sys.path.append('{{ graphite.path.install_root }}/opt/graphite/webapp') + +os.environ['DJANGO_SETTINGS_MODULE'] = 'graphite.settings' + +import django.core.handlers.wsgi + +application = django.core.handlers.wsgi.WSGIHandler() + +# READ THIS +# Initializing the search index can be very expensive, please include +# the WSGIImportScript directive pointing to this script in your vhost +# config to ensure the index is preloaded before any requests are handed +# to the process. +from graphite.logger import log +log.info("graphite.wsgi - pid %d - reloading search index" % os.getpid()) +import graphite.metrics.search diff --git a/roles/graphite/templates/opt/graphite/conf/storage-schemas.conf b/roles/graphite/templates/opt/graphite/conf/storage-schemas.conf new file mode 100644 index 0000000..01a4fb3 --- /dev/null +++ b/roles/graphite/templates/opt/graphite/conf/storage-schemas.conf @@ -0,0 +1,24 @@ +# {{ ansible_managed }} + +# Schema definitions for Whisper files. Entries are scanned in order, +# and first match wins. This file is scanned for changes every 60 seconds. +# +# [name] +# pattern = regex +# retentions = timePerPoint:timeToStore, timePerPoint:timeToStore, ... +# Priority order as follows: + +[{{ graphite.storage_schemas.tap.name }}] +# {{ graphite.storage_schemas.tap.comment }} +pattern = {{ graphite.storage_schemas.tap.pattern }} +retentions = {{ graphite.storage_schemas.tap.retentions }} + +[{{ graphite.storage_schemas.carbon.name }}] +# {{ graphite.storage_schemas.carbon.comment }} +pattern = {{ graphite.storage_schemas.carbon.pattern }} +retentions = {{ graphite.storage_schemas.carbon.retentions }} + +[{{ graphite.storage_schemas.default.name }}] +# {{ graphite.storage_schemas.default.comment }} +pattern = {{ graphite.storage_schemas.default.pattern }} +retentions = {{ graphite.storage_schemas.default.retentions }} diff --git a/roles/graphite/templates/opt/graphite/webapp/graphite/local_settings.py b/roles/graphite/templates/opt/graphite/webapp/graphite/local_settings.py new file mode 100644 index 0000000..e05b607 --- /dev/null +++ b/roles/graphite/templates/opt/graphite/webapp/graphite/local_settings.py @@ -0,0 +1,245 @@ +## Graphite local_settings.py +# Edit this file to customize the default Graphite webapp settings +# +# Additional customizations to Django settings can be added to this file as well + +##################################### +# General Configuration # +##################################### +# Set this to a long, random unique string to use as a secret key for this +# install. This key is used for salting of hashes used in auth tokens, +# CRSF middleware, cookie storage, etc. This should be set identically among +# instances if used behind a load balancer. +SECRET_KEY = '{{ graphite.secret_key }}' + +# In Django 1.5+ set this to the list of hosts your graphite instances is +# accessible as. See: +# https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-ALLOWED_HOSTS +#ALLOWED_HOSTS = [ '*' ] + +# Set your local timezone (Django's default is America/Chicago) +# If your graphs appear to be offset by a couple hours then this probably +# needs to be explicitly set to your local timezone. +TIME_ZONE = 'Etc/UTC' + +# Override this to provide documentation specific to your Graphite deployment +#DOCUMENTATION_URL = "http://graphite.readthedocs.org/" + +# Logging +#LOG_RENDERING_PERFORMANCE = True +#LOG_CACHE_PERFORMANCE = True +#LOG_METRIC_ACCESS = True + +# Enable full debug page display on exceptions (Internal Server Error pages) +#DEBUG = True + +# If using RRD files and rrdcached, set to the address or socket of the daemon +#FLUSHRRDCACHED = 'unix:/var/run/rrdcached.sock' + +# This lists the memcached servers that will be used by this webapp. +# If you have a cluster of webapps you should ensure all of them +# have the *exact* same value for this setting. That will maximize cache +# efficiency. Setting MEMCACHE_HOSTS to be empty will turn off use of +# memcached entirely. +# +# You should not use the loopback address (127.0.0.1) here if using clustering +# as every webapp in the cluster should use the exact same values to prevent +# unneeded cache misses. Set to [] to disable caching of images and fetched data +#MEMCACHE_HOSTS = ['10.10.10.10:11211', '10.10.10.11:11211', '10.10.10.12:11211'] +#DEFAULT_CACHE_DURATION = 60 # Cache images and data for 1 minute + + +##################################### +# Filesystem Paths # +##################################### +# Change only GRAPHITE_ROOT if your install is merely shifted from /opt/graphite +# to somewhere else +# GRAPHITE_ROOT = '/opt/graphite' + +# Most installs done outside of a separate tree such as /opt/graphite will only +# need to change these three settings. Note that the default settings for each +# of these is relative to GRAPHITE_ROOT +# CONF_DIR = '/opt/graphite/conf' +STORAGE_DIR = '{{ graphite.path.data }}' +# CONTENT_DIR = '/opt/graphite/webapp/content' + +# To further or fully customize the paths, modify the following. Note that the +# default settings for each of these are relative to CONF_DIR and STORAGE_DIR +# +## Webapp config files +DASHBOARD_CONF = '/opt/graphite/conf/dashboard.conf' +GRAPHTEMPLATES_CONF = '/opt/graphite/conf/graphTemplates.conf' + +## Data directories +# NOTE: If any directory is unreadable in DATA_DIRS it will break metric browsing +WHISPER_DIR = '{{ graphite.path.data }}/whisper' +RRD_DIR = '{{ graphite.path.data }}/rrd' +#DATA_DIRS = [WHISPER_DIR, RRD_DIR] # Default: set from the above variables +LOG_DIR = '/var/log/graphite/webapp' +INDEX_FILE = '{{ graphite.path.data }}/index' # Search index file + + +##################################### +# Email Configuration # +##################################### +# This is used for emailing rendered Graphs +# Default backend is SMTP +#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +#EMAIL_HOST = 'localhost' +#EMAIL_PORT = 25 +#EMAIL_HOST_USER = '' +#EMAIL_HOST_PASSWORD = '' +#EMAIL_USE_TLS = False +# To drop emails on the floor, enable the Dummy backend: +#EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' + + +##################################### +# Authentication Configuration # +##################################### +## LDAP / ActiveDirectory authentication setup +#USE_LDAP_AUTH = True +#LDAP_SERVER = "ldap.mycompany.com" +#LDAP_PORT = 389 +# OR +#LDAP_URI = "ldaps://ldap.mycompany.com:636" +#LDAP_SEARCH_BASE = "OU=users,DC=mycompany,DC=com" +#LDAP_BASE_USER = "CN=some_readonly_account,DC=mycompany,DC=com" +#LDAP_BASE_PASS = "readonly_account_password" +#LDAP_USER_QUERY = "(username=%s)" #For Active Directory use "(sAMAccountName=%s)" +# +# If you want to further customize the ldap connection options you should +# directly use ldap.set_option to set the ldap module's global options. +# For example: +# +#import ldap +#ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW) +#ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, "/etc/ssl/ca") +#ldap.set_option(ldap.OPT_X_TLS_CERTFILE, "/etc/ssl/mycert.pem") +#ldap.set_option(ldap.OPT_X_TLS_KEYFILE, "/etc/ssl/mykey.pem") +# See http://www.python-ldap.org/ for further details on these options. + +## REMOTE_USER authentication. See: https://docs.djangoproject.com/en/dev/howto/auth-remote-user/ +#USE_REMOTE_USER_AUTHENTICATION = True + +# Override the URL for the login link (e.g. for django_openid_auth) +#LOGIN_URL = '/account/login' + + +########################## +# Database Configuration # +########################## +# By default sqlite is used. If you cluster multiple webapps you will need +# to setup an external database (such as MySQL) and configure all of the webapp +# instances to use the same database. Note that this database is only used to store +# Django models such as saved graphs, dashboards, user preferences, etc. +# Metric data is not stored here. +# +# DO NOT FORGET TO RUN 'manage.py syncdb' AFTER SETTING UP A NEW DATABASE +# +# The following built-in database engines are available: +# django.db.backends.postgresql # Removed in Django 1.4 +# django.db.backends.postgresql_psycopg2 +# django.db.backends.mysql +# django.db.backends.sqlite3 +# django.db.backends.oracle +# +# The default is 'django.db.backends.sqlite3' with file 'graphite.db' +# located in STORAGE_DIR +# +DATABASES = { +{% if graphite.mysql.enabled -%} + 'default': { + 'NAME': '{{ graphite.mysql.database }}', + 'ENGINE': 'django.db.backends.mysql', + 'USER': '{{ graphite.mysql.username }}', + 'PASSWORD': '{{ graphite.mysql.password }}', + 'HOST': '{{ graphite.mysql.host }}', + 'PORT': '{{ graphite.mysql.port }}' + } +{%- else -%} + 'default': { + 'NAME': '{{ graphite.path.data }}/graphite.db', + 'ENGINE': 'django.db.backends.sqlite3', + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '' + } +{% endif -%} +} + + +######################### +# Cluster Configuration # +######################### +# (To avoid excessive DNS lookups you want to stick to using IP addresses only in this entire section) +# +# This should list the IP address (and optionally port) of the webapp on each +# remote server in the cluster. These servers must each have local access to +# metric data. Note that the first server to return a match for a query will be +# used. +#CLUSTER_SERVERS = ["10.0.2.2:80", "10.0.2.3:80"] + +## These are timeout values (in seconds) for requests to remote webapps +#REMOTE_STORE_FETCH_TIMEOUT = 6 # Timeout to fetch series data +#REMOTE_STORE_FIND_TIMEOUT = 2.5 # Timeout for metric find requests +#REMOTE_STORE_RETRY_DELAY = 60 # Time before retrying a failed remote webapp +#REMOTE_STORE_USE_POST = False # Use POST instead of GET for remote requests +#REMOTE_FIND_CACHE_DURATION = 300 # Time to cache remote metric find results + +# Provide a list of HTTP headers that you want forwarded on from this host +# when making a request to a remote webapp server in CLUSTER_SERVERS +#REMOTE_STORE_FORWARD_HEADERS = [] # An iterable of HTTP header names + +## Prefetch cache +# set to True to fetch all metrics using a single http request per remote server +# instead of one http request per target, per remote server. +# Especially useful when generating graphs with more than 4-5 targets or if +# there's significant latency between this server and the backends. (>20ms) +#REMOTE_PREFETCH_DATA = False + +# During a rebalance of a consistent hash cluster, after a partition event on a replication > 1 cluster, +# or in other cases we might receive multiple TimeSeries data for a metric key. Merge them together rather +# that choosing the "most complete" one (pre-0.9.14 behaviour). +#REMOTE_STORE_MERGE_RESULTS = True + +## Remote rendering settings +# Set to True to enable rendering of Graphs on a remote webapp +#REMOTE_RENDERING = True +# List of IP (and optionally port) of the webapp on each remote server that +# will be used for rendering. Note that each rendering host should have local +# access to metric data or should have CLUSTER_SERVERS configured +#RENDERING_HOSTS = [] +#REMOTE_RENDER_CONNECT_TIMEOUT = 1.0 + +# If you are running multiple carbon-caches on this machine (typically behind a relay using +# consistent hashing), you'll need to list the ip address, cache query port, and instance name of each carbon-cache +# instance on the local machine (NOT every carbon-cache in the entire cluster). The default cache query port is 7002 +# and a common scheme is to use 7102 for instance b, 7202 for instance c, etc. +# +# You *should* use 127.0.0.1 here in most cases +#CARBONLINK_HOSTS = ["127.0.0.1:7002:a", "127.0.0.1:7102:b", "127.0.0.1:7202:c"] +#CARBONLINK_TIMEOUT = 1.0 +# Using 'query-bulk' queries for carbon +# It's more effective, but python-carbon 0.9.13 (or latest from 0.9.x branch) is required +# See https://github.com/graphite-project/carbon/pull/132 for details +#CARBONLINK_QUERY_BULK = False + +# Type of metric hashing function. +# The default `carbon_ch` is Graphite's traditional consistent-hashing implementation. +# Alternatively, you can use `fnv1a_ch`, which supports the Fowler-Noll-Vo hash +# function (FNV-1a) hash implementation offered by the carbon-c-relay project +# https://github.com/grobian/carbon-c-relay +# +# Supported values: carbon_ch, fnv1a_ch +# +#CARBONLINK_HASHING_TYPE = 'carbon_ch' + +##################################### +# Additional Django Settings # +##################################### +# Uncomment the following line for direct access to Django settings such as +# MIDDLEWARE_CLASSES or APPS +#from graphite.app_settings import * + diff --git a/roles/graphite/templates/opt/graphite/webapp/graphite/settings.py b/roles/graphite/templates/opt/graphite/webapp/graphite/settings.py new file mode 100644 index 0000000..96ee663 --- /dev/null +++ b/roles/graphite/templates/opt/graphite/webapp/graphite/settings.py @@ -0,0 +1,259 @@ +# {{ ansible_managed }} + +"""Copyright 2008 Orbitz WorldWide + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.""" +# Django settings for graphite project. +# DO NOT MODIFY THIS FILE DIRECTLY - use local_settings.py instead +import sys, os +from django import VERSION as DJANGO_VERSION +from django.core.exceptions import ImproperlyConfigured +from exceptions import DeprecationWarning +from os.path import abspath, dirname, join +from warnings import warn + +try: + import rrdtool +except ImportError: + rrdtool = False + +GRAPHITE_WEB_APP_SETTINGS_LOADED = False +WEBAPP_VERSION = '0.9.15' +DEBUG = False +JAVASCRIPT_DEBUG = False + +# Filesystem layout +WEB_DIR = dirname( abspath(__file__) ) +WEBAPP_DIR = dirname(WEB_DIR) +GRAPHITE_ROOT = dirname(WEBAPP_DIR) +THIRDPARTY_DIR = join(WEB_DIR,'thirdparty') +# Initialize additional path variables +# Defaults for these are set after local_settings is imported +CONTENT_DIR = '' +CSS_DIR = '' +CONF_DIR = '' +DASHBOARD_CONF = '' +GRAPHTEMPLATES_CONF = '' +STORAGE_DIR = '' +WHITELIST_FILE = '' +INDEX_FILE = '' +LOG_DIR = '' +WHISPER_DIR = '' +RRD_DIR = '' +DATA_DIRS = [] + +CLUSTER_SERVERS = [] + +sys.path.insert(0, WEBAPP_DIR) +# Allow local versions of the libs shipped in thirdparty to take precedence +sys.path.append(THIRDPARTY_DIR) + +# Memcache settings +MEMCACHE_HOSTS = [] +DEFAULT_CACHE_DURATION = 60 #metric data and graphs are cached for one minute by default +LOG_CACHE_PERFORMANCE = False + +# Remote store settings +REMOTE_STORE_FETCH_TIMEOUT = 6 +REMOTE_STORE_FIND_TIMEOUT = 2.5 +REMOTE_STORE_RETRY_DELAY = 60 +REMOTE_FIND_CACHE_DURATION = 300 +REMOTE_PREFETCH_DATA = False + +#Remote rendering settings +REMOTE_RENDERING = False #if True, rendering is delegated to RENDERING_HOSTS +RENDERING_HOSTS = [] +REMOTE_RENDER_CONNECT_TIMEOUT = 1.0 +LOG_RENDERING_PERFORMANCE = False + +#Miscellaneous settings +CARBONLINK_HOSTS = ["127.0.0.1:7002"] +CARBONLINK_TIMEOUT = 1.0 +CARBONLINK_QUERY_BULK = False +SMTP_SERVER = "localhost" +DOCUMENTATION_URL = "http://graphite.readthedocs.org/" +ALLOW_ANONYMOUS_CLI = True +LOG_METRIC_ACCESS = False +LEGEND_MAX_ITEMS = 10 + +#Authentication settings +USE_LDAP_AUTH = False +LDAP_SERVER = "" # "ldapserver.mydomain.com" +LDAP_PORT = 389 +LDAP_SEARCH_BASE = "" # "OU=users,DC=mydomain,DC=com" +LDAP_BASE_USER = "" # "CN=some_readonly_account,DC=mydomain,DC=com" +LDAP_BASE_PASS = "" # "my_password" +LDAP_USER_QUERY = "" # "(username=%s)" For Active Directory use "(sAMAccountName=%s)" +LDAP_URI = None + +#Set this to True to delegate authentication to the web server +USE_REMOTE_USER_AUTHENTICATION = False +REMOTE_USER_BACKEND = "" # Provide an alternate or subclassed backend + +# Django 1.5 requires this so we set a default but warn the user +SECRET_KEY = '{{ graphite.secret_key }}' + +# Django 1.5 requires this to be set. Here we default to prior behavior and allow all +ALLOWED_HOSTS = [ '*' ] + +# Override to link a different URL for login (e.g. for django_openid_auth) +LOGIN_URL = '/account/login' + +# Set the default timezone to UTC +TIME_ZONE = 'UTC' + +#Initialize deprecated database settings +DATABASE_ENGINE = '' +DATABASE_NAME = '' +DATABASE_USER = '' +DATABASE_PASSWORD = '' +DATABASE_HOST = '' +DATABASE_PORT = '' + +DATABASES = { +{% if graphite.mysql.enabled -%} + 'default': { + 'NAME': '{{ graphite.mysql.database }}', + 'ENGINE': 'django.db.backends.mysql', + 'USER': '{{ graphite.mysql.username }}', + 'PASSWORD': '{{ graphite.mysql.password }}', + 'HOST': '{{ graphite.mysql.host }}', + 'PORT': '{{ graphite.mysql.port }}' + } +{%- else -%} + 'default': { + 'NAME': '/opt/graphite/storage/graphite.db', + 'ENGINE': 'django.db.backends.sqlite3', + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '' + } +{% endif -%} +} + +# If using rrdcached, set to the address or socket of the daemon +FLUSHRRDCACHED = '' + +## Load our local_settings +try: + from graphite.local_settings import * +except ImportError: + print >> sys.stderr, "Could not import graphite.local_settings, using defaults!" + +## Load Django settings if they werent picked up in local_settings +if not GRAPHITE_WEB_APP_SETTINGS_LOADED: + from graphite.app_settings import * + +## Set config dependent on flags set in local_settings +# Path configuration +if not CONTENT_DIR: + CONTENT_DIR = join(WEBAPP_DIR, 'content') +if not CSS_DIR: + CSS_DIR = join(CONTENT_DIR, 'css') + +if not CONF_DIR: + CONF_DIR = os.environ.get('GRAPHITE_CONF_DIR', join(GRAPHITE_ROOT, 'conf')) +if not DASHBOARD_CONF: + DASHBOARD_CONF = join(CONF_DIR, 'dashboard.conf') +if not GRAPHTEMPLATES_CONF: + GRAPHTEMPLATES_CONF = join(CONF_DIR, 'graphTemplates.conf') + +if not STORAGE_DIR: + STORAGE_DIR = os.environ.get('GRAPHITE_STORAGE_DIR', join(GRAPHITE_ROOT, 'storage')) +if not WHITELIST_FILE: + WHITELIST_FILE = join(STORAGE_DIR, 'lists', 'whitelist') +if not INDEX_FILE: + INDEX_FILE = join(STORAGE_DIR, 'index') +if not LOG_DIR: + LOG_DIR = join(STORAGE_DIR, 'log', 'webapp') +if not WHISPER_DIR: + WHISPER_DIR = join(STORAGE_DIR, 'whisper/') +if not RRD_DIR: + RRD_DIR = join(STORAGE_DIR, 'rrd/') +if not DATA_DIRS: + if rrdtool and os.path.exists(RRD_DIR): + DATA_DIRS = [WHISPER_DIR, RRD_DIR] + else: + DATA_DIRS = [WHISPER_DIR] + +# Default sqlite db file +# This is set here so that a user-set STORAGE_DIR is available +#XXX This can finally be removed once we only support Django >= 1.4 +# Support old local_settings.py db configs for a bit longer +if DJANGO_VERSION < (1,4): + warn_deprecated = False + if DATABASE_ENGINE and 'sqlite3' not in DATABASES['default']['ENGINE']: + DATABASES['default']['ENGINE'] = DATABASE_ENGINE + warn_deprecated = True + if DATABASE_NAME and not DATABASES['default']['NAME']: + DATABASES['default']['NAME'] = DATABASE_NAME + warn_deprecated = True + if DATABASE_USER and not DATABASES['default']['USER']: + DATABASES['default']['USER'] = DATABASE_USER + warn_deprecated = True + if DATABASE_PASSWORD and not DATABASES['default']['PASSWORD']: + DATABASES['default']['PASSWORD'] = DATABASE_PASSWORD + warn_deprecated = True + if DATABASE_HOST and not DATABASES['default']['HOST']: + DATABASES['default']['HOST'] = DATABASE_HOST + warn_deprecated = True + if DATABASE_PORT and not DATABASES['default']['PORT']: + DATABASES['default']['PORT'] = DATABASE_PORT + warn_deprecated = True + + if warn_deprecated: + warn("Found old-style settings.DATABASE_* configuration. Please see " \ + "local_settings.py.example for an example of the updated database " \ + "configuration style", DeprecationWarning) +else: + if DATABASE_ENGINE or \ + DATABASE_NAME or \ + DATABASE_USER or \ + DATABASE_PASSWORD or \ + DATABASE_HOST or \ + DATABASE_PORT: + raise ImproperlyConfigured("Found old-style settings.DATABASE_* configuration. Please remove " + "these settings from local_settings.py before continuing. See local_settings.py.example " + "for an example of the updated database configuration style") + +# Set a default sqlite file in STORAGE_DIR +if 'sqlite3' in DATABASES['default']['ENGINE'] and not DATABASES['default']['NAME']: + DATABASES['default']['NAME'] = join(STORAGE_DIR, 'graphite.db') + +# Caching shortcuts +if MEMCACHE_HOSTS: + CACHES['default'] = { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': MEMCACHE_HOSTS, + 'TIMEOUT': DEFAULT_CACHE_DURATION, + } + +# Authentication shortcuts +if USE_LDAP_AUTH and LDAP_URI is None: + LDAP_URI = "ldap://%s:%d/" % (LDAP_SERVER, LDAP_PORT) + +if USE_REMOTE_USER_AUTHENTICATION or REMOTE_USER_BACKEND: + MIDDLEWARE_CLASSES += ('django.contrib.auth.middleware.RemoteUserMiddleware',) + if REMOTE_USER_BACKEND: + AUTHENTICATION_BACKENDS.insert(0,REMOTE_USER_BACKEND) + else: + AUTHENTICATION_BACKENDS.insert(0,'django.contrib.auth.backends.RemoteUserBackend') + +if USE_LDAP_AUTH: + AUTHENTICATION_BACKENDS.insert(0,'graphite.account.ldapBackend.LDAPBackend') + +if SECRET_KEY == 'UNSAFE_DEFAULT': + warn('SECRET_KEY is set to an unsafe default. This should be set in local_settings.py for better security') + +USE_TZ = True diff --git a/roles/graphite/templates/serverspec/graphite_spec.rb b/roles/graphite/templates/serverspec/graphite_spec.rb new file mode 100644 index 0000000..89d24be --- /dev/null +++ b/roles/graphite/templates/serverspec/graphite_spec.rb @@ -0,0 +1,89 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe user('graphite') do + it { should exist } #GPH001 + it { should belong_to_group 'graphite' } #GPH002 + it { should have_home_directory '/nonexistent'} #GPH003 + it { should have_login_shell '/bin/false' } #GPH004 +end + +describe file('{{ graphite.path.home }}/conf') do + it { should be_mode 755 } #GPH005 + it { should be_owned_by 'graphite' } #GPH006 + it { should be_directory } #GPH007 +end + +describe file('{{ graphite.path.home }}/conf/carbon.conf') do + it { should be_mode 644 } #GPH008 + it { should be_owned_by 'graphite' } #GPH009 + it { should be_file } #GPH010 +end + +describe file('{{ graphite.path.home }}/conf/storage-schemas.conf') do + it { should be_mode 644 } #GPH011 + it { should be_owned_by 'graphite' } #GPH012 + it { should be_file } #GPH013 +{% for name, params in graphite.storage_schemas.items() %} + file_contents = ['[{{ name }}]', + '# {{ params.comment }}', +{% if name != "carbon" %} + 'pattern = {{ params.pattern }}', +{% endif %} + 'retentions = {{ params.retentions }}'] + file_contents.each do |file_line| + its(:content) { should contain(file_line) } #GPH014 + end +{% endfor %} +end + +describe file('{{ graphite.path.home }}/conf/graphite.wsgi') do + it { should be_mode 644 } #GPH015 + it { should be_owned_by 'graphite' } #GPH016 + it { should be_file } #GPH017 +end + +describe file('{{ graphite.path.virtualenv }}/bin/carbon-cache.py') do + it { should be_mode 755 } #GPH018 + it { should be_owned_by 'root' } #GPH019 + it { should be_file } #GPH020 +end + +describe file('{{ graphite.path.install_root }}/webapp/graphite/storage.py') do + it { should be_mode 644 } #GPH021 + it { should be_owned_by 'root' } #GPH022 + it { should be_file } #GPH023 +end + +describe file('{{ graphite.path.install_root }}/webapp/graphite/local_settings.py') do + it { should be_mode 644 } #GPH024 + it { should be_owned_by 'root' } #GPH025 + it { should be_file } #GPH026 +end + +describe file('/etc/init/carbon-cache.conf') do + it { should be_file } #GPH027 +end + +describe file('/etc/apache2/sites-available/graphite.conf') do + it { should be_file } #GPH029 +end + +describe file('/var/log/graphite') do + it { should be_owned_by 'graphite' } #GPH031 + it { should be_grouped_into 'graphite' } #GPH032 + it { should be_directory } #GPH033 +end + +describe service('carbon-cache') do + it { should be_enabled } +end + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end diff --git a/roles/harden/tasks/checks.yml b/roles/harden/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/harden/tasks/main.yml b/roles/harden/tasks/main.yml new file mode 100644 index 0000000..e4d1dda --- /dev/null +++ b/roles/harden/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: remove unneeded NFS services + apt: pkg={{ item }} state=absent + with_items: + - nfs-common + - rpcbind + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/harden/tasks/metrics.yml b/roles/harden/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/harden/tasks/serverspec.yml b/roles/harden/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/imagebuilder/defaults/main.yml b/roles/imagebuilder/defaults/main.yml new file mode 100644 index 0000000..e3b8e10 --- /dev/null +++ b/roles/imagebuilder/defaults/main.yml @@ -0,0 +1,15 @@ +--- +imagebuilder: + # sensible defaults for ci images + build_user: dib + cron: '0 0 * * *' + images: + - distro: ubuntu + series: trusty + name: ci-trusty + openstack: + # taken from the defaults used for allinone local deployments + os_username: admin + os_password: asdf + os_project_id: ~ + os_auth_url: https://openstack.example.org:5000/v2.0 diff --git a/roles/imagebuilder/files/usr/local/bin/image-refresh.sh b/roles/imagebuilder/files/usr/local/bin/image-refresh.sh new file mode 100755 index 0000000..936355a --- /dev/null +++ b/roles/imagebuilder/files/usr/local/bin/image-refresh.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -x + +DISTRO=$1 +SERIES=$2 +IMAGE_NAME=$3 + +# get our stack env vars +. ~/stackrc + +# create the image +sudo OVERWRITE_OLD_IMAGE=1 DIB_RELEASE=${SERIES} disk-image-create -o ${IMAGE_NAME}.qcow2 ${DISTRO} vm + +# does the image already exist? +IMAGE_ID=`openstack image show ${IMAGE_NAME} -c id -f value` + +# upload the new image, retrying twice if it fails +n=0 +until [ $n -ge 2 ] +do + glance image-create --visibility public --file ${IMAGE_NAME}.qcow2 --name ${IMAGE_NAME} --container-format bare --disk-format qcow2 && break + n=$[$n+1] + sleep 15 +done + +if [ $n -eq 2 ] +then + echo Upload Failed + exit 1 +fi + +if [ -n "${IMAGE_ID}" ] ; then + # delete the old one + glance image-delete ${IMAGE_ID} +fi diff --git a/roles/imagebuilder/meta/main.yml b/roles/imagebuilder/meta/main.yml new file mode 100644 index 0000000..223c0f1 --- /dev/null +++ b/roles/imagebuilder/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: ci-common diff --git a/roles/imagebuilder/tasks/checks.yml b/roles/imagebuilder/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/imagebuilder/tasks/main.yml b/roles/imagebuilder/tasks/main.yml new file mode 100644 index 0000000..ffb0dba --- /dev/null +++ b/roles/imagebuilder/tasks/main.yml @@ -0,0 +1,43 @@ +--- +- name: install disk image builder dependencies + apt: pkg="{{ item }}" + with_items: + - python-pip + - curl + - qemu-utils + - python-dev + +- name: install openstack clients and diskimage builder + pip: name="{{ item }}" + with_items: + - pbr + - diskimage-builder + - python-novaclient + - python-glanceclient + - python-openstackclient + +- name: allow dib user sudo access for running the image refresh script + template: src=etc/sudoers.d/dib + dest=/etc/sudoers.d/dib + mode=0440 + +- name: install the image refresh script + copy: src=usr/local/bin/image-refresh.sh dest=/usr/local/bin/image-refresh.sh mode=0755 + +- name: image refresh cron + template: src=etc/cron.d/dib-image-refresh + dest=/etc/cron.d/dib-image-refresh-{{ item.distro }}-{{ item.series }} + mode=0640 + with_items: "{{ imagebuilder.images }}" + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/imagebuilder/tasks/metrics.yml b/roles/imagebuilder/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/imagebuilder/tasks/serverspec.yml b/roles/imagebuilder/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/imagebuilder/templates/etc/cron.d/dib-image-refresh b/roles/imagebuilder/templates/etc/cron.d/dib-image-refresh new file mode 100644 index 0000000..4b025f3 --- /dev/null +++ b/roles/imagebuilder/templates/etc/cron.d/dib-image-refresh @@ -0,0 +1 @@ +{{ imagebuilder.cron }} {{ imagebuilder.build_user }} PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /usr/local/bin/image-refresh.sh {{ item.distro }} {{ item.series }} {{ item.name }} diff --git a/roles/imagebuilder/templates/etc/sudoers.d/dib b/roles/imagebuilder/templates/etc/sudoers.d/dib new file mode 100644 index 0000000..fed4f9e --- /dev/null +++ b/roles/imagebuilder/templates/etc/sudoers.d/dib @@ -0,0 +1,3 @@ +Defaults env_keep += "DIB_RELEASE" +Defaults env_keep += "OVERWRITE_OLD_IMAGE" +{{ imagebuilder.build_user }} ALL=(ALL)NOPASSWD:/usr/local/bin/disk-image-create diff --git a/roles/ipmi-proxy/README.md b/roles/ipmi-proxy/README.md new file mode 100644 index 0000000..e57b6ae --- /dev/null +++ b/roles/ipmi-proxy/README.md @@ -0,0 +1,10 @@ +## manual pre-requisite steps + +1. create DNS record for ipmi-proxy..blueboxgrid.com +2. create & attach additional neutron ports for proxy connections + + +## manual post-deployment requisite steps + +1. update /etc/blueboxgrp-hostid from a Box Panel entry for this IPMI card +2. run: shell: /usr/local/bin/sync-proxy-cache.py diff --git a/roles/ipmi-proxy/defaults/main.yml b/roles/ipmi-proxy/defaults/main.yml new file mode 100644 index 0000000..db4ad22 --- /dev/null +++ b/roles/ipmi-proxy/defaults/main.yml @@ -0,0 +1,52 @@ +--- +ipmi_proxy: + datacenters: + - name: lab00 + data_center_uuid: 0 + backend_source_ip: 127.0.0.1 + ip_pool: + - 127.0.0.101 + - 127.0.0.102 + apache: + ip: '*' + port: 8091 + allow_from: + - 127.0.0.1 + - "{{ hostvars[inventory_hostname][private_interface]['ipv4'].network }}/{{ hostvars[inventory_hostname][private_interface]['ipv4'].netmask }}" + modules: + cgi_path: /usr/lib/apache2/modules/mod_cgi.so + + system_deps: + - at + - sqlite3 + git: + - name: ipmi-proxy + path: /opt/git/ipmi-proxy/master + repo: "{{ git_repos.ipmi_proxy }}" + rev: master + symlink: /opt/git/ipmi-proxy/current + virtualenvs: + - name: ipmi-proxy-master + path: /opt/venv/ipmi-proxy/master + # requirements: /opt/git/ipmi-proxy/master/requirements.txt + alternatives: + - name: ipmi-proxy-tool.py + path: /opt/git/ipmi-proxy/current/src/ipmi-proxy-tool.py + link: /usr/local/bin/ipmi-proxy-tool.py + - name: sync-proxy-cache.py + path: /opt/git/ipmi-proxy/current/src/sync-proxy-cache.py + link: /usr/local/bin/sync-proxy-cache.py + + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/apache2/ipmi-proxy_access.log + fields: + tags: apache_access,ipmi-proxy + - paths: + - /var/log/apache2/ipmi-proxy_error.log + fields: + tags: apache_error,ipmi-proxy + + logging: + forwarder: filebeat diff --git a/roles/ipmi-proxy/meta/main.yml b/roles/ipmi-proxy/meta/main.yml new file mode 100644 index 0000000..6bd1f80 --- /dev/null +++ b/roles/ipmi-proxy/meta/main.yml @@ -0,0 +1,19 @@ +--- +dependencies: + - role: apache + - role: git-repos + - role: source-install + source_install: + name: ipmi-proxy + git: "{{ ipmi_proxy.git }}" + system_deps: "{{ ipmi_proxy.system_deps }}" + virtualenvs: "{{ ipmi_proxy.virtualenvs }}" + alternatives: "{{ ipmi_proxy.alternatives }}" + tags: + - source-install + - role: logging-config + service: ipmi_proxy + logdata: "{{ ipmi_proxy.logs }}" + forward_type: "{{ ipmi_proxy.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/ipmi-proxy/tasks/checks.yml b/roles/ipmi-proxy/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/ipmi-proxy/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/ipmi-proxy/tasks/main.yml b/roles/ipmi-proxy/tasks/main.yml new file mode 100644 index 0000000..3f00369 --- /dev/null +++ b/roles/ipmi-proxy/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: enable packet forwarding + sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes state=present reload=yes + +- name: install ipmi-proxy sync-cache cron + cron: name="ipmi-proxy sync-cache" user="root" + cron_file=ipmi-proxy minute="*/10" job="sync-proxy-cache.py" + +- name: create ipmi_proxy conf dir + file: path=/etc/bluebox state=directory mode=0755 + +- name: create ipmi_proxy data dir + file: path=/var/lib/ipmi-proxy state=directory mode=0755 + +- name: install ipmi-proxy conf + template: src=etc/bluebox/ipmi-proxy.conf dest=/etc/bluebox/ipmi-proxy.conf owner=root group=root mode=0644 + +- name: create ipmi_proxy lib dir + file: path=/usr/local/lib/ipmi-proxy state=directory mode=0755 + +- name: install cgi lib + shell: cp -a {{ ipmi_proxy.git[0].path }}/src/cgi/* /usr/local/lib/ipmi-proxy + +- name: www-data sudoers + template: src=etc/sudoers.d/www-data + dest=/etc/sudoers.d/www-data + owner=root + group=root + mode=0440 + +- name: configure apache + template: src=etc/apache2/sites-available/ipmi-proxy.conf + dest=/etc/apache2/sites-available/ipmi-proxy.conf owner=root group=root mode=0644 + notify: + - restart apache + +- name: enable apache mods + apache2_module: state=present name={{ item }} + with_items: + - ssl + - cgid + - cache_socache + +- name: enable ipmi-proxy vhost + apache2_site: state=enabled name=ipmi-proxy + notify: + - restart apache + +- meta: flush_handlers + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes diff --git a/roles/ipmi-proxy/tasks/metrics.yml b/roles/ipmi-proxy/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/ipmi-proxy/tasks/serverspec.yml b/roles/ipmi-proxy/tasks/serverspec.yml new file mode 100644 index 0000000..1702058 --- /dev/null +++ b/roles/ipmi-proxy/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests ipmi-proxy tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/ipmi-proxy/templates/etc/apache2/sites-available/ipmi-proxy.conf b/roles/ipmi-proxy/templates/etc/apache2/sites-available/ipmi-proxy.conf new file mode 100644 index 0000000..1ae0bf0 --- /dev/null +++ b/roles/ipmi-proxy/templates/etc/apache2/sites-available/ipmi-proxy.conf @@ -0,0 +1,25 @@ +# {{ ansible_managed }} + + + LoadModule cgi_module {{ ipmi_proxy.apache.modules.cgi_path }} + + + + ServerName ipmi-proxy + DocumentRoot "/usr/local/lib/ipmi-proxy" + ErrorLog /var/log/apache2/ipmi-proxy_error.log + CustomLog /var/log/apache2/ipmi-proxy_access.log combined + + Alias /ipmi "/usr/local/lib/ipmi-proxy" + + DirectoryIndex index.py + + {% for ip in ipmi_proxy.apache.allow_from %} + Require ip {{ ip }} + {% endfor %} + + AllowOverride None + Options ExecCGI + AddHandler cgi-script cgi py + + diff --git a/roles/ipmi-proxy/templates/etc/bluebox/ipmi-proxy.conf b/roles/ipmi-proxy/templates/etc/bluebox/ipmi-proxy.conf new file mode 100644 index 0000000..0f9436c --- /dev/null +++ b/roles/ipmi-proxy/templates/etc/bluebox/ipmi-proxy.conf @@ -0,0 +1,35 @@ +{ + "_comment": [ + "Definitions for this data structure:", + "data_center_uuid = UUID corresponding to the local datacenter.", + "backend_source = IP address from which connections to the back-end", + " IPMI network should originate. This IP should be bound to a real,", + " arping interface on the proxy box. If this is ommitted, we assume", + " we have layer-2 connectivity to the IPMI vlan back-end and attempt", + " to auto-detect the IP we should use.", + "pool_ips = The first string or IP will be used in the URLs generated by", + " the CGI script that the user will presumably click on to connect to", + " an ipmi card. (This may be useful if the IPMI proxy is located", + " behind a static NAT of some kind.) the second IP should correspond", + " to an IP address on the IPMI proxy box that is in the pool. These", + " IPs should be bound to either the primary interface (if the proxy", + " is behind a NAT and we're using layer-2 connectivity to talk to it),", + " or on a dummy or loopback interface (if we're using layer-3", + " connectivity to talk to it). If this value is ommitted from the", + " config, we assume we are using layer-3 connectivity and try to", + " auto-detect pool IPs from the dummy0 interface." ], + "datacenters": [ + {% for datacenter in ipmi_proxy.datacenters %} + { + "name": "{{ datacenter.name }}", + "data_center_uuid": "{{ datacenter.data_center_uuid }}", + "backend_source": "{{ datacenter.backend_source_ip }}" + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "pool_ips": [ + {% for ip in ipmi_proxy.ip_pool %} + ["{{ ip }}", "{{ ip }}"]{% if not loop.last %},{% endif %} + {% endfor %} + ] +} diff --git a/roles/ipmi-proxy/templates/etc/sudoers.d/www-data b/roles/ipmi-proxy/templates/etc/sudoers.d/www-data new file mode 100644 index 0000000..a995378 --- /dev/null +++ b/roles/ipmi-proxy/templates/etc/sudoers.d/www-data @@ -0,0 +1,4 @@ +# {{ ansible_managed }} + +Cmnd_Alias IPMI_PROXY = /usr/local/bin/ipmi-proxy-tool.py +www-data ALL=(ALL) NOPASSWD:IPMI_PROXY diff --git a/roles/ipmi-proxy/templates/serverspec/ipmi-proxy_spec.rb b/roles/ipmi-proxy/templates/serverspec/ipmi-proxy_spec.rb new file mode 100644 index 0000000..e9c6bb3 --- /dev/null +++ b/roles/ipmi-proxy/templates/serverspec/ipmi-proxy_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe 'Linux kernel parameters' do + context linux_kernel_parameter('net.ipv4.ip_forward') do + its(:value) { should eq 1 } #IPP001 + end +end + +describe file('/etc/bluebox') do + it { should be_mode 755 } #IPP002 + it { should be_directory } #IPP003 +end + +describe file('/var/lib/ipmi-proxy') do + it { should be_mode 755 } #IPP004 + it { should be_directory } #IPP005 +end + +describe file('/etc/bluebox/ipmi-proxy.conf') do + it { should be_mode 644 } #IPP006 + it { should be_owned_by 'root' } #IPP007 + it { should be_grouped_into 'root' } #IPP008 + it { should be_file } #IPP009 +end + +describe file('/usr/local/lib/ipmi-proxy') do + it { should be_mode 755 } #IPP010 + it { should be_directory } #IPP011 +end + +describe file('/etc/sudoers.d/www-data') do + it { should be_mode 440 } #IPP012 + it { should be_owned_by 'root' } #IPP013 + it { should be_grouped_into 'root' } #IPP014 + it { should be_file } #IPP015 +end + +describe file('/etc/apache2/sites-available/ipmi-proxy.conf') do + it { should be_mode 644 } #IPP016 + it { should be_owned_by 'root' } #IPP017 + it { should be_grouped_into 'root' } #IPP018 + it { should be_file } #IPP019 + its(:content) { should contain('') } #IPP020 +end + +describe file('/etc/apache2/sites-enabled/ipmi-proxy.conf') do + it { should be_file } #IPP021 +end + +describe command('apache2ctl -M') do + its(:stdout) { should contain ('ssl_module') } #IPP022 + its(:stdout) { should contain ('cgi_module') } #IPP023 + its(:stdout) { should contain ('cache_socache_module') } #IPP024 +end + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end diff --git a/roles/ipsec/defaults/main.yml b/roles/ipsec/defaults/main.yml new file mode 100644 index 0000000..1f7d9fb --- /dev/null +++ b/roles/ipsec/defaults/main.yml @@ -0,0 +1,11 @@ +--- +ipsec: + nat_enabled: False + vti_interface: vti0 + implementation: + package: openswan + service: ipsec + config: {} # see envs/examples/ipsec/host_vars for example + connections: {} # see envs/examples/ipsec/host_vars for example + sharedkeys: {} # see envs/examples/ipsec/host_vars for example + nat_rules: # see envs/examples/ipsec/host_vars for example diff --git a/roles/ipsec/handlers/main.yml b/roles/ipsec/handlers/main.yml new file mode 100644 index 0000000..7c56bef --- /dev/null +++ b/roles/ipsec/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart ipsec + service: name=ipsec state=restarted + +- name: restart strongswan + service: name=strongswan state=restarted diff --git a/roles/ipsec/meta/main.yml b/roles/ipsec/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/ipsec/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/ipsec/tasks/checks.yml b/roles/ipsec/tasks/checks.yml new file mode 100644 index 0000000..7627904 --- /dev/null +++ b/roles/ipsec/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: IPS020 install ipsec process check + sensu_check_dict: name="check-ipsec-process" check="{{ sensu_checks.ipsec.check_ipsec_process }}" + notify: restart sensu-client missing ok diff --git a/roles/ipsec/tasks/main.yml b/roles/ipsec/tasks/main.yml new file mode 100644 index 0000000..a4cc6d5 --- /dev/null +++ b/roles/ipsec/tasks/main.yml @@ -0,0 +1,74 @@ +--- +- name: install ipsec packages + apt: name={{ ipsec.implementation.package }} + +- name: enable ipsec ip fowarding + sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes state=present reload=yes + +- name: disable accept redirects + sysctl: name="net.ipv4.conf.{{ item }}.accept_redirects" value=0 sysctl_set=yes state=present reload=yes + with_items: + - all + - default + +- name: disable send redirects + sysctl: name="net.ipv4.conf.{{ item }}.send_redirects" value=0 sysctl_set=yes state=present reload=yes + with_items: + - all + - default + +- name: write ipsec config file + template: src=etc/ipsec.conf dest=/etc/ipsec.conf + owner=root group=root mode=0644 + notify: + - restart {{ ipsec.implementation.service }} + +- name: write ipsec configs for connections + template: src=etc/ipsec.d/connections.conf dest=/etc/ipsec.d/connections.conf + owner=root group=root mode=0644 + notify: + - restart {{ ipsec.implementation.service }} + +- name: write ipsec secrets + template: src=etc/ipsec.secrets dest=/etc/ipsec.secrets + owner=root group=root mode=0600 + notify: + - restart {{ ipsec.implementation.service }} + +- include: strongswan.yml + when: ipsec.implementation.package == "strongswan" + +- name: "allow remote ipsec traffic port 500 for {{ item.key }}" + ufw: rule=allow to_port=500 proto=udp src={{ item.value.right }} + with_dict: "{{ ipsec.connections }}" + tags: + - firewall + +- name: "allow remote ipsec traffic port 4500 for {{ item.key }}" + ufw: rule=allow to_port=4500 proto=udp src={{ item.value.right }} + with_dict: "{{ ipsec.connections }}" + tags: + - firewall + +- name: set nat rules + blockinfile: + dest: /etc/ufw/before.rules + marker: "# <-- {mark} ANSIBLE MANAGED BLOCK -->" + block: "{{ ipsec.nat_rules }}" + insertafter: "^# don't delete the 'COMMIT' line or these rules won't be processed" + when: ipsec.nat_enabled|default("False")|bool and ipsec.nat_rules + tags: + - firewall + +- meta: flush_handlers + +- name: ensure {{ ipsec.implementation.service }} is running + service: name={{ ipsec.implementation.service }} state=started enabled=yes + +- include: checks.yml + when: sensu is defined and sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/ipsec/tasks/serverspec.yml b/roles/ipsec/tasks/serverspec.yml new file mode 100644 index 0000000..c2d5f2c --- /dev/null +++ b/roles/ipsec/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests ipsec tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/ipsec/tasks/strongswan.yml b/roles/ipsec/tasks/strongswan.yml new file mode 100644 index 0000000..18dc404 --- /dev/null +++ b/roles/ipsec/tasks/strongswan.yml @@ -0,0 +1,24 @@ +--- +- name: write ipsec-notify.sh + template: src=etc/ipsec.d/ipsec-notify.sh dest=/etc/ipsec.d/ipsec-notify.sh + owner=root group=root mode=0755 + notify: + - restart {{ ipsec.implementation.service }} + +- name: avoid strongswan adjusting routes + lineinfile: dest=/etc/strongswan.d/charon.conf regexp="^ \# install_routes" + line=" install_routes = no" + notify: + - restart {{ ipsec.implementation.service }} + +- name: set config_file for strongswan + lineinfile: dest=/etc/strongswan.d/starter.conf regexp="^ config_file" + insertbefore="^}" line=" config_file = /etc/ipsec.conf" + notify: + - restart {{ ipsec.implementation.service }} + +- name: adjust sysctls + sysctl: name={{ item.name }} value={{ item.value }} reload=yes + with_items: + - {name: "net.ipv4.conf.{{ ipsec.vti_interface }}.rp_filter", value: 0} + - {name: "net.ipv4.conf.{{ ipsec.vti_interface }}.disable_policy", value: 1} diff --git a/roles/ipsec/templates/etc/ipsec.conf b/roles/ipsec/templates/etc/ipsec.conf new file mode 100644 index 0000000..8e71f57 --- /dev/null +++ b/roles/ipsec/templates/etc/ipsec.conf @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +version 2.0 + +config setup +{% for key, value in ipsec.config.items() %} + {{ key }}={{ value }} +{% endfor %} + + +include /etc/ipsec.d/*.conf diff --git a/roles/ipsec/templates/etc/ipsec.d/connections.conf b/roles/ipsec/templates/etc/ipsec.d/connections.conf new file mode 100644 index 0000000..736472b --- /dev/null +++ b/roles/ipsec/templates/etc/ipsec.d/connections.conf @@ -0,0 +1,10 @@ + +{% for name, config in ipsec.connections.items() %} +conn {{ name }} +{% for key, value in config.items() %} + {{key}}={{value}} +{% endfor %} + +{% endfor %} + +# {{ ansible_managed }} diff --git a/roles/ipsec/templates/etc/ipsec.d/ipsec-notify.sh b/roles/ipsec/templates/etc/ipsec.d/ipsec-notify.sh new file mode 100644 index 0000000..674e9ea --- /dev/null +++ b/roles/ipsec/templates/etc/ipsec.d/ipsec-notify.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +echo $PLUTO_VERB + +case "$PLUTO_VERB:$1" in + up-client:) + ip link add {{ ipsec.vti_interface }} type vti key 100 remote {{ ipsec.connections.example.right }} local {{ ipsec.connections.example.left }} + ip link set {{ ipsec.vti_interface }} up + ip addr add {{ ipsec.connections.example.left_vti_ip }} remote {{ ipsec.connections.example.right_vti_ip }} dev {{ ipsec.vti_interface }} + ;; + down-client:) + ip tunnel del {{ ipsec.vti_interface }} + ;; +esac diff --git a/roles/ipsec/templates/etc/ipsec.secrets b/roles/ipsec/templates/etc/ipsec.secrets new file mode 100644 index 0000000..8b3129f --- /dev/null +++ b/roles/ipsec/templates/etc/ipsec.secrets @@ -0,0 +1,4 @@ +{% for connection, secret in ipsec.sharedkeys.items() %} +0.0.0.0 {{ secret.remote_ip }} : PSK "{{ secret.key }}" + +{% endfor %} diff --git a/roles/ipsec/templates/etc/network/interfaces.d/vti0.cfg b/roles/ipsec/templates/etc/network/interfaces.d/vti0.cfg new file mode 100644 index 0000000..607b7d8 --- /dev/null +++ b/roles/ipsec/templates/etc/network/interfaces.d/vti0.cfg @@ -0,0 +1,2 @@ +iface vti0 inet static + diff --git a/roles/ipsec/templates/serverspec/ipsec_spec.rb b/roles/ipsec/templates/serverspec/ipsec_spec.rb new file mode 100644 index 0000000..d65bafe --- /dev/null +++ b/roles/ipsec/templates/serverspec/ipsec_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe package({{ ipsec.implementation.package }}) do + it { should be_installed.by('apt') } #IPS001 +end + +describe 'Linux kernel parameters' do + items = ['all','default'] + context linux_kernel_parameter('net.ipv4.ip_forward') do + its(:value) { should eq 1 } #IPS002 + end + items.each do |item| + context linux_kernel_parameter("net.ipv4.conf.#{ item }.accept_redirects") do + its(:value) { should eq 0 } #IPS003 + end + context linux_kernel_parameter("net.ipv4.conf.#{ item }.send_redirects") do + its(:value) { should eq 0 } #IPS004 + end + end +end + +describe file('/etc/ipsec.conf') do + it { should be_mode 644 } #IPS005 + it { should be_owned_by 'root' } #IPS006 + it { should be_grouped_into 'root' } #IPS007 + it { should be_file } #IPS008 +end + +describe file('/etc/ipsec.d/connections.conf') do + it { should be_mode 644 } #IPS009 + it { should be_owned_by 'root' } #IPS010 + it { should be_grouped_into 'root' } #IPS011 + it { should be_file } #IPS012 +end + +describe file('/etc/ipsec.secrets') do + it { should be_mode 600 } #IPS013 + it { should be_owned_by 'root' } #IPS014 + it { should be_grouped_into 'root' } #IPS015 + it { should be_file } #IPS016 + its(:content) { should_not contain /(PSK "\w{0,15}")$/ } #IPS017 +end + +describe port(500) do + it { should be_listening.with('udp') } #IPS018 +end + +describe port(4500) do + it { should be_listening.with('udp') } #IPS019 +end + +describe package({{ ipsec.implementation.package }}) do + it { should be_installed } +end + +describe service({{ ipsec.implementation.package }}) do + it { should be_enabled } +end + +{% if ipsec.nat_rules %} +describe file('/etc/ufw/before.rules') do + it { should be_mode 640 } #IPS021 + it { should be_owned_by 'root' } #IPS022 + it { should be_grouped_into 'root' } #IPS023 + it { should be_file } #IPS024 + its(:content) { should contain /^({{ ipsec.nat_rules }})/ } #IPS025 +end +{% endif %} diff --git a/roles/jenkins-common/defaults/main.yml b/roles/jenkins-common/defaults/main.yml new file mode 100644 index 0000000..5aa7a9b --- /dev/null +++ b/roles/jenkins-common/defaults/main.yml @@ -0,0 +1,237 @@ +jenkins: + apt: + pkgs: + - libxml2-dev + - libxslt1-dev + - python-dev + - jenkins=1.* + plugins: + - name: ansicolor + version: '0.4.2' + - name: antisamy-markup-formatter + version: '1.1' + - name: ant + version: '1.2' + - name: build-timeout + version: '1.16' + - name: conditional-buildstep + version: '1.3.3' + - name: copyartifact + version: '1.38' + - name: cvs + version: '2.11' + - name: envinject + version: '1.92.1' + - name: external-monitor-job + version: '1.4' + - name: gerrit-trigger + version: '2.21.1' + - name: ghprb + version: '1.32.2' + - name: git-client + version: '1.19.6' + - name: github-api + version: '1.75' + - name: github + version: '1.19.1' + - name: github-oauth + version: '0.23' + - name: git + version: '2.4.4' + - name: icon-shim + version: '2.0.3' + - name: javadoc + version: '1.1' + - name: jenkins-multijob-plugin + version: '1.21' + - name: ldap + version: '1.11' + - name: mapdb-api + version: '1.0.6.0' + - name: matrix-auth + version: '1.1' + - name: maven-plugin + version: '2.7.1' + - name: notification + version: '1.10' + - name: pam-auth + version: '1.1' + - name: parameterized-trigger + version: '2.30' + - name: plain-credentials + version: '1.1' + - name: postbuildscript + version: '0.17' + - name: postbuild-task + version: '1.8' + - name: random-string-parameter + version: '1.0' + - name: run-condition + version: '1.0' + - name: scm-api + version: '1.2' + - name: script-security + version: '1.13' + - name: slack + version: '1.8.1' + - name: ssh-agent + version: '1.10' + - name: ssh + version: '2.4' + - name: ssh-slaves + version: '1.9' + - name: structs + version: '1.1' + - name: subversion + version: '1.54' + - name: throttle-concurrents + version: '1.9.0' + - name: timestamper + version: '1.7.4' + - name: token-macro + version: '1.12.1' + - name: translation + version: '1.10' + - name: windows-slaves + version: '1.0' + - name: workflow-step-api + version: '2.0' + - name: ws-cleanup + version: '0.29' + - name: jquery + version: '1.7.2-1' + - name: nodelabelparameter + version: '1.7.2' + - name: graphiteIntegrator + version: '1.2' + stackrc: '' + jjb: + git: + source: + repo: https://github.com/blueboxgroup/jenkins-job-builder.git + rev: master + path: /usr/local/src/jjb + jobs: + repo: https://{{secrets.jjb.user}}:{{secrets.jjb.password}}@github.com/notreal/jenkins-job-builder.git + rev: master + path: /var/lib/jenkins/jjb-jobs + key_file: /var/lib/jenkins/.ssh/ghe + port: 8080 + host: 127.0.0.1 + url: http://jenkins.example.com/ + prefix: "" + slack: + teamdomain: ~ + token: ~ + room: ~ + buildserverurl: http://jenkins.example.com/ + apache: + enabled: True + github_hooks_only: False + port: 80 + host: 0.0.0.0 + allow_from: + - 0.0.0.0/0 + servername: jenkins.example.com + basicauth: + enabled: False + username: ~ + password: ~ + ssl: + port: 443 + intermediate: ~ + cert: | + -----BEGIN CERTIFICATE----- + MIIDbjCCAlagAwIBAgIJANyptbhIpO8TMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV + BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX + aWRnaXRzIFB0eSBMdGQwHhcNMTUwMTE0MTg0MTQyWhcNMTYwMTE0MTg0MTQyWjBF + MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 + ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEA1MWu8kJ20FCLbBIMqgTlKCOL54X06C0bERm3wIOwu6dk35s7uy78I2pt + dA2sbwLeIMiJHKY85eBNI+pMZGNsRYajl3BZRvSWcjO6DHGN8k0ljf6gvzAlzyG0 + 2Pz0Th1R2fveOE0fNZcT/JqbKLFb/Cu3GoaC/wUAbRK36qgzQkX4hQD0QVylhdmS + 82Fsr6H4fl7iybn7w1HwA2DG4MuJRjCeskujfz0ch7/BBdON84SDVGcemHkO45R/ + c6n49jnTHiJ5CJXsZz4uH8lEs0Q6CW2GMtVbPiXfJ8TLBdSgMp6atVMCMNsa0Pg1 + I5hXAqknZf9Uc3KWdBRkufJrwHnFuwIDAQABo2EwXzAPBgNVHREECDAGhwSsEAAN + MB0GA1UdDgQWBBQzjGYx1ss5NQuRGAf3nk7KenvLyTAfBgNVHSMEGDAWgBQzjGYx + 1ss5NQuRGAf3nk7KenvLyTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IB + AQBAcq4Fe5+5jeRBqS0R9EApBeQZfCV+h88UpzAsWWhxicft1BUiftMzAwAE0VnG + xTB96jmlmHXjSR8ugCED4A6wkjW3mDo5SmkWLQBCY1EHSUdIgVbhK4zxP4TLhGbD + 54+nGAsRLM1Hb5UlI1uCa4E/1gdeLUd41vSfKc8/A133Rl1CMpFLHuMgE1VgVQkU + ElkHVD8xQlOrza/yMT1eGg4tbR4ukqjdC4vOWqGPmajlR+gk/sJ2Ut1CzN4fcmtj + yqcwxri9aiDB1mimS+m/SjKvPf6lV5bDRtQWXbCPAWzq8gKvv2PqoNyyZOxbqJhM + eb0+9ULVAGAYtcZuNHgs0bH+ + -----END CERTIFICATE----- + key: | + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDUxa7yQnbQUIts + EgyqBOUoI4vnhfToLRsRGbfAg7C7p2Tfmzu7Lvwjam10DaxvAt4gyIkcpjzl4E0j + 6kxkY2xFhqOXcFlG9JZyM7oMcY3yTSWN/qC/MCXPIbTY/PROHVHZ+944TR81lxP8 + mpsosVv8K7cahoL/BQBtErfqqDNCRfiFAPRBXKWF2ZLzYWyvofh+XuLJufvDUfAD + YMbgy4lGMJ6yS6N/PRyHv8EF043zhINUZx6YeQ7jlH9zqfj2OdMeInkIlexnPi4f + yUSzRDoJbYYy1Vs+Jd8nxMsF1KAynpq1UwIw2xrQ+DUjmFcCqSdl/1RzcpZ0FGS5 + 8mvAecW7AgMBAAECggEAeP6BYdpR3lwvLKGG+hgWiCDOqjYO8wjTX4IUcDFzCwNB + 5bZM3UD2uN0IqPotmGM1Fcdz0QrnjoFi3I2cK2ouY8sQtEl7O1JTS1YG8pSQd71P + IdQubQpgNc2hHdOayeD6bs8/qxyQJtVm1DrHCPjyqg/h6/+Z9pNNjrkaRSKpI4HF + Jtuj8iRHV9yWDoNU1eWO6qzoEU1fNE48XVefpLpVDJx0u4Ih8/BHguTBH1DO5qQt + sNkBUNh09Fy3ZqM2085ZMmi+W+EqtNJyYsB7J2324Bga7Voo71s9HNDZWLWHTIYW + jaPBRvn4mjlVua1o8Dd3/RQ2JVzwiv/aZ47IGw3cgQKBgQDwco2IduJkWHnOyPKZ + +uo4xmYBa8UswBMqmfPaTijZ4naG9J34LjZFtvsJhSqOrdKEYbP3ONz3UwvOFUQg + kzC/F5TJidDzNXPiLd+3NpssofnVwLKdZUwORkLoBph7oAY2fImaoWIGfPsHyam5 + o4RWbsyhxKozFyBn2NBIyxQ0EQKBgQDiiN6rsHusVdH5OreO6Z8RvB9ULUH8seB+ + f1oYiHSfPZQ4fkwsB8k9gF6yJOb/Kob3FSoc5GRpd4TK9DXHTZh41Q3/CSwveYYa + 5BJlupXGgAzu6vFuyUxAw/TclZFD9uAjrlfu8lbRpBHzKZtJnJQtgNPFflH6ZxbY + ljlNO4f5CwKBgA42l0szy9omqLyigES94k6M28bFuhgVGozwIMwMxrlqe5sqppPf + F3IziM9dQdDBUaplpB+/CsDL9eyusSJD0SPanv7y2Jkn1bvO/mR0I+QVhxEtnOFU + 9ZP6b0YL7cORCAz8e53aYFMF3EjvkMracZ4yWoJNf8oZWd8Jn/ZNmtohAoGAN/yF + s78BQb1QEJ2PYhWhB5wLzh0FUvOPPRQyax/GWti4OiIUp0khVj7UqIhwQp37DzO+ + 3bcgjeRJAHPMmr41sZ9OPzrAHdeV6i110oiDnbRl/eI42x2K5/LGIIIijb6E9KyQ + 9PAVvugiu4sL4ux8vqY5MHUgw5cY0VyHOuw8lbMCgYBPZtqsJs+zrkZj9HD+lQvo + N2CQtonrnIwqaorgbZVr1PDBJo0AHBraQRt/0Xpzu4Q/rjkHxlhESjW17Ax3JS1s + gRwuu+SIjud/7fZcGy8to7MbdJrWiYFUOlQF6F/kEIq3bsHmJWbi1FgTdeQkkEl6 + DFeUkc2q0uaO1lZIAnMA4w== + -----END PRIVATE KEY----- + git: + email: jenkins@bluebox.net + name: Leeroy Jenkins + security: + github: + orgs: + - blueboxgroup + admins: [] + web_uri: https://github.com + api_uri: https://api.github.com + client_id: '' + client_secret: '' + credentials: [] + configuration_templates: + - config.xml + - credentials.xml + - github-plugin-configuration.xml + - jenkins.model.JenkinsLocationConfiguration.xml + - jenkins.plugins.slack.SlackNotifier.xml + - gerrit-trigger.xml + - com.tikal.jenkins.plugins.multijob.PhaseJobsConfig.xml + github_servers: [] + github_pull_config: [] + gerrit_servers: [] + slave: + apt: + - libssl-dev + pip: + - python-keystoneclient + - python-swiftclient + gem: + - package_cloud + ssh_keys: [] + multijob: + retry: + rules: + - name: multijob-failure-retry-rule + path: /var/lib/jenkins/failure.prop + master_mode_exclusive: true + logrotate: + frequency: weekly + rotations: 90 + packagecloud: + token: "thisisbadtoken" diff --git a/roles/jenkins-common/handlers/main.yml b/roles/jenkins-common/handlers/main.yml new file mode 100644 index 0000000..8fa26ee --- /dev/null +++ b/roles/jenkins-common/handlers/main.yml @@ -0,0 +1,17 @@ +--- + +- name: restart node + command: shutdown -r now + async: 0 + poll: 0 + failed_when: False +- name: wait for node to come back + wait_for: + host: "{{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}" + state: started + port: 22 + delay: 1 + timeout: 300 + connect_timeout: 15 + delegate_to: "{{ groups['sshproxy'][0] }}" + diff --git a/roles/jenkins-common/meta/main.yml b/roles/jenkins-common/meta/main.yml new file mode 100644 index 0000000..223c0f1 --- /dev/null +++ b/roles/jenkins-common/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: ci-common diff --git a/roles/jenkins-common/tasks/checks.yml b/roles/jenkins-common/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/jenkins-common/tasks/main.yml b/roles/jenkins-common/tasks/main.yml new file mode 100644 index 0000000..a2c068d --- /dev/null +++ b/roles/jenkins-common/tasks/main.yml @@ -0,0 +1,70 @@ +--- +- name: Install java + apt: name=openjdk-7-jre + +- name: jenkins ssh directory + file: path=~jenkins/.ssh state=directory mode=0700 owner=jenkins group=jenkins + +- name: install ssh keys + copy: + dest: '~jenkins/.ssh/{{ item.name }}' + mode: '0600' + content: '{{ item.key }}' + owner: jenkins + group: jenkins + with_items: '{{ jenkins.ssh_keys }}' + +- name: set up log rotation for jenkins + logrotate: name=jenkins path=/var/log/jenkins/*.log + args: + options: + - "{{ jenkins.logrotate.frequency }}" + - "rotate {{ jenkins.logrotate.rotations }}" + - missingok + - compress + - delaycompress + - copytruncate + - notifempty + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec + +- name: Set max num of file descriptors to sane value + lineinfile: + dest: /etc/security/limits.conf + line: "{{ item }}" + with_items: + - '* soft nofile 6400' + - '* hard nofile 6400' + - 'root soft nofile 6400' + - 'root hard nofile 6400' + register: sec_limits + +- name: enable max file descriptors in session + lineinfile: + dest: /etc/pam.d/common-session + line: session required pam_limits.so + register: session_max_fd + +- name: enable max file descriptors in session-noninteractive + lineinfile: + dest: /etc/pam.d/common-session-noninteractive + line: session required pam_limits.so + register: sec_non_max_fd + +- name: restart if need to + command: /bin/true + when: sec_limits.changed or session_max_fd.changed or sec_non_max_fd.changed + notify: + - restart node + - wait for node to come back + diff --git a/roles/jenkins-common/tasks/metrics.yml b/roles/jenkins-common/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/jenkins-common/tasks/serverspec.yml b/roles/jenkins-common/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/jenkins-master/files/rules/failure.prop b/roles/jenkins-master/files/rules/failure.prop new file mode 100644 index 0000000..358004b --- /dev/null +++ b/roles/jenkins-master/files/rules/failure.prop @@ -0,0 +1 @@ +Finished: FAILURE diff --git a/roles/jenkins-master/files/usr/local/bin/load-jenkins-jobs.sh b/roles/jenkins-master/files/usr/local/bin/load-jenkins-jobs.sh new file mode 100644 index 0000000..73db86e --- /dev/null +++ b/roles/jenkins-master/files/usr/local/bin/load-jenkins-jobs.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ex + +JJB_CONFIG=$1 +JOBS_PATH=$2 +JJB_ACTION=${3:-"update"} + +if [ -d "${JOBS_PATH}" ]; then + cd ${JOBS_PATH} + if [[ "${JJB_ACTION}" == "update" ]]; then + jenkins-jobs --conf ${JJB_CONFIG} update --delete-old . + elif [[ "${JJB_ACTION}" == "test" ]]; then + jenkins-jobs --conf ${JJB_CONFIG} test . + else + echo Action not supported + exit 1 + fi +fi diff --git a/roles/jenkins-master/handlers/main.yml b/roles/jenkins-master/handlers/main.yml new file mode 100644 index 0000000..337b5de --- /dev/null +++ b/roles/jenkins-master/handlers/main.yml @@ -0,0 +1,14 @@ +--- +- name: restart jenkins + service: name=jenkins state=started + changed_when: true + notify: wait for jenkins +- name: wait for jenkins + jenkins: task=wait_for_startup url="http://{{ jenkins.host }}:{{ jenkins.port }}/{{ jenkins.prefix }}" + username="{{ jenkins.admin_username|default('') }}" + password="{{ jenkins.admin_password|default('') }}" + notify: restart jenkins safely +- name: restart jenkins safely + jenkins: task=restart url="http://{{ jenkins.host }}:{{ jenkins.port }}/{{ jenkins.prefix }}" + username="{{ jenkins.admin_username|default('') }}" + password="{{ jenkins.admin_password|default('') }}" diff --git a/roles/jenkins-master/meta/main.yml b/roles/jenkins-master/meta/main.yml new file mode 100644 index 0000000..9b32794 --- /dev/null +++ b/roles/jenkins-master/meta/main.yml @@ -0,0 +1,14 @@ +--- +dependencies: + - role: jenkins-common + - role: apache + when: jenkins.apache.enabled|bool + - role: bbg-ssl + name: jenkins + ssl_cert: "{{ jenkins.apache.ssl.cert }}" + ssl_key: "{{ jenkins.apache.ssl.key }}" + ssl_intermediate: "{{ jenkins.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: jenkins.apache.enabled|bool + tags: ['bbg-ssl'] + - role: sensu-check diff --git a/roles/jenkins-master/tasks/apache.yml b/roles/jenkins-master/tasks/apache.yml new file mode 100644 index 0000000..95cfc3c --- /dev/null +++ b/roles/jenkins-master/tasks/apache.yml @@ -0,0 +1,53 @@ +--- +- name: http firewall rule for jenkins vhost + ufw: + rule: allow + src: "{{ item }}" + direction: in + interface: "{{ private_device_interface }}" + to_port: "{{ jenkins.apache.port }}" + proto: tcp + with_items: "{{ jenkins.apache.allow_from }}" + tags: firewall + +- name: https firewall rule for jenkins vhost + ufw: + rule: allow + src: "{{ item }}" + direction: in + interface: "{{ private_device_interface }}" + to_port: "{{ jenkins.apache.ssl.port }}" + proto: tcp + with_items: "{{ jenkins.apache.allow_from }}" + tags: firewall + +- name: enable apache mod proxy_http + apache2_module: name=proxy_http + notify: restart apache + +- name: enable apache mod ssl + apache2_module: name=ssl + notify: restart apache + +- name: create basic auth user + htpasswd: + path: /etc/apache2/htpasswd + name: "{{ jenkins.apache.basicauth.username }}" + password: "{{ jenkins.apache.basicauth.password }}" + owner: root + group: www-data + mode: 0640 + when: jenkins.apache.basicauth.enabled|default("False")|bool + +- name: jenkins vhosts configuration + template: src=etc/apache2/sites-available/jenkins dest=/etc/apache2/sites-available/jenkins.conf + +- name: enable jenkins vhost + apache2_site: state=enabled name=jenkins + notify: + - restart apache + +- meta: flush_handlers + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes diff --git a/roles/jenkins-master/tasks/checks.yml b/roles/jenkins-master/tasks/checks.yml new file mode 100644 index 0000000..e729628 --- /dev/null +++ b/roles/jenkins-master/tasks/checks.yml @@ -0,0 +1,5 @@ +--- +- name: install jenkins process check + sensu_check_dict: name="check-jenkins-process" check="{{ sensu_checks.jenkins.check_jenkins_process }}" + notify: restart sensu-client missing ok + diff --git a/roles/jenkins-master/tasks/jjb.yml b/roles/jenkins-master/tasks/jjb.yml new file mode 100644 index 0000000..3fb2fdc --- /dev/null +++ b/roles/jenkins-master/tasks/jjb.yml @@ -0,0 +1,33 @@ +--- +- name: jjb source repo + git: repo="{{ jenkins.jjb.git.source.repo }}" + dest="{{ jenkins.jjb.git.source.path }}" + version="{{ jenkins.jjb.git.source.rev }}" + update=true + force=true + accept_hostkey=true + +- name: jjb jobs repo + git: repo="{{ jenkins.jjb.git.jobs.repo }}" + dest="{{ jenkins.jjb.git.jobs.path }}" + version="{{ jenkins.jjb.git.jobs.rev }}" + update=true + force=true + become: yes + become_user: jenkins + +- name: install jenkins job builder + pip: name="{{ jenkins.jjb.git.source.path }}" + +- name: create jjb config directory + file: dest=/etc/jenkins_jobs state=directory mode=0755 + +- name: configure jenkins job builder + template: src=etc/jenkins_jobs/jenkins_jobs.ini dest=/etc/jenkins_jobs/jenkins_jobs.ini + owner=root group=jenkins mode=0640 + +- name: install the job loading script + copy: src=usr/local/bin/load-jenkins-jobs.sh dest=/usr/local/bin/load-jenkins-jobs.sh mode=0755 + +- name: seed the jenkins jobs + shell: "/usr/local/bin/load-jenkins-jobs.sh /etc/jenkins_jobs/jenkins_jobs.ini {{ jenkins.jjb.git.jobs.path }}/servers/{{ hostname }}" diff --git a/roles/jenkins-master/tasks/main.yml b/roles/jenkins-master/tasks/main.yml new file mode 100644 index 0000000..ff97619 --- /dev/null +++ b/roles/jenkins-master/tasks/main.yml @@ -0,0 +1,92 @@ +--- +- name: firewall rules for jenkins + ufw: rule=allow dest={{ jenkins.host }} to_port={{ jenkins.port }} + tags: + - firewall + +- name: install jenkins packages + apt: + pkg: "{{ item }}" + with_items: "{{ jenkins.apt.pkgs }}" + +- name: Ensure Jenkins is started and runs on startup. + service: name=jenkins state=started enabled=yes + +- name: wait for jenkins to start + jenkins: + task: wait_for_startup + url: "http://{{ jenkins.host }}:{{ jenkins.port }}/{{ jenkins.prefix }}" + username: "{{ jenkins.admin_username|default('') }}" + password: "{{ jenkins.admin_password|default('') }}" + +- name: install jenkins plugins + jenkins: + task: install_plugin + url: "http://{{ jenkins.host }}:{{ jenkins.port }}/{{ jenkins.prefix }}" + username: "{{ jenkins.admin_username|default('') }}" + password: "{{ jenkins.admin_password|default('') }}" + plugin: "{{ item.name }}" + version: "{{ item.version|default('latest') }}" + with_items: "{{ jenkins.plugins }}" + notify: restart jenkins + tags: ['jenkins-plugins'] + become: yes + become_user: jenkins + +- meta: flush_handlers + +# now we have all the plugins, install the global config file and restart, +# so any encryption keys are generated and we'll have the correct values +# everywhere to install all the other configuration files with +- name: install global configuration file + template: src=var/lib/jenkins/config.xml dest=/var/lib/jenkins/config.xml owner=jenkins group=jenkins mode=0644 + notify: restart jenkins + +- meta: flush_handlers + +- name: Read jenkins master key for use in configuring credentials. + command: cat /var/lib/jenkins/secrets/master.key + register: jenkins_master_key + become: yes + become_user: jenkins + changed_when: false + +- name: Read jenkins secret key for use in configuring credentials. + command: base64 /var/lib/jenkins/secrets/hudson.util.Secret + register: jenkins_secret_key + become: yes + become_user: jenkins + changed_when: false + +- name: copy multijob retry rule file into place + copy: src=rules/failure.prop dest=/var/lib/jenkins/failure.prop owner=jenkins group=jenkins mode=0644 + +- name: Install configuration files. + template: src=var/lib/jenkins/{{ item }} dest=/var/lib/jenkins/{{ item }} owner=jenkins group=jenkins mode=0644 + with_items: "{{ jenkins.configuration_templates }}" + notify: restart jenkins + +- name: Adjust stateful github pull request configuration file. + xml_configuration: file=/var/lib/jenkins/org.jenkinsci.plugins.ghprb.GhprbTrigger.xml xpath={{ item.path }} value="{{ item.value }}" + notify: restart jenkins + with_items: "{{ jenkins.github_pull_config }}" + +- meta: flush_handlers + +- include: jjb.yml + tags: jenkins-jjb + +- include: apache.yml + when: jenkins.apache.enabled|bool + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/jenkins-master/tasks/metrics.yml b/roles/jenkins-master/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/jenkins-master/tasks/serverspec.yml b/roles/jenkins-master/tasks/serverspec.yml new file mode 100644 index 0000000..375a560 --- /dev/null +++ b/roles/jenkins-master/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests jenkins tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/jenkins-master/templates/etc/apache2/sites-available/jenkins b/roles/jenkins-master/templates/etc/apache2/sites-available/jenkins new file mode 100644 index 0000000..55076a1 --- /dev/null +++ b/roles/jenkins-master/templates/etc/apache2/sites-available/jenkins @@ -0,0 +1,95 @@ +# {{ ansible_managed }} + + + ServerName {{ jenkins.apache.servername }} + Redirect permanent / https://{{ jenkins.apache.servername }}:{{ jenkins.apache.ssl.port }} + + + ProxyPass http://{{ jenkins.host }}:{{ jenkins.port }}/github-webhook/ + ProxyPassReverse http://{{ jenkins.host }}:{{ jenkins.port }}/github-webhook/ + {% if jenkins.apache.basicauth.enabled %} + RequestHeader unset Authorization + {% endif %} + + + + ProxyPass http://{{ jenkins.host }}:{{ jenkins.port }}/ghprbhook/ + ProxyPassReverse http://{{ jenkins.host }}:{{ jenkins.port }}/ghprbhook/ + {% if jenkins.apache.basicauth.enabled %} + RequestHeader unset Authorization + {% endif %} + +{% if jenkins.apache.basicauth.enabled %} + + AuthType Basic + AuthName "Restricted" + AuthUserFile /etc/apache2/htpasswd + Require valid-user + +{% endif %} + + + + + ServerName {{ jenkins.apache.servername }} + ServerAdmin jenkins + + {{ apache.ssl.settings }} + SSLCertificateFile /etc/ssl/certs/jenkins.crt + SSLCertificateKeyFile /etc/ssl/private/jenkins.key + {% if jenkins.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/jenkins-intermediate.crt + {% endif %} + + DocumentRoot /var/www/html + ErrorDocument 404 /index.html + + {% if jenkins.apache.github_hooks_only %} + + ProxyPass http://{{ jenkins.host }}:{{ jenkins.port }}/github-webhook/ + ProxyPassReverse http://{{ jenkins.host }}:{{ jenkins.port }}/github-webhook/ + {% if jenkins.apache.basicauth.enabled %} + RequestHeader unset Authorization + {% endif %} + + + + ProxyPass http://{{ jenkins.host }}:{{ jenkins.port }}/ghprbhook/ + ProxyPassReverse http://{{ jenkins.host }}:{{ jenkins.port }}/ghprbhook/ + {% if jenkins.apache.basicauth.enabled %} + RequestHeader unset Authorization + {% endif %} + + + {% if jenkins.apache.basicauth.enabled %} + + AuthType Basic + AuthName "Restricted" + AuthUserFile /etc/apache2/htpasswd + Require valid-user + + {% endif %} + + {% else %} + + ProxyPass http://{{ jenkins.host }}:{{ jenkins.port }}/ + ProxyPassReverse http://{{ jenkins.host }}:{{ jenkins.port }}/ + {% if jenkins.apache.basicauth.enabled %} + AuthType Basic + AuthName "Restricted" + AuthUserFile /etc/apache2/htpasswd + Require valid-user + {% endif %} + + {% endif %} + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + CustomLog ${APACHE_LOG_DIR}/jenkins-access.log combined + ErrorLog ${APACHE_LOG_DIR}/jenkins-error.log + + RequestHeader set X-Forwarded-Proto "https" + RequestHeader set X-Forwarded-Port "443" + diff --git a/roles/jenkins-master/templates/etc/default/jenkins b/roles/jenkins-master/templates/etc/default/jenkins new file mode 100644 index 0000000..92cdf9a --- /dev/null +++ b/roles/jenkins-master/templates/etc/default/jenkins @@ -0,0 +1,70 @@ +# {{ ansible_managed }} + +# defaults for jenkins continuous integration server + +# pulled in from the init script; makes things easier. +NAME=jenkins + +# location of java +JAVA=/usr/bin/java + +# arguments to pass to java +JAVA_ARGS="-Djava.awt.headless=true" # Allow graphs etc. to work even when an X server is present +#JAVA_ARGS="-Xmx256m" +#JAVA_ARGS="-Djava.net.preferIPv4Stack=true" # make jenkins listen on IPv4 address + +PIDFILE=/var/run/$NAME/$NAME.pid + +# user and group to be invoked as (default to jenkins) +JENKINS_USER=$NAME +JENKINS_GROUP=$NAME + +# location of the jenkins war file +JENKINS_WAR=/usr/share/$NAME/$NAME.war + +# jenkins home location +JENKINS_HOME=/var/lib/$NAME + +# set this to false if you don't want Hudson to run by itself +# in this set up, you are expected to provide a servlet container +# to host jenkins. +RUN_STANDALONE=true + +# log location. this may be a syslog facility.priority +JENKINS_LOG=/var/log/$NAME/$NAME.log +#JENKINS_LOG=daemon.info + +# OS LIMITS SETUP +# comment this out to observe /etc/security/limits.conf +# this is on by default because http://github.com/jenkinsci/jenkins/commit/2fb288474e980d0e7ff9c4a3b768874835a3e92e +# reported that Ubuntu's PAM configuration doesn't include pam_limits.so, and as a result the # of file +# descriptors are forced to 1024 regardless of /etc/security/limits.conf +MAXOPENFILES=8192 + +# set the umask to control permission bits of files that Jenkins creates. +# 027 makes files read-only for group and inaccessible for others, which some security sensitive users +# might consider benefitial, especially if Jenkins runs in a box that's used for multiple purposes. +# Beware that 027 permission would interfere with sudo scripts that run on the master (JENKINS-25065.) +# +# Note also that the particularly sensitive part of $JENKINS_HOME (such as credentials) are always +# written without 'others' access. So the umask values only affect job configuration, build records, +# that sort of things. +# +# If commented out, the value from the OS is inherited, which is normally 022 (as of Ubuntu 12.04, +# by default umask comes from pam_umask(8) and /etc/login.defs + +# UMASK=027 + +# port for HTTP connector (default 8080; disable with -1) +HTTP_PORT={{ jenkins.port }} + +# IP to listen on +HTTP_HOST={{ jenkins.host }} + +# port for AJP connector (disabled by default) +AJP_PORT=-1 + +# servlet context, important if you want to use apache proxying +PREFIX=/{{ jenkins.prefix }} + +JENKINS_ARGS="--webroot=/var/cache/$NAME/war --prefix=$PREFIX --httpPort=$HTTP_PORT --ajp13Port=$AJP_PORT --httpListenAddress=$HTTP_HOST" diff --git a/roles/jenkins-master/templates/etc/jenkins_jobs/jenkins_jobs.ini b/roles/jenkins-master/templates/etc/jenkins_jobs/jenkins_jobs.ini new file mode 100644 index 0000000..8568712 --- /dev/null +++ b/roles/jenkins-master/templates/etc/jenkins_jobs/jenkins_jobs.ini @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +[job_builder] +allow_duplicates=True + +[jenkins] +user={{ jenkins.admin_username|default('') }} +password={{ jenkins.admin_password|default('') }} +url=http://{{ jenkins.host }}:{{ jenkins.port }}/{{ jenkins.prefix }} diff --git a/roles/jenkins-master/templates/serverspec/jenkins_spec.rb b/roles/jenkins-master/templates/serverspec/jenkins_spec.rb new file mode 100644 index 0000000..943d656 --- /dev/null +++ b/roles/jenkins-master/templates/serverspec/jenkins_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe user('jenkins') do + it { should exist } #JNK002 + it { should belong_to_group 'jenkins' } #JNK003 + it { should have_home_directory '/var/lib/jenkins' } #JNK004 + it { should_not have_login_shell '/usr/sbin/nologin' } #JNK005 +end + +describe file('/var/lib/jenkins') do + it { should be_owned_by 'jenkins' } #JNK007 + it { should be_grouped_into 'jenkins' } #JNK008 + it { should be_mode 755 } #JNK006 + it { should be_directory } #JNK006 +end + +describe file('/var/log/jenkins') do + it { should be_owned_by 'jenkins' } #JNK007 + it { should be_grouped_into 'jenkins' } #JNK008 + it { should be_mode 755 } #JNK006 + it { should be_directory } #JNK006 +end + +describe command(%q) do + its(:stdout) { should contain('644') } #JNK009 + its(:stdout) { should contain('.xml') } #JNK009 +end + +describe command(%q) do + its(:stdout) { should contain('jenkins') } #JNK010 +end + +describe command(%q) do + its(:stdout) { should contain('jenkins') } #JNK011 +end + +describe file('/var/log/jenkins/jenkins.log') do + it { should be_owned_by 'jenkins' } #JNK013 + it { should be_grouped_into 'jenkins' } #JNK014 + it { should be_mode 644 } #JNK012 + it { should be_file } #JNK012 +end + +describe file('/etc/logrotate.d/jenkins') do + it { should be_owned_by 'root' } #JNK017 + it { should be_grouped_into 'root' } #JNK018 + it { should be_mode 644 } #JNK016 + it { should be_file } #JNK016 + its(:content) { should contain 'weekly' } #JNK019 + #its(:content) { should contain 'rotate 90' } #JNK019 - logrotate.d/jenkins/ set to `rotate 52`, needs to be `rotate 90` +end + +describe file('/var/lib/jenkins/private_vars') do + it { should be_owned_by 'jenkins' } #JNK021 + it { should be_grouped_into 'root' } #JNK022 + it { should be_mode 755 } #JNK020 + it { should be_directory } #JNK020 +end + +describe file('/var/lib/jenkins/private_vars/default.yml') do + it { should be_owned_by 'jenkins' } #JNK023 + it { should be_grouped_into 'root' } #JNK023 + it { should be_mode 644 } #JNK023 - Needs update to 640 + it { should be_file } #JNK023 +end + +describe file('/var/lib/jenkins/.packagecloud') do + it { should be_owned_by 'jenkins' } #JNK024 + it { should be_grouped_into 'root' } #JNK024 + it { should be_mode 600 } #JNK024 + it { should be_file } #JNK024 +end + +# Waiting for access to tardis-sl to write/test the ansible automation for this rule. +#describe file('~jenkins/.ssh/*') do +# it { should be_owned_by 'jenkins' } #JNK025 +# it { should be_grouped_into 'jenkins' } #JNK025 +# it { should be_mode 600 } #JNK025 +# it { should be_file } #JNK025 +#end +# JNK026 - max files open - deprecated rule/test + +describe package('jenkins') do + it { should be_installed } #JNK027 +end + +describe service('jenkins') do + it { should be_enabled } #JNK027 +end + +describe package('openjdk-7-jre') do + it { should be_installed } #JNK028 +end + +describe file('/var/lib/jenkins/plugins/timestamper') do + it { should exist } #JNK030 + it { should be_directory } #JNK030 +end + +describe file('/var/lib/jenkins/plugins/ws-cleanup') do + it { should exist } #JNK031 + it { should be_directory } #JNK031 +end + diff --git a/roles/jenkins-master/templates/ssh-key b/roles/jenkins-master/templates/ssh-key new file mode 100644 index 0000000..dc31938 --- /dev/null +++ b/roles/jenkins-master/templates/ssh-key @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ item.key }} diff --git a/roles/jenkins-master/templates/var/lib/jenkins/com.tikal.jenkins.plugins.multijob.PhaseJobsConfig.xml b/roles/jenkins-master/templates/var/lib/jenkins/com.tikal.jenkins.plugins.multijob.PhaseJobsConfig.xml new file mode 100644 index 0000000..161adfe --- /dev/null +++ b/roles/jenkins-master/templates/var/lib/jenkins/com.tikal.jenkins.plugins.multijob.PhaseJobsConfig.xml @@ -0,0 +1,11 @@ + + + + {% for rule in jenkins.multijob.retry.rules %} + + {{ rule.name }} + {{ rule.path }} + + {% endfor %} + + diff --git a/roles/jenkins-master/templates/var/lib/jenkins/config.xml b/roles/jenkins-master/templates/var/lib/jenkins/config.xml new file mode 100644 index 0000000..c106050 --- /dev/null +++ b/roles/jenkins-master/templates/var/lib/jenkins/config.xml @@ -0,0 +1,61 @@ + + + + 1.610 + 4 + {{ ( jenkins.master_mode_exclusive|bool ) | ternary('EXCLUSIVE','NORMAL') }} + true + + + + {% for org in jenkins.security.github.orgs %} + {{ org }}{% endfor %} + + + {% for admin in jenkins.security.github.admins %} + {{ admin }}{% endfor %} + + true + false + false + true + false + false + false + + + + {{ jenkins.security.github.web_uri }} + {{ jenkins.security.github.api_uri }} + {{ jenkins.security.github.client_id }} + {{ jenkins.security.github.client_secret }} + read:org,user:email + + false + + ${ITEM_ROOTDIR}/workspace + ${ITEM_ROOTDIR}/builds + + false + + + + + + 5 + 0 + + + + All + false + false + + + + All + 0 + + + + diff --git a/roles/jenkins-master/templates/var/lib/jenkins/credentials.xml b/roles/jenkins-master/templates/var/lib/jenkins/credentials.xml new file mode 100644 index 0000000..f668835 --- /dev/null +++ b/roles/jenkins-master/templates/var/lib/jenkins/credentials.xml @@ -0,0 +1,72 @@ + + + + {% for domain in jenkins.security.credentials %} + + + {% if domain.domain == 'GLOBAL' %} + + {% else %} + {{ domain.domain }} + {{ domain.description|default('') }} + + + {{ domain.hostname }} + + + + {{ domain.uri_scheme }} + + + + {{ domain.uri_path }} + false + + + {% endif %} + + + {% for ssh in domain.ssh_keys %} + + GLOBAL + {{ ssh.uuid }} + {{ ssh.description|default('') }} + {{ ssh.username }} + {{ ssh.passphrase|jenkins_encrypt(jenkins_master_key, jenkins_secret_key) }} + {% if ssh.private_key|default('') %} + + {{ ssh.private_key }} + + {% else %} + {% if ssh.private_key_path|default('') %} + + {{ ssh.private_key_path }} + + {% else %} + + {% endif %} + {% endif %} + + {% endfor %} + {% for auth in domain.basic_auth %} + + GLOBAL + {{ auth.uuid }} + {{ auth.description|default('') }} + {{ auth.username }} + {{ auth.password|jenkins_encrypt(jenkins_master_key, jenkins_secret_key) }} + + {% endfor %} + {% for token in domain.tokens %} + + GLOBAL + {{ token.uuid }} + {{ token.description|default('') }} + {{ token.secret|jenkins_encrypt(jenkins_master_key, jenkins_secret_key) }} + + {% endfor %} + + + {% endfor %} + + \ No newline at end of file diff --git a/roles/jenkins-master/templates/var/lib/jenkins/gerrit-trigger.xml b/roles/jenkins-master/templates/var/lib/jenkins/gerrit-trigger.xml new file mode 100644 index 0000000..a76b1ed --- /dev/null +++ b/roles/jenkins-master/templates/var/lib/jenkins/gerrit-trigger.xml @@ -0,0 +1,77 @@ + + + + {% for server in jenkins.gerrit_servers %} + + {{ server.name }} + false + + {{ server.host }} + {{ server.port }} + + {{ server.username }} + + {{ server.ssh.key }} + {{ server.ssh.password }} + false + false + false + {{ server.messages.successful }} + {{ server.messages.unstable }} + {{ server.messages.failed }} + {{ server.messages.started }} + {{ server.messages.notbuilt }} + {{ server.url }} + {{ server.voting.verify.started }} + {{ server.voting.verify.successful }} + {{ server.voting.verify.failed }} + {{ server.voting.verify.unstable }} + {{ server.voting.verify.notbuilt }} + {{ server.voting.codereview.started }} + {{ server.voting.codereview.successful }} + {{ server.voting.codereview.failed }} + {{ server.voting.codereview.unstable }} + {{ server.voting.codereview.notbuilt }} + true + true + 3 + 30 + true + 3600 + 0 + + + Code-Review + Code Review + + + Verified + Verified + + + + false + + false + + 0 + + + + + ALL + + false + false + false + + + + {% endfor %} + + + 3 + 1 + 360 + + diff --git a/roles/jenkins-master/templates/var/lib/jenkins/github-plugin-configuration.xml b/roles/jenkins-master/templates/var/lib/jenkins/github-plugin-configuration.xml new file mode 100644 index 0000000..ef32dc0 --- /dev/null +++ b/roles/jenkins-master/templates/var/lib/jenkins/github-plugin-configuration.xml @@ -0,0 +1,12 @@ + + + + {% for server in jenkins.github_servers %} + + {{ server.api_url }} + {{ server.manage_hooks|default('true') }} + {{ server.credentials_id }} + + {% endfor %} + + \ No newline at end of file diff --git a/roles/jenkins-master/templates/var/lib/jenkins/jenkins.model.JenkinsLocationConfiguration.xml b/roles/jenkins-master/templates/var/lib/jenkins/jenkins.model.JenkinsLocationConfiguration.xml new file mode 100644 index 0000000..245831b --- /dev/null +++ b/roles/jenkins-master/templates/var/lib/jenkins/jenkins.model.JenkinsLocationConfiguration.xml @@ -0,0 +1,5 @@ + + + address not configured <nobody@nowhere> + {{ jenkins.url }} + diff --git a/roles/jenkins-master/templates/var/lib/jenkins/jenkins.plugins.slack.SlackNotifier.xml b/roles/jenkins-master/templates/var/lib/jenkins/jenkins.plugins.slack.SlackNotifier.xml new file mode 100644 index 0000000..4629def --- /dev/null +++ b/roles/jenkins-master/templates/var/lib/jenkins/jenkins.plugins.slack.SlackNotifier.xml @@ -0,0 +1,7 @@ + + + {{ jenkins.slack.teamdomain }} + {{ jenkins.slack.token }} + {{ jenkins.slack.room }} + {{ jenkins.slack.buildserverurl }} + diff --git a/roles/jenkins-slave/defaults/main.yml b/roles/jenkins-slave/defaults/main.yml new file mode 100644 index 0000000..a795eb7 --- /dev/null +++ b/roles/jenkins-slave/defaults/main.yml @@ -0,0 +1,7 @@ +common: + users: + - name: jenkins + home: /var/lib/jenkins +jenkins: + packagecloud: + token: "thisisbadtoken" diff --git a/roles/jenkins-slave/meta/main.yml b/roles/jenkins-slave/meta/main.yml new file mode 100644 index 0000000..f06c8cf --- /dev/null +++ b/roles/jenkins-slave/meta/main.yml @@ -0,0 +1,8 @@ +--- +dependencies: + - role: jenkins-common + # NOTE: jenkins slave needs the private repo to calculate the latest version + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.openstack_packages.repo }} trusty main' + key_url: '{{ apt_repos.openstack_packages.key_url }}' diff --git a/roles/jenkins-slave/tasks/checks.yml b/roles/jenkins-slave/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/jenkins-slave/tasks/main.yml b/roles/jenkins-slave/tasks/main.yml new file mode 100644 index 0000000..f77aac8 --- /dev/null +++ b/roles/jenkins-slave/tasks/main.yml @@ -0,0 +1,60 @@ +--- +- name: install gitconfig + template: + src: gitconfig + dest: ~jenkins/.gitconfig + +- name: install system dependencies + apt: pkg={{ item }} + with_items: "{{ jenkins.slave.apt }}" + +- name: install python dependencies + pip: name={{ item }} + with_items: "{{ jenkins.slave.pip }}" + +- name: install gem dependencies + gem: + name: "{{ item }}" + user_install: no + include_dependencies: yes + with_items: "{{ jenkins.slave.gem }}" + register: result + until: result|succeeded + retries: 5 + +- name: package_cloud credentials file + template: + src: var/lib/jenkins/packagecloud + dest: ~jenkins/.packagecloud + owner: jenkins + mode: "0600" + +- name: make vault private_vars directories + file: dest=~jenkins/private_vars state=directory owner=jenkins + +- name: make vault rhel and ubuntu directories + file: dest=~jenkins/private_vars/{{ item }} state=directory owner=jenkins + with_items: + - rhel + - ubuntu + +- name: copy private variables templates into place + template: + src: private_vars/{{ item }}/default.yml + dest: ~jenkins/private_vars/{{ item }}/default.yml + owner: jenkins + with_items: + - rhel + - ubuntu + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/jenkins-slave/tasks/metrics.yml b/roles/jenkins-slave/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/jenkins-slave/tasks/serverspec.yml b/roles/jenkins-slave/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/jenkins-slave/templates/gitconfig b/roles/jenkins-slave/templates/gitconfig new file mode 100644 index 0000000..d6be5f4 --- /dev/null +++ b/roles/jenkins-slave/templates/gitconfig @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +[user] + email = {{ jenkins.git.email }} + name = {{ jenkins.git.name }} diff --git a/roles/jenkins-slave/templates/home/jenkins/jenkins.stackrc b/roles/jenkins-slave/templates/home/jenkins/jenkins.stackrc new file mode 100644 index 0000000..5ad38b0 --- /dev/null +++ b/roles/jenkins-slave/templates/home/jenkins/jenkins.stackrc @@ -0,0 +1 @@ +{{ jenkins.stackrc }} \ No newline at end of file diff --git a/roles/jenkins-slave/templates/private_vars/rhel/default.yml b/roles/jenkins-slave/templates/private_vars/rhel/default.yml new file mode 100644 index 0000000..f01c3ef --- /dev/null +++ b/roles/jenkins-slave/templates/private_vars/rhel/default.yml @@ -0,0 +1,37 @@ +rhn_subscription: + username: {{ secrets.rhn_subscription.username }} + password: {{ secrets.rhn_subscription.password }} + +keystone: + jellyroll: True + distro: + python_post_dependencies: + - name: python-memcached + - name: jellyroll + version: {{ distro.python_post_dependencies.jellyroll.version }} + pip_extra_args: "--extra-index-url {{ pypi_repo.bluebox }}" + +neutron: + jellyroll: True + distro: + python_post_dependencies: + - name: python-memcached + - name: jellyroll + version: {{ distro.python_post_dependencies.jellyroll.version }} + pip_extra_args: "--extra-index-url {{ pypi_repo.bluebox }}" + +heat: + distro: + python_post_dependencies: + - name: semver + - name: ibm-sw-orch + version: {{ distro.python_post_dependencies.ibm_sw_orch.version }} + pip_extra_args: "--extra-index-url {{ pypi_repo.ucd }}" + +horizon: + customize: True + distro: + python_post_dependencies: + - name: bb-horizon-customization + version: {{ distro.python_post_dependencies.bb_horizon_customization.version }} + pip_extra_args: "--extra-index-url {{ pypi_repo.bluebox }}" diff --git a/roles/jenkins-slave/templates/private_vars/ubuntu/default.yml b/roles/jenkins-slave/templates/private_vars/ubuntu/default.yml new file mode 100644 index 0000000..7a1a38c --- /dev/null +++ b/roles/jenkins-slave/templates/private_vars/ubuntu/default.yml @@ -0,0 +1,4 @@ +apt_repos: + blueboxcloud_giftwrap: + repo: {{ apt_repos.openstack_packages.repo }} + key_url: {{ apt_repos.openstack_packages.key_url }} diff --git a/roles/jenkins-slave/templates/var/lib/jenkins/packagecloud b/roles/jenkins-slave/templates/var/lib/jenkins/packagecloud new file mode 100644 index 0000000..867f9bf --- /dev/null +++ b/roles/jenkins-slave/templates/var/lib/jenkins/packagecloud @@ -0,0 +1 @@ +{"url":"https://packagecloud.io","token":"{{ jenkins.packagecloud.token }}"} diff --git a/roles/kibana/defaults/main.yml b/roles/kibana/defaults/main.yml new file mode 100644 index 0000000..708b2b5 --- /dev/null +++ b/roles/kibana/defaults/main.yml @@ -0,0 +1,29 @@ +--- +kibana: + version: "5.4.1" + config: + server.host: 0.0.0.0 + server.port: 5601 + server.basePath: "/kibana" + logging.dest: /var/log/kibana/kibana.log + firewall: [] + plugins: + - name: x-pack@5.4.1 + file: x-pack-5.4.1.zip + url: http://artifacts.elastic.co/downloads/packs/x-pack/x-pack-5.4.1.zip + sha256sum: 7a93565b8d4af2d7d4dc804245543a4f38b2b31f8d0395dad792b7ea05d0088c + config: + xpack.security.enabled: false + default_index: + name: "[logstash-]YYYY.MM.DD" + intervalName: "days" + config_in_es: true + force_config: false + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/upstart/kibana.log + fields: + tags: elk,kibana + logging: + forwarder: filebeat diff --git a/roles/kibana/handlers/main.yml b/roles/kibana/handlers/main.yml new file mode 100644 index 0000000..55ea3d3 --- /dev/null +++ b/roles/kibana/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart kibana + service: name=kibana state=restarted diff --git a/roles/kibana/meta/main.yml b/roles/kibana/meta/main.yml new file mode 100644 index 0000000..158ad7c --- /dev/null +++ b/roles/kibana/meta/main.yml @@ -0,0 +1,12 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.kibana.repo }} stable main' + key_url: '{{ apt_repos.kibana.key_url }}' + - role: logging-config + service: kibana + logdata: "{{ kibana.logs }}" + forward_type: "{{ kibana.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/kibana/tasks/checks.yml b/roles/kibana/tasks/checks.yml new file mode 100644 index 0000000..1ee73e9 --- /dev/null +++ b/roles/kibana/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: KIB010 install kibana process check + sensu_check_dict: name="check-kibana-process" check="{{ sensu_checks.kibana.check_kibana_process }}" + notify: restart sensu-client missing ok diff --git a/roles/kibana/tasks/config.yml b/roles/kibana/tasks/config.yml new file mode 100644 index 0000000..460df18 --- /dev/null +++ b/roles/kibana/tasks/config.yml @@ -0,0 +1,37 @@ +--- +- name: check if kibana has config + uri: method=HEAD return_content=yes status_code=200,404 + url=http://localhost:9200/.kibana/config/{{ kibana.version }} + register: kibana_configured + until: kibana_configured|succeeded + retries: 5 + delay: 120 + +- name: check if kibana has index-pattern + uri: method=HEAD return_content=yes status_code=200,404 + url=http://localhost:9200/.kibana/index-pattern/{{ kibana.default_index.name }} + register: kibana_has_index_pattern + +- name: install kibana json + template: src=opt/kibana/config/{{ item }} dest=/etc/kibana/{{ item }} + with_items: + - config.json + - index-pattern.json + +- name: upload kibana config + uri: + url: http://localhost:9200/.kibana/config/{{ kibana.version }}?version_type=force&version=1 + method: PUT + body: "{{ lookup('template', 'opt/kibana/config/config.json') }}" + body_format: json + status_code: 200,201 + when: kibana_configured.status != 200 or kibana.force_config + +- name: upload kibana index-pattern + uri: + url: http://localhost:9200/.kibana/index-pattern/{{ kibana.default_index.name }}?version_type=force&version=1 + method: PUT + body: "{{ lookup('template', 'opt/kibana/config/index-pattern.json') }}" + body_format: json + status_code: 200,201 + when: kibana_has_index_pattern.status != 200 or kibana.force_config diff --git a/roles/kibana/tasks/main.yml b/roles/kibana/tasks/main.yml new file mode 100644 index 0000000..7119827 --- /dev/null +++ b/roles/kibana/tasks/main.yml @@ -0,0 +1,97 @@ +--- +- name: install kibana + apt: + name: "kibana={{ kibana.version }}" + register: result + until: result|succeeded + retries: 5 + +- name: ensure kibana logs directory exists + file: + dest: /var/log/kibana + state: directory + owner: kibana + mode: 0750 + +- name: configure kibana + template: src=opt/kibana/config/kibana.yml + dest="/etc/kibana/kibana.yml" + notify: restart kibana + tags: kibana-config + +- meta: flush_handlers + +- name: enable and start kibana service + service: name=kibana state=started enabled=yes + +- name: allow kibana traffic + ufw: rule=allow to_port={{ item.0.port }} src={{ item.1|default('127.0.0.1') }} + with_subelements: + - "{{ kibana.firewall }}" + - src + tags: + - firewall + +- name: wait until kibana is listening + wait_for: port=5601 + +- name: make kibana plugin directory + file: + dest: /opt/kibana-plugins + mode: 0755 + state: directory + tags: kibana-plugins + +- name: get list of installed kibana plugins + command: /usr/share/kibana/bin/kibana-plugin list + register: installed_plugins + changed_when: false + tags: kibana-plugins + +- name: download kibana plugin files via proxy + get_url: + url: "{{ item.url }}" + dest: "/opt/kibana-plugins/{{ item.file }}" + mode: 0644 + sha256sum: "{{ item.sha256sum|default(omit) }}" + force: "{{ item.force|default(omit) }}" + with_items: "{{ kibana.plugins }}" + when: + - item.name not in installed_plugins.stdout_lines + - item.url is defined + tags: kibana-plugins + register: result + until: result|succeeded + retries: 5 + +- name: install kibana plugins (may take a while) + shell: "sudo -u kibana /usr/share/kibana/bin/kibana-plugin install file:///opt/kibana-plugins/{{ item.file }}" + with_items: "{{ kibana.plugins }}" + when: item.name not in installed_plugins.stdout_lines + tags: kibana-plugins + notify: restart kibana + +- include: config.yml + when: kibana.config_in_es|bool + tags: kibana-config + run_once: true + +- name: allow kibana traffic + ufw: rule=allow to_port={{ item.0.port }} src={{ item.1|default('127.0.0.1') }} + with_subelements: + - "{{ kibana.firewall }}" + - src + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/kibana/tasks/metrics.yml b/roles/kibana/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/kibana/tasks/serverspec.yml b/roles/kibana/tasks/serverspec.yml new file mode 100644 index 0000000..8836a49 --- /dev/null +++ b/roles/kibana/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests kibana tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/kibana/templates/etc/init/kibana.conf b/roles/kibana/templates/etc/init/kibana.conf new file mode 100644 index 0000000..1e6a34a --- /dev/null +++ b/roles/kibana/templates/etc/init/kibana.conf @@ -0,0 +1,25 @@ +# {{ ansible_managed }} + +# kibana.conf# kibana - log viewer +# + +description "Kibana elasticsearch viewer" + +start on virtual-filesystems +stop on runlevel [06] + +respawn +respawn limit 5 30 +limit nofile 65550 65550 + +# Environment +env HOME=/opt/kibana-{{ kibana.version }}-linux-x64 +chdir /opt/kibana-{{ kibana.version }}-linux-x64 +setuid www-data +setgid www-data +console log + +# Run Kibana, which is in /opt/kibana +script + bin/kibana +end script diff --git a/roles/kibana/templates/opt/kibana/config/config.json b/roles/kibana/templates/opt/kibana/config/config.json new file mode 100644 index 0000000..0b80008 --- /dev/null +++ b/roles/kibana/templates/opt/kibana/config/config.json @@ -0,0 +1,3 @@ +{ + "defaultIndex": "{{ kibana.default_index.name }}" +} diff --git a/roles/kibana/templates/opt/kibana/config/index-pattern.json b/roles/kibana/templates/opt/kibana/config/index-pattern.json new file mode 100644 index 0000000..c964f51 --- /dev/null +++ b/roles/kibana/templates/opt/kibana/config/index-pattern.json @@ -0,0 +1,9 @@ +{ + "title":"{{ kibana.default_index.name }}", + "timeFieldName":"@timestamp", + {% if kibana.default_index.intervalName is defined %} + "intervalName":"{{ kibana.default_index.intervalName }}", + {% endif %} + "customFormats":"{}", + "fields":"[{\"type\":\"string\",\"indexed\":false,\"analyzed\":false,\"name\":\"_source\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":true,\"doc_values\":false,\"name\":\"type\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false,\"name\":\"@version\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"name\":\"_type\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":false,\"analyzed\":false,\"name\":\"_id\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false,\"name\":\"file\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false,\"name\":\"offset\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"doc_values\":true,\"name\":\"tags\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"doc_values\":true,\"name\":\"host\",\"count\":1,\"scripted\":false},{\"type\":\"string\",\"indexed\":false,\"analyzed\":false,\"name\":\"_index\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":false,\"analyzed\":false,\"name\":\"_routing\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":true,\"doc_values\":false,\"name\":\"message\",\"count\":1,\"scripted\":false},{\"type\":\"date\",\"indexed\":true,\"analyzed\":false,\"doc_values\":false,\"name\":\"@timestamp\",\"count\":0,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"doc_values\":true,\"name\":\"customer_id\",\"count\":1,\"scripted\":false},{\"type\":\"string\",\"indexed\":true,\"analyzed\":false,\"doc_values\":true,\"name\":\"cluster_name\",\"count\":1,\"scripted\":false}]" +} diff --git a/roles/kibana/templates/opt/kibana/config/kibana.yml b/roles/kibana/templates/opt/kibana/config/kibana.yml new file mode 100644 index 0000000..8cb7a18 --- /dev/null +++ b/roles/kibana/templates/opt/kibana/config/kibana.yml @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +# Base Kibana Config +{{ kibana.config | to_yaml(default_flow_style=False) }} + +{% for plugin in kibana.plugins %}{% if plugin.config is defined %} +# Kibana Plugin {{ plugin.name }} Config +{{ plugin.config | to_yaml(default_flow_style=False) }} +{% endif %}{% endfor %} diff --git a/roles/kibana/templates/serverspec/kibana_spec.rb b/roles/kibana/templates/serverspec/kibana_spec.rb new file mode 100644 index 0000000..59f9d05 --- /dev/null +++ b/roles/kibana/templates/serverspec/kibana_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe package('openjdk-7-jre') do + it { should be_installed.by('apt') } #KIB001 +end + +describe file('/opt/kibana') do + it { should be_mode 775 } #KIB010 + it { should be_directory } #KIB011 + it { should be_owned_by 'root' } #KIB012 + it { should be_grouped_into 'root' } #KIB013 +end + +describe file('/opt/kibana/config/kibana.yml') do + it { should be_file } #KIB014 +end + +describe file('/etc/init.d/kibana') do + it { should be_file } #KIB015 +end + +describe port(5601) do + it { should be_listening } #KIB009 +end + +describe service('kibana') do + it { should be_enabled } +end diff --git a/roles/logging-config/defaults/main.yml b/roles/logging-config/defaults/main.yml new file mode 100644 index 0000000..696f4e4 --- /dev/null +++ b/roles/logging-config/defaults/main.yml @@ -0,0 +1,28 @@ +--- +#logging: +# follow: +# enabled: true +# global_fields: +# customer_id: "unknown" +# cluster_name: "unknown" +# logs: [] + +# EXAMPLE +#logs: +# +# FILEBEAT +#- paths: +# - /var/log/syslog +# document_type: syslog +# fields: +# tags: syslog +# +# LOGSTASH FORWARDER +#- paths: +# - /var/log/syslog +# fields: +# type: syslog +# tags: syslog +# +#logging: +# forwarder: filebeat # or 'logstash-forwarder' diff --git a/roles/logging-config/handlers/main.yml b/roles/logging-config/handlers/main.yml new file mode 100644 index 0000000..53d00fc --- /dev/null +++ b/roles/logging-config/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: restart filebeat + service: + name: filebeat + state: restarted + register: handler + failed_when: + - handler.failed is defined + - handler.msg.find('Could not find the requested service') == -1 + - handler.msg.find('service not found') == -1 diff --git a/roles/logging-config/meta/main.yml b/roles/logging-config/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/logging-config/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/logging-config/tasks/checks.yml b/roles/logging-config/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/logging-config/tasks/filebeat.yml b/roles/logging-config/tasks/filebeat.yml new file mode 100644 index 0000000..c1799bc --- /dev/null +++ b/roles/logging-config/tasks/filebeat.yml @@ -0,0 +1,18 @@ +--- +- name: filebeat config directory + file: + dest: /etc/filebeat/filebeat.d + state: directory + mode: 0755 + +- name: "install {{ service }} log template" + template: + src: etc/filebeat/filebeat.d/template.yml + dest: "/etc/filebeat/filebeat.d/{{ service }}.yml" + notify: restart filebeat + +- name: "remove {{ service }} log template for logstash-forwarder" + file: + path: "/etc/logstash-forwarder.d/{{ service }}.conf" + state: absent + notify: restart logstash-forwarder diff --git a/roles/logging-config/tasks/main.yml b/roles/logging-config/tasks/main.yml new file mode 100644 index 0000000..92814f1 --- /dev/null +++ b/roles/logging-config/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- include: filebeat.yml + tags: logging-config + +- meta: flush_handlers + tags: logging-config + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/logging-config/tasks/metrics.yml b/roles/logging-config/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/logging-config/tasks/serverspec.yml b/roles/logging-config/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/logging-config/templates/etc/filebeat/filebeat.d/template.yml b/roles/logging-config/templates/etc/filebeat/filebeat.d/template.yml new file mode 100644 index 0000000..fd7ee9d --- /dev/null +++ b/roles/logging-config/templates/etc/filebeat/filebeat.d/template.yml @@ -0,0 +1,5 @@ +# {{ ansible_managed }} +--- +filebeat: + prospectors: + {{ logdata | to_yaml( indent=2, default_flow_style=False ) | indent ( width=4 ) }} diff --git a/roles/logging/defaults/main.yml b/roles/logging/defaults/main.yml new file mode 100644 index 0000000..9e5855e --- /dev/null +++ b/roles/logging/defaults/main.yml @@ -0,0 +1,21 @@ +logging: + enabled: true + service: + user: logstash + group: adm + home: /var/lib/logstash + follow: + enabled: true + global_fields: + customer_id: "unknown" + cluster_name: "unknown" + logs: [] + forward: + host: null + filebeat: + version: 1.3.1 + port: 4561 + config_dir: /etc/filebeat/filebeat.d + tls: + enabled: false + ca_cert: ~ diff --git a/roles/logging/files/etc/init/logstash-forwarder.conf b/roles/logging/files/etc/init/logstash-forwarder.conf new file mode 100644 index 0000000..bf47934 --- /dev/null +++ b/roles/logging/files/etc/init/logstash-forwarder.conf @@ -0,0 +1,15 @@ +description "Logstash Forwarding Agent" +author "Myles Steinhauser" + +setuid logstash +setgid adm + +respawn +respawn limit 3 30 + +start on runlevel [2345] +stop on runlevel [!2345] + +chdir /var/lib/logstash + +exec /opt/logstash-forwarder/bin/logstash-forwarder.sh -config /etc/logstash-forwarder.d diff --git a/roles/logging/files/etc/rsyslog.d/49-haproxy.conf b/roles/logging/files/etc/rsyslog.d/49-haproxy.conf new file mode 100644 index 0000000..822316c --- /dev/null +++ b/roles/logging/files/etc/rsyslog.d/49-haproxy.conf @@ -0,0 +1,7 @@ +# Create an additional socket in haproxy's chroot in order to allow logging via +# /dev/log to chroot'ed HAProxy processes +$AddUnixListenSocket /var/lib/haproxy/dev/log + +# Write HAProxy messages to async dedicated logfile +if $programname startswith 'haproxy' then -/var/log/haproxy.log +& stop diff --git a/roles/logging/handlers/main.yml b/roles/logging/handlers/main.yml new file mode 100644 index 0000000..62ded3b --- /dev/null +++ b/roles/logging/handlers/main.yml @@ -0,0 +1,21 @@ +--- +- name: refresh cert auths + command: update-ca-certificates + +- name: restart logstash-forwarder + service: name=logstash-forwarder state=restarted + +- name: stop logstash-forwarder + service: name=logstash-forwarder state=stopped + +- name: stop and disable logstash-forwarder + service: name=logstash-forwarder state=stopped enabled=no + +- name: restart filebeat + service: name=filbeat state=restarted + +- name: stop filebeat + service: name=filebeat state=stopped + +- name: stop and disable filebeat + service: name=filebeat state=stopped enabled=no diff --git a/roles/logging/meta/main.yml b/roles/logging/meta/main.yml new file mode 100644 index 0000000..88d9963 --- /dev/null +++ b/roles/logging/meta/main.yml @@ -0,0 +1,15 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.filebeat.repo }} stable main' + key_url: '{{ apt_repos.filebeat.key_url }}' + - role: bbg-ssl + name: "logging-forward-ca_cert" + ssl_cert: ~ + ssl_key: ~ + ssl_intermediate: ~ + ssl_ca_cert: "{{ logging.forward.tls.ca_cert }}" + when: logging.forward.tls.ca_cert + tags: ['bbg-ssl'] + - role: sensu-check diff --git a/roles/logging/tasks/checks.yml b/roles/logging/tasks/checks.yml new file mode 100644 index 0000000..603b964 --- /dev/null +++ b/roles/logging/tasks/checks.yml @@ -0,0 +1,6 @@ +--- +- name: install filebeat process check + sensu_check_dict: + name: "check-filebeat-process" + check: "{{ sensu_checks.logging.check_filebeat_process }}" + notify: restart sensu-client missing ok diff --git a/roles/logging/tasks/filebeat.yml b/roles/logging/tasks/filebeat.yml new file mode 100644 index 0000000..757aa10 --- /dev/null +++ b/roles/logging/tasks/filebeat.yml @@ -0,0 +1,30 @@ +--- +- name: install filebeat + apt: + pkg: "filebeat={{ logging.forward.filebeat.version }}" + notify: restart filebeat + +- name: filebeat config directory + file: + dest: /etc/filebeat/filebeat.d + state: directory + mode: 0755 + +- name: configure filebeat + template: + src: etc/filebeat/filebeat.yml + dest: /etc/filebeat/filebeat.yml + mode: 0644 + notify: restart filebeat + when: logging.forward.host + +# We create our own index template in roles/logstash +- name: remove filebeat.template.json + file: + path: /etc/filebeat/filebeat.template.json + state: absent + +- meta: flush_handlers + +- name: start and enable filebeat service + service: name=filebeat state=started enabled=yes diff --git a/roles/logging/tasks/main.yml b/roles/logging/tasks/main.yml new file mode 100644 index 0000000..fe13a07 --- /dev/null +++ b/roles/logging/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: create logstash user + user: + name: "{{ logging.service.user }}" + home: "{{ logging.service.home }}" + comment: "Logstash Service User" + shell: /sbin/nologin + system: yes + +- include: filebeat.yml + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/logging/tasks/metrics.yml b/roles/logging/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/logging/tasks/serverspec.yml b/roles/logging/tasks/serverspec.yml new file mode 100644 index 0000000..21b9650 --- /dev/null +++ b/roles/logging/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for logging role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/logging/templates/etc/default/logstash-forwarder b/roles/logging/templates/etc/default/logstash-forwarder new file mode 100644 index 0000000..9a4569a --- /dev/null +++ b/roles/logging/templates/etc/default/logstash-forwarder @@ -0,0 +1,8 @@ +# {{ ansible_managed }} + +user="{{ logging.service.user }}" +group="{{ logging.service.group }}" +chroot="/" +chdir="{{ logging.service.home }}" +nice="" +args=-config\ /etc/logstash-forwarder.d diff --git a/roles/logging/templates/etc/filebeat/filebeat.yml b/roles/logging/templates/etc/filebeat/filebeat.yml new file mode 100644 index 0000000..bd15e93 --- /dev/null +++ b/roles/logging/templates/etc/filebeat/filebeat.yml @@ -0,0 +1,18 @@ +# {{ ansible_managed }} +--- +output: + logstash: + hosts: + - "{{ logging.forward.host }}:{{ logging.forward.filebeat.port }}" +{% if logging.forward.tls.enabled|bool and logging.forward.tls.ca_cert %} + tls: + certificate_authorities: + - "/usr/local/share/ca-certificates/logging-forward-ca_cert.crt" +{% endif %} + +filebeat: + config_dir: "{{ logging.forward.filebeat.config_dir }}" +{% if logging.follow.logs | length > 0 %} + prospectors: + {{ logging.follow.logs | to_yaml( indent=2, default_flow_style=False ) | indent ( width=4 ) }} +{% endif %} diff --git a/roles/logging/templates/etc/init/logstash-forwarder.conf b/roles/logging/templates/etc/init/logstash-forwarder.conf new file mode 100644 index 0000000..94249dc --- /dev/null +++ b/roles/logging/templates/etc/init/logstash-forwarder.conf @@ -0,0 +1,21 @@ +# {{ ansible_managed }} + +description "Logstash Forwarding Agent" +author "Myles Steinhauser" + +setuid logstash +setgid adm + +respawn +respawn limit 3 30 + +start on runlevel [2345] +stop on runlevel [!2345] + +chdir /var/lib/logstash + +{% if logging.forward.logstash_forwarder.version == '0.3.1' %} +exec /opt/logstash-forwarder/bin/logstash-forwarder.sh -config /etc/logstash-forwarder.d +{% else %} +exec /opt/logstash-forwarder/bin/logstash-forwarder -config /etc/logstash-forwarder.d +{% endif %} diff --git a/roles/logging/templates/etc/logstash-forwarder.d/main.conf b/roles/logging/templates/etc/logstash-forwarder.d/main.conf new file mode 100644 index 0000000..518e9de --- /dev/null +++ b/roles/logging/templates/etc/logstash-forwarder.d/main.conf @@ -0,0 +1,31 @@ +# {{ ansible_managed }} + +{ + "network": { + "servers": [ + "{{ logging.forward.host }}:{{ logging.forward.logstash_forwarder.port }}" + ] + + {% if logging.forward.tls.ca_cert %} + ,"ssl ca": "/usr/local/share/ca-certificates/logging-forward-ca_cert.crt" + {% endif -%} + }, + "files": [ + {% for item in logging.follow.logs %} + { + "paths": + {{ item.paths | to_nice_json }} + , + "fields": + {% if item.fields and logging.follow.global_fields %} + {# Merge the two field dicts together #} + {% set _dummy = item.fields.update(logging.follow.global_fields) %} + {{ item.fields | to_nice_json }} + {% elif logging.follow.global_fields %} + {{ logging.follow.global_fields | to_nice_json }} + {% endif %} + }{% if not loop.last %},{% endif %} + + {% endfor -%} + ] +} diff --git a/roles/logging/templates/etc/rsyslog.d/50-default.conf b/roles/logging/templates/etc/rsyslog.d/50-default.conf new file mode 100644 index 0000000..89126ef --- /dev/null +++ b/roles/logging/templates/etc/rsyslog.d/50-default.conf @@ -0,0 +1,44 @@ +# {{ ansible_managed }} + +# +# First some standard log files. Log by facility. +# +auth,authpriv.* /var/log/auth.log +*.*;auth,authpriv.none,local3.none -/var/log/syslog +#cron.* /var/log/cron.log +#daemon.* -/var/log/daemon.log +kern.* -/var/log/kern.log +#lpr.* -/var/log/lpr.log +mail.* -/var/log/mail.log +#user.* -/var/log/user.log + +# +# Logging for the mail system. Split it up so that +# it is easy to write scripts to parse these files. +# +#mail.info -/var/log/mail.info +#mail.warn -/var/log/mail.warn +mail.err /var/log/mail.err + +# +# Logging for INN news system. +# +news.crit /var/log/news/news.crit +news.err /var/log/news/news.err +news.notice -/var/log/news/news.notice + +# +# Some "catch-all" log files. +# +#*.=debug;\ +# auth,authpriv.none;\ +# news.none;mail.none -/var/log/debug +#*.=info;*.=notice;*.=warn;\ +# auth,authpriv.none;\ +# cron,daemon.none;\ +# mail,news.none -/var/log/messages + +# +# Emergencies are sent to everybody logged in. +# +*.emerg :omusrmsg:* diff --git a/roles/logging/templates/serverspec/logging_spec.rb b/roles/logging/templates/serverspec/logging_spec.rb new file mode 100644 index 0000000..9321820 --- /dev/null +++ b/roles/logging/templates/serverspec/logging_spec.rb @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('filebeat') do + it { should be_installed } +end + +describe service('filebeat') do + it { should be_enabled } +end diff --git a/roles/logstash/README.md b/roles/logstash/README.md new file mode 100644 index 0000000..dba4c40 --- /dev/null +++ b/roles/logstash/README.md @@ -0,0 +1,13 @@ +# Logstash + +## Variables + +To configure `ufw` rules for your logstash configs: +```yaml +logstash: + firewall: + - port: 1514 + protocol: tcp + src: + - 127.0.0.1/8 +``` diff --git a/roles/logstash/defaults/main.yml b/roles/logstash/defaults/main.yml new file mode 100644 index 0000000..39136b2 --- /dev/null +++ b/roles/logstash/defaults/main.yml @@ -0,0 +1,122 @@ +--- +logstash: + version: '1:5.4.1-1' + + service: + opts: "-w {{ (ansible_processor_cores / 2)|round|int }}" + heap: 500m + group: logstash + open_files: 16384 + + firewall: + - port: 1514 + protocol: tcp + src: + - 127.0.0.1 + - ::1 + - port: 1515 + protocol: tcp + src: + - 127.0.0.1 + - ::1 + inputs: + - name: syslog + config: + port: 1514 + type: syslog + - name: syslog + config: + port: 1515 + type: syslog + - name: lumberjack + config: + port: 4560 + ssl_certificate: '/etc/ssl/certs/logstash.crt' + ssl_key: '/etc/ssl/private/logstash.key' + type: 'lumberjack' + - name: beats + config: + port: 4561 + ssl: false + ssl_certificate: '/etc/ssl/certs/logstash.crt' + ssl_key: '/etc/ssl/private/logstash.key' + type: 'beats' + filters: + - include: filter-drop-empty + - include: filter-tags + - include: filter-json + - include: filter-add-missing-customer_id + - include: filter-syslog + - include: filter-openstack + patterns: + - openstack + outputs: + - name: elasticsearch + config: + hosts: "127.0.0.1:9200" + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/logstash/logstash.err + - /var/log/logstash/logstash.stdout + fields: + tags: elk,logstash + logging: + forwarder: filebeat + logrotate: + frequency: 'daily' + rotations: 7 + ssl: + name: logstash + intermediate: ~ + cert: | + -----BEGIN CERTIFICATE----- + MIIDbjCCAlagAwIBAgIJAM0BGcYd3vMHMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV + BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX + aWRnaXRzIFB0eSBMdGQwHhcNMTUwMTE0MTk0NzMxWhcNMTYwMTE0MTk0NzMxWjBF + MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 + ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEAxRMHhTXHqEnH9mPUtreyJARa7820HOBf9Fe5zZNnIYXIhx7GpaxZp1pV + vcg2fB9iSUw81Oz+/Y91cyUJID1TF0RNHap/n7bVEu0kMHBbozScQcsE4/zljgX5 + gqSECXpCZ9KQAX3WNBbFGHV/QrJTZA1Teb/Ne77vwTcOVLa++1BRyKc88kLeWS11 + n5IMqWAytLS+1TtvcI+9iMKTb4udKo8UE0ojOMz/xpHbeVBumWBB0Uh8tlv2Bv2r + Qeflib9Djdbj5mj5BPyO2cSUy9blg51vK3wg7XImVpqqTaa97w938I9XICNhvxxm + iS5u4juu5Xuy0Fy0WBWgnaKC7zAwQQIDAQABo2EwXzAPBgNVHREECDAGhwSsEAAN + MB0GA1UdDgQWBBRo6C9RWMTxDhYgBFR701yvGlnw7TAfBgNVHSMEGDAWgBRo6C9R + WMTxDhYgBFR701yvGlnw7TAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IB + AQAYiC7dnmG/HloCPvmnqJ8nuGvHjExF1yaSQbbeCdpS3RcfRi4FvhvQsiQoO4aL + EYVzTEfamy9xD6GDREcQ0ex6b91b2aS71b46S/snzoEwqyc51Fikf93qUGW8Y5hT + /TTsu35mRUkCsOW9JBTE8hHuxaqC4sXvEgVTSQxrfLu9vqQ34c+PWh6sS0nA7aYA + 8GS4HLwpwQNfhO8feQgyLljFrYMSe0iHiLWg2y8FB2ZjWgC/ZX9lY5YiBIe5K9A2 + ToXF6Rn3zaU1rZEQ0vZOK+MdAXNoG1L9jqSRgrSoD92VXKsU0WHorFCN5NLknRlh + S+3FRsZsB9xRpd7jnrShLnVh + -----END CERTIFICATE----- + key: | + -----BEGIN PRIVATE KEY----- + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDFEweFNceoScf2 + Y9S2t7IkBFrvzbQc4F/0V7nNk2chhciHHsalrFmnWlW9yDZ8H2JJTDzU7P79j3Vz + JQkgPVMXRE0dqn+fttUS7SQwcFujNJxBywTj/OWOBfmCpIQJekJn0pABfdY0FsUY + dX9CslNkDVN5v817vu/BNw5Utr77UFHIpzzyQt5ZLXWfkgypYDK0tL7VO29wj72I + wpNvi50qjxQTSiM4zP/Gkdt5UG6ZYEHRSHy2W/YG/atB5+WJv0ON1uPmaPkE/I7Z + xJTL1uWDnW8rfCDtciZWmqpNpr3vD3fwj1cgI2G/HGaJLm7iO67le7LQXLRYFaCd + ooLvMDBBAgMBAAECggEACU9U054uyGxz3dfpfJhA+iKaiSmWkKao4cojJOsJvDOt + vXRevgTeV2GVG1oR+NsisYPAe+6WPTRBwCbHv5YmDrtTSUt8q/bfKf257+/e5F7f + 4tvMZ1jTH1p45wsOkcQMzwrLcsaXD+7jcLxcPrJd7iQTBXViJ65ldSM8shPQAd88 + GFr2Bb0GS3visBWks8Tl+j2f+n2sak5HaDgQGFblSeKOZ4oDi9ikZ1qY2nfVctGy + A28eFtdHmLXzvVLTWdVSBohabOaRy9wDAlEvofnWmYpaeVw9CyZhe/aq4DRtClx2 + 25kSCfK/f72v2hcOSRUojjfuT0sURNyljqppRR4AAQKBgQD/URcXTv2F6RoZ/GKa + PNZQNmcQGjdC639YzdMJfolJ777AIKLHrGMopBZxOUTL2sJ7wi7RoZ5gDMFwmdPX + YX4O300mbvAyTbdn/3xnTuywXkdTkCPkf1vKwZWeUA/nFPSHLCyd3O7K6HUhRVxH + NnR6uAVi1p5LddHrrkaxpX7EwQKBgQDFmgn/Noh8Hd6W2BPuAeesUli+mNABhioM + 4AYA+lqAJaidM3IuvldMOGToLXd3hx8HH4tR8o9o/xGGdUL3R/N9yh/SM0FRbgZn + J/3tuP1X7Qe4Ey6xkN0V7k87DNyGP9oDSLD51vw9/eodl+vsH+rKDv406xNyz6qz + Vo4YxMrLgQKBgQCDKaF3M/lCRhJGr0XofsCKzKf9uboSAvGVKYf1JLBa73NLOHjn + o0P9qO5ulEEniObItWVgBGtcZLErq5sM1uTvtv/ncq6q6QoDv1ilqgImSQjTgQUv + ac46R2EZx3+j8zv8BVGWd92lF+60fPF/FBaaxNbfg+omUgzZyto+gQqzAQKBgFX+ + gIVBzUn+kcUhyiKVT6Ztu9NOm49ePOSXheVdDo+gU+392p4/FazFCh0E1G3/LuCh + uLb0EbdG8fCLDZaiCHRgx1JqHe37LOwtulN/YzmlnOtd5b8+5QhLSs3O/hWqqg0t + 0F8aUXIFE6LHX9PF3B8NQVH0T+VyPL6JV5Ot6PeBAoGAbG9Jmvo0DrrlqQnVk1qm + AKlzUDnHBHlNe+OR73Zm4YIMGC3Z03/xVNMNyakGutVGHSo1rIWbTeoXuIOzMrON + WtcIVT6XcZfoqjRi3Fh8d20wUuQnzFwPv8cZhlC8RAodII6dtyQHylAAyZPtXFYa + Cw8r6ILGkIHtU/WYRFIChZ4= + -----END PRIVATE KEY----- diff --git a/roles/logstash/handlers/main.yml b/roles/logstash/handlers/main.yml new file mode 100644 index 0000000..56f376c --- /dev/null +++ b/roles/logstash/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart logstash + service: name=logstash state=restarted diff --git a/roles/logstash/meta/main.yml b/roles/logstash/meta/main.yml new file mode 100644 index 0000000..3f58df3 --- /dev/null +++ b/roles/logstash/meta/main.yml @@ -0,0 +1,20 @@ +--- +dependencies: + - role: bbg-ssl + name: "{{ logstash.ssl.name }}" + ssl_cert: "{{ logstash.ssl.cert }}" + ssl_key: "{{ logstash.ssl.key }}" + ssl_intermediate: "{{ logstash.ssl.intermediate }}" + ssl_ca_cert: ~ + tags: bbg-ssl + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.logstash.repo }} stable main' + key_url: '{{ apt_repos.logstash.key_url }}' + - role: runtime/java + - role: logging-config + service: logstash + logdata: "{{ logstash.logs }}" + forward_type: "{{ logstash.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/logstash/tasks/checks.yml b/roles/logstash/tasks/checks.yml new file mode 100644 index 0000000..3970c87 --- /dev/null +++ b/roles/logstash/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: LST031 install logstash process check + sensu_check_dict: name="check-logstash-process" check="{{ sensu_checks.logstash.check_logstash_process }}" + notify: restart sensu-client missing ok diff --git a/roles/logstash/tasks/main.yml b/roles/logstash/tasks/main.yml new file mode 100644 index 0000000..afec794 --- /dev/null +++ b/roles/logstash/tasks/main.yml @@ -0,0 +1,73 @@ +--- +- name: install logstash + apt: + pkg: "logstash={{ logstash.version }}" + register: result + until: result|succeeded + retries: 5 + +- name: logstash log directory + file: dest=/var/log/logstash + owner=logstash group=logstash mode=0750 + state=directory + +- name: remove logstash directory acl + command: setfacl -b /var/log/logstash + +- name: set up log rotation for logstash + logrotate: name=logstash path=/var/log/logstash/*.log + args: + options: + - "{{ logstash.logrotate.frequency }}" + - "rotate {{ logstash.logrotate.rotations }}" + - missingok + - compress + - copytruncate + - delaycompress + - notifempty + +- name: configure logstash + template: src={{ item }} dest=/{{ item }} + with_items: + - etc/default/logstash + notify: restart logstash + +- name: configure logstash pipeline + template: src=etc/logstash/conf.d/pipeline.conf dest=/etc/logstash/conf.d/pipeline.conf + notify: restart logstash + +- name: create logstash patterns directory + file: dest=/etc/logstash/patterns state=directory mode=0755 + +- name: configure logstash patterns + template: src=etc/logstash/patterns/{{ item }} dest=/etc/logstash/patterns/{{ item }} + with_items: "{{ logstash.patterns }}" + notify: restart logstash + +- name: add logstash user to needed groups + user: name=logstash groups=ssl-key,ssl-cert append=true + +- name: allow logstash traffic + ufw: rule=allow to_port={{ item.0.port }} proto={{ item.0.protocol }} src={{ item.1|default('127.0.0.1') }} + with_subelements: + - "{{ logstash.firewall }}" + - src + tags: + - firewall + +- meta: flush_handlers + +- name: enable and start logstash service + service: name=logstash state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/logstash/tasks/metrics.yml b/roles/logstash/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/logstash/tasks/serverspec.yml b/roles/logstash/tasks/serverspec.yml new file mode 100644 index 0000000..d3238ed --- /dev/null +++ b/roles/logstash/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests logstash tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/logstash/templates/etc/default/logstash b/roles/logstash/templates/etc/default/logstash new file mode 100644 index 0000000..c70eb89 --- /dev/null +++ b/roles/logstash/templates/etc/default/logstash @@ -0,0 +1,37 @@ +# {{ ansible_managed }} + +############################### +# Default settings for logstash +############################### + +# Override Java location +#JAVACMD=/usr/bin/java + +# Set a home directory +#LS_HOME=/var/lib/logstash + +# Arguments to pass to logstash agent +LS_OPTS="{{ logstash.service.opts }}" + +# Arguments to pass to java +LS_HEAP_SIZE="{{ logstash.service.heap }}" +#LS_JAVA_OPTS="-Djava.io.tmpdir=$HOME" + +# pidfiles aren't used for upstart; this is for sysv users. +#LS_PIDFILE=/var/run/logstash.pid + +# user id to be invoked as; for upstart: edit /etc/init/logstash.conf +LS_GROUP={{ logstash.service.group }} + +# logstash logging +#LS_LOG_FILE=/var/log/logstash/logstash.log +#LS_USE_GC_LOGGING="true" + +# logstash configuration directory +#LS_CONF_DIR=/etc/logstash/conf.d + +# Open file limit; cannot be overridden in upstart +LS_OPEN_FILES={{ logstash.service.open_files }} + +# Nice level +#LS_NICE=19 diff --git a/roles/logstash/templates/etc/logstash/conf.d/filter-add-missing-customer_id.conf b/roles/logstash/templates/etc/logstash/conf.d/filter-add-missing-customer_id.conf new file mode 100644 index 0000000..a221098 --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/filter-add-missing-customer_id.conf @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +if ! [customer_id] { + mutate { + add_field => { "customer_id" => "unknown" } + } + } diff --git a/roles/logstash/templates/etc/logstash/conf.d/filter-drop-empty.conf b/roles/logstash/templates/etc/logstash/conf.d/filter-drop-empty.conf new file mode 100644 index 0000000..853408a --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/filter-drop-empty.conf @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +if [message] == "" { + drop { } +} diff --git a/roles/logstash/templates/etc/logstash/conf.d/filter-json.conf b/roles/logstash/templates/etc/logstash/conf.d/filter-json.conf new file mode 100644 index 0000000..0ab5c1f --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/filter-json.conf @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +if [type] == "json" { + json{ + source => "message" + } +} diff --git a/roles/logstash/templates/etc/logstash/conf.d/filter-offset-integer.conf b/roles/logstash/templates/etc/logstash/conf.d/filter-offset-integer.conf new file mode 100644 index 0000000..3c893cc --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/filter-offset-integer.conf @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +mutate { + convert => ["offset", "integer"] +} diff --git a/roles/logstash/templates/etc/logstash/conf.d/filter-openstack.conf b/roles/logstash/templates/etc/logstash/conf.d/filter-openstack.conf new file mode 100644 index 0000000..9eb4852 --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/filter-openstack.conf @@ -0,0 +1,86 @@ +# {{ ansible_managed }} + +if [type] == "openstack" { + grok { + # Do multiline matching as the above mutliline filter may add newlines + # to the log messages. + # TODO move the LOGLEVELs into a proper grok pattern. + patterns_dir => "/etc/logstash/patterns" + + match => { "message" => "^%{OPENSTACK_DATE:timestamp}%{SPACE}%{NUMBER:pid}?%{SPACE}?(?AUDIT|CRITICAL|DEBUG|INFO|TRACE|WARNING|ERROR)%{SPACE}%{NOTSPACE:module}%{SPACE}\[%{NOTSPACE:context}\]%{SPACE}?%{GREEDYDATA:message}?" } + overwrite => ["message"] + add_field => { "received_at" => "%{@timestamp}" } + } + + date { + match => [ "timestamp", "YYY-MM-dd HH:mm:ss.SSSSSS" ] + remove_field => "timestamp" + timezone => "UTC" + } + + if "keystone-api" in [tags] { + mutate { + gsub => ['message',"\"",""] + } + grok { + match => { "message" => "%{NOTSPACE:method} %{NOTSPACE:url_path} %{NOTSPACE:http_ver} %{NUMBER:response} %{NUMBER:bytes} %{NUMBER:seconds}" } + add_tag => ["apimetrics"] + } + } else if "nova-api" in [tags] { + if [module] == "nova.osapi_compute.wsgi.server" { + mutate { + gsub => ['message',"\"",""] + } + grok { + match => { "message" => "%{NOTSPACE:requesterip} %{NOTSPACE:method} %{NOTSPACE:url_path} %{NOTSPACE:http_ver} status\: %{NUMBER:response} len\: %{NUMBER:bytes} time\: %{NUMBER:seconds}" } + add_tag => ["apimetrics"] + } + } + } else if "neutron-api" in [tags] { + if [module] == "neutron.wsgi" { + if "accepted" not in [message] { + mutate { + gsub => ['message',"\"",""] + } + grok { + match => { "message" => "%{NOTSPACE:method} %{NOTSPACE:url_path} %{NOTSPACE:http_ver} %{NUMBER:response} %{NUMBER:bytes} %{NUMBER:seconds}" } + add_tag => ["apimetrics"] + } + } + } + } else if "glance-api" in [tags] { + if [module] == "glance.wsgi.server" { + mutate { + gsub => ['message',"\"",""] + } + grok { + match => { "message" => "%{NOTSPACE:requesterip} \- \- \[%{NOTSPACE:req_date} %{NOTSPACE:req_time}\] %{NOTSPACE:method} %{NOTSPACE:url_path} %{NOTSPACE:http_ver} %{NUMBER:response} %{NUMBER:bytes} %{NUMBER:seconds}" } + add_tag => ["apimetrics"] + } + } + } else if "novameta-api" in [tags] { + mutate { + gsub => ['message',"\"",""] + } + if [module] == "nova.api.ec2" { + grok { + match => { "message" => "%{NUMBER:seconds}s %{NOTSPACE:requesterip} %{NOTSPACE:method} %{NOTSPACE:url_path} None\:None %{NUMBER:response} %{GREEDYDATA:user_agent}" } + add_field => ["api", "metadata-ec2"] + add_tag => ["apimetrics"] + } + } else if [module] == "nova.metadata.wsgi.server" { + grok { + match => { "message" => "%{NOTSPACE:requesterip} %{NOTSPACE:method} %{NOTSPACE:url_path} %{NOTSPACE:http_ver} status\: %{NUMBER:response} len\: %{NUMBER:bytes} time\: %{NUMBER:seconds}" } + add_tag => ["apimetrics"] + } + } + } + } else if "libvirt" in [tags] { + grok { + match => { "message" => "(?m)^%{TIMESTAMP_ISO8601:logdate}:%{SPACE}%{NUMBER:code}:?%{SPACE}\[?\b%{NOTSPACE:loglevel}\b\]?%{SPACE}?:?%{SPACE}\[?\b%{NOTSPACE:module}\b\]?%{SPACE}?%{GREEDYDATA:message}?" } + add_field => { "received_at" => "%{@timestamp}"} + } + mutate { + uppercase => [ "loglevel" ] + } + } diff --git a/roles/logstash/templates/etc/logstash/conf.d/filter-syslog.conf b/roles/logstash/templates/etc/logstash/conf.d/filter-syslog.conf new file mode 100644 index 0000000..6b07c89 --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/filter-syslog.conf @@ -0,0 +1,27 @@ +# {{ ansible_managed }} + +if [type] == "syslog" or "syslog" in [tags] { + grok { + match => [ + "message", "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{TIMESTAMP_ISO8601:log_timestamp}\.[0-9]+ %{POSINT:syslog_pid} %{GREEDYDATA:syslog_message}", + "message", "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{DAY} %{MONTH} %{MONTHDAY} %{TIME} %{YEAR} %{GREEDYDATA:syslog_message}", + "message", "%{SYSLOGTIMESTAMP:syslog_timestamp} %{SYSLOGHOST:syslog_hostname} %{DATA:syslog_program}(?:\[%{POSINT:syslog_pid}\])?: %{GREEDYDATA:syslog_message}" + ] + add_field => [ "received_at", "%{@timestamp}" ] + } + + syslog_pri { + severity_labels => ["ERROR", "ERROR", "ERROR", "ERROR", "WARNING", "INFO", "INFO", "DEBUG" ] + } + + date { + match => [ "syslog_timestamp", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ] + } + + if !("_grokparsefailure" in [tags]) { + mutate { + replace => [ "message", "%{syslog_message}" ] + replace => [ "host", "%{syslog_hostname}" ] + } + } +} diff --git a/roles/logstash/templates/etc/logstash/conf.d/filter-tags.conf b/roles/logstash/templates/etc/logstash/conf.d/filter-tags.conf new file mode 100644 index 0000000..0a609ab --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/filter-tags.conf @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +# logstash-forwarder does not support tags array, the tags then have +# to be shipped as a csv string; +# before any other thing happens, filter application etc., the tags +# array must be constructed from the csv string that comes in. +mutate { + split => ["tags", ","] +} diff --git a/roles/logstash/templates/etc/logstash/conf.d/pipeline.conf b/roles/logstash/templates/etc/logstash/conf.d/pipeline.conf new file mode 100644 index 0000000..6fabf68 --- /dev/null +++ b/roles/logstash/templates/etc/logstash/conf.d/pipeline.conf @@ -0,0 +1,51 @@ +# {{ ansible_managed }} + +{% macro config_loop(list) %} + {% for item in list %} + {% if item.test is defined %} + {{- config_test(item) }} + {% elif item.include is defined %} + {% include item.include +'.conf' %} + + {% elif item.config is defined %} + {{- config_plugin(item) }} + {% endif %} + + {% endfor %} +{% endmacro %} + +{% macro config_plugin(item) %} + {{- item.name }} { + {% for key, value in item.config | dictsort %} + {% if value|lower == "true" or value|lower == "false" %} + '{{ key }}' => {{ value|lower }} + {% elif value is string %} + '{{ key }}' => '{{ value }}' + {% else %} + '{{ key }}' => {{ value }} + {% endif %} + {% endfor %} + } +{% endmacro %} + +{% macro config_test(item) %} + {{- item.test }} { + {% if item.include is defined %} + {% include item.include +'.conf' %} + {% elif item.config is defined %} + {{ config_plugin(item) }} + {% endif %} + } +{% endmacro %} + +input { + {{ config_loop(logstash.inputs) }} +} + +filter { + {{ config_loop(logstash.filters) }} +} + +output { + {{ config_loop(logstash.outputs) }} +} diff --git a/roles/logstash/templates/etc/logstash/patterns/openstack b/roles/logstash/templates/etc/logstash/patterns/openstack new file mode 100644 index 0000000..5e32002 --- /dev/null +++ b/roles/logstash/templates/etc/logstash/patterns/openstack @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +OPENSTACK_DATE %{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{HOUR}:?%{MINUTE}(?::?%{SECOND}) diff --git a/roles/logstash/templates/serverspec/logstash_spec.rb b/roles/logstash/templates/serverspec/logstash_spec.rb new file mode 100644 index 0000000..b4291c1 --- /dev/null +++ b/roles/logstash/templates/serverspec/logstash_spec.rb @@ -0,0 +1,90 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe user('logstash') do + it { should exist } #LST001 + it { should belong_to_group 'logstash' } #LST002 + it { should have_home_directory '/var/lib/logstash' } #LST003 + it { should have_login_shell '/sbin/nologin' } #LST004 +end + +describe file('/opt/logstash/') do + it { should be_mode 775 } #LST005 + it { should be_owned_by 'logstash' } #LST006 + it { should be_grouped_into 'logstash' } #LST007 + it { should be_directory } #LST008 +end + +describe file('/var/log/logstash') do + it { should be_mode 750 } #LST009 + it { should be_owned_by 'logstash' } #LST010 + it { should be_grouped_into 'logstash' } #LST011 + it { should be_directory } #LST012 +end + +describe file('/var/log/logstash/logstash.log') do + it { should be_mode 644 } #LST013 + it { should be_owned_by 'logstash' } #LST014 + it { should be_grouped_into 'logstash' } #LST015 + it { should be_file } #LST016 +end + +describe file('/var/log/logstash/logstash.err') do + it { should be_mode 644 } #LST017 + it { should be_file } #LST018 +end + +describe file('/var/log/logstash/logstash.stdout') do + it { should be_mode 644} #LST019 + it { should be_file } #LST020 +end + +describe file('/etc/default/logstash') do + it { should be_file } #LST021 +end + +describe file('/etc/logstash/conf.d/pipeline.conf') do + it { should be_file } #LST022 +end + +describe file('/etc/logstash/patterns') do + it { should be_mode 755 } #LST023 + it { should be_directory } #LST024 +end + +{% for pattern in logstash.patterns %} +describe file('/etc/logstash/patterns/{{ pattern }}' ) do + it { should be_exist } #LST025 +end +{% endfor %} + +describe file('/etc/ssl/private/logstash.key') do + it { should be_mode 640 } #LST026 + it { should be_owned_by 'root' } #LST027 + it { should be_grouped_into 'ssl-key' } #LST028 + it { should be_file } #LST029 +end + +describe file('/etc/ssl/certs/logstash.crt') do + it { should be_file } #LST030 +end + +describe package('logstash') do + it { should be_installed } +end + +describe service('logstash') do + it { should be_enabled } +end + +describe file('/etc/logrotate.d/logstash') do + it { should be_mode 644 } #LST032 + it { should be_file } #LST033 +end + +{% for rule in logstash.firewall %} +describe port("{{ rule.port }}") do + it { should be_listening } +end +{% endfor %} diff --git a/roles/manage-disks/defaults/main.yml b/roles/manage-disks/defaults/main.yml new file mode 100644 index 0000000..171056a --- /dev/null +++ b/roles/manage-disks/defaults/main.yml @@ -0,0 +1,27 @@ +--- +manage_disks: + enabled: False + defaults: + pesize: 4 + loopback: + enabled: False + file: /tmp/loopback + device: /dev/loop2 + size: 1G + volume_groups: [] +# EXAMPLE: +# - name: test +# pvs: +# - /dev/loop2 + logical_volumes: [] +# EXAMPLE: +# - name: test +# volume_group: test +# size: 500m +# filesystem: ext4 +# filesystem_opts: +# mount_point: /mnt/test +# mount_opts: + luks_volumes: [] +# Same format as above, plus +# - passphrase: whatever diff --git a/roles/manage-disks/handlers/main.yml b/roles/manage-disks/handlers/main.yml new file mode 100644 index 0000000..343e0e3 --- /dev/null +++ b/roles/manage-disks/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: update-initramfs + command: update-initramfs -u -k all + +- name: start mount-loopback + service: + name: mount-loopback + state: started + enabled: yes diff --git a/roles/manage-disks/meta/main.yml b/roles/manage-disks/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/manage-disks/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/manage-disks/tasks/checks.yml b/roles/manage-disks/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/manage-disks/tasks/main.yml b/roles/manage-disks/tasks/main.yml new file mode 100644 index 0000000..abb1785 --- /dev/null +++ b/roles/manage-disks/tasks/main.yml @@ -0,0 +1,159 @@ +--- +- block: + - name: create loopback file + command: dd if=/dev/zero of={{ manage_disks.loopback.file }} bs=1 count=0 seek={{ manage_disks.loopback.size }} + args: + creates: "{{ manage_disks.loopback.file }}" + + - name: mount loopback devices on boot + template: + src: mount-loopback.conf + dest: /etc/init/mount-loopback.conf + notify: start mount-loopback + + - meta: flush_handlers + + when: manage_disks.loopback.enabled|bool + +- block: + - name: install cryptsetup if needed + apt: + pkg: cryptsetup + + - name: load xts module during boot + lineinfile: + dest: /etc/initramfs-tools/modules + line: 'xts' + insertafter: EOF + notify: update-initramfs + when: manage_disks.luks_volumes|length > 0 + +- name: create volume groups + lvg: + vg: "{{ item.name }}" + pvs: "{{ item.pvs|join(',') }}" + pesize: "{{ item.pesize|default(manage_disks.defaults.pesize) }}" + with_items: "{{ manage_disks.volume_groups }}" + +- name: create logical volumes + lvol: + vg: "{{ item.volume_group }}" + lv: "{{ item.name }}" + size: "{{ item.size }}" + with_flattened: + - "{{ manage_disks.logical_volumes }}" + - "{{ manage_disks.luks_volumes }}" + +- name: determine if luks device already setup + command: "cryptsetup isLuks /dev/{{ item.volume_group }}/{{ item.name }}" + register: isluks + failed_when: false + with_items: "{{ manage_disks.luks_volumes }}" + +- name: fail immediately if /dev/shm is not tmpfs + command: df -t tmpfs /dev/shm + +- name: place cryptsetup keys if necessary + template: + src: passphrase + dest: "/dev/shm/{{ item.item.volume_group }}_{{ item.item.name }}.key" + owner: root + group: root + mode: 0700 + with_items: + - "{{ isluks.results }}" + when: not isluks | skipped and item.rc != 0 + +- name: run cryptsetup luksFormat + command: "cryptsetup luksFormat --key-file \"/dev/shm/{{ item.item.volume_group }}_{{ item.item.name }}.key\" --use-urandom --hash sha256 /dev/{{ item.item.volume_group }}/{{ item.item.name }}" + with_items: + - "{{ isluks.results }}" + when: not isluks | skipped and item.rc != 0 + +- name: open luks filesystems if necessary + shell: "cryptsetup status luks-{{ item.volume_group }}-{{ item.name }} || cryptsetup luksOpen --key-file \"/dev/shm/{{ item.volume_group }}_{{ item.name }}.key\" /dev/{{ item.volume_group }}/{{ item.name }} luks-{{ item.volume_group }}-{{ item.name }}" + with_items: "{{ manage_disks.luks_volumes }}" + +- name: back up luks header + shell: "umask 077; cryptsetup luksHeaderBackup \"/dev/{{ item.item.volume_group }}/{{ item.item.name }}\" --header-backup-file \"/root/{{ ansible_fqdn }}_{{ item.item.volume_group }}_{{ item.item.name }}.luksHeaderBackup\"" + with_items: "{{ isluks.results }}" + when: not isluks | skipped and item.rc != 0 + +- name: create filesystems + filesystem: + fstype: "{{ item.filesystem }}" + dev: "/dev/mapper/{{ item.volume_group }}-{{ item.name }}" + opts: "{{ item.filesystem_opts|default(omit) }}" + when: item.filesystem is defined + with_items: "{{ manage_disks.logical_volumes }}" + +- name: create luks filesystems + filesystem: + fstype: "{{ item.filesystem }}" + dev: "/dev/mapper/luks-{{ item.volume_group }}-{{ item.name }}" + opts: "{{ item.filesystem_opts|default(omit) }}" + when: item.filesystem is defined + with_items: "{{ manage_disks.luks_volumes }}" + +# crypttab needs an fs label not a /dev/mapper entry, so just use UUID +- name: get UUID for devices + command: "blkid -s UUID -o value /dev/{{ item.volume_group }}/{{ item.name }}" + register: blkid + with_items: "{{ manage_disks.luks_volumes }}" + +- name: add luks filesystems to /etc/crypttab + crypttab: + backing_device: "UUID={{ item.stdout }}" + name: "/dev/mapper/luks-{{ item.item.volume_group }}-{{ item.item.name }}" + opts: "{{ item.item.crypttab_opts|default('luks') }}" + password: "{{ item.item.password_file|default('-') }}" # default enter pass at boot + state: present + with_items: "{{ blkid.results }}" + # when: not blkid | skipped + +- name: create mount points + file: path={{ item.mount_point }} state=directory + when: item.mount_point is defined + with_flattened: + - "{{ manage_disks.logical_volumes }}" + - "{{ manage_disks.luks_volumes }}" + +- name: mount filesystems + mount: + name: "{{ item.mount_point }}" + src: "/dev/mapper/{{ item.volume_group }}-{{ item.name }}" + fstype: "{{ item.filesystem }}" + opts: "{{ item.mount_opts|default(omit) }}" + state: mounted + when: item.mount_point is defined and + item.filesystem is defined + with_items: "{{ manage_disks.logical_volumes }}" + +- name: mount filesystems + mount: + name: "{{ item.mount_point }}" + src: "/dev/mapper/luks-{{ item.volume_group }}-{{ item.name }}" + fstype: "{{ item.filesystem }}" + opts: "{{ item.mount_opts|default(omit) }}" + state: mounted + when: item.mount_point is defined and + item.filesystem is defined + with_items: "{{ manage_disks.luks_volumes }}" + +- name: remove cryptsetup keys + file: + path: "/dev/shm/{{ item.volume_group }}_{{ item.name }}.key" + state: absent + with_items: "{{ manage_disks.luks_volumes }}" + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/manage-disks/tasks/metrics.yml b/roles/manage-disks/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/manage-disks/tasks/serverspec.yml b/roles/manage-disks/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/manage-disks/templates/mount-loopback.conf b/roles/manage-disks/templates/mount-loopback.conf new file mode 100644 index 0000000..aaf1257 --- /dev/null +++ b/roles/manage-disks/templates/mount-loopback.conf @@ -0,0 +1,9 @@ +description "Setup loop devices after filesystems are mounted" + +start on mounted MOUNTPOINT=/ +task +script + losetup {{ manage_disks.loopback.device }} {{ manage_disks.loopback.file }} + + pvscan +end script diff --git a/roles/manage-disks/templates/passphrase b/roles/manage-disks/templates/passphrase new file mode 100644 index 0000000..9faf6a7 --- /dev/null +++ b/roles/manage-disks/templates/passphrase @@ -0,0 +1 @@ +{{ luks_passphrase }} \ No newline at end of file diff --git a/roles/netdata-dashboard/defaults/main.yml b/roles/netdata-dashboard/defaults/main.yml new file mode 100644 index 0000000..f23e3ff --- /dev/null +++ b/roles/netdata-dashboard/defaults/main.yml @@ -0,0 +1,35 @@ +--- +netdata_dashboard: + path: /var/www/html/netdata_dashboard + title: "Netdata Dashboard" + auth: [] + apache: + enabled: true + servername: netdata.local + serveraliases: + - netdata + port: 80 + ip: '*' + ssl: + enabled: False + port: 443 + ip: '*' + name: sitecontroller + cert: ~ + key: ~ + intermediate: ~ + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + logs: + - paths: + - /var/log/apache2/netdata_dashboard-access.log + fields: + tags: mirror,apache_access + - paths: + - /var/log/apache2/netdata_dashboard-error.log + fields: + tags: mirror,apache_error + logging: + forwarder: filebeat diff --git a/roles/netdata-dashboard/meta/main.yml b/roles/netdata-dashboard/meta/main.yml new file mode 100644 index 0000000..0877a58 --- /dev/null +++ b/roles/netdata-dashboard/meta/main.yml @@ -0,0 +1,16 @@ +--- +dependencies: + - role: bbg-ssl + name: "{{ netdata_dashboard.apache.ssl.name }}" + ssl_cert: "{{ netdata_dashboard.apache.ssl.cert }}" + ssl_key: "{{ netdata_dashboard.apache.ssl.key }}" + ssl_intermediate: "{{ netdata_dashboard.apache.ssl.intermediate }}" + when: netdata_dashboard.apache.ssl.enabled + tags: ['bbg-ssl'] + - role: apache + - role: logging-config + service: netdata_dashboard + logdata: "{{ netdata_dashboard.logs }}" + forward_type: "{{ netdata_dashboard.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/netdata-dashboard/tasks/apache.yml b/roles/netdata-dashboard/tasks/apache.yml new file mode 100644 index 0000000..4a48806 --- /dev/null +++ b/roles/netdata-dashboard/tasks/apache.yml @@ -0,0 +1,24 @@ +--- +- name: enable apache mods for file-mirror + apache2_module: name={{ item }} + with_items: + - proxy_http + - rewrite + - headers + +- name: add netdata_dashboard apache vhost + template: src=etc/apache2/sites-available/netdata_dashboard + dest=/etc/apache2/sites-available/netdata_dashboard.conf + notify: + - restart apache + tags: foo + +- name: enable netdata_dashboard vhost + apache2_site: state=enabled name=netdata_dashboard + notify: + - restart apache + +- meta: flush_handlers + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes diff --git a/roles/netdata-dashboard/tasks/checks.yml b/roles/netdata-dashboard/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/netdata-dashboard/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/netdata-dashboard/tasks/main.yml b/roles/netdata-dashboard/tasks/main.yml new file mode 100644 index 0000000..dc78cd6 --- /dev/null +++ b/roles/netdata-dashboard/tasks/main.yml @@ -0,0 +1,52 @@ +--- +- name: create .htpasswd + htpasswd: name={{ item.username }} password={{ item.password }} + path="{{ netdata_dashboard.mirror_location }}/{{ item.path }}/.htpasswd" + with_items: "{{ netdata_dashboard.auth }}" + +- name: create .htaccess + template: src=etc/apache2/htaccess + dest="{{ netdata_dashboard.mirror_location }}/{{ item.path }}/.htaccess" + with_items: "{{ netdata_dashboard.auth }}" + +- name: create netdata dashboard path + file: + dest: "{{ netdata_dashboard.path }}" + state: directory + recurse: true + +- name: install netdata dashboard + template: + src: var/www/html/index.html + dest: "{{ netdata_dashboard.path }}/index.html" + tags: foo + +- name: install apache healthcheck file + template: + src: var/www/html/health_check + dest: "{{ netdata_dashboard.path }}/health_check" + when: netdata_dashboard.health_check_enabled|default('False')|bool + +- include: apache.yml + when: netdata_dashboard.apache.enabled|bool + +- name: allow netdata dashboard traffic + ufw: rule=allow + to_port={{ item.port }} + src={{ item.src }} + proto={{ item.protocol }} + with_items: "{{ netdata_dashboard.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/netdata-dashboard/tasks/metrics.yml b/roles/netdata-dashboard/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/netdata-dashboard/tasks/serverspec.yml b/roles/netdata-dashboard/tasks/serverspec.yml new file mode 100644 index 0000000..d9184d3 --- /dev/null +++ b/roles/netdata-dashboard/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for file-mirror role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/netdata-dashboard/templates/etc/apache2/htaccess b/roles/netdata-dashboard/templates/etc/apache2/htaccess new file mode 100644 index 0000000..d30f48d --- /dev/null +++ b/roles/netdata-dashboard/templates/etc/apache2/htaccess @@ -0,0 +1,6 @@ +# {{ ansible_managed }} + +AuthType Basic +AuthName "Authentication Required" +AuthUserFile "{{ netdata_dashboard.mirror_location }}/{{ item.path }}/.htpasswd" +Require valid-user diff --git a/roles/netdata-dashboard/templates/etc/apache2/sites-available/netdata_dashboard b/roles/netdata-dashboard/templates/etc/apache2/sites-available/netdata_dashboard new file mode 100644 index 0000000..f099060 --- /dev/null +++ b/roles/netdata-dashboard/templates/etc/apache2/sites-available/netdata_dashboard @@ -0,0 +1,59 @@ +# {{ ansible_managed }} + +{% macro virtualhost() %} + ServerAdmin openstack@bluebox.net + ServerName {{ netdata_dashboard.apache.servername }} + ServerAlias {{ netdata_dashboard.apache.serveraliases|join(" ") }} + DocumentRoot {{ netdata_dashboard.path }} + ErrorLog ${APACHE_LOG_DIR}/netdata_dashboard-error.log + CustomLog ${APACHE_LOG_DIR}/netdata_dashboard-access.log combined + FileETag MTime Size + Header set Cache-Control public + RewriteEngine On + + Options Indexes + AllowOverride All + Require all granted + + + + ProxyPass http://127.0.0.1:19999/ + ProxyPassReverse http://127.0.0.1:19999/ + + {% for host in groups['all'] %} + {% if hostvars[host]['ansible_distribution'] == 'Ubuntu' and hostvars[host]['netdata']['enabled']|bool %} + + ProxyPass http://{{ hostvars[host][public_interface]['ipv4']['address'] }}:19999/ + ProxyPassReverse http://{{ hostvars[host][public_interface]['ipv4']['address'] }}:19999/ + + {% endif %} + {% endfor %} +{% endmacro %} + +{% if netdata_dashboard.apache.ssl.enabled|bool and netdata_dashboard.apache.http_redirect|bool %} + + ServerName {{ netdata_dashboard.apache.servername }} + ServerAlias {{ netdata_dashboard.apache.serveraliases|join(" ") }} + RewriteEngine on + RewriteCond %{HTTPS} !=on + RewriteRule ^(.*)$ https://%{HTTP_HOST}:{{ netdata_dashboard.apache.ssl.port }}$1 [R=301,L] + +{% else %} + +{{ virtualhost() }} + +{% endif %} + +{% if netdata_dashboard.apache.ssl.enabled|bool %} + + {{ apache.ssl.settings }} + SSLCertificateFile /etc/ssl/certs/{{ netdata_dashboard.apache.ssl.name|default('sitecontroller') }}.crt + SSLCertificateKeyFile /etc/ssl/private/{{ netdata_dashboard.apache.ssl.name|default('sitecontroller') }}.key +{% if bbg_ssl.intermediate or netdata_dashboard.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ netdata_dashboard.apache.ssl.name|default('sitecontroller') }}-intermediate.crt +{% endif %} +{% else %} + +{% endif %} +{{ virtualhost() }} + diff --git a/roles/netdata-dashboard/templates/serverspec/file_mirror_spec.rb b/roles/netdata-dashboard/templates/serverspec/file_mirror_spec.rb new file mode 100644 index 0000000..40a1509 --- /dev/null +++ b/roles/netdata-dashboard/templates/serverspec/file_mirror_spec.rb @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe iptables do + it { should have_rule('-p tcp -m tcp --dport {{ netdata_dashboard.apache.port }} -j ACCEPT') } +end diff --git a/roles/netdata-dashboard/templates/var/www/html/health_check b/roles/netdata-dashboard/templates/var/www/html/health_check new file mode 100644 index 0000000..d86bac9 --- /dev/null +++ b/roles/netdata-dashboard/templates/var/www/html/health_check @@ -0,0 +1 @@ +OK diff --git a/roles/netdata-dashboard/templates/var/www/html/index.html b/roles/netdata-dashboard/templates/var/www/html/index.html new file mode 100644 index 0000000..b33dd97 --- /dev/null +++ b/roles/netdata-dashboard/templates/var/www/html/index.html @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + diff --git a/roles/netdata/defaults/main.yml b/roles/netdata/defaults/main.yml new file mode 100644 index 0000000..06f04a4 --- /dev/null +++ b/roles/netdata/defaults/main.yml @@ -0,0 +1,6 @@ +--- +netdata: + package: https://github.com/IBM/cuttle/releases/download/packages/netdata_1.5.1_amd64.deb + enabled: false + firewall: + allow_from: [] diff --git a/roles/netdata/handlers/main.yml b/roles/netdata/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/netdata/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/netdata/meta/main.yml b/roles/netdata/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/netdata/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/netdata/tasks/checks.yml b/roles/netdata/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/netdata/tasks/main.yml b/roles/netdata/tasks/main.yml new file mode 100644 index 0000000..1ba9121 --- /dev/null +++ b/roles/netdata/tasks/main.yml @@ -0,0 +1,34 @@ +--- +- name: create netdata user + user: name=netdata comment=netdata shell=/bin/false + system=yes home=/nonexistent + +- name: install netdata + apt: + package: https://github.com/IBM/cuttle/releases/download/packages/netdata_1.5.1_amd64.deb + +- name: ensure netdata service is running + service: + name: netdata + enabled: true + state: started + +- name: allow access to netdata web + ufw: rule=allow to_port=19999 proto=tcp src={{ item }} + with_items: "{{ netdata.firewall.allow_from }}" + tags: + - firewall + +- meta: flush_handlers + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/netdata/tasks/metrics.yml b/roles/netdata/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/netdata/tasks/serverspec.yml b/roles/netdata/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/oauth2_proxy/README.md b/roles/oauth2_proxy/README.md new file mode 100644 index 0000000..c830192 --- /dev/null +++ b/roles/oauth2_proxy/README.md @@ -0,0 +1,53 @@ +# Building the oauth2-proxy package # + +## Fetching oauth2_proxy ## + +1. Install Go and set up a Go environment + + http://golang.org/doc/install + +2. Get the oauth2_proxy source and build the binary in your $GOPATH + + ``` + go get github.com/bitly/oauth2_proxy + ``` + +## Building the Debian package ## + +1. Install 'fpm' + + https://github.com/jordansissel/fpm/blob/master/README.md + +2. Build the oauth2-proxy package + + ``` + cd $GOPATH/bin + fpm -s dir -t deb -n oauth2_proxy -v $VERSION+$NUM~$COMMIT --prefix /opt/oauth2_proxy/bin oauth2_proxy + ``` + + At the time of writing, the latest released version of oauth2_proxy is + 2.0.1. To make sure we can upgrade this package between releases with our + own builds and that the next released version properly supercedes our last + package build, we set $VERSION to the latest released version and each time + we build the package we increment $NUM and set $COMMIT to the short hash of + the oauth2_proxy repo our packaged binary was built from. For example: + + ``` + fpm -s dir -t deb -n oauth2_proxy -v 2.0.1+1~a631197 --prefix /opt/oauth2_proxy/bin oauth2_proxy + ``` + +## Mirroring the Debian package ## + +1. Upload package to the `blueboxcloud / misc` repo at https://packagecloud.io + + https://boxpanel.bluebox.net/private/service_passwords + + If you do not have access to this location, please ask in \#bluebox-ci or + \#sitecontroller on Slack and someone should be able to upload the package + for you. + +2. Mirror package to the BBC apt mirror + + Once the package has been uploaded to the `blueboxcloud / misc` package + cloud repo, ask in \#sitecontroller on Slack for a mirror sync. Once the + package is synced to the BBC apt mirror, re-run the oauth2_proxy playbook. diff --git a/roles/oauth2_proxy/defaults/main.yml b/roles/oauth2_proxy/defaults/main.yml new file mode 100644 index 0000000..3829300 --- /dev/null +++ b/roles/oauth2_proxy/defaults/main.yml @@ -0,0 +1,76 @@ +--- +# setup settings for oauth2_proxy +oauth2_proxy: + package: https://github.com/IBM/cuttle/releases/download/packages/oauth2-proxy_2.0.1.2.a631197_amd64.deb + vhost_name: "oauth2_proxy" + docroot: "/var/www/html" + apache_status: true + http_redirect: true + locations: [] + apache: + enabled: true + ip: 0.0.0.0 + port: 80 + servername: "oauth2_proxy" + serveradmin: "admin@cuttle.net" + serveraliases: [] + ssl: + enabled: true + ip: 0.0.0.0 + port: 443 + key: ~ + cert: ~ + intermediate: ~ + auth_proxy: + enabled: true + log_level: "warn" + ip: 127.0.0.1 + port: 8080 + protected_locations: + enabled: true + log_level: "warn" + ip: 127.0.0.1 + port: 80 + url: "http://127.0.0.1" + firewall: + - port: 443 + protocol: tcp + src: + - any + - port: 80 + protocol: tcp + src: + - any + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - "/var/log/apache2/oauth2-auth-proxy-*-access.log" + - "/var/log/apache2/oauth2-protected-locations-*-access.log" + fields: + tags: oauth2_proxy,apache_access + - paths: + - "/var/log/apache2/oauth2-auth-proxy-*-error.log" + - "/var/log/apache2/oauth2-protected-locations-*-error.log" + fields: + tags: oauth2_proxy,apache_error + logging: + forwarder: filebeat + config: + # Implies port 80 + client_id: "add GitHub client ID here" + client_secret: "add GitHub client secret here" + email_domains: + - '"ibm.com"' + - '"us.ibm.com"' + - '"cn.ibm.com"' + - '"uk.ibm.com"' + pass_access_token: "true" + cookie_name: "oauth2_proxy_cookie" + cookie_secret: "oauth2_proxy_secret_pass" + cookie_domain: "" + cookie_expire: "168h" + cookie_refresh: "24h" + cookie_secure: "true" + cookie_httponly: "false" + provider: "github" + provider_url: "https://github.com" diff --git a/roles/oauth2_proxy/handlers/main.yml b/roles/oauth2_proxy/handlers/main.yml new file mode 100644 index 0000000..c6077cd --- /dev/null +++ b/roles/oauth2_proxy/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart oauth2_proxy + service: name=oauth2_proxy state=restarted diff --git a/roles/oauth2_proxy/meta/main.yml b/roles/oauth2_proxy/meta/main.yml new file mode 100644 index 0000000..d608e98 --- /dev/null +++ b/roles/oauth2_proxy/meta/main.yml @@ -0,0 +1,17 @@ +--- +dependencies: + - role: bbg-ssl + name: "oauth2_proxy" + ssl_cert: "{{ oauth2_proxy.apache.ssl.cert }}" + ssl_key: "{{ oauth2_proxy.apache.ssl.key }}" + ssl_intermediate: "{{ oauth2_proxy.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: oauth2_proxy.apache.ssl.enabled + tags: bbg-ssl + - role: apache + - role: logging-config + service: oauth2_proxy + logdata: "{{ oauth2_proxy.logs }}" + forward_type: "{{ oauth2_proxy.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/oauth2_proxy/tasks/apache.yml b/roles/oauth2_proxy/tasks/apache.yml new file mode 100644 index 0000000..69977dd --- /dev/null +++ b/roles/oauth2_proxy/tasks/apache.yml @@ -0,0 +1,48 @@ +--- +- name: enable apache mod proxy_http + apache2_module: name=proxy_http + notify: reload apache + +- name: enable apache mod status + apache2_module: name=status + when: oauth2_proxy.apache_status + notify: reload apache + +- name: install apache template + template: src=etc/apache2/sites-available/oauth2_proxy.conf + dest=/etc/apache2/sites-available/oauth2_proxy.conf + notify: restart apache + +- name: enable site + apache2_site: name=oauth2_proxy.conf state=enabled + notify: restart apache + when: oauth2_proxy.apache.enabled + +- name: allow oauth2_proxy traffic + ufw: rule=allow to_port={{ item.0.port }} src={{ item.1|default('127.0.0.1') }} + with_subelements: + - "{{ oauth2_proxy.firewall }}" + - src + tags: + - firewall + +- meta: flush_handlers + +- name: install apache location index + template: src=var/www/html/index.html + dest=/var/www/html/index.html + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/oauth2_proxy/tasks/checks.yml b/roles/oauth2_proxy/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/oauth2_proxy/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/oauth2_proxy/tasks/main.yml b/roles/oauth2_proxy/tasks/main.yml new file mode 100644 index 0000000..cf61954 --- /dev/null +++ b/roles/oauth2_proxy/tasks/main.yml @@ -0,0 +1,27 @@ +--- +- name: install oauth2_proxy package + apt: + deb: "{{ oauth2_proxy.package }}" + +- name: Create a folder to hold the oauth2_proxy configuration file. + file: path=/etc/oauth2_proxy state=directory mode=0755 + +- name: Place an oauth2_proxy configuration file to the specified folder + template: src=etc/oauth2_proxy/oauth2_proxy.cfg.j2 dest=/etc/oauth2_proxy/oauth2_proxy.cfg mode=0600 + +- name: Allow oauth2_proxy traffic. + ufw: rule=allow to_port={{ item.0.port }} proto={{ item.0.protocol }} src={{ item.1|default('127.0.0.1') }} + with_subelements: + - "{{ oauth2_proxy.firewall }}" + - src + tags: + - firewall + +- name: Install oauth2_proxy upstart service. + upstart_service: name=oauth2_proxy cmd=/opt/oauth2_proxy/bin/oauth2_proxy args="-config=/etc/oauth2_proxy/oauth2_proxy.cfg" user=root + notify: restart oauth2_proxy + +- name: Ensure the oauth2_proxy is running. + service: name=oauth2_proxy state=started enabled=yes + +- include: apache.yml diff --git a/roles/oauth2_proxy/tasks/metrics.yml b/roles/oauth2_proxy/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/oauth2_proxy/tasks/serverspec.yml b/roles/oauth2_proxy/tasks/serverspec.yml new file mode 100644 index 0000000..3eeeb89 --- /dev/null +++ b/roles/oauth2_proxy/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests oauth2_proxy tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/oauth2_proxy/templates/etc/apache2/sites-available/oauth2_proxy.conf b/roles/oauth2_proxy/templates/etc/apache2/sites-available/oauth2_proxy.conf new file mode 100644 index 0000000..121e617 --- /dev/null +++ b/roles/oauth2_proxy/templates/etc/apache2/sites-available/oauth2_proxy.conf @@ -0,0 +1,75 @@ +#jinja2:trim_blocks: False +# {{ ansible_managed }} +{%- macro apache_location(path, location) %} + + {%- if location.config is defined %} + {%- for config_item in location.config %} + {%- for key, value in config_item.iteritems() %} + {{ key }} {{ value }} + {%- endfor %} + {%- endfor %} + {%- endif %} + {%- if location.proxy is defined %} + ProxyPass {{ location.proxy }} + ProxyPassReverse {{ location.proxy }} + {%- endif %} + +{%- endmacro %} +{%- if oauth2_proxy.apache.ssl.enabled and oauth2_proxy.http_redirect %} + + ServerName {{ oauth2_proxy.apache.servername }} + Redirect permanent / https://{{ oauth2_proxy.apache.servername }}/ + FileETag MTime Size + +{%- endif %} +{%- if oauth2_proxy.apache.ssl.enabled %} + + ServerName {{ oauth2_proxy.apache.servername }} + ServerAdmin {{ oauth2_proxy.apache.serveradmin }} + FileETag MTime Size + SSLCertificateFile /etc/ssl/certs/oauth2_proxy.crt + SSLCertificateKeyFile /etc/ssl/private/oauth2_proxy.key + {{ apache.ssl.settings }} + + RequestHeader set X-Forwarded-Proto "https" + + {%- if oauth2_proxy.apache.auth_proxy.enabled %} + ProxyPreserveHost On + ProxyPass / http://{{ oauth2_proxy.apache.auth_proxy.ip }}:{{ oauth2_proxy.apache.auth_proxy.port }}/ + ProxyPassReverse / http://{{ oauth2_proxy.apache.auth_proxy.ip }}:{{ oauth2_proxy.apache.auth_proxy.port }}/ + {%- endif %} + + ErrorLog ${APACHE_LOG_DIR}/oauth2-auth-proxy-{{ oauth2_proxy.vhost_name }}-error.log + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel {{ oauth2_proxy.apache.auth_proxy.log_level }} + CustomLog ${APACHE_LOG_DIR}/oauth2-auth-proxy-{{ oauth2_proxy.vhost_name }}-access.log combined + +{%- endif %} +{%- if oauth2_proxy.apache.auth_proxy.enabled and oauth2_proxy.apache.protected_locations.enabled %} + + {%- if oauth2_proxy.docroot is defined %} + DocumentRoot {{ oauth2_proxy.docroot }} + ErrorDocument 404 /index.html + {%- endif %} + {%- if oauth2_proxy.locations %} + {%- for location, data in oauth2_proxy.locations|dictsort %} + {%- if location == "root" %} + {{ apache_location(data.path, data) }} + {%- else %} + {{ apache_location('/'+location+'/', data) }} + {%- endif %} + {%- endfor %} + {% endif %} + {%- if oauth2_proxy.apache_status %} + + SetHandler server-status + + {% endif %} + ErrorLog ${APACHE_LOG_DIR}/oauth2-protected-locations-{{ oauth2_proxy.vhost_name }}-error.log + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel {{ oauth2_proxy.apache.protected_locations.log_level }} + CustomLog ${APACHE_LOG_DIR}/oauth2-protected-locations-{{ oauth2_proxy.vhost_name }}-access.log combined + +{%- endif %} diff --git a/roles/oauth2_proxy/templates/etc/oauth2_proxy/oauth2_proxy.cfg.j2 b/roles/oauth2_proxy/templates/etc/oauth2_proxy/oauth2_proxy.cfg.j2 new file mode 100644 index 0000000..440e83a --- /dev/null +++ b/roles/oauth2_proxy/templates/etc/oauth2_proxy/oauth2_proxy.cfg.j2 @@ -0,0 +1,74 @@ +# {{ ansible_managed }} + +## OAuth2 Proxy Config File +## https://github.com/bitly/oauth2_proxy + +## : to listen on for HTTP/HTTPS clients +http_address = "{{ oauth2_proxy.apache.auth_proxy.ip }}:{{ oauth2_proxy.apache.auth_proxy.port }}" + +provider = "{{oauth2_proxy.config.provider}}" +login_url = "{{ oauth2_proxy.config.provider_url }}/login/oauth/authorize" +redeem_url = "{{ oauth2_proxy.config.provider_url }}/login/oauth/access_token" +validate_url = "{{ oauth2_proxy.config.provider_url }}/api/v3/user/emails" + +## the OAuth Redirect URL. +# defaults to the "https://" + requested host header + "/oauth2/callback" +redirect_url = "https://{{ oauth2_proxy.apache.servername }}/oauth2/callback" + + +## the http url(s) of the upstream endpoint. If multiple, routing is based on path +upstreams = "{{ oauth2_proxy.apache.protected_locations.url }}" + +## The OAuth Client ID, Secret +client_id = "{{oauth2_proxy.config.client_id}}" +client_secret = "{{oauth2_proxy.config.client_secret}}" + +## Log requests to stdout +request_logging = true + +## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream +# pass_basic_auth = true +## pass the request Host Header to upstream +## when disabled the upstream Host is used as the Host Header +# pass_host_header = true + +## Email Domains to allow authentication for (this authorizes any email on this domain) +## for more granular authorization use `authenticated_emails_file` +## To authorize any email addresses use "*" +email_domains = [{{oauth2_proxy.config.email_domains|join(",")}}] + +## Pass OAuth Access token to upstream via "X-Forwarded-Access-Token" +pass_access_token = {{oauth2_proxy.config.pass_access_token}} + +## Authenticated Email Addresses File (one email per line) +# authenticated_emails_file = "" + +## Htpasswd File (optional) +## Additionally authenticate against a htpasswd file. Entries must be created with "htpasswd -s" for SHA encryption +## enabling exposes a username/login signin form +# htpasswd_file = "" + +## Templates +## optional directory with custom sign_in.html and error.html +# custom_templates_dir = "" + +## Cookie Settings +## Name - the cookie name +## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes +## for use with an AES cipher when cookie_refresh or pass_access_token +## is set +## Domain - (optional) cookie domain to force cookies to (ie: .yourcompany.com) +## Expire - (duration) expire timeframe for cookie +## Refresh - (duration) refresh the cookie when duration has elapsed after cookie was initially set. +## Should be less than cookie_expire; set to 0 to disable. +## On refresh, OAuth token is re-validated. +## (ie: 1h means tokens are refreshed on request 1hr+ after it was set) +## Secure - secure cookies are only sent by the browser of a HTTPS connection (recommended) +## HttpOnly - httponly cookies are not readable by javascript (recommended) +cookie_name = "{{oauth2_proxy.config.cookie_name}}" +cookie_secret = "{{oauth2_proxy.config.cookie_secret}}" +cookie_domain = "{{oauth2_proxy.config.cookie_domain}}" +cookie_expire = "{{oauth2_proxy.config.cookie_expire}}" +cookie_refresh = "{{oauth2_proxy.config.cookie_refresh}}" +cookie_secure = {{oauth2_proxy.config.cookie_secure}} +cookie_httponly = {{oauth2_proxy.config.cookie_httponly}} diff --git a/roles/oauth2_proxy/templates/serverspec/oauth2-proxy_spec.rb b/roles/oauth2_proxy/templates/serverspec/oauth2-proxy_spec.rb new file mode 100644 index 0000000..e861344 --- /dev/null +++ b/roles/oauth2_proxy/templates/serverspec/oauth2-proxy_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe command('apache2ctl -M') do +{% if oauth2_proxy.apache.ssl.enabled %} + its(:stdout) { should contain('ssl_module') } #OIP002 +{% endif %} +{% if oauth2_proxy.apache_status %} + its(:stdout) { should contain('status_module') } #OIP003 +{% endif %} + its(:stdout) { should contain('headers_module') } #OIP004 + its(:stdout) { should contain('proxy_http_module') } #OIP005 +end + +{% if oauth2_proxy.apache.ssl.enabled %} +describe file('/etc/ssl/private/{{ oauth2_proxy.vhost_name }}.key') do + it { should be_mode 640 } #OIP007 + it { should be_owned_by 'root' } #OIP008 + it { should be_grouped_into 'ssl-key' } #OIP009 + it { should be_file } #OIP010 +end +describe file('/etc/ssl/certs/{{ oauth2_proxy.vhost_name }}.crt') do + it { should be_file } #OIP011 +end +{% endif %} + +describe file('/etc/apache2/sites-available/{{ oauth2_proxy.vhost_name }}.conf') do + it { should be_file } #OIP012 + its(:content) { should contain('') } #OIP013 +end + +describe file('/etc/apache2/sites-enabled/{{ oauth2_proxy.vhost_name }}.conf') do + it { should be_file } #OIP014 +end + +describe file('/var/www/html/index.html') do + it { should be_file } #OIP021 +end + +{% for item in oauth2_proxy.firewall %} +describe port('{{ item.port }}') do + it { should be_listening } #OIP022 +end +{% endfor %} + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end diff --git a/roles/oauth2_proxy/templates/var/www/html/index.html b/roles/oauth2_proxy/templates/var/www/html/index.html new file mode 100644 index 0000000..f3605c9 --- /dev/null +++ b/roles/oauth2_proxy/templates/var/www/html/index.html @@ -0,0 +1,172 @@ + + + + + + + +
+ +

Control List

+ + {% if oauth2_proxy.locations %} +
+

Locations

+ +
    + {% for location, data in oauth2_proxy.locations|dictsort %} + {% if data.url is defined %} +
  • {{ location|capitalize }} /{{ location }}/
  • + {% endif %} + {% endfor %} +
+
+ {% endif %} + +
+
+ + + + + + + diff --git a/roles/openid_proxy/defaults/main.yml b/roles/openid_proxy/defaults/main.yml new file mode 100644 index 0000000..8d633d1 --- /dev/null +++ b/roles/openid_proxy/defaults/main.yml @@ -0,0 +1,92 @@ +--- +openid_proxy: + apache: + state: present + servername: "elk-local" + serveraliases: [] + serveradmin: "hostmaster@blueboxgrid.com" + ssl: + enabled: false + name: openid_proxy + intermediate: ~ + cert: ~ + key: ~ + redirects: [] + # - name: openid_proxy + # state: present + # type: temp # or permanent + # redirect_url: "https://127.0.0.1/" + version: "1.8.10.1" + download: + url: https://github.com/pingidentity/mod_auth_openidc/releases/download/v1.8.10.1/libapache2-mod-auth-openidc_1.8.10.1-1ubuntu1.trusty.1_amd64.deb + log_level: "warn" + http_redirect: false + apache_status: true + root: "/var/www/html" + vhost_name: "openid_proxy" + + admin: + apache: + state: present + users: + - username: admin + password: admin + # Only use when reverse proxying to an HTTPS secured endpoint + sslproxy: + enabled: false + locations: [] + remote_locations: [] + listen: + ip: "*" + port: 80 + port_ssl: 443 + admin_ip: "*" + admin_port: 1080 + admin_port_ssl: 10443 + firewall: + - port: 80 + protocol: tcp + src: + - 0.0.0.0/0 + - port: 443 + protocol: tcp + src: + - 0.0.0.0/0 + - port: 1080 + protocol: tcp + src: + - 0.0.0.0/0 + - port: 10443 + protocol: tcp + src: + - 0.0.0.0/0 + oidc: + enabled: false + id: "" + secret: "" + passphrase: "" + issuer: auth.bluebox.net + auth_endpoint: https://auth.bluebox.net/oauth/authorize + token_endpoint: https://auth.bluebox.net/oauth/token + info_endpoint: https://auth.bluebox.net/oauth/userinfo + + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - "/var/log/apache2/openid-*-access.log" + fields: + tags: openid_proxy,apache_access + - paths: + - "/var/log/apache2/openid-*-error.log" + fields: + tags: openid_proxy,apache_error + - paths: + - "/var/log/apache2/admin-*-access.log" + fields: + tags: admin_proxy,apache_access + - paths: + - "/var/log/apache2/admin-*-error.log" + fields: + tags: admin_proxy,apache_error + logging: + forwarder: filebeat diff --git a/roles/openid_proxy/handlers/main.yml b/roles/openid_proxy/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/openid_proxy/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/openid_proxy/meta/main.yml b/roles/openid_proxy/meta/main.yml new file mode 100644 index 0000000..3467379 --- /dev/null +++ b/roles/openid_proxy/meta/main.yml @@ -0,0 +1,17 @@ +--- +dependencies: + - role: apache + - role: bbg-ssl + name: "{{ openid_proxy.apache.ssl.name }}" + ssl_cert: "{{ openid_proxy.apache.ssl.cert }}" + ssl_key: "{{ openid_proxy.apache.ssl.key }}" + ssl_intermediate: "{{ openid_proxy.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: openid_proxy.apache.ssl.enabled + tags: ['bbg-ssl'] + - role: logging-config + service: openid_proxy + logdata: "{{ openid_proxy.logs }}" + forward_type: "{{ openid_proxy.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/openid_proxy/tasks/checks.yml b/roles/openid_proxy/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/openid_proxy/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/openid_proxy/tasks/main.yml b/roles/openid_proxy/tasks/main.yml new file mode 100644 index 0000000..7295bb5 --- /dev/null +++ b/roles/openid_proxy/tasks/main.yml @@ -0,0 +1,124 @@ +--- +- name: install mod_auth_openidc deps + apt: pkg=ssl-cert + +- name: download mod_auth_openidc + get_url: url={{ openid_proxy.download.url }} + dest=/tmp/libapache2-mod-auth-openidc_{{ openid_proxy.version }}-1_amd64.deb + when: openid_proxy.oidc.enabled|bool + +- name: install mod_auth_openidc + apt: deb=/tmp/libapache2-mod-auth-openidc_{{ openid_proxy.version }}-1_amd64.deb + install_recommends=yes + notify: restart apache + when: openid_proxy.oidc.enabled|bool + +- name: enable apache mod auth_openidc + apache2_module: name=auth_openidc + when: openid_proxy.oidc.enabled|bool + notify: restart apache + +# if auth_openidc.conf.dpkg-dist exists, it is because we upgraded the package +# and we need to overwrite our custom config with the new one. +- name: overwrite old module config if new is installed + command: mv /etc/apache2/mods-available/auth_openidc.conf.dpkg-dist /etc/apache2/mods-available/auth_openidc.conf + args: + creates: /etc/apache2/mods-available/auth_openidc.conf + removes: /etc/apache2/mods-available/auth_openidc.conf.dpkg-dist + notify: restart apache + +# if mod auth_openidc is enabled but not configured or used by any sites +# it blocks apache start +- name: configure default auth provider + lineinfile: dest=/etc/apache2/mods-available/auth_openidc.conf + line="OIDCProviderIssuer auth.bluebox.net" + notify: restart apache + when: openid_proxy.oidc.enabled|bool + +- name: enable apache mod ssl + apache2_module: name=ssl + notify: restart apache + when: openid_proxy.apache.ssl.enabled + +- name: enable apache mod header + apache2_module: name=headers + notify: restart apache + +- name: enable apache mod proxy_http + apache2_module: name=proxy_http + notify: restart apache + +- name: enable apache mod status + apache2_module: name=status + when: openid_proxy.apache_status + +- name: install apache template + template: src=etc/apache2/sites-available/openid_proxy.conf + dest=/etc/apache2/sites-available/{{ openid_proxy.vhost_name }}.conf + notify: restart apache + +- name: install redirect template + template: + src: etc/apache2/sites-available/redirect.conf + dest: "/etc/apache2/sites-available/redirect-{{ item.name }}.conf" + with_items: "{{ openid_proxy.apache.redirects }}" + notify: restart apache + +- name: install admin apache template + template: src=etc/apache2/sites-available/admin_proxy.conf + dest=/etc/apache2/sites-available/admin_{{ openid_proxy.vhost_name }}.conf + notify: restart apache + +- name: admin users + htpasswd: name={{ item.username }} password={{ item.password }} + path=/etc/apache2/openid_admin_passwd + with_items: "{{ openid_proxy.admin.users }}" + +- name: install apache location index + template: src=var/www/html/index.html + dest=/var/www/html/index.html + +- name: install apache healthcheck file + template: src=var/www/html/health_check + dest=/var/www/html/health_check + when: openid_proxy.health_check_enabled|default('False')|bool + +- name: enable site + apache2_site: name={{ openid_proxy.vhost_name }}.conf state={{ openid_proxy.apache.state }} + notify: restart apache + +- name: enable redirects + apache2_site: + name: "redirect-{{ item.name }}.conf" + state: "{{ item.state }}" + with_items: "{{ openid_proxy.apache.redirects }}" + notify: restart apache + +- name: enable admin site + apache2_site: name=admin_{{ openid_proxy.vhost_name }}.conf state={{ openid_proxy.admin.apache.state }} + notify: restart apache + +- name: allow openid_proxy traffic + ufw: rule=allow to_port={{ item.0.port }} src={{ item.1|default('127.0.0.1') }} + with_subelements: + - "{{ openid_proxy.firewall }}" + - src + tags: + - firewall + +- meta: flush_handlers + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/openid_proxy/tasks/metrics.yml b/roles/openid_proxy/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/openid_proxy/tasks/serverspec.yml b/roles/openid_proxy/tasks/serverspec.yml new file mode 100644 index 0000000..742a368 --- /dev/null +++ b/roles/openid_proxy/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests openid_proxy tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/openid_proxy/templates/etc/apache2/sites-available/admin_proxy.conf b/roles/openid_proxy/templates/etc/apache2/sites-available/admin_proxy.conf new file mode 100644 index 0000000..70538df --- /dev/null +++ b/roles/openid_proxy/templates/etc/apache2/sites-available/admin_proxy.conf @@ -0,0 +1,94 @@ +#jinja2:trim_blocks: False +# {{ ansible_managed }} +{%- macro apache_location(path, location) %} + + {%- if location.config is defined %} + {%- for config_item in location.config %} + {%- for key, value in config_item.iteritems() %} + {{ key }} {{ value }} + {%- endfor %} + {%- endfor %} + {%- endif %} + {%- if location.proxy is defined %} + ProxyPass {{ location.proxy }} + ProxyPassReverse {{ location.proxy }} + {%- endif %} + AuthUserFile /etc/apache2/openid_admin_passwd + AuthType Basic + AuthName "Admin Auth Override" + Require valid-user + +{%- endmacro %} +{%- if openid_proxy.apache.ssl.enabled and openid_proxy.http_redirect %} + + ServerName {{ openid_proxy.apache.servername }} + Redirect permanent / https://{{ openid_proxy.apache.servername }}:{{ openid_proxy.listen.admin_port_ssl }}/ + +{%- endif %} +{%- if openid_proxy.apache.ssl.enabled %} + +{%- else %} + +{%- endif %} + ServerName {{ openid_proxy.apache.servername }} + ServerAdmin {{ openid_proxy.apache.serveradmin }} + + {%- if openid_proxy.apache.ssl.enabled %} + SSLCertificateFile /etc/ssl/certs/{{ openid_proxy.apache.ssl.name }}.crt + SSLCertificateKeyFile /etc/ssl/private/{{ openid_proxy.apache.ssl.name }}.key + {{ apache.ssl.settings }} + {%- endif %} + + {% if openid_proxy.sslproxy.enabled|bool %} + SSLProxyEngine On + SSLProxyCheckPeerCN Off + SSLProxyCheckPeerExpire Off + SSLProxyCheckPeerName Off + {% endif %} + + {%- if openid_proxy.root is defined %} + DocumentRoot {{ openid_proxy.root }} + ErrorDocument 404 /index.html + {%- endif %} + {%- if openid_proxy.apache.ssl.enabled %} + RequestHeader set X-Forwarded-Proto "https" + {%- endif %} + {%- if openid_proxy.remote_locations %} + {%- for location, services in openid_proxy.remote_locations|dictsort %} + {%- for service, data in services|dictsort %} + {{ apache_location('/'+location+'/'+service+'/', data) }} + {%- endfor %} + {%- endfor %} + {%- endif %} + {%- if openid_proxy.locations %} + {%- for location, data in openid_proxy.locations|dictsort %} + {%- if location == "root" %} + {{ apache_location(data.path, data) }} + {%- else %} + {{ apache_location('/'+location+'/', data) }} + {%- endif %} + {%- endfor %} + {% endif %} + + + {% if openid_proxy.apache_status %} + + AuthUserFile /etc/apache2/openid_admin_passwd + AuthType Basic + AuthName "Admin Auth Override" + Require valid-user + + {% endif %} + {%- if openid_proxy.health_check_enabled|default('False')|bool %} + + Order allow,deny + Allow from all + Satisfy any + + {% endif -%} + ErrorLog ${APACHE_LOG_DIR}/admin-{{ openid_proxy.vhost_name }}-error.log + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel {{ openid_proxy.log_level }} + CustomLog ${APACHE_LOG_DIR}/admin-{{ openid_proxy.vhost_name }}-access.log combined + diff --git a/roles/openid_proxy/templates/etc/apache2/sites-available/openid_proxy.conf b/roles/openid_proxy/templates/etc/apache2/sites-available/openid_proxy.conf new file mode 100644 index 0000000..1108474 --- /dev/null +++ b/roles/openid_proxy/templates/etc/apache2/sites-available/openid_proxy.conf @@ -0,0 +1,126 @@ +# {{ ansible_managed }} + +{% macro apache_location(path, location) %} + + {% if openid_proxy.oidc.enabled %} + {% if location.auth_type|default('') != "config" %} + AuthType openid-connect + Require claim {{ location.openid_claim|default('is_staff:true') }} + {% endif %} + {% endif %} + {% if location.config is defined %} + {% for config_item in location.config %} + {% for key, value in config_item.iteritems() %} + {{ key }} {{ value }} + {% endfor %} + {% endfor %} + {% endif %} + {% if location.proxy is defined %} + ProxyPass {{ location.proxy }} + ProxyPassReverse {{ location.proxy }} + {% endif %} + +{% endmacro %} + +{% if openid_proxy.apache.ssl.enabled and openid_proxy.http_redirect %} + + ServerName {{ openid_proxy.apache.servername }} + Redirect permanent / https://{{ openid_proxy.apache.servername }}/ + FileETag MTime Size + +{% endif %} + +{% if openid_proxy.apache.ssl.enabled %} + +{% else %} + +{% endif %} + ServerName {{ openid_proxy.apache.servername }} + {% if openid_proxy.apache.serveraliases %} + ServerAlias {{ openid_proxy.apache.serveraliases|join(" ") }} + {% endif %} + ServerAdmin {{ openid_proxy.apache.serveradmin }} + FileETag MTime Size + {% if openid_proxy.apache.ssl.enabled %} + SSLCertificateKeyFile /etc/ssl/private/{{ openid_proxy.apache.ssl.name }}.key + SSLCertificateFile /etc/ssl/certs/{{ openid_proxy.apache.ssl.name }}.crt + {% if bbg_ssl.intermediate or openid_proxy.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ openid_proxy.apache.ssl.name }}-intermediate.crt + {% endif %} + {{ apache.ssl.settings }} + {% endif %} + + {% if openid_proxy.sslproxy.enabled|bool %} + SSLProxyEngine On + SSLProxyCheckPeerCN Off + SSLProxyCheckPeerExpire Off + SSLProxyCheckPeerName Off + {% endif %} + + {% if openid_proxy.oidc.enabled %} + OIDCProviderIssuer {{ openid_proxy.oidc.issuer }} + OIDCProviderAuthorizationEndpoint {{ openid_proxy.oidc.auth_endpoint }} + OIDCProviderTokenEndpoint {{ openid_proxy.oidc.token_endpoint }} + OIDCProviderUserInfoEndpoint {{ openid_proxy.oidc.info_endpoint }} + OIDCClientID {{ openid_proxy.oidc.id }} + OIDCClientSecret {{ openid_proxy.oidc.secret }} + # only used internally for passing state around securely + OIDCCryptoPassphrase {{ openid_proxy.oidc.passphrase }} + # required + OIDCScope "openid" + # Arbitrary but must be a path under the protected resource. + OIDCRedirectURI https://{{ openid_proxy.apache.servername }}/auth/callback + # Store session state in stateful manner (default) + OIDCSessionType server-cache + # sets REMOTE_USER to the user's username from Auth + # see below for full options + OIDCRemoteUserClaim preferred_username + OIDCAuthNHeader X-Proxy-Remote-User + {% endif %} + {% if openid_proxy.root is defined %} + DocumentRoot {{ openid_proxy.root }} + ErrorDocument 404 /index.html + {% endif %} + {% if openid_proxy.apache.ssl.enabled %} + RequestHeader set X-Forwarded-Proto "https" + {% endif %} + {% if openid_proxy.remote_locations %} + {% for location, services in openid_proxy.remote_locations|dictsort %} + {% for service, data in services|dictsort %} + {{ apache_location('/'+location+'/'+service+'/', data) }} + {% endfor %} + {% endfor %} + {% endif %} + {% if openid_proxy.locations %} + {% for location, data in openid_proxy.locations|dictsort %} + {% if location == "root" %} + {{ apache_location(data.path, data) }} + {% else %} + {{ apache_location('/'+location+'/', data) }} + {% endif %} + {% endfor %} + {% endif %} + {% if openid_proxy.apache_status %} + + {% if openid_proxy.oidc.enabled %} + AuthType openid-connect + Require claim is_staff:true + {% endif %} + SetHandler server-status + + {% endif %} + + {% if openid_proxy.health_check_enabled|default('False')|bool %} + + Order allow,deny + Allow from all + Satisfy any + + {% endif %} + + ErrorLog ${APACHE_LOG_DIR}/openid-{{ openid_proxy.vhost_name }}-error.log + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel {{ openid_proxy.log_level }} + CustomLog ${APACHE_LOG_DIR}/openid-{{ openid_proxy.vhost_name }}-access.log combined + diff --git a/roles/openid_proxy/templates/etc/apache2/sites-available/redirect.conf b/roles/openid_proxy/templates/etc/apache2/sites-available/redirect.conf new file mode 100644 index 0000000..eef7813 --- /dev/null +++ b/roles/openid_proxy/templates/etc/apache2/sites-available/redirect.conf @@ -0,0 +1,27 @@ +# {{ ansible_managed }} + +{% if openid_proxy.apache.ssl.enabled %} + + ServerName {{ item.name }} + ServerAdmin {{ openid_proxy.apache.serveradmin }} + Redirect {{ item.type|default('temp') }} / {{ item.redirect_url }} + +{% endif %} + +{% if openid_proxy.apache.ssl.enabled %} + +{% else %} + +{% endif %} + ServerName {{ item.name }} + ServerAdmin {{ openid_proxy.apache.serveradmin }} + Redirect {{ item.type|default('temp') }} / {{ item.redirect_url }} +{% if openid_proxy.apache.ssl.enabled %} + SSLCertificateKeyFile /etc/ssl/private/{{ openid_proxy.apache.ssl.name }}.key + SSLCertificateFile /etc/ssl/certs/{{ openid_proxy.apache.ssl.name }}.crt +{% if bbg_ssl.intermediate or openid_proxy.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ openid_proxy.apache.ssl.name }}-intermediate.crt +{% endif %} + {{ apache.ssl.settings }} +{% endif %} + diff --git a/roles/openid_proxy/templates/serverspec/openid-proxy_spec.rb b/roles/openid_proxy/templates/serverspec/openid-proxy_spec.rb new file mode 100644 index 0000000..b0eef4f --- /dev/null +++ b/roles/openid_proxy/templates/serverspec/openid-proxy_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe command('apache2ctl -M') do +{% if openid_proxy.oidc.enabled %} + its(:stdout) { should contain('auth_openidc_module') } #OIP001 +{% endif %} +{% if openid_proxy.apache.ssl.enabled %} + its(:stdout) { should contain('ssl_module') } #OIP002 +{% endif %} +{% if openid_proxy.apache_status %} + its(:stdout) { should contain('status_module') } #OIP003 +{% endif %} + its(:stdout) { should contain('headers_module') } #OIP004 + its(:stdout) { should contain('proxy_http_module') } #OIP005 +end + +{% if openid_proxy.oidc.enabled %} +describe file('/etc/apache2/mods-available/auth_openidc.conf') do + its(:content) { should contain('OIDCProviderIssuer auth.bluebox.net') } #OIP006 +end +{% endif %} + +{% if openid_proxy.apache.ssl.enabled %} +describe file('/etc/ssl/private/{{ openid_proxy.apache.ssl.name }}.key') do + it { should be_mode 640 } #OIP007 + it { should be_owned_by 'root' } #OIP008 + it { should be_grouped_into 'ssl-key' } #OIP009 + it { should be_file } #OIP010 +end +describe file('/etc/ssl/certs/{{ openid_proxy.apache.ssl.name }}.crt') do + it { should be_file } #OIP011 +end +{% endif %} + +describe file('/etc/apache2/sites-enabled/{{ openid_proxy.vhost_name }}.conf') do + it { should be_file } #OIP014 +end + +{% if openid_proxy.http_redirect %} +describe file('/etc/apache2/sites-available/{{ openid_proxy.vhost_name }}.conf') do + it { should be_file } #OIP012 + its(:content) { should contain('') } #OIP013 +end +{% endif %} + +describe file('/etc/apache2/sites-enabled/admin_{{ openid_proxy.vhost_name }}.conf') do + it { should be_file } #OIP017 +end + +{% if openid_proxy.http_redirect %} +describe file('/etc/apache2/sites-available/admin_{{ openid_proxy.vhost_name }}.conf') do + it { should be_file } #OIP015 + its(:content) { should contain('') } #OIP016 +end +{% endif %} + +{% for user in openid_proxy.admin.users %} +describe file('/etc/apache2/openid_admin_passwd') do + it { should be_file } #OIP018 + its(:content) { should contain('{{ user.username }}') } #OIP019 + its(:content) { should_not contain /({{ user.username }}:.{0,15})$/} #OIP020 +end +{% endfor %} + +describe file('/var/www/html/index.html') do + it { should be_file } #OIP021 +end + +{% for item in openid_proxy.firewall %} +describe port('{{ item.port }}') do + it { should be_listening } #OIP022 +end +{% endfor %} + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end diff --git a/roles/openid_proxy/templates/var/www/html/health_check b/roles/openid_proxy/templates/var/www/html/health_check new file mode 100644 index 0000000..d86bac9 --- /dev/null +++ b/roles/openid_proxy/templates/var/www/html/health_check @@ -0,0 +1 @@ +OK diff --git a/roles/openid_proxy/templates/var/www/html/index.html b/roles/openid_proxy/templates/var/www/html/index.html new file mode 100644 index 0000000..ef3deb4 --- /dev/null +++ b/roles/openid_proxy/templates/var/www/html/index.html @@ -0,0 +1,194 @@ + + + + + +
+ {% if openid_proxy.locations %} +
+
+ +
+

Central control

+ +
    + {% for location, data in openid_proxy.locations|dictsort %} + {% if data.url is defined %} +
  • {{ location|capitalize }} /{{ location }}/
  • + {% endif -%} + {% endfor -%} +
+
+ {% endif %} + + {% if openid_proxy.remote_locations %} +
+

Remote Locations

+ + {% for location, services in openid_proxy.remote_locations|dictsort %} +
+

{{ location|upper }}

+ + +
+ {% endfor %} + +
+ {% endif %} +
+
+ + + + + + diff --git a/roles/percona/defaults/main.yml b/roles/percona/defaults/main.yml new file mode 100644 index 0000000..e0c3440 --- /dev/null +++ b/roles/percona/defaults/main.yml @@ -0,0 +1,35 @@ +--- +database: + users: {} + +percona: + replication: False + root_password: asdf + galera_version: 3.x + client_version: 5.6 + server_version: 5.6 + sst_auth_user: sst_admin + sst_auth_password: asdf + wsrep_cluster_name: ursula_infra + ip: "{{ private_ipv4.address }}" + firewall: + - port: 3306 + src: "{{ groups['percona'] | map('extract', hostvars, [private_interface, 'ipv4', 'address']) | list }}" + - port: 4010 + src: "{{ groups['percona'] | map('extract', hostvars, [private_interface, 'ipv4', 'address']) | list }}" + - port: 4011 + src: "{{ groups['percona'] | map('extract', hostvars, [private_interface, 'ipv4', 'address']) | list }}" + - port: 4444 + src: "{{ groups['percona'] | map('extract', hostvars, [private_interface, 'ipv4', 'address']) | list }}" + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/lib/mysql/mysql-error.log + fields: + tags: mysql,percona + - paths: + - /var/log/garbd.log + fields: + tags: mysql,percona,garbd + logging: + forwarder: filebeat diff --git a/roles/percona/handlers/main.yml b/roles/percona/handlers/main.yml new file mode 100644 index 0000000..282381c --- /dev/null +++ b/roles/percona/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart mysql server + service: name=mysql state=restarted diff --git a/roles/percona/meta/main.yml b/roles/percona/meta/main.yml new file mode 100644 index 0000000..8951515 --- /dev/null +++ b/roles/percona/meta/main.yml @@ -0,0 +1,12 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.percona.repo }} {{ ansible_distribution_release }} main' + key_url: '{{ apt_repos.percona.key_url }}' + - role: logging-config + service: percona + logdata: "{{ percona.logs }}" + forward_type: "{{ percona.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/percona/tasks/arbiter.yml b/roles/percona/tasks/arbiter.yml new file mode 100644 index 0000000..1f69563 --- /dev/null +++ b/roles/percona/tasks/arbiter.yml @@ -0,0 +1,19 @@ +--- +- name: try to stop old garbd service + service: name=garbd state=stopped enabled=false pattern=/usr/bin/garbd + failed_when: False + +- name: make garbd log + copy: dest=/var/log/garbd.log owner=nobody content='' force=no + +# the arbiter node only needs the garbd daemon +- name: install percona garbd package + apt: pkg={{ item }} state=installed + with_items: + - percona-xtradb-cluster-garbd-3.x + +- name: install garbd config + template: src=etc/default/garbd dest=/etc/default/garbd mode=0644 + +- name: ensure garbd running + service: name=garbd state=started enabled=on pattern=/usr/bin/garbd diff --git a/roles/percona/tasks/backup.yml b/roles/percona/tasks/backup.yml new file mode 100644 index 0000000..9ea7a71 --- /dev/null +++ b/roles/percona/tasks/backup.yml @@ -0,0 +1,6 @@ +--- +- name: mailutils package + apt: pkg=mailutils + +- name: add percona-xtrabackup.sh to cron.daily + template: src=percona-xtrabackup.sh dest=/etc/cron.daily/percona-xtrabackup owner=root group=root mode=0755 diff --git a/roles/percona/tasks/checks.yml b/roles/percona/tasks/checks.yml new file mode 100644 index 0000000..8c5b6b8 --- /dev/null +++ b/roles/percona/tasks/checks.yml @@ -0,0 +1,10 @@ +--- +- name: install mysql process check + sensu_check_dict: name="check-mysql-process" check="{{ sensu_checks.percona.check_mysql_process }}" + notify: restart sensu-client missing ok + when: inventory_hostname in groups.percona_primary or inventory_hostname in groups.percona_secondary|default('[]') + +- name: install garbd process check + sensu_check_dict: name="check-garbd-process" check="{{ sensu_checks.percona.check_garbd_process }}" + notify: restart sensu-client missing ok + when: inventory_hostname in groups.percona_arbiter|default('[]') diff --git a/roles/percona/tasks/main.yml b/roles/percona/tasks/main.yml new file mode 100644 index 0000000..f973360 --- /dev/null +++ b/roles/percona/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: install python-pycurl + apt: pkg=python-pycurl + +- include: server.yml + when: inventory_hostname in groups.percona_primary or inventory_hostname in groups.percona_secondary|default('[]') + +- include: backup.yml + when: inventory_hostname in groups.percona_backup|default('[]') + +- include: arbiter.yml + when: inventory_hostname in groups.percona_arbiter|default('[]') + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec + diff --git a/roles/percona/tasks/metrics.yml b/roles/percona/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/percona/tasks/replication.yml b/roles/percona/tasks/replication.yml new file mode 100644 index 0000000..2b4f28e --- /dev/null +++ b/roles/percona/tasks/replication.yml @@ -0,0 +1,45 @@ +--- +- name: are we already already bootstrapped + shell: mysql -e "SHOW VARIABLES LIKE 'wsrep_on'" | grep 'ON' + register: already_bootstrapped + failed_when: False + +- name: create state snapshot transfer user from anywhere + mysql_user: name={{ percona.sst_auth_user }} host={{ item }} + password={{ percona.sst_auth_password }} + priv='*.*:RELOAD,LOCK TABLES,REPLICATION CLIENT' + with_items: + - '%' + - 'localhost' + +- name: stop all of the mysql processes before we boostrap any replication + service: name=mysql state=stopped + when: not already_bootstrapped.rc == 0 + +- name: register replication databases + command: echo {% for host in groups['percona_primary']|union(groups['percona_secondary']) %}{% if not loop.last %}{{ hostvars[host][private_interface]['ipv4']['address'] }},{% else %}{{ hostvars[host][private_interface]['ipv4']['address'] }}{% endif %}{% endfor %} + register: percona_replication_nodes + +- name: configure bootstrap replication + template: src=etc/mysql/conf.d/replication.cnf + dest=/etc/mysql/conf.d/replication.cnf + owner=mysql group=mysql mode=0600 + +- name: allow replication traffic + ufw: + rule: allow + to_port: "{{ item.0.port }}" + src: "{{ item.1 }}" + with_subelements: + - "{{ percona.firewall }}" + - src + tags: + - firewall + +- name: bootstrap the primary node + command: /etc/init.d/mysql bootstrap-pxc + when: should_bootstrap_as_primary and not already_bootstrapped.rc == 0 + +- name: bootstrap any secondary nodes + service: name=mysql state=started enabled=yes + when: not should_bootstrap_as_primary and not already_bootstrapped.rc == 0 diff --git a/roles/percona/tasks/server.yml b/roles/percona/tasks/server.yml new file mode 100644 index 0000000..ccac8b7 --- /dev/null +++ b/roles/percona/tasks/server.yml @@ -0,0 +1,90 @@ +--- +- name: python-mysqldb package + apt: pkg=python-mysqldb + +- name: create mysql user + user: name=mysql comment=mysql shell=/bin/false + system=yes home=/nonexistent + +- name: mysql log directory + file: path=/var/log/mysql state=directory + owner=mysql group=mysql mode=0755 + +- name: create mysql config directory + file: path=/etc/mysql/conf.d state=directory + owner=mysql group=mysql mode=0755 + +- name: configure my.cnf + template: src=etc/my.cnf dest=/etc/mysql/my.cnf + owner=mysql group=mysql mode=0644 + notify: + - restart mysql server + +- name: install mysql config files + template: src=etc/mysql/conf.d/{{ item }} dest=/etc/mysql/conf.d/{{ item }} + owner=mysql group=mysql mode=0644 + with_items: + - bind-inaddr-any.cnf + - tuning.cnf + - utf8.cnf + notify: + - restart mysql server + +- name: install percona xtradb packages + apt: pkg={{ item }} state=installed + with_items: + - percona-xtradb-cluster-galera-{{ percona.galera_version }} + - percona-xtradb-cluster-client-{{ percona.client_version }} + - percona-xtradb-cluster-server-{{ percona.server_version }} + - percona-xtrabackup + +# Workaround for 0.8.6 to 0.9.1 upgrade path +- name: fix config header in replication.cnf + lineinfile: dest=/etc/mysql/conf.d/replication.cnf create=yes + regexp="^\[mysqld\]" line="[mysqld]" insertbefore=BOF + +- name: adjust various other lines in replication.cnf + lineinfile: dest=/etc/mysql/conf.d/replication.cnf create=yes + regexp="{{ item.value.regexp }}" + line="{{ item.value.line }}" + with_dict: + dse: + regexp: '^default_storage_engine\s*=' + line: "default_storage_engine = InnoDB" + blf: + regexp: '^binlog_format\s*=' + line: "binlog_format = ROW" + ilub: + regexp: '^innodb_locks_unsafe_for_binlog\s*=' + line: "innodb_locks_unsafe_for_binlog = 1" + +- meta: flush_handlers + +- name: start mysql server + service: name=mysql state=started enabled=yes + +- name: set mysql root password + mysql_user: name=root password={{ percona.root_password }} + +- name: install root .my.cnf defaults file + template: src=root/.my.cnf dest=/root/.my.cnf owner=root group=root mode=0600 + +- name: mysql error log + file: path=/var/lib/mysql/mysql-error.log state=touch + owner=mysql group=mysql mode=660 + +- name: remove mysql test database + mysql_db: state=absent name=test + +- include: replication.yml + when: percona.replication + tags: + - percona-replication + +- name: remove anonymous users + mysql_user: name='' host={{ item }} state=absent + with_items: + - localhost + - "{{ ansible_hostname }}" + +- include: setup.yml diff --git a/roles/percona/tasks/serverspec.yml b/roles/percona/tasks/serverspec.yml new file mode 100644 index 0000000..cacea10 --- /dev/null +++ b/roles/percona/tasks/serverspec.yml @@ -0,0 +1,7 @@ +--- +- name: serverspec tests percona tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* + diff --git a/roles/percona/tasks/setup.yml b/roles/percona/tasks/setup.yml new file mode 100644 index 0000000..5e0a628 --- /dev/null +++ b/roles/percona/tasks/setup.yml @@ -0,0 +1,19 @@ +--- +- name: create databases + mysql_db: name={{ item.1 }} state=present + with_subelements: + - "{{ database.users }}" + - databases + run_once: true + +- name: create users + mysql_user: name={{ item.0.username }} + password={{ item.0.password }} + priv={{ item.1 }}.*:ALL + host={{ item.0.host }} + append_privs=yes + state=present + with_subelements: + - "{{ database.users }}" + - databases + run_once: true diff --git a/roles/percona/templates/etc/default/garbd b/roles/percona/templates/etc/default/garbd new file mode 100644 index 0000000..c289b80 --- /dev/null +++ b/roles/percona/templates/etc/default/garbd @@ -0,0 +1,8 @@ +# {{ ansible_managed }} + +{% macro garbd_hosts() -%} +{% for host in groups['db'] -%} {{ private_ipv4['address'] -}}:4567 {% endfor -%} +{% endmacro -%} +GALERA_NODES="{{ garbd_hosts() }}" +GALERA_GROUP="{{ percona.wsrep_cluster_name }}" +LOG_FILE="/var/log/garbd.log" diff --git a/roles/percona/templates/etc/my.cnf b/roles/percona/templates/etc/my.cnf new file mode 100644 index 0000000..001f752 --- /dev/null +++ b/roles/percona/templates/etc/my.cnf @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +[mysqld] + +datadir = /var/lib/mysql +user = mysql +bind-address = 0.0.0.0 +log_error = mysql-error.log + + +!includedir /etc/mysql/conf.d diff --git a/roles/percona/templates/etc/mysql/conf.d/bind-inaddr-any.cnf b/roles/percona/templates/etc/mysql/conf.d/bind-inaddr-any.cnf new file mode 100644 index 0000000..73dda52 --- /dev/null +++ b/roles/percona/templates/etc/mysql/conf.d/bind-inaddr-any.cnf @@ -0,0 +1,4 @@ +# {{ ansible_managed }} + +[mysqld] +bind-address = 0.0.0.0 diff --git a/roles/percona/templates/etc/mysql/conf.d/replication.cnf b/roles/percona/templates/etc/mysql/conf.d/replication.cnf new file mode 100644 index 0000000..6be2adf --- /dev/null +++ b/roles/percona/templates/etc/mysql/conf.d/replication.cnf @@ -0,0 +1,42 @@ +# {{ ansible_managed }} + +[mysqld] + +wsrep_provider = /usr/lib/libgalera_smm.so + +# Empty gcomm address is being used when cluster is getting bootstrapped +{% if percona_replication_nodes is defined -%} +wsrep_provider_options = "base_port=4010; gmcast.listen_addr=tcp://{{ percona.ip }}:4010; " +wsrep_cluster_address = gcomm://{{ percona_replication_nodes.stdout }} +{% endif -%} + +wsrep_slave_threads = 4 + +# +# wsrep_sst_method={xtrabackup|mysqldum|rsync|custom_script} +# +# The downside of mysqldump and rsync is that the donor node becomes READ-ONLY while +# data is being copied from one node to another (SST applies FLUSH TABLES WITH READ LOCK command). +# Xtrabackup SST does not require READ LOCK for the entire syncing process, only for syncing the MySQL system +# tables and writing the information about the binlog, galera and slave information (same as the regular +# XtraBackup backup). +# +wsrep_sst_method = xtrabackup-v2 +wsrep_sst_auth = "{{ percona.sst_auth_user }}:{{ percona.sst_auth_password }}" + +# Cluster name +wsrep_cluster_name = "{{ percona.wsrep_cluster_name }}" + +# This Node's address +wsrep_node_address = {{ percona.ip }} + +# MyISAM storage engine replication has only experimental support in Galera +default_storage_engine = InnoDB + +# README-wsrep indicates the follow three options are mandatory: +# In order for Galera to work correctly binlog format should be ROW +binlog_format = ROW +# This changes how InnoDB autoincrement locks are managed and is a requirement for Galera +innodb_autoinc_lock_mode = 2 +# This is a recommended tuning variable for performance (required for parallel applying) +innodb_locks_unsafe_for_binlog = 1 diff --git a/roles/percona/templates/etc/mysql/conf.d/tuning.cnf b/roles/percona/templates/etc/mysql/conf.d/tuning.cnf new file mode 100644 index 0000000..1269810 --- /dev/null +++ b/roles/percona/templates/etc/mysql/conf.d/tuning.cnf @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +[mysqld] + +max_connections = 512 +innodb_log_file_size=50331648 +lock_wait_timeout = 1800 diff --git a/roles/percona/templates/etc/mysql/conf.d/utf8.cnf b/roles/percona/templates/etc/mysql/conf.d/utf8.cnf new file mode 100644 index 0000000..0250ab1 --- /dev/null +++ b/roles/percona/templates/etc/mysql/conf.d/utf8.cnf @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +[client] +default-character-set=utf8 + +[mysql] +default-character-set=utf8 + +[mysqld] +collation-server = utf8_unicode_ci +character-set-server = utf8 diff --git a/roles/percona/templates/percona-xtrabackup.sh b/roles/percona/templates/percona-xtrabackup.sh new file mode 100755 index 0000000..909fc6f --- /dev/null +++ b/roles/percona/templates/percona-xtrabackup.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# +# {{ ansible_managed }} +# +# File: percona-xtrabackup.sh +# +# Purpose: This script uses percona's innobackupex script to create db archives. +# +# $backup_retention_num is the number of full archives that will reside on disk. +# The desired frequency is up to you and should be defined via cron. +# +# Note: +# - To extract Percona XtraBackup‘s archive you must use tar with -i option +# - To restore an archive, you must use innobackupex with --apply-log option (--apply-log can be ran whenever and does not require a running mysql instance) +# For more info on using innobackup see: http://www.percona.com/doc/percona-xtrabackup/2.1/innobackupex/innobackupex_option_reference.html +# +# Author: mpatterson@bluebox.net + +set -o errexit + +email=team-infrastructure@bluebox.net +backup_script=/usr/bin/innobackupex +gzip=/bin/gzip + +# The number of full archives to keep. +# +# default value is 7. when ran every 24 hours, this +# provides 7 full archives, for up to 7 days. +backup_retention_num=7 + +# set & ensure the backup root dir exists +backup_root_dir=/backup/percona/ +/usr/bin/test -d $backup_root_dir || /bin/mkdir -p $backup_root_dir + +# get list of existing backups. +# innobackupex timestamp format: 2014-01-14_18-43-21 +backup_list=($(/bin/ls -urt $backup_root_dir | /bin/grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' || /bin/echo "")) + +# create a new db archive, use tar stream to compress on the fly +$backup_script --user=root --stream=tar $backup_root_dir | $gzip - > $backup_root_dir`/bin/date +"%Y-%m-%d_%H-%M-%S"`.tar.gz || (/bin/echo "failed to create db archive at: `/bin/date`" | mail $email -s "Pecona backup failed") + +# clean up: delete any archives that exceed the $backup_retention_num +if [ "$#backup_list[@]" -ge "$backup_retention_num" ]; then + + # this should always be true, but let's be paranoid and ensure 100% that the $backup_list + # index value isn't empty, which would result in deleting the entire $backup_root_dir... + if [ "$backup_root_dir$backup_list[0]" != "$backup_root_dir" ]; then + # delete the oldest backup... + /bin/echo deleting $backup_root_dir${backup_list[0]} + /usr/bin/test -f $backup_root_dir${backup_list[0]} && /bin/rm -f $backup_root_dir${backup_list[0]} + fi +fi diff --git a/roles/percona/templates/root/.my.cnf b/roles/percona/templates/root/.my.cnf new file mode 100644 index 0000000..909c787 --- /dev/null +++ b/roles/percona/templates/root/.my.cnf @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +[client] +user=root +password={{ percona.root_password }} diff --git a/roles/percona/templates/serverspec/percona_spec.rb b/roles/percona/templates/serverspec/percona_spec.rb new file mode 100644 index 0000000..920999f --- /dev/null +++ b/roles/percona/templates/serverspec/percona_spec.rb @@ -0,0 +1,103 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe user('mysql') do + it { should exist } #PER001 + it { should belong_to_group 'mysql' } #PER002 + it { should have_home_directory '/nonexistent' } #PER003 + it { should have_login_shell '/bin/false' } #PER004 +end + +describe file('/etc/mysql/conf.d') do + it { should be_mode 755 } #PER005 + it { should be_owned_by 'mysql' } #PER006 + it { should be_grouped_into 'mysql' } #PER007 + it { should be_directory } #PER008 +end + +describe file('/etc/mysql/conf.d/bind-inaddr-any.cnf') do + it { should be_mode 644 } #PER009 + it { should be_owned_by 'mysql' } #PER010 + it { should be_grouped_into 'mysql' } #PER011 + it { should be_file } #PER012 +end + +{% if '{{ percona.replication }}' %} +describe file('/etc/mysql/conf.d/replication.cnf') do + it { should be_mode 644 } #PER013 + it { should be_file } #PER014 +end +{% endif %} + +describe file('/etc/mysql/conf.d/tuning.cnf') do + it { should be_mode 644 } #PER015 + it { should be_owned_by 'mysql' } #PER016 + it { should be_grouped_into 'mysql' } #PER017 + it { should be_file } #PER018 +end + +describe file('/etc/mysql/conf.d/utf8.cnf') do + it { should be_mode 644 } #PER019 + it { should be_owned_by 'mysql' } #PER020 + it { should be_grouped_into 'mysql' } #PER021 + it { should be_file } #PER022 +end + +describe package('percona-xtradb-cluster-galera-{{ percona.galera_version }}') do + it { should be_installed } #PER023 +end + +describe package('percona-xtradb-cluster-client-{{ percona.client_version }}') do + it { should be_installed } #PER024 +end + +describe package('percona-xtradb-cluster-server-{{ percona.server_version }}') do + it { should be_installed } #PER025 +end + +describe package('percona-xtrabackup') do + it { should be_installed } #PER026 +end + +describe package('python-mysqldb') do + it { should be_installed } #PER027 +end + +describe service('mysql') do + it { should be_enabled } +end + +describe file('/var/log/mysql.log') do + it { should be_mode 640 } #PER028 + it { should be_owned_by 'mysql' } #PER029 + it { should be_grouped_into 'adm' } #PER030 + it { should be_file } #PER031 +end + +describe file('/var/log/mysql.err') do + it { should be_mode 640 } #PER032 + it { should be_owned_by 'mysql' } #PER033 + it { should be_grouped_into 'adm' } #PER034 + it { should be_file } #PER035 +end + +describe file('/var/lib/mysql/mysql-error.log') do + it { should be_mode 660 } #PER036 + it { should be_owned_by 'mysql' } #PER037 + it { should be_file } #PER038 +end + +describe file('/var/lib/mysql') do + it { should be_mode 700 } #PER039 + it { should be_owned_by 'mysql' } #PER040 + it { should be_grouped_into 'mysql' } #PER041 + it { should be_directory } #PER042 +end + +describe file('/etc/mysql/my.cnf') do + it { should be_mode 644 } #PER047 + it { should be_owned_by 'mysql' } #PER048 + it { should be_grouped_into 'mysql' } #PER049 + it { should be_file } #PER050 +end diff --git a/roles/postfix-simple/handlers/main.yml b/roles/postfix-simple/handlers/main.yml new file mode 100644 index 0000000..58e2088 --- /dev/null +++ b/roles/postfix-simple/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart postfix + service: name=postfix state=restarted + +- name: reload aliases + command: /usr/sbin/postalias /etc/aliases diff --git a/roles/postfix-simple/meta/main.yml b/roles/postfix-simple/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/postfix-simple/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/postfix-simple/tasks/checks.yml b/roles/postfix-simple/tasks/checks.yml new file mode 100644 index 0000000..802c1e2 --- /dev/null +++ b/roles/postfix-simple/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install postfix process check + sensu_check_dict: name="check-postfix-process" check="{{ sensu_checks.postfix.check_postfix_process }}" + notify: restart sensu-client missing ok diff --git a/roles/postfix-simple/tasks/main.yml b/roles/postfix-simple/tasks/main.yml new file mode 100644 index 0000000..2960452 --- /dev/null +++ b/roles/postfix-simple/tasks/main.yml @@ -0,0 +1,26 @@ +--- +- name: install postfix + apt: pkg=postfix state=present + +- name: install postfix configuration + template: src=etc/postfix/main.cf + dest=/etc/postfix/main.cf + notify: + - restart postfix + +- meta: flush_handlers + +- name: start and enable postfix service + service: name=postfix state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/postfix-simple/tasks/metrics.yml b/roles/postfix-simple/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/postfix-simple/tasks/serverspec.yml b/roles/postfix-simple/tasks/serverspec.yml new file mode 100644 index 0000000..c0d71dd --- /dev/null +++ b/roles/postfix-simple/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for postfix-simple role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/postfix-simple/templates/etc/postfix/main.cf b/roles/postfix-simple/templates/etc/postfix/main.cf new file mode 100644 index 0000000..9921bf1 --- /dev/null +++ b/roles/postfix-simple/templates/etc/postfix/main.cf @@ -0,0 +1,22 @@ +# {{ ansible_managed }} + +smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu) +biff = no +append_dot_mydomain = no +readme_directory = no +smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem +smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key +smtpd_use_tls=yes +smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache +smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache +smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination +myhostname = {{ ansible_hostname }} +alias_maps = hash:/etc/aliases +alias_database = hash:/etc/aliases +mydestination = {{ ansible_hostname }}, {{ ansible_fqdn }}, localhost.localdomain, localhost +relayhost = +mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 +mailbox_size_limit = 0 +recipient_delimiter = + +inet_interfaces = 127.0.0.1,::1 +inet_protocols = all diff --git a/roles/postfix-simple/templates/serverspec/postfix-simple_spec.rb b/roles/postfix-simple/templates/serverspec/postfix-simple_spec.rb new file mode 100644 index 0000000..5c1a6fe --- /dev/null +++ b/roles/postfix-simple/templates/serverspec/postfix-simple_spec.rb @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('postfix') do + it { should be_installed } +end + +describe service('postfix') do + it { should be_enabled } +end diff --git a/roles/pxe/defaults/main.yml b/roles/pxe/defaults/main.yml new file mode 100644 index 0000000..724bbd1 --- /dev/null +++ b/roles/pxe/defaults/main.yml @@ -0,0 +1,56 @@ +--- +pxe_files: False + +pxe: + enable_server: True + interactive: false + pxe_interface: "{{ private_device_interface }}" + tftpboot_path: /srv/pxe/tftpboot + tftp_interface: "{{ private_device_interface }}" + tftp_server: "{{ private_ipv4.address }}" + no_dhcp_interface: ~ + timeout: 100 + dhcp_ranges: + - tag: default + range: 172.16.0.50,172.16.0.70,255.255.255.0,2h + gateway: 172.16.0.1 + dns: 8.8.8.8 + - tag: second + range: 172.16.5.50,172.16.5.70,255.255.255.0,2h + gateway: 172.16.5.1 + dns: 8.8.8.8 + # for serial_com: COM1=0, COM2=1, and COM3=2. Overrideable on each host. + nat: + enabled: False + serial_com: 1 + ks_interface: auto + mirror_http_hostname: archive.ubuntu.com + mirror_http_directory: /ubuntu/ + mirror_http_proxy: '' + ntp_server: + partman_disk: /dev/sda + packages: + - curl + - openssh-server + - build-essential + - sudo + - wget + - ifenslave + - vlan + root_password: changeme + blueboxadmin_password: changeme + ssh_pub_keys: [] + os: + - name: precise + kernel: http://archive.ubuntu.com/ubuntu/dists/precise-updates/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux + bootloader: http://archive.ubuntu.com/ubuntu/dists/precise-updates/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz + root_password: password + kernel_image: linux-generic-lts-quantal + ntp_server: + - name: trusty + kernel: http://archive.ubuntu.com/ubuntu/dists/trusty-updates/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux + bootloader: http://archive.ubuntu.com/ubuntu/dists/trusty-updates/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz + root_password: password + kernel_image: + ntp_server: + servers: [] diff --git a/roles/pxe/handlers/main.yml b/roles/pxe/handlers/main.yml new file mode 100644 index 0000000..e346407 --- /dev/null +++ b/roles/pxe/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: restart dnsmasq + service: + name: dnsmasq + state: restarted + must_exist: false + +- name: reload ufw + ufw: + state: enabled diff --git a/roles/pxe/meta/main.yml b/roles/pxe/meta/main.yml new file mode 100644 index 0000000..f638eed --- /dev/null +++ b/roles/pxe/meta/main.yml @@ -0,0 +1,5 @@ +--- +dependencies: + - role: dnsmasq + when: pxe.enable_server|bool + - role: sensu-check diff --git a/roles/pxe/tasks/checks.yml b/roles/pxe/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/pxe/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/pxe/tasks/install.yml b/roles/pxe/tasks/install.yml new file mode 100644 index 0000000..2350cd1 --- /dev/null +++ b/roles/pxe/tasks/install.yml @@ -0,0 +1,50 @@ +--- +- name: create pxelinux.cfg directory + file: + path: "{{ pxe.tftpboot_path }}/pxelinux.cfg" + state: directory + mode: 0755 + +- name: create server preseeds directory + file: + path: "{{ pxe.tftpboot_path }}/servers" + state: directory + mode: 0755 + +- name: create default tftp boot file + template: + src: tftpboot/pxelinux.cfg/default + dest: "{{ pxe.tftpboot_path }}/pxelinux.cfg/default" + +- name: create per machine tftp boot files + template: + src: tftpboot/pxelinux.cfg/server + dest: "{{ pxe.tftpboot_path }}/pxelinux.cfg/01-{{ item.mac | replace(':','-') | lower }}" + with_items: "{{ pxe.servers }}" + +- name: create per machine preseeds + template: + src: "tftpboot/os/{{ item.preseed|default('default_preseed.cfg') }}" + dest: "{{ pxe.tftpboot_path }}/servers/{{ item.mac | replace(':','-') | lower }}.cfg" + with_items: "{{ pxe.servers }}" + +- name: create default post-install script + template: + src: tftpboot/post_install.sh + dest: "{{ pxe.tftpboot_path }}/servers/default_post.sh" + with_items: + - none + +- name: create per machine post-install scripts + template: + src: "tftpboot/post_install.sh" + dest: "{{ pxe.tftpboot_path }}/servers/{{ item.mac | replace(':','-') | lower }}_post.sh" + with_items: "{{ pxe.servers }}" + +- name: create tftp config + template: + src: etc/dnsmasq.d/pxeboot.conf + dest: /etc/dnsmasq.d/pxeboot.conf + +- command: /bin/true + notify: restart dnsmasq diff --git a/roles/pxe/tasks/main.yml b/roles/pxe/tasks/main.yml new file mode 100644 index 0000000..8b6e684 --- /dev/null +++ b/roles/pxe/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: create tftpboot directory + file: + path: "{{ pxe.tftpboot_path }}" + state: directory + owner: dnsmasq + mode: 0755 + +- include: server.yml + when: pxe.enable_server|bool + +- include: install.yml + when: pxe_files|bool + +- include: remove.yml + when: not pxe_files|bool + +- meta: flush_handlers + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/pxe/tasks/metrics.yml b/roles/pxe/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/pxe/tasks/remove.yml b/roles/pxe/tasks/remove.yml new file mode 100644 index 0000000..f160866 --- /dev/null +++ b/roles/pxe/tasks/remove.yml @@ -0,0 +1,18 @@ +--- +- name: remove pxelinux.cfg directory + file: + path: "{{ pxe.tftpboot_path }}/pxelinux.cfg" + state: absent + +- name: remove server preseeds directory + file: + path: "{{ pxe.tftpboot_path }}/servers" + state: absent + +- name: remove tftp config + file: + path: /etc/dnsmasq.d/pxeboot.conf + state: absent + +- command: /bin/true + notify: restart dnsmasq diff --git a/roles/pxe/tasks/server.yml b/roles/pxe/tasks/server.yml new file mode 100644 index 0000000..def0c60 --- /dev/null +++ b/roles/pxe/tasks/server.yml @@ -0,0 +1,105 @@ +--- +- name: install common pxe prereqs + apt: pkg={{ item }} state=installed + with_items: + - tftp + +- name: install 16.04 pxe prereqs + apt: pkg={{ item }} state=installed + with_items: + - pxelinux + when: ansible_distribution_version == "16.04" + +- name: install x86 pxe prereqs + apt: pkg={{ item }} state=installed + with_items: + - syslinux + - syslinux-common + when: + - ansible_architecture != "armv7l" + - ansible_architecture != "aarch64" + +- name: copy files to tftpboot + command: creates="{{ pxe.tftpboot_path }}/{{ item }}" cp /usr/lib/syslinux/{{ item }} {{ pxe.tftpboot_path }}/ + with_items: + - pxelinux.0 + - vesamenu.c32 + - memdisk + - reboot.c32 + - poweroff.com + +- name: create os tftp directories + file: + path: "{{ pxe.tftpboot_path }}/{{ item.name }}" + state: directory + mode: 0755 + with_items: "{{ pxe.os }}" + +- name: download kernel + get_url: url={{ item.kernel }} + dest={{ pxe.tftpboot_path }}/{{ item.name }}/linux mode=0644 + with_items: "{{ pxe.os }}" + when: proxy_env is not defined + +- name: download bootloader + get_url: url={{ item.bootloader }} + dest={{ pxe.tftpboot_path }}/{{ item.name }}/initrd.gz mode=0644 + with_items: "{{ pxe.os }}" + when: proxy_env is not defined + +- name: download kernel via proxy + get_url: url={{ item.kernel }} + dest={{ pxe.tftpboot_path }}/{{ item.name }}/linux mode=0644 + with_items: "{{ pxe.os }}" + environment: proxy_env + when: proxy_env is defined + +- name: download bootloader via proxy + get_url: url={{ item.bootloader }} + dest={{ pxe.tftpboot_path }}/{{ item.name }}/initrd.gz mode=0644 + with_items: "{{ pxe.os }}" + environment: proxy_env + when: proxy_env is defined + +- name: create os preseeds + template: + src: "tftpboot/os/{{ item.preseed|default('default_preseed.cfg') }}" + dest: "{{ pxe.tftpboot_path }}/{{ item.name }}/preseed.cfg" + with_items: "{{ pxe.os }}" + +- name: ensure dnsmasq service is running + service: name=dnsmasq state=started enabled=yes + +- name: allow ipv4 forwarding + command: echo 1 > /proc/sys/net/ipv4/ip_forward + when: pxe.nat.enabled|bool + +- name: Do not enforce forwarding rules with UFW + lineinfile: dest=/etc/default/ufw regexp="^DEFAULT_FORWARD_POLICY" + line="DEFAULT_FORWARD_POLICY=\"ACCEPT\"" + when: pxe.nat.enabled|bool + notify: reload ufw + tags: + - firewall + +# this is not idempotent, but is only expected to be needed on short lived +# mini bootstrappers +- name: enable NAT on correct interface + command: /sbin/iptables --table nat -A POSTROUTING -o {{ pxe.nat.interface_out }} -j MASQUERADE + when: pxe.nat.enabled|bool + +- name: enable forwarding on correct interface + command: /sbin/iptables --append FORWARD --in-interface {{ pxe.nat.interface_in }} -j ACCEPT + when: pxe.nat.enabled|bool + +- name: permit access to tftpd + ufw: rule=allow port=69 proto=udp + to_ip={{ pxe.tftp_server }} + tags: + - firewall + +- name: permit access to dhcp + ufw: rule=allow to_port=67 from_port=68 proto=udp + direction=in interface={{ pxe.pxe_interface }} + tags: + - firewall diff --git a/roles/pxe/tasks/serverspec.yml b/roles/pxe/tasks/serverspec.yml new file mode 100644 index 0000000..90264b4 --- /dev/null +++ b/roles/pxe/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for pxe role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/pxe/templates/etc/dnsmasq.d/pxeboot.conf b/roles/pxe/templates/etc/dnsmasq.d/pxeboot.conf new file mode 100644 index 0000000..6487223 --- /dev/null +++ b/roles/pxe/templates/etc/dnsmasq.d/pxeboot.conf @@ -0,0 +1,18 @@ +# {{ ansible_managed }} + +domain=bootstrap.local +dhcp-boot=pxelinux.0 +log-dhcp +enable-tftp +tftp-root={{ pxe.tftpboot_path }} + +{% if pxe.no_dhcp_interface %} +no-dhcp-interface={{ pxe.no_dhcp_interface }} +{% endif %} + +{% for network in pxe.dhcp_ranges -%} +dhcp-range=set:{{ network.tag }},{{ network.range }} +dhcp-option=tag:{{ network.tag }},3,{{ network.gateway }} +dhcp-option=tag:{{ network.tag }},6,{{ network.dns }} +dhcp-option=tag:{{ network.tag }},26,{{ network.mtu|default("1500") }} +{% endfor %} diff --git a/roles/pxe/templates/serverspec/pxe_spec.yml b/roles/pxe/templates/serverspec/pxe_spec.yml new file mode 100644 index 0000000..ca30cfb --- /dev/null +++ b/roles/pxe/templates/serverspec/pxe_spec.yml @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('dnsmasq') do + it { should be_installed } +end + +describe service('dnsmasq') do + it { should be_enabled } +end diff --git a/roles/pxe/templates/tftpboot/os/block_monitor_preseed.cfg b/roles/pxe/templates/tftpboot/os/block_monitor_preseed.cfg new file mode 100644 index 0000000..1d6bad9 --- /dev/null +++ b/roles/pxe/templates/tftpboot/os/block_monitor_preseed.cfg @@ -0,0 +1,133 @@ +# {{ ansible_managed }} + +d-i console-setup/ask_detect boolean false +d-i keyboard-configuration/layoutcode string us +d-i netcfg/choose_interface select auto +d-i netcfg/get_domain string example.com +d-i netcfg/no_default_route boolean true +d-i mirror/protocol string http +d-i mirror/country string manual +d-i mirror/http/hostname string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i mirror/http/directory string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i mirror/http/proxy string {{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +d-i mirror/https/proxy string {{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +d-i apt-setup/security_host string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i apt-setup/security_path string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i clock-setup/utc boolean true +d-i time/zone string UTC +d-i clock-setup/ntp boolean true +d-i clock-setup/ntp-server string {{ item.ntp_server|default(pxe.ntp_server) }} +d-i user-setup/encrypt-home boolean false +d-i partman-auto/disk string {{ item.partman_disk|default(pxe.partman_disk) }} +d-i partman-auto/method string lvm +d-i partman-auto/choose_recipe select disk-partitioned +d-i partman-lvm/device_remove_lvm boolean true +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm_nooverwrite boolean true +d-i partman/confirm_write_new_label boolean true +d-i partman-auto-lvm/new_vg_name string vg0 +d-i partman-auto-lvm/guided_size string max +d-i partman/choose_label string gpt +d-i partman/default_label string gpt +d-i partman-auto/expert_recipe string \ +disk-partitioned :: \ +1 1 1 free \ + $iflabel{ gpt } \ + method{ biosgrub } \ +. \ +1024 2048 2048 ext4 \ + $primary{ } \ + $bootable{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /boot } \ +. \ +50000 125000 125000 ext4 \ + $lvmok{ } \ + lv_name{ root } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ / } \ +. \ +20480 100000 100000 xfs \ + $lvmok{ } \ + lv_name{ ceph_monitor } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ xfs } \ + mountpoint{ /var/lib/ceph } \ +. \ +10240 50000 50000 ext4 \ + $lvmok{ } \ + lv_name{ tmp } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /tmp } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +10240 100000 100000 ext4 \ + $lvmok{ } \ + lv_name{ var_log } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /var/log } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +1024 2048 2048 linux-swap \ + method{ swap } \ + $defaultignore{ } \ + format{ } $lvmok{ } \ + lv_name{ swap } \ +. \ +512 1 -1 ext4 \ + format{ } \ + $lvmok{ } \ + $defaultignore{ } \ + lv_name{ deleteme } \ +. +d-i partman/choose_partition select Finish partitioning and write changes to disk +d-i partman/confirm boolean true +d-i partman-lvm/confirm boolean true +d-i partman/confirm_nooverwrite boolean true +d-i partman/choose_partition select finish +d-i passwd/root-login boolean true +d-i passwd/make-user boolean true +d-i passwd/root-password password {{ pxe.root_password|default('password') }} +d-i passwd/root-password-again password {{ pxe.root_password|default('password') }} +d-i passwd/user-fullname string Bluebox Admin +d-i passwd/username string blueboxadmin +d-i passwd/user-password password {{ pxe.blueboxadmin_password|default('password') }} +d-i passwd/user-password-again password {{ pxe.blueboxadmin_password|default('password') }} +d-i user-setup/allow-password-weak boolean true +d-i apt-setup/restricted boolean true +d-i pkgsel/include string curl {{ item.packages|default(pxe.packages)|join(" ") }} +tasksel tasksel/first multiselect ubuntu-server, standard +d-i pkgsel/install-language-support boolean false +d-i pkgsel/language-packs multiselect en +d-i pkgsel/update-policy select none +d-i grub-installer/only_debian boolean true +d-i grub-installer/with_other_os boolean true +d-i finish-install/keep-consoles boolean true +d-i finish-install/reboot_in_progress note +d-i base-installer/kernel/image string {{ item.kernel_image|default('') }} + +d-i preseed/late_command string \ +lvdisplay vg0/deleteme > /dev/null && lvremove -f vg0/deleteme > /dev/null; \ +in-target curl -o /root/post_install.sh tftp://{{pxe.tftp_server}}/servers/{{ item.mac | default('default') | replace(":","-") | lower }}_post.sh; \ +in-target /bin/bash /root/post_install.sh; \ +cp /target/etc/network/interfaces /etc/network/interfaces diff --git a/roles/pxe/templates/tftpboot/os/default_preseed.cfg b/roles/pxe/templates/tftpboot/os/default_preseed.cfg new file mode 100644 index 0000000..e860e05 --- /dev/null +++ b/roles/pxe/templates/tftpboot/os/default_preseed.cfg @@ -0,0 +1,124 @@ +# {{ ansible_managed }} + +d-i console-setup/ask_detect boolean false +d-i keyboard-configuration/layoutcode string us +d-i netcfg/choose_interface select auto +d-i netcfg/get_domain string example.com +d-i netcfg/no_default_route boolean true +d-i mirror/protocol string http +d-i mirror/country string manual +d-i mirror/http/hostname string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i mirror/http/directory string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i mirror/http/proxy string {{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +d-i apt-setup/security_host string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i apt-setup/security_path string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i clock-setup/utc boolean true +d-i time/zone string UTC +d-i clock-setup/ntp boolean true +d-i clock-setup/ntp-server string {{ item.ntp_server|default(pxe.ntp_server) }} +d-i user-setup/encrypt-home boolean false +d-i partman-auto/disk string {{ item.partman_disk|default(pxe.partman_disk) }} +d-i partman-auto/method string lvm +d-i partman-auto/choose_recipe select disk-partitioned +d-i partman-lvm/device_remove_lvm boolean true +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm_nooverwrite boolean true +d-i partman/confirm_write_new_label boolean true +d-i partman-auto-lvm/new_vg_name string vg0 +d-i partman-auto-lvm/guided_size string max +d-i partman/choose_label string gpt +d-i partman/default_label string gpt +d-i partman-auto/expert_recipe string \ +disk-partitioned :: \ +1 1 1 free \ + $iflabel{ gpt } \ + method{ biosgrub } \ +. \ +1024 1024 2048 ext4 \ + $primary{ } \ + $bootable{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /boot } \ +. \ +50000 300000 300000 ext4 \ + $lvmok{ } \ + lv_name{ root } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ / } \ +. \ +10240 50000 50000 ext4 \ + $lvmok{ } \ + lv_name{ tmp } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /tmp } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +10240 100000 100000 ext4 \ + $lvmok{ } \ + lv_name{ var_log } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /var/log } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +1024 2048 2048 linux-swap \ + method{ swap } \ + $defaultignore{ } \ + format{ } $lvmok{ } \ + lv_name{ swap } \ +. \ +512 1 -1 ext4 \ + format{ } \ + $lvmok{ } \ + $defaultignore{ } \ + lv_name{ deleteme } \ +. +d-i partman/choose_partition select Finish partitioning and write changes to disk +d-i partman/confirm boolean true +d-i partman-lvm/confirm boolean true +d-i partman/confirm_nooverwrite boolean true +d-i partman/choose_partition select finish +d-i passwd/root-login boolean true +d-i passwd/make-user boolean true +d-i passwd/root-password password {{ pxe.root_password|default('password') }} +d-i passwd/root-password-again password {{ pxe.root_password|default('password') }} +d-i passwd/user-fullname string Bluebox Admin +d-i passwd/username string blueboxadmin +d-i passwd/user-password password {{ pxe.blueboxadmin_password|default('password') }} +d-i passwd/user-password-again password {{ pxe.blueboxadmin_password|default('password') }} +d-i user-setup/allow-password-weak boolean true +d-i apt-setup/restricted boolean true +d-i pkgsel/include string curl {{ item.packages|default(pxe.packages)|join(" ") }} +tasksel tasksel/first multiselect ubuntu-server, standard +d-i pkgsel/install-language-support boolean false +d-i pkgsel/language-packs multiselect en +d-i pkgsel/update-policy select none +d-i grub-installer/bootdev string {{ item.partman_disk|default(pxe.partman_disk) }} +d-i grub-installer/only_debian boolean true +d-i grub-installer/with_other_os boolean true +d-i finish-install/keep-consoles boolean true +d-i finish-install/reboot_in_progress note +d-i base-installer/kernel/image string {{ item.kernel_image|default('') }} + +d-i preseed/late_command string \ +lvdisplay vg0/deleteme > /dev/null && lvremove -f vg0/deleteme > /dev/null; \ +in-target curl -o /root/post_install.sh tftp://{{pxe.tftp_server}}/servers/{{ item.mac | default('default') | replace(":","-") | lower }}_post.sh; \ +in-target /bin/bash /root/post_install.sh; \ +cp /target/etc/network/interfaces /etc/network/interfaces diff --git a/roles/pxe/templates/tftpboot/os/swift_preseed.cfg b/roles/pxe/templates/tftpboot/os/swift_preseed.cfg new file mode 100644 index 0000000..28bd261 --- /dev/null +++ b/roles/pxe/templates/tftpboot/os/swift_preseed.cfg @@ -0,0 +1,132 @@ +# {{ ansible_managed }} + +d-i console-setup/ask_detect boolean false +d-i keyboard-configuration/layoutcode string us +d-i netcfg/choose_interface select auto +d-i netcfg/get_domain string example.com +d-i netcfg/no_default_route boolean true +d-i mirror/protocol string http +d-i mirror/country string manual +d-i mirror/http/hostname string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i mirror/http/directory string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i mirror/http/proxy string {{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +d-i mirror/https/proxy string {{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +d-i apt-setup/security_host string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i apt-setup/security_path string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i clock-setup/utc boolean true +d-i time/zone string UTC +d-i clock-setup/ntp boolean true +d-i clock-setup/ntp-server string {{ item.ntp_server|default(pxe.ntp_server) }} +d-i user-setup/encrypt-home boolean false +d-i partman-auto/disk string {{ item.partman_disk|default(pxe.partman_disk) }} +d-i partman-auto/method string lvm +d-i partman-auto/choose_recipe select disk-partitioned +d-i partman-lvm/device_remove_lvm boolean true +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm_nooverwrite boolean true +d-i partman/confirm_write_new_label boolean true +d-i partman-auto-lvm/new_vg_name string vg0 +d-i partman-auto-lvm/guided_size string max +d-i partman/choose_label string gpt +d-i partman/default_label string gpt +d-i partman-auto/expert_recipe string \ +disk-partitioned :: \ +1 1 1 free \ + $iflabel{ gpt } \ + method{ biosgrub } \ +. \ +1024 4096 2048 ext4 \ + $primary{ } \ + $bootable{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /boot } \ +. \ +50000 125000 125000 ext4 \ + $lvmok{ } \ + lv_name{ root } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ / } \ +. \ +20480 204800 204800 xfs \ + $lvmok{ } \ + lv_name{ swift} \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ xfs } \ +. \ +10240 50000 50000 ext4 \ + $lvmok{ } \ + lv_name{ tmp } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /tmp } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +10240 100000 100000 ext4 \ + $lvmok{ } \ + lv_name{ var_log } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /var/log } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +1024 2048 2048 linux-swap \ + method{ swap } \ + $defaultignore{ } \ + format{ } $lvmok{ } \ + lv_name{ swap } \ +. \ +512 1 -1 ext4 \ + format{ } \ + $lvmok{ } \ + $defaultignore{ } \ + lv_name{ deleteme } \ +. +d-i partman/choose_partition select Finish partitioning and write changes to disk +d-i partman/confirm boolean true +d-i partman-lvm/confirm boolean true +d-i partman/confirm_nooverwrite boolean true +d-i partman/choose_partition select finish +d-i passwd/root-login boolean true +d-i passwd/make-user boolean true +d-i passwd/root-password password {{ pxe.root_password|default('password') }} +d-i passwd/root-password-again password {{ pxe.root_password|default('password') }} +d-i passwd/user-fullname string Bluebox Admin +d-i passwd/username string blueboxadmin +d-i passwd/user-password password {{ pxe.blueboxadmin_password|default('password') }} +d-i passwd/user-password-again password {{ pxe.blueboxadmin_password|default('password') }} +d-i user-setup/allow-password-weak boolean true +d-i apt-setup/restricted boolean true +d-i pkgsel/include string curl {{ item.packages|default(pxe.packages)|join(" ") }} +tasksel tasksel/first multiselect ubuntu-server, standard +d-i pkgsel/install-language-support boolean false +d-i pkgsel/language-packs multiselect en +d-i pkgsel/update-policy select none +d-i grub-installer/only_debian boolean true +d-i grub-installer/with_other_os boolean true +d-i finish-install/keep-consoles boolean true +d-i finish-install/reboot_in_progress note +d-i base-installer/kernel/image string {{ item.kernel_image|default('') }} + +d-i preseed/late_command string \ +lvdisplay vg0/deleteme > /dev/null && lvremove -f vg0/deleteme > /dev/null; \ +in-target curl -o /root/post_install.sh tftp://{{pxe.tftp_server}}/servers/{{ item.mac | default('default') | replace(":","-") | lower }}_post.sh; \ +in-target /bin/bash /root/post_install.sh; \ +cp /target/etc/network/interfaces /etc/network/interfaces diff --git a/roles/pxe/templates/tftpboot/os/vagrant_preseed.cfg b/roles/pxe/templates/tftpboot/os/vagrant_preseed.cfg new file mode 100644 index 0000000..f90be2b --- /dev/null +++ b/roles/pxe/templates/tftpboot/os/vagrant_preseed.cfg @@ -0,0 +1,124 @@ +# {{ ansible_managed }} + +d-i console-setup/ask_detect boolean false +d-i keyboard-configuration/layoutcode string us +d-i netcfg/choose_interface select auto +d-i netcfg/get_domain string example.com +d-i netcfg/no_default_route boolean true +d-i mirror/protocol string http +d-i mirror/country string manual +d-i mirror/http/hostname string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i mirror/http/directory string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i mirror/http/proxy string {{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +d-i mirror/https/proxy string {{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +d-i apt-setup/security_host string {{ item.mirror_http_hostname|default(pxe.mirror_http_hostname) }} +d-i apt-setup/security_path string {{ item.mirror_http_directory|default(pxe.mirror_http_directory) }} +d-i clock-setup/utc boolean true +d-i time/zone string UTC +d-i clock-setup/ntp boolean true +d-i clock-setup/ntp-server string {{ item.ntp_server|default(pxe.ntp_server) }} +d-i user-setup/encrypt-home boolean false +d-i partman-auto/disk string {{ item.partman_disk|default(pxe.partman_disk) }} +d-i partman-auto/method string lvm +d-i partman-auto/choose_recipe select disk-partitioned +d-i partman-lvm/device_remove_lvm boolean true +d-i partman-lvm/confirm boolean true +d-i partman-lvm/confirm_nooverwrite boolean true +d-i partman/confirm_write_new_label boolean true +d-i partman-auto-lvm/new_vg_name string vg0 +d-i partman-auto-lvm/guided_size string max +d-i partman/choose_label string gpt +d-i partman/default_label string gpt +d-i partman-auto/expert_recipe string \ +disk-partitioned :: \ +1 1 1 free \ + $iflabel{ gpt } \ + method{ biosgrub } \ +. \ +512 1024 1024 ext4 \ + $primary{ } \ + $bootable{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /boot } \ +. \ +5120 1024 10240 ext4 \ + $lvmok{ } \ + lv_name{ root } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ / } \ +. \ +512 1024 1024 ext4 \ + $lvmok{ } \ + lv_name{ tmp } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /tmp } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +512 1024 1024 ext4 \ + $lvmok{ } \ + lv_name{ var_log } \ + $defaultignore{ } \ + method{ format } \ + format{ } \ + use_filesystem{ } \ + filesystem{ ext4 } \ + mountpoint{ /var/log } \ + options/nosuid{ nosuid } \ + options/nodev{ nodev } \ + options/noexec{ noexec } \ +. \ +512 1024 1024 linux-swap \ + method{ swap } \ + $defaultignore{ } \ + format{ } $lvmok{ } \ + lv_name{ swap } \ +. \ +512 1 -1 ext4 \ + format{ } \ + $lvmok{ } \ + $defaultignore{ } \ + lv_name{ deleteme } \ +. +d-i partman/choose_partition select Finish partitioning and write changes to disk +d-i partman/confirm boolean true +d-i partman-lvm/confirm boolean true +d-i partman/confirm_nooverwrite boolean true +d-i partman/choose_partition select finish +d-i passwd/root-login boolean true +d-i passwd/make-user boolean true +d-i passwd/root-password password {{ pxe.root_password|default('password') }} +d-i passwd/root-password-again password {{ pxe.root_password|default('password') }} +d-i passwd/user-fullname string Bluebox Admin +d-i passwd/username string blueboxadmin +d-i passwd/user-password password {{ pxe.blueboxadmin_password|default('password') }} +d-i passwd/user-password-again password {{ pxe.blueboxadmin_password|default('password') }} +d-i user-setup/allow-password-weak boolean true +d-i apt-setup/restricted boolean true +d-i pkgsel/include string curl {{ item.packages|default(pxe.packages)|join(" ") }} +tasksel tasksel/first multiselect ubuntu-server, standard +d-i pkgsel/install-language-support boolean false +d-i pkgsel/language-packs multiselect en +d-i pkgsel/update-policy select none +d-i grub-installer/only_debian boolean true +d-i grub-installer/with_other_os boolean true +d-i finish-install/keep-consoles boolean true +d-i finish-install/reboot_in_progress note +d-i base-installer/kernel/image string {{ item.kernel_image|default('') }} + +d-i preseed/late_command string \ +lvdisplay vg0/deleteme > /dev/null && lvremove -f vg0/deleteme > /dev/null; \ +in-target curl -o /root/post_install.sh tftp://{{pxe.tftp_server}}/servers/{{ item.mac | default('default') | replace(":","-") | lower }}_post.sh; \ +in-target /bin/bash /root/post_install.sh; \ +cp /target/etc/network/interfaces /etc/network/interfaces diff --git a/roles/pxe/templates/tftpboot/post_install.sh b/roles/pxe/templates/tftpboot/post_install.sh new file mode 100644 index 0000000..8832ca2 --- /dev/null +++ b/roles/pxe/templates/tftpboot/post_install.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# +# {{ ansible_managed }} + +{% if item.mirror_http_proxy|default(pxe.mirror_http_proxy) %} +export http_proxy={{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +export https_proxy={{ item.mirror_http_proxy|default(pxe.mirror_http_proxy) }} +export no_proxy=127.0.0.1 +{% endif %} + +echo 'UseDNS no' >> /etc/ssh/sshd_config +echo 'PermitRootLogin no' >> /etc/ssh/sshd_config + +OUMASK=$( umask 0 ) +mkdir -p /home/blueboxadmin/.ssh +cat < /home/blueboxadmin/.ssh/authorized_keys +{{ pxe.ssh_pub_keys|join('\n') }} +EOF +chown -R blueboxadmin:blueboxadmin /home/blueboxadmin/.ssh +echo "blueboxadmin ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/blueboxadmin +umask ${OUMASK} + + +# strip out non-mirror sources +sed -i '/http:\/\/security\.ubuntu\.com/d' /etc/apt/sources.list + +echo "$(date): upgrading linux" > /root/post_install.log + +apt-get update >> /root/post_install.log +apt-get -y dist-upgrade >> /root/post_install.log + +{% if item.network is defined -%} +echo $(date): writing out network config >> /root/post_install.log +cat < /etc/network/interfaces +{{ item.network }} +EOF +{% else %} +echo $(date): using default network >> /root/post_install.log +{% endif -%} + +echo $(date): finished post-install >> /root/post_install.log + +APT_CONF="/etc/apt/apt.conf" +if [ -e "$APT_CONF" ]; then + rm -f $APT_CONF +fi diff --git a/roles/pxe/templates/tftpboot/pxelinux.cfg/default b/roles/pxe/templates/tftpboot/pxelinux.cfg/default new file mode 100644 index 0000000..0dfe557 --- /dev/null +++ b/roles/pxe/templates/tftpboot/pxelinux.cfg/default @@ -0,0 +1,57 @@ +# {{ ansible_managed }} + +{% macro os_menu_items() -%} +{% for OS in pxe.os -%} +label {{ OS.name }} + kernel {{ OS.name }}/linux + append initrd={{ OS.name }}/initrd.gz vga=normal ramdisk_size=16384 root=/dev/ram rw preseed/url=tftp://{{pxe.tftp_server}}/{{ OS.name }}/preseed.cfg preseed/interactive={{pxe.interactive}} debian-installer/locale=en_US keyboard-configuration/layoutcode=us localechooser/translation/warn-light=true localechooser/translation/warn-severe=true netcfg/choose_interface={{ pxe.ks_interface }} netcfg/get_hostname=ubuntu -- {% if pxe.serial_com is defined %} console=tty0 console=ttyS{{ pxe.serial_com }},115200n8{% endif %} consoleblank=0 -- + +{% endfor -%} +{% endmacro -%} + +serial {{ pxe.serial_com }} 115200 0 + +# PXE boot options +prompt 0 +default vesamenu.c32 +menu title Rebuild and diagnosis options +timeout 100 + +# Launch local installation +label local +menu label Start system from local storage +localboot 0 + +menu separator + +menu begin Operating systems +menu title Operating system options + +{{ os_menu_items() }} + +# Go back to the previous menu +menu separator +label exit +menu label <- Exit +menu goto .top +menu end + +menu begin Tools +menu title Tools + +# Local command - reboot +label reboot +menu label Reboot +com32 reboot.c32 + +# Local command - shutdown +label poweroff +menu label Power Off +comboot poweroff.com + +# Go back to the previous menu +menu separator +label exit +menu label <- Exit +menu goto .top +menu end diff --git a/roles/pxe/templates/tftpboot/pxelinux.cfg/server b/roles/pxe/templates/tftpboot/pxelinux.cfg/server new file mode 100644 index 0000000..719ac87 --- /dev/null +++ b/roles/pxe/templates/tftpboot/pxelinux.cfg/server @@ -0,0 +1,19 @@ +# {{ ansible_managed }} + +# PXE boot options +serial {{ item.serial_com|default(pxe.serial_com) }} 115200 0 +prompt 0 +default vesamenu.c32 +menu boot options for {{ item.name }} +timeout {{ item.timeout|default(pxe.timeout) }} + +label {{ item.os }}_{{ item.name }} +menu label install {{ item.os }} on {{ item.name }} + kernel {{ item.os }}/linux + ipappend 2 + append initrd={{ item.os }}/initrd.gz vga=normal ramdisk_size=16384 root=/dev/ram rw preseed/url=tftp://{{pxe.tftp_server}}/servers/{{ item.mac | replace(":","-") | lower }}.cfg preseed/interactive={{pxe.interactive}} debian-installer/locale=en_US keyboard-configuration/layoutcode=us localechooser/translation/warn-light=true localechooser/translation/warn-severe=true netcfg/choose_interface={{ item.ks_interface|default(pxe.ks_interface) }} biosdevname=0 netcfg/dhcp_timeout=180 netcfg/get_hostname={{ item.name }} -- {% if item.serial_com is defined or pxe.serial_com is defined %}console=tty0 serial console=ttyS{{ item.serial_com|default(pxe.serial_com) }},115200n8 {% endif %} consoleblank=0 -- + +# Launch local installation +label local +menu label Boot existing OS on {{ item.name }} +localboot 0 diff --git a/roles/pypi-mirror/README.md b/roles/pypi-mirror/README.md new file mode 100644 index 0000000..06cbd4e --- /dev/null +++ b/roles/pypi-mirror/README.md @@ -0,0 +1,28 @@ +# Pypi-mirror role + +## devpi + +PyPi Proxy/Mirror + +Apache is fronting it, if mirror already has a files Apache will serve it directly, if not, it will refer to pypi which will feed metadata and proxy/mirror the appropriate wheel from Pypi. + +Can also be used for private pip repos ... but not implemented via ansible yet. + +To use once set up do: + +### ~/.pip/pip.conf +``` +[global] +index-url = http://mirror01.local:81/root/pypi/+simple/ +``` + +### ~/.pydistutils.cfg +``` +[easy_install] +index_url = http://mirror01.local:81/root/pypi/+simple/ +``` + +## bandersnatch + +depreciated ... going away. + diff --git a/roles/pypi-mirror/defaults/main.yml b/roles/pypi-mirror/defaults/main.yml new file mode 100644 index 0000000..874c49e --- /dev/null +++ b/roles/pypi-mirror/defaults/main.yml @@ -0,0 +1,70 @@ +--- +pypi_mirror: + offline_mode: false + master_url: ~ + role: auto # default auto, master if others replicate from this pypi-mirror + virtualenv: '/opt/pypi_mirror/app' + mirror_location: '/opt/pypi_mirror/mirror' + htpasswd_location: '/opt/pypi_mirror/etc' + cron: true + ip: 127.0.0.1 + port: 4040 + root_password: lrvgfjrwlekgfejklfw22 + users: + - username: bluebox + password: test + repos: {} + # EXAMPLE + #bluebox_private: + # username: bluebox + # password: nopenopenope + # index: private + # mirror: + # url: https://:@packagecloud.io/blueboxcloud + # cache_expiry: 300 + #bluebox_openstack: + # username: bluebox + # index: openstack + # mirror: + # url: https://packagecloud.io/blueboxcloud + # cache_expiry: 300 + apache: + http_redirect: False + servername: mirror01.local + serveraliases: + - mirror01 + port: 80 + ip: '*' + ssl: + enabled: False + port: 443 + ip: '*' + servername: mirror01.local + serveraliases: + - mirror01 + name: sitecontroller + cert: ~ + key: ~ + intermediate: ~ + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/upstart/pypi_mirror.log + fields: + tags: mirror,devpi + - paths: + - /var/log/apache2/pypi_mirror-access.log + fields: + tags: mirror,apache_access + - paths: + - /var/log/apache2/pypi_mirror-error.log + fields: + tags: mirror,apache_error + + logging: + forwarder: filebeat diff --git a/roles/pypi-mirror/handlers/main.yml b/roles/pypi-mirror/handlers/main.yml new file mode 100644 index 0000000..b5c331f --- /dev/null +++ b/roles/pypi-mirror/handlers/main.yml @@ -0,0 +1,13 @@ +--- +- name: restart pypi_mirror server + service: name=pypi_mirror state=restarted + +- name: stop pypi_mirror server + service: name=pypi_mirror state=stopped + +- name: initialize pypi-mirror server + command: "{{ pypi_mirror.virtualenv }}/bin/devpi-server --serverdir {{ pypi_mirror.mirror_location }} --role {{ pypi_mirror.role }} {% if pypi_mirror.master_url %}--master {{ pypi_mirror.master_url }}{% endif %} --init --stop" + args: + creates: "{{ pypi_mirror.mirror_location }}/.serverversion" + become: true + become_user: mirror diff --git a/roles/pypi-mirror/meta/main.yml b/roles/pypi-mirror/meta/main.yml new file mode 100644 index 0000000..caff7c4 --- /dev/null +++ b/roles/pypi-mirror/meta/main.yml @@ -0,0 +1,17 @@ +--- +dependencies: + - role: bbg-ssl + name: "{{ pypi_mirror.apache.ssl.name }}" + ssl_cert: "{{ pypi_mirror.apache.ssl.cert }}" + ssl_key: "{{ pypi_mirror.apache.ssl.key }}" + ssl_intermediate: "{{ pypi_mirror.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: pypi_mirror.apache.ssl.enabled + tags: ['bbg-ssl'] + - role: apache + - role: logging-config + service: pypi_mirror + logdata: "{{ pypi_mirror.logs }}" + forward_type: "{{ pypi_mirror.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/pypi-mirror/tasks/checks.yml b/roles/pypi-mirror/tasks/checks.yml new file mode 100644 index 0000000..f08d169 --- /dev/null +++ b/roles/pypi-mirror/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install pypi_mirror process check + sensu_check_dict: name="check-pypi_mirror-process" check="{{ sensu_checks.pypi_mirror.check_pypi_mirror_process }}" + notify: restart sensu-client missing ok diff --git a/roles/pypi-mirror/tasks/main.yml b/roles/pypi-mirror/tasks/main.yml new file mode 100644 index 0000000..3b8786f --- /dev/null +++ b/roles/pypi-mirror/tasks/main.yml @@ -0,0 +1,97 @@ +--- +- name: create mirror user + user: name=mirror comment=mirror shell=/bin/false + system=yes home=/nonexistent + +- name: enable apache mods for pypi-mirror + apache2_module: name={{ item }} + with_items: + - proxy_http + - rewrite + - headers + +- name: install virtualenv + apt: pkg=python-virtualenv + +- name: install devpi + pip: name=devpi virtualenv="{{ pypi_mirror.virtualenv }}" + notify: + - initialize pypi-mirror server + +- name: devpi update-alternatives + alternatives: name=devpi + path={{ pypi_mirror.virtualenv }}/bin/devpi + link=/usr/local/bin/devpi + +- name: create pypi mirror location + file: name="{{ pypi_mirror.mirror_location }}" state=directory + owner=mirror + +- name: create pypi htpasswd location + file: name="{{ pypi_mirror.htpasswd_location }}" state=directory + owner=mirror + +- name: pypi_mirror service + upstart_service: name=pypi_mirror + cmd={{ pypi_mirror.virtualenv }}/bin/devpi-server + args="--port {{ pypi_mirror.port }} --host {{ pypi_mirror.ip }} --serverdir {{ pypi_mirror.mirror_location }} --restrict-modify=root --role {{ pypi_mirror.role }} {% if pypi_mirror.master_url %}--master {{ pypi_mirror.master_url }}{% endif %} {% if pypi_mirror.offline_mode|bool %}--offline-mode{% endif %}" + user=mirror + notify: restart pypi_mirror server + +- name: add pypy_mirror apache vhost + template: src=etc/apache2/sites-available/pypi_mirror + dest=/etc/apache2/sites-available/pypi_mirror.conf + notify: + - restart apache + +- name: enable pypy_mirror vhost + apache2_site: state=enabled name=pypi_mirror + notify: + - restart apache + +- meta: flush_handlers + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes + +- name: ensure pypi is running + service: name=pypi_mirror state=started enabled=yes + +- name: ensure devpi is available + wait_for: + port: 4040 + delay: 0 + timeout: 300 + +- name: wait for devpi replication to finish + uri: + url: "http://{{ pypi_mirror.ip }}:{{ pypi_mirror.port }}/+status" + method: GET + when: pypi_mirror.master_url + register: pypi_mirror_replication_status + until: pypi_mirror_replication_status.status == 200 and pypi_mirror_replication_status.json.result['serial'] == pypi_mirror_replication_status.json.result['master-serial'] + retries: 60 + delay: 10 + +- include: users.yml + +- name: allow apt-mirror traffic + ufw: rule=allow + to_port={{ item.port }} + src={{ item.src }} + proto={{ item.protocol }} + with_items: "{{ pypi_mirror.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/pypi-mirror/tasks/metrics.yml b/roles/pypi-mirror/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/pypi-mirror/tasks/serverspec.yml b/roles/pypi-mirror/tasks/serverspec.yml new file mode 100644 index 0000000..f88591c --- /dev/null +++ b/roles/pypi-mirror/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for pypi-mirror role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/pypi-mirror/tasks/users.yml b/roles/pypi-mirror/tasks/users.yml new file mode 100644 index 0000000..fd1e054 --- /dev/null +++ b/roles/pypi-mirror/tasks/users.yml @@ -0,0 +1,29 @@ +--- +- name: devpi use local server + command: "{{ pypi_mirror.virtualenv }}/bin/devpi use http://127.0.0.1:4040/root/pypi" + +- name: check if root password is set + command: "{{ pypi_mirror.virtualenv }}/bin/devpi login root --password ''" + register: devpi + failed_when: False + +- name: set root password + command: "{{ pypi_mirror.virtualenv }}/bin/devpi user -m root password='{{ pypi_mirror.root_password }}'" + when: devpi.rc == 0 + +- name: set protected repo users + htpasswd: name={{ item.value.username }} password={{ item.value.password }} + path={{ pypi_mirror.htpasswd_location }}/.htpasswd + when: item.value.password is defined + with_dict: "{{ pypi_mirror.repos }}" + +- name: create user management script + template: src=pypi_mirror/devpi_users.sh + dest=/root/devpi_users.sh + mode=0700 owner=root + +- name: run user management script + command: /root/devpi_users.sh + +- name: remove user management script + file: dest=/root/devpi_users.sh state=absent diff --git a/roles/pypi-mirror/templates/etc/apache2/sites-available/pypi_mirror b/roles/pypi-mirror/templates/etc/apache2/sites-available/pypi_mirror new file mode 100644 index 0000000..4199a05 --- /dev/null +++ b/roles/pypi-mirror/templates/etc/apache2/sites-available/pypi_mirror @@ -0,0 +1,53 @@ +# {{ ansible_managed }} + +{% macro virtualhost(servername) %} + ServerAdmin openstack@bluebox.net + ServerName {{ pypi_mirror.apache.servername }} + ServerAlias {{ pypi_mirror.apache.serveraliases|join(" ") }} + DocumentRoot {{ pypi_mirror.mirror_location}} + ErrorLog ${APACHE_LOG_DIR}/pypi_mirror-error.log + CustomLog ${APACHE_LOG_DIR}/pypi_mirror-access.log combined + FileETag MTime Size + Header set Cache-Control public + RequestHeader set X-outside-url %{REQUEST_SCHEME}s://%{HTTP_HOST}s + RequestHeader set X-Real-IP %{REMOTE_ADDR}s +{% for repo,location in pypi_mirror.repos.iteritems() %} +{% if location.password is defined %} + + AuthUserFile {{ pypi_mirror.htpasswd_location }}/.htpasswd + AuthType Basic + AuthName "Authentication Required" + Require valid-user + +{% endif %} +{% endfor %} + + ProxyPass http://localhost:4040/ + ProxyPassReverse http://localhost:4040/ + ProxyPreserveHost On + +{% endmacro %} + +{% if pypi_mirror.apache.ssl.enabled|bool and pypi_mirror.apache.http_redirect|bool %} + + RewriteEngine on + RewriteCond %{HTTPS} !=on + RewriteRule ^(.*)$ https://%{HTTP_HOST}:{{ pypi_mirror.apache.ssl.port }}$1 [R=301,L] + +{% else %} + + {{ virtualhost(pypi_mirror.apache.servername) }} + +{% endif %} + +{% if pypi_mirror.apache.ssl.enabled|bool %} + + {{ virtualhost(pypi_mirror.apache.ssl.servername) }} + {{ apache.ssl.settings }} + SSLCertificateFile /etc/ssl/certs/{{ pypi_mirror.apache.ssl.name|default('sitecontroller') }}.crt + SSLCertificateKeyFile /etc/ssl/private/{{ pypi_mirror.apache.ssl.name|default('sitecontroller') }}.key +{% if bbg_ssl.intermediate or pypi_mirror.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ pypi_mirror.apache.ssl.name|default('sitecontroller') }}-intermediate.crt +{% endif %} + +{% endif %} diff --git a/roles/pypi-mirror/templates/pypi_mirror/devpi_users.sh b/roles/pypi-mirror/templates/pypi_mirror/devpi_users.sh new file mode 100644 index 0000000..25a668d --- /dev/null +++ b/roles/pypi-mirror/templates/pypi_mirror/devpi_users.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# +# {{ ansible_managed }} + +DEVPI={{ pypi_mirror.virtualenv }}/bin/devpi + +# use localhost devpi +${DEVPI} use http://127.0.0.1:4040 + +# Login as root +${DEVPI} login root --password='{{ pypi_mirror.root_password }}' + +# modify users in an idempotent way +{% for user in pypi_mirror.users %} +{% if user.disabled|default("False")|bool %} +${DEVPI} user --delete {{ user.username }} +{% else %} +if ${DEVPI} user -l | egrep "^test$"; then + # ensure password is correct, not idempotent, but pretty safe. + ${DEVPI} user -m {{ user.username }} password='{{ user.password }}' +else + # create user with password + ${DEVPI} user -c {{ user.username }} password='{{ user.password }}' +fi +{% endif %} +{% endfor %} + +# Create user indexes if they do not exist +{% for repo,location in pypi_mirror.repos.iteritems() %} +if ! devpi use -l | grep -e '^{{ location.username }}/{{ location.index }}\s'; then + devpi index -c {{ location.username }}/{{ location.index }} type=mirror mirror_cache_expiry={{ location.mirror.cache_expiry }} mirror_url={{ location.mirror.url }}/{{ repo }}/pypi/simple +fi +{% endfor %} + +# logoff +${DEVPI} logoff diff --git a/roles/pypi-mirror/templates/serverspec/pypi_mirror_spec.rb b/roles/pypi-mirror/templates/serverspec/pypi_mirror_spec.rb new file mode 100644 index 0000000..36dd6be --- /dev/null +++ b/roles/pypi-mirror/templates/serverspec/pypi_mirror_spec.rb @@ -0,0 +1,56 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe user('mirror') do + it { should exist } + it { should have_home_directory '/nonexistent' } + it { should have_login_shell '/bin/false' } +end + +['proxy_http', 'rewrite', 'headers'].each do |file| + describe file("/etc/apache2/mods-available/#{file}.load") do + it { should exist } + end + describe file("/etc/apache2/mods-enabled/#{file}.load") do + it { should be_symlink } + end +end + +describe virtualenv('{{ pypi_mirror.virtualenv }}') do + it { should be_virtualenv } + its(:pip_freeze) { should include('devpi') } +end + +describe file("{{ pypi_mirror.mirror_location }}") do + it { should be_directory } + it { should be_owned_by 'mirror' } +end + +describe service('pypi_mirror') do + it { should be_enabled } +end + +describe port({{ pypi_mirror.port }}) do + it { should be_listening.on('{{ pypi_mirror.ip }}').with('tcp') } +end + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end + +describe port({{ pypi_mirror.apache.port }}) do + it { should be_listening } +end + +describe file('/etc/apache2/sites-available/pypi_mirror.conf') do + it { should be_file } +end + +describe file('/etc/apache2/sites-enabled/pypi_mirror.conf') do + it { should be_symlink } +end diff --git a/roles/rabbitmq/defaults/main.yml b/roles/rabbitmq/defaults/main.yml new file mode 100644 index 0000000..23d3234 --- /dev/null +++ b/roles/rabbitmq/defaults/main.yml @@ -0,0 +1,112 @@ +--- +rabbitmq: + apt: + rabbit_repo: 'http://www.rabbitmq.com/debian/' + rabbit_key: 'http://www.rabbitmq.com/rabbitmq-signing-key-public.asc' + erlang_repo: 'http://packages.erlang-solutions.com/debian' + erlang_key: 'http://packages.erlang-solutions.com/debian/erlang_solutions.asc' + cluster: False + erlang_cookie: 6IMgelGs3Ygu + users: + - username: admin + password: admin + vhost: / + - username: rabbit + password: rabbit + vhost: /rabbit + user: guest + password: guest + nodename: 'rabbit@{{ ansible_hostname }}' + ip: '0.0.0.0' + port: 5672 + firewall: + - port: 5671 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 5672 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + management_port: 15672 + nofile: 10240 + admin_cli_url: 'http://127.0.0.1:15672/cli/rabbitmqadmin' + plugins: + - rabbitmq_management + sensu_plugins: + - name: sensu-plugins-rabbitmq + version: 1.3.1 + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/rabbitmq/* + fields: + tags: rabbitmq + logging: + forwarder: filebeat + ssl: + enabled: True + verify: peer + peer_depth: 2 + no_peer_cert: "true" + ca_cert: | + -----BEGIN CERTIFICATE----- + MIICxDCCAaygAwIBAgIJAICPn230G+UBMA0GCSqGSIb3DQEBBQUAMBIxEDAOBgNV + BAMTB1NlbnN1Q0EwHhcNMTUwMTA1MjEzNjIyWhcNMjAwMTA0MjEzNjIyWjASMRAw + DgYDVQQDEwdTZW5zdUNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA + yflRHWCINGhh1+dRXTZ9lTAYEyPD953wyXh9MOmR1H5I9vi+So3MhkMUxfor2fXr + zSE89b/y5xh5p/ZPWBNPv9AiEh+YdoMxzPtDFVmk/EhzHlvQEnbCh8/E0+1fNK49 + VoORqgf0TV8dr5mpCAJ0tQWYi81zSCzRoLYlr3MkJ6/UtI3aL+r9wN+JmIJcWTk3 + rent86iqdlI2nof/ayMm9xrLTC8XRUNjCi9CBc0uYL7hWjPo7wkwd/zxhrd2tgAM + ASicsKuh1M0FegUcJZ26/O66MFoKJZtjotP9MWYj6FljrIDBpA4z6pjq0WqSsuQv + UjMCZAYA7lyJNuPd3k0IewIDAQABox0wGzAMBgNVHRMEBTADAQH/MAsGA1UdDwQE + AwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAX8TZupwRnQkDhV5+FElb+TVLnQNfwyXE + cuxBLcPwnQi+0NVZWc6C8I0eVUmF90AqfoIWiiSiMMvuVe+irqBlcrRsU6uNqU8x + Ql8dWVjcEMcW21oJydHbxQTuaUqLyjqPcZIVRwA0f2ZefGku6RE3PyZd58mRNGa2 + 3JGPxdRLuNAcUgz8gmOBro7a3xIN3RHISJAW1cHdcdmJzWuSkJzCiyac04AU7LwN + jrM71jQ17JwlkfgceZueclE+dwtDlhNRTgc/I6FnLZwDTufPiNyczsGfEX6TiP9c + uMqoFYgJqU159bJgNyFcmK73/bE4LSDWFi/cyXSFz0+L0vrDHV1LXA== + -----END CERTIFICATE----- + cert: | + -----BEGIN CERTIFICATE----- + MIIC3TCCAcWgAwIBAgIBATANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdTZW5z + dUNBMB4XDTE1MDEwNTIxMzYyMloXDTIwMDEwNDIxMzYyMlowITEOMAwGA1UEAwwF + c2Vuc3UxDzANBgNVBAoMBnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC + AQoCggEBANz5fCW/+b/wTFnxvtNeErWdec6wK32JLeoFKF7L2JhxxtmvfzyM467F + fJYTcrhM4FJi/r0KRK/AFX5j/AZ4mb5GMWGrjh8Z9fX6gctznAmvnGvIBohe0nFd + iNVhx/+fuhTf/BTWhpdRbjdq239FINyVZ2+yPRp0/TLHOnxwpONaxj4e7BfUu94B + NHn/aO7yQGsGUyCg7SpgQSE9lje3XhyIxYvoXVmGuxxzf+X6PIyt0lfgNcL/JYmr + cDmDh6B8pLypYUhovRJOc/+l29ggkn9GjyGwvTUuMk3N0bqhEOanlIstnw/ejBw9 + T8ZkDqtLQ1V8srR+8Q47PfnBz+JcSyMCAwEAAaMvMC0wCQYDVR0TBAIwADALBgNV + HQ8EBAMCBSAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQADggEB + ALWNVuVzIVxQzHjpqAapoqP8jxvYePFvlAdc+laEUdud/+4sP5fuXD+wddNMIcmM + a8xGyt7QLHxYctvKPvTUvNRNmDlfL7WyMe7VjM51352NJie0pjFGW16cq1stpF15 + jGatNPXGhfgatEbd/1UTF/h09p57LUk/HwU8z0nkkNmZ23RbEIqqj53zfLd875jm + NVevW6c3HCMz0gT06jEG/PZPqe2gS9QiCmsuCbaqZJPx4Ognc5Y686D8ynThROgY + PZU6KcOVtFAYyTqSAf9Lso6f1pfDi11hligHXvw+XN7uRN55WjuLgL9nR/dbz6Rh + zdU78dRXVkbG/GBIFTt7U80= + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpQIBAAKCAQEA3Pl8Jb/5v/BMWfG+014StZ15zrArfYkt6gUoXsvYmHHG2a9/ + PIzjrsV8lhNyuEzgUmL+vQpEr8AVfmP8BniZvkYxYauOHxn19fqBy3OcCa+ca8gG + iF7ScV2I1WHH/5+6FN/8FNaGl1FuN2rbf0Ug3JVnb7I9GnT9Msc6fHCk41rGPh7s + F9S73gE0ef9o7vJAawZTIKDtKmBBIT2WN7deHIjFi+hdWYa7HHN/5fo8jK3SV+A1 + wv8liatwOYOHoHykvKlhSGi9Ek5z/6Xb2CCSf0aPIbC9NS4yTc3RuqEQ5qeUiy2f + D96MHD1PxmQOq0tDVXyytH7xDjs9+cHP4lxLIwIDAQABAoIBAQDGPgB1sXvzCTbG + Po3u/GeWjZeYqQ3rSqVpXIUyPnirku9NnhdtEAy3X678yKfT1gwtfWiB2fNzJMVj + pXmVqJ5tSHXG5OzU1CqIKdxF1Qr42ZZgT4wkzcTINZyrqlSN7RM45RU28JrAqCpU + B0ayMCdiHZs1ZtdQpAH/iyLaPwh+eeeoHwbFNIWxeVfJM/Qce6CNrEnniT9k0cPS + e7HoVs5IBp2BJM53aroL6UlgiPWIJVsmmwRvgG0Kr3+FoOo9jlzwEZTlifRpnyqs + 5aGI9/ZpA8MvUgD/azqwp6va4J3G9clvCiGLn7SyNgbpsgQl3ndVpZp8IsxQHmoO + QUsZvrehAoGBAPJuYq4urK9n+3W7rapgcx06lAmePWIijko9TEXutKJiDl5ihp8i + Hk3tY/GamKNhUj8IZd/57Z41nu+3V3kU1cWdllmS+AUx1kfYbEFL/J/d8hUAQIHo + xBzRCiyoczxVECkF5QEsXowJiHYVJuXrfzlP3mvLmdAAOPD6zvvuf95TAoGBAOlX + qdHvvjEj3DXIUCmWZoYEfyjkuLAj7fwUVPx6I62c9knH+YDAuQKoX6Kp7MpPNb/+ + FfJPIvOP9YUkBW+5Dukx5jWDc46vNpOUnK38ZV3dBQ6lCCq/JgqwjgjmUoYhmM/F + PR6NwfspeJATAIiAQB1RofOHlo6l6GAo5zs8fiXxAoGACaPWDLMSbs8lsqLJ3xKC + wVDI/jDqo/JX0h/p3oYFbMeVZq9oRv+mUNb1uG+7ThPan2MIGgXoKvha35FcyxXH + Alqn1dwAPMSkjqrOczJAU2MR8z/VUNUepz6wtuHj4yxGvjrbxlYrVEo0wO1KZK4e + mvrQD11DOoAnw31VqbLsJr0CgYEAsrtiKZtAjHVSXKPVuzUnzKmvGQyw5sJurPKJ + 8mY/4+DhybvyQgvc6ss7jeYoqQIpQqmF9/dj5zoyrsvbmise38JH+l946ScOX9aq + eJ5mqpH8KK6CZfPKWM/Jy8lYFsOvQB5tZXThy9eFMJ5Nf0D1Wz/HFDmcOiGcr+NE + 0tvQKJECgYEAqwvyJkSVJ7Lp/G7/7pC+U+i6MYsg2YR1XRQgo94lEiorXthLpAT5 + Dvxd6LpDkJ4/OoofdJ2F9difig4Jf4m6ngrJADvXvBhFjXq0VWtBr1fLcJWTJ/PC + ZaGzorSqi2FDrD1TQ36u+OLGppYgVt5y5kJbiFhFNqh5dKlFtuz045w= + -----END RSA PRIVATE KEY----- diff --git a/roles/rabbitmq/handlers/main.yml b/roles/rabbitmq/handlers/main.yml new file mode 100644 index 0000000..439b1b5 --- /dev/null +++ b/roles/rabbitmq/handlers/main.yml @@ -0,0 +1,8 @@ +--- +- name: restart rabbitmq + service: name=rabbitmq-server state=restarted + +- name: login as rabbitmq to update limits + command: sudo -i -u rabbitmq + register: rabbitmq_login + failed_when: rabbitmq_login.rc != 1 diff --git a/roles/rabbitmq/meta/main.yml b/roles/rabbitmq/meta/main.yml new file mode 100644 index 0000000..4898bf7 --- /dev/null +++ b/roles/rabbitmq/meta/main.yml @@ -0,0 +1,14 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.erlang.repo }} {{ ansible_lsb.codename }} contrib' + key_url: '{{ apt_repos.erlang.key_url }}' + - repo: 'deb {{ apt_repos.rabbitmq.repo }} testing main' + key_url: '{{ apt_repos.rabbitmq.key_url }}' + - role: logging-config + service: rabbitmq + logdata: "{{ rabbitmq.logs }}" + forward_type: "{{ rabbitmq.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/rabbitmq/tasks/checks.yml b/roles/rabbitmq/tasks/checks.yml new file mode 100644 index 0000000..535b011 --- /dev/null +++ b/roles/rabbitmq/tasks/checks.yml @@ -0,0 +1,22 @@ +--- +- name: install rabbitmq-server process check + sensu_check_dict: name="check-rabbitmq-server-process" check="{{ sensu_checks.rabbitmq.check_rabbitmq_server_process }}" + notify: restart sensu-client missing ok + +- name: install sensu plugins + gem: + name: "{{ item.name }}" + version: "{{ item.version }}" + executable: /opt/sensu/embedded/bin/gem + user_install: no + with_items: "{{ rabbitmq.sensu_plugins }}" + register: result + until: result|succeeded + retries: 5 + +- name: install sensu checks + sensu_check_dict: name="{{ item.name }}" check="{{ item.check }}" + with_items: + - name: check-rabbitmq-messages + check: "{{ sensu_checks.rabbitmq.check_rabbitmq_messages }}" + notify: restart sensu-client missing ok diff --git a/roles/rabbitmq/tasks/cluster.yml b/roles/rabbitmq/tasks/cluster.yml new file mode 100644 index 0000000..a505403 --- /dev/null +++ b/roles/rabbitmq/tasks/cluster.yml @@ -0,0 +1,56 @@ +--- +# It is recommended that this role be played with serial set to 1 because +# There is a bug with initializing multiple nodes in the HA cluster at once +# http://rabbitmq.1065348.n5.nabble.com/Rabbitmq-boot-failure-with-quot-tables-not-present-quot-td24494.html + +- name: add rabbitmq erlang cookie + template: + src: var/lib/rabbitmq/erlang.cookie + dest: /var/lib/rabbitmq/.erlang.cookie + owner: rabbitmq + group: rabbitmq + mode: 0400 + register: erlang_cookie + +- name: add rabbitmq cluster configuration + template: + src: etc/rabbitmq/rabbitmq.config + dest: /etc/rabbitmq/rabbitmq.config + owner: rabbitmq + group: rabbitmq + mode: 0600 + register: cluster_configuration + +# When rabbitmq starts it creates '/var/lib/rabbitmq/mnesia'. This dir +# should be deleted before clustering is setup because it retains data that +# can conflict with the clustering information. +- name: remove mnesia configuration + file: path=/var/lib/rabbitmq/mnesia state=absent + when: erlang_cookie.changed or + cluster_configuration.changed + +- name: stop rabbit cluster + service: name=rabbitmq-server state=stopped + when: erlang_cookie.changed or + cluster_configuration.changed + + # In case there are lingering processes, ignore errors silently +- name: send sigterm to any running rabbitmq processes + command: killall -u rabbitmq + failed_when: false + when: erlang_cookie.changed or + cluster_configuration.changed + +- name: start rabbitmq + service: name=rabbitmq-server state=started enabled=yes + +- name: wait for rabbit to start + wait_for: port={{ rabbitmq.port }} host={{ rabbitmq.ip }} delay=2 + +- name: set the HA mirror queues policy + rabbitmq_policy: name=HA + node={{ rabbitmq.nodename }} + pattern='.*' + tags=ha-mode=all + +- meta: flush_handlers diff --git a/roles/rabbitmq/tasks/main.yml b/roles/rabbitmq/tasks/main.yml new file mode 100644 index 0000000..0d89a24 --- /dev/null +++ b/roles/rabbitmq/tasks/main.yml @@ -0,0 +1,164 @@ +--- +- name: add rabbit user + user: name=rabbitmq home=/var/lib/rabbitmq system=true shell=/bin/false comment=rabbitmq + +- name: rabbit directory + file: dest=/etc/rabbitmq + owner=rabbitmq group=rabbitmq mode=0755 + state=directory + +- name: rabbit log directory + file: dest=/var/log/rabbitmq + owner=rabbitmq group=rabbitmq mode=0755 + state=directory + +- name: install erlang-solutions erlang + apt: pkg=esl-erlang + +- name: install rabbitmq + apt: pkg=rabbitmq-server + +- name: add rabbitmq environment configuration + template: src=etc/rabbitmq/rabbitmq-env.conf + dest=/etc/rabbitmq/rabbitmq-env.conf + owner=rabbitmq group=rabbitmq mode=0600 + +- name: add rabbitmq security limits file + template: + src: etc/security/limits.d/10-rabbitmq.conf + dest: /etc/security/limits.d/10-rabbitmq.conf + owner: root + group: root + mode: 0644 + notify: + - login as rabbitmq to update limits + - restart rabbitmq + +- name: add rabbitmq defaults + template: + src: etc/default/rabbitmq-server + dest: /etc/default/rabbitmq-server + owner: root + group: root + mode: 0644 + notify: + - login as rabbitmq to update limits + - restart rabbitmq + +# Create the cluster if desired +- include: cluster.yml + when: rabbitmq.cluster + +- name: rabbit cert directory + file: dest=/etc/rabbitmq/ssl + owner=rabbitmq group=rabbitmq mode=0755 + state=directory + +- name: create CA cert + template: src=etc/rabbitmq/ssl/cacert.pem + dest=/etc/rabbitmq/ssl/cacert.pem + owner=rabbitmq group=rabbitmq mode=0600 + when: rabbitmq.ssl and rabbitmq.ssl.ca_cert is defined + notify: restart rabbitmq + +- name: create cert + template: src=etc/rabbitmq/ssl/cert.pem + dest=/etc/rabbitmq/ssl/cert.pem + owner=rabbitmq group=rabbitmq mode=0600 + when: rabbitmq.ssl + notify: restart rabbitmq + +- name: create key + template: src=etc/rabbitmq/ssl/key.pem + dest=/etc/rabbitmq/ssl/key.pem + owner=rabbitmq group=rabbitmq mode=0600 + when: rabbitmq.ssl + notify: restart rabbitmq + +- name: configure rabbitmq server + template: src=etc/rabbitmq/rabbitmq.config + dest=/etc/rabbitmq/rabbitmq.config + owner=rabbitmq group=rabbitmq mode=0600 + notify: restart rabbitmq + +- meta: flush_handlers + +- name: enable and start rabbitmq-server service + service: name=rabbitmq-server state=started enabled=yes + +- name: install rabbitmq plugins + rabbitmq_plugin: names={{ item }} state=enabled + with_items: "{{ rabbitmq.plugins }}" + +- name: install rabbitmqadmin + get_url: + url: "{{ rabbitmq.admin_cli_url }}" + dest: /usr/local/bin/rabbitmqadmin + +- name: correct rabbitmqadmin modes + file: group=root owner=root mode=0755 path=/usr/local/bin/rabbitmqadmin + +- name: rabbit vhost + rabbitmq_vhost: + name: "{{ item.vhost }}" + state: present + when: item.vhost is defined + with_items: "{{ rabbitmq.users }}" + +- name: rabbit user + rabbitmq_user: + user: "{{ item.username }}" + password: "{{ item.password }}" + vhost: "{{ item.vhost | default(omit) }}" + tags: administrator + configure_priv: .* + read_priv: .* + write_priv: .* + permissions: "{{ item.permissions | default(omit) }}" + state: present + when: not rabbitmq.cluster and + item.username is defined and item.password is defined + with_items: "{{ rabbitmq.users }}" + +- name: rabbit user clustered + rabbitmq_user: + user: "{{ item.username }}" + password: "{{ item.password }}" + node: "{{ rabbitmq.nodename }}" + vhost: "{{ item.vhost | default(omit) }}" + tags: administrator + configure_priv: .* + read_priv: .* + write_priv: .* + permissions: "{{ item.permissions | default(omit) }}" + state: present + when: rabbitmq.cluster and + item.username is defined and item.password is defined + with_items: "{{ rabbitmq.users }}" + +# Backward compatibility with existing configurations. +- name: remove default rabbit user + rabbitmq_user: user=guest state=absent + when: not rabbitmq.cluster + +- name: remove default rabbit user clustered + rabbitmq_user: user=guest state=absent node={{ rabbitmq.nodename }} + when: rabbitmq.cluster + +- name: rabbitmq firewall + ufw: rule=allow to_port={{ item.port }} proto=tcp src={{ item.src }} + with_items: "{{ rabbitmq.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/rabbitmq/tasks/metrics.yml b/roles/rabbitmq/tasks/metrics.yml new file mode 100644 index 0000000..89b4ac7 --- /dev/null +++ b/roles/rabbitmq/tasks/metrics.yml @@ -0,0 +1,12 @@ +--- +- name: install sensu metrics checks + sensu_metrics_check: + name: "{{ item.name }}" + plugin: "{{ item.plugin }}" + interval: "{{ item.interval|default(60) }}" + args: "{{ item.args|default('') }} --user {{ rabbitmq.users[0].username }} --password {{ rabbitmq.users[0].password }} --scheme {{ monitoring_common.graphite.host_prefix }}.rabbitmq" + service_owner: "{{ monitoring_common.service_owner }}" + with_items: + - name: metrics-rabbitmq-overview + plugin: metrics-rabbitmq-overview.rb + notify: restart sensu-client missing ok diff --git a/roles/rabbitmq/tasks/serverspec.yml b/roles/rabbitmq/tasks/serverspec.yml new file mode 100644 index 0000000..f11a7ac --- /dev/null +++ b/roles/rabbitmq/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests rabbitmq tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/rabbitmq/templates/etc/default/rabbitmq-server b/roles/rabbitmq/templates/etc/default/rabbitmq-server new file mode 100644 index 0000000..c80f06d --- /dev/null +++ b/roles/rabbitmq/templates/etc/default/rabbitmq-server @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +# This file is sourced by /etc/init.d/rabbitmq-server. Its primary +# reason for existing is to allow adjustment of system limits for the +# rabbitmq-server process. +# +# Maximum number of open file handles. This will need to be increased +# to handle many simultaneous connections. Refer to the system +# documentation for ulimit (in man bash) for more information. +# +#ulimit -n 1024 +ulimit -n {{ rabbitmq.nofile }} diff --git a/roles/rabbitmq/templates/etc/rabbitmq/rabbitmq-env.conf b/roles/rabbitmq/templates/etc/rabbitmq/rabbitmq-env.conf new file mode 100644 index 0000000..c5d9652 --- /dev/null +++ b/roles/rabbitmq/templates/etc/rabbitmq/rabbitmq-env.conf @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +NODE_PORT={{ rabbitmq.port }} +NODE_IP_ADDRESS={{ rabbitmq.ip }} +NODENAME={{ rabbitmq.nodename }} diff --git a/roles/rabbitmq/templates/etc/rabbitmq/rabbitmq.config b/roles/rabbitmq/templates/etc/rabbitmq/rabbitmq.config new file mode 100644 index 0000000..04ac4f7 --- /dev/null +++ b/roles/rabbitmq/templates/etc/rabbitmq/rabbitmq.config @@ -0,0 +1,40 @@ +{% macro rabbitmq_hosts() -%} +{% for host in groups['rabbitmq'] -%} + {% if loop.last -%} +'{{ rabbitmq.user }}@{{ hostvars[host]['ansible_hostname'] }}' + {%- else -%} +'{{ rabbitmq.user }}@{{ hostvars[host]['ansible_hostname'] }}', + {%- endif -%} +{% endfor -%} +{% endmacro -%} + +[ + {rabbit, [ + {default_user, <<"{{ rabbitmq.users[0].username }}">>}, + {default_pass, <<"{{ rabbitmq.users[0].password }}">>}, +{% if rabbitmq.cluster|bool %} + {cluster_nodes, {[{{ rabbitmq_hosts() }}], disc} }, +{% else %} + {cluster_nodes, {["{{ rabbitmq.user }}@{{ ansible_hostname }}"], disc} }, +{% endif %} +{% if rabbitmq.ssl.enabled|bool %} + {ssl, [{versions, ['tlsv1.2', 'tlsv1.1']}]}, + {ssl_listeners, [5671]}, + {ssl_options, [{cacertfile,"/etc/rabbitmq/ssl/cacert.pem"}, + {certfile,"/etc/rabbitmq/ssl/cert.pem"}, + {keyfile,"/etc/rabbitmq/ssl/key.pem"}, + {depth, {{ rabbitmq.ssl.peer_depth }}}, + {verify, verify_{{ rabbitmq.ssl.verify|lower}}}, + {fail_if_no_peer_cert, {{ rabbitmq.ssl.no_peer_cert|lower }}}]} +{% endif %} + ]}, + {rabbitmq_management, [ + {listener, [ + {port, {{ rabbitmq.management_port }}} + ]} + ]}, + {kernel, [ + {inet_dist_listen_min, 65535}, + {inet_dist_listen_max, 65535} + ]} +]. diff --git a/roles/rabbitmq/templates/etc/rabbitmq/ssl/cacert.pem b/roles/rabbitmq/templates/etc/rabbitmq/ssl/cacert.pem new file mode 100644 index 0000000..9725936 --- /dev/null +++ b/roles/rabbitmq/templates/etc/rabbitmq/ssl/cacert.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ rabbitmq.ssl.ca_cert }} diff --git a/roles/rabbitmq/templates/etc/rabbitmq/ssl/cert.pem b/roles/rabbitmq/templates/etc/rabbitmq/ssl/cert.pem new file mode 100644 index 0000000..adf7480 --- /dev/null +++ b/roles/rabbitmq/templates/etc/rabbitmq/ssl/cert.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ rabbitmq.ssl.cert }} diff --git a/roles/rabbitmq/templates/etc/rabbitmq/ssl/key.pem b/roles/rabbitmq/templates/etc/rabbitmq/ssl/key.pem new file mode 100644 index 0000000..188049c --- /dev/null +++ b/roles/rabbitmq/templates/etc/rabbitmq/ssl/key.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ rabbitmq.ssl.key }} diff --git a/roles/rabbitmq/templates/etc/security/limits.d/10-rabbitmq.conf b/roles/rabbitmq/templates/etc/security/limits.d/10-rabbitmq.conf new file mode 100644 index 0000000..690d21f --- /dev/null +++ b/roles/rabbitmq/templates/etc/security/limits.d/10-rabbitmq.conf @@ -0,0 +1,3 @@ +# {{ ansible_managed }} +rabbitmq hard nofile {{ rabbitmq.nofile }} +rabbitmq soft nofile {{ rabbitmq.nofile }} diff --git a/roles/rabbitmq/templates/serverspec/rabbitmq_spec.rb b/roles/rabbitmq/templates/serverspec/rabbitmq_spec.rb new file mode 100644 index 0000000..4d7b674 --- /dev/null +++ b/roles/rabbitmq/templates/serverspec/rabbitmq_spec.rb @@ -0,0 +1,54 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe user('rabbitmq') do + it { should exist } #RMQ001 + it { should belong_to_group 'rabbitmq' } #RMQ002 + it { should have_home_directory '/var/lib/rabbitmq' } #RMQ003 + it { should have_login_shell '/bin/false' } #RMQ004 +end + +describe file('/etc/rabbitmq/rabbitmq.config') do + it { should be_mode 600 } #RMQ005 + it { should be_owned_by 'rabbitmq' } #RMQ006 + it { should be_grouped_into 'rabbitmq' } #RMQ007 + it { should be_file } #RMQ008 + its(:content) { should_not contain /({default_user, <<"guest">>})/ } #RMQ009 + its(:content) { should_not contain /({default_pass, <<"\w{0,15}">>})$/ } #RMQ010 +end + +describe package('rabbitmq-server') do + it { should be_installed } +end + +describe service('rabbitmq-server') do + it { should be_enabled } +end + +{% if '{{ rabbit.ssl }}' %} +describe file('/etc/rabbitmq/ssl') do + it { should be_mode 755 } #RMQ011 + it { should be_owned_by 'rabbitmq' } #RMQ012 + it { should be_grouped_into 'rabbitmq' } #RMQ013 + it { should be_directory } #RMQ014 +end +describe file('/etc/rabbitmq/ssl/cacert.pem') do + it { should be_mode 600 } #RMQ015 + it { should be_owned_by 'rabbitmq' } #RMQ016 + it { should be_grouped_into 'rabbitmq' } #RMQ017 + it { should be_file } #RMQ018 +end +describe file('/etc/rabbitmq/ssl/cert.pem') do + it { should be_mode 600 } #RMQ019 + it { should be_owned_by 'rabbitmq' } #RMQ020 + it { should be_grouped_into 'rabbitmq' } #RMQ021 + it { should be_file } #RMQ022 +end +describe file('/etc/rabbitmq/ssl/key.pem') do + it { should be_mode 600 } #RMQ023 + it { should be_owned_by 'rabbitmq' } #RMQ024 + it { should be_grouped_into 'rabbitmq' } #RMQ025 + it { should be_file } #RMQ026 +end +{% endif %} diff --git a/roles/rabbitmq/templates/var/lib/rabbitmq/erlang.cookie b/roles/rabbitmq/templates/var/lib/rabbitmq/erlang.cookie new file mode 100644 index 0000000..ff79bef --- /dev/null +++ b/roles/rabbitmq/templates/var/lib/rabbitmq/erlang.cookie @@ -0,0 +1 @@ +{{ rabbitmq.erlang_cookie }} diff --git a/roles/rally/defaults/main.yml b/roles/rally/defaults/main.yml new file mode 100644 index 0000000..f503025 --- /dev/null +++ b/roles/rally/defaults/main.yml @@ -0,0 +1,5 @@ +--- +rally: + rev: 1c761cfbb4cf053219ed2d9b5046d996a1a0ddd9 + git_mirror: https://github.com/openstack + git_update: yes diff --git a/roles/rally/handlers/main.yml b/roles/rally/handlers/main.yml new file mode 100644 index 0000000..12a64e7 --- /dev/null +++ b/roles/rally/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: install rally venv + command: /opt/stack/rally/install_rally.sh --target /opt/bbc/rally diff --git a/roles/rally/meta/main.yml b/roles/rally/meta/main.yml new file mode 100644 index 0000000..280cb51 --- /dev/null +++ b/roles/rally/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: apache + - role: bbg-ssl diff --git a/roles/rally/tasks/checks.yml b/roles/rally/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/rally/tasks/main.yml b/roles/rally/tasks/main.yml new file mode 100644 index 0000000..5a87bce --- /dev/null +++ b/roles/rally/tasks/main.yml @@ -0,0 +1,66 @@ +--- +- name: install rally required packages + apt: name={{ item }} + with_items: + - build-essential + - libssl-dev + - libffi-dev + - python-dev + - libxml2-dev + - libxslt1-dev + - libpq-dev + +- name: get rally source repo + git: repo={{ rally.git_mirror }}/rally.git + dest=/opt/stack/rally + version={{ rally.rev }} + update={{ rally.git_update }} + register: result + until: result|success + retries: 3 + delay: 60 + notify: + - install rally venv + +- meta: flush_handlers + +- name: rally dirs + file: dest={{ item }} state=directory + with_items: + - /etc/rally + - /var/log/rally + - /opt/bbc/rally-tests + - /opt/bbc/rally-public + +- name: rally config + template: dest=/etc/rally/rally.conf src=etc/rally/rally.conf + + # use copy to keep ansible from trying to render the template +- name: rally tests + copy: src={{ item }} dest=/opt/bbc/rally-tests mode=0644 + with_fileglob: ../templates/opt/bbc/rally-tests/* + +- name: rally deployment definition example + template: src=etc/rally/rally-deployment-example.conf + dest=/etc/rally/rally-deployment-example.conf + +- name: add rally public apache vhost + template: src=etc/apache2/sites-available/rally.conf + dest=/etc/apache2/sites-available/rally.conf + +- name: enable rally vhost + apache2_site: state=enabled name=rally + notify: + - reload apache + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/rally/tasks/metrics.yml b/roles/rally/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/rally/tasks/serverspec.yml b/roles/rally/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/rally/templates/etc/apache2/sites-available/rally.conf b/roles/rally/templates/etc/apache2/sites-available/rally.conf new file mode 100644 index 0000000..c069d64 --- /dev/null +++ b/roles/rally/templates/etc/apache2/sites-available/rally.conf @@ -0,0 +1,13 @@ +# {{ ansible_managed }} + + + ServerAdmin admin@example.com + ServerName rally.openstack.blueboxgrid.com + DocumentRoot /opt/bbc/rally-public + ErrorLog ${APACHE_LOG_DIR}/rally.openstack.blueboxgrid.com.error.log + CustomLog ${APACHE_LOG_DIR}/rally.openstack.blueboxgrid.com.access.log combined + + AllowOverride None + Require all granted + + diff --git a/roles/rally/templates/etc/rally/rally-deployment-example.conf b/roles/rally/templates/etc/rally/rally-deployment-example.conf new file mode 100644 index 0000000..09a180c --- /dev/null +++ b/roles/rally/templates/etc/rally/rally-deployment-example.conf @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +{ + "type": "ExistingCloud", + "auth_url": "auth_uri", + "admin": { + "username": "provider_admin", + "password": "provider_admin_password", + "tenant_name": admin + } +} diff --git a/roles/rally/templates/etc/rally/rally.conf b/roles/rally/templates/etc/rally/rally.conf new file mode 100644 index 0000000..0be5204 --- /dev/null +++ b/roles/rally/templates/etc/rally/rally.conf @@ -0,0 +1,590 @@ +# {{ ansible_managed }} + +[DEFAULT] + +# +# From oslo.log +# + +# Print debugging output (set logging level to DEBUG instead of +# default WARNING level). (boolean value) +#debug = false + +# Print more verbose output (set logging level to INFO instead of +# default WARNING level). (boolean value) +#verbose = false + +# The name of a logging configuration file. This file is appended to +# any existing logging configuration files. For details about logging +# configuration files, see the Python logging module documentation. +# (string value) +# Deprecated group/name - [DEFAULT]/log_config +#log_config_append = + +# DEPRECATED. A logging.Formatter log message format string which may +# use any of the available logging.LogRecord attributes. This option +# is deprecated. Please use logging_context_format_string and +# logging_default_format_string instead. (string value) +#log_format = + +# Format string for %%(asctime)s in log records. Default: %(default)s +# . (string value) +#log_date_format = %Y-%m-%d %H:%M:%S + +# (Optional) Name of log file to output to. If no default is set, +# logging will go to stdout. (string value) +# Deprecated group/name - [DEFAULT]/logfile +#log_file = + +# (Optional) The base directory used for relative --log-file paths. +# (string value) +# Deprecated group/name - [DEFAULT]/logdir +#log_dir = + +# Use syslog for logging. Existing syslog format is DEPRECATED during +# I, and will change in J to honor RFC5424. (boolean value) +#use_syslog = false + +# (Optional) Enables or disables syslog rfc5424 format for logging. If +# enabled, prefixes the MSG part of the syslog message with APP-NAME +# (RFC5424). The format without the APP-NAME is deprecated in K, and +# will be removed in L, along with this option. (boolean value) +#use_syslog_rfc_format = true + +# Syslog facility to receive log lines. (string value) +#syslog_log_facility = LOG_USER + +# Log output to standard error. (boolean value) +#use_stderr = true + +# Format string to use for log messages with context. (string value) +#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s + +# Format string to use for log messages without context. (string +# value) +#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s + +# Data to append to log format when level is DEBUG. (string value) +#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d + +# Prefix each line of exception output with this format. (string +# value) +#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s + +# List of logger=LEVEL pairs. (list value) +#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN + +# Enables or disables publication of error events. (boolean value) +#publish_errors = false + +# Enables or disables fatal status of deprecations. (boolean value) +#fatal_deprecations = false + +# The format for an instance that is passed with the log message. +# (string value) +#instance_format = "[instance: %(uuid)s] " + +# The format for an instance UUID that is passed with the log message. +# (string value) +#instance_uuid_format = "[instance: %(uuid)s] " + +# +# From rally +# + +# Print debugging output only for Rally. Off-site components stay +# quiet. (boolean value) +#rally_debug = false + +# make exception message format errors fatal (boolean value) +#fatal_exception_format_errors = false + +# HTTP timeout for any of OpenStack service in seconds (floating point +# value) +#openstack_client_http_timeout = 180.0 + +# Use SSL for all OpenStack API interfaces (boolean value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +#https_insecure = false + +# Path to CA server cetrificate for SSL (string value) +# This option is deprecated for removal. +# Its value may be silently ignored in the future. +https_cacert = /usr/local/share/ca-certificates/bbg/bbg-root-ca.crt + + +[benchmark] + +# +# From rally +# + +# Time to sleep after creating a resource before polling for it status +# (floating point value) +#cinder_volume_create_prepoll_delay = 2.0 + +# Time to wait for cinder volume to be created. (floating point value) +#cinder_volume_create_timeout = 600.0 + +# Interval between checks when waiting for volume creation. (floating +# point value) +#cinder_volume_create_poll_interval = 2.0 + +# Time to wait for cinder volume to be deleted. (floating point value) +#cinder_volume_delete_timeout = 600.0 + +# Interval between checks when waiting for volume deletion. (floating +# point value) +#cinder_volume_delete_poll_interval = 2.0 + +# Time to sleep after creating a resource before polling for it status +# (floating point value) +#glance_image_create_prepoll_delay = 2.0 + +# Time to wait for glance image to be created. (floating point value) +#glance_image_create_timeout = 120.0 + +# Interval between checks when waiting for image creation. (floating +# point value) +#glance_image_create_poll_interval = 1.0 + +# Time to wait for glance image to be deleted. (floating point value) +#glance_image_delete_timeout = 120.0 + +# Interval between checks when waiting for image deletion. (floating +# point value) +#glance_image_delete_poll_interval = 1.0 + +# Time(in sec) to sleep after creating a resource before polling for +# it status. (floating point value) +#heat_stack_create_prepoll_delay = 2.0 + +# Time(in sec) to wait for heat stack to be created. (floating point +# value) +#heat_stack_create_timeout = 3600.0 + +# Time interval(in sec) between checks when waiting for stack +# creation. (floating point value) +#heat_stack_create_poll_interval = 1.0 + +# Time(in sec) to wait for heat stack to be deleted. (floating point +# value) +#heat_stack_delete_timeout = 3600.0 + +# Time interval(in sec) between checks when waiting for stack +# deletion. (floating point value) +#heat_stack_delete_poll_interval = 1.0 + +# Time(in sec) to wait for stack to be checked. (floating point value) +#heat_stack_check_timeout = 3600.0 + +# Time interval(in sec) between checks when waiting for stack +# checking. (floating point value) +#heat_stack_check_poll_interval = 1.0 + +# Time(in sec) to sleep after updating a resource before polling for +# it status. (floating point value) +#heat_stack_update_prepoll_delay = 2.0 + +# Time(in sec) to wait for stack to be updated. (floating point value) +#heat_stack_update_timeout = 3600.0 + +# Time interval(in sec) between checks when waiting for stack update. +# (floating point value) +#heat_stack_update_poll_interval = 1.0 + +# Time(in sec) to wait for stack to be suspended. (floating point +# value) +#heat_stack_suspend_timeout = 3600.0 + +# Time interval(in sec) between checks when waiting for stack suspend. +# (floating point value) +#heat_stack_suspend_poll_interval = 1.0 + +# Time(in sec) to wait for stack to be resumed. (floating point value) +#heat_stack_resume_timeout = 3600.0 + +# Time interval(in sec) between checks when waiting for stack resume. +# (floating point value) +#heat_stack_resume_poll_interval = 1.0 + +# Time to sleep after start before polling for status (floating point +# value) +#nova_server_start_prepoll_delay = 0.0 + +# Server start timeout (floating point value) +#nova_server_start_timeout = 300.0 + +# Server start poll interval (floating point value) +#nova_server_start_poll_interval = 1.0 + +# Time to sleep after stop before polling for status (floating point +# value) +#nova_server_stop_prepoll_delay = 0.0 + +# Server stop timeout (floating point value) +#nova_server_stop_timeout = 300.0 + +# Server stop poll interval (floating point value) +#nova_server_stop_poll_interval = 2.0 + +# Time to sleep after boot before polling for status (floating point +# value) +#nova_server_boot_prepoll_delay = 1.0 + +# Server boot timeout (floating point value) +#nova_server_boot_timeout = 300.0 + +# Server boot poll interval (floating point value) +#nova_server_boot_poll_interval = 1.0 + +# Time to sleep after delete before polling for status (floating point +# value) +#nova_server_delete_prepoll_delay = 2.0 + +# Server delete timeout (floating point value) +#nova_server_delete_timeout = 300.0 + +# Server delete poll interval (floating point value) +#nova_server_delete_poll_interval = 2.0 + +# Time to sleep after reboot before polling for status (floating point +# value) +#nova_server_reboot_prepoll_delay = 2.0 + +# Server reboot timeout (floating point value) +#nova_server_reboot_timeout = 300.0 + +# Server reboot poll interval (floating point value) +#nova_server_reboot_poll_interval = 2.0 + +# Time to sleep after rebuild before polling for status (floating +# point value) +#nova_server_rebuild_prepoll_delay = 1.0 + +# Server rebuild timeout (floating point value) +#nova_server_rebuild_timeout = 300.0 + +# Server rebuild poll interval (floating point value) +#nova_server_rebuild_poll_interval = 1.0 + +# Time to sleep after rescue before polling for status (floating point +# value) +#nova_server_rescue_prepoll_delay = 2.0 + +# Server rescue timeout (floating point value) +#nova_server_rescue_timeout = 300.0 + +# Server rescue poll interval (floating point value) +#nova_server_rescue_poll_interval = 2.0 + +# Time to sleep after unrescue before polling for status (floating +# point value) +#nova_server_unrescue_prepoll_delay = 2.0 + +# Server unrescue timeout (floating point value) +#nova_server_unrescue_timeout = 300.0 + +# Server unrescue poll interval (floating point value) +#nova_server_unrescue_poll_interval = 2.0 + +# Time to sleep after suspend before polling for status (floating +# point value) +#nova_server_suspend_prepoll_delay = 2.0 + +# Server suspend timeout (floating point value) +#nova_server_suspend_timeout = 300.0 + +# Server suspend poll interval (floating point value) +#nova_server_suspend_poll_interval = 2.0 + +# Time to sleep after resume before polling for status (floating point +# value) +#nova_server_resume_prepoll_delay = 2.0 + +# Server resume timeout (floating point value) +#nova_server_resume_timeout = 300.0 + +# Server resume poll interval (floating point value) +#nova_server_resume_poll_interval = 2.0 + +# Time to sleep after pause before polling for status (floating point +# value) +#nova_server_pause_prepoll_delay = 2.0 + +# Server pause timeout (floating point value) +#nova_server_pause_timeout = 300.0 + +# Server pause poll interval (floating point value) +#nova_server_pause_poll_interval = 2.0 + +# Time to sleep after unpause before polling for status (floating +# point value) +#nova_server_unpause_prepoll_delay = 2.0 + +# Server unpause timeout (floating point value) +#nova_server_unpause_timeout = 300.0 + +# Server unpause poll interval (floating point value) +#nova_server_unpause_poll_interval = 2.0 + +# Time to sleep after shelve before polling for status (floating point +# value) +#nova_server_shelve_prepoll_delay = 2.0 + +# Server shelve timeout (floating point value) +#nova_server_shelve_timeout = 300.0 + +# Server shelve poll interval (floating point value) +#nova_server_shelve_poll_interval = 2.0 + +# Time to sleep after unshelve before polling for status (floating +# point value) +#nova_server_unshelve_prepoll_delay = 2.0 + +# Server unshelve timeout (floating point value) +#nova_server_unshelve_timeout = 300.0 + +# Server unshelve poll interval (floating point value) +#nova_server_unshelve_poll_interval = 2.0 + +# Time to sleep after image_create before polling for status (floating +# point value) +#nova_server_image_create_prepoll_delay = 0.0 + +# Server image_create timeout (floating point value) +#nova_server_image_create_timeout = 300.0 + +# Server image_create poll interval (floating point value) +#nova_server_image_create_poll_interval = 2.0 + +# Time to sleep after image_delete before polling for status (floating +# point value) +#nova_server_image_delete_prepoll_delay = 0.0 + +# Server image_delete timeout (floating point value) +#nova_server_image_delete_timeout = 300.0 + +# Server image_delete poll interval (floating point value) +#nova_server_image_delete_poll_interval = 2.0 + +# Time to sleep after resize before polling for status (floating point +# value) +#nova_server_resize_prepoll_delay = 2.0 + +# Server resize timeout (floating point value) +#nova_server_resize_timeout = 400.0 + +# Server resize poll interval (floating point value) +#nova_server_resize_poll_interval = 5.0 + +# Time to sleep after resize_confirm before polling for status +# (floating point value) +#nova_server_resize_confirm_prepoll_delay = 0.0 + +# Server resize_confirm timeout (floating point value) +#nova_server_resize_confirm_timeout = 200.0 + +# Server resize_confirm poll interval (floating point value) +#nova_server_resize_confirm_poll_interval = 2.0 + +# Time to sleep after resize_revert before polling for status +# (floating point value) +#nova_server_resize_revert_prepoll_delay = 0.0 + +# Server resize_revert timeout (floating point value) +#nova_server_resize_revert_timeout = 200.0 + +# Server resize_revert poll interval (floating point value) +#nova_server_resize_revert_poll_interval = 2.0 + +# Time to sleep after live_migrate before polling for status (floating +# point value) +#nova_server_live_migrate_prepoll_delay = 1.0 + +# Server live_migrate timeout (floating point value) +#nova_server_live_migrate_timeout = 400.0 + +# Server live_migrate poll interval (floating point value) +#nova_server_live_migrate_poll_interval = 2.0 + +# Time to sleep after migrate before polling for status (floating +# point value) +#nova_server_migrate_prepoll_delay = 1.0 + +# Server migrate timeout (floating point value) +#nova_server_migrate_timeout = 400.0 + +# Server migrate poll interval (floating point value) +#nova_server_migrate_poll_interval = 2.0 + +# Nova volume detach timeout (floating point value) +#nova_detach_volume_timeout = 200.0 + +# Nova volume detach poll interval (floating point value) +#nova_detach_volume_poll_interval = 2.0 + +# A timeout in seconds for a cluster create operation (integer value) +#cluster_create_timeout = 1800 + +# A timeout in seconds for a cluster delete operation (integer value) +#cluster_delete_timeout = 900 + +# Cluster status polling interval in seconds (integer value) +#cluster_check_interval = 5 + +# A timeout in seconds for a Job Execution to complete (integer value) +#job_execution_timeout = 600 + +# Job Execution status polling interval in seconds (integer value) +#job_check_interval = 5 + +# Time to sleep after boot before polling for status (floating point +# value) +#ec2_server_boot_prepoll_delay = 1.0 + +# Server boot timeout (floating point value) +#ec2_server_boot_timeout = 300.0 + +# Server boot poll interval (floating point value) +#ec2_server_boot_poll_interval = 1.0 + + +[database] + +# +# From oslo.db +# + +# The file name to use with SQLite. (string value) +# Deprecated group/name - [DEFAULT]/sqlite_db +#sqlite_db = oslo.sqlite + +# If True, SQLite uses synchronous mode. (boolean value) +# Deprecated group/name - [DEFAULT]/sqlite_synchronous +#sqlite_synchronous = true + +# The back end to use for the database. (string value) +# Deprecated group/name - [DEFAULT]/db_backend +#backend = sqlalchemy + +# The SQLAlchemy connection string to use to connect to the database. +# (string value) +# Deprecated group/name - [DEFAULT]/sql_connection +# Deprecated group/name - [DATABASE]/sql_connection +# Deprecated group/name - [sql]/connection +#connection = + +# The SQLAlchemy connection string to use to connect to the slave +# database. (string value) +#slave_connection = + +# The SQL mode to be used for MySQL sessions. This option, including +# the default, overrides any server-set SQL mode. To use whatever SQL +# mode is set by the server configuration, set this to no value. +# Example: mysql_sql_mode= (string value) +#mysql_sql_mode = TRADITIONAL + +# Timeout before idle SQL connections are reaped. (integer value) +# Deprecated group/name - [DEFAULT]/sql_idle_timeout +# Deprecated group/name - [DATABASE]/sql_idle_timeout +# Deprecated group/name - [sql]/idle_timeout +#idle_timeout = 3600 + +# Minimum number of SQL connections to keep open in a pool. (integer +# value) +# Deprecated group/name - [DEFAULT]/sql_min_pool_size +# Deprecated group/name - [DATABASE]/sql_min_pool_size +#min_pool_size = 1 + +# Maximum number of SQL connections to keep open in a pool. (integer +# value) +# Deprecated group/name - [DEFAULT]/sql_max_pool_size +# Deprecated group/name - [DATABASE]/sql_max_pool_size +#max_pool_size = + +# Maximum number of database connection retries during startup. Set to +# -1 to specify an infinite retry count. (integer value) +# Deprecated group/name - [DEFAULT]/sql_max_retries +# Deprecated group/name - [DATABASE]/sql_max_retries +#max_retries = 10 + +# Interval between retries of opening a SQL connection. (integer +# value) +# Deprecated group/name - [DEFAULT]/sql_retry_interval +# Deprecated group/name - [DATABASE]/reconnect_interval +#retry_interval = 10 + +# If set, use this value for max_overflow with SQLAlchemy. (integer +# value) +# Deprecated group/name - [DEFAULT]/sql_max_overflow +# Deprecated group/name - [DATABASE]/sqlalchemy_max_overflow +#max_overflow = + +# Verbosity of SQL debugging information: 0=None, 100=Everything. +# (integer value) +# Deprecated group/name - [DEFAULT]/sql_connection_debug +#connection_debug = 0 + +# Add Python stack traces to SQL as comment strings. (boolean value) +# Deprecated group/name - [DEFAULT]/sql_connection_trace +#connection_trace = false + +# If set, use this value for pool_timeout with SQLAlchemy. (integer +# value) +# Deprecated group/name - [DATABASE]/sqlalchemy_pool_timeout +#pool_timeout = + +# Enable the experimental use of database reconnect on connection +# lost. (boolean value) +#use_db_reconnect = false + +# Seconds between retries of a database transaction. (integer value) +#db_retry_interval = 1 + +# If True, increases the interval between retries of a database +# operation up to db_max_retry_interval. (boolean value) +#db_inc_retry_interval = true + +# If db_inc_retry_interval is set, the maximum seconds between retries +# of a database operation. (integer value) +#db_max_retry_interval = 10 + +# Maximum retries in case of connection error or deadlock error before +# error is raised. Set to -1 to specify an infinite retry count. +# (integer value) +#db_max_retries = 20 + + +[image] + +# +# From rally +# + +# Version of cirros image (string value) +#cirros_version = 0.3.2 + +# Cirros image name (string value) +#cirros_image = cirros-0.3.2-x86_64-disk.img + +# Cirros image base URL (string value) +#cirros_base_url = http://download.cirros-cloud.net + + +[users_context] + +# +# From rally +# + +# How many concurrent threads use for serving users context (integer +# value) +#resource_management_workers = 30 + +# ID of domain in which projects will be created. (string value) +#project_domain = default + +# ID of domain in which users will be created. (string value) +#user_domain = default diff --git a/roles/rally/templates/opt/bbc/rally-tests/bbc-cloud-validate.yml b/roles/rally/templates/opt/bbc/rally-tests/bbc-cloud-validate.yml new file mode 100644 index 0000000..ff9afb6 --- /dev/null +++ b/roles/rally/templates/opt/bbc/rally-tests/bbc-cloud-validate.yml @@ -0,0 +1,451 @@ +# {{ ansible_managed }} + +--- + KeystoneBasic.create_delete_user: + - + args: + name_length: 10 + runner: + type: "constant" + times: 100 + concurrency: 20 + sla: + failure_rate: + max: 5 + + KeystoneBasic.create_and_delete_role: + - + runner: + type: "constant" + times: 100 + concurrency: 20 + sla: + failure_rate: + max: 5 + + KeystoneBasic.create_update_and_delete_tenant: + - + args: + name_length: 10 + runner: + type: "constant" + times: 100 + concurrency: 20 + sla: + failure_rate: + max: 5 + + Authenticate.validate_cinder: + - + args: + repetitions: 2 + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 3 + users_per_tenant: 5 + sla: + failure_rate: + max: 5 + + Authenticate.validate_glance: + - + args: + repetitions: 2 + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 3 + users_per_tenant: 5 + sla: + failure_rate: + max: 5 + + Authenticate.validate_neutron: + - + args: + repetitions: 2 + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 3 + users_per_tenant: 5 + sla: + failure_rate: + max: 5 + + Authenticate.validate_nova: + - + args: + repetitions: 2 + runner: + type: "constant" + times: 50 + concurrency: 10 + context: + users: + tenants: 3 + users_per_tenant: 5 + sla: + failure_rate: + max: 5 + + GlanceImages.create_and_delete_image: + - + args: + image_location: "http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-disk.img" + container_format: "bare" + disk_format: "qcow2" + runner: + type: "constant" + times: 50 + concurrency: 5 + context: + users: + tenants: 2 + users_per_tenant: 3 + sla: + failure_rate: + max: 5 + + NeutronNetworks.create_and_delete_networks: + - + args: + network_create_args: {} + runner: + type: "constant" + times: 20 + concurrency: 1 + context: + users: + tenants: 1 + users_per_tenant: 1 + quotas: + neutron: + network: -1 + sla: + failure_rate: + max: 5 + + NeutronNetworks.create_and_delete_ports: + - + args: + network_create_args: {} + port_create_args: {} + ports_per_network: 5 + runner: + type: "constant" + times: 100 + concurrency: 5 + context: + users: + tenants: 1 + users_per_tenant: 1 + quotas: + neutron: + network: -1 + port: -1 + sla: + failure_rate: + max: 5 + + NeutronNetworks.create_and_delete_subnets: + - + args: + network_create_args: {} + subnet_create_args: {} + subnet_cidr_start: "1.1.0.0/30" + subnets_per_network: 2 + runner: + type: "constant" + times: 20 + concurrency: 1 + context: + users: + tenants: 1 + users_per_tenant: 1 + quotas: + neutron: + network: -1 + subnet: -1 + sla: + failure_rate: + max: 5 + + CinderVolumes.create_and_delete_volume: + - + args: + size: 1 + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + quotas: + cinder: + volumes: -1 + sla: + failure_rate: + max: 5 + + CinderVolumes.create_and_delete_snapshot: + - + args: + force: false + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + volumes: + size: 1 + sla: + failure_rate: + max: 5 + + CinderVolumes.create_from_volume_and_delete_volume: + - + args: + size: 1 + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + volumes: + size: 1 + sla: + failure_rate: + max: 5 + + NovaServers.boot_and_delete_server: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + force_delete: false + nics: + - { "net-id": "{{ net_id }}" } + runner: + type: "constant" + times: 100 + concurrency: 12 + context: + users: + tenants: 3 + users_per_tenant: 2 + quotas: + nova: + instances: -1 + cores: -1 + ram: -1 + sla: + failure_rate: + max: 5 + + NovaServers.boot_server_from_volume_and_delete: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + volume_size: 10 + force_delete: false + nics: + - { "net-id": "{{ net_id }}" } + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + quotas: + cinder: + volumes: -1 + sla: + failure_rate: + max: 10 + + NovaServers.suspend_and_resume_server: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + force_delete: false + nics: + - { "net-id": {{ net_id }} } + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + sla: + failure_rate: + max: 5 + + NovaServers.snapshot_server: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + force_delete: false + nics: + - { "net-id": "{{ net_id }}" } + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + sla: + failure_rate: + max: 5 + + NovaServers.resize_server: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + to_flavor: + name: "m1.small" + confirm: true + force_delete: false + nics: + - { "net-id": "{{ net_id }}" } + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + sla: + failure_rate: + max: 5 + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + to_flavor: + name: "m1.small" + confirm: false + force_delete: false + nics: + - { "net-id": "{{ net_id }}" } + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + sla: + failure_rate: + max: 5 + + NovaServers.boot_and_list_server: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + detailed: True + force_delete: false + nics: + - { "net-id": "{{ net_id }}" } + runner: + type: "constant" + times: {{ vcpu_limit }} + concurrency: 12 + context: + users: + tenants: 3 + users_per_tenant: 2 + quotas: + nova: + instances: -1 + cores: -1 + ram: -1 + sla: + failure_rate: + max: 5 + + NovaKeypair.create_and_delete_keypair: + - + runner: + type: "constant" + times: 20 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + sla: + failure_rate: + max: 5 + + VMTasks.boot_runcommand_delete: + - + args: + flavor: + name: "m1.tiny" + image: + name: {{ image_name | default("cirros") }} + nics: + - { "net-id": "{{ net_id }}" } + floating_network: "external" + force_delete: false + volume_args: + size: 1 + script: "rally/samples/tasks/support/instance_dd_test.sh" + interpreter: "/bin/sh" + username: "cirros" + runner: + type: "constant" + times: 50 + concurrency: 5 + context: + users: + tenants: 3 + users_per_tenant: 2 + sla: + failure_rate: + max: 10 diff --git a/roles/redis/defaults/main.yml b/roles/redis/defaults/main.yml new file mode 100644 index 0000000..34f295a --- /dev/null +++ b/roles/redis/defaults/main.yml @@ -0,0 +1,10 @@ +--- +redis: + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/redis/redis-server.log + fields: + tags: redis + logging: + forwarder: filebeat diff --git a/roles/redis/handlers/main.yml b/roles/redis/handlers/main.yml new file mode 100644 index 0000000..f840fea --- /dev/null +++ b/roles/redis/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: stop redis-server + service: name=redis-server state=stopped + +- name: restart redis-server + service: name=redis-server state=restarted diff --git a/roles/redis/meta/main.yml b/roles/redis/meta/main.yml new file mode 100644 index 0000000..ec1fc37 --- /dev/null +++ b/roles/redis/meta/main.yml @@ -0,0 +1,8 @@ +--- +dependencies: + - role: logging-config + service: redis + logdata: "{{ redis.logs }}" + forward_type: "{{ redis.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/redis/tasks/checks.yml b/roles/redis/tasks/checks.yml new file mode 100644 index 0000000..74e1725 --- /dev/null +++ b/roles/redis/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: RED011 install redis-server process check + sensu_check_dict: name="check-redis-server-process" check="{{ sensu_checks.redis.check_redis_server_process }}" + notify: restart sensu-client missing ok diff --git a/roles/redis/tasks/main.yml b/roles/redis/tasks/main.yml new file mode 100644 index 0000000..6a9ca9c --- /dev/null +++ b/roles/redis/tasks/main.yml @@ -0,0 +1,45 @@ +--- +- name: stop redis-server + service: name=redis-server state=stopped must_exist=false + +- name: create redis user + user: name=redis groups=adm home=/usr/share/redis + comment=redis shell=/bin/false + system=yes + notify: + - restart redis-server + +- name: redis log directory + file: dest=/var/log/redis + owner=redis group=adm mode=0775 + state=directory + notify: + - restart redis-server + +- name: fix redis service + template: src=etc/init.d/redis-server dest=/etc/init.d/redis-server + mode=0755 + notify: + - restart redis-server + +- name: install redis-server package + apt: pkg=redis-server + notify: + - restart redis-server + +- meta: flush_handlers + +- name: start redis-server + service: name=redis-server state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/redis/tasks/metrics.yml b/roles/redis/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/redis/tasks/serverspec.yml b/roles/redis/tasks/serverspec.yml new file mode 100644 index 0000000..a4fd890 --- /dev/null +++ b/roles/redis/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests redis tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/redis/templates/etc/init.d/redis-server b/roles/redis/templates/etc/init.d/redis-server new file mode 100644 index 0000000..265847a --- /dev/null +++ b/roles/redis/templates/etc/init.d/redis-server @@ -0,0 +1,91 @@ +# {{ ansible_managed }} + +#! /bin/sh +### BEGIN INIT INFO +# Provides: redis-server +# Required-Start: $syslog $remote_fs +# Required-Stop: $syslog $remote_fs +# Should-Start: $local_fs +# Should-Stop: $local_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: redis-server - Persistent key-value db +# Description: redis-server - Persistent key-value db +### END INIT INFO + + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +DAEMON=/usr/bin/redis-server +DAEMON_ARGS=/etc/redis/redis.conf +NAME=redis-server +DESC=redis-server + +RUNDIR=/var/run/redis +PIDFILE=$RUNDIR/redis-server.pid + +test -x $DAEMON || exit 0 + +if [ -r /etc/default/$NAME ] +then + . /etc/default/$NAME +fi + +. /lib/lsb/init-functions + +set -e + +case "$1" in + start) + echo -n "Starting $DESC: " + mkdir -p $RUNDIR + touch $PIDFILE + chown redis:adm $RUNDIR $PIDFILE + chmod 755 $RUNDIR + + if [ -n "$ULIMIT" ] + then + ulimit -n $ULIMIT + fi + + if start-stop-daemon --start --quiet --umask 007 --pidfile $PIDFILE --chuid redis:adm --exec $DAEMON -- $DAEMON_ARGS + then + echo "$NAME." + else + echo "failed" + fi + ;; + stop) + echo -n "Stopping $DESC: " + if start-stop-daemon --stop --retry forever/TERM/1 --quiet --oknodo --pidfile $PIDFILE --exec $DAEMON + then + echo "$NAME." + else + echo "failed" + fi + rm -f $PIDFILE + sleep 1 + ;; + + restart|force-reload) + ${0} stop + ${0} start + ;; + + status) + echo -n "$DESC is " + if start-stop-daemon --stop --quiet --signal 0 --name ${NAME} --pidfile ${PIDFILE} + then + echo "running" + else + echo "not running" + exit 1 + fi + ;; + + *) + echo "Usage: /etc/init.d/$NAME {start|stop|restart|force-reload|status}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/roles/redis/templates/serverspec/redis_spec.rb b/roles/redis/templates/serverspec/redis_spec.rb new file mode 100644 index 0000000..ee6b061 --- /dev/null +++ b/roles/redis/templates/serverspec/redis_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe user('redis') do + it { should exist } #RED001 + it { should belong_to_group 'adm' } #RED002 + it { should have_home_directory '/usr/share/redis' } #RED003 + it { should have_login_shell '/bin/false' } #RED004 +end + +describe file('/var/log/redis') do + it { should be_mode 775 } #RED005 + it { should be_owned_by 'redis' } #RED006 + it { should be_directory } #RED007 +end + +describe file('/etc/init.d/redis-server') do + it { should be_mode 755 } #RED008 + it { should be_file } #RED009 +end + +describe package('redis-server') do + it { should be_installed.by('apt') } #RED010 +end + +describe service('redis-server') do + it { should be_enabled } +end diff --git a/roles/runtime/java/defaults/main.yml b/roles/runtime/java/defaults/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/java/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/java/handlers/main.yml b/roles/runtime/java/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/java/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/java/meta/main.yml b/roles/runtime/java/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/java/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/java/tasks/checks.yml b/roles/runtime/java/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/runtime/java/tasks/main.yml b/roles/runtime/java/tasks/main.yml new file mode 100644 index 0000000..4514e6e --- /dev/null +++ b/roles/runtime/java/tasks/main.yml @@ -0,0 +1,50 @@ +--- +- name: install java + apt: + name: openjdk-8-jre + when: ansible_distribution_version == "16.04" + register: result + until: result|succeeded + retries: 5 + +- block: # when trusty + - apt_repository: + repo: 'ppa:webupd8team/java' + + - name: install debconf so we can sign oracle license + apt: + name: software-properties-common + register: result + until: result|succeeded + retries: 5 + + - name: sign oracle java license agreement + debconf: + name: 'oracle-java8-installer' + question: 'shared/accepted-oracle-license-v1-1' + value: 'true' + vtype: 'select' + + - name: install java + apt: + name: "{{ item }}" + with_items: + - oracle-java8-installer + - ca-certificates + - oracle-java8-set-default + register: result + until: result|succeeded + retries: 5 + when: ansible_distribution_version == "14.04" + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/runtime/java/tasks/metrics.yml b/roles/runtime/java/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/runtime/java/tasks/serverspec.yml b/roles/runtime/java/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/runtime/python/defaults/main.yml b/roles/runtime/python/defaults/main.yml new file mode 100644 index 0000000..da9d15e --- /dev/null +++ b/roles/runtime/python/defaults/main.yml @@ -0,0 +1,4 @@ +--- +python: + pip_version: 9.0.1 + setuptools_version: 36.0.1 diff --git a/roles/runtime/python/handlers/main.yml b/roles/runtime/python/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/python/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/python/meta/main.yml b/roles/runtime/python/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/python/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/python/tasks/main.yml b/roles/runtime/python/tasks/main.yml new file mode 100644 index 0000000..10d0974 --- /dev/null +++ b/roles/runtime/python/tasks/main.yml @@ -0,0 +1,21 @@ +--- +- name: install python pip system package + apt: + name: python-pip + register: result + until: result|succeeded + retries: 5 + +- name: update pip + command: "pip install --upgrade pip=={{ python.pip_version }}" + register: result + until: result|succeeded + retries: 5 + changed_when: "result.stdout.find('Successfully installed') and result.rc == 0" + +- name: update setuptools + command: "pip install --upgrade setuptools=={{ python.setuptools_version }}" + register: result + until: result|succeeded + retries: 5 + changed_when: "result.stdout.find('Successfully installed') and result.rc == 0" diff --git a/roles/runtime/ruby/defaults/main.yml b/roles/runtime/ruby/defaults/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/ruby/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/ruby/handlers/main.yml b/roles/runtime/ruby/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/ruby/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/ruby/meta/main.yml b/roles/runtime/ruby/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/runtime/ruby/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/runtime/ruby/tasks/main.yml b/roles/runtime/ruby/tasks/main.yml new file mode 100644 index 0000000..fc64c89 --- /dev/null +++ b/roles/runtime/ruby/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: set 14.04 ruby version + set_fact: ruby_version=2.0 + when: ansible_distribution_version == "14.04" + +- name: set 16.04 ruby version + set_fact: ruby_version=2.3 + when: ansible_distribution_version == "16.04" + +- name: install ruby packages + apt: pkg={{ item }} + with_items: + - ruby{{ ruby_version }} + - ruby{{ ruby_version }}-dev + register: result + until: result|succeeded + retries: 5 + +- name: install ruby gems + gem: name={{ item }} user_install=no + with_items: + - rake + register: result + until: result|succeeded + retries: 5 diff --git a/roles/security/defaults/main.yml b/roles/security/defaults/main.yml new file mode 100644 index 0000000..f2f1934 --- /dev/null +++ b/roles/security/defaults/main.yml @@ -0,0 +1,11 @@ +--- +security: + always_update: + apt: + - bash + - openssl + - libssl1.0.0 + - libc6 + - openssh-server + - openssh-client + - ntp diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/security/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/security/meta/main.yml b/roles/security/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/security/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/security/tasks/checks.yml b/roles/security/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml new file mode 100644 index 0000000..4bcb764 --- /dev/null +++ b/roles/security/tasks/main.yml @@ -0,0 +1,19 @@ +--- +- name: always update these packages + apt: pkg={{ item }} update_cache=yes state=latest + register: result + until: result|succeeded + retries: 5 + with_items: "{{ security.always_update.apt }}" + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/security/tasks/metrics.yml b/roles/security/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/security/tasks/serverspec.yml b/roles/security/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/sensu-check/defaults/main.yml b/roles/sensu-check/defaults/main.yml new file mode 100644 index 0000000..acfa9c7 --- /dev/null +++ b/roles/sensu-check/defaults/main.yml @@ -0,0 +1,368 @@ +--- +# These are all known checks and are made available to +# roles by including it as a meta dependency. +# Remember that dict merging is enabled when modifying +# values of them in var overrides. + +# When adding extra checks to it .. the second level of +# the dict should be named for the role that uses it and +# the third level should be the check name. + +sensu: + client: + enable_checks: true + enable_metrics: true + +monitoring_common: + # Should be the same as a name listed in: sensu.handlers.pagerduty.api_keys + service_owner: default + checks: + memory: + warning: 4096 + critical: 1024 + raid: + severity: "critical" + graphite: + cluster_prefix: "stats.sc.{{ datacenter|replace('.', '_') }}" + host_prefix: "stats.sc.{{ datacenter|replace('.', '_') }}.{{ ansible_nodename|replace('.', '_') }}" + +sensu_checks: + apache: + check_apache_process: + handler: default + notification: "unexpected number of apache2 processes" + interval: 120 + standalone: true + command: "check-procs.rb -p apache2 -w 50 -c 100 -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + collectd: + check_collectd_process: + handler: default + notification: "unexpected number of collectd processes" + interval: 120 + standalone: true + command: "check-procs.rb -p collectd -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + common: + check_sshd_process: + handler: default + notification: "unexpected number of sshd processes" + interval: 120 + standalone: true + command: "check-procs.rb -p sshd -w 50 -c 100 -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + check_ntp_process: + handler: default + notification: "unexpected number of ntp processes" + interval: 120 + standalone: true + command: "check-procs.rb -p ntp -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + check_disk_space: + handler: default + interval: 120 + standalone: true + command: "check-disk.rb -w 85 -c 95" + service_owner: "{{ monitoring_common.service_owner }}" + + consul: + check_consul_process: + handler: default + notification: "unexpected number of consul processes" + interval: 120 + standalone: true + command: "check-procs.rb -p consul -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + dnsmasq: + check_dnsmasq_process: + handler: default + notification: "unexpected number of dnsmasq processes" + interval: 120 + standalone: true + command: "check-procs.rb -p dnsmasq -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + docker: + check_docker_process: + handler: default + notification: "unexpected number of docker processes" + interval: 120 + standalone: true + command: "check-procs.rb -p docker -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + elasticsearch: + check_elasticsearch_process: + handler: default + notification: "unexpected number of elasticsearch processes" + interval: 120 + standalone: true + command: "check-procs.rb -p elasticsearch -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + check_es_cluster_status: + handler: default + interval: 120 + standalone: true + command: "check-es-cluster-status.rb --master-only" + service_owner: "{{ monitoring_common.service_owner }}" + check_es_node_status: + handler: default + interval: 120 + standalone: true + command: "check-es-node-status.rb" + service_owner: "{{ monitoring_common.service_owner }}" + check_es_file_descriptors: + handler: default + interval: 120 + standalone: true + command: "check-es-file-descriptors.rb -w 70 -c 80" + service_owner: "{{ monitoring_common.service_owner }}" + check_es_heap: + handler: default + interval: 120 + standalone: true + command: "check-es-heap.rb -w 85 -c 90 -P" + service_owner: "{{ monitoring_common.service_owner }}" + check_es_circuit_breakers: + handler: default + interval: 120 + standalone: true + command: "check-es-circuit-breakers.rb --localhost" + service_owner: "{{ monitoring_common.service_owner }}" + + flapjack: + check_flapjack_process: + handler: default + notification: "unexpected number of flapjack processes" + interval: 120 + standalone: true + command: "check-procs.rb -p flapjack -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + check_flapjack_httpbroker_process: + handler: default + notification: "unexpected number of flapjack-httpbroker processes" + interval: 120 + standalone: true + command: "check-procs.rb -p flapjack-httpbroker -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + gem_mirror: + check_gem_mirror_process: + handler: default + notification: "unexpected number of gem_mirror processes" + interval: 120 + standalone: true + command: "check-procs.rb -p gem_mirror -W 17 -C 17" + service_owner: "{{ monitoring_common.service_owner }}" + + grafana: + check_grafana_process: + handler: default + notification: "unexpected number of grafana processes" + interval: 120 + standalone: true + command: "check-procs.rb -p grafana-server -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + graphite: + check_carbon_cache_process: + handler: default + notification: "unexpected number of carbon-cache processes" + interval: 120 + standalone: true + command: "check-procs.rb -p carbon-cache -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + ipsec: + check_ipsec_process: + handler: default + notification: "unexpected number of ipsec processes" + interval: 120 + standalone: true + command: "check-procs.rb -p ipsec -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + jenkins: + check_jenkins_process: + handler: default + notification: "unexpected number of jenkins processes" + interval: 120 + standalone: true + command: "check-procs.rb -p jenkins -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + kibana: + check_kibana_process: + handler: default + notification: "unexpected number of kibana processes" + interval: 120 + standalone: true + command: "check-procs.rb -p kibana -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + logging: + check_logstash_forwarder_process: + handler: default + notification: "unexpected number of logstash-forwarder processes" + interval: 120 + standalone: true + command: "check-procs.rb -p logstash-forwarder -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + check_filebeat_process: + handler: default + notification: "unexpected number of filebeat processes" + interval: 120 + standalone: true + command: "check-procs.rb -p filebeat -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + logstash: + check_logstash_process: + handler: default + notification: "unexpected number of logstash processes" + interval: 120 + standalone: true + command: "check-procs.rb -p logstash -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + percona: + check_mysql_process: + handler: default + notification: "unexpected number of mysql processes" + interval: 120 + standalone: true + command: "check-procs.rb -p mysql -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + check_garbd_process: + handler: default + notification: "unexpected number of garbd processes" + interval: 120 + standalone: true + command: "check-procs.rb -p garbd -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + postfix: + check_postfix_process: + handler: default + notification: "unexpected number of postfix processes" + interval: 120 + standalone: true + command: "check-procs.rb -p postfix -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + pypi_mirror: + check_pypi_mirror_process: + handler: default + notification: "unexpected number of pypi_mirror processes" + interval: 120 + standalone: true + command: "check-procs.rb -p pypi_mirror -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + rabbitmq: + check_rabbitmq_server_process: + handler: default + notification: "unexpected number of rabbitmq-server processes" + interval: 120 + standalone: true + command: "check-procs.rb -p rabbitmq-server -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + check_rabbitmq_messages: + handler: default + notification: "too many queued rabbitmq messages" + interval: 120 + occurrences: 5 + standalone: true + command: "check-rabbitmq-messages.rb --user admin --password {{ secrets.sensu.server.rabbitmq.admin|default('admin') }} -w 10000 -c 50000" + service_owner: "{{ monitoring_common.service_owner }}" + + redis: + check_redis_server_process: + handler: default + notification: "unexpected number of redis-server processes" + interval: 120 + standalone: true + command: "check-procs.rb -p redis-server -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + sensu_api: + check_sensu_api_health: + handler: default + interval: 60 + occurrences: 5 + standalone: true + command: "/opt/sitecontroller/sensu-plugins/check-sensu-api-health.rb --user sensu --password {{ secrets.sensu.server.rabbitmq.admin|default('admin') }} --keepalives 100 --results 10000" + service_owner: "{{ monitoring_common.service_owner }}" + + sensu_client: + check_serverspec: + handler: default + interval: 600 + standalone: true + command: "sudo check-serverspec.rb -d /etc/serverspec -s warning" + service_owner: "{{ monitoring_common.service_owner }}" + + squid: + check_squid_process: + handler: default + notification: "unexpected number of squid3 processes" + interval: 120 + standalone: true + command: "check-procs.rb -p squid3 -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + sshauthmux: + check_authorization_proxy_process: + handler: default + notification: "unexpected number of authorization_proxy processes" + interval: 120 + standalone: true + command: "check-procs.rb -p authorization_proxy -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + ttyspyd: + check_ttyspyd_process: + handler: default + notification: "unexpected number of ttyspyd processes" + interval: 120 + standalone: true + command: "check-procs.rb -p ttyspyd -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + ttyspy_server: + check_ttyspy_receiver_process: + handler: default + notification: "unexpected number of ttyspy_receiver processes" + interval: 120 + standalone: true + command: "check-procs.rb -p ttyspy_receiver -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + uchiwa: + check_uchiwa_health: + handler: default + interval: 60 + standalone: true + command: "check-uchiwa-health.rb" + service_owner: "{{ monitoring_common.service_owner }}" + + varnish: + check_varnish_process: + handler: default + notification: "unexpected number of varnish processes" + interval: 120 + standalone: true + command: "check-procs.rb -p varnish -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" + + yubiauthd: + check_yubiauthd_process: + handler: default + notification: "unexpected number of yubiauthd processes" + interval: 120 + standalone: true + command: "check-procs.rb -p yubiauthd -W 1 -C 1" + service_owner: "{{ monitoring_common.service_owner }}" diff --git a/roles/sensu-check/handlers/main.yml b/roles/sensu-check/handlers/main.yml new file mode 100644 index 0000000..552df20 --- /dev/null +++ b/roles/sensu-check/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: restart sensu-client missing ok + service: + name: sensu-client + state: restarted + register: handler + failed_when: + - handler.failed is defined + - handler.msg.find('Could not find the requested service') == -1 + - handler.msg.find('service not found') == -1 diff --git a/roles/sensu-check/tasks/main.yml b/roles/sensu-check/tasks/main.yml new file mode 100644 index 0000000..5143a0c --- /dev/null +++ b/roles/sensu-check/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: delete antiquated ursula-monitoring serverspec + file: + path: /etc/serverspec/spec/localhost/monitoring-common_spec.rb + state: absent + +- name: sensu checks directory + file: dest=/etc/sensu/conf.d/checks + state=directory mode=0755 diff --git a/roles/sensu-client/files/plugins/check-arping.sh b/roles/sensu-client/files/plugins/check-arping.sh new file mode 100755 index 0000000..9899fcf --- /dev/null +++ b/roles/sensu-client/files/plugins/check-arping.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Send 2 ARP REQUEST broadcast packets and alert if: +# 1. There are no replies. +# 2. There is more than 1 MAC address in the replies. + +while getopts 'I:d:z:' OPT; do + case "$OPT" in + I) interface="$OPTARG";; + d) destination="$OPTARG";; + z) CRITICALITY="$OPTARG";; + esac +done + +CRITICALITY=${CRITICALITY:-critical} + +if [[ -z "$interface" || -z "$destination" ]]; then + echo "Usage: $0 -I device -d destination" + exit 1 +fi + +output=$(arping -b -c 2 -I $interface $destination) + +if [ $? -ne 0 ]; then + echo "ERROR: No ARP replies for destination: $destination" + echo "$output" + if [ "$CRITICALITY" == "warning" ]; then + exit 1 + else + exit 2 + fi +fi + +mac_address=$(echo "$output" | grep 'reply from' | awk '{ print $5 }' | sort | uniq) +if [[ -z "$mac_address" ]]; then + echo "WARN: Error parsing output for MAC addresses:" + echo "$output" + exit 1 +fi + +num_address=$(echo "$mac_address" | wc -l) +status="Received replies from ${mac_address//$'\n'/,} for destination: $destination" + +if [ $num_address -ne 1 ]; then + echo "ERROR: $status" + echo "$output" + if [ "$CRITICALITY" == "warning" ]; then + exit 1 + else + exit 2 + fi +else + echo "OK: $status" + exit 0 +fi diff --git a/roles/sensu-client/files/plugins/check-cpu.rb b/roles/sensu-client/files/plugins/check-cpu.rb new file mode 100755 index 0000000..01c5c85 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-cpu.rb @@ -0,0 +1,106 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check CPU Plugin +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckCPU < Sensu::Plugin::Check::CLI + + option :warn, + :short => '-w WARN', + :proc => proc {|a| a.to_f }, + :default => 80 + + option :crit, + :short => '-c CRIT', + :proc => proc {|a| a.to_f }, + :default => 10 + + option :sleep, + :long => '--sleep SLEEP', + :proc => proc {|a| a.to_f }, + :default => 1 + + option :process_white_list, + :short => '-p PROCESS_WHITE_LIST', + :long => '--process-white-list PROCESS_WHITE_LIST', + :proc => proc {|a| a.split(',') }, + :default => [] + + [:user, :nice, :system, :idle, :iowait, :irq, :softirq, :steal, :guest].each do |metric| + option metric, + :long => "--#{metric}", + :description => "Check cpu #{metric} instead of total cpu usage", + :boolean => true, + :default => false + end + + def get_cpu_stats + File.open("/proc/stat", "r").each_line do |line| + info = line.split(/\s+/) + name = info.shift + return info.map{|i| i.to_f} if name.match(/^cpu$/) + end + end + + def run + metrics = [:user, :nice, :system, :idle, :iowait, :irq, :softirq, :steal, :guest] + + cpu_stats_before = get_cpu_stats + sleep config[:sleep] + cpu_stats_after = get_cpu_stats + + cpu_total_diff = 0.to_f + cpu_stats_diff = [] + metrics.each_index do |i| + # Some OS's don't have a 'guest' values (RHEL) + unless cpu_stats_after[i].nil? + cpu_stats_diff[i] = cpu_stats_after[i] - cpu_stats_before[i] + cpu_total_diff += cpu_stats_diff[i] + end + end + + cpu_stats = [] + metrics.each_index do |i| + cpu_stats[i] = 100*(cpu_stats_diff[i]/cpu_total_diff) + end + + cpu_usage = 100*(cpu_total_diff - cpu_stats_diff[3])/cpu_total_diff + checked_usage = cpu_usage + + self.class.check_name 'CheckCPU TOTAL' + metrics.each do |metric| + if config[metric] + self.class.check_name "CheckCPU #{metric.to_s.upcase}" + checked_usage = cpu_stats[metrics.find_index(metric)] + end + end + + msg = "total=#{cpu_usage.round(2)}" + cpu_stats.each_index {|i| msg += " #{metrics[i]}=#{cpu_stats[i].round(2)}"} + + message msg + + if checked_usage > config[:crit] || checked_usage > config[:warn] + unless process_in_white_list?(get_top_process_by_cpu_mem) + critical if checked_usage > config[:crit] + warning if checked_usage > config[:warn] + end + end + exit + end + + def process_in_white_list?(process) + config[:process_white_list].any? do |p| + process.include?(p) + end + end + + def get_top_process_by_cpu_mem + `ps axo pcpu,pmem,cmd k pcpu,pmem | tail -n 1`.chomp + end + +end + diff --git a/roles/sensu-client/files/plugins/check-dir-new-files.rb b/roles/sensu-client/files/plugins/check-dir-new-files.rb new file mode 100755 index 0000000..9c4830d --- /dev/null +++ b/roles/sensu-client/files/plugins/check-dir-new-files.rb @@ -0,0 +1,103 @@ +#! /usr/bin/env ruby +# +# check-dir-new-files +# +# DESCRIPTION: +# Checks the number of specific files in a directory +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux, BSD +# +# DEPENDENCIES: +# gem: sensu-plugin +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Copyright 2014 Sonian, Inc. and contributors. +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/check/cli' +require 'fileutils' +require 'time' + +class DirCount < Sensu::Plugin::Check::CLI + BASE_DIR = '/var/cache/sensu/check-dir-new-files' + + option :directory, + description: 'Directory to count files in', + short: '-d DIR', + long: '--dir DIR', + default: '/var/crash' + + option :filename_pattern, + description: 'filename patten to match', + short: '-p PATTERN', + long: '--pattern PATTERN', + default: '*.crash' + + option :criticality, + description: "Set sensu alert level, default is critical", + short: '-z CRITICALITY', + long: '--criticality CRITICALITY', + default: 'critical' + + def getLastCheckTime() + begin + @last_check_time = 0 + @state_file = File.join(BASE_DIR,config[:directory].gsub('/','_'),config[:filename_pattern]) + File.open(@state_file, "r") do |file| + file.flock(File::LOCK_SH) + @last_check_time = file.readline.to_i + end + rescue + return + end + end + + def setLastCheckTime() + begin + FileUtils.mkdir_p(File.dirname(@state_file)) + File.open(@state_file, File::RDWR|File::CREAT, 0644) do |file| + file.flock(File::LOCK_EX) + file.truncate(0) + file.write(Time.now.to_i) + end + rescue + return + end + end + + def run + + getLastCheckTime() + + file_count = 0 + + begin + Dir.chdir(config[:directory]) + Dir.glob(config[:filename_pattern]).each {|file| file_count += 1 if File.mtime(file) >= Time.at(@last_check_time)} + rescue Exception => e + puts e + unknown "Error listing files in #{config[:directory]}" + end + + setLastCheckTime() + + msg = "#{file_count} new file(s) like #{config[:filename_pattern]} created at #{config[:directory]}." + + ok msg if file_count == 0 + warning msg if config[:criticality] == "warning" + critical msg + + end +end + diff --git a/roles/sensu-client/files/plugins/check-disk.rb b/roles/sensu-client/files/plugins/check-disk.rb new file mode 100755 index 0000000..bd5c225 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-disk.rb @@ -0,0 +1,225 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# check-disk +# +# DESCRIPTION: +# Uses the sys-filesystem gem to get filesystem mount points and metrics +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux, BSD, Windows +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: sys-filesystem +# +# USAGE: +# +# NOTES: +# +# LICENSE: +# Copyright 2015 Yieldbot Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# +# Code adapted from Yieldbot Inc's script in the Sensu Plugins community: +# https://github.com/sensu-plugins/sensu-plugins-disk-checks/blob/master/bin/check-disk-usage.rb +# with additional change for add criticality option. +# +# Released under the same terms as Sensu (the MIT license); see MITLICENSE +# for details. +# +# Xiao Hua,Shen +# + +require 'sensu-plugin/check/cli' +require 'sys/filesystem' +include Sys + +# +# Check Disk +# +class CheckDisk < Sensu::Plugin::Check::CLI + option :fstype, + short: '-t TYPE[,TYPE]', + description: 'Only check fs type(s)', + proc: proc { |a| a.split(',') } + + option :ignoretype, + short: '-x TYPE[,TYPE]', + description: 'Ignore fs type(s)', + proc: proc { |a| a.split(',') } + + option :ignoremnt, + short: '-i MNT[,MNT]', + description: 'Ignore mount point(s)', + proc: proc { |a| a.split(',') } + + option :includemnt, + description: 'Include only mount point(s)', + short: '-I MNT[,MNT]', + proc: proc { |a| a.split(',') } + + option :ignorepathre, + short: '-p PATHRE', + description: 'Ignore mount point(s) matching regular expression', + proc: proc { |a| Regexp.new(a) } + + option :ignoreopt, + short: '-o TYPE[.TYPE]', + description: 'Ignore option(s)', + proc: proc { |a| a.split('.') } + + option :bwarn, + short: '-w PERCENT', + description: 'Warn if PERCENT or more of disk full', + proc: proc(&:to_i), + default: 85 + + option :bcrit, + short: '-c PERCENT', + description: 'Critical if PERCENT or more of disk full', + proc: proc(&:to_i), + default: 95 + + option :iwarn, + short: '-W PERCENT', + description: 'Warn if PERCENT or more of inodes used', + proc: proc(&:to_i), + default: 85 + + option :icrit, + short: '-K PERCENT', + description: 'Critical if PERCENT or more of inodes used', + proc: proc(&:to_i), + default: 95 + + option :magic, + short: '-m MAGIC', + description: 'Magic factor to adjust warn/crit thresholds. Example: .9', + proc: proc(&:to_f), + default: 1.0 + + option :normal, + short: '-n NORMAL', + description: 'Levels are not adapted for filesystems of exactly this '\ + 'size, where levels are reduced for smaller filesystems and raised '\ + 'for larger filesystems.', + proc: proc(&:to_f), + default: 20 + + option :minimum, + short: '-l MINIMUM', + description: 'Minimum size to adjust (in GB)', + proc: proc(&:to_f), + default: 100 + + option :criticality, + short: '-z criticality', + default: 'critical' + + # Setup variables + # + def initialize + super + @crit_fs = [] + @warn_fs = [] + end + + # Get mount data + # + def fs_mounts + Filesystem.mounts.each do |line| + begin + next if config[:fstype] && !config[:fstype].include?(line.mount_type) + next if config[:ignoretype] && config[:ignoretype].include?(line.mount_type) + next if config[:ignoremnt] && config[:ignoremnt].include?(line.mount_point) + next if config[:ignorepathre] && config[:ignorepathre].match(line.mount_point) + next if config[:ignoreopt] && config[:ignoreopt].include?(line.options) + next if config[:includemnt] && !config[:includemnt].include?(line.mount_point) + rescue + unknown 'An error occured getting the mount info' + end + check_mount(line) + end + end + + # Adjust the percentages based on volume size + # + def adj_percent(size, percent) + hsize = (size / (1024.0 * 1024.0)) / config[:normal].to_f + felt = hsize**config[:magic] + scale = felt / hsize + 100 - ((100 - percent) * scale) + end + + def check_mount(line) + begin + fs_info = Filesystem.stat(line.mount_point) + rescue + @warn_fs << "#{line.mount_point} Unable to read." + return + end + if fs_info.respond_to?(:inodes) && !fs_info.inodes.nil? # needed for windows + percent_i = percent_inodes(fs_info) + if percent_i >= config[:icrit] + @crit_fs << "#{line.mount_point} #{percent_i}% inode usage" + elsif percent_i >= config[:iwarn] + @warn_fs << "#{line.mount_point} #{percent_i}% inode usage" + end + end + percent_b = percent_bytes(fs_info) + + if fs_info.bytes_total < (config[:minimum] * 1_000_000_000) + bcrit = config[:bcrit] + bwarn = config[:bwarn] + else + bcrit = adj_percent(fs_info.bytes_total, config[:bcrit]) + bwarn = adj_percent(fs_info.bytes_total, config[:bwarn]) + end + + used = to_human(fs_info.bytes_used) + total = to_human(fs_info.bytes_total) + + if percent_b >= bcrit + @crit_fs << "#{line.mount_point} #{percent_b}% bytes usage (#{used}/#{total})" + elsif percent_b >= bwarn + @warn_fs << "#{line.mount_point} #{percent_b}% bytes usage (#{used}/#{total})" + end + end + + def to_human(s) + unit = [[1_099_511_627_776, 'TiB'], [1_073_741_824, 'GiB'], [1_048_576, 'MiB'], [1024, 'KiB'], [0, 'B']].detect { |u| s >= u[0] } + format("%.2f #{unit[1]}", (s >= 1024 ? s.to_f / unit[0] : s)) + end + + # Determine the percent inode usage + # + def percent_inodes(fs_info) + (100.0 - (100.0 * fs_info.inodes_free / fs_info.inodes)).round(2) + end + + # Determine the percent byte usage + # + def percent_bytes(fs_info) + (100.0 - (100.0 * fs_info.bytes_free / fs_info.bytes_total)).round(2) + end + + # Generate output + # + def usage_summary + (@crit_fs + @warn_fs).join(', ') + end + + # Main function + # + def run + fs_mounts + ok "All disk usage under #{config[:bwarn]}% and inode usage under #{config[:iwarn]}%" if @crit_fs.empty? && @warn_fs.empty? + critical usage_summary if !(@crit_fs.empty?) && config[:criticality] == 'critical' + warning usage_summary + + end +end \ No newline at end of file diff --git a/roles/sensu-client/files/plugins/check-es-cluster-status.rb b/roles/sensu-client/files/plugins/check-es-cluster-status.rb new file mode 100755 index 0000000..e3acda3 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-es-cluster-status.rb @@ -0,0 +1,108 @@ +#!/opt/sensu/embedded/bin/ruby +# +# check-es-cluster-status +# +# DESCRIPTION: +# This plugin checks the ElasticSearch cluster status, using its API. +# Works with ES 0.9x and ES 1.x +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: rest-client +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Copyright 2012 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' +require 'rest-client' +require 'json' + +class ESClusterStatus < Sensu::Plugin::Check::CLI + option :host, + description: 'Elasticsearch host', + short: '-h HOST', + long: '--host HOST', + default: 'localhost' + + option :port, + description: 'Elasticsearch port', + short: '-p PORT', + long: '--port PORT', + proc: proc(&:to_i), + default: 9200 + + option :master_only, + description: 'Use master Elasticsearch server only', + short: '-m', + long: '--master-only', + default: false + + option :timeout, + description: 'Sets the connection timeout for REST client', + short: '-t SECS', + long: '--timeout SECS', + proc: proc(&:to_i), + default: 30 + + def get_es_resource(resource) + r = RestClient::Resource.new("http://#{config[:host]}:#{config[:port]}/#{resource}", timeout: config[:timeout]) + JSON.parse(r.get) + rescue Errno::ECONNREFUSED + critical 'Connection refused' + rescue RestClient::RequestTimeout + critical 'Connection timed out' + rescue Errno::ECONNRESET + critical 'Connection reset by peer' + end + + def acquire_es_version + info = get_es_resource('/') + info['version']['number'] + end + + def master? + if Gem::Version.new(acquire_es_version) >= Gem::Version.new('1.0.0') + master = get_es_resource('_cluster/state/master_node')['master_node'] + local = get_es_resource('/_nodes/_local') + else + master = get_es_resource('/_cluster/state?filter_routing_table=true&filter_metadata=true&filter_indices=true')['master_node'] + local = get_es_resource('/_cluster/nodes/_local') + end + local['nodes'].keys.first == master + end + + def acquire_status + health = get_es_resource('/_cluster/health') + health['status'].downcase + end + + def run + if !config[:master_only] || master? + case acquire_status + when 'green' + ok 'Cluster is green' + when 'yellow' + warning 'Cluster is yellow' + when 'red' + critical 'Cluster is red' + end + else + ok 'Not the master' + end + end +end diff --git a/roles/sensu-client/files/plugins/check-es-file-descriptors.rb b/roles/sensu-client/files/plugins/check-es-file-descriptors.rb new file mode 100755 index 0000000..b9f5b77 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-es-file-descriptors.rb @@ -0,0 +1,110 @@ +#!/opt/sensu/embedded/bin/ruby +# +# check-es-file-descriptors +# +# DESCRIPTION: +# This plugin checks the ElasticSearch file descriptor usage, using its API. +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: json +# gem: rest-client +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Author: S. Zachariah Sprackett +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' +require 'rest-client' +require 'json' + +class ESClusterStatus < Sensu::Plugin::Check::CLI + option :host, + description: 'Elasticsearch host', + short: '-h HOST', + long: '--host HOST', + default: 'localhost' + + option :port, + description: 'Elasticsearch port', + short: '-p PORT', + long: '--port PORT', + proc: proc(&:to_i), + default: 9200 + + option :timeout, + description: 'Sets the connection timeout for REST client', + short: '-t SECS', + long: '--timeout SECS', + proc: proc(&:to_i), + default: 30 + + option :critical, + description: 'Critical percentage of FD usage', + short: '-c PERCENTAGE', + proc: proc(&:to_i), + default: 90 + + option :warning, + description: 'Warning percentage of FD usage', + short: '-w PERCENTAGE', + proc: proc(&:to_i), + default: 80 + + def get_es_resource(resource) + r = RestClient::Resource.new("http://#{config[:host]}:#{config[:port]}/#{resource}", timeout: config[:timeout]) + JSON.parse(r.get) + rescue Errno::ECONNREFUSED + warning 'Connection refused' + rescue RestClient::RequestTimeout + warning 'Connection timed out' + end + + def acquire_open_fds(stats) + begin + keys = stats['nodes'].keys + stats['nodes'][keys[0]]['process']['open_file_descriptors'].to_i + rescue NoMethodError + warning 'Failed to retrieve open_file_descriptors' + end + end + + def acquire_max_fds(stats) + begin + keys = stats['nodes'].keys + stats['nodes'][keys[0]]['process']['max_file_descriptors'].to_i + rescue NoMethodError + warning 'Failed to retrieve max_file_descriptors' + end + end + + def run + es_stats = get_es_resource('/_nodes/_local/stats?process=true') + + open = acquire_open_fds es_stats + max = acquire_max_fds es_stats + used_percent = ((open.to_f / max.to_f) * 100).to_i + + if used_percent >= config[:critical] + critical "fd usage #{used_percent}% exceeds #{config[:critical]}% (#{open}/#{max})" + elsif used_percent >= config[:warning] + warning "fd usage #{used_percent}% exceeds #{config[:warning]}% (#{open}/#{max})" + else + ok "fd usage at #{used_percent}% (#{open}/#{max})" + end + end +end diff --git a/roles/sensu-client/files/plugins/check-es-heap.rb b/roles/sensu-client/files/plugins/check-es-heap.rb new file mode 100755 index 0000000..0f42330 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-es-heap.rb @@ -0,0 +1,134 @@ +#!/opt/sensu/embedded/bin/ruby +# +# check-es-heap +# +# DESCRIPTION: +# This plugin checks ElasticSearch's Java heap usage using its API. +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: rest-client +# +# USAGE: +# example commands +# +# NOTES: +# +# LICENSE: +# Copyright 2012 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' +require 'rest-client' +require 'json' + +class ESHeap < Sensu::Plugin::Check::CLI + option :host, + description: 'Elasticsearch host', + short: '-h HOST', + long: '--host HOST', + default: 'localhost' + + option :port, + description: 'Elasticsearch port', + short: '-p PORT', + long: '--port PORT', + proc: proc(&:to_i), + default: 9200 + + option :warn, + short: '-w N', + long: '--warn N', + description: 'Heap used in bytes WARNING threshold', + proc: proc(&:to_i), + default: 0 + + option :timeout, + description: 'Sets the connection timeout for REST client', + short: '-t SECS', + long: '--timeout SECS', + proc: proc(&:to_i), + default: 30 + + option :crit, + short: '-c N', + long: '--crit N', + description: 'Heap used in bytes CRITICAL threshold', + proc: proc(&:to_i), + default: 0 + + option :percentage, + short: '-P', + long: '--percentage', + description: 'Use the WARNING and CRITICAL threshold numbers as percentage indicators of the total heap available', + default: false + + def acquire_es_version + info = acquire_es_resource('/') + info['version']['number'] + end + + def acquire_es_resource(resource) + r = RestClient::Resource.new("http://#{config[:host]}:#{config[:port]}/#{resource}", timeout: config[:timeout]) + JSON.parse(r.get) + rescue Errno::ECONNREFUSED + warning 'Connection refused' + rescue RestClient::RequestTimeout + warning 'Connection timed out' + rescue JSON::ParserError + warning 'Elasticsearch API returned invalid JSON' + end + + def acquire_heap_data(return_max = false) + if Gem::Version.new(acquire_es_version) >= Gem::Version.new('1.0.0') + stats = acquire_es_resource('_nodes/_local/stats?jvm=true') + node = stats['nodes'].keys.first + else + stats = acquire_es_resource('_cluster/nodes/_local/stats?jvm=true') + node = stats['nodes'].keys.first + end + begin + if return_max + return stats['nodes'][node]['jvm']['mem']['heap_used_in_bytes'], stats['nodes'][node]['jvm']['mem']['heap_max_in_bytes'] + else + stats['nodes'][node]['jvm']['mem']['heap_used_in_bytes'] + end + rescue + warning 'Failed to obtain heap used in bytes' + end + end + + def run + if config[:percentage] + heap_used, heap_max = acquire_heap_data(true) + heap_used_ratio = ((100 * heap_used) / heap_max).to_i + message "Heap used in bytes #{heap_used} (#{heap_used_ratio}% full)" + if heap_used_ratio >= config[:crit] + critical + elsif heap_used_ratio >= config[:warn] + warning + else + exit + end + else + heap_used = acquire_heap_data(false) + message "Heap used in bytes #{heap_used}" + if heap_used >= config[:crit] + critical + elsif heap_used >= config[:warn] + warning + else + exit + end + end + end +end diff --git a/roles/sensu-client/files/plugins/check-es-insert-rate.py b/roles/sensu-client/files/plugins/check-es-insert-rate.py new file mode 100755 index 0000000..202b29c --- /dev/null +++ b/roles/sensu-client/files/plugins/check-es-insert-rate.py @@ -0,0 +1,166 @@ +#!/usr/bin/python + +from __future__ import division +import argparse +import time +import pyes + + +###CONSTANTS### +OK = 0 +WARNING = 1 +CRITICAL = 2 +UNKNOWN = 3 +###END CONSTANTS### + + +class Calculator(): + '''This is our main class. It takes results from Elasticsearch and from what was previously recorded + and can give the text to be printed and the exit code''' + def __init__(self, warn, crit, myfile='/tmp/check_es_insert', myaddress='localhost:9200', threshold='lt', index=''): + self.warn = warn + self.crit = crit + self.my_elasticsearcher = Elasticsearcher(address=myaddress) + self.my_disker = Disker(file=myfile) + self.threshold = threshold + self.index = index + def calculate(self, old_value, new_value, old_time, new_time): + '''Calculates the number of inserts per second since the last recording''' + return (new_value - old_value)/(new_time - old_time) + def getPrevious(self): + '''Gets the previously recorded number of documents and UNIX time''' + try: + previous = self.my_disker.getPrevious() + except: + #-2 is an error code. In case shit goes wrong while accessing the file + return (-2, 0) + #return the result and time + return (previous[0], previous[1]) + def getCurrent(self): + '''Gets current number of documents and current UNIX time''' + try: + current_result = self.my_elasticsearcher.getCurrent(self.index) + except: + #-1 is an error code. In case shit goes wrong while interrogating Elasticsearch + return (-1, 0) + current_time = timer() + return (current_result, current_time) + + def printandexit(self, result): + '''Given the number of inserts per second, it gives the formatted text and the exit code''' + text="Number of documents inserted per second (index: %s) is %f" % (self.index if self.index != '' else 'all', result) + if self.threshold == 'lt': + if result +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckFstabMounts < Sensu::Plugin::Check::CLI + option :fstypes, + :description => 'Filesystem types to check, comma-separated', + :short => '-t TYPES', + :long => '--types TYPES', + :proc => proc {|a| a.split(',')}, + :required => false + + option :criticality, + :description => 'Set sensu alert level, default is critical', + :short => '-z CRITICALITY', + :long => '--criticality CRITICALITY', + :default => 'critical' + + def switch_on_criticality(msg) + if config[:criticality] == 'warning' + warning msg + else + critical msg + end + end + + def initialize + super + @fstab = IO.readlines '/etc/fstab' + @proc_mounts = IO.readlines '/proc/mounts' + @missing_mounts = [] + end + + def check_mounts + # check by mount destination, which is col 2 in fstab and proc/mounts + @fstab.each do |line| + next if line =~ /^\s*#/ + fields = line.split(/\s+/) + next if fields[1] == 'none' + next if config[:fstypes] and !config[:fstypes].include? fields[2] + if @proc_mounts.select {|m| m.split(/\s+/)[1] == fields[1]}.empty? + @missing_mounts << fields[1] + end + end + end + + def run + check_mounts + if @missing_mounts.any? + switch_on_criticality("Mountpoint(s) #{@missing_mounts.join(',')} not mounted!") + else + ok 'All mountpoints accounted for' + end + end +end diff --git a/roles/sensu-client/files/plugins/check-hostname.sh b/roles/sensu-client/files/plugins/check-hostname.sh new file mode 100755 index 0000000..0c26b97 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-hostname.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +trap "{ echo 'unable to check hostname'; exit 2; }" ERR + +while getopts 'k:v:' OPT; do + case "$OPT" in + k) key="$OPTARG";; # e.g. ansible_nodename + v) value="$OPTARG";; # e.g. vagrant + esac +done + +[ -n "$value" ] || exit 0 + +host="$(hostname)" + +if [ $(hostname) != "$value" ]; then + if [ -n "$key" ]; then + echo "hostname ($host) should match $key ($value)" + else + echo "hostname ($host) should be set to $value" + fi + exit 2 +else + exit 0 +fi diff --git a/roles/sensu-client/files/plugins/check-http.rb b/roles/sensu-client/files/plugins/check-http.rb new file mode 100755 index 0000000..b0cce40 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-http.rb @@ -0,0 +1,138 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check HTTP +# === +# +# Takes either a URL or a combination of host/path/port/ssl, and checks for +# a 200 response (that matches a pattern, if given). Can use client certs. +# +# Copyright 2011 Sonian, Inc +# Updated by Lewis Preson 2012 to accept basic auth credentials +# Updated by SweetSpot 2012 to require specified redirect +# Updated by Chris Armstrong 2013 to accept multiple headers +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' +require 'net/http' +require 'net/https' + +class CheckHTTP < Sensu::Plugin::Check::CLI + + option :url, :short => '-u URL' + option :host, :short => '-h HOST' + option :path, :short => '-p PATH' + option :port, :short => '-P PORT', :proc => proc { |a| a.to_i } + option :header, :short => '-H HEADER', :long => '--header HEADER' + option :ssl, :short => '-s', :boolean => true, :default => false + option :insecure, :short => '-k', :boolean => true, :default => false + option :user, :short => '-U', :long => '--username USER' + option :password, :short => '-a', :long => '--password PASS' + option :cert, :short => '-c FILE' + option :cacert, :short => '-C FILE' + option :pattern, :short => '-q PAT' + option :timeout, :short => '-t SECS', :proc => proc { |a| a.to_i }, :default => 15 + option :redirectok, :short => '-r', :boolean => true, :default => false + option :redirectto, :short => '-R URL' + option :criticality, :short => '-z CRITICALITY', :default => 'critical' + + def switch_on_criticality(msg) + if config[:criticality] == 'warning' + warning msg + else + critical msg + end + end + + def run + if config[:url] + uri = URI.parse(config[:url]) + config[:host] = uri.host + config[:path] = uri.path + config[:port] = uri.port + config[:ssl] = uri.scheme == 'https' + else + unless config[:host] and config[:path] + unknown 'No URL specified' + end + config[:port] ||= config[:ssl] ? 443 : 80 + end + + begin + Timeout.timeout(config[:timeout]) do + get_resource + end + rescue Timeout::Error + switch_on_criticality("Connection timed out") + rescue => e + switch_on_criticality("Connection error: #{e.message}") + end + end + + + def get_resource + http = Net::HTTP.new(config[:host], config[:port]) + + if config[:ssl] + http.use_ssl = true + if config[:cert] + cert_data = File.read(config[:cert]) + http.cert = OpenSSL::X509::Certificate.new(cert_data) + http.key = OpenSSL::PKey::RSA.new(cert_data, nil) + end + if config[:cacert] + http.ca_file = config[:cacert] + end + if config[:insecure] + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + end + + req = Net::HTTP::Get.new(config[:path]) + if (config[:user] != nil and config[:password] != nil) + req.basic_auth config[:user], config[:password] + end + if config[:header] + config[:header].split(',').each do |header| + h, v = header.split(':', 2) + req[h] = v.strip + end + end + res = http.request(req) + + case res.code + when /^2/ + if config[:redirectto] + switch_on_criticality("expected redirect to #{config[:redirectto]} but got #{res.code}") + elsif config[:pattern] + if res.body =~ /#{config[:pattern]}/ + ok "#{res.code}, found /#{config[:pattern]}/ in #{res.body.size} bytes" + else + switch_on_criticality("#{res.code}, did not find /#{config[:pattern]}/ in #{res.body.size} bytes: #{res.body[0...200]}...") + end + else + ok "#{res.code}, #{res.body.size} bytes" + end + when /^3/ + if config[:redirectok] || config[:redirectto] + if config[:redirectok] + ok "#{res.code}, #{res.body.size} bytes" + elsif config[:redirectto] + if config[:redirectto] == res['Location'] + ok "#{res.code} found redirect to #{res['Location']}" + else + switch_on_criticality("expected redirect to #{config[:redirectto]} instead redirected to #{res['Location']}") + end + end + else + warning res.code + end + when /^4/, /^5/ + switch_on_criticality(res.code) + else + warning res.code + end + end +end diff --git a/roles/sensu-client/files/plugins/check-inspec.rb b/roles/sensu-client/files/plugins/check-inspec.rb new file mode 100755 index 0000000..f70bef3 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-inspec.rb @@ -0,0 +1,90 @@ +#!/opt/sensu/embedded/bin/ruby +# +# check-inspec +# +# DESCRIPTION: +# Runs inspec tests against your servers. +# Fails with a warning or a critical if tests are failing, depending +# on the severity level set. +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: json +# +# USAGE: +# Run entire suite of testd +# check-inspec --controls /etc/inspec/controls +# +# NOTES: +# Critical severity level is set as the default +# +# LICENSE: +# Copyright 2016 IBM +# Copyright 2014 Sonian, Inc. and contributors. +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/check/cli' +require 'json' + +class CheckInspec < Sensu::Plugin::Check::CLI + option :controls, + short: '-c /tmp/dir', + long: '--controls /tmp/dir', + required: true, + default: '/etc/inspec/controls' + + option :attrs, + short: '-a /tmp/dir', + long: '--attrs /tmp/dir', + default: '/etc/inspec/controls/attributes.yml' + + option :severity, + short: '-s severity', + long: '--severity severity', + default: 'critical' + + def inspec(controls, attrs) + inspec = `inspec exec #{controls} --attrs #{attrs} --format=json-min` + JSON.parse(inspec) + end + + def run + results = inspec(config[:controls], config[:attrs]) + passed = 0 + failed = 0 + skipped = 0 + msg = "" + results['controls'].each do |control| + #puts control + if control['status'] == "passed" + passed += 1 + elsif control['status'] == "skipped" + skipped += 1 + else + failed += 1 + msg += "#{control['id']} #{control['code_desc']} - #{control['status']}\n" + end + end + + msg += "Passed: %s Skipped: %s Failed: %s" % [passed, skipped, failed] + + if failed > 0 + if config[:severity] == 'warning' + warning msg + else + critical msg + end + else + ok msg + end + end + +end diff --git a/roles/sensu-client/files/plugins/check-ipmi-sensors.py b/roles/sensu-client/files/plugins/check-ipmi-sensors.py new file mode 100755 index 0000000..512e882 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-ipmi-sensors.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# +# Check whether there's warning and error in IPMI sensor status +# +# Return CRITICAL or WARNING when there's sensor error or there's PFA alert +# +# Fan He + +import argparse +import re +import subprocess +import sys + +STATE_OK = 0 +STATE_WARNING = 1 +STATE_CRITICAL = 2 + +def exit_error(criticality): + if criticality == 'warning': + sys.exit(STATE_WARNING) + else: + sys.exit(STATE_CRITICAL) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--debug', help='Enable ipmitool output debugging', action='store_true') + parser.add_argument('--criticality', help='Set sensu alert level, "warning" or "critical" (default)', default='critical') + args = parser.parse_args() + + # Check Sensor Data Record (SDR) Repository info by elist containing asserted discrete states + cmd = "ipmitool sdr elist" + try: + elist = subprocess.check_output(cmd, shell=True) + except subprocess.CalledProcessError as e: + print(e.output) + exit_error(args.criticality) + + lines = elist.splitlines() + error_messages = [] + + for s in lines: + if s == '': + continue + if args.debug: + print(s) + + sensor = [x.strip() for x in s.split('|')] + name = sensor[0] + status = sensor[2] + asserted_states = sensor[-1] + + # Check if there are sensor not in OK or No Status + if status not in ['ok', 'ns']: + error_messages.append("Sensor [%s] has unexpected status [%s] %s" % (name, status, asserted_states)) + + # Additionally, check unexpecetd asserted states to discover PFA alerts for RAM and disks + if re.match('DIMM\s\d+\Z', name) or re.match('Drive\s\d+\Z', name): + if asserted_states == '': + continue + for state in [x.strip() for x in asserted_states.split(',')]: + if state not in ['Drive Present', 'No Reading', 'Presence Detected']: + error_messages.append("Sensor [%s] has unexpected assertion [%s]" % (name, state)) + + if len(error_messages) > 0: + for msg in error_messages: + print(msg) + exit_error(args.criticality) + + sys.exit(STATE_OK) + +if __name__ == "__main__": + main() diff --git a/roles/sensu-client/files/plugins/check-kernel-options.rb b/roles/sensu-client/files/plugins/check-kernel-options.rb new file mode 100755 index 0000000..2e37f49 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-kernel-options.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check Kernel boot options +# === +# +# This plugin checks that the running kernel has been booted with the specified +# kernel options +# +# Copyright 2014 Dustin Lundquist +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' +require 'timeout' +require 'socket' + +class CheckKernelOptions < Sensu::Plugin::Check::CLI + + def run + kernel_cmd_line = File::open('/proc/cmdline', 'r'){ |io| io.read } + + [ + 'consoleblank=0', + /console=ttyS\d+,115200n8/ + ].each do |option| + warning "Kernel booted without #{option}" unless kernel_cmd_line.match(option) + end + + ok "Kernel boot options appear good" + end + +end diff --git a/roles/sensu-client/files/plugins/check-large-receive-offload.py b/roles/sensu-client/files/plugins/check-large-receive-offload.py new file mode 100755 index 0000000..44cd520 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-large-receive-offload.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# Checks that large receive offload is disabled +# +# Return CRITICAL if large receive offload is enabled +# +# Dean Daskalantonakis + +import argparse +import re +import subprocess +import sys + +STATE_OK = 0 +STATE_WARNING = 1 +STATE_CRITICAL = 2 + + +def exit_with_error_status(warning): + if warning: + sys.exit(STATE_WARNING) + else: + sys.exit(STATE_CRITICAL) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--devices', help='primary interface devices', + required=True) + parser.add_argument('-w', '--warning', action='store_true') + args = parser.parse_args() + + crit_level = 'CRITICAL' + if args.warning: + crit_level = 'WARNING' + + for eth in [s.strip() for s in args.devices.split(',')]: + cmd = "ethtool -k %s | grep large-receive-offload | \ + grep ' off'" % (eth) + + try: + lro_check = subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + print(e.output) + print('%s: Device %s has large-receive-offload (LRO) enabled' + % (crit_level, eth)) + exit_with_error_status(args.warning) + + print('Device %s has large-receive-offload (LRO) disabled' % (eth)) + + sys.exit(STATE_OK) + +if __name__ == "__main__": + main() diff --git a/roles/sensu-client/files/plugins/check-log.rb b/roles/sensu-client/files/plugins/check-log.rb new file mode 100755 index 0000000..c5a076d --- /dev/null +++ b/roles/sensu-client/files/plugins/check-log.rb @@ -0,0 +1,140 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check Log Plugin +# === +# +# This plugin checks a log file for a regular expression, skipping lines +# that have already been read, like Nagios's check_log. However, instead +# of making a backup copy of the whole log file (very slow with large +# logs), it stores the number of bytes read, and seeks to that position +# next time. +# +# Copyright 2011 Sonian, Inc +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' +require 'fileutils' + +class CheckLog < Sensu::Plugin::Check::CLI + + BASE_DIR = '/var/cache/check-log' + + option :state_auto, + :description => "Set state file dir automatically using name", + :short => '-n NAME', + :long => '--name NAME', + :proc => proc {|arg| "#{BASE_DIR}/#{arg}" } + + option :state_dir, + :description => "Dir to keep state files under", + :short => '-s DIR', + :long => '--state-dir DIR', + :default => "#{BASE_DIR}/default" + + option :log_file, + :description => "Path to log file", + :short => '-f FILE', + :long => '--log-file FILE' + + option :pattern, + :description => "Pattern to search for", + :short => '-q PAT', + :long => '--pattern PAT' + + option :warn, + :description => "Warning level if pattern has a group", + :short => '-w N', + :long => '--warn N', + :proc => proc {|a| a.to_i } + + option :crit, + :description => "Critical level if pattern has a group", + :short => '-c N', + :long => '--crit N', + :proc => proc {|a| a.to_i } + + option :only_warn, + :description => "Warn instead of critical on match", + :short => '-o', + :long => '--warn-only', + :boolean => true + + option :silent, + :description => "do not Crit if log does not exist", + :short => '-s', + :long => '--silent', + :boolean => false + + def run + unknown "No log file specified" unless config[:log_file] + unknown "No pattern specified" unless config[:pattern] + begin + open_log + rescue => e + msg = "Could not open log file: #{e}" + config[:silent] ? ok(msg) : critical(msg) + end + n_warns, n_crits = search_log + message "#{n_warns} warnings, #{n_crits} criticals" + if n_crits > 0 + critical + elsif n_warns > 0 + warning + else + exit + end + end + + def open_log + state_dir = config[:state_auto] || config[:state_dir] + @log = File.open(config[:log_file], 'r:ISO-8859-1') + @state_file = File.join(state_dir, File.expand_path(config[:log_file])) + @bytes_to_skip = begin + File.open(@state_file) do |file| + file.readline.to_i + end + rescue + 0 + end + end + + def search_log + log_file_size = @log.stat.size + if log_file_size < @bytes_to_skip + @bytes_to_skip = 0 + end + bytes_read = 0 + n_warns = 0 + n_crits = 0 + if @bytes_to_skip > 0 + @log.seek(@bytes_to_skip, File::SEEK_SET) + end + @log.each_line do |line| + bytes_read += line.size + if m = line.match(config[:pattern]) + if m[1] + if config[:crit] && m[1].to_i > config[:crit] + n_crits += 1 + elsif config[:warn] && m[1].to_i > config[:warn] + n_warns += 1 + end + else + if config[:only_warn] + n_warns += 1 + else + n_crits += 1 + end + end + end + end + FileUtils.mkdir_p(File.dirname(@state_file)) + File.open(@state_file, 'w') do |file| + file.write(@bytes_to_skip + bytes_read) + end + [n_warns, n_crits] + end + +end diff --git a/roles/sensu-client/files/plugins/check-mcelog.sh b/roles/sensu-client/files/plugins/check-mcelog.sh new file mode 100755 index 0000000..2a70056 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-mcelog.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Queries the running mcelog daemon for accumulated errors. +# by Ulysses Kanigel + +while getopts 'z:' OPT; do + case $OPT in + z) CRITICALITY=$OPTARG;; + esac +done + +CRITICALITY=${CRITICALITY:-critical} + +trap 'exit 1' ERR +if mcelog --client | grep total | grep -v "0 total"; then + echo "mcelog reports memory errors" + exit 2 +fi +exit 0 diff --git a/roles/sensu-client/files/plugins/check-mem.sh b/roles/sensu-client/files/plugins/check-mem.sh new file mode 100755 index 0000000..e3f5c46 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-mem.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Evaluate free system memory from Linux based systems. +# +# Date: 2007-11-12 +# Author: Thomas Borger - ESG +# Date: 2012-04-02 +# Modified: Norman Harman - norman.harman@mutualmobile.com +# +# The memory check is done with following command line: +# free -m | grep buffers/cache | awk '{ print $4 }' + +# set lang +LANG=C + +# get arguments + +# #RED +while getopts 'w:c:hp' OPT; do + case $OPT in + w) WARN=$OPTARG;; + c) CRIT=$OPTARG;; + h) hlp="yes";; + p) perform="yes";; + *) unknown="yes";; + esac +done + +# usage +HELP=" + usage: $0 [ -w value -c value -p -h ] + -w --> Warning MB < value + -c --> Critical MB < value + -p --> print out performance data + -h --> print this help screen +" + +if [ "$hlp" = "yes" ]; then + echo "$HELP" + exit 0 +fi + +WARN=${WARN:=0} +CRIT=${CRIT:=0} + +set -o pipefail +FREE_MEMORY=$(free -m | grep buffers/cache | awk '{ print $4 }') +if [ $? -ne 0 ]; then + FREE_MEMORY=$(free -m | grep Mem | awk '{ print $7 }') +fi + +if [ "$FREE_MEMORY" = "" ]; then + echo "MEM UNKNOWN -" + exit 3 +fi + +if [ "$perform" = "yes" ]; then + output="free system memory: $FREE_MEMORY MB | free memory="$FREE_MEMORY"MB;$WARN;$CRIT;0" +else + output="free system memory: $FREE_MEMORY MB" +fi + +if (( $FREE_MEMORY <= $CRIT )); then + echo "MEM CRITICAL - $output" + exit 2 +elif (( $FREE_MEMORY <= $WARN )); then + echo "MEM WARNING - $output" + exit 1 +else + echo "MEM OK - $output" + exit 0 +fi \ No newline at end of file diff --git a/roles/sensu-client/files/plugins/check-netif.rb b/roles/sensu-client/files/plugins/check-netif.rb new file mode 100755 index 0000000..1b69622 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-netif.rb @@ -0,0 +1,75 @@ +#!/opt/sensu/embedded/bin/ruby +# +# netif-metrics +# +# DESCRIPTION: +# Network interface throughput +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Copyright 2014 Sonian, Inc. and contributors. +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/check/cli' +require 'socket' + +# +# Netif Metrics +# +class NetIFMetrics < Sensu::Plugin::Check::CLI + option :interfaces, + description: 'list of interfaces to check', + long: '--interfaces [eth0,eth1]', + default: 'eth0' + option :warn, + short: '-w Mbps', + default: 250, + proc: proc(&:to_i), + description: 'Warning Mbps, default: 250' + option :crit, + short: '-c Mbps', + default: 500, + proc: proc(&:to_i), + description: 'Critical Mbps, default: 500' + option :interval, + descrption: 'how many seconds to collect data for', + long: '--interval 1', + default: 1 + + def run + `sar -n DEV #{config[:interval]} 1 | grep Average | grep -v IFACE`.each_line do |line| # rubocop:disable Style/Next + stats = line.split(/\s+/) + unless stats.empty? + stats.shift + nic = stats.shift + if config[:interfaces].include? nic + rx_mbps = ( stats[2].to_f * 8 ) / 1000 + tx_mbps = ( stats[3].to_f * 8 ) / 1000 + if rx_mbps > config[:crit] || tx_mbps > config[:crit] + status = "#{nic} #{rx_mbps} rx_mbps or #{tx_mbps} tx_mbps is too high" + critical status + elsif rx_mbps > config[:warn] || tx_mbps > config[:warn] + status = "#{nic} #{rx_mbps} rx_mbps or #{tx_mbps} tx_mbps is pretty high" + warning status + end + end + end + end + exit + end +end diff --git a/roles/sensu-client/files/plugins/check-network-traffic.rb b/roles/sensu-client/files/plugins/check-network-traffic.rb new file mode 100755 index 0000000..305c91a --- /dev/null +++ b/roles/sensu-client/files/plugins/check-network-traffic.rb @@ -0,0 +1,63 @@ +#!/opt/sensu/embedded/bin/ruby +# +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckNetworkStats < Sensu::Plugin::Check::CLI + + linkspeed = `ethtool eth0 | grep Speed` + + if linkspeed.include? "10000Mb" + linkwarn = 9000 + linkcrit = 12000 + else + linkwarn = 900 + linkcrit = 1200 + end + + option :warn, + :short => '-w WARN', + :proc => proc {|a| a.to_i }, + :default => linkwarn + option :crit, + :short => '-c CRIT', + :proc => proc {|a| a.to_i }, + :default => linkcrit + option :rxmcswarn, + :short => '-pw WARN', + :proc => proc {|a| a.to_i }, + :default => 9000 + option :rxmcscrit, + :short => '-pc CRIT', + :proc => proc {|a| a.to_i }, + :default => 10000 + option :iface, + :short => '-i IFACE', + :default => "eth0" + + def run + iface = config[:iface] + line = %x{sar -n DEV 5 1}.lines.find { |x| x =~ /Average/ && x =~/\s#{iface}\s/ } + stats = line.split + unless stats.empty? + stats.shift + nic = stats.shift + stats.map! { |x| x.to_f } + rxpck, txpck, rxkB, txkB, rxcmp, txcmp, rxmcs = stats + + msg = "\nIngress kB/s=#{rxkB} \nEgress kB/s=#{txkB} \nIngress multicast packets per second=#{rxmcs}" + message msg + + warning if rxkB >= config[:warn] or rxkB <= -config[:warn] + warning if txkB >= config[:warn] or txkB <= -config[:warn] + warning if rxmcs >= config[:rxmcswarn] or rxmcs <= -config[:rxmcswarn] + critical if rxkB >= config[:crit] or rxkB <= -config[:crit] + critical if txkB >= config[:crit] or txkB <= -config[:crit] + critical if rxmcs >= config[:rxmcscrit] or rxmcs <= -config[:rxmcscrit] + + exit + end + end +end diff --git a/roles/sensu-client/files/plugins/check-ntp.rb b/roles/sensu-client/files/plugins/check-ntp.rb new file mode 100755 index 0000000..52a3ade --- /dev/null +++ b/roles/sensu-client/files/plugins/check-ntp.rb @@ -0,0 +1,33 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check NTP offset - yeah this is horrible. +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckNTP < Sensu::Plugin::Check::CLI + + option :warn, + :short => '-w WARN', + :proc => proc {|a| a.to_i }, + :default => 50 + + option :crit, + :short => '-c CRIT', + :proc => proc {|a| a.to_i }, + :default => 100 + + def run + begin + offset = `ntpq -c "rv 0 offset"`.split('=')[1].strip().to_i() + rescue + unknown "NTP command Failed" + end + + critical if offset >= config[:crit] or offset <= -config[:crit] + warning if offset >= config[:warn] or offset <= -config[:warn] + exit + + end +end diff --git a/roles/sensu-client/files/plugins/check-percona-xtrabackup.py b/roles/sensu-client/files/plugins/check-percona-xtrabackup.py new file mode 100755 index 0000000..9fd2fc5 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-percona-xtrabackup.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +# Copyright 2016 Blue Box, an IBM Company +# Copyright 2016 Paul Durivage +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import print_function + +import argparse +import os +import sys +import datetime + +# Standard Nagios return codes +OK = 0 +WARNING = 1 +CRITICAL = 2 +UNKNOWN = 3 + +# time deltas to indicate whether last backup time is warning or critical +WARNING_TIME = datetime.timedelta(days=1) +CRITICAL_TIME = datetime.timedelta(days=3) + +LOG_PATH = '/backup/percona/percona-backup.last.log' + +argparser = argparse.ArgumentParser() +argparser.add_argument('--criticality', help='Set sensu alert level, default is critical', + default='critical') +options = argparser.parse_args() + +def switch_on_criticality(): + if options.criticality == 'warning': + sys.exit(WARNING) + else: + sys.exit(CRITICAL) + +def main(): + if not os.path.isfile(LOG_PATH): + print('Log file missing: %s' % LOG_PATH, file=sys.stderr) + sys.exit(UNKNOWN) + + with open(LOG_PATH) as f: + data = f.readline() + + try: + exit_code, timestamp = data.split() + except ValueError: + print('Unable to get exit code or timestamp from log', file=sys.stderr) + sys.exit(UNKNOWN) + + if int(exit_code) != 0: + print('Critical: Last backup exited with status: %s' % exit_code, + file=sys.stderr) + switch_on_criticality() + + try: + parsed = datetime.datetime.fromtimestamp(float(timestamp)) + except ValueError: + print("Couldn't parse date from log file", file=sys.stderr) + sys.exit(UNKNOWN) + + now = datetime.datetime.now() + if now - parsed > CRITICAL_TIME: + print('Critical: Last backup greater than 72 hours ago', + file=sys.stderr) + switch_on_criticality() + elif now - parsed > WARNING_TIME: + print('Warning: Last backup greater than 24 hours ago', + file=sys.stderr) + sys.exit(WARNING) + else: + sys.exit(OK) + + +if __name__ == '__main__': + main() diff --git a/roles/sensu-client/files/plugins/check-port-listening.sh b/roles/sensu-client/files/plugins/check-port-listening.sh new file mode 100755 index 0000000..1b7f062 --- /dev/null +++ b/roles/sensu-client/files/plugins/check-port-listening.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Check how many files are larger than X in DIR. +# + +# get arguments +while getopts 'l:i:p:v:t:h' OPT; do + case $OPT in + l) LEVEL=$OPTARG;; + h) hlp="yes";; + i) IP=$OPTARG;; + p) PORT=$OPTARG;; + v) IPV=$OPTARG;; + t) PROTO=$OPTARG;; + esac +done + +# usage +HELP=" + Tests if local system is listening on a specific port + usage: $0 -l [crit|warn] -p 8080 [-i 127.0.0.1] -h + + -l --> Level: crit|warn + -p --> Port: port to check for. + -v --> 4|6 - IPv4 or IPv6, default 4. + -i --> IP to check for. + -t --> Protocol: tcp, udp, dccp, raw, unix, default: tcp + -h --> print this help screen +" + +if [ "$hlp" = "yes" ]; then + echo "$HELP" + exit 0 +fi + +if [[ -z $PORT ]]; then + echo Must specify Port. + echo $HELP + exit 0 +fi + +LEVEL=${LEVEL:-warn} +IP=${IP:-''} +IPV=${IPV:-4} +PROTO=${PROTO:-tcp} + +[[ $LEVEL == 'crit' ]] && EXIT=1 || EXIT=2 + +OPTIONS="--listening --numeric --${PROTO}" +[[ -n $IPV ]] && OPTIONS+=" -${IPV}" + +OUTPUT=$(ss $OPTIONS | awk '{print $4}' | grep ${IP}:${PORT}) +ret=$? + +if [[ ${ret} != 0 ]]; then + echo "not listening on $PROTO/$IP:$PORT" + exit $EXIT +fi diff --git a/roles/sensu-client/files/plugins/check-procs.rb b/roles/sensu-client/files/plugins/check-procs.rb new file mode 100755 index 0000000..15afd1c --- /dev/null +++ b/roles/sensu-client/files/plugins/check-procs.rb @@ -0,0 +1,132 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check Procs +# === +# +# Finds processes matching various filters (name, state, etc). Will not +# match itself by default. The number of processes found will be tested +# against the Warning/critical thresholds. By default, fails with a +# CRITICAL if more than one process matches -- you must specify values +# for -w and -c to override this. +# +# Attempts to work on Cygwin (where ps does not have the features we +# need) by calling Windows' tasklist.exe, but this is not well tested. +# +# Examples: +# +# # chef-client is running +# check-procs -p chef-client -W 1 +# +# # there are not too many zombies +# check-procs -s Z -w 5 -c 10 +# +# Copyright 2011 Sonian, Inc +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckProcs < Sensu::Plugin::Check::CLI + + def self.read_pid(path) + begin + File.read(path).chomp.to_i + rescue + self.new.unknown "Could not read pid file #{path}" + end + end + + option :warn_over, :short => '-w N', :proc => proc {|a| a.to_i } + option :crit_over, :short => '-c N', :proc => proc {|a| a.to_i } + option :warn_under, :short => '-W N', :proc => proc {|a| a.to_i }, :default => 1 + option :crit_under, :short => '-C N', :proc => proc {|a| a.to_i }, :default => 1 + option :metric, :short => '-t METRIC', :proc => proc {|a| a.to_sym } + + option :match_self, :short => '-m', :boolean => true, :default => false + option :match_parent, :short => '-M', :boolean => true, :default => false + option :cmd_pat, :short => '-p PATTERN' + option :file_pid, :short => '-f PATH', :proc => proc {|a| read_pid(a) } + option :vsz, :short => '-z VSZ', :proc => proc {|a| a.to_i } + option :rss, :short => '-r RSS', :proc => proc {|a| a.to_i } + option :pcpu, :short => '-P PCPU', :proc => proc {|a| a.to_f } + option :state, :short => '-s STATE', :proc => proc {|a| a.split(',') } + option :user, :short => '-u USER', :proc => proc {|a| a.split(',') } + + def read_lines(cmd) + IO.popen(cmd + ' 2>&1') do |child| + child.read.split("\n") + end + end + + def line_to_hash(line, *cols) + Hash[cols.zip(line.strip.split(/\s+/, cols.size))] + end + + def on_cygwin? + `ps -W 2>&1`; $?.exitstatus == 0 + end + + def get_procs + if on_cygwin? + read_lines('ps -aWl').drop(1).map do |line| + # Horrible hack because cygwin's ps has no o option, every + # format includes the STIME column (which may contain spaces), + # and the process state (which isn't actually a column) can be + # blank. As of revision 1.35, the format is: + # const char *lfmt = "%c %7d %7d %7d %10u %4s %4u %8s %s\n"; + state = line.slice!(0..0) + _stime = line.slice!(45..53) + line_to_hash(line, :pid, :ppid, :pgid, :winpid, :tty, :uid, :command).merge(:state => state) + end + else + read_lines('ps axwwo user,pid,vsz,rss,pcpu,state,command').drop(1).map do |line| + line_to_hash(line, :user, :pid, :vsz, :rss, :pcpu, :state, :command) + end + end + end + + def run + procs = get_procs + + procs.reject! {|p| p[:pid].to_i != config[:file_pid] } if config[:file_pid] + procs.reject! {|p| p[:pid].to_i == $$ } unless config[:match_self] + procs.reject! {|p| p[:pid].to_i == Process.ppid } unless config[:match_parent] + procs.reject! {|p| p[:command] !~ /#{config[:cmd_pat]}/ } if config[:cmd_pat] + procs.reject! {|p| p[:vsz].to_f < config[:vsz] } if config[:vsz] + procs.reject! {|p| p[:rss].to_f < config[:rss] } if config[:rss] + procs.reject! {|p| p[:pcpu].to_f < config[:pcpu] } if config[:pcpu] + procs.reject! {|p| !config[:state].include?(p[:state]) } if config[:state] + procs.reject! {|p| !config[:user].include?(p[:user]) } if config[:user] + + msg = "Found #{procs.size} matching processes" + msg += "; cmd /#{config[:cmd_pat]}/" if config[:cmd_pat] + msg += "; state #{config[:state].join(',')}" if config[:state] + msg += "; user #{config[:user].join(',')}" if config[:user] + msg += "; vsz > #{config[:vsz]}" if config[:vsz] + msg += "; rss > #{config[:rss]}" if config[:rss] + msg += "; pcpu > #{config[:pcpu]}" if config[:pcpu] + msg += "; pid #{config[:file_pid]}" if config[:file_pid] + + if config[:metric] + count = procs.map {|p| p[config[:metric]].to_i }.reduce {|a, b| a + b } + msg += "; #{config[:metric]} == #{count}" + else + count = procs.size + end + + if config[:crit_over] && count > config[:crit_over] + critical msg + elsif config[:crit_under] != 0 && count < config[:crit_under] + critical msg + elsif config[:warn_over] && count > config[:warn_over] + warning msg + elsif config[:warn_under] != 0 && count < config[:warn_under] + warning msg + else + ok msg + end + end + +end diff --git a/roles/sensu-client/files/plugins/check-rabbitmq-cluster.rb b/roles/sensu-client/files/plugins/check-rabbitmq-cluster.rb new file mode 100755 index 0000000..7286cfa --- /dev/null +++ b/roles/sensu-client/files/plugins/check-rabbitmq-cluster.rb @@ -0,0 +1,45 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check Rabbitmq Cluster +# === +# +# Purpose: to check the health of the rabbitmq cluster. +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckRabbitCluster < Sensu::Plugin::Check::CLI + option :expected, + :description => "Number of servers expected in the cluster", + :short => '-e NUMBER', + :long => '--expected NUMBER', + :default => 2 + + option :criticality, + :description => "Set sensu alert level, default is critical", + :short => '-z CRITICALITY', + :long => '--criticality CRITICALITY', + :default => 'critical' + + def switch_on_criticality() + if config[:criticality] == 'warning' + warning + else + critical + end + end + + def run + cmd = "/usr/sbin/rabbitmqctl -q cluster_status | awk '/disc/,/\},/' | awk '/@/ {++nodes} END {print nodes}' | grep #{config[:expected]}" + system(cmd) + + if $?.exitstatus == 0 + exit + else + switch_on_criticality() + end + end +end diff --git a/roles/sensu-client/files/plugins/check-rabbitmq-queues.rb b/roles/sensu-client/files/plugins/check-rabbitmq-queues.rb new file mode 100755 index 0000000..4065efe --- /dev/null +++ b/roles/sensu-client/files/plugins/check-rabbitmq-queues.rb @@ -0,0 +1,96 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check Rabbitmq Queues +# === +# +# Purpose: to check the size or number of rabbitmq queues. +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckRabbitCluster < Sensu::Plugin::Check::CLI + option :warning, + :description => "Minimum number of messages in the queue before alerting warning", + :short => '-w NUMBER', + :long => '--warn NUMBER' + + option :critical, + :description => "Minimum number of messages in the queue before alerting critical", + :short => '-c NUMBER', + :long => '--crit NUMBER' + + option :ignore, + :description => "Comma-separated list of queues to ignore in our check", + :short => '-i QUEUE,QUEUE,...', + :long => '--ignore QUEUE,QUEUE,...', + :default => nil + + option :type, + :description => "Type of check to perform", + :short => '-t TYPE', + :long => '--type TYPE', + :valid => %w[length number], + :default => 'length' + + option :timeout, + :description => "timeout in seconds when querying rabbit", + :short => '-m SECONDS', + :long => '--timeout SECONDS', + :default => 3 + + def set_defaults + if config[:type] == 'length' + config[:warning] = 5 unless !config[:warning].nil? + config[:critical] = 20 unless !config[:critical].nil? + else + config[:warning] = 200 unless !config[:warning].nil? + config[:critical] = 400 unless !config[:critical].nil? + end + end + + def run + + set_defaults + + ignored_queues = [] + ignored_queues = config[:ignore].split(',') unless config[:ignore] == nil + + count = 0 + cmd = "/usr/bin/timeout -s 9 #{config[:timeout]}s /usr/sbin/rabbitmqctl list_queues -p /" + process = IO.popen(cmd) do |io| + while line = io.gets + line.chomp! + lineparts = line.split(/\s+/) + + if /^Listing queues/ =~ line || /^\.\.\.done/ =~ line || ignored_queues.include?(lineparts[0]) + next + end + + if config[:type] == 'number' + count += 1 + else + count += lineparts[1].to_i + end + end + end + critical "Listing queues is timing out" if $?.to_i == 137 + critical "Error checking rabbit queues" if $?.to_i > 0 + + # Queue size checking + queue_count = count.to_i + msg = "Queues not empty" + msg = "Number of queues" if config[:type] == 'number' + + if queue_count > 0 + if queue_count > config[:critical].to_i + critical "CRITICAL: #{msg}: #{queue_count}" + elsif queue_count > config[:warning].to_i + warning "WARNING: #{msg}: #{queue_count}" + end + end + exit + end +end diff --git a/roles/sensu-client/files/plugins/check-raid.sh b/roles/sensu-client/files/plugins/check-raid.sh new file mode 100755 index 0000000..c84dbcc --- /dev/null +++ b/roles/sensu-client/files/plugins/check-raid.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +while getopts 'z' OPT; do + case $OPT in + z) CRITICALITY=$OPTARG;; + esac +done + +CRITICALITY=${CRITICALITY:-critical} + +if lspci | grep RAID | grep -i 3ware >> /dev/null; then + sudo check_3ware_raid.py -b /usr/sbin/tw-cli -z $CRITICALITY +elif lspci | grep RAID | grep -i "MegaRAID" >> /dev/null; then + if [[ -e /etc/sensu/plugins/check-storcli.pl && -e /opt/MegaRAID/storcli/storcli64 ]];then + sudo check-storcli.pl -p /opt/MegaRAID/storcli/storcli64 -Io 63 -z $CRITICALITY + else + sudo check_megaraid_sas.pl -b /usr/sbin/megacli -o 63 -z $CRITICALITY + fi +elif lspci | grep RAID | grep -i "Adaptec" >> /dev/null; then + sudo check_adaptec_raid.py -z $CRITICALITY +fi; diff --git a/roles/sensu-client/files/plugins/check-serverspec.rb b/roles/sensu-client/files/plugins/check-serverspec.rb new file mode 100755 index 0000000..ee2a81e --- /dev/null +++ b/roles/sensu-client/files/plugins/check-serverspec.rb @@ -0,0 +1,90 @@ +#!/opt/sensu/embedded/bin/ruby +# +# check-serverspec +# +# DESCRIPTION: +# Runs http://serverspec.org/ spec tests against your servers. +# Fails with a warning or a critical if tests are failing, depending +# on the severity level set. +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: json +# gem: socket +# gem: serverspec +# +# USAGE: +# Run entire suite of testd +# check-serverspec -d /etc/my_tests_dir +# +# Run only one set of tests +# check-serverspec -d /etc/my_tests_dir -t spec/test_one +# +# Run with a warning severity level +# check-serverspec -d /etc/my_tests_dir -s warning +# +# NOTES: +# Critical severity level is set as the default +# +# LICENSE: +# Copyright 2014 Sonian, Inc. and contributors. +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/check/cli' +require 'json' +require 'socket' +require 'serverspec' + + +class CheckServerspec < Sensu::Plugin::Check::CLI + option :tests_dir, + short: '-d /tmp/dir', + long: '--tests-dir /tmp/dir', + required: true + + option :spec_tests, + short: '-t spec/test', + long: '--spec-tests spec/test', + default: nil + + option :severity, + short: '-s severity', + long: '--severity severity', + default: 'critical' + + def run + serverspec_results = `cd #{config[:tests_dir]} ; /opt/sensu/embedded/bin/rspec #{config[:spec_tests]} --format json` + parsed = JSON.parse(serverspec_results) + num_failures = parsed['summary_line'].split[2] + + failures = [] + parsed['examples'].each do |serverspec_test| + test_name = serverspec_test['file_path'].split('/')[-1] + output = serverspec_test['full_description'].gsub!(/\"/, '') + + if serverspec_test['status'] != 'passed' + failures << "#{serverspec_test['status'].upcase}: #{test_name}:#{serverspec_test['line_number']}, #{serverspec_test['full_description']}" + end + end + str_failures = failures.join("\n") + + if num_failures != '0' + if config[:severity] == 'warning' + warning [parsed['summary_line'], '', str_failures].join("\n") + else + critical [parsed['summary_line'], '', str_failures].join("\n") + end + else + ok parsed['summary_line'] + end + end +end + diff --git a/roles/sensu-client/files/plugins/check-storcli.pl b/roles/sensu-client/files/plugins/check-storcli.pl new file mode 100755 index 0000000..b1eaf4f --- /dev/null +++ b/roles/sensu-client/files/plugins/check-storcli.pl @@ -0,0 +1,1287 @@ +#!/usr/bin/perl -w +# ====================================================================================== +# check_lsi_raid: Nagios/Icinga plugin to check LSI Raid Controller status +# -------------------------------------------------------------------------------------- +# Created as part of a semester project at the University of Applied Sciences Hagenberg +# (http://www.fh-ooe.at/en/hagenberg-campus/) +# +# Copyright (c) 2013-2014: +# Georg Schoenberger (gschoenberger@thomas-krenn.com) +# Grubhofer Martin (s1110239013@students.fh-hagenberg.at) +# Scheipner Alexander (s1110239032@students.fh-hagenberg.at) +# Werner Sebastian (s1110239038@students.fh-hagenberg.at) +# Jonas Meurer (jmeurer@inet.de) +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, see . +# ====================================================================================== +use strict; +use warnings; +use Getopt::Long qw(:config no_ignore_case); + +our $VERBOSITY = 0; +our $VERSION = "2.1"; +our $NAME = "check_lsi_raid: Nagios/Icinga plugin to check LSI Raid Controller status"; +our $C_TEMP_WARNING = 80; +our $C_TEMP_CRITICAL = 90; +our $PD_TEMP_WARNING = 40; +our $PD_TEMP_CRITICAL = 45; +our $BBU_TEMP_WARNING = 50; +our $BBU_TEMP_CRITICAL = 60; +our $CV_TEMP_WARNING = 70; +our $CV_TEMP_CRITICAL = 85; +our ($IGNERR_M, $IGNERR_O, $IGNERR_P, $IGNERR_S, $IGNERR_B) = (0, 0, 0, 0, 0); +our $NOENCLOSURES = 0; +our $CONTROLLER = 0; +our $criticality = "critical"; + +use constant { + STATE_OK => 0, + STATE_WARNING => 1, + STATE_CRITICAL => 2, + STATE_UNKNOWN => 3, + }; + +# Header maps to parse logical and physical devices +our $LDMAP; +our @map_a = ('DG/VD','TYPE','State','Access','Consist','Cache','sCC','Size'); +our @map_cc_a = ('DG/VD','TYPE','State','Access','Consist','Cache','Cac','sCC','Size'); +our @pdmap_a = ('EID:Slt','DID','State','DG','Size','Intf','Med','SED','PI','SeSz','Model','Sp'); + +# Print command line usage to stdout. +sub displayUsage { + print "Usage: \n"; + print " [ -h | --help ] + Display this help page\n"; + print " [ -v | -vv | -vvv | --verbose ] + Sets the verbosity level. + No -v is the normal single line output for Nagios/Icinga, -v is a + more detailed version but still usable in Nagios. -vv is a + multiline output for debugging configuration errors or more + detailed information. -vvv is for plugin problem diagnosis. + For further information please visit: + http://nagiosplug.sourceforge.net/developer-guidelines.html#AEN39\n"; + print " [ -V --version ] + Displays the plugin and, if available, the version if StorCLI.\n"; + print " [ -C | --controller ] + Specifies a controller number, defaults to 0.\n"; + print " [ -EID | --enclosure ] + Specifies one or more enclosure numbers, per default all enclosures. Takes either + an integer as additional argument or a commaseperated list, + e.g. '0,1,2'. With --noenclosures enclosures can be disabled.\n"; + print " [ -LD | --logicaldevice ] + Specifies one or more logical devices, defaults to all. Takes either an + integer as additional argument or a comma seperated list e.g. '0,1,2'.\n"; + print " [ -PD | --physicaldevice ] + Specifies one or more physical devices, defaults to all. Takes either an + integer as additional argument or a comma seperated list e.g. '0,1,2'.\n"; + print " [ -Tw | --temperature-warn ] + Specifies the RAID controller temperature warning threshold, the default + threshold is ${C_TEMP_WARNING}C.\n"; + print " [ -Tc | --temperature-critical ] + Specifies the RAID controller temperature critical threshold, the default + threshold is ${C_TEMP_CRITICAL}C.\n"; + print " [ -PDTw | --physicaldevicetemperature-warn ] + Specifies the disk temperature warning threshold, the default threshold + is ${PD_TEMP_WARNING}C.\n"; + print " [ -PDTc | --physicaldevicetemperature-critical ] + Specifies the disk temperature critical threshold, the default threshold + is ${PD_TEMP_CRITICAL}C.\n"; + print " [ -BBUTw | --bbutemperature-warning ] + Specifies the BBU temperature warning threshold, default threshold + is ${BBU_TEMP_WARNING}C.\n"; + print " [ -BBUTc | --bbutemperature-critical ] + Specifies the BBU temperature critical threshold, default threshold + is ${BBU_TEMP_CRITICAL}C.\n"; + print " [ -CVTw | --cvtemperature-warning ] + Specifies the CV temperature warning threshold, default threshold + is ${CV_TEMP_WARNING}C.\n"; + print " [ -CVTc | --cvtemperature-critical ] + Specifies the CV temperature critical threshold, default threshold + is ${CV_TEMP_CRITICAL}C.\n"; + print " [ -Im | --ignore-media-errors ] + Specifies the warning threshold for media errors per disk, the default + threshold is $IGNERR_M.\n"; + print " [ -Io | --ignore-other-errors ] + Specifies the warning threshold for media errors per disk, the default + threshold is $IGNERR_O.\n"; + print " [ -Ip | --ignore-predictive-fail-count ] + Specifies the warning threshold for media errors per disk, the default + threshold is $IGNERR_P.\n"; + print " [ -Is | --ignore-shield-counter ] + Specifies the warning threshold for media errors per disk, the default + threshold is $IGNERR_S.\n"; + print " [ -Ib | --ignore-bbm-counter ] + Specifies the warning threshold for bbm errors per disk, the default + threshold is $IGNERR_B.\n"; + print " [ -p | --path ] + Specifies the path to StorCLI, per default uses the tool 'which' to get + the StorCLI path.\n"; + print " [ -z | --criticality ] + Specifies the criticality alert level for this check, the default is + critical for this alert.\n"; + print " [ -b <0/1> | --BBU <0/1> ] + Check if a BBU or a CacheVault module is present. One must be present unless + '-b 0' is defined. This ensures that for a given controller a BBU/CV must be + present per default.\n"; + print " [ --noenclosures <0/1> ] + Specifies if enclosures are present or not. 0 means enclosures are + present (default), 1 states no enclosures are used (no 'eall' in + storcli commands).\n" +} + +# Displays a short Help text for the user +sub displayHelp { + print $NAME."\n"; + print "Pulgin version: " . $VERSION ."\n"; + print "Copyright (C) 2013-2014 Thomas-Krenn.AG\n"; + print "Current updates available at + http://git.thomas-krenn.com/check_lsi_raid.git\n"; + print "This Nagios/Icinga Plugin checks LSI RAID controllers for controller, +physical device, logical device, BBU and CV warnings and errors.\n"; + print "In order for this plugin to work properly you need to add the nagios +user to your sudoers file (or create a new one in /etc/sudoers.d/).\n"; + displayUsage(); + print "Further information about this plugin can be found at: + http://www.thomas-krenn.com/de/wiki/LSI_RAID_Monitoring_Plugin and + http://www.thomas-krenn.com/de/wiki/LSI_RAID_Monitoring_Plugin +Please send an email to the tk-monitoring plugin-user mailing list: + tk-monitoring-plugins-user\@lists.thomas-krenn.com +if you have questions regarding use of this software, to submit patches, or +suggest improvements. +Example usage: +* check_lsi_raid -p /opt/MegaRAID/storcli/storcli64 +* check_lsi_raid -p /opt/MegaRAID/storcli/storcli64 -C 1\n"; + exit(STATE_OK); +} + +# Prints the name anmd the version of check_lsi_raid. If storcli is available, +# the version if it is printed also. +# @param storcli The path to storcli command utility +sub displayVersion { + my $storcli = shift; + if(defined($storcli)){ + my @storcliVersion = `$storcli -v`; + foreach my $line (@storcliVersion){ + if($line =~ /^\s+StorCli.*/) { + $line =~ s/^\s+|\s+$//g; + print $line; + } + } + print "\n"; + } + exit(STATE_OK); +} + +# Checks if a storcli call was successfull, i.e. if the line 'Status = Sucess' +# is present in the command output. +# @param output The output of the storcli command as array +# @return 1 on success, 0 if not +sub checkCommandStatus{ + my @output = @{(shift)}; + foreach my $line (@output){ + if($line =~ /^Status/){ + if($line eq "Status = Success\n"){ + return 1; + } + else{ + return 0; + } + } + } +} + +# Shows the time the controller is using. Can be used to check if the +# controller number is a correct one. +# @param storcli The path to storcli command utility, followed by the controller +# number, e.g. 'storcli64 /c0'. +# @return 1 on success, 0 if not +sub getControllerTime{ + my $storcli = shift; + my @output = `$storcli show time`; + return (checkCommandStatus(\@output)); +} + +# Get the status of the raid controller +# @param storcli The path to storcli command utility, followed by the controller +# number, e.g. 'storcli64 /c0'. +# @param logDevices If given, a list of desired logical device numbers +# @param commands_a An array to push the used command to +# @return A hash, each key a value of the raid controller info +sub getControllerInfo{ + my $storcli = shift; + my $commands_a = shift; + my $command = ''; + + $storcli =~ /^(.*)\/c[0-9]+/; + $command = $1.'adpallinfo a'.$CONTROLLER; + push @{$commands_a}, $command; + my @output = `$command`; + if($? >> 8 != 0){ + print "Invalid StorCLI command! ($command)\n"; + exit(STATE_UNKNOWN); + } + my %foundController_h; + foreach my $line(@output){ + if($line =~ /\:/){ + my @lineVals = split(':', $line); + $lineVals[0] =~ s/^\s+|\s+$//g; + $lineVals[1] =~ s/^\s+|\s+$//g; + $foundController_h{$lineVals[0]} = $lineVals[1]; + } + } + return \%foundController_h; +} + +# Checks the status of the raid controller +# @param statusLevel_a The status level array, elem 0 is the current status, +# elem 1 the warning sensors, elem 2 the critical sensors, elem 3 the verbose +# information for the sensors. +# @param foundController The hash of controller infos, created by getControllerInfo +sub getControllerStatus{ + my @statusLevel_a = @{(shift)}; + my %foundController = %{(shift)}; + my $status = 'OK'; + foreach my $key (%foundController){ + if($key eq 'ROC temperature'){ + $foundController{$key} =~ /^([0-9]+\.?[0-9]+).*$/; + if(defined($1)){ + if(!(checkThreshs($1, $C_TEMP_CRITICAL))){ + $status = getMaxStatus('Critical',$status); + push @{$statusLevel_a[2]}, 'ROC_Temperature'; + } + elsif(!(checkThreshs($1, $C_TEMP_WARNING))){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, 'ROC_Temperature'; + } + $statusLevel_a[3]->{'ROC_Temperature'} = $1; + } + } + elsif($key eq 'Degraded'){ + if($foundController{$key} != 0){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, 'CTR_Degraded_drives'; + $statusLevel_a[3]->{'CTR_Degraded_drives'} = $foundController{$key}; + } + } + elsif($key eq 'Offline'){ + if($foundController{$key} != 0){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, 'CTR_Offline_drives'; + $statusLevel_a[3]->{'CTR_Offline_drives'} = $foundController{$key}; + } + } + elsif($key eq 'Critical Disks'){ + if($foundController{$key} != 0){ + $status = getMaxStatus('Critical',$status); + push @{$statusLevel_a[2]}, 'CTR_Critical_disks'; + $statusLevel_a[3]->{'CTR_Critical_disks'} = $foundController{$key}; + } + } + elsif($key eq 'Failed Disks'){ + if($foundController{$key} != 0){ + $status = getMaxStatus('Critical',$status); + push @{$statusLevel_a[2]}, 'CTR_Failed_disks'; + $statusLevel_a[3]->{'CTR_Failed_disks'} = $foundController{$key}; + } + } + elsif($key eq 'Memory Correctable Errors'){ + if($foundController{$key} != 0){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, 'CTR_Memory_correctable_errors'; + $statusLevel_a[3]->{'CTR_Memory_correctable_errors'} = $foundController{$key}; + } + } + elsif($key eq 'Memory Uncorrectable Errors'){ + if($foundController{$key} != 0){ + $status = getMaxStatus('Critical',$status); + push @{$statusLevel_a[2]}, 'CTR_Memory_Uncorrectable_errors'; + $statusLevel_a[3]->{'CTR_Memory_Uncorrectable_errors'} = $foundController{$key}; + } + } + } + $statusLevel_a[3]->{'CTR_Status'} = $status; + ${$statusLevel_a[0]} = getMaxStatus(${$statusLevel_a[0]},$status); +} + +# Checks which logical devices are present for the given controller and parses +# the logical devices to a list of hashes. Each hash represents a logical device +# with its values from the output. +# @param storcli The path to storcli command utility, followed by the controller +# number, e.g. 'storcli64 /c0'. +# @param logDevices If given, a list of desired logical device numbers +# @param action The storcli action to check, 'all' or 'init' +# @param commands_a An array to push the used command to +# @return A list of hashes, each hash is one logical device. Check ldmap_a for valid +# hash keys. +sub getLogicalDevices{ + my $storcli = shift; + my @logDevices = @{(shift)}; + my $action = shift; + my $commands_a = shift; + + my $command = $storcli; + if(scalar(@logDevices) == 0) { $command .= "/vall"; } + elsif(scalar(@logDevices) == 1) { $command .= "/v$logDevices[0]"; } + else { $command .= "/v".join(",", @logDevices); } + $command .= " show $action"; + push @{$commands_a}, $command; + + my @output = `$command`; + my @foundDevs; + if(checkCommandStatus(\@output)) { + if($action eq "all") { + my $currBlock; + foreach my $line(@output){ + my @splittedLine; + if($line =~ /^\/(c[0-9]*\/v[0-9]*).*/){ + $currBlock = $1; + next; + } + if(defined($currBlock)){ + if($line =~ /^DG\/VD TYPE.*/){ + @splittedLine = split(' ', $line); + if(scalar(@splittedLine)== 9){ + $LDMAP = \@map_a; + } + if(scalar(@splittedLine)== 10){ + $LDMAP = \@map_cc_a; + } + } + if($line =~ /^\d+\/\d+\s+\w+\d\s+\w+.*/){ + @splittedLine = map { s/^\s*//; s/\s*$//; $_; } split(/\s+/,$line); + my %lineValues_h; + # The current block is the c0/v0 name + $lineValues_h{'ld'} = $currBlock; + for(my $i = 0; $i < @{$LDMAP}; $i++){ + $lineValues_h{$LDMAP->[$i]} = $splittedLine[$i]; + } + push @foundDevs, \%lineValues_h; + } + } + } + } + elsif($action eq "init") { + foreach my $line(@output){ + $line =~ s/^\s+|\s+$//g;#trim line + if($line =~ /^([0-9]+)\s+INIT.*$/){ + my $vdNum = 'c'.$CONTROLLER.'/v'.$1; + if($line !~ /Not in progress/i){ + my %lineValues_h; + my @vals = split('\s+',$line); + $lineValues_h{'ld'} = $vdNum; + $lineValues_h{'init'} = $vals[2]; + push @foundDevs, \%lineValues_h; + } + } + } + } + } + else { + print "Invalid StorCLI command! ($command)\n"; + exit(STATE_UNKNOWN); + } + return \@foundDevs; +} + +# Checks the status of the logical devices. +# @param statusLevel_a The status level array, elem 0 is the current status, +# elem 1 the warning sensors, elem 2 the critical sensors, elem 3 the verbose +# information for the sensors. +# @param foundLDs The array of logical devices, created by getLogicalDevices +sub getLDStatus{ + my @statusLevel_a = @{(shift)}; + my @foundLDs = @{(shift)}; + my $status = 'OK'; + foreach my $LD (@foundLDs){ + if(exists($LD->{'State'})){ + if($LD->{'State'} ne 'Optl'){ + $status = getMaxStatus('Critical', $status); + push @{$statusLevel_a[2]}, $LD->{'ld'}.'_State'; + $statusLevel_a[3]->{$LD->{'ld'}.'_State'} = $LD->{'State'}; + } + } + if(exists($LD->{'Consist'})){ + if($LD->{'Consist'} ne 'Yes' && $LD->{'TYPE'} ne 'Cac1'){ + $status = getMaxStatus('Warning', $status); + push @{$statusLevel_a[1]}, $LD->{'ld'}.'_Consist'; + $statusLevel_a[3]->{$LD->{'ld'}.'_Consist'} = $LD->{'Consist'}; + } + } + if(exists($LD->{'init'})){ + $status = getMaxStatus('Warning', $status); + push @{$statusLevel_a[1]}, $LD->{'ld'}.'_Init'; + $statusLevel_a[3]->{$LD->{'ld'}.'_Init'} = $LD->{'init'}; + } + } + if (exists($statusLevel_a[3]->{'LD_Status'})) { + $statusLevel_a[3]->{'LD_Status'} = getMaxStatus($statusLevel_a[3]->{'LD_Status'} ,$status); + } + else { + $statusLevel_a[3]->{'LD_Status'} = $status; + } + + ${$statusLevel_a[0]} = getMaxStatus(${$statusLevel_a[0]},$status); +} + +# Checks which physical devices are present for the given controller and parses +# the physical devices to a list of hashes. Each hash represents a physical device +# with its values from the output. +# @param storcli The path to storcli command utility, followed by the controller +# number, e.g. 'storcli64 /c0'. +# @param physDevices If given, a list of desired physical device numbers +# @param action The storcli action to check, 'all', 'initialization' or 'rebuild' +# @param commands_a An array to push the used command to +# @return A list of hashes, each hash is one physical device. Check pdmap_a for valid +# hash keys. +sub getPhysicalDevices{ + my $storcli = shift; + my @enclosures = @{(shift)}; + my @physDevices = @{(shift)}; + my $action = shift; + my $commands_a = shift; + + my $command = $storcli; + if(!$NOENCLOSURES){ + if(scalar(@enclosures) == 0) { $command .= "/eall"; } + elsif(scalar(@enclosures) == 1) { $command .= "/e$enclosures[0]"; } + else { $command .= "/e".join(",", @enclosures); } + } + if(scalar(@physDevices) == 0) { $command .= "/sall"; } + elsif(scalar(@physDevices) == 1) { $command .= "/s$physDevices[0]"; } + else { $command .= "/s".join(",", @physDevices); } + $command .= " show $action"; + push @{$commands_a}, $command; + + my @output = `$command`; + my @foundDevs; + if(checkCommandStatus(\@output)){ + if($action eq "all") { + my $currBlock; + my $line_ref; + foreach my $line(@output){ + my @splittedLine; + if($line =~ /^Drive \/(c[0-9]*\/e[0-9]*\/s[0-9]*) \:$/){ + $currBlock = $1; + $line_ref = {}; + next; + } + if(defined($currBlock)){ + # If a drive is not in a group, a - is at the DG column + if($line =~ /^\d+\:\d+\s+\d+\s+\w+\s+[0-9-F]+.*/){ + @splittedLine = map { s/^\s*//; s/\s*$//; $_; } split(/\s+/,$line); + # The current block is the c0/e252/s0 name + $line_ref->{'pd'} = $currBlock; + my $j = 0; + for(my $i = 0; $i < @pdmap_a; $i++){ + if($pdmap_a[$i] eq 'Size'){ + my $size = $splittedLine[$j]; + if($splittedLine[$j+1] eq 'GB' || $splittedLine[$j+1] eq 'TB'){ + $size .= ''.$splittedLine[$j+1]; + $j++; + } + $line_ref->{$pdmap_a[$i]} = $size; + $j++; + } + elsif($pdmap_a[$i] eq 'Model'){ + my $model = $splittedLine[$j]; + # Model should be the next last element, j starts at 0 + if(($j+2) != scalar(@splittedLine)){ + $model .= ' '.$splittedLine[$j+1]; + $j++; + } + $line_ref->{$pdmap_a[$i]} = $model; + $j++; + } + else{ + $line_ref->{$pdmap_a[$i]} = $splittedLine[$j]; + $j++; + } + } + } + if($line =~ /^(Shield Counter|Media Error Count|Other Error Count|BBM Error Count|Drive Temperature|Predictive Failure Count|S\.M\.A\.R\.T alert flagged by drive)\s\=\s+(.*)$/){ + $line_ref->{$1} = $2; + } + # If the last value is parsed, set up for the next device + if(exists($line_ref->{'S.M.A.R.T alert flagged by drive'})){ + push @foundDevs, $line_ref; + undef $currBlock; + undef $line_ref; + } + } + } + } + elsif($action eq 'rebuild' || $action eq 'initialization') { + foreach my $line(@output){ + $line =~ s/^\s+|\s+$//g;#trim line + if($line =~ /^\/c$CONTROLLER\/.*/){ + if($line !~ /Not in progress/i){ + my %lineValues_h; + my @vals = split('\s+',$line); + my $key; + if($action eq 'rebuild'){ $key = 'rebuild'; } + if($action eq 'initialization'){ $key = 'init'; } + $lineValues_h{'pd'} = substr($vals[0], 1); + $lineValues_h{$key} = $vals[1]; + push @foundDevs, \%lineValues_h; + } + } + } + } + } + else { + print "Invalid StorCLI command! ($command)\n"; + exit(STATE_UNKNOWN); + } + return \@foundDevs; +} + +# Checks the status of the physical devices. +# @param statusLevel_a The status level array, elem 0 is the current status, +# elem 1 the warning sensors, elem 2 the critical sensors, elem 3 the vebose +# information for the sensors. +# @param foundPDs The array of physical devices, created by getPhysicalDevices +sub getPDStatus{ + my @statusLevel_a = @{(shift)}; + my @foundPDs = @{(shift)}; + my $status = 'OK'; + foreach my $PD (@foundPDs){ + if(exists($PD->{'State'})){ + if($PD->{'State'} ne 'Onln' && $PD->{'State'} ne 'UGood' && $PD->{'State'} ne 'GHS'){ + $status = getMaxStatus('Critical',$status); + push @{$statusLevel_a[2]}, $PD->{'pd'}.'_State'; + $statusLevel_a[3]->{$PD->{'pd'}.'_State'} = $PD->{'State'}; + } + } + if(exists($PD->{'Shield Counter'})){ + if($PD->{'Shield Counter'} > $IGNERR_S){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_Shield_counter'; + $statusLevel_a[3]->{$PD->{'pd'}.'_Shield_counter'} = $PD->{'Shield Counter'}; + } + } + if(exists($PD->{'Media Error Count'})){ + if($PD->{'Media Error Count'} > $IGNERR_M){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_Media_error_count'; + $statusLevel_a[3]->{$PD->{'pd'}.'_Media_error_count'} = $PD->{'Media Error Count'}; + } + } + if(exists($PD->{'Other Error Count'})){ + if($PD->{'Other Error Count'} > $IGNERR_O){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_Other_error_count'; + $statusLevel_a[3]->{$PD->{'pd'}.'_Other_error_count'} = $PD->{'Other Error Count'}; + } + } + if(exists($PD->{'BBM Error Count'})){ + if($PD->{'BBM Error Count'} > $IGNERR_B){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_BBM_error_count'; + $statusLevel_a[3]->{$PD->{'pd'}.'_BBM_error_count'} = $PD->{'BBM Error Count'}; + } + } + if(exists($PD->{'Predictive Failure Count'})){ + if($PD->{'Predictive Failure Count'} > $IGNERR_P){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_Predictive_failure_count'; + $statusLevel_a[3]->{$PD->{'pd'}.'_Predictive_failure_count'} = $PD->{'Predictive Failure Count'}; + } + } + if(exists($PD->{'S.M.A.R.T alert flagged by drive'})){ + if($PD->{'S.M.A.R.T alert flagged by drive'} ne 'No'){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_SMART_flag'; + } + } + if(exists($PD->{'DG'})){ + if($PD->{'DG'} eq 'F'){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_DG'; + $statusLevel_a[3]->{$PD->{'pd'}.'_DG'} = $PD->{'DG'}; + } + } + if(exists($PD->{'Drive Temperature'})){ + my $temp = $PD->{'Drive Temperature'}; + if($temp ne 'N/A' && $temp ne '0C (32.00 F)'){ + $temp =~ /^([0-9]+)C/; + if(!(checkThreshs($1, $PD_TEMP_CRITICAL))){ + $status = getMaxStatus('Critical',$status); + push @{$statusLevel_a[2]}, $PD->{'pd'}.'_Drive_Temperature'; + } + elsif(!(checkThreshs($1, $PD_TEMP_WARNING))){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_Drive_Temperature'; + } + $statusLevel_a[3]->{$PD->{'pd'}.'_Drive_Temperature'} = $1; + } + } + if(exists($PD->{'init'})){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_Init'; + $statusLevel_a[3]->{$PD->{'pd'}.'_Init'} = $PD->{'init'}; + } + if(exists($PD->{'rebuild'})){ + $status = getMaxStatus('Warning',$status); + push @{$statusLevel_a[1]}, $PD->{'pd'}.'_Rebuild'; + $statusLevel_a[3]->{$PD->{'pd'}.'_Rebuild'} = $PD->{'rebuild'}; + } + } + if(exists($statusLevel_a[3]->{'PD_Status'})) { + $statusLevel_a[3]->{'PD_Status'} = getMaxStatus($statusLevel_a[3]->{'PD_Status'},$status ); + } + else { + $statusLevel_a[3]->{'PD_Status'} = $status; + } + ${$statusLevel_a[0]} = getMaxStatus(${$statusLevel_a[0]},$status); +} + +# Checks the status of the BBU, parses 'bbu show status' for the given controller. +# @param storcli The path to storcli command utility, followed by the controller +# number, e.g. 'storcli64 /c0'. +# @param statusLevel_a The status level array, elem 0 is the current status, +# elem 1 the warning sensors, elem 2 the critical sensors, elem 3 the verbose +# information for the sensors. +# @param commands_a An array to push the used command to +sub getBBUStatus { + my $storcli = shift; + my @statusLevel_a = @{(shift)}; + my $commands_a = shift; + + my $command = "$storcli /bbu show status"; + push @{$commands_a}, $command; + + my $status; + my @output = `$command`; + if(checkCommandStatus(\@output)) { + my $currBlock; + foreach my $line (@output) { + if($line =~ /^(BBU_Info|BBU_Firmware_Status|GasGaugeStatus)/){ + $currBlock = $1; + next; + } + if(defined($currBlock)){ + $line =~ s/^\s+|\s+$//g;#trim line + if($currBlock eq 'BBU_Info'){ + if ($line =~ /^Battery State/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne 'Optimal'){ + $status = 'Warning'; + push @{$statusLevel_a[1]}, 'BBU_State'; + $statusLevel_a[3]->{'BBU_State'} = $1 + } + } + elsif($line =~ /^Temperature/){ + $line =~ /([0-9]+) C$/; + if(!(checkThreshs($1, $BBU_TEMP_CRITICAL))){ + $status = 'Critical'; + push @{$statusLevel_a[2]}, 'BBU_Temperature'; + } + elsif(!(checkThreshs($1, $BBU_TEMP_WARNING))){ + $status = 'Warning'; + push @{$statusLevel_a[1]}, 'BBU_Temperature'; + } + $statusLevel_a[3]->{'BBU_Temperature'} = $1; + } + } + elsif($currBlock eq 'BBU_Firmware_Status'){ + if($line =~ /^Temperature/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "OK") { + $status = 'Critical'; + push @{$statusLevel_a[2]},'BBU_Firmware_temperature'; + $statusLevel_a[3]->{'BBU_Firmware_temperature'} = $1; + } + } + elsif($line =~ /^Voltage/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "OK") { + $status = 'Warning'; + push @{$statusLevel_a[1]},'BBU_Voltage'; + $statusLevel_a[3]->{'BBU_Voltage'} = $1; + } + } + elsif($line =~ /^I2C Errors Detected/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Critical'; + push @{$statusLevel_a[2]},'BBU_Firmware_I2C_errors'; + $statusLevel_a[3]->{'BBU_Firmware_I2C_Errors'} = $1; + } + } + elsif($line =~ /^Battery Pack Missing/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Critical'; + push @{$statusLevel_a[2]},'BBU_Pack_missing'; + $statusLevel_a[3]->{'BBU_Pack_missing'} = $1; + } + } + elsif($line =~ /^Replacement required/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Critical'; + push @{$statusLevel_a[2]},'BBU_Replacement_required'; + $statusLevel_a[3]->{'BBU_Replacement_required'} = $1; + } + } + elsif($line =~ /^Remaining Capacity Low/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Warning'; + push @{$statusLevel_a[1]},'BBU_Remaining_capacity_low'; + $statusLevel_a[3]->{'BBU_Remaining_capacity_low'} = $1; + } + } + elsif($line =~ /^Pack is about to fail \& should be replaced/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Critical'; + push @{$statusLevel_a[2]},'BBU_Should_be_replaced'; + $statusLevel_a[3]->{'BBU_Should_be_replaced'} = $1; + } + } + } + elsif($currBlock eq 'GasGaugeStatus'){ + if($line =~ /^Fully Discharged/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Critical'; + push @{$statusLevel_a[2]},'BBU_GasGauge_discharged'; + $statusLevel_a[3]->{'BBU_GasGauge_discharged'} = $1; + } + } + elsif($line =~ /^Over Temperature/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Warning'; + push @{$statusLevel_a[1]},'BBU_GasGauge_over_temperature'; + $statusLevel_a[3]->{'BBU_GasGauge_over_temperature'} = $1; + } + } + elsif($line =~ /^Over Charged/){ + $line =~ /([a-zA-Z]*)$/; + if($1 ne "No") { + $status = 'Critical'; + push @{$statusLevel_a[2]},'BBU_GasGauge_over_charged'; + $statusLevel_a[3]->{'BBU_GasGauge_over_charged'} = $1; + } + } + } + } + if(defined($status)){ + if($status eq 'Warning'){ + if(${$statusLevel_a[0]} ne 'Critical'){ + ${$statusLevel_a[0]} = 'Warning'; + } + } + else{ + ${$statusLevel_a[0]} = 'Critical'; + } + $statusLevel_a[3]->{'BBU_Status'} = $status; + } + else{ + $statusLevel_a[3]->{'BBU_Status'} = 'OK'; + } + } + } + else { + print "Invalid StorCLI command! ($command)\n"; + exit(STATE_UNKNOWN); + } +} + +# Checks the status of the CV module, parses 'cv show status' for the given +# controller. +# @param storcli The path to storcli command utility, followed by the controller +# number, e.g. 'storcli64 /c0'. +# @param statusLevel_a The status level array, elem 0 is the current status, +# elem 1 the warning sensors, elem 2 the critical sensors, elem 3 the verbose +# information for the sensors. +# @param commands_a An array to push the used command to +sub getCVStatus { + my $storcli = shift; + my @statusLevel_a = @{(shift)}; + my $commands_a = shift; + + my $command = "$storcli /cv show status"; + push @{$commands_a}, $command; + + my $status = 'OK'; + my @output = `$command`; + if(checkCommandStatus(\@output)) { + my $currBlock; + foreach my $line (@output) { + if($line =~ /^(Cachevault_Info|Firmware_Status)/){ + $currBlock = $1; + next; + } + if(defined($currBlock)){ + $line =~ s/^\s+|\s+$//g;#trim line + if($currBlock eq 'Cachevault_Info' && $line =~ /^State/){ + my @vals = split('\s{2,}',$line); + if($vals[1] ne "Optimal") { + $status = getMaxStatus('Warning', $status); + push @{$statusLevel_a[1]}, 'CV_State'; + $statusLevel_a[3]->{'CV_State'} = $vals[1] + } + } + elsif($currBlock eq 'Cachevault_Info' && $line =~ /^Temperature/){ + $line =~ /([0-9]+) C$/; + if(!(checkThreshs($1, $CV_TEMP_CRITICAL))){ + $status = getMaxStatus('Critical', $status); + push @{$statusLevel_a[2]}, 'CV_Temperature'; + } + elsif(!(checkThreshs($1, $CV_TEMP_WARNING))){ + $status = getMaxStatus('Warning', $status); + push @{$statusLevel_a[1]}, 'CV_Temperature'; + } + $statusLevel_a[3]->{'CV_Temperature'} = $1; + } + elsif($currBlock eq 'Firmware_Status' && $line =~ /^Replacement required/){ + $line =~ /([a-zA-Z0-9]*)$/; + if($1 ne "No") { + $status = getMaxStatus('Critical', $status); + push @{$statusLevel_a[2]},'CV_Replacement_required'; + } + $statusLevel_a[3]->{'CV_Replacement_required'} = $1; + } + } + } + + $statusLevel_a[3]->{'CV_Status'} = $status; + ${$statusLevel_a[0]} = getMaxStatus(${$statusLevel_a[0]},$status); + } + else { + print "Invalid StorCLI command! ($command)\n"; + exit(STATE_UNKNOWN); + } +} + +# Checks if wheter BBU or CV is present +# @param storcli The path to storcli command utility, followed by the controller +# number, e.g. 'storcli64 /c0'. +# @return A tuple, e.g. (0,0), where 0 means module is not present, 1 present +sub checkBBUorCVIsPresent{ + my $storcli = shift; + my ($bbu,$cv); + my @output = `$storcli /bbu show`; + if(checkCommandStatus(\@output)){ $bbu = 1; } + else{ $bbu = 0 }; + @output = `$storcli /cv show`; + if(checkCommandStatus(\@output)) { $cv = 1; } + else{ $cv = 0 }; + return ($bbu, $cv); +} + +# Checks if a given value is in a specified range, the range must follow the +# nagios development guidelines: +# http://nagiosplug.sourceforge.net/developer-guidelines.html#THRESHOLDFORMAT +# @param value The given value to check the pattern for +# @param pattern The pattern specifying the threshold range, e.g. '10:', '@10:20' +# @return 0 if the value is outside the range, 1 if the value satisfies the range +sub checkThreshs{ + my $value = shift; + my $pattern = shift; + if($pattern =~ /(^[0-9]+$)/){ + if($value < 0 || $value > $1){ + return 0; + } + } + elsif($pattern =~ /(^[0-9]+)\:$/){ + if($value < $1){ + return 0; + } + } + elsif($pattern =~ /^\~\:([0-9]+)$/){ + if($value > $1){ + return 0; + } + } + elsif($pattern =~ /^([0-9]+)\:([0-9]+)$/){ + if($value < $1 || $value > $2){ + return 0; + } + } + elsif($pattern =~ /^\@([0-9]+)\:([0-9]+)$/){ + if($value >= $1 and $value <= $2){ + return 0; + } + } + else{ + print "Invalid temperature parameter! ($pattern)\n"; + exit(STATE_UNKNOWN); + } + return 1; +} + +# Get the status string as plugin output +# @param level The desired level to get the status string for. Either 'Warning' +# or 'Critical'. +# @param statusLevel_a The status level array, elem 0 is the current status, +# elem 1 the warning sensors, elem 2 the critical sensors, elem 3 the verbose +# information for the sensors, elem 4 the used storcli commands. +# @return The created status string +sub getStatusString{ + my $level = shift; + my @statusLevel_a = @{(shift)}; + my @sensors_a; + my $status_str = ""; + if($level eq "Warning"){ + @sensors_a = @{$statusLevel_a[1]}; + } + if($level eq "Critical"){ + @sensors_a = @{$statusLevel_a[2]}; + } + # Add the controller parts only once + my $parts = ''; + # level comes from the method call, not the real status level + if($level eq "Critical"){ + my @keys = ('CTR_Status','LD_Status','PD_Status','BBU_Status','CV_Status'); + # Check which parts where checked + foreach my $key (@keys){ + $key =~ /^([A-Z]+)\_.*$/; + my $part = $1; + if(${$statusLevel_a[0]} eq 'OK'){ + if(exists($statusLevel_a[3]->{$key}) && $statusLevel_a[3]->{$key} eq 'OK'){ + $parts .= ", " unless $parts eq ''; + $parts .= $part; + } + } + else{ + if(exists($statusLevel_a[3]->{$key}) && $statusLevel_a[3]->{$key} ne 'OK'){ + $parts .= ", " unless $parts eq ''; + $parts .= $part; + $parts .= ' '.substr($statusLevel_a[3]->{$key}, 0, 4); + } + } + } + $status_str.= '('; + $status_str .= $parts unless !defined($parts); + $status_str.= ')'; + } + if($level eq 'Critical'){ + $status_str.= ' ' unless !(@sensors_a); + } + if($level eq 'Warning' && !@{$statusLevel_a[2]}){ + $status_str.= ' ' unless !(@sensors_a); + } + if($level eq "Warning" || $level eq "Critical"){ + if(@sensors_a){ + # Print which sensors are Warn or Crit + foreach my $sensor (@sensors_a){ + $status_str .= "[".$sensor." = ".$level; + if($VERBOSITY){ + if(exists($statusLevel_a[3]->{$sensor})){ + $status_str .= " (".$statusLevel_a[3]->{$sensor}.")"; + } + } + $status_str .= "]"; + } + } + } + return $status_str; +} + +# Get the verbose string if a higher verbose level is used +# @param statusLevel_a The status level array, elem 0 is the current status, +# elem 1 the warning sensors, elem 2 the critical sensors, elem 3 the verbose +# information for the sensors, elem 4 the used storcli commands. +# @param controllerToCheck Controller parsed by getControllerInfo +# @param LDDevicesToCheck LDs parsed by getLogicalDevices +# @param LDInitToCheck LDs parsed by getLogicalDevices init +# @param PDDevicesToCheck PDs parsed by getPhysicalDevices +# @param PDInitToCheck PDs parsed by getPhysicalDevices init +# @param PDRebuildToCheck PDs parsed by getPhysicalDevices rebuild +# @return The created verbosity string +sub getVerboseString{ + my @statusLevel_a = @{(shift)}; + my %controllerToCheck = %{(shift)}; + my @LDDevicesToCheck = @{(shift)}; + my @LDInitToCheck = @{(shift)}; + my @PDDevicesToCheck = @{(shift)}; + my @PDInitToCheck = @{(shift)}; + my @PDRebuildToCheck = @{(shift)}; + my @sensors_a; + my $verb_str; + + $verb_str .= "Used storcli commands:\n"; + foreach my $cmd (@{$statusLevel_a[4]}){ + $verb_str .= '- '.$cmd."\n"; + } + if(${$statusLevel_a[0]} eq 'Critical'){ + $verb_str .= "Critical sensors:\n"; + foreach my $sensor (@{$statusLevel_a[2]}){ + $verb_str .= "\t- ".$sensor; + if(exists($statusLevel_a[3]->{$sensor})){ + $verb_str .= ' ('.$statusLevel_a[3]->{$sensor}.')'; + } + $verb_str .= "\n"; + } + + } + if( ${$statusLevel_a[0]} ne 'OK'){ + $verb_str .= "Warning sensors:\n"; + foreach my $sensor (@{$statusLevel_a[1]}){ + $verb_str .= "\t- ".$sensor; + if(exists($statusLevel_a[3]->{$sensor})){ + $verb_str .= ' ('.$statusLevel_a[3]->{$sensor}.')'; + } + $verb_str .= "\n"; + } + + } + if($VERBOSITY == 3){ + $verb_str .= "CTR information:\n"; + $verb_str .= "\t- ".$controllerToCheck{'Product Name'}.":\n"; + $verb_str .= "\t\t- ".'Serial No='.$controllerToCheck{'Serial No'}."\n"; + $verb_str .= "\t\t- ".'FW Package Build='.$controllerToCheck{'FW Package Build'}."\n"; + $verb_str .= "\t\t- ".'Mfg. Date='.$controllerToCheck{'Mfg. Date'}."\n"; + $verb_str .= "\t\t- ".'Revision No='.$controllerToCheck{'Revision No'}."\n"; + $verb_str .= "\t\t- ".'BIOS Version='.$controllerToCheck{'BIOS Version'}."\n"; + $verb_str .= "\t\t- ".'FW Version='.$controllerToCheck{'FW Version'}."\n"; + $verb_str .= "\t\t- ".'ROC temperature='.$controllerToCheck{'ROC temperature'}."\n"; + $verb_str .= "LD information:\n"; + foreach my $LD (@LDDevicesToCheck){ + $verb_str .= "\t- ".$LD->{'ld'}.":\n"; + foreach my $key (sort (keys($LD))){ + $verb_str .= "\t\t- ".$key.'='.$LD->{$key}."\n"; + } + foreach my $LDinit (@LDInitToCheck){ + if($LDinit->{'ld'} eq $LD->{'ld'}){ + $verb_str .= "\t\t- init=".$LDinit->{'init'}."\n"; + } + } + } + $verb_str .= "PD information:\n"; + foreach my $PD (@PDDevicesToCheck){ + $verb_str .= "\t- ".$PD->{'pd'}.":\n"; + foreach my $key (sort (keys($PD))){ + $verb_str .= "\t\t- ".$key.'='.$PD->{$key}."\n"; + } + foreach my $PDinit (@PDInitToCheck){ + if($PDinit->{'pd'} eq $PD->{'pd'}){ + $verb_str .= "\t\t- init=".$PDinit->{'init'}."\n"; + } + } + foreach my $PDrebuild (@PDRebuildToCheck){ + if($PDrebuild->{'pd'} eq $PD->{'pd'}){ + $verb_str .= "\t\t- rebuild=".$PDrebuild->{'rebuild'}."\n"; + } + } + } + my @keys = ('BBU_Status','CV_Status'); + foreach my $key(@keys){ + if(exists($statusLevel_a[3]->{$key})){ + $key =~ /^(\w+)_\w+$/; + my $type = $1; + $verb_str .= $type." information:\n"; + foreach my $stat (sort (keys($statusLevel_a[3]))){ + if($stat =~ /^$type.+$/){ + $verb_str .= "\t\t- $stat=".$statusLevel_a[3]->{$stat}."\n"; + } + } + } + } + } + return $verb_str; +} + +# Get the performance string for the current check. The values are taken from +# the varbose hash in the status level array. +# @param statusLevel_a The current status level array +# @return The created performance string +sub getPerfString{ + my @statusLevel_a = @{(shift)}; + my %verboseValues_h = %{$statusLevel_a[3]}; + my $perf_str; + foreach my $key (sort (keys(%verboseValues_h))){ + if($key =~ /temperature/i){ + $perf_str .= ' ' unless !defined($perf_str); + $perf_str .= $key.'='.$verboseValues_h{$key}; + } + if($key =~ /ROC_Temperature$/){ + $perf_str .= ';'.$C_TEMP_WARNING.';'.$C_TEMP_CRITICAL; + } + elsif($key =~ /Drive_Temperature$/){ + $perf_str .= ';'.$PD_TEMP_WARNING.';'.$PD_TEMP_CRITICAL; + } + elsif($key eq 'BBU_Temperature'){ + $perf_str .= ';'.$BBU_TEMP_WARNING.';'.$BBU_TEMP_CRITICAL; + } + elsif($key eq 'CV_Temperature'){ + $perf_str .= ';'.$CV_TEMP_WARNING.';'.$CV_TEMP_CRITICAL; + } + } + return $perf_str; +} + +# Get max Status for two status value: Critical > Warning > OK +sub getMaxStatus { + my $Status1 = shift; + my $Status2 = shift; + + if ($Status1 eq 'Critical' || $Status2 eq 'Critical') { + return 'Critical' + } + elsif ($Status1 eq 'Warning' || $Status2 eq 'Warning') { + return 'Warning' + } + return 'OK' +} + +MAIN: { + my ($storcli, $sudo, $noSudo, $version, $exitCode); + # Create default sensor arrays and push them to status level + my @statusLevel_a ; + my $status_str = 'OK'; + my $warnings_a = []; + my $criticals_a = []; + my $verboseValues_h = {}; + my $verboseCommands_a = []; + push @statusLevel_a, \$status_str; + push @statusLevel_a, $warnings_a; + push @statusLevel_a, $criticals_a; + push @statusLevel_a, $verboseValues_h; + push @statusLevel_a, $verboseCommands_a; + # Per default use a BBU + my $bbu = 1; + my @enclosures; + my @logDevices; + my @physDevices; + my $platform = $^O; + + # Check storcli tool + $storcli = `which storcli64 2>/dev/null`; + chomp($storcli); + + if( !(GetOptions( + 'h|help' => sub {displayHelp();}, + 'v|verbose' => sub {$VERBOSITY = 1 }, + 'vv' => sub {$VERBOSITY = 2}, + 'vvv' => sub {$VERBOSITY = 3}, + 'V|version' => \$version, + 'C|controller=i' => \$CONTROLLER, + 'EID|enclosure=s' => \@enclosures, + 'LD|logicaldevice=s' => \@logDevices, + 'PD|physicaldevice=s' => \@physDevices, + 'Tw|temperature-warn=s' => \$C_TEMP_WARNING, + 'Tc|temperature-critical=s' => \$C_TEMP_CRITICAL, + 'PDTw|physicaldevicetemperature-warn=s' => \$PD_TEMP_WARNING, + 'PDTc|physicaldevicetemperature-critical=s' => \$PD_TEMP_CRITICAL, + 'BBUTw|bbutemperature-warning=s' => \$BBU_TEMP_WARNING, + 'BBUTc|bbutemperature-critical=s' => \$BBU_TEMP_CRITICAL, + 'CVTw|cvtemperature-warning=s' => \$CV_TEMP_WARNING, + 'CVTc|cvtemperature-critical=s' => \$CV_TEMP_CRITICAL, + 'Im|ignore-media-errors=i' => \$IGNERR_M, + 'Io|ignore-other-errors=i' => \$IGNERR_O, + 'Ip|ignore-predictive-fail-count=i' => \$IGNERR_P, + 'Is|ignore-shield-counter=i' => \$IGNERR_S, + 'Ib|ignore-bbm-counter=i' => \$IGNERR_B, + 'p|path=s' => \$storcli, + 'z|criticality=s' => \$criticality, + 'b|BBU=i' => \$bbu, + 'noenclosures=i' => \$NOENCLOSURES, + 'nosudo' => \$noSudo, + ))){ + print $NAME . " Version: " . $VERSION ."\n"; + displayUsage(); + exit(STATE_UNKNOWN); + } + if(defined($version)){ print $NAME . "\nVersion: ". $VERSION . "\n"; } + + if($storcli eq ""){ + print "Error: cannot find storcli executable.\n"; + print "Ensure storcli is in your path, or use the '-p ' switch!\n"; + exit(STATE_UNKNOWN); + } + if($platform eq 'linux') { + if(!defined($noSudo)){ + my $sudo; + chomp($sudo = `which sudo`); + if(!defined($sudo)){ + print "Error: cannot find sudo executable.\n"; + exit(STATE_UNKNOWN); + } + $storcli = $sudo.' '.$storcli; + } + } + # Print storcli version if available + if(defined($version)){ displayVersion($storcli) } + # Prepare storcli command + $storcli .= " /c$CONTROLLER"; + # Check if the controller number can be used + if(!getControllerTime($storcli)){ + print "Error: invalid controller number, controller not found!\n"; + exit(STATE_UNKNOWN); + } + # Prepare command line arrays + @enclosures = split(/,/,join(',', @enclosures)); + @logDevices = split(/,/,join(',', @logDevices)); + @physDevices = split(/,/,join(',', @physDevices)); + # Check if the BBU param is correct + if(($bbu != 1) && ($bbu != 0)) { + print "Error: invalid BBU/CV parameter, must be 0 or 1!\n"; + exit(STATE_UNKNOWN); + } + my ($bbuPresent,$cvPresent) = (0,0); + if($bbu == 1){ + ($bbuPresent,$cvPresent) = checkBBUorCVIsPresent($storcli); + if($bbuPresent == 0 && $cvPresent == 0){ + ${$statusLevel_a[0]} = 'Critical'; + push @{$criticals_a}, 'BBU/CV_Present'; + $statusLevel_a[3]->{'BBU_Status'} = 'Critical'; + $statusLevel_a[3]->{'CV_Status'} = 'Critical'; + } + } + if($bbuPresent == 1){getBBUStatus($storcli, \@statusLevel_a, $verboseCommands_a); } + if($cvPresent == 1){ getCVStatus($storcli, \@statusLevel_a, $verboseCommands_a); } + + my $controllerToCheck = getControllerInfo($storcli, $verboseCommands_a); + my $LDDevicesToCheck = getLogicalDevices($storcli, \@logDevices, 'all', $verboseCommands_a); + my $LDInitToCheck = getLogicalDevices($storcli, \@logDevices, 'init', $verboseCommands_a); + my $PDDevicesToCheck = getPhysicalDevices($storcli, \@enclosures, \@physDevices, 'all', $verboseCommands_a); + my $PDInitToCheck = getPhysicalDevices($storcli, \@enclosures, \@physDevices, 'initialization', $verboseCommands_a); + my $PDRebuildToCheck = getPhysicalDevices($storcli, \@enclosures, \@physDevices, 'rebuild', $verboseCommands_a); + + getControllerStatus(\@statusLevel_a, $controllerToCheck); + getLDStatus(\@statusLevel_a, $LDDevicesToCheck); + getLDStatus(\@statusLevel_a, $LDInitToCheck); + getPDStatus(\@statusLevel_a, $PDDevicesToCheck); + getPDStatus(\@statusLevel_a, $PDInitToCheck); + getPDStatus(\@statusLevel_a, $PDRebuildToCheck); + + print ${$statusLevel_a[0]}." "; + print getStatusString("Critical",\@statusLevel_a); + print getStatusString("Warning",\@statusLevel_a); + my $perf_str = getPerfString(\@statusLevel_a); + if($perf_str){ + print "|".$perf_str; + } + if($VERBOSITY == 2 || $VERBOSITY == 3){ + print "\n".getVerboseString(\@statusLevel_a, $controllerToCheck, $LDDevicesToCheck, $LDInitToCheck, + $PDDevicesToCheck, $PDInitToCheck, $PDRebuildToCheck) + } + + + if(${$statusLevel_a[0]} eq "Critical" && ($criticality eq "critical")){ + $exitCode = STATE_CRITICAL; + } + elsif (${$statusLevel_a[0]} eq "OK") { + $exitCode = STATE_OK; + } + else { + $exitCode = STATE_WARNING; + } + exit($exitCode); +} diff --git a/roles/sensu-client/files/plugins/check-syslog-socket.rb b/roles/sensu-client/files/plugins/check-syslog-socket.rb new file mode 100755 index 0000000..952829c --- /dev/null +++ b/roles/sensu-client/files/plugins/check-syslog-socket.rb @@ -0,0 +1,87 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Check Syslog Socket Plugin +# === +# +# This plugin checks that the local syslog socket is accepting messages +# without blocking. +# +# Copyright 2014 Dustin Lundquist +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' +require 'timeout' +require 'socket' + +class CheckSyslogSocket < Sensu::Plugin::Check::CLI + + option :log_socket, + :description => "Path to log socket", + :short => '-s SOCKET', + :long => '--log-socket SOCKET', + :default => '/dev/log' + + option :warn, + :short => '-w WARN', + :proc => proc {|a| a.to_f }, + :default => 200 + + option :crit, + :short => '-c CRIT', + :proc => proc {|a| a.to_f }, + :default => 1000 + + + class CriticalExceeded < StandardError + end + + def run + time_start = Time.now + + Timeout::timeout(config[:crit] / 1000.0, CriticalExceeded) do + do_syslog_msg + end + + duration = Time.now - time_start + duration_ms = duration * 1000 + + if duration_ms > config[:warn] + warning "#{config[:log_socket]} took #{duration_ms.round}ms to accept message" + else + ok "#{config[:log_socket]} tool #{duration_ms.round}ms to accept message" + end + rescue CriticalExceeded + critical "#{config[:log_socket]} took longer than #{config[:crit]}ms to accept message" + end + + private + + def log_msg + facility = 23 # Local 7 + level = 7 # Debug + priority = facility * 8 + level + timestamp = Time.now.strftime("%c") + hostname = Socket.gethostname + + "<#{priority}>#{timestamp} check-syslog-socket: ping" + end + + def do_syslog_msg + log_socket_address = Socket.pack_sockaddr_un(config[:log_socket]) + + socket = Socket::new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0) + socket.send(log_msg, 0, log_socket_address) + socket.close + rescue Errno::ECONNREFUSED + critical "Connection refused to #{config[:log_socket]}" + rescue Errno::EACCES + critical "Permission denied to #{config[:log_socket]}" + rescue Errno::ENOENT, Errno::ENOTDIR + critical "Socket #{config[:log_socket]} doesn't exist" + rescue Exception => e + critical "Unexpected error: #{e.inspect}" + end +end diff --git a/roles/sensu-client/files/plugins/check-ucarp-procs.sh b/roles/sensu-client/files/plugins/check-ucarp-procs.sh new file mode 100755 index 0000000..565310b --- /dev/null +++ b/roles/sensu-client/files/plugins/check-ucarp-procs.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +while getopts 'z:' OPT; do + case $OPT in + z) CRITICALITY=$OPTARG;; + esac +done + +CRITICALITY=${CRITICALITY:-critical} + +if $(which ifquery >/dev/null 2>&1); then # for ubuntu + for IFACE in $(ifquery --list); do + for VIP in $(ifquery ${IFACE} | awk '/^ucarp-vip:/ {print $2}'); do + if ! ps -ef | grep '/usr/sbin/ucarp' | grep ${VIP} >/dev/null; then + echo "no ucarp process is running for IP ${VIP}" + if [ "$CRITICALITY" == "warning" ]; then + exit 1 + else + exit 2 + fi + fi + done + done +else # for centos/rhel + for VIP in $(awk '/^[^#]*VIP_ADDRESS/' /etc/ucarp/*.conf | sed 's/.*VIP_ADDRESS="\([^,]*\)"/\1/g'); do + if ! ps -ef | grep '/usr/sbin/ucarp' | grep ${VIP} >/dev/null; then + echo "no ucarp process is running for IP ${VIP}" + if [ "$CRITICALITY" == "warning" ]; then + exit 1 + else + exit 2 + fi + fi + done +fi + +echo "All interfaces configured with uCARP have running process" diff --git a/roles/sensu-client/files/plugins/check_3ware_raid.py b/roles/sensu-client/files/plugins/check_3ware_raid.py new file mode 100755 index 0000000..87d5e91 --- /dev/null +++ b/roles/sensu-client/files/plugins/check_3ware_raid.py @@ -0,0 +1,486 @@ +#!/usr/bin/env python +# +# Author: Hari Sekhon +# Date: 2007-02-20 17:49:00 +0000 (Tue, 20 Feb 2007) +# +# http://github.com/harisekhon +# +# License: see accompanying LICENSE file +# + +"""Nagios plugin to test the state of all 3ware raid arrays and/or drives + on all 3ware controllers on the local machine. Requires the tw_cli program + written by 3ware, which should be called tw_cli_64 if running on a 64-bit + system. May be remotely executed via any of the standard remote nagios + execution mechanisms""" + +__author__ = "Hari Sekhon" +__title__ = "Nagios Plugin for 3ware RAID" +__version__ = "1.1" + +# Standard Nagios return codes +OK = 0 +WARNING = 1 +CRITICAL = 2 +UNKNOWN = 3 + +import os +import re +import sys +try: + from subprocess import Popen, PIPE, STDOUT +except ImportError: + print "Failed to import subprocess module.", + print "Perhaps you are using a version of python older than 2.4?" + sys.exit(CRITICAL) +from optparse import OptionParser + +SRCDIR = os.path.dirname(sys.argv[0]) + + +def end(status, message, criticality, disks=False): + """Exits the plugin with first arg as the return code and the second + arg as the message to output""" + + check = "RAID" + if disks == True: + check = "DISKS" + if status == OK: + print "%s OK: %s" % (check, message) + sys.exit(OK) + elif status == UNKNOWN: + print "UNKNOWN: %s" % message + sys.exit(UNKNOWN) + elif (status == WARNING) or (criticality == "warning"): + print "%s WARNING: %s" % (check, message) + sys.exit(WARNING) + else: + print "%s CRITICAL: %s" % (check, message) + sys.exit(CRITICAL) + + + +if os.geteuid() != 0: + end(UNKNOWN, "You must be root to run this plugin") + +ARCH = os.uname()[4] + +BIN = None + +def _set_twcli_binary(path=None): + """ set the path to the twcli binary""" + global BIN + + if path: + BIN = path + elif re.match("i[3456]86", ARCH): + BIN = SRCDIR + "/tw_cli" + elif ARCH == "x86_64": + BIN = SRCDIR + "/tw_cli_64" + else: + end(UNKNOWN, "architecture is not x86 or x86_64, cannot run 3ware " \ + "utility") + + if not os.path.exists(BIN): + end(UNKNOWN, "3ware utility for this architecture '%s' cannot be " \ + "found" % BIN) + + if not os.access(BIN, os.X_OK): + end(UNKNOWN, "3ware utility '%s' is not executable" % BIN) + + +def run(cmd): + """runs a system command and returns stripped output""" + if cmd == "" or cmd == None: + end(UNKNOWN, "internal python error - " \ + + "no cmd supplied for 3ware utility") + try: + process = Popen(BIN, stdin=PIPE, stdout=PIPE, stderr=STDOUT) + except OSError, error: + error = str(error) + if error == "No such file or directory": + end(UNKNOWN, "Cannot find 3ware utility '%s'" % BIN) + else: + end(UNKNOWN, "error trying to run 3ware utility - %s" % error) + + if process.poll(): + end(UNKNOWN, "3ware utility process ended prematurely") + + try: + stdout, stderr = process.communicate(cmd) + except OSError, error: + end(UNKNOWN, "unable to communicate with 3ware utility - %s" % error) + + + if stdout == None or stdout == "": + end(UNKNOWN, "No output from 3ware utility") + + output = str(stdout).split("\n") + + if output[1] == "No controller found.": + end(UNKNOWN, "No 3ware controllers were found on this machine") + + stripped_output = output[3:-2] + + if process.returncode != 0: + stderr = str(stdout).replace("\n"," ") + end(UNKNOWN, "3ware utility returned an exit code of %s - %s" \ + % (process.returncode, stderr)) + else: + return stripped_output + + +def test_all(verbosity, warn_true=False, no_summary=False, show_drives=False): + """Calls the raid and drive testing functions""" + + array_result, array_message = test_arrays(verbosity, warn_true, no_summary) + + if array_result != OK and not show_drives: + return array_result, array_message + + drive_result, drive_message = test_drives(verbosity, warn_true, no_summary) + + if drive_result > array_result: + result = drive_result + else: + result = array_result + + if drive_result != OK: + if array_result == OK: + message = "Arrays OK but... " + drive_message + else: + message = array_message + ", " + drive_message + else: + if show_drives: + message = array_message + ", " + drive_message + else: + message = array_message + + return result, message + + +def test_arrays(verbosity, warn_true=False, no_summary=False): + """Tests all the raid arrays on all the 3ware controllers on + the local machine""" + + lines = run("show") + #controllers = [ line.split()[0] for line in lines ] + controllers = [ line.split()[0] for line in lines if line and line[0] == "c" ] + + status = OK + message = "" + number_arrays = 0 + arrays_not_ok = 0 + number_controllers = len(controllers) + for controller in controllers: + unit_lines = run("/%s show unitstatus" % controller) + if verbosity >= 3: + for unit_line in unit_lines: + print unit_line + print + + for unit_line in unit_lines: + number_arrays += 1 + unit_line = unit_line.split() + state = unit_line[2] + if state == "OK": + continue + elif state == "REBUILDING" or \ + state == "VERIFY-PAUSED" or \ + state == "VERIFYING" or \ + state == "INITIALIZING": + + unit = int(unit_line[0][1:]) + raid = unit_line[1] + if state == "VERIFY-PAUSED" or \ + state == "VERIFYING" or \ + state == "INITIALIZING": + percent_complete = unit_line[4] + else: + percent_complete = unit_line[3] + + message += "Array %s status is '%s'(%s on adapter %s) - " \ + % (unit, state, raid, controller[1:]) + if state == "REBUILDING": + message += "Rebuild " + elif state == "VERIFY-PAUSED" or state == "VERIFYING": + message += "Verify " + elif state == "INITIALIZING": + message += "Initializing " + message += "Status: %s%% complete, " % percent_complete + if warn_true: + arrays_not_ok += 1 + if status == OK: + status = WARNING + else: + arrays_not_ok += 1 + unit = int(unit_line[0][1:]) + raid = unit_line[1] + message += "Array %s status is '%s'" % (unit, state) + message += "(%s on adapter %s), " % (raid, controller[1:]) + status = CRITICAL + + message = message.rstrip(", ") + + message = add_status_summary(status, message, arrays_not_ok, "arrays") + + if not no_summary: + message = add_checked_summary(message, \ + number_arrays, \ + number_controllers, \ + "arrays") + + return status, message + + +def test_drives(verbosity, warn_true=False, no_summary=False): + """Tests all the drives on the all the 3ware raid controllers + on the local machine""" + + lines = run("show") + controllers = [] + for line in lines: + parts = line.split() + if len(parts): + controllers.append(parts[0]) + + status = OK + message = "" + number_drives = 0 + drives_not_ok = 0 + number_controllers = len(controllers) + for controller in controllers: + drive_lines = run("/%s show drivestatus" % controller) + number_drives += len(drive_lines) + + if verbosity >= 3: + for drive_line in drive_lines: + print drive_line + print + + for drive_line in drive_lines: + drive_line = drive_line.split() + state = drive_line[1] + if state == "OK" or state == "NOT-PRESENT": + continue + if not warn_true and \ + state in ('VERIFYING', 'REBUILDING', 'INITIALIZING'): + continue + else: + drives_not_ok += 1 + drive = drive_line[0] + if drive[0] == "d": + drive = drive[1:] + array = drive_line[2] + if array[0] == "u": + array = array[1:] + message += "Status of drive in port " + message += "%s is '%s'(Array %s on adapter %s), " \ + % (drive, state, array, controller[1:]) + status = CRITICAL + + message = message.rstrip(", ") + + message = add_status_summary(status, message, drives_not_ok, "drives") + + if not no_summary: + message = add_checked_summary(message, \ + number_drives, \ + number_controllers, \ + "drives") + + return status, message + + +def add_status_summary(status, message, number_failed, device): + """Adds a status summary string to the beginning of the message + and returns the message""" + + if device == "arrays": + if number_failed == 1: + device = "array" + elif device == "drives": + if number_failed == 1: + device = "drive" + else: + device = "[unknown devices, please check code]" + + if status == OK: + if message == "": + message = "All %s OK" % device + message + else: + message = "All %s OK - " % device + message + else: + message = "%s %s not OK - " % (number_failed, device) + message + + return message + + +def add_checked_summary(message, number_devices, number_controllers, device): + """Adds a summary string of what was checked to the end of the message + and returns the message""" + + if device == "arrays": + if number_devices == 1: + device = "array" + elif device == "drives": + if number_devices == 1: + device = "drive" + else: + device = "[unknown devices, please check code]" + + if number_controllers == 1: + controller = "controller" + else: + controller = "controllers" + + message += " [%s %s checked on %s %s]" % (number_devices, device, \ + number_controllers, controller) + + return message + + +def main(): + """Parses command line options and calls the function to + test the arrays/drives""" + + parser = OptionParser() + + + parser.add_option( "-a", + "--arrays-only", + action="store_true", + dest="arrays_only", + help="Only test the arrays. By default both arrays " \ + + "and drives are checked") + + parser.add_option( "-b", + "--binary", + dest="binary", + help="Full path of the tw_cli binary to use.") + + parser.add_option( "-z", + "--criticality", + action="store_true", + default="critical", + dest="criticality", + help="Allow for criticality level to be overwritten. " \ + + "Options are warning and critical, with critical " \ + + "being the default level. ") + + parser.add_option( "-d", + "--drives-only", + action="store_true", + dest="drives_only", + help="Only test the drives. By default both arrays " \ + + "and drives are checked") + + parser.add_option( "-n", + "--no-summary", + action="store_true", + dest="no_summary", + help="Do not display the number of arrays/drives " \ + + "checked. By default the number of arrays and " \ + + "drives checked are printed at the end of the " \ + + "line. This is useful information and helps to " \ + + "know that they are detected properly") + + parser.add_option( "-s", + "--show-drives", + action="store_true", + dest="show_drives", + help="Show drive status. By default drives are " \ + + "checked as well as arrays, but there is no " \ + + "output regarding them unless there is a " \ + + "problem. Use this is you want drive details as " \ + + "well when there is an array problem (default " \ + + "behaviour is to only show the array problem to " \ + + "avoid too much cluttering information), " \ + + "or if you want to see the drive information " \ + + "even when all drives are ok") + + parser.add_option( "-w", + "--warn-rebuilding", + action="store_true", + dest="warn_true", + help="Warn when an array is Rebuilding, Initializing " \ + + "or Verifying. You might want to do this to keep " \ + + "a closer eye on things. Also, these conditions " \ + + "can affect performance so you might want to " \ + + "know this is going on. Default is to not warn " \ + + "during these states as they are not usually " \ + + "problems") + + parser.add_option( "-v", + "--verbose", + action="count", + dest="verbosity", + help="Verbose mode. Good for testing plugin. By default\ + only one result line is printed as per Nagios standards") + + parser.add_option( "-V", + "--version", + action="store_true", + dest="version", + help="Print version number and exit") + + (options, args) = parser.parse_args() + + if args: + parser.print_help() + sys.exit(UNKNOWN) + + arrays_only = options.arrays_only + binary = options.binary + criticality = options.criticality + drives_only = options.drives_only + no_summary = options.no_summary + show_drives = options.show_drives + warn_true = options.warn_true + verbosity = options.verbosity + version = options.version + + if version: + print __version__ + sys.exit(OK) + + if arrays_only and drives_only: + print "You cannot use the -a and -d switches together, they are", + print "mutually exclusive\n" + parser.print_help() + sys.exit(UNKNOWN) + elif arrays_only and show_drives: + print "You cannot use the -a and -s switches together" + print "No drive information can be printed if you only check arrays\n" + parser.print_help() + sys.exit(UNKNOWN) + elif drives_only and warn_true: + print "You cannot use the -d and -w switches together" + print "Array warning states are invalid when testing only drives\n" + parser.print_help() + sys.exit(UNKNOWN) + + if (criticality != "warning") and (criticality != "critical"): + print "Criticality must be one of these options: warning, critical" + parser.print_help() + sys.exit(UNKNOWN) + + _set_twcli_binary(binary) + + if arrays_only: + result, output = test_arrays(verbosity, warn_true, no_summary) + elif drives_only: + result, output = test_drives(verbosity, warn_true, no_summary) + end(result, output, True) + else: + result, output = test_all(verbosity, warn_true, no_summary, show_drives) + + end(result, output, criticality) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print "Caught Control-C..." + sys.exit(CRITICAL) diff --git a/roles/sensu-client/files/plugins/check_adaptec_raid.py b/roles/sensu-client/files/plugins/check_adaptec_raid.py new file mode 100755 index 0000000..38abb51 --- /dev/null +++ b/roles/sensu-client/files/plugins/check_adaptec_raid.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python + +# Copyright 2016, Craig Tracey +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations + +import argparse +import os +import re +import subprocess +import sys + +SUCCESS = 'success' +CRITICAL = 'critical' +WARNING = 'warning' + +RETURN_STATUS = { + SUCCESS: 0, + CRITICAL: 2, + WARNING: 1, +} +DEFAULT_FAILED_RETURN = CRITICAL +DEFAULT_ARCCONF_PATH = '/usr/Adaptec_Event_Monitor/arcconf' + + +def exit_with_status(status, criticality): + return_status = None + return_text = None + if not status == CRITICAL: + return_text = status + return_status = RETURN_STATUS[status] + else: + return_text = criticality + return_status = RETURN_STATUS[criticality] + print("Check status: %s" % return_text) + sys.exit(return_status) + + +def arcconf_exists(path): + return os.path.exists(path) + + +def check_adaptec_status(args): + if not arcconf_exists(args.arcconf_path): + print("arcconf utility no present at %s" % args.arcconf_path) + exit_with_status(WARNING, args.criticality) + + check_commands = ( + (['GETCONFIG', '1'], + r"\s*Controller Status\s*:\s*(.*)", + "Optimal", "Controller status not optimal"), + (['GETCONFIG', '1', 'LD'], + r"\s*Status of logical device\s*:\s*(.*)", + "Optimal", "Logical drive(s) not optimal"), + (['GETCONFIG', '1', 'PD'], + r"\s*S.M.A.R.T. warnings\s*:\s*(.*)", + "0", "Physical device SMART warnings", WARNING) + ) + + statuses = [] + for command in check_commands: + status = _run_command(args, *command) + statuses.append(status) + + if CRITICAL in statuses: + exit_with_status(CRITICAL, args.criticality) + elif WARNING in statuses: + exit_with_status(WARNING, args.criticality) + exit_with_status(SUCCESS, args.criticality) + + +def _run_command(args, command, regex, expected, hint, failed_status=CRITICAL): + cmd = [args.arcconf_path] + cmd += command + + output = subprocess.check_output(cmd) + failed = False + found = False + for line in output.split('\n'): + match = re.match(regex, line) + if match: + found = True + status = match.groups(1)[0] + if status.lower() != expected.lower(): + failed = True + + if not found: + print("Failed to determine RAID status " + "with command: '%s'" % " ".join(cmd)) + return WARNING + if failed: + print("Failed RAID check: %s" % hint) + print("Failed command: '%s'" % " ".join(cmd)) + return failed_status + return SUCCESS + + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument('-a', '--arcconf-path', + default=DEFAULT_ARCCONF_PATH, + help="override the default arcconf path") + + parser.add_argument('-c', '--controller', + default=1, + help="controller to check") + + parser.add_argument('-z', '--criticality', + default=DEFAULT_FAILED_RETURN, + choices=RETURN_STATUS.keys(), + help="override the criticality upon failure") + + args = parser.parse_args() + try: + check_adaptec_status(args) + except Exception as e: + print("Failed to check RAID status: %s" % e) + exit_with_status(CRITICAL, args.criticality) + exit_with_status(SUCCESS, args.criticality) + + +if __name__ == '__main__': + main() diff --git a/roles/sensu-client/files/plugins/check_megaraid_sas.pl b/roles/sensu-client/files/plugins/check_megaraid_sas.pl new file mode 100755 index 0000000..78cdfe8 --- /dev/null +++ b/roles/sensu-client/files/plugins/check_megaraid_sas.pl @@ -0,0 +1,263 @@ +#!/usr/bin/perl -w + +# check_megaraid_sas Nagios plugin +# Copyright (C) 2007 Jonathan Delgado, delgado@molbio.mgh.harvard.edu +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# +# Nagios plugin to monitor the status of volumes attached to a LSI Megaraid SAS +# controller, such as the Dell PERC5/i and PERC5/e. If you have any hotspares +# attached to the controller, you can specify the number you should expect to +# find with the '-s' flag. +# +# The paths for the Nagios plugins lib and MegaCli may need to me changed. +# +# Code for correct RAID level reporting contributed by Frode Nordahl, 2009/01/12. +# +# $Author: delgado $ +# $Revision: #12 $ $Date: 2010/10/18 $ + +use strict; +use Getopt::Std; +use lib qw(/usr/lib/nagios/plugins /usr/lib64/nagios/plugins); # possible pathes to your Nagios plugins and utils.pm + +my %ERRORS = ( + 'OK' => 0, + 'WARNING' => 1, + 'CRITICAL' => 2, + 'UNKNOWN' => 3 +); + +our($opt_h, $opt_b, $opt_e, $opt_s, $opt_o, $opt_z, $opt_m, $opt_p); + + +getopts('hb:es:o:z:p:m:'); + +if ( $opt_h ) { + print "Usage: $0 [-s number] [-m number] [-o number] [-z criticality]\n"; + print " -b the megacli binary path to use\n"; + print " -z the criticality of this alert\n"; + print " -e run megacli with sudo privileges"; + print " -s is how many hotspares are attached to the controller\n"; + print " -m is the number of media errors to ignore\n"; + print " -p is the predictive error count to ignore\n"; + print " -o is the number of other disk errors to ignore\n"; + exit; +} + +my $sudo = ''; +my $criticality = ''; +my $megaclibin = '/usr/sbin/MegaCli'; # the full path to your MegaCli binary +my $megapostopt = '-NoLog'; # additional options to call at the end of MegaCli arguments + +my ($adapters); +my $hotspares = 0; +my $hotsparecount = 0; +my $pdbad = 0; +my $pdcount = 0; +my $mediaerrors = 0; +my $mediaallow = 0; +my $prederrors = 0; +my $predallow = 0; +my $othererrors = 0; +my $otherallow = 0; +my $result = ''; +my $status = 'OK'; + +sub max_state ($$) { + my ($current, $compare) = @_; + + if (($compare eq 'CRITICAL') || ($current eq 'CRITICAL') && ($criticality eq 'critical')) { + return 'CRITICAL'; + } elsif ($compare eq 'OK') { + return $current; + } elsif ($compare eq 'WARNING') { + return 'WARNING'; + } elsif (($compare eq 'UNKNOWN') && ($current eq 'OK')) { + return 'UNKNOWN'; + } else { + return $current; + } +} + +sub exitreport ($$) { + my ($status, $message) = @_; + + print STDOUT "$status: $message\n"; + exit $ERRORS{$status}; +} + +if ( $opt_b ) { + $megaclibin = $opt_b; +} +if ( $opt_z ) { + $criticality = $opt_z; +} +if ( $opt_e ) { + $sudo = "sudo" +} +if ( $opt_s ) { + $hotspares = $opt_s; +} +if ( $opt_m ) { + $mediaallow = $opt_m; +} +if ( $opt_p ) { + $predallow = $opt_p; +} +if ( $opt_o ) { + $otherallow = $opt_o; +} + +my $megacli = "$sudo $megaclibin"; # how we actually call MegaCli + +# Some sanity checks that you actually have something where you think MegaCli is +(-e $megaclibin) + || exitreport('UNKNOWN',"error: $megaclibin does not exist"); + +# Get the number of RAID controllers we have +open (ADPCOUNT, "$megacli -adpCount $megapostopt |") + || exitreport('UNKNOWN',"error: Could not execute $megacli -adpCount $megapostopt"); + +while () { + if ( m/Controller Count:\s*(\d+)/ ) { + $adapters = $1; + last; + } +} +close ADPCOUNT; + +ADAPTER: for ( my $adp = 0; $adp < $adapters; $adp++ ) { + # Get the number of logical drives on this adapter + open (LDGETNUM, "$megacli -LdGetNum -a$adp $megapostopt |") + || exitreport('UNKNOWN', "error: Could not execute $megacli -LdGetNum -a$adp $megapostopt"); + + my ($ldnum); + while () { + if ( m/Number of Virtual drives configured on adapter \d:\s*(\d+)/i ) { + $ldnum = $1; + last; + } + } + close LDGETNUM; + + LDISK: for ( my $ld = 0; $ld < $ldnum; $ld++ ) { + # Get info on this particular logical drive + open (LDINFO, "$megacli -LdInfo -L$ld -a$adp $megapostopt |") + || exitreport('UNKNOWN', "error: Could not execute $megacli -LdInfo -L$ld -a$adp $megapostopt "); + + my ($size, $unit, $raidlevel, $ldpdcount, $state, $spandepth); + while () { + if ( m/^Size\s*:\s*((\d+\.?\d*)\s*(MB|GB|TB))/ ) { + $size = $2; + $unit = $3; + # Adjust MB to GB if that's what we got + if ( $unit eq 'MB' ) { + $size = sprintf( "%.0f", ($size / 1024) ); + $unit= 'GB'; + } + } elsif ( m/State\s*:\s*(\w+)/ ) { + $state = $1; + if ( $state ne 'Optimal' ) { + $status = 'CRITICAL'; + } + } elsif ( m/Number Of Drives\s*(per span\s*)?:\s*(\d+)/ ) { + $ldpdcount = $2; + } elsif ( m/Span Depth\s*:\s*(\d+)/ ) { + $spandepth = $1; + } elsif ( m/RAID Level\s*: Primary-(\d)/ ) { + $raidlevel = $1; + } + } + close LDINFO; + + # Report correct RAID-level and number of drives in case of Span configurations + if ($ldpdcount && $spandepth > 1) { + $ldpdcount = $ldpdcount * $spandepth; + if ($raidlevel < 10) { + $raidlevel = $raidlevel . "0"; + } + } + + $result .= "$adp:$ld:RAID-$raidlevel:$ldpdcount drives:$size$unit:$state "; + + } #LDISK + close LDINFO; + + # Get info on physical disks for this adapter + open (PDLIST, "$megacli -PdList -a$adp $megapostopt |") + || exitreport('UNKNOWN', "error: Could not execute $megacli -PdList -a$adp $megapostopt "); + + my ($slotnumber,$fwstate); + PDISKS: while () { + if ( m/Slot Number\s*:\s*(\d+)/ ) { + $slotnumber = $1; + $pdcount++; + } elsif ( m/(\w+) Error Count\s*:\s*(\d+)/ ) { + if ( $1 eq 'Media') { + $mediaerrors += $2; + } else { + $othererrors += $2; + } + } elsif ( m/Predictive Failure Count\s*:\s*(\d+)/ ) { + $prederrors += $1; + } elsif ( m/Firmware state\s*:\s*(\w+)/ ) { + $fwstate = $1; + if ( $fwstate eq 'Hotspare' ) { + $hotsparecount++; + } elsif ( $fwstate eq 'Online' ) { + # Do nothing + } elsif ( $fwstate eq 'Unconfigured' ) { + # A drive not in anything, or a non drive device + $pdcount--; + } elsif ( $slotnumber != 255 ) { + $pdbad++; + $status = 'CRITICAL'; + } + } + } #PDISKS + close PDLIST; +} + +$result .= "Drives:$pdcount "; + +# Any bad disks? +if ( $pdbad ) { + $result .= "$pdbad Bad Drives "; +} + +my $errorcount = $mediaerrors + $prederrors + $othererrors; +# Were there any errors? +if ( $errorcount ) { + $result .= "($errorcount Errors) "; + if ( ( $mediaerrors > $mediaallow ) || + ( $prederrors > $predallow ) || + ( $othererrors > $otherallow ) ) { + $status = max_state($status, 'WARNING'); + } +} + +# Do we have as many hotspares as expected (if any) +if ( $hotspares ) { + if ( $hotsparecount < $hotspares ) { + $status = max_state($status, 'WARNING'); + $result .= "Hotspare(s):$hotsparecount (of $hotspares)"; + } else { + $result .= "Hotspare(s):$hotsparecount"; + } +} + +exitreport($status, $result); diff --git a/roles/sensu-client/files/plugins/es-cluster-metrics.rb b/roles/sensu-client/files/plugins/es-cluster-metrics.rb new file mode 100755 index 0000000..8adbbd4 --- /dev/null +++ b/roles/sensu-client/files/plugins/es-cluster-metrics.rb @@ -0,0 +1,108 @@ +#!/opt/sensu/embedded/bin/ruby +# +# es-cluster-metrics +# +# DESCRIPTION: +# This plugin uses the ES API to collect metrics, producing a JSON +# document which is outputted to STDOUT. An exit status of 0 indicates +# the plugin has successfully collected and produced. +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: rest-client +# gem: json +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Copyright 2011 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'rest-client' +require 'json' + +class ESClusterMetrics < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to metric', + short: '-s SCHEME', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}.elasticsearch.cluster" + + option :host, + description: 'Elasticsearch host', + short: '-h HOST', + long: '--host HOST', + default: 'localhost' + + option :port, + description: 'Elasticsearch port', + short: '-p PORT', + long: '--port PORT', + proc: proc(&:to_i), + default: 9200 + + option :timeout, + description: 'Sets the connection timeout for REST client', + short: '-t SECS', + long: '--timeout SECS', + proc: proc(&:to_i), + default: 30 + + def acquire_es_version + info = get_es_resource('/') + info['version']['number'] + end + + def get_es_resource(resource) + r = RestClient::Resource.new("http://#{config[:host]}:#{config[:port]}/#{resource}", timeout: config[:timeout]) + JSON.parse(r.get) + rescue Errno::ECONNREFUSED + warning 'Connection refused' + rescue RestClient::RequestTimeout + warning 'Connection timed out' + end + + def master? + state = get_es_resource('/_cluster/state?filter_routing_table=true&filter_metadata=true&filter_indices=true') + if Gem::Version.new(acquire_es_version) >= Gem::Version.new('1.0.0') + local = get_es_resource('/_nodes/_local') + else + local = get_es_resource('/_cluster/nodes/_local') + end + local['nodes'].keys.first == state['master_node'] + end + + def acquire_health + health = get_es_resource('/_cluster/health').reject { |k, _v| %w(cluster_name timed_out).include?(k) } + health['status'] = %w(red yellow green).index(health['status']) + health + end + + def acquire_document_count + document_count = get_es_resource('/_count?q=*:*') + document_count['count'] + end + + def run + if master? + acquire_health.each do |k, v| + output(config[:scheme] + '.' + k, v) + end + output(config[:scheme] + '.document_count', acquire_document_count) + end + exit + end +end diff --git a/roles/sensu-client/files/plugins/es-node-graphite.rb b/roles/sensu-client/files/plugins/es-node-graphite.rb new file mode 100755 index 0000000..de9cc46 --- /dev/null +++ b/roles/sensu-client/files/plugins/es-node-graphite.rb @@ -0,0 +1,238 @@ +#!/opt/sensu/embedded/bin/ruby +# +# es-node-graphite +# +# DESCRIPTION: +# This check creates node metrics from the elasticsearch API +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux, Windows, BSD, Solaris, etc +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: rest-client +# gem: json +# +# USAGE: +# #YELLOW +# +# NOTES: +# 2014/04 +# Modifid by Vincent Janelle @randomfrequency http://github.com/vjanelle +# Add more metrics, fix es 1.x URLs, translate graphite stats from +# names directly +# +# 2012/12 - Modified by Zach Dunn @SillySophist http://github.com/zadunn +# To add more metrics, and correct for new versins of ES. Tested on +# ES Version 0.19.8 +# +# LICENSE: +# Copyright 2013 Vincent Janelle +# Copyright 2012 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'rest-client' +require 'json' + +class ESMetrics < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to queue_name.metric', + short: '-s SCHEME', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}.elasticsearch" + + option :server, + description: 'Elasticsearch server host.', + short: '-h HOST', + long: '--host HOST', + default: 'localhost' + + option :port, + description: 'Elasticsearch port.', + short: '-p PORT', + long: '--port PORT', + proc: proc(&:to_i), + default: 9200 + + option :timeout, + description: 'Request timeout to elasticsearch', + short: '-t TIMEOUT', + long: '--timeout TIMEOUT', + proc: proc(&:to_i), + default: 30 + + option :disable_jvm_stats, + description: 'Disable JVM statistics', + long: '--disable-jvm-stats', + boolean: true, + default: false + + option :disable_os_stats, + description: 'Disable OS Stats', + long: '--disable-os-stat', + boolean: true, + default: false + + option :disable_process_stats, + description: 'Disable process statistics', + long: '--disable-process-stats', + boolean: true, + default: false + + option :disable_thread_pool_stats, + description: 'Disable thread-pool statistics', + long: '--disable-thread-pool-stats', + boolean: true, + default: false + + def get_es_resource(resource) + r = RestClient::Resource.new("http://#{config[:server]}:#{config[:port]}/#{resource}?pretty", timeout: config[:timeout]) + JSON.parse(r.get) + rescue Errno::ECONNREFUSED + warning 'Connection refused' + rescue RestClient::RequestTimeout + warning 'Connection timed out' + end + + def acquire_es_version + info = get_es_resource('/') + info['version']['number'] + end + + def run + # invert various stats depending on if some flags are set + os_stat = !config[:disable_os_stats] + process_stats = !config[:disable_process_stats] + jvm_stats = !config[:disable_jvm_stats] + tp_stats = !config[:disable_thread_pool_stats] + + es_version = Gem::Version.new(acquire_es_version) + + if es_version >= Gem::Version.new('1.0.0') + stats_query_array = %w(indices http network transport thread_pool) + stats_query_array.push('jvm') if jvm_stats == true + stats_query_array.push('os') if os_stat == true + stats_query_array.push('process') if process_stats == true + stats_query_array.push('tp_stats') if tp_stats == true + stats_query_string = stats_query_array.join(',') + else + stats_query_string = [ + 'clear=true', + 'indices=true', + 'http=true', + "jvm=#{jvm_stats}", + 'network=true', + "os=#{os_stat}", + "process=#{process_stats}", + "thread_pool=#{tp_stats}", + 'transport=true', + 'thread_pool=true' + ].join('&') + end + + if Gem::Version.new(acquire_es_version) >= Gem::Version.new('1.0.0') + stats = get_es_resource("_nodes/_local/stats?#{stats_query_string}") + else + stats = get_es_resource("_cluster/nodes/_local/stats?#{stats_query_string}") + end + + timestamp = Time.now.to_i + node = stats['nodes'].values.first + + metrics = {} + + if os_stat + metrics['os.load_average'] = node['os']['load_average'][0] + metrics['os.load_average.1'] = node['os']['load_average'][0] + metrics['os.load_average.5'] = node['os']['load_average'][1] + metrics['os.load_average.15'] = node['os']['load_average'][2] + metrics['os.mem.free_in_bytes'] = node['os']['mem']['free_in_bytes'] + # ... Process uptime in millis? + metrics['os.uptime'] = node['os']['uptime_in_millis'] + end + + if process_stats + metrics['process.mem.resident_in_bytes'] = node['process']['mem']['resident_in_bytes'] + end + + if jvm_stats + metrics['jvm.mem.heap_used_in_bytes'] = node['jvm']['mem']['heap_used_in_bytes'] + metrics['jvm.mem.non_heap_used_in_bytes'] = node['jvm']['mem']['non_heap_used_in_bytes'] + metrics['jvm.mem.max_heap_size_in_bytes'] = 0 + + node['jvm']['mem']['pools'].each do |k, v| + metrics["jvm.mem.#{k.gsub(' ', '_')}.max_in_bytes"] = v['max_in_bytes'] + metrics['jvm.mem.max_heap_size_in_bytes'] += v['max_in_bytes'] + end + + # This makes absolutely no sense - not sure what it's trying to measure - @vjanelle + # metrics['jvm.gc.collection_time_in_millis'] = node['jvm']['gc']['collection_time_in_millis'] + \ + # node['jvm']['mem']['pools']['CMS Old Gen']['max_in_bytes'] + + node['jvm']['gc']['collectors'].each do |gc, gc_value| + gc_value.each do |k, v| + # this contains stupid things like '28ms' and '2s', and there's already + # something that counts in millis, which makes more sense + unless k.end_with? 'collection_time' + metrics["jvm.gc.collectors.#{gc}.#{k}"] = v + end + end + end + + metrics['jvm.threads.count'] = node['jvm']['threads']['count'] + metrics['jvm.threads.peak_count'] = node['jvm']['threads']['peak_count'] + end + + node['indices'].each do |type, index| + index.each do |k, v| + # #YELLOW + unless k =~ /(_time|memory|size$)/ # rubocop:disable IfUnlessModifier + metrics["indices.#{type}.#{k}"] = v + end + end + end + + node['transport'].each do |k, v| + # #YELLOW + unless k =~ /(_size$)/ # rubocop:disable IfUnlessModifier + metrics["transport.#{k}"] = v + end + end + + metrics['http.current_open'] = node['http']['current_open'] + metrics['http.total_opened'] = node['http']['total_opened'] + + metrics['network.tcp.active_opens'] = node['network']['tcp']['active_opens'] + metrics['network.tcp.passive_opens'] = node['network']['tcp']['passive_opens'] + + metrics['network.tcp.in_segs'] = node['network']['tcp']['in_segs'] + metrics['network.tcp.out_segs'] = node['network']['tcp']['out_segs'] + metrics['network.tcp.retrans_segs'] = node['network']['tcp']['retrans_segs'] + metrics['network.tcp.attempt_fails'] = node['network']['tcp']['attempt_fails'] + metrics['network.tcp.in_errs'] = node['network']['tcp']['in_errs'] + metrics['network.tcp.out_rsts'] = node['network']['tcp']['out_rsts'] + + metrics['network.tcp.curr_estab'] = node['network']['tcp']['curr_estab'] + metrics['network.tcp.estab_resets'] = node['network']['tcp']['estab_resets'] + + if tp_stats + node['thread_pool'].each do |pool, stat| + stat.each do |k, v| + metrics["thread_pool.#{pool}.#{k}"] = v + end + end + end + + metrics.each do |k, v| + output([config[:scheme], k].join('.'), v, timestamp) + end + exit + end +end diff --git a/roles/sensu-client/files/plugins/es-node-metrics.rb b/roles/sensu-client/files/plugins/es-node-metrics.rb new file mode 100755 index 0000000..99bcebf --- /dev/null +++ b/roles/sensu-client/files/plugins/es-node-metrics.rb @@ -0,0 +1,75 @@ +#!/opt/sensu/embedded/bin/ruby +# +# es-node-metrics +# +# DESCRIPTION: +# This plugin uses the ES API to collect metrics, producing a JSON +# document which is outputted to STDOUT. An exit status of 0 indicates +# the plugin has successfully collected and produced. +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Copyright 2011 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'rest-client' +require 'json' + +class ESMetrics < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to queue_name.metric', + short: '-s SCHEME', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}.elasticsearch" + + option :host, + description: 'Elasticsearch server host.', + short: '-h HOST', + long: '--host HOST', + default: 'localhost' + + option :port, + description: 'Elasticsearch port', + short: '-p PORT', + long: '--port PORT', + proc: proc(&:to_i), + default: 9200 + + def run + ln = RestClient::Resource.new "http://#{config[:host]}:#{config[:port]}/_cluster/nodes/_local", timeout: 30 + stats = RestClient::Resource.new "http://#{config[:host]}:#{config[:port]}/_cluster/nodes/_local/stats", timeout: 30 + ln = JSON.parse(ln.get) + stats = JSON.parse(stats.get) + timestamp = Time.now.to_i + node = stats['nodes'].values.first + node['jvm']['mem']['heap_max_in_bytes'] = ln['nodes'].values.first['jvm']['mem']['heap_max_in_bytes'] + metrics = {} + metrics['os.load_average'] = node['os']['load_average'][0] + metrics['os.mem.free_in_bytes'] = node['os']['mem']['free_in_bytes'] + metrics['process.mem.resident_in_bytes'] = node['process']['mem']['resident_in_bytes'] + metrics['jvm.mem.heap_used_in_bytes'] = node['jvm']['mem']['heap_used_in_bytes'] + metrics['jvm.mem.non_heap_used_in_bytes'] = node['jvm']['mem']['non_heap_used_in_bytes'] + metrics['jvm.gc.collection_time_in_millis'] = node['jvm']['gc']['collection_time_in_millis'] + metrics.each do |k, v| + output([config[:scheme], k].join('.'), v, timestamp) + end + exit + end +end diff --git a/roles/sensu-client/files/plugins/flapper.rb b/roles/sensu-client/files/plugins/flapper.rb new file mode 100755 index 0000000..3cb5645 --- /dev/null +++ b/roles/sensu-client/files/plugins/flapper.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Generate an event every N minutes to verify upstream functionality +# + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class Flapper < Sensu::Plugin::Check::CLI + + option :duration, + :short => '-d DURATION', + :description => "Time until emitting next state change", + :proc => proc {|a| a.to_i }, + :in => [1, 2, 3, 5, 10, 15], + :default => 1 + + option :criticality, + :description => "Set sensu alert level, default is critical", + :short => '-z CRITICALITY', + :long => '--criticality CRITICALITY', + :default => 'critical' + + def switch_on_criticality + if config[:criticality] == 'warning' + warning + else + critical + end + end + + def run + (Time.now.min / config[:duration] % 2) == 0 ? switch_on_criticality : ok + end +end diff --git a/roles/sensu-client/files/plugins/load-metrics.rb b/roles/sensu-client/files/plugins/load-metrics.rb new file mode 100755 index 0000000..2a062bc --- /dev/null +++ b/roles/sensu-client/files/plugins/load-metrics.rb @@ -0,0 +1,43 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# System Load Stats Plugin +# === +# +# This plugin uses uptime to collect load metrics +# Basically copied from sensu-community-plugins/plugins/system/vmstat-metrics.rb +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'socket' + +class LoadStat < Sensu::Plugin::Metric::CLI::Graphite + + option :scheme, + :description => "Metric naming scheme, text to prepend to .$parent.$child", + :long => "--scheme SCHEME", + :default => "#{Socket.gethostname}" + + def run + result = `uptime`.gsub(',', '').split(' ') + result = result[-3..-1] + + timestamp = Time.now.to_i + metrics = { + :load_avg => { + :one => result[0], + :five => result[1], + :fifteen => result[2] + } + } + metrics.each do |parent, children| + children.each do |child, value| + output [config[:scheme], parent, child].join("."), value, timestamp + end + end + exit + end + +end diff --git a/roles/sensu-client/files/plugins/memcached-graphite.rb b/roles/sensu-client/files/plugins/memcached-graphite.rb new file mode 100755 index 0000000..abbbcc7 --- /dev/null +++ b/roles/sensu-client/files/plugins/memcached-graphite.rb @@ -0,0 +1,54 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Push Memcached stats into graphite +# +# Dependencies +# ----------- +# - Ruby gem `memcached` +# +# === +# +# TODO: HitRatio percent and per second calculations +# +# Copyright 2012 Pete Shima +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'memcached' +require 'socket' + +class MemcachedGraphite < Sensu::Plugin::Metric::CLI::Graphite + + option :host, + :short => "-h HOST", + :long => "--host HOST", + :description => "Memcached Host to connect to", + :default => 'localhost' + + option :port, + :short => "-p PORT", + :long => "--port PORT", + :description => "Memcached Port to connect to", + :proc => proc { |p| p.to_i }, + :default => 11211 + + option :scheme, + :description => "Metric naming scheme, text to prepend to metric", + :short => "-s SCHEME", + :long => "--scheme SCHEME", + :default => "#{::Socket.gethostname}.memcached" + + def run + cache = Memcached.new("#{config[:host]}:#{config[:port]}") + + cache.stats.each do |k, v| + output "#{config[:scheme]}.#{k}", v + end + + exit + end + +end diff --git a/roles/sensu-client/files/plugins/memory-metrics.rb b/roles/sensu-client/files/plugins/memory-metrics.rb new file mode 100755 index 0000000..cd0514c --- /dev/null +++ b/roles/sensu-client/files/plugins/memory-metrics.rb @@ -0,0 +1,45 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'socket' + +class MemoryGraphite < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to metric', + short: '-s SCHEME', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}.memory" + + def run + # Metrics borrowed from hoardd: https://github.com/coredump/hoardd + mem = metrics_hash + mem.each do |k, v| + output "#{config[:scheme]}.#{k}", v + end + exit + end + + def metrics_hash + mem = {} + meminfo_output.each_line do |line| + mem['total'] = line.split(/\s+/)[1].to_i * 1024 if line.match(/^MemTotal/) + mem['free'] = line.split(/\s+/)[1].to_i * 1024 if line.match(/^MemFree/) + mem['buffers'] = line.split(/\s+/)[1].to_i * 1024 if line.match(/^Buffers/) + mem['cached'] = line.split(/\s+/)[1].to_i * 1024 if line.match(/^Cached/) + mem['swapTotal'] = line.split(/\s+/)[1].to_i * 1024 if line.match(/^SwapTotal/) + mem['swapFree'] = line.split(/\s+/)[1].to_i * 1024 if line.match(/^SwapFree/) + mem['dirty'] = line.split(/\s+/)[1].to_i * 1024 if line.match(/^Dirty/) + end + mem['swapUsed'] = mem['swapTotal'] - mem['swapFree'] + mem['used'] = mem['total'] - mem['free'] + mem['usedWOBuffersCaches'] = mem['used'] - (mem['buffers'] + mem['cached']) + mem['freeWOBuffersCaches'] = mem['free'] + (mem['buffers'] + mem['cached']) + mem['swapUsedPercentage'] = 100 * mem['swapUsed'] / mem['swapTotal'] if mem['swapTotal'] > 0 + mem + end + + def meminfo_output + File.open('/proc/meminfo', 'r') + end +end diff --git a/roles/sensu-client/files/plugins/metrics-disk-capacity.rb b/roles/sensu-client/files/plugins/metrics-disk-capacity.rb new file mode 100755 index 0000000..447be12 --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-disk-capacity.rb @@ -0,0 +1,113 @@ +#! /usr/bin/env ruby +# encoding: UTF-8 +# +# disk-capacity-metrics +# +# DESCRIPTION: +# This plugin uses df to collect disk capacity metrics +# disk-metrics.rb looks at /proc/stat which doesnt hold capacity metricss. +# could have intetrated this into disk-metrics.rb, but thought I'd leave it up to +# whomever implements the checks. +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: socket +# +# USAGE: +# +# NOTES: +# +# LICENSE: +# Copyright 2012 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/metric/cli' +require 'socket' + +# +# Disk Capacity +# +class DiskCapacity < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to .$parent.$child', + long: '--scheme SCHEME', + default: Socket.gethostname.to_s + + # Unused ? + # + def convert_integers(values) + values.each_with_index do |value, index| + begin + converted = Integer(value) + values[index] = converted + rescue ArgumentError # rubocop:disable HandleExceptions + end + end + values + end + + # Main function + # + def run + # Get capacity metrics from DF as they don't appear in /proc + `df -PT`.split("\n").drop(1).each do |line| + begin + fs, _type, _blocks, used, avail, capacity, _mnt = line.split + + timestamp = Time.now.to_i + if fs =~ /\/dev/ + fs = fs.gsub('/dev/', '') + metrics = { + disk: { + "#{fs}.used" => used, + "#{fs}.avail" => avail, + "#{fs}.capacity" => capacity.delete('%') + } + } + metrics.each do |parent, children| + children.each do |child, value| + output [config[:scheme], parent, child].join('.'), value, timestamp + end + end + end + rescue + unknown "malformed line from df: #{line}" + end + end + + # Get inode capacity metrics + `df -Pi`.split("\n").drop(1).each do |line| + begin + fs, _inodes, used, avail, capacity, _mnt = line.split + + timestamp = Time.now.to_i + if fs =~ /\/dev/ + fs = fs.gsub('/dev/', '') + metrics = { + disk: { + "#{fs}.iused" => used, + "#{fs}.iavail" => avail, + "#{fs}.icapacity" => capacity.delete('%') + } + } + metrics.each do |parent, children| + children.each do |child, value| + output [config[:scheme], parent, child].join('.'), value, timestamp + end + end + end + rescue + unknown "malformed line from df: #{line}" + end + end + ok + end +end diff --git a/roles/sensu-client/files/plugins/metrics-disk-usage.rb b/roles/sensu-client/files/plugins/metrics-disk-usage.rb new file mode 100755 index 0000000..56c6fd6 --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-disk-usage.rb @@ -0,0 +1,121 @@ +#! /usr/bin/env ruby +# encoding: UTF-8 +# +# disk-usage-metrics +# +# DESCRIPTION: +# This plugin uses df to collect disk capacity metrics +# disk-usage-metrics.rb looks at /proc/stat which doesnt hold capacity metricss. +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: socket +# +# USAGE: +# +# NOTES: +# Based on disk-capacity-metrics.rb by bhenerey and nstielau +# The difference here being how the key is defined in graphite and the +# size we emit to graphite(now using megabytes). Also i dropped inode info. +# Using this as an example +# Filesystem Size Used Avail Use% Mounted on +# /dev/mapper/precise64-root 79G 3.5G 72G 5% / +# /dev/sda1 228M 25M 192M 12% /boot +# /dev/sdb1 99G 2G 97G 2% /media/sda1 +# The keys with this plugin will be +# disk_usage.root, disk_usage.root.boot, and disk_usage.root.media.sda1 +# instead of disk.dev.mapper.precise64-root, disk.sda1, and disk.sda2 +# +# Use --flatten option to reduce graphite "tree" by using underscores rather +# then dots for subdirs. Also eliminates 'root' on mounts other than '/'. +# Keys with --flatten option would be +# disk_usage.root, disk_usage.boot, and disk_usage.media_sda1 +# +# Mountpoints can be specifically included or ignored using -i or -I options: +# e.g. disk-usage-metric.rb -i ^/boot,^/media +# +# LICENSE: +# Copyright 2012 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/metric/cli' +require 'socket' + +# +# Disk Usage Metrics +# +class DiskUsageMetrics < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to .$parent.$child', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}.disk_usage" + + option :ignore_mnt, + description: 'Ignore mounts matching pattern(s)', + short: '-i MNT[,MNT]', + long: '--ignore-mount', + proc: proc { |a| a.split(',') } + + option :include_mnt, + description: 'Include only mounts matching pattern(s)', + short: '-I MNT[,MNT]', + long: '--include-mount', + proc: proc { |a| a.split(',') } + + option :flatten, + description: 'Output mounts with underscore rather than dot', + short: '-f', + long: '--flatten', + boolean: true, + default: false + + option :local, + description: 'Only check local filesystems (df -l option)', + short: '-l', + long: '--local', + boolean: true, + default: false + + option :block_size, + description: 'Set block size for sizes printed', + short: '-B BLOCK_SIZE', + long: '--block-size BLOCK_SIZE', + default: 'M' + + # Main function + # + def run + delim = config[:flatten] == true ? '_' : '.' + # Get disk usage from df with used and avail in megabytes + # #YELLOW + `df -PB#{config[:block_size]} #{config[:local] ? '-l' : ''}`.split("\n").drop(1).each do |line| + _, _, used, avail, used_p, mnt = line.split + + unless %r{/sys[/|$]|/dev[/|$]|/run[/|$]} =~ mnt + next if config[:ignore_mnt] && config[:ignore_mnt].find { |x| mnt.match(x) } + next if config[:include_mnt] && !config[:include_mnt].find { |x| mnt.match(x) } + mnt = if config[:flatten] + mnt.eql?('/') ? 'root' : mnt.gsub(/^\//, '') + else + # If mnt is only / replace that with root if its /tmp/foo + # replace first occurance of / with root. + mnt.length == 1 ? 'root' : mnt.gsub(/^\//, 'root.') + end + # Fix subsequent slashes + mnt = mnt.gsub '/', delim + output [config[:scheme], mnt, 'used'].join('.'), used.gsub(config[:block_size], '') + output [config[:scheme], mnt, 'avail'].join('.'), avail.gsub(config[:block_size], '') + output [config[:scheme], mnt, 'used_percentage'].join('.'), used_p.delete('%') + end + end + ok + end +end diff --git a/roles/sensu-client/files/plugins/metrics-disk.rb b/roles/sensu-client/files/plugins/metrics-disk.rb new file mode 100755 index 0000000..59285c8 --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-disk.rb @@ -0,0 +1,87 @@ +#! /usr/bin/env ruby +# encoding: UTF-8 +# +# disk-metrics +# +# DESCRIPTION: +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: socket +# +# USAGE: +# +# NOTES: +# Devices can be specifically included or ignored using -i or -I options: +# e.g. metrics-disk.rb -I [svx]d[a-z][0-9]* +# +# LICENSE: +# Copyright 2012 Sonian, Inc +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/metric/cli' +require 'socket' + +# +# Disk Graphite +# +class DiskGraphite < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to metric', + short: '-s SCHEME', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}.disk" + + # this option uses lsblk to convert the dm- name to the LVM name. + # sample metric scheme without this: + # .disk.dm-0 + # sample metric scheme with this: + # .disk.vg-root + option :convert, + description: 'Convert devicemapper to logical volume name', + short: '-c', + long: '--convert', + default: false + + option :ignore_device, + description: 'Ignore devices matching pattern(s)', + short: '-i DEV[,DEV]', + long: '--ignore-device', + proc: proc { |a| a.split(',') } + + option :include_device, + description: 'Include only devices matching pattern(s)', + short: '-I DEV[,DEV]', + long: '--include-device', + proc: proc { |a| a.split(',') } + + # Main function + def run + # http://www.kernel.org/doc/Documentation/iostats.txt + metrics = %w(reads readsMerged sectorsRead readTime writes writesMerged sectorsWritten writeTime ioInProgress ioTime ioTimeWeighted) + + File.open('/proc/diskstats', 'r').each_line do |line| + stats = line.strip.split(/\s+/) + _major, _minor, dev = stats.shift(3) + if config[:convert] + dev = `lsblk -P -o NAME /dev/"#{dev}"| cut -d\\" -f2`.lines.first.chomp! if dev =~ /^dm-.*$/ + end + next if stats == ['0'].cycle.take(stats.size) + + next if config[:ignore_device] && config[:ignore_device].find { |x| dev.match(x) } + next if config[:include_device] && !config[:include_device].find { |x| dev.match(x) } + + metrics.size.times { |i| output "#{config[:scheme]}.#{dev}.#{metrics[i]}", stats[i] } + end + + ok + end +end diff --git a/roles/sensu-client/files/plugins/metrics-filesize.rb b/roles/sensu-client/files/plugins/metrics-filesize.rb new file mode 100755 index 0000000..e5f0ecf --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-filesize.rb @@ -0,0 +1,76 @@ +#! /usr/bin/env ruby +# +# metrics-filesize +# +# DESCRIPTION: +# Simple wrapper around `stat` for getting file size stats, +# in both, bytes and blocks. +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# +# USAGE: +# #YELLOW +# +# NOTES: +# Based on: Curl HTTP Timings metric (Sensu Community Plugins) by Joe Miller +# +# LICENSE: +# Copyright 2014 Pablo Figue +# Released under the same terms as Sensu (the MIT license); see MITLICENSE +# for details. +# + +require 'socket' +require 'sensu-plugin/metric/cli' + +class FilesizeMetrics < Sensu::Plugin::Metric::CLI::Graphite + option :filepath, + short: '-f PATH', + long: '--file PATH', + description: 'Absolute path to file to measure', + required: true + + option :omitblocks, + short: '-o', + long: '--blocksno', + description: 'Don\'t report size in blocks', + required: true, + default: false + + option :omitbytes, + short: '-b', + long: '--bytesno', + description: 'Don\'t report size in bytes', + required: true, + default: false + + option :scheme, + description: 'Metric naming scheme, text to prepend to metric', + short: '-s SCHEME', + long: '--scheme SCHEME', + required: true, + default: "#{Socket.gethostname}.filesize" + + def run + cmd = "/usr/bin/stat --format=\"%s,%b,\" #{config[:filepath]}" + output = `#{cmd}` + + (bytes, blocks) = output.split(',') + unless config[:omitbytes] + output "#{config[:scheme]}.#{config[:filepath]}.bytes", bytes + end + unless config[:omitblocks] + output "#{config[:scheme]}.#{config[:filepath]}.blocks", blocks + end + + ok + end +end + diff --git a/roles/sensu-client/files/plugins/metrics-net.rb b/roles/sensu-client/files/plugins/metrics-net.rb new file mode 100755 index 0000000..240b66f --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-net.rb @@ -0,0 +1,86 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Linux network interface metrics +# ==== +# +# Simple plugin that fetchs metrics from all interfaces +# on the box using the /sys/class interface. +# +# Use the data with graphite's `nonNegativeDerivative()` function +# to construct per-second graphs for your hosts. +# +# Non `eth` and `bond` ifaces are ignored by default. +# +# Compat +# ------ +# +# This plugin uses the `/sys/class/net//statistics/{rx,tx}_*` +# files to fetch stats. On older linux boxes without /sys, this same +# info can be fetched from /proc/net/dev but additional parsing +# will be required. +# +# Example: +# -------- +# +# $ ./metrics-packets.rb --scheme servers.web01 -i eth +# servers.web01.eth0.tx_packets 982965 1351112745 +# servers.web01.eth0.rx_packets 1180186 1351112745 +# servers.web01.eth1.tx_packets 273936669 1351112745 +# servers.web01.eth1.rx_packets 563787422 1351112745 +# +# Copyright 2012 Joe Miller +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'socket' + +class LinuxPacketMetrics < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to metric', + short: '-s SCHEME', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}.net" + option :all, + description: 'Return results for all interfaces', + short: '-a', + long: '--all', + default: false + option :accept_ifaces, + description: 'List of interfaces to collect from (default eth,bond)', + short: '-i IFACE1,IFACE2,...', + long: '--interfaces IFACE1,IFACE2,...', + default: 'eth,bond' + + def run + timestamp = Time.now.to_i + + Dir.glob('/sys/class/net/*').each do |iface_path| + next if File.file?(iface_path) + current_iface = File.basename(iface_path) + accept_ifaces = config[:accept_ifaces].split(',') + next if current_iface == 'lo' + next if !config[:all] and !accept_ifaces.any? { |accept_iface| current_iface.start_with?(accept_iface) } + + tx_pkts = File.open(iface_path + '/statistics/tx_packets').read.strip + rx_pkts = File.open(iface_path + '/statistics/rx_packets').read.strip + tx_bytes = File.open(iface_path + '/statistics/tx_bytes').read.strip + rx_bytes = File.open(iface_path + '/statistics/rx_bytes').read.strip + tx_errors = File.open(iface_path + '/statistics/tx_errors').read.strip + rx_errors = File.open(iface_path + '/statistics/rx_errors').read.strip + tx_dropped = File.open(iface_path + '/statistics/tx_dropped').read.strip + rx_dropped = File.open(iface_path + '/statistics/rx_dropped').read.strip + output "#{config[:scheme]}.#{current_iface}.tx_packets", tx_pkts, timestamp + output "#{config[:scheme]}.#{current_iface}.rx_packets", rx_pkts, timestamp + output "#{config[:scheme]}.#{current_iface}.tx_bytes", tx_bytes, timestamp + output "#{config[:scheme]}.#{current_iface}.rx_bytes", rx_bytes, timestamp + output "#{config[:scheme]}.#{current_iface}.tx_errors", tx_errors, timestamp + output "#{config[:scheme]}.#{current_iface}.rx_errors", rx_errors, timestamp + output "#{config[:scheme]}.#{current_iface}.tx_dropped", tx_dropped, timestamp + output "#{config[:scheme]}.#{current_iface}.rx_dropped", rx_dropped, timestamp + end + exit + end +end diff --git a/roles/sensu-client/files/plugins/metrics-netif.rb b/roles/sensu-client/files/plugins/metrics-netif.rb new file mode 100755 index 0000000..e2b0120 --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-netif.rb @@ -0,0 +1,64 @@ +#! /usr/bin/env ruby +# +# netif-metrics +# +# DESCRIPTION: +# Network interface throughput +# +# OUTPUT: +# metric data +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Copyright 2014 Sonian, Inc. and contributors. +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/metric/cli' +require 'socket' + +# +# Netif Metrics +# +class NetIFMetrics < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to .$parent.$child', + long: '--scheme SCHEME', + default: "#{Socket.gethostname}" + option :interfaces, + description: 'list of interfaces to check', + long: '--interfaces [eth0,eth1]', + default: 'eth0' + option :interval, + descrption: 'how many seconds to collect data for', + long: '--interval 1', + default: 1 + + def run + `sar -n DEV #{config[:interval]} 1 | grep Average | grep -v IFACE`.each_line do |line| # rubocop:disable Style/Next + stats = line.split(/\s+/) + unless stats.empty? + stats.shift + nic = stats.shift + if config[:interfaces].include? nic + output "#{config[:scheme]}.#{nic}.rx_kb_per_sec", stats[2].to_f * 8 if stats[3] + output "#{config[:scheme]}.#{nic}.tx_kb_per_sec", stats[3].to_f * 8 if stats[3] + end + end + end + + exit + end +end + diff --git a/roles/sensu-client/files/plugins/metrics-network-queues.py b/roles/sensu-client/files/plugins/metrics-network-queues.py new file mode 100755 index 0000000..3985f91 --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-network-queues.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# +# metrics-network-queues.py +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# Python 2.7+ (untested on Python3, should work though) +# +# USAGE: +# +# metrics-network-queues.py -n [-s ] +# +# DESCRIPTION: +# Finds the pid[s] corresponding to a process name and obtains the lengths of +# the receive and send network queues for all of the process' sockets. This +# data is gathered from the "netstat -tpane" command. +# +# Code adapted from Jaime Gogo's script in the Sensu Plugins community +# https://github.com/sensu-plugins/sensu-plugins-process-checks/blob/master/bin/metrics-per-process.py +# +# Released under the same terms as Sensu (the MIT license); see MITLICENSE +# for details. +# +# Siva Mullapudi + +import argparse +import sys +import os +import time +import subprocess + +STATE_OK = 0 +STATE_WARNING = 1 +STATE_CRITICAL = 2 + +PROC_ROOT_DIR = '/proc/' + +def find_pids_from_name(process_name): + '''Find process PID from name using /proc//comm''' + + pids_in_proc = [ pid for pid in os.listdir(PROC_ROOT_DIR) if pid.isdigit() ] + pids = [] + for pid in pids_in_proc: + path = PROC_ROOT_DIR + pid + if 'comm' in os.listdir(path): + file_handler = open(path + '/comm', 'r') + if file_handler.read().rstrip() == process_name: + pids.append(int(pid)) + return pids + +def search_output(output, token): + matches = "" + for line in output.splitlines(): + if token in line: + matches = matches + line + "\n" + return matches.rstrip("\n") + +def sum_dicts(dict1, dict2): + return dict(dict1.items() + dict2.items() + + [(k, dict1[k] + dict2[k]) for k in dict1.viewkeys() & dict2.viewkeys()]) + +def queue_lengths_per_pid(pid): + '''Gets network rx/tx queue lengths for a specific pid''' + + process_queues = {'receive_queue_length': 0, 'send_queue_length': 0} + netstat = subprocess.check_output(['netstat -tpane'], shell=True) + process_sockets = search_output(netstat, str(pid)) + + for socket in process_sockets.splitlines(): + rx_queue_length = int(socket.split()[1]) + tx_queue_length = int(socket.split()[2]) + process_queues['receive_queue_length'] += rx_queue_length + process_queues['send_queue_length'] += tx_queue_length + + return process_queues + +def multi_pid_queue_lengths(pids): + stats = {'receive_queue_length': 0, 'send_queue_length': 0} + for pid in pids: + stats = sum_dicts(stats, queue_lengths_per_pid(pid)) + return stats + +def graphite_printer(stats, graphite_scheme): + now = time.time() + for stat in stats: + print "%s.%s %s %d" % (graphite_scheme, stat, stats[stat], now) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-n', '--process_name', required=True) + parser.add_argument('-s', '--scheme', required=True) + args = parser.parse_args() + + pids = find_pids_from_name(args.process_name) + + if not pids: + print 'Cannot find pids for this process. Enter a valid process name.' + sys.exit(STATE_CRITICAL) + + total_process_queues = multi_pid_queue_lengths(pids) + graphite_printer(total_process_queues, args.scheme) + + sys.exit(STATE_OK) + +if __name__ == "__main__": + main() + diff --git a/roles/sensu-client/files/plugins/metrics-power-temp.py b/roles/sensu-client/files/plugins/metrics-power-temp.py new file mode 100755 index 0000000..0e97464 --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-power-temp.py @@ -0,0 +1,64 @@ +#!/usr/bin/python +import re +import socket +import subprocess +import time + +def _get_key_value_pair(key, stanza): + value = None + for line in stanza: + if key in line: + parts = line.split(':') + value = parts[1].strip() + return value + + +def handle_k10temp(stanza): + temp = _get_key_value_pair('temp1_input', stanza) + return 'temp', temp + + +def handle_fam15h_power(stanza): + power = _get_key_value_pair('power1_input', stanza) + return 'power', power + + +def stanza_type_device(stanza): + m = re.match(r'(\w+)-(pci-[0-9a-fA-F]+)', stanza[0]) + if not m: + raise Exception("Unkown sensor type %s" % stanza[0]) + return m.group(1), m.group(2) + + +def output_graphite(sensor_type, device, value): + hostname = socket.gethostname().split('.') + shortname = hostname[0] + datacenter = hostname[1] + unixtime = int(time.time()) + print "stats.%s.%s.%s.%s %s %d" % (shortname, datacenter, sensor_type, + device, value, unixtime) + + +def main(): + stanzas = [] + output = subprocess.check_output(['sensors -u'], shell=True) + current_stanza = [] + for line in output.split('\n'): + if len(line) == 0 and len(current_stanza): + stanzas.append(current_stanza) + current_stanza = [] + else: + current_stanza.append(line) + + for stanza in stanzas: + try: + sensor, device = stanza_type_device(stanza) + func = globals()['handle_%s' % sensor] + sensor_type, value = func(stanza[1:]) + output_graphite(sensor_type, device, value) + except: + continue + +if __name__ == '__main__': + main() + diff --git a/roles/sensu-client/files/plugins/metrics-process-usage.py b/roles/sensu-client/files/plugins/metrics-process-usage.py new file mode 100755 index 0000000..d1c4c37 --- /dev/null +++ b/roles/sensu-client/files/plugins/metrics-process-usage.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# +# metrics-process-usage.py +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# Python 2.7+ (untested on Python3, should work though) +# Python module: psutil https://pypi.python.org/pypi/psutil +# +# USAGE: +# +# metrics-process-usage.py -n -w -c -W -C [-s ] [-z ] +# +# DESCRIPTION: +# Finds the pid[s] corresponding to a process name and obtains the necessary +# cpu and memory usage stats. Returns WARNING or CRITICAL when these stats +# exceed user specified limits. +# +# Code adapted from Jaime Gogo's script in the Sensu Plugins community: +# https://github.com/sensu-plugins/sensu-plugins-process-checks/blob/master/bin/metrics-per-process.py +# +# Released under the same terms as Sensu (the MIT license); see MITLICENSE +# for details. +# +# Siva Mullapudi + +import argparse +import sys +import os +import time +import psutil + +STATE_OK = 0 +STATE_WARNING = 1 +STATE_CRITICAL = 2 +CRITICALITY = 'critical' + +PROC_ROOT_DIR = '/proc/' + +def switch_on_criticality(): + if CRITICALITY == 'warning': + sys.exit(STATE_WARNING) + else: + sys.exit(STATE_CRITICAL) + +def find_pids_from_name(process_name): + '''Find process PID from name using /proc//comm''' + + pids_in_proc = [ pid for pid in os.listdir(PROC_ROOT_DIR) if pid.isdigit() ] + pids = [] + for pid in pids_in_proc: + path = PROC_ROOT_DIR + pid + if 'comm' in os.listdir(path): + file_handler = open(path + '/comm', 'r') + if file_handler.read().rstrip() == process_name: + pids.append(int(pid)) + return pids + +def sum_dicts(dict1, dict2): + return dict(dict1.items() + dict2.items() + + [(k, dict1[k] + dict2[k]) for k in dict1.viewkeys() & dict2.viewkeys()]) + +def stats_per_pid(pid): + '''Gets process stats, cpu and memory usage in %, using the psutil module''' + + stats = {} + process_handler = psutil.Process(pid) + stats['cpu_percent'] = process_handler.cpu_percent(interval=0.1) + stats['memory_percent'] = process_handler.memory_percent() + + return stats + +def multi_pid_process_stats(pids): + stats = {'cpu_percent': 0, 'memory_percent': 0} + for pid in pids: + stats = sum_dicts(stats, stats_per_pid(pid)) + return stats + +def graphite_printer(stats, graphite_scheme): + now = time.time() + for stat in stats: + print "%s.%s %s %d" % (graphite_scheme, stat, stats[stat], now) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-n', '--process_name', required=True) + parser.add_argument('-w', '--cpu_warning_pct', required=True) + parser.add_argument('-c', '--cpu_critical_pct', required=True) + parser.add_argument('-W', '--memory_warning_pct', required=True) + parser.add_argument('-C', '--memory_critical_pct', required=True) + parser.add_argument('-s', '--scheme', required=True) + parser.add_argument('-z', '--criticality', default='critical') + args = parser.parse_args() + + CRITICALITY = args.criticality + + pids = find_pids_from_name(args.process_name) + + if not pids: + print 'Cannot find pids for this process. Enter a valid process name.' + switch_on_criticality() + + total_process_stats = multi_pid_process_stats(pids) + graphite_printer(total_process_stats, args.scheme) + + if total_process_stats['cpu_percent'] > float(args.cpu_critical_pct) or \ + total_process_stats['memory_percent'] > float(args.memory_critical_pct): + print 'CPU Usage and/or memory usage at critical levels!!!' + switch_on_criticality() + + if total_process_stats['cpu_percent'] > float(args.cpu_warning_pct) or \ + total_process_stats['memory_percent'] > float(args.memory_warning_pct): + print 'Warning: CPU Usage and/or memory usage exceeding normal levels!' + sys.exit(STATE_WARNING) + + sys.exit(STATE_OK) + +if __name__ == "__main__": + main() + diff --git a/roles/sensu-client/files/plugins/percona-cluster-size.rb b/roles/sensu-client/files/plugins/percona-cluster-size.rb new file mode 100755 index 0000000..c74e80f --- /dev/null +++ b/roles/sensu-client/files/plugins/percona-cluster-size.rb @@ -0,0 +1,68 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# Percona Cluster Size Plugin +# === +# +# This plugin checks the number of servers in the Percona cluster and warns you according to specified limits +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/check/cli' + +class CheckPerconaClusterSize < Sensu::Plugin::Check::CLI + + option :user, + :description => "MySQL User", + :short => '-u USER', + :long => '--user USER', + :default => 'root' + + option :password, + :description => "MySQL Password", + :short => '-p PASS', + :long => '--password PASS' + + option :hostname, + :description => "Hostname to login to", + :short => '-h HOST', + :long => '--hostname HOST', + :default => 'localhost' + + option :min_expected, + :description => "Minimum number of servers expected in the cluster", + :short => '-e NUMBER', + :long => '--expected NUMBER', + :default => 2 + + option :defaults_file, + :description => "mysql defaults file", + :short => '-d DEFAULTS_FILE', + :long => '--defaults-file DEFAULTS_FILE' + + option :criticality, + :description => "Set sensu alert level, default is critical", + :short => '-z CRITICALITY', + :long => '--criticality CRITICALITY', + :default => 'critical' + + def switch_on_criticality(msg) + if config[:criticality] == 'warning' + warning msg + else + critical msg + end + end + + def run + if config[:defaults_file] + db_cluster_size = `mysql --defaults-file=#{config[:defaults_file]} -e "SHOW STATUS WHERE Variable_name like 'wsrep_cluster_size' AND Value >= #{config[:min_expected]};" | grep 'wsrep_cluster_size' | awk '{print $2}'` + else + db_cluster_size = `mysql -u #{config[:user]} -p#{config[:password]} -h #{config[:hostname]} -e "SHOW STATUS WHERE Variable_name like 'wsrep_cluster_size' AND Value >= #{config[:min_expected]};" | grep 'wsrep_cluster_size' | awk '{print $2}'` + end + + ok "Expected to find #{config[:min_expected]} or more nodes and found #{db_cluster_size}" if db_cluster_size.to_i >= config[:min_expected].to_i + switch_on_criticality("Expected to find #{config[:min_expected]} or more nodes, found #{db_cluster_size}") if db_cluster_size.to_i < config[:min_expected].to_i + end +end diff --git a/roles/sensu-client/files/plugins/vmstat-metrics.rb b/roles/sensu-client/files/plugins/vmstat-metrics.rb new file mode 100755 index 0000000..1b8aacf --- /dev/null +++ b/roles/sensu-client/files/plugins/vmstat-metrics.rb @@ -0,0 +1,79 @@ +#!/usr/bin/env /opt/sensu/embedded/bin/ruby +# +# System VMStat Plugin +# === +# +# This plugin uses vmstat to collect basic system metrics, produces +# Graphite formated output. +# +# Copyright 2011 Sonian, Inc +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# +# rubocop:disable HandleExceptions + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-plugin/metric/cli' +require 'socket' + +class VMStat < Sensu::Plugin::Metric::CLI::Graphite + + option :scheme, + :description => "Metric naming scheme, text to prepend to .$parent.$child", + :long => "--scheme SCHEME", + :default => "#{Socket.gethostname}.vmstat" + + def convert_integers(values) + values.each_with_index do |value, index| + begin + converted = Integer(value) + values[index] = converted + rescue ArgumentError + end + end + values + end + + def run + result = convert_integers(`vmstat 1 2|tail -n1`.split(" ")) + timestamp = Time.now.to_i + metrics = { + :procs => { + :waiting => result[0], + :uninterruptible => result[1] + }, + :memory => { + :swap_used => result[2], + :free => result[3], + :buffers => result[4], + :cache => result[5] + }, + :swap => { + :in => result[6], + :out => result[7] + }, + :io => { + :received => result[8], + :sent => result[9] + }, + :system => { + :interrupts_per_second => result[10], + :context_switches_per_second => result[11] + }, + :cpu => { + :user => result[12], + :system => result[13], + :idle => result[14], + :waiting => result[15] + } + } + metrics.each do |parent, children| + children.each do |child, value| + output [config[:scheme], parent, child].join("."), value, timestamp + end + end + exit + end + +end diff --git a/roles/sensu-client/meta/main.yml b/roles/sensu-client/meta/main.yml new file mode 100644 index 0000000..cc21760 --- /dev/null +++ b/roles/sensu-client/meta/main.yml @@ -0,0 +1,8 @@ +--- +dependencies: + - role: sensu-common + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.sensu_checks.repo }} trusty main' + key_url: '{{ apt_repos.sensu_checks.key_url }}' + when: sensu.client.plugins_from == "apt" diff --git a/roles/sensu-client/tasks/checks.yml b/roles/sensu-client/tasks/checks.yml new file mode 100644 index 0000000..322a453 --- /dev/null +++ b/roles/sensu-client/tasks/checks.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec check + sensu_check_dict: name="check-serverspec" check="{{ sensu_checks.sensu_client.check_serverspec }}" + notify: restart sensu-client + when: sensu.client.serverspec.enabled|bool + diff --git a/roles/sensu-client/tasks/main.yml b/roles/sensu-client/tasks/main.yml new file mode 100644 index 0000000..72d8be1 --- /dev/null +++ b/roles/sensu-client/tasks/main.yml @@ -0,0 +1,99 @@ +--- +- name: sensu config directories + file: dest={{ item }} mode=0755 + state=directory + owner=sensu + with_items: + - /etc/sensu/ssl + - /etc/sensu/conf.d/checks + +- name: create cert + template: src=etc/sensu/ssl/cert.pem + dest=/etc/sensu/ssl/cert.pem + owner=sensu + mode=0660 + notify: + - restart sensu-client + +- name: create key + template: src=etc/sensu/ssl/key.pem + dest=/etc/sensu/ssl/key.pem + owner=sensu + mode=0660 + notify: + - restart sensu-client + +- name: configure sensu + template: src={{ item }} + dest=/{{ item }} mode=0644 + with_items: + - etc/sensu/conf.d/client.json + - etc/sensu/conf.d/rabbitmq.json + notify: + - restart sensu-client + +- name: create sensu plugins dir + file: + dest: "{{ sensu.client.plugin_path }}" + state: directory + owner: root + mode: 0755 + +- name: install ursula sensu plugins + apt: + pkg: "ursula-monitoring-sensu={{ sensu.client.ursula_sensu_plugins.version }}" + when: sensu.client.plugins_from == "apt" + notify: + - restart sensu-client + +- name: Copy each file over that matches the given pattern + copy: + src: "{{ item }}" + dest: "{{ sensu.client.plugin_path }}" + owner: "root" + mode: 0600 + when: sensu.client.plugins_from == "file" + with_fileglob: + - "plugins/*" + notify: + - restart sensu-client + +- name: specify plugins path + lineinfile: dest=/etc/default/sensu regexp=^PLUGINS_DIR + line=PLUGINS_DIR="{{ sensu.client.plugin_path }}" + notify: restart sensu-client + +- name: use embedded ruby + lineinfile: dest=/etc/default/sensu regexp=^EMBEDDED_RUBY + line=EMBEDDED_RUBY=true + notify: restart sensu-client + +- name: install gems into sensu embedded ruby + gem: + name: "{{ item }}" + executable: /opt/sensu/embedded/bin/gem + user_install: no + with_items: + - rest-client + - http-cookie + - domain_name + register: result + until: result|succeeded + retries: 5 + notify: + - restart sensu-client + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec + +- meta: flush_handlers + +- name: start sensu-client + service: name=sensu-client state=started enabled=yes diff --git a/roles/sensu-client/tasks/metrics.yml b/roles/sensu-client/tasks/metrics.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/sensu-client/tasks/metrics.yml @@ -0,0 +1 @@ +--- diff --git a/roles/sensu-client/tasks/serverspec.yml b/roles/sensu-client/tasks/serverspec.yml new file mode 100644 index 0000000..0b6dd2a --- /dev/null +++ b/roles/sensu-client/tasks/serverspec.yml @@ -0,0 +1,43 @@ +--- +- name: install version-locked gems + gem: name="{{ item.name }}" version="{{ item.version | default(omit) }}" + executable=/opt/sensu/embedded/bin/gem + user_install=no + with_items: "{{ serverspec.version_locked_gems }}" + register: result + until: result|succeeded + retries: 5 + notify: restart sensu-client + +- name: install serverspec gem + gem: name=serverspec version="{{ serverspec.version }}" + executable=/opt/sensu/embedded/bin/gem + user_install=no + register: result + until: result|succeeded + retries: 5 + notify: restart sensu-client + +- name: install serverspec sensu plugin + gem: name=sensu-plugins-serverspec version=0.0.2 + executable=/opt/sensu/embedded/bin/gem + user_install=no + register: result + until: result|succeeded + retries: 5 + notify: restart sensu-client + +- name: install serverspec-extended-types gem + gem: name=serverspec-extended-types version=0.0.3 + executable=/opt/sensu/embedded/bin/gem + user_install=no + register: result + until: result|succeeded + retries: 5 + notify: restart sensu-client + +- name: serverspec checks for sensu-client role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/sensu-client/templates/etc/sensu/conf.d/client.json b/roles/sensu-client/templates/etc/sensu/conf.d/client.json new file mode 100644 index 0000000..5781e3a --- /dev/null +++ b/roles/sensu-client/templates/etc/sensu/conf.d/client.json @@ -0,0 +1,23 @@ +{% macro local_external_address() -%} +{% for address in ansible_all_ipv4_addresses %} +{% if loop.last %} +{{- address -}} +{%- endif -%} +{% endfor -%} +{% endmacro -%} + +{ + "client": { + "name": "{{ sensu.client_name|default(ansible_hostname ~ '.' ~ ansible_domain ~ '-' ~ site_abrv|default('unknown_site')) }}", + "address": "{{ local_external_address() }}", + "subscriptions": [ "all" ], + "keepalive": { + "thresholds": { + "warning": 60, + "critical": 180 + }, + "handlers": ["default"], + "refresh": 1800 + } + } +} diff --git a/roles/sensu-client/templates/etc/sensu/conf.d/rabbitmq.json b/roles/sensu-client/templates/etc/sensu/conf.d/rabbitmq.json new file mode 100644 index 0000000..3a95f9c --- /dev/null +++ b/roles/sensu-client/templates/etc/sensu/conf.d/rabbitmq.json @@ -0,0 +1,13 @@ +{ + "rabbitmq": { + "ssl": { + "cert_chain_file": "/etc/sensu/ssl/cert.pem", + "private_key_file": "/etc/sensu/ssl/key.pem" + }, + "host": "{{ sensu.client.rabbitmq.host }}", + "port": 5671, + "vhost": "{{ sensu.client.rabbitmq.vhost }}", + "user": "{{ sensu.client.rabbitmq.username }}", + "password": "{{ sensu.client.rabbitmq.password }}" + } +} diff --git a/roles/sensu-client/templates/etc/sensu/ssl/cert.pem b/roles/sensu-client/templates/etc/sensu/ssl/cert.pem new file mode 100644 index 0000000..97f72dd --- /dev/null +++ b/roles/sensu-client/templates/etc/sensu/ssl/cert.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ sensu.client.rabbitmq.ssl.cert }} diff --git a/roles/sensu-client/templates/etc/sensu/ssl/key.pem b/roles/sensu-client/templates/etc/sensu/ssl/key.pem new file mode 100644 index 0000000..e7f64c8 --- /dev/null +++ b/roles/sensu-client/templates/etc/sensu/ssl/key.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ sensu.client.rabbitmq.ssl.key }} diff --git a/roles/sensu-client/templates/serverspec/sensu-client_spec.rb b/roles/sensu-client/templates/serverspec/sensu-client_spec.rb new file mode 100644 index 0000000..0af4b8c --- /dev/null +++ b/roles/sensu-client/templates/serverspec/sensu-client_spec.rb @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('ursula-monitoring-sensu') do + it { should be_installed } +end + +describe service('sensu-client') do + it { should be_enabled } +end diff --git a/roles/sensu-common/defaults/main.yml b/roles/sensu-common/defaults/main.yml new file mode 100644 index 0000000..1c1ff0d --- /dev/null +++ b/roles/sensu-common/defaults/main.yml @@ -0,0 +1,218 @@ +--- +sensu: + enabled: True + version: 0.25.7-1 + api: + bind_ip: 127.0.0.1 + port: 4567 + username: admin + password: admin + firewall: [] + ssl_enabled: False + dashboard: + version: 0.17.1* + host: 127.0.0.1 + port: 3000 + refresh: 10 + firewall: [] + datacenters: + - hostname: localhost + password: admin + sensu_plugins: + - name: sensu-plugins-uchiwa + version: 0.0.3 + client: + enabled: true + enable_checks: true + enable_metrics: true + plugins_from: file + plugin_path: /etc/sensu/plugins + rabbitmq: + host: 172.16.0.15 + username: rabbit + password: rabbit + vhost: /rabbit + hostgroup: sensu + ssl: + cert: | + -----BEGIN CERTIFICATE----- + MIIC3TCCAcWgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdTZW5z + dUNBMB4XDTE1MDEwNTIxMzYyM1oXDTIwMDEwNDIxMzYyM1owITEOMAwGA1UEAwwF + c2Vuc3UxDzANBgNVBAoMBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC + AQoCggEBAPOCFDzom4sBkFbpy7D6OZX2Re/mbbcpdz7zAYR27X8WE7ai4FCe/OlW + lViHBkUdHl8TigYBLpZOX+BBu8UMFThotaq10+E07JQygeZLF/Yg6ihoC2qq1n2w + Nan2CkhTdKwYfzeMM14Kdc/cu+1TG64kZQQ6j/TCSho9n1BinxbjJi+Gnp34gDLY + c3pI/dpxflPNo9PiPD3XS0WUk1gJm1anNOJLdipGEZYW1fTXL3XciLyrEFEV3am1 + pUFyD7l+ygHtUfPGJFc+i7iuHqPBU/HvSeIRd/5ynLIqJmNHzmtrUiRiie3TVIjo + 6PHTLI9l/YS20TGuNQTWnYEFZp0WGtUCAwEAAaMvMC0wCQYDVR0TBAIwADALBgNV + HQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQEFBQADggEB + AGmzryodqCedFin/9ytQerlAf4To3B1j+F/FSoQfnm7smRV3dz7FRr7vlNhX4Spg + p2AQAPV+PBobtvSToDZZGj5Puwog/lfX9PIcAI0JoQOi+ylXjMybJtGSC513fVEj + 4h5isvAtLEtiEB0czVhF5REXwzFIMTSwTDuCAxgfvaZqkn6J7R5NChcQUpsHxeP+ + TAuDsm+7LJC8K7mAYS4sCi1VR1Wmq7rF3Wqoe2PdzYg9jKzWDhoBrL+cHZ/CifoY + GH4uMZWaTZgJuDNCseRjRwrONFcJqdFm0IAFCt/8r8dlgLL8G3Uu2ABJt+lZvvHf + NaVOvEPwUrNq9IJtmam/Wlw= + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA84IUPOibiwGQVunLsPo5lfZF7+Zttyl3PvMBhHbtfxYTtqLg + UJ786VaVWIcGRR0eXxOKBgEulk5f4EG7xQwVOGi1qrXT4TTslDKB5ksX9iDqKGgL + aqrWfbA1qfYKSFN0rBh/N4wzXgp1z9y77VMbriRlBDqP9MJKGj2fUGKfFuMmL4ae + nfiAMthzekj92nF+U82j0+I8PddLRZSTWAmbVqc04kt2KkYRlhbV9NcvddyIvKsQ + URXdqbWlQXIPuX7KAe1R88YkVz6LuK4eo8FT8e9J4hF3/nKcsiomY0fOa2tSJGKJ + 7dNUiOjo8dMsj2X9hLbRMa41BNadgQVmnRYa1QIDAQABAoIBAD5Ju9uItKDuGXnh + /BSdLTXK9UDUdKQxmysjBfRYZ4/mmcxSBYcZ5B/b4f/vKg9rz2UKnnfCvaP9EFFQ + /zL8UEFJp46vTC+DVjDiao/6DXsMGFmVo7X5VTx5YoDDSwYNMv8ClSgkWM9pZZFy + KTAf3ijBboWSZVh6inID6UiqsX8sw8ZjHdexT2/b29/Q64nLKTrG472F0mACMCqT + TTNUSLCH78blAzySyZaK9miL7ehvxMTv2MfNwspg7vVV4PRtqlM0+bnYsz2aLkQ1 + gMNzVTuyux1lg0mj+xst9aKpQ4ZGmfRT7MPr9USa65k2vakaik6DaAQNkS+C8l74 + +JcvyIECgYEA/9ILhig/vvHxch7D8eq9sNXld0NNhZd9PKbb05t8K0zeCXTXVM4d + DQ1SLErpae6yFcTGWJWdSua6mClURGm2W10JH+QLUNq51Drl++Kj1scUSADVpjYZ + JkmvvRjGlLX5bzJ3mBGbmdKyGdMeppZRWk3+jhGuEMTo0z4URaDllE0CgYEA863S + gJBd7/bhVCFbNuXEATMjoXjLsKK9QPDjBaL/PEnKoYT1aTsVUN43Hkd86c5NXszR + ///IzA49TluKHvXjscLk2mzInUU3YyjwzWgN5vp6ZEh0bubXWABgQVVFsmqV3Gzp + KBrBg9mzWtC6KQGwtt3RRInFycTYE6d+uf8OBKkCgYEAv8PNXbC4MEOfMWO8kJDd + xzxf+ana58SqXZfa+2H3j2Hco4wRioHDJzCQI6G4HO+QUV8jK+5jbW1N/Jgmke4I + g65XE6/Xh7GeUWghVFMNbAfpvRvbC/BLo/bipMZ4vas3otJa8gRo9sMJPPCUbl4J + 9761jfvU1r80pH1JpvQ4hJUCgYAS+zpHe3dlxtRcilChoRo4gbRH0rIDK1oUoe79 + NGovVwwqssGvFcQeTYD7odPwHnrWZJwDFfidNIq//M9wg/TdlvHetdSWs5qR5dGE + HpPepyo7f41aSi8CEt1smWjZcgYEapNq5VKno6Cd9V5a7V/HjtXLQfQfOG8gcM6l + TGHaKQKBgFRnRUE3tBeEKvM1touGxNjURea4aFVzFk4b8lL/OQJnkWhMmhrBKCHw + WRiRItwHQxTunlN0gK4qty2rczWIv/yzWALHJAOFLIyX0ynO4xY7x/3H9df9IuDe + Gs1548GcEmUG0CWujFXsEJkjjBxT4/fabx4NOJjg9Ij1Mi3LYmB2 + -----END RSA PRIVATE KEY----- + ursula_sensu_plugins: + version: 2.2.5-1 + serverspec: + enabled: True + server: + instances: 0 + rabbitmq: + host: 172.16.0.15 + username: rabbit + password: rabbit + vhost: /rabbit + hostgroup: sensu + ssl: + cert: | + -----BEGIN CERTIFICATE----- + MIIC3TCCAcWgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdTZW5z + dUNBMB4XDTE1MDEwNTIxMzYyM1oXDTIwMDEwNDIxMzYyM1owITEOMAwGA1UEAwwF + c2Vuc3UxDzANBgNVBAoMBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC + AQoCggEBAPOCFDzom4sBkFbpy7D6OZX2Re/mbbcpdz7zAYR27X8WE7ai4FCe/OlW + lViHBkUdHl8TigYBLpZOX+BBu8UMFThotaq10+E07JQygeZLF/Yg6ihoC2qq1n2w + Nan2CkhTdKwYfzeMM14Kdc/cu+1TG64kZQQ6j/TCSho9n1BinxbjJi+Gnp34gDLY + c3pI/dpxflPNo9PiPD3XS0WUk1gJm1anNOJLdipGEZYW1fTXL3XciLyrEFEV3am1 + pUFyD7l+ygHtUfPGJFc+i7iuHqPBU/HvSeIRd/5ynLIqJmNHzmtrUiRiie3TVIjo + 6PHTLI9l/YS20TGuNQTWnYEFZp0WGtUCAwEAAaMvMC0wCQYDVR0TBAIwADALBgNV + HQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQEFBQADggEB + AGmzryodqCedFin/9ytQerlAf4To3B1j+F/FSoQfnm7smRV3dz7FRr7vlNhX4Spg + p2AQAPV+PBobtvSToDZZGj5Puwog/lfX9PIcAI0JoQOi+ylXjMybJtGSC513fVEj + 4h5isvAtLEtiEB0czVhF5REXwzFIMTSwTDuCAxgfvaZqkn6J7R5NChcQUpsHxeP+ + TAuDsm+7LJC8K7mAYS4sCi1VR1Wmq7rF3Wqoe2PdzYg9jKzWDhoBrL+cHZ/CifoY + GH4uMZWaTZgJuDNCseRjRwrONFcJqdFm0IAFCt/8r8dlgLL8G3Uu2ABJt+lZvvHf + NaVOvEPwUrNq9IJtmam/Wlw= + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA84IUPOibiwGQVunLsPo5lfZF7+Zttyl3PvMBhHbtfxYTtqLg + UJ786VaVWIcGRR0eXxOKBgEulk5f4EG7xQwVOGi1qrXT4TTslDKB5ksX9iDqKGgL + aqrWfbA1qfYKSFN0rBh/N4wzXgp1z9y77VMbriRlBDqP9MJKGj2fUGKfFuMmL4ae + nfiAMthzekj92nF+U82j0+I8PddLRZSTWAmbVqc04kt2KkYRlhbV9NcvddyIvKsQ + URXdqbWlQXIPuX7KAe1R88YkVz6LuK4eo8FT8e9J4hF3/nKcsiomY0fOa2tSJGKJ + 7dNUiOjo8dMsj2X9hLbRMa41BNadgQVmnRYa1QIDAQABAoIBAD5Ju9uItKDuGXnh + /BSdLTXK9UDUdKQxmysjBfRYZ4/mmcxSBYcZ5B/b4f/vKg9rz2UKnnfCvaP9EFFQ + /zL8UEFJp46vTC+DVjDiao/6DXsMGFmVo7X5VTx5YoDDSwYNMv8ClSgkWM9pZZFy + KTAf3ijBboWSZVh6inID6UiqsX8sw8ZjHdexT2/b29/Q64nLKTrG472F0mACMCqT + TTNUSLCH78blAzySyZaK9miL7ehvxMTv2MfNwspg7vVV4PRtqlM0+bnYsz2aLkQ1 + gMNzVTuyux1lg0mj+xst9aKpQ4ZGmfRT7MPr9USa65k2vakaik6DaAQNkS+C8l74 + +JcvyIECgYEA/9ILhig/vvHxch7D8eq9sNXld0NNhZd9PKbb05t8K0zeCXTXVM4d + DQ1SLErpae6yFcTGWJWdSua6mClURGm2W10JH+QLUNq51Drl++Kj1scUSADVpjYZ + JkmvvRjGlLX5bzJ3mBGbmdKyGdMeppZRWk3+jhGuEMTo0z4URaDllE0CgYEA863S + gJBd7/bhVCFbNuXEATMjoXjLsKK9QPDjBaL/PEnKoYT1aTsVUN43Hkd86c5NXszR + ///IzA49TluKHvXjscLk2mzInUU3YyjwzWgN5vp6ZEh0bubXWABgQVVFsmqV3Gzp + KBrBg9mzWtC6KQGwtt3RRInFycTYE6d+uf8OBKkCgYEAv8PNXbC4MEOfMWO8kJDd + xzxf+ana58SqXZfa+2H3j2Hco4wRioHDJzCQI6G4HO+QUV8jK+5jbW1N/Jgmke4I + g65XE6/Xh7GeUWghVFMNbAfpvRvbC/BLo/bipMZ4vas3otJa8gRo9sMJPPCUbl4J + 9761jfvU1r80pH1JpvQ4hJUCgYAS+zpHe3dlxtRcilChoRo4gbRH0rIDK1oUoe79 + NGovVwwqssGvFcQeTYD7odPwHnrWZJwDFfidNIq//M9wg/TdlvHetdSWs5qR5dGE + HpPepyo7f41aSi8CEt1smWjZcgYEapNq5VKno6Cd9V5a7V/HjtXLQfQfOG8gcM6l + TGHaKQKBgFRnRUE3tBeEKvM1touGxNjURea4aFVzFk4b8lL/OQJnkWhMmhrBKCHw + WRiRItwHQxTunlN0gK4qty2rczWIv/yzWALHJAOFLIyX0ynO4xY7x/3H9df9IuDe + Gs1548GcEmUG0CWujFXsEJkjjBxT4/fabx4NOJjg9Ij1Mi3LYmB2 + -----END RSA PRIVATE KEY----- + handlers: + hijack: + default: + - flapjack_http + pagerduty: [] + flapjack: [] + flapjack_http: [] + sensu_api: [] + metrics: + - graphite + graphite: + enabled: True + host: 127.0.0.1 + port: 2003 + pagerduty: + enabled: False + api_key_default: notarealkey + api_keys: + - name: support + api_key: notarealkey + remedy: + enabled: False + username: BluemixMonitor + password: notarealpassword + service: bluebox # Please change to bluebox_boxpanel for Box Panel monitoring + baseurl: https://chinabluemix.itsm.unisysedge.cn/arsys/WSDL/public/golddevar01 + wsdl_create: Uni_HPDIncidentInterface_IBM_Create_Incident + wsdl_query: Uni_HPDIncidentInterface_IBM_Query_Incident + wsdl_update: Uni_HPDIncidentInterface_IBM_Update_Incident + flapjack: + enabled: False + host: 127.0.0.1 + # flapjack dedicated redis instance + port: 6380 + db: 0 + flapjack_http: + enabled: True + uri: http://localhost:3090 + ttl: -1 # -1 disables, otherwise httpbroker re-sends last received state until ttl expires + sensu_api: + uri: http://127.0.0.1:4567 + user: admin + pass: admin + sensu_api: + enabled: True + host: 127.0.0.1 + port: 4567 + user: admin + pass: admin + datacenter: "{{ site_abrv|default('unknown') }}" + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/sensu/sensu-client.log + document_type: json + fields: + tags: sensu,sensu-client + - paths: + - /var/log/sensu/sensu-server.log + document_type: json + fields: + tags: sensu,sensu-server + - paths: + - /var/log/sensu/sensu-api.log + document_type: json + fields: + tags: sensu,sensu-api + - paths: + - /var/log/uchiwa.log + - /var/log/uchiwa.err + document_type: json + fields: + tags: sensu,uchiwa + logging: + forwarder: filebeat diff --git a/roles/sensu-common/handlers/main.yml b/roles/sensu-common/handlers/main.yml new file mode 100644 index 0000000..5b6b165 --- /dev/null +++ b/roles/sensu-common/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart sensu-client + service: + name: sensu-client + state: restarted diff --git a/roles/sensu-common/meta/main.yml b/roles/sensu-common/meta/main.yml new file mode 100644 index 0000000..07124c6 --- /dev/null +++ b/roles/sensu-common/meta/main.yml @@ -0,0 +1,11 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: "deb {{ apt_repos.sensu.repo }} {{ apt_repos.sensu.distribution|default('sensu') }} main" + key_url: '{{ apt_repos.sensu.key_url }}' + - role: logging-config + service: sensu + logdata: "{{ sensu.logs }}" + forward_type: "{{ sensu.logging.forwarder }}" + when: logging.enabled|default("True")|bool diff --git a/roles/sensu-common/tasks/checks.yml b/roles/sensu-common/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/sensu-common/tasks/main.yml b/roles/sensu-common/tasks/main.yml new file mode 100644 index 0000000..a315938 --- /dev/null +++ b/roles/sensu-common/tasks/main.yml @@ -0,0 +1,30 @@ +--- +- name: install sensu + apt: + name: "sensu={{ sensu.version }}" + state: present + force: yes + +- name: sensu sudoers + template: src=etc/sudoers.d/sensu + dest=/etc/sudoers.d/sensu + owner=root + group=root + mode=0440 + +- name: sensu service script + template: src=etc/init.d/sensu-service + dest=/etc/init.d/sensu-service + owner=root mode=0755 + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/sensu-common/tasks/metrics.yml b/roles/sensu-common/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/sensu-common/tasks/serverspec.yml b/roles/sensu-common/tasks/serverspec.yml new file mode 100644 index 0000000..04a07b0 --- /dev/null +++ b/roles/sensu-common/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for sensu-common role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/sensu-common/templates/etc/init.d/sensu-service b/roles/sensu-common/templates/etc/init.d/sensu-service new file mode 100644 index 0000000..3d689fa --- /dev/null +++ b/roles/sensu-common/templates/etc/init.d/sensu-service @@ -0,0 +1,349 @@ +# {{ ansible_managed }} + +#!/bin/bash + +# chkconfig: 345 90 90 +# description: Sensu monitoring framework service + +### BEGIN INIT INFO +# Provides: sensu-service +# Required-Start: $remote_fs $network +# Required-Stop: $remote_fs $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: Sensu monitoring framework service script +### END INIT INFO + +sensu_service=sensu-$1 + +EMBEDDED_RUBY=true +CONFIG_FILE=/etc/sensu/config.json +CONFIG_DIR=/etc/sensu/conf.d +EXTENSION_DIR=/etc/sensu/extensions +PLUGINS_DIR=/etc/sensu/plugins +HANDLERS_DIR=/etc/sensu/handlers +LOG_DIR=/var/log/sensu +LOG_LEVEL=info +PID_DIR=/var/run/sensu +USER=sensu +SERVICE_MAX_WAIT=10 + +if [ -f /etc/default/sensu ]; then + . /etc/default/sensu +fi + +if [ -f /etc/default/$sensu_service ]; then + . /etc/default/$sensu_service +fi + +system=unknown +if [ -f /etc/redhat-release ]; then + system=redhat +elif [ -f /etc/system-release ]; then + system=redhat +elif [ -f /etc/debian_version ]; then + system=debian +elif [ -f /etc/SuSE-release ]; then + system=suse +elif [ -f /etc/gentoo-release ]; then + system=gentoo +elif [ -f /etc/arch-release ]; then + system=arch +elif [ -f /etc/slackware-version ]; then + system=slackware +elif [ -f /etc/lfs-release ]; then + system=lfs +fi + +## +## Set platform specific bits here. +## The original platform for this script was redhat so the scripts are partial +## to redhat style. Eventually we may want to be more LSB compliant +## such as what Debian platforms already implement instead. +## +## Each platform must implement at least the following functions: +## +## start_daemon $user $pidfile $executable "arguments" +## killproc -p $pid $sensu_service +## log_daemon_msg $@ +## log_action_msg $@ +## log_success_msg $@ +## log_failure_msg $@ +## echo_ok +## echo_fail +## +if [ "$system" = "redhat" ]; then + ## source platform specific external scripts + . /etc/init.d/functions + [ -r /etc/sysconfig/$sensu_service ] && . /etc/sysconfig/$sensu_service + + ## set or override platform specific variables + lockfile=${LOCKFILE-/var/lock/subsys/$sensu_service} + + ## set or override platform specific functions + start_daemon() { + daemon --user $1 --pidfile $2 "$3 $4" + } + log_daemon_msg() { + echo -n $"$1" + } + echo_ok() { + echo_success; echo + } + echo_fail() { + echo_failure; echo + } + log_success_msg() { + success $"$@" + } + log_failure_msg() { + failure $"$@" + echo $"$@" + } + log_action_msg() { + echo $@ + } +fi + +if [ "$system" = "debian" ]; then + ## source platform specific external scripts + . /lib/lsb/init-functions + [ -r /etc/default/$sensu_service ] && . /etc/default/$sensu_service + + ## set or override platform specific variables + lockfile=${LOCKFILE-/var/lock/$sensu_service} + + ## set or override platform specific functions + start_daemon() { + start-stop-daemon --start --chuid $1 --pidfile $2 --exec $3 -- $4 + } + echo_ok() { + log_end_msg 0 + } + echo_fail() { + log_end_msg 1 + } +fi + +if [ "$system" = "suse" ]; then + ## source platform specific external scripts + . /lib/lsb/init-functions + [ -r /etc/default/$sensu_service ] && . /etc/default/$sensu_service + + ## set or override platform specific variables + lockfile=${LOCKFILE-/var/lock/subsys/$sensu_service} + + ## set or override platform specific functions + start_daemon() { + startproc -s -u $1 -p $2 $3 $4 + } + echo_ok() { + log_success_msg + } + echo_fail() { + log_failure_msg + } + log_daemon_msg() { + echo -n $"$1" + } + log_action_msg() { + echo $@ + } +fi + +cd /opt/sensu + +exec=/opt/sensu/bin/$sensu_service + +pidfile=$PID_DIR/$sensu_service.pid +logfile=$LOG_DIR/$sensu_service.log + +options="-b -c $CONFIG_FILE -d $CONFIG_DIR -e $EXTENSION_DIR -p $pidfile -l $logfile -L $LOG_LEVEL $OPTIONS" + +ensure_dir() { + if [ ! -d $1 ]; then + mkdir -p $1 + chown -R $2 $1 + chmod 755 $1 + fi +} + +set_sensu_paths() { + if [ "x$EMBEDDED_RUBY" = "xtrue" ]; then + export PATH=/opt/sensu/embedded/bin:$PATH:$PLUGINS_DIR:$HANDLERS_DIR + export GEM_PATH=/opt/sensu/embedded/lib/ruby/gems/2.0.0:$GEM_PATH + else + export PATH=$PATH:$PLUGINS_DIR:$HANDLERS_DIR + fi +} + +pid_pgrep() { + pgrep -f -P 1 -u $USER " $exec " +} + +wait_for_stop() { + stopped=1; attempts=0 + + while [ $attempts -lt $SERVICE_MAX_WAIT ]; do + attempts=$((attempts + 1)) + + status &> /dev/null + + if [ $? -eq 0 ]; then + # still running + sleep 1 + else + rm -f $pidfile + rm -f $lockfile + stopped=0 + break + fi + done + + return $stopped +} + +start() { + log_daemon_msg "Starting $sensu_service" + + [ -x $exec ] || exit 5 + + status &> /dev/null + + if [ $? -eq 0 ]; then + log_action_msg "$sensu_service is already running." + echo_ok + exit 0 + fi + + set_sensu_paths + ensure_dir $PID_DIR $USER + ensure_dir $LOG_DIR $USER + + start_daemon $USER $pidfile $exec "$options" + + retval=$? + sleep 1 + + # make sure it's still running, some errors occur only after startup. + status &> /dev/null + if [ $? -ne 0 ]; then + echo_fail + exit 1 + fi + + if [ $retval -eq 0 ]; then + touch $lockfile + fi + echo_ok + + return $retval +} + +stop() { + log_daemon_msg "Stopping $sensu_service" + + # try pgrep + pid=$(pid_pgrep) + + if [ ! -n "$pid" ]; then + # try the pid file + if [ -f "$pidfile" ] ; then + read pid < "$pidfile" + fi + fi + + if [ -n "$pid" ]; then + kill $pid + + retval=$? + + if [ $retval -eq 0 ]; then + wait_for_stop + + retval=$? + + if [ $retval -eq 0 ]; then + echo_ok + else + log_failure_msg "Timed out waiting for $sensu_service to stop" + echo_fail + fi + else + echo_fail + fi + + return $retval + else + log_action_msg "$sensu_service is stopped" + return 3 + fi +} + +status() { + local pid + + # try pgrep + pid=$(pid_pgrep) + + if [ -n "$pid" ]; then + log_action_msg "$sensu_service (pid $pid) is running" + return 0 + fi + + # try the pid file + if [ -f "$pidfile" ] ; then + read pid < "$pidfile" + + kill -0 $pid + + if [ $? -eq 0 ]; then + log_action_msg "$sensu_service (pid $pid) is running" + return 0 + else + log_action_msg "$sensu_service dead but pid file exists" + return 1 + fi + fi + + # check for the lock file + if [ -f "$lockfile" ]; then + log_action_msg "$sensu_service dead but subsys locked" + return 2 + fi + + log_action_msg "$sensu_service is stopped" + return 3 +} + +restart() { + stop + + retval=$? + + if [ $retval -eq 0 ] || [ $retval -eq 3 ]; then + start + else + return $retval + fi +} + +case "$2" in + start) + start + ;; + stop) + stop + ;; + status) + status + ;; + restart) + restart + ;; + *) + echo "Usage: $0 {client|server|api} {start|stop|status|restart}" + exit 2 +esac + +exit $? diff --git a/roles/sensu-common/templates/etc/sudoers.d/sensu b/roles/sensu-common/templates/etc/sudoers.d/sensu new file mode 100644 index 0000000..578fca6 --- /dev/null +++ b/roles/sensu-common/templates/etc/sudoers.d/sensu @@ -0,0 +1,20 @@ +# {{ ansible_managed }} + +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-raid.sh +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-log.rb +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-for-large-files.sh +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-os-api.rb +sensu ALL= NOPASSWD: /opt/sensu/plugins/percona-cluster-size.rb +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-rabbitmq-cluster.rb +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-rabbitmq-queues.rb +sensu ALL= NOPASSWD: /opt/sensu/plugins/check_3ware_raid.py +sensu ALL= NOPASSWD: /opt/sensu/plugins/check_megaraid_sas.pl +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-swift-dispersion.sh +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-nova-services.sh +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-cinder-services.sh +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-neutron-agents.sh +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-ucarp-procs.sh +sensu ALL= NOPASSWD: /opt/sensu/plugins/nimble-check.py +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-glance-store.py +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-storcli.pl +sensu ALL= NOPASSWD: /opt/sensu/plugins/check-serverspec.rb diff --git a/roles/sensu-common/templates/serverspec/sensu-common_spec.rb b/roles/sensu-common/templates/serverspec/sensu-common_spec.rb new file mode 100644 index 0000000..ccc6c8d --- /dev/null +++ b/roles/sensu-common/templates/serverspec/sensu-common_spec.rb @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('sensu') do + it { should be_installed } +end diff --git a/roles/sensu-server/defaults/main.yml b/roles/sensu-server/defaults/main.yml new file mode 100644 index 0000000..0a1f0e6 --- /dev/null +++ b/roles/sensu-server/defaults/main.yml @@ -0,0 +1,3 @@ +--- +openid_proxy: + remote_locations: {} diff --git a/roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack.rb b/roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack.rb new file mode 100644 index 0000000..a31569f --- /dev/null +++ b/roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack.rb @@ -0,0 +1,98 @@ +# Sends events to Flapjack for notification routing. See http://flapjack.io/ +# +# This extension requires Flapjack >= 0.8.7 and Sensu >= 0.13.1 +# +# In order for Flapjack to keep its entities up to date, it is necssary to set +# metric to "true" for each check that is using the flapjack handler extension. +# +# Here is an example of what the Sensu configuration for flapjack should +# look like, assuming your Flapjack's redis service is running on the +# same machine as the Sensu server: +# +# { +# "flapjack": { +# "host": "localhost", +# "port": 6379, +# "db": "0" +# } +# } +# +# Copyright 2014 Jive Software and contributors. +# +# Released under the same terms as Sensu (the MIT license); see LICENSE for details. + +require 'sensu/redis' +require 'multi_json' + +module Sensu + module Extension + class Flapjack < Handler + def name + 'flapjack' + end + + def description + 'sends sensu events to the flapjack redis queue' + end + + def options + return @options if @options + @options = { + host: '127.0.0.1', + port: 6379, + channel: 'events', + db: 0 + } + if @settings[:flapjack].is_a?(Hash) + @options.merge!(@settings[:flapjack]) + end + @options + end + + def definition + { + type: 'extension', + name: name, + mutator: 'ruby_hash' + } + end + + def post_init + @redis = Sensu::Redis.connect(options) + @redis.on_error do |_error| + @logger.warn('Flapjack Redis instance not available on ' + options[:host]) + end + end + + def run(event) + client = event[:client] + check = event[:check] + tags = [] + tags.concat(client[:tags]) if client[:tags].is_a?(Array) + tags.concat(check[:tags]) if check[:tags].is_a?(Array) + tags << client[:environment] unless client[:environment].nil? + # #YELLOW + unless check[:subscribers].nil? || check[:subscribers].empty? # rubocop:disable UnlessElse + tags.concat(client[:subscriptions] - (client[:subscriptions] - check[:subscribers])) + else + tags.concat(client[:subscriptions]) + end + details = ['Address:' + client[:address]] + details << 'Tags:' + tags.join(',') + details << "Raw Output: #{check[:output]}" if check[:notification] + flapjack_event = { + entity: client[:name], + check: check[:name], + type: 'service', + state: Sensu::SEVERITIES[check[:status]] || 'unknown', + summary: check[:notification] || check[:output], + details: details.join(' '), + time: check[:executed], + tags: tags + } + @redis.lpush(options[:channel], MultiJson.dump(flapjack_event)) + yield 'sent an event to the flapjack redis queue', 0 + end + end + end +end diff --git a/roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack_http.rb b/roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack_http.rb new file mode 100755 index 0000000..a0798da --- /dev/null +++ b/roles/sensu-server/files/etc/sensu/extensions/handlers/flapjack_http.rb @@ -0,0 +1,352 @@ +# Sends events to Flapjack HTTP Broker for notification routing. See http://flapjack.io/ +# +# This extension requires Flapjack >= 0.8.7 and Sensu >= 0.20.0 +# +# In order for Flapjack to keep its entities up to date, it is necssary to set +# metric to "true" for each check that is using the flapjack handler extension. +# +# Here is an example of what the Sensu configuration for flapjack_http should +# look like, assuming your Flapjack's HTTP Broker is running on the +# same machine as the Sensu server: +# +# { +# "flapjack_http": { +# "uri": "http://sensu.example.com:3090" +# } +# } +# +# Copyright 2015 Blue Box, an IBM Company +# 12/11/2015 - Myles Steinhauser +# +# Released under the same terms as Sensu (the MIT license); see LICENSE for details. + +require 'faraday' +require 'multi_json' +require 'timeout' +require 'net/http/persistent' +require 'net/http' + +module Sensu + module Extension + class FlapjackHttp < Bridge + def name + 'flapjack_http' + end + + def description + 'sends sensu events to the flapjack http broker' + end + + def options + return @options if @options + @options = { + uri: 'http://127.0.0.1:3090', + sensu_api: { + uri: 'http://127.0.0.1:4567', + user: nil, + pass: nil + }, + ttl: 30, # seconds before event enters unknown state + # default: "http_proxy" environment variable + ## https://github.com/lostisland/faraday/blob/3579225fd18c770dd2dc5d020d74b72701e4e647/lib/faraday/options.rb#L216 + # proxy: { + # }, + ## https://github.com/lostisland/faraday/blob/3579225fd18c770dd2dc5d020d74b72701e4e647/lib/faraday/options.rb#L204-L205 + # ssl: { + # }, + service_owner: 'default', + default_send: true, + default_send_metric: false, + default_send_check: true, + occurrences: 1, + interval: 30, + refresh: 1800, + request: { + open_timeout: 3, + timeout: 3 + } + } + if @settings[:flapjack_http].is_a?(Hash) + @options.merge!(@settings[:flapjack_http]) + end + @logger.debug('flapjack_http -- merged settings: ' + MultiJson.dump(options)) + @options + end + + def definition + { + type: 'extension', + name: name, + mutator: 'ruby_hash' + } + end + + def post_init + @logger.debug('flapjack_http -- connecting with settings: ' + MultiJson.dump(options)) + begin + @flapjack = Faraday.new(:url => options[:uri], + :ssl => options[:ssl], + :proxy => options[:proxy], + :request => options[:request]) do |faraday| + # Use the net/http/persistent gem implementation + faraday.adapter :net_http_persistent + end + rescue Faraday::ClientError + @logger.warn('flapjack_http -- http broker not available on ' + options[:uri]) + else + @logger.info('flapjack_http -- connected to: ' + options[:uri]) + end + + @logger.info("flapjack_http -- connecting to sensu-api: #{options[:sensu_api][:user]}@#{options[:sensu_api][:uri]}") + begin + @sensu_api = Faraday.new(:url => options[:sensu_api][:uri]) do |faraday| + faraday.request :basic_auth, options[:sensu_api][:user], options[:sensu_api][:pass] + # Use the net/http/persistent gem implementation + faraday.adapter :net_http + end + rescue Faraday::ClientError + @logger.warn('flapjack_http -- sensu-api not available') + else + @logger.info('flapjack_http -- connected to sensu-api!') + end + end + + def _tag_service_owners(event) + tags = [] + unless (event[:client].has_key? :service_owner or event[:check].has_key? :service_owner) + tags << 'service_owner:' + options[:service_owner] + return tags + end + + if event[:client][:service_owner] + Array(event[:client][:service_owner]).each { |service_owner| + tags << 'service_owner:' + service_owner + } + end + + if event[:check][:service_owner] + Array(event[:check][:service_owner]).each { |service_owner| + tags << 'service_owner:' + service_owner + } + end + + return tags + end + + def _get_tags(event, client, check) + tags = [] + tags.concat(_tag_service_owners(event)) + tags.concat(client[:tags]) if client[:tags].is_a?(Array) + tags.concat(check[:tags]) if check[:tags].is_a?(Array) + tags << client[:environment] unless client[:environment].nil? + unless check[:subscribers].nil? || check[:subscribers].empty? # rubocop:disable UnlessElse + tags.concat(client[:subscriptions] - (client[:subscriptions] - check[:subscribers])) + else + tags.concat(client[:subscriptions]) + end + return tags + end + + def _get_details(event, client, check, tags) + details = ['Address:' + client[:address]] + details << 'Tags:' + tags.join(',') + details << "Raw Output: #{check[:output]}" if check[:notification] + return details + end + + def _build_flapjack_event(event) + client = event[:client] + check = event[:check] + + tags = _get_tags(event, client, check) + details = _get_details(event, client, check, tags) + + flapjack_event = { + entity: client[:name], + check: check[:name], + type: 'service', + state: Sensu::SEVERITIES[check[:status]] || 'unknown', + summary: check[:notification] || check[:output], + details: details.join(' '), + time: check[:executed], + tags: tags, + ttl: event[:ttl] ? event[:ttl] : options[:ttl] + } + + return flapjack_event + end + + def send_event(event) + begin + flapjack_event = _build_flapjack_event(event) + + @flapjack.post do |req| + req.url '/state' + req.headers['Content-Type'] = 'application/json' + req.body = MultiJson.dump(flapjack_event) + end + + @logger.debug("flapjack_http -- sent an event to the flapjack http broker") + rescue Faraday::TimeoutError => e + @logger.warn("flapjack_http -- timeout when sending event to http broker at #{options[:uri]}: #{e}") + rescue Faraday::ClientError => e + @logger.warn("flapjack_http -- client error when sending event to http broker at #{options[:uri]}: #{e}") + rescue StandardError => e + @logger.error("flapjack_http -- error sending event to http broker at #{options[:uri]}: #{e}") + end + end + + def run(event) + if event[:check][:status] == 0 # always forward ok alerts + @logger.debug('alert ok, forwarding') + send_event(event) + elsif filter(event) # only returns true for events which pass + send_event(event) + else + @logger.debug("NOT sending event: #{event[:client][:name]}/#{event[:check][:name]}") + end + yield + end + + # Filters yield events that should not be handled. + def filter(event) + @logger.debug('filtering event:') + @logger.debug(event) + unless filter_send_event(event) + @logger.debug('event type should not be sent') + return false + end + unless filter_disabled(event) + @logger.debug('event is disabled') + return false + end + unless filter_repeated(event) + @logger.debug('event has not repeated enough') + return false + end + unless filter_silenced(event) + @logger.debug('event should be silenced') + return false + end + unless filter_dependencies(event) + @logger.debug('event has alerting dependencies') + return false + end + return true + end + + def filter_send_event(event) + # Only send event if that type is enabled. + # Merge order: options[:default_send] < options[:default_send_[:type]] + case event[:check][:type] + when 'metric' + alert = options[:default_send_metric] + when 'check' + alert = options[:default_send_check] + else + alert = options[:default_send] + end + unless alert + @logger.debug("event should not be sent: #{event[:client][:name]}/#{event[:check][:name]}") + end + return alert + end + + def filter_disabled(event) + alert = true + if event[:check].key?(:alert) + if event[:check][:alert] == false + @logger.debug('alert disabled') + alert = false + end + end + return alert + end + + def filter_repeated(event) + alert = true + occurrences = (event[:check][:occurrences] || options[:occurrences]).to_i + interval = (event[:check][:interval] || options[:interval]).to_i + refresh = (event[:check][:refresh] || options[:refresh]).to_i + if event[:occurrences] < occurrences + alert = false + end + if event[:occurrences] > occurrences && event[:action] == 'create' + number = refresh.fdiv(interval).to_i + unless number == 0 || (event[:occurrences] - occurrences) % number == 0 + @logger.debug("only handling every #{number} occurrences") + alert = false + end + end + return alert + end + + def stash_exists?(path) + path = "/stashes#{path}" + @logger.debug("checking for stash at: #{path}") + res = @sensu_api.get(path) + @logger.debug("#{path} : #{res.status} - #{res.body}") + res.status == 200 + end + + def filter_silenced(event) + alert = true + stashes = { + "client" => "/silence/#{event[:client][:name]}", + "client_check" => "/silence/#{event[:client][:name]}/#{event[:check][:name]}", + "all_check" => "/silence/all/#{event[:check][:name]}" + } + stashes.each do |scope, path| + if alert + begin + Timeout.timeout(5) do + if stash_exists?(path) + @logger.debug("#{scope} alerts silenced") + alert = false + end + end + rescue Errno::ECONNREFUSED + @logger.error('connection refused attempting to query the sensu api for a stash') + rescue Timeout::Error + @logger.error('timed out while attempting to query the sensu api for a stash') + end + end + end + return alert + end + + def event_exists?(client, check) + path = '/events/' + client + '/' + check + @logger.debug("checking for dependency at: #{path}") + res = @sensu_api.get(path) + @logger.debug("#{path} : #{res.status} - #{res.body}") + res.status == 200 + end + + def filter_dependencies(event) + alert = true + @logger.debug("filter dependencies") + deps = event[:check].fetch(:dependencies, [].freeze) + @logger.debug("dependencies: #{deps}") + Array(deps).each do |dependency| + begin + @logger.debug("dependency: #{dependency}") + Timeout.timeout(2) do + check, client = dependency.split("/").reverse + if event_exists?(client || event[:client][:name], check) + @logger.debug("check dependency event exists") + alert = false + end + end + rescue Errno::ECONNREFUSED + @logger.error("connection refused while attempting to query the sensu api for an events") + rescue Timeout::Error + @logger.error("timed out while attempting to query the sensu api for an event") + end + end + return alert + end + + end + end +end diff --git a/roles/sensu-server/files/etc/sensu/extensions/handlers/sensu_api.rb b/roles/sensu-server/files/etc/sensu/extensions/handlers/sensu_api.rb new file mode 100755 index 0000000..96f9c07 --- /dev/null +++ b/roles/sensu-server/files/etc/sensu/extensions/handlers/sensu_api.rb @@ -0,0 +1,226 @@ +# Issue common actions against the Sensu API via messages submitted via sensu-client socket. +# +# This extension requires Sensu >= 0.20.0 +# +# Here is an example of what the Sensu configuration for sensu_api should +# look like, assuming your Sensu API is running on the +# same machine as the Sensu Server: +# +# { +# "sensu_api": { +# "host": "127.0.0.1", +# "port": 4567, +# "user": nil, +# "pass": nil +# } +# } +# +# Copyright 2016 Blue Box, an IBM Company +# 07/22/2016 - Myles Steinhauser +# +# Released under the same terms as Sensu (the MIT license); see LICENSE for details. + +require 'faraday' +require 'json' + +module Sensu + module Extension + class SensuApi < Handler + def name + 'sensu_api' + end + + def description + 'issue requests against the sensu api' + end + + def options + return @options if @options + @options = { + host: '127.0.0.1', + port: 4567, + user: nil, + pass: nil, + datacenter: 'unknown', + request: { + open_timeout: 3, + timeout: 3 + } + } + if @settings[:sensu_api].is_a?(Hash) + @options.merge!(@settings[:sensu_api]) + end + @logger.debug('sensu_api -- merged settings: ' + JSON.dump(options)) + @options + end + + def definition + { + type: 'extension', + name: name, + mutator: 'ruby_hash' + } + end + + def post_init + @logger.info("sensu_api -- connecting to sensu-api: #{options[:sensu_api]}") + begin + @sensu_api = Faraday.new(:url => "http://#{options[:host]}:#{options[:port]}") do |conn| + conn.request :basic_auth, options[:user], options[:pass] + conn.options.timeout = options[:request][:timeout] + conn.options.open_timeout = options[:request][:timeout] + # Use the net/http gem implementation + conn.adapter :net_http # must be last line before end + end + rescue Faraday::ClientError + @logger.warn('sensu_api -- sensu-api not available') + else + @logger.info('sensu_api -- connected to sensu-api!') + end + end + + def _stash_exists?(path) + path = "/stashes/#{path}" + @logger.debug("checking for stash: #{path}") + res = @sensu_api.get(path) + @logger.debug("#{path}: #{res.status} - #{res.body}") + res.status == 200 + end + + def _stash_get(path) + path = "/stashes/#{path}" + @logger.debug("getting stash: #{path}") + res = @sensu_api.get(path) + @logger.debug("#{path}: #{res.status} - #{res.body}") + return res.body + end + + def _stash_create(path:, reason:"sensu-api handler", expire:86400) # default expire in 24 hours + stash = { + path: path, + expire: expire, + dc: options[:datacenter], + content: { + reason: reason, + source: "sensu-server sensu-api handler", + timestamp: Time.now.to_i + } + } + @logger.debug("creating stash: path: /stashes/#{path}, body: #{stash}") + res = @sensu_api.post do |req| + req.url '/stashes' + req.headers['Content-Type'] = 'application/json' + req.body = stash.to_json + end + @logger.debug("/stashes/#{path}: #{res.status} - #{res.body}") + end + + def _stash_delete(path) + path = "/stashes/#{path}" + @logger.debug("deleting stash: #{path}") + res = @sensu_api.delete("#{path}") + @logger.debug("#{path}: #{res.status} - #{res.body}") + end + + def run(event) + action = event[:check][:action] + host = event[:check][:host] || event[:client][:name] + check = event[:check][:name] || nil + + # event type dispatcher + case action + when "silence_host" + _handle_silence("host", "create", host, check, event) + when "unsilence_host" + _handle_silence("host", "delete", host, check, event) + when "silence_check" + _handle_silence("check", "create", host, check, event) + when "unsilence_check" + _handle_silence("check", "delete", host, check, event) + when "resolve_check" + _handle_resolve("check", host, check, event) + when "delete_host" + _handle_delete("host", host, check, event) + when "delete_check" + _handle_delete("check", host, check, event) + when "host_all_clear" + _handle_all_clear("host", host, check, event) + else + @logger.error("unknown action: #{action}. Known actions: silence_host, unsilence_host, silence_check, unsilence_check, delete_host, delete_check, resolve_check, host_all_clear") + end + end + + def _handle_silence(target, op, host, check, event) + @logger.debug('handle silence: starting') + case target + when "host" + path = "silence/#{host}" + when "check" + path = "silence/#{host}/#{check}" + end + + case op + when "create" + reason = event[:check][:reason] || nil + expire = event[:check][:expire] || nil + _stash_create(path:path, reason:reason, expire:expire) + when "delete" + _stash_delete(path:path) + end + @logger.debug('handle silence: completed') + end + + def _handle_resolve(host, check, event) + @logger.debug('handle resolve: starting') + payload = { + client: host, + check: check + } + @logger.debug("resolving check: #{payload}") + res = @sensu_api.post do |req| + req.url '/resolve' + req.headers['Content-Type'] = 'application/json' + req.body = payload.to_json + end + @logger.debug('handle resolve: completed') + return res.body + end + + def _handle_all_clear(target, host, check, event) + @logger.debug('handle all clear: starting') + if target != "host" + @logger.error("handle all clear: only host is supported for now") + end + + unsilence = event[:check][:unsilence] || false + + # get all events for host + res = @sensu_api.get("/events/#{host}") + + # loop over all found events and resolve each, possibly removing stashes + res.body.each do |current_check| + _handle_resolve("check", host, current_check, event) + if unsilence + _handle_silence("check", "delete", host, current_check, nil) + end + end + @logger.debug('handle all clear: completed') + return res.body + end + + def _handle_delete(target, host, check, event) + @logger.debug('handle delete: starting') + case target + when "host" + path = "clients/#{host}" + when "check" + path = "events/#{host}/#{check}" + end + + res = @sensu_api.delete("#{path}") + @logger.debug('handle delete: completed') + return res.body + end + end + end +end diff --git a/roles/sensu-server/files/etc/sensu/handlers/pagerduty.rb b/roles/sensu-server/files/etc/sensu/handlers/pagerduty.rb new file mode 100644 index 0000000..aee77c7 --- /dev/null +++ b/roles/sensu-server/files/etc/sensu/handlers/pagerduty.rb @@ -0,0 +1,55 @@ +#!/usr/bin/env ruby +# +# This handler creates and resolves PagerDuty incidents, refreshing +# stale incident details every 30 minutes +# +# Copyright 2011 Sonian, Inc +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. + +require 'rubygems' if RUBY_VERSION < '1.9.0' +require 'sensu-handler' +require 'redphone/pagerduty' + +class Pagerduty < Sensu::Handler + + def incident_key + @event['client']['name'] + '/' + @event['check']['name'] + end + + def service_owners + settings['pagerduty']['api_keys'] + end + + def handle + description = @event['notification'] || [@event['client']['name'], @event['check']['name'], @event['check']['output']].join(' : ') + service_key = @event['check']['service_owner'] ? service_owners[@event['check']['service_owner']] : settings['pagerduty']['api_key_default'] + begin + timeout(3) do + response = case @event['action'] + when 'create' + Redphone::Pagerduty.trigger_incident( + :service_key => service_key, + :incident_key => incident_key, + :description => description, + :details => @event + ) + when 'resolve' + Redphone::Pagerduty.resolve_incident( + :service_key => service_key, + :incident_key => incident_key + ) + end + if response['status'] == 'success' + puts 'pagerduty -- ' + @event['action'].capitalize + 'd incident -- ' + incident_key + else + puts 'pagerduty -- failed to ' + @event['action'] + ' incident -- ' + incident_key + end + end + rescue Timeout::Error + puts 'pagerduty -- timed out while attempting to ' + @event['action'] + ' a incident -- ' + incident_key + end + end + +end diff --git a/roles/sensu-server/files/etc/sensu/handlers/remedy.rb b/roles/sensu-server/files/etc/sensu/handlers/remedy.rb new file mode 100644 index 0000000..b9b25cd --- /dev/null +++ b/roles/sensu-server/files/etc/sensu/handlers/remedy.rb @@ -0,0 +1,191 @@ +#!/opt/sensu/embedded/bin/ruby +# +# This handler creates and resolves remedy incidents, refreshing +# stale incident details every 30 minutes +# +# Copyright +# +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# +# Dependencies: +# +# sensu-plugin >= 1.0.0 +# savon == 2.11.1 +# + +require 'sensu-handler' +require 'savon' + +class Remedy < Sensu::Handler + + def get_wsdl + @baseurl = settings['remedy']['urls']['baseurl'] + @wsdl_create = settings['remedy']['urls']['wsdl_create'] + @wsdl_query = settings['remedy']['urls']['wsdl_query'] + @wsdl_update = settings['remedy']['urls']['wsdl_update'] + @wsdlfiles = { + 'create' => "#@baseurl/#@wsdl_create", + 'query' => "#@baseurl/#@wsdl_query", + 'update' => "#@baseurl/#@wsdl_update", + } + end + + def get_soap + @userName = settings['remedy']['userName'] + @password = settings['remedy']['password'] + + @soap_header = { + 'AuthenticationInfo' => { + 'userName' => @userName, + 'password' => @password + }} + end + + def incident_name + source = @event['check']['source'] || @event['client']['name'] + [source, @event['check']['name']].join('/') + end + + def create_incident + if not get_open_incidents + puts "No incidents relate to #{incident_name}. Creates a new one." + # no open incidents about the incident_name + # create a new one + client = Savon.client( + wsdl: get_wsdl['create'], + log: false, + ssl_verify_mode: :none, + soap_header: get_soap) + + severity_map = { + '2' => '1', + '1' => '2'} + urgency_map = { + '2' => '1-Critical', + '1' => '3-Medium'} + impact_map = { + '2' => '1-Extensive/Widespread', + '1' => '3-Moderate/Limited'} + + description = ['Blue Box Alert', + "SEV#{severity_map[@event['check']['status'].to_s]} -", + "#{@event['client']['name']}:", + "#{incident_name}" + ].join(' ') + detailed_description = ['Blue Box Alert:', + "\"Severity\": #{severity_map[@event['check']['status'].to_s]},", + "\"Hostname\": #{@event['client']['name']},", + "\"Event Summary\": #{event_summary}" + ].join(' ') + + response = client.call(:create_operation, message: { + 'Status' => 'Assigned', + 'Description' => description, + 'Company' => 'China Bluemix', + 'Last_Name' => 'Bluemix', + 'First_Name' => 'Monitor', + 'z1D_Action' => 'CREATE', + 'Service_Type' => 'Infrastructure Event', + 'Detailed_Decription' => detailed_description, + 'Urgency' => urgency_map[@event['check']['status'].to_s], + 'Impact' => impact_map[@event['check']['status'].to_s], + 'Support_Tier' => 'Standard', + 'type' => 'alert ticket', + 'Uni_sendAttachment1' => 'No', + 'Uni_sendAttachment2' => 'No', + 'Uni_sendAttachment3' => 'No', + 'Uni_sendAttachment4' => 'No', + 'Uni_sendAttachment5' => 'No', + 'Uni_Service' => settings['remedy']['service'], + 'Reported_Source_OOTB_Required' => 'BMC Impact Manager Event', + 'Locale_Language' => 'en_US', + 'Reported_Source_Real' => 'bluebox_monitor'}) + + puts "Successfully create a remedy incident: #{response.to_hash[:create_operation_response][:request_id]}" + else + puts "Already had incidents relating to #{incident_name}. No need to create a new one." + end + end + + def query_incident(query_msg) + wsdl_file = settings['remedy']['wsdlQuery'] + + client = Savon.client( + wsdl: get_wsdl['query'], + log: false, + soap_header: get_soap) + + begin + puts "Query the incidents with query_msg: #{query_msg}" + response = client.call(:get_list_operation, message: query_msg) + response.to_hash[:get_list_operation_response][:get_list_values] || nil + rescue Exception => exp_msg + puts "Query failed with message: #{exp_msg}" + nil + end + + end + + def update_incident(updated_hash) + puts "Start to update incident: #{updated_hash}" + + client = Savon.client( + wsdl: get_wsdl['update'], + log: true, + soap_header: get_soap) + response = client.call(:set_operation_update, message: updated_hash) + puts "Successfully update a remedy incident: #{response.to_hash[:set_operation_update_response][:request_id]}" + end + + def resolve_incident + incidents = get_open_incidents + if incidents + # actually only one incident may match the incident_name + if incidents.is_a? Array + incident = incidents[0] + else + incident = incidents + end + + puts "Start to resolve incident #{incident[:incident_number]}" + # set the status of the incident to Resolved + # + updated_hash = {'Status' => 'Closed', + 'Resolution' => 'Auto-resolved by Sensu', + 'Incident_Number' => incident[:incident_number], + 'Locale_Language' => 'en_US',} + update_incident(updated_hash) + puts "Successfully resolve incident #{incident[:incident_number]}" + else + puts 'Did not find the incidents to be resolved.' + end + end + + def get_open_incidents + status = ["'Status'=\"New\"", "'Status'=\"Assigned\"", + "'Status'=\"In Progress\"", "'Status'=\"Pending\""].join(' OR ') + description = "'Description' LIKE \"%#{incident_name}%\"" + qualification = "(#{status}) AND #{description}" + query_msg = {'Qualification' => qualification, + 'startRecord' => 0, + 'maxLimit' => 100} + query_incident(query_msg) + end + + def handle + begin + timeout(10) do + case @event['action'] + when 'create' + create_incident + when 'resolve' + resolve_incident + end + end + rescue Timeout::Error + puts "remedy -- timed out while attempting to handle a incident -- #{incident_name}" + end + end + +end diff --git a/roles/sensu-server/files/etc/sensu/scripts/clear-stashes.rb b/roles/sensu-server/files/etc/sensu/scripts/clear-stashes.rb new file mode 100644 index 0000000..5ed7be5 --- /dev/null +++ b/roles/sensu-server/files/etc/sensu/scripts/clear-stashes.rb @@ -0,0 +1,84 @@ +#!/opt/sensu/embedded/bin/ruby +# + +require 'rubygems' +require 'sensu-plugin/metric/cli' +require 'sensu-plugin/utils' +require 'rest-client' +require 'json' +require 'socket' + +include Sensu::Plugin::Utils + +class CheckSilenced < Sensu::Plugin::Metric::CLI::Graphite + default_host = settings['api']['host'] rescue 'localhost' # rubocop:disable RescueModifier + + option :host, + :short => '-h HOST', + :long => '--host HOST', + :description => 'Hostname for sensu-api endpoint', + :default => default_host + + option :port, + :short => '-p PORT', + :long => '--port PORT', + :description => 'Port for sensu-api endpoint', + :default => 4567 + + option :scheme, + :short => '-s SCHEME', + :long => '--scheme SCHEME', + :description => 'Metric naming scheme, text to prepend to metric', + :default => "#{Socket.gethostname}.sensu.stashes" + + option :filter, + :short => '-f PREFIX', + :long => '--filter PREFIX', + :description => 'Stash prefix filter', + :default => 'silence' + + option :noop, + :short => '-d', + :long => '--dry-run', + :description => 'Do not delete expired stashes', + :default => false + + def api + endpoint = URI.parse("http://#{@config[:host]}:#{@config[:port]}") + @config[:use_ssl?] ? endpoint.scheme = 'https' : endpoint.scheme = 'http' + @api ||= RestClient::Resource.new(endpoint, :timeout => 45) + end + + def acquire_stashes + all_stashes = JSON.parse(api['/stashes'].get) + filtered_stashes = [] + all_stashes.each do |stash| + filtered_stashes << stash if stash['path'].match(/^#{@config[:filter]}\/.*/) + end + return filtered_stashes + rescue Errno::ECONNREFUSED + warning 'Connection refused' + rescue RestClient::RequestTimeout + warning 'Connection timed out' + rescue JSON::ParserError + warning 'Sensu API returned invalid JSON' + end + + def delete_stash(stash) + api["/stash/#{stash['path']}"].delete + end + + def run + @config = config + stashes = acquire_stashes + now = Time.now.to_i + @count = 0 + if stashes.count > 0 + stashes.each do |stash| + delete_stash(stash) unless config[:noop] + @count += 1 + end + end + ok "#{config[:scheme]}.sensu.stashes", @count + end +end diff --git a/roles/sensu-server/files/opt/sitecontroller/sensu-plugins/check-sensu-api-health.rb b/roles/sensu-server/files/opt/sitecontroller/sensu-plugins/check-sensu-api-health.rb new file mode 100644 index 0000000..bb203fe --- /dev/null +++ b/roles/sensu-server/files/opt/sitecontroller/sensu-plugins/check-sensu-api-health.rb @@ -0,0 +1,125 @@ +#! /usr/bin/env ruby +# +# sensu-api-health +# +# DESCRIPTION: +# Check health of sensu-api and consumption of keepalives and results. +# +# OUTPUT: +# plain text +# +# PLATFORMS: +# Linux +# +# DEPENDENCIES: +# gem: sensu-plugin +# gem: json +# gem: uri +# +# USAGE: +# #YELLOW +# +# NOTES: +# +# LICENSE: +# Copyright 2016 Myles Steinhauser +# Released under the same terms as Sensu (the MIT license); see LICENSE +# for details. +# + +require 'sensu-plugin/check/cli' +require 'net/http' +require 'net/https' +require 'json' +require 'uri' + +class SensuApiHealthCheck < Sensu::Plugin::Check::CLI + option :host, + short: '-h HOST', + long: '--host HOST', + description: 'Your sensu-api endpoint', + required: true, + default: 'localhost' + + option :port, + short: '-P PORT', + long: '--port PORT', + description: 'Your sensu-api port', + required: true, + default: 4567 + + option :username, + short: '-u USERNAME', + long: '--username USERNAME', + description: 'Your sensu-api username', + required: false + option :password, + short: '-p PASSWORD', + long: '--password PASSWORD', + description: 'Your sensu-api password', + required: false + + option :https, + short: '-s', + long: '--secure', + description: 'Use HTTPS instead of HTTP', + default: false, + required: false + + option :results, + short: '-r results', + long: '--results results', + description: 'Number of results allowed to be queued.', + required: false, + default: 1000 + + option :keepalives, + short: '-k keepalives', + long: '--keepalives keepalives', + description: 'Number of keepalives allowed to be queued.', + required: false, + default: 10 + + def json_valid?(str) + begin + JSON.parse(str) + return true + rescue + return false + end + end + + def run + endpoint = config[:https] ? "https://#{config[:host]}:#{config[:port]}" : "http://#{config[:host]}:#{config[:port]}" + url = URI.parse(endpoint) + + begin + # res = Net::HTTP.start(url.host, url.port, :use_ssl => url.scheme == 'https', verify_mode => OpenSSL::SSL::VERIFY_NONE) do |http| + res = Net::HTTP.start(url.host, url.port, :use_ssl => url.scheme == 'https') do |http| + req = Net::HTTP::Get.new('/info') + req.basic_auth config[:username], config[:password] if config[:username] && config[:password] + http.request(req) + end + rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, + Net::HTTPHeaderSyntaxError, Net::ProtocolError, Errno::ECONNREFUSED => e + critical e + resuce Net::HTTPUnauthorized + critical "Unauthorized to check sensu-api health!" + end + + if json_valid?(res.body) + json = JSON.parse(res.body) + if json['transport']['keepalives']['messages'] > Integer(config[:keepalives]) + critical "Sensu not processing keepalives fast enough! #{json['transport']['keepalives']['messages']} keepalives queued, #{config[:keepalives]} acceptable." + end + if json['transport']['results']['messages'] > Integer(config[:results]) + critical "Sensu not processing results fast enough! #{json['transport']['results']['messages']} results queued, #{config[:results]} acceptable." + end + else + critical 'Response contains invalid JSON' + end + + ok + end +end + diff --git a/roles/sensu-server/handlers/main.yml b/roles/sensu-server/handlers/main.yml new file mode 100644 index 0000000..dbf5ec6 --- /dev/null +++ b/roles/sensu-server/handlers/main.yml @@ -0,0 +1,39 @@ +--- +- name: start sensu + service: + name: "{{ item }}" + state: started + must_exist: false + with_items: + - sensu-api + - sensu-server + +- name: stop sensu + service: + name: "{{ item }}" + state: stopped + must_exist: true + with_items: + - sensu-api + - sensu-server + +- name: kill sensu + shell: "cat /var/run/sensu/sensu*-{{ item }}.pid | xargs -r -I% kill -9 %" + with_items: + - api + - server + +- name: restart sensu + service: + name: "{{ item }}" + state: restarted_if_running + must_exist: true + with_items: + - sensu-api # need this up before sensu-server gets restarted + - sensu-server + +- name: restart sensu dashboard + service: + name: uchiwa + state: restarted_if_running + must_exist: true diff --git a/roles/sensu-server/meta/main.yml b/roles/sensu-server/meta/main.yml new file mode 100644 index 0000000..87cb089 --- /dev/null +++ b/roles/sensu-server/meta/main.yml @@ -0,0 +1,6 @@ +--- +dependencies: + - role: rabbitmq + - role: redis + - role: sensu-common + - role: sensu-check diff --git a/roles/sensu-server/tasks/checks.yml b/roles/sensu-server/tasks/checks.yml new file mode 100644 index 0000000..45fc631 --- /dev/null +++ b/roles/sensu-server/tasks/checks.yml @@ -0,0 +1,42 @@ +--- +- name: copy sensu-client plugins + copy: + src: "{{ item }}" + dest: /opt/sitecontroller/sensu-plugins/ + mode: 0755 + with_fileglob: opt/sitecontroller/sensu-plugins/* + notify: restart sensu-client + +# FIXME: this is a really bad spot for this check to live +- name: install elk-stats checks + sensu_check: + name: "elk-stats-check_{{ item.key }}" + plugin: check-graphite-stats.rb + args: "-h 127.0.0.1:8081 -p -12hrs -t stats.sc.{{ item.key|replace('.', '_') }}.*.elasticsearch.days_remaining -w 20 -c 10 -r true" + interval: 21600 + service_owner: "{{ monitoring_common.service_owner }}" + when: deploy_type == "control" + with_dict: "{{ openid_proxy.remote_locations }}" + tags: elk-stats + +- name: install sensu-server process check + sensu_process_check: service=sensu-server + notify: restart sensu-client missing ok + +- name: install sensu-api process check + sensu_process_check: service=sensu-api + notify: restart sensu-client missing ok + +- name: install uchiwa process check + sensu_process_check: service=uchiwa + notify: restart sensu-client missing ok + +- name: install sensu-api health check + sensu_check_dict: + name: check-sensu-api-health + check: "{{ sensu_checks.sensu_api.check_sensu_api_health }}" + +- name: install uchiwa health check + sensu_check_dict: + name: check-uchiwa-health + check: "{{ sensu_checks.uchiwa.check_uchiwa_health }}" diff --git a/roles/sensu-server/tasks/dashboard.yml b/roles/sensu-server/tasks/dashboard.yml new file mode 100644 index 0000000..337e436 --- /dev/null +++ b/roles/sensu-server/tasks/dashboard.yml @@ -0,0 +1,59 @@ +--- +- name: install sensu dashboard + apt: + name: uchiwa={{ sensu.dashboard.version }} + force: yes + +- name: add uchiwa user to sensu group + user: name=uchiwa + groups=sensu + append=yes + +- name: sensu dashboard default + template: src=etc/default/uchiwa + dest=/etc/default/uchiwa + notify: + - restart sensu dashboard + +- name: configure sensu dashboard + template: src=etc/sensu/uchiwa.json + dest=/etc/sensu-server/uchiwa.json + owner=uchiwa + group=sensu + mode=0660 + notify: + - restart sensu dashboard + +- name: install sensu dashboard plugins + gem: + name: "{{ item.name }}" + version: "{{ item.version | default(omit) }}" + executable: /opt/sensu/embedded/bin/gem + user_install: no + with_items: "{{ sensu.dashboard.sensu_plugins }}" + register: result + until: result|succeeded + retries: 5 + +- name: configure uchiwa upstart + upstart_service: name=uchiwa + cmd=/opt/uchiwa/bin/uchiwa + args="-c /etc/sensu-server/uchiwa.json -d /etc/sensu/dashboard.d -p /opt/uchiwa/src/public" + user=uchiwa + notify: restart sensu dashboard + +- name: remove uchiwa init file + file: + path: /etc/init.d/uchiwa + state: absent + +- meta: flush_handlers + +- name: start sensu dashboard + service: name=uchiwa state=started enabled=yes + +- name: permit dashboard traffic + ufw: rule=allow to_port={{ item.port }} proto=tcp src={{ item.src }} + with_items: "{{ sensu.dashboard.firewall }}" + tags: + - firewall diff --git a/roles/sensu-server/tasks/main.yml b/roles/sensu-server/tasks/main.yml new file mode 100644 index 0000000..4570e86 --- /dev/null +++ b/roles/sensu-server/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- include: server.yml + +- include: dashboard.yml + tags: + - uchiwa + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/sensu-server/tasks/metrics.yml b/roles/sensu-server/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/sensu-server/tasks/server.yml b/roles/sensu-server/tasks/server.yml new file mode 100644 index 0000000..af6dcc5 --- /dev/null +++ b/roles/sensu-server/tasks/server.yml @@ -0,0 +1,201 @@ +--- +- name: sensu server config directories + file: dest={{ item }} mode=0770 + state=directory + owner=sensu + group=sensu + with_items: + - /etc/sensu-server + - /etc/sensu-server/conf.d + - /etc/sensu-server/conf.d/handlers + - /etc/sensu-server/handlers + - /etc/sensu-server/extensions/handlers + - /etc/sensu-server/scripts + - /etc/sensu-server/ssl + +- name: create cert + template: src=etc/sensu/ssl/cert.pem + dest=/etc/sensu-server/ssl/cert.pem + owner=sensu + group=sensu + mode=0660 + notify: restart sensu + +- name: create key + template: src=etc/sensu/ssl/key.pem + dest=/etc/sensu-server/ssl/key.pem + owner=sensu + group=sensu + mode=0660 + notify: restart sensu + +- name: sensu-server defaults + template: src=etc/default/sensu-server + dest=/etc/default/sensu-server + notify: + - kill sensu + - start sensu + +- name: sensu-api defaults + template: src=etc/default/sensu-api + dest=/etc/default/sensu-api + notify: restart sensu + +- name: sensu-server init script + template: src=etc/init.d/sensu-server + dest=/etc/init.d/sensu-server + notify: + - kill sensu + - start sensu + +- name: sensu-server logrotate + template: src=etc/logrotate.d/sensu + dest=/etc/logrotate.d/sensu + +- name: configure sensu + template: src=etc/sensu/conf.d/{{ item }} + dest=/etc/sensu-server/conf.d/{{ item }} mode=0644 + with_items: + - api.json + - rabbitmq.json + - redis.json + notify: restart sensu + +- name: copy sensu handlers + copy: src={{ item }} dest=/etc/sensu-server/handlers mode=0755 + with_fileglob: etc/sensu/handlers/* + notify: restart sensu + +- name: copy sensu extension handlers + copy: src=etc/sensu/extensions/handlers/{{ item.name }} dest=/etc/sensu-server/extensions/handlers/{{ item.name }} mode=0755 + when: "{{ item.when }}" # Necessary syntax due to variable evaluation + with_items: + - name: flapjack.rb + when: sensu.server.handlers.flapjack.enabled + - name: flapjack_http.rb + when: sensu.server.handlers.flapjack_http.enabled + - name: sensu_api.rb + when: sensu.server.handlers.sensu_api.enabled + notify: restart sensu + +- name: remove unused sensu extension handlers + file: dest=/etc/sensu-server/extensions/handlers/{{ item.name }} state=absent + when: "{{ item.when }}" # Necessary syntax due to variable evaluation + with_items: + - name: flapjack.rb + when: not sensu.server.handlers.flapjack.enabled + - name: flapjack_http.rb + when: not sensu.server.handlers.flapjack_http.enabled + - name: sensu_api.rb + when: not sensu.server.handlers.sensu_api.enabled + notify: restart sensu + +- name: configure handlers + template: src="etc/sensu/conf.d/handlers/{{ item.key }}.json" + dest="/etc/sensu-server/conf.d/handlers/{{ item.key }}.json" + mode=0640 + group=sensu + when: "{{ item.value.when }}" # Necessary syntax due to variable evaluation + with_dict: + metrics: + when: true + graphite: + when: sensu.server.handlers.graphite.enabled|bool + pagerduty: + when: sensu.server.handlers.pagerduty.enabled|bool + flapjack: + when: sensu.server.handlers.flapjack.enabled|bool + flapjack_http: + when: sensu.server.handlers.flapjack_http.enabled|bool + remedy: + when: sensu.server.handlers.remedy.enabled|bool + sensu_api: + when: sensu.server.handlers.sensu_api.enabled|bool + notify: restart sensu + +- name: configure hijack handlers + template: src="etc/sensu/conf.d/handlers/hijack.json" + dest="/etc/sensu-server/conf.d/handlers/{{ item.key }}_hijack.json" + mode=0640 + group=sensu + with_dict: "{{ sensu.server.handlers.hijack }}" + when: item.value|length > 0 + notify: restart sensu + +- name: de-configure handlers + file: dest="/etc/sensu-server/conf.d/handlers/{{ item.key }}.json" + state=absent + when: "{{ item.value.when }}" # Necessary syntax due to variable evaluation + with_dict: + graphite: + when: not sensu.server.handlers.graphite.enabled|bool + pagerduty: + when: not sensu.server.handlers.pagerduty.enabled|bool + flapjack: + when: not sensu.server.handlers.flapjack.enabled|bool + flapjack_http: + when: not sensu.server.handlers.flapjack_http.enabled|bool + remedy: + when: not sensu.server.handlers.remedy.enabled|bool + sensu_api: + when: not sensu.server.handlers.sensu_api.enabled|bool + notify: restart sensu + +- name: copy sensu scripts + template: src={{ item }} dest=/etc/sensu-server/scripts/ mode=0755 + with_fileglob: etc/sensu/scripts/* + notify: restart sensu + +- name: configure clear-stashes cron + cron: name="clear stashes" + user="sensu" + minute="0" + hour="20" + weekday="3" + job="/etc/sensu-server/scripts/clear-stashes.rb" + +- name: copy sensu apache config + template: src=etc/apache2/sites-available/sensu-api.conf dest=/etc/apache2/sites-available/ mode=0755 + when: sensu.api.ssl_enabled|bool + +- name: use embedded ruby + lineinfile: dest=/etc/default/sensu regexp=^EMBEDDED_RUBY + line=EMBEDDED_RUBY=true + notify: restart sensu + +- name: install gems into sensu embedded ruby + gem: + name: "{{ item }}" + executable: /opt/sensu/embedded/bin/gem + user_install: no + with_items: + - redphone + - net-http-persistent + - savon + - sensu-plugins-graphite + register: result + until: result|succeeded + retries: 5 + notify: restart sensu + tags: elk-stats + +- meta: flush_handlers + +- name: wait for redis-db to load + command: redis-cli ping + register: result + until: result.stdout.find("PONG") != -1 + retries: 15 + delay: 1 + +- name: start sensu-server + service: name=sensu-server state=started enabled=yes + +- name: start sensu-api + service: name=sensu-api state=started enabled=yes + +- name: permit api traffic + ufw: rule=allow to_port={{ item.port }} proto=tcp src={{ item.src }} + with_items: "{{ sensu.api.firewall }}" + tags: + - firewall diff --git a/roles/sensu-server/tasks/serverspec.yml b/roles/sensu-server/tasks/serverspec.yml new file mode 100644 index 0000000..02c15ce --- /dev/null +++ b/roles/sensu-server/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for sensu-server role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/sensu-server/templates/etc/apache2/sites-available/sensu-api.conf b/roles/sensu-server/templates/etc/apache2/sites-available/sensu-api.conf new file mode 100644 index 0000000..d0a08ce --- /dev/null +++ b/roles/sensu-server/templates/etc/apache2/sites-available/sensu-api.conf @@ -0,0 +1,23 @@ +# {{ ansible_managed }} + + + + ServerName {{ openid_proxy.apache.servername }} + ServerAdmin {{ openid_proxy.apache.serveradmin }} + + {{ apache.ssl.settings }} + SSLCertificateFile /etc/ssl/certs/sensu.pem + SSLCertificateKeyFile /etc/ssl/private/sensu.key + + + ProxyPass http://127.0.0.1:{{ sensu.api.port }}/ + ProxyPassReverse http://127.0.0.1:{{ sensu.api.port }}/ + + ErrorLog ${APACHE_LOG_DIR}/sensu-api-error.log + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + CustomLog ${APACHE_LOG_DIR}/sensu-access.log combined + diff --git a/roles/sensu-server/templates/etc/default/sensu-api b/roles/sensu-server/templates/etc/default/sensu-api new file mode 100644 index 0000000..996b4cc --- /dev/null +++ b/roles/sensu-server/templates/etc/default/sensu-api @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +CONFIG_FILE=/etc/sensu-server/api.json +CONFIG_DIR=/etc/sensu-server/conf.d +EXTENSION_DIR=/etc/sensu-server/extensions +PLUGINS_DIR=/etc/sensu-server/plugins +HANDLERS_DIR=/etc/sensu-server/handlers diff --git a/roles/sensu-server/templates/etc/default/sensu-server b/roles/sensu-server/templates/etc/default/sensu-server new file mode 100644 index 0000000..aaec063 --- /dev/null +++ b/roles/sensu-server/templates/etc/default/sensu-server @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +CONFIG_FILE=/etc/sensu-server/config.json +CONFIG_DIR=/etc/sensu-server/conf.d +EXTENSION_DIR=/etc/sensu-server/extensions +PLUGINS_DIR=/etc/sensu-server/plugins +HANDLERS_DIR=/etc/sensu-server/handlers + +ADDITIONAL_INSTANCE_COUNT={{ sensu.server.instances }} diff --git a/roles/sensu-server/templates/etc/default/uchiwa b/roles/sensu-server/templates/etc/default/uchiwa new file mode 100644 index 0000000..74c2614 --- /dev/null +++ b/roles/sensu-server/templates/etc/default/uchiwa @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +args="-c /etc/sensu-server/uchiwa.json -p /opt/uchiwa/src/public" diff --git a/roles/sensu-server/templates/etc/init.d/sensu-server b/roles/sensu-server/templates/etc/init.d/sensu-server new file mode 100644 index 0000000..c5cdce4 --- /dev/null +++ b/roles/sensu-server/templates/etc/init.d/sensu-server @@ -0,0 +1,53 @@ +#!/bin/bash + +# chkconfig: 345 90 90 +# description: Sensu monitoring framework server + +### BEGIN INIT INFO +# Provides: ${serverTag} +# Required-Start: $remote_fs $network +# Required-Stop: $remote_fs $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: Sensu monitoring framework server +### END INIT INFO + +cmdLine="$1" +ADDITIONAL_INSTANCE_COUNT=0 # default to a single sensu-server instance + +/etc/init.d/sensu-service server "${cmdLine}" + +if [ -f "/etc/default/sensu-server" ]; then + . /etc/default/sensu-server +fi + +# Additional instance(s) ---| +exitStatus="${?}" + +allOkExitCode=0 +serverTag=`echo "${0}" | xargs basename` + +# --------------------------| + + ( test "${1}" = "status" ) && allOkExitCode="${exitStatus}" + + if [ ! -z "${ADDITIONAL_INSTANCE_COUNT}" ] + then + + if [ "${ADDITIONAL_INSTANCE_COUNT}" -gt 0 ] + then + for index in `seq 1 ${ADDITIONAL_INSTANCE_COUNT} | xargs` + do + instanceId="${index}-server"; + + ( test "${exitStatus}" -eq 0 ) && (( test -f "/etc/default/${serverTag}" ) && ln -s "/etc/default/${serverTag}" "/etc/default/sensu-${instanceId}" 2>/dev/null ) + ( test "${exitStatus}" -eq 0 ) && (( test -L "/opt/sensu/bin/${serverTag}" ) && ln -s "/opt/sensu/bin/${serverTag}" "/opt/sensu/bin/sensu-${instanceId}" 2>/dev/null ) + ( test "${exitStatus}" -eq "${allOkExitCode}" ) && /etc/init.d/sensu-service "${instanceId}" "${cmdLine}"; exitStatus="${?}"; + + done + fi + fi + +# -------------------------| + +exit "${exitStatus}" diff --git a/roles/sensu-server/templates/etc/logrotate.d/sensu b/roles/sensu-server/templates/etc/logrotate.d/sensu new file mode 100644 index 0000000..2bd1aab --- /dev/null +++ b/roles/sensu-server/templates/etc/logrotate.d/sensu @@ -0,0 +1,50 @@ +/var/log/sensu/sensu-client.log { + rotate 7 + daily + missingok + notifempty + sharedscripts + compress + postrotate + kill -USR2 `cat /var/run/sensu/sensu-client.pid 2> /dev/null` 2> /dev/null || true + endscript +} + +/var/log/sensu/sensu-server.log { + rotate 7 + daily + missingok + notifempty + sharedscripts + compress + postrotate + kill -USR2 `cat /var/run/sensu/sensu-server.pid 2> /dev/null` 2> /dev/null || true + endscript +} + +{% for i in range(1, sensu.server.instances + 1) %} +/var/log/sensu/sensu-{{ i }}-server.log { + rotate 7 + daily + missingok + notifempty + sharedscripts + compress + postrotate + kill -USR2 `cat /var/run/sensu/sensu-{{ i }}-server.pid 2> /dev/null` 2> /dev/null || true + endscript +} +{% endfor %} + +/var/log/sensu/sensu-api.log { + rotate 7 + daily + missingok + notifempty + sharedscripts + compress + postrotate + kill -USR2 `cat /var/run/sensu/sensu-api.pid 2> /dev/null` 2> /dev/null || true + endscript +} + diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/api.json b/roles/sensu-server/templates/etc/sensu/conf.d/api.json new file mode 100644 index 0000000..36336b9 --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/api.json @@ -0,0 +1,9 @@ +{ + "api": { + "host": "127.0.0.1", + "bind": "{{ sensu.api.bind_ip }}", + "port": {{ sensu.api.port }}, + "user": "{{ sensu.api.username }}", + "password": "{{ sensu.api.password }}" + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/default.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/default.json new file mode 100644 index 0000000..232d5ad --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/default.json @@ -0,0 +1,8 @@ +{ + "handlers": { + "default": { + "type": "set", + "handlers": {{ sensu.server.handlers.default | to_json }} + } + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack.json new file mode 100644 index 0000000..fd78588 --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack.json @@ -0,0 +1,3 @@ +{ + "flapjack": {{ sensu.server.handlers.flapjack | to_nice_json }} +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack_http.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack_http.json new file mode 100644 index 0000000..37d23df --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/flapjack_http.json @@ -0,0 +1,3 @@ +{ + "flapjack_http": {{ sensu.server.handlers.flapjack_http | to_nice_json }} +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/graphite.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/graphite.json new file mode 100644 index 0000000..ccf6aba --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/graphite.json @@ -0,0 +1,12 @@ +{ + "handlers": { + "graphite": { + "type": "tcp", + "socket": { + "host": "{{ sensu.server.handlers.graphite.host }}", + "port": {{ sensu.server.handlers.graphite.port }} + }, + "mutator": "only_check_output" + } + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/hijack.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/hijack.json new file mode 100644 index 0000000..603d17b --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/hijack.json @@ -0,0 +1,8 @@ +{ + "handlers": { + "{{ item.key }}": { + "type": "set", + "handlers": {{ item.value|to_json }} + } + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/metrics.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/metrics.json new file mode 100644 index 0000000..d644132 --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/metrics.json @@ -0,0 +1,8 @@ +{ + "handlers": { + "metrics": { + "type": "set", + "handlers": {{ sensu.server.handlers.metrics | to_json }} + } + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/pagerduty.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/pagerduty.json new file mode 100644 index 0000000..834bb20 --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/pagerduty.json @@ -0,0 +1,24 @@ +{ + "handlers": { + "pagerduty": { + "type": "pipe", + "command": "/etc/sensu-server/handlers/pagerduty.rb", + "severities": [ + "ok", + "critical" + ] + } + }, + "pagerduty": { + "api_key_default": "{{ sensu.server.handlers.pagerduty.api_key_default }}", + "api_keys": { +{% for item in sensu.server.handlers.pagerduty.api_keys -%} + {% if loop.last %} + "{{ item['name'] }}": "{{ item['api_key'] }}" + {% else %} + "{{ item['name'] }}": "{{ item['api_key'] }}", + {% endif %} +{% endfor -%} + } + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/remedy.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/remedy.json new file mode 100644 index 0000000..33f6df0 --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/remedy.json @@ -0,0 +1,20 @@ +{ + "handlers": { + "remedy": { + "type": "pipe", + "command": "/etc/sensu-server/handlers/remedy.rb", + "severities": ["critical", "warning", "ok"] + } + }, + "remedy": { + "userName": "{{ sensu.server.handlers.remedy.username }}", + "password": "{{ sensu.server.handlers.remedy.password }}", + "service": "{{ sensu.server.handlers.remedy.service }}", + "urls": { + "baseurl": "{{ sensu.server.handlers.remedy.baseurl }}", + "wsdl_create": "{{ sensu.server.handlers.remedy.wsdl_create }}", + "wsdl_query": "{{ sensu.server.handlers.remedy.wsdl_query }}", + "wsdl_update": "{{ sensu.server.handlers.remedy.wsdl_update }}" + } + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/handlers/sensu_api.json b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/sensu_api.json new file mode 100644 index 0000000..6cd289d --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/handlers/sensu_api.json @@ -0,0 +1,3 @@ +{ + "sensu_api": {{ sensu.server.handlers.sensu_api | to_nice_json }} +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/rabbitmq.json b/roles/sensu-server/templates/etc/sensu/conf.d/rabbitmq.json new file mode 100644 index 0000000..b2de33a --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/rabbitmq.json @@ -0,0 +1,13 @@ +{ + "rabbitmq": { + "ssl": { + "cert_chain_file": "/etc/sensu-server/ssl/cert.pem", + "private_key_file": "/etc/sensu-server/ssl/key.pem" + }, + "host": "{{ sensu.server.rabbitmq.host }}", + "port": 5671, + "vhost": "{{ sensu.server.rabbitmq.vhost }}", + "user": "{{ sensu.server.rabbitmq.username }}", + "password": "{{ sensu.server.rabbitmq.password }}" + } +} diff --git a/roles/sensu-server/templates/etc/sensu/conf.d/redis.json b/roles/sensu-server/templates/etc/sensu/conf.d/redis.json new file mode 100644 index 0000000..14af3aa --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/conf.d/redis.json @@ -0,0 +1,6 @@ +{ + "redis": { + "host": "127.0.0.1", + "port": 6379 + } +} diff --git a/roles/sensu-server/templates/etc/sensu/ssl/cert.pem b/roles/sensu-server/templates/etc/sensu/ssl/cert.pem new file mode 100644 index 0000000..4e9722a --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/ssl/cert.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ sensu.server.rabbitmq.ssl.cert }} diff --git a/roles/sensu-server/templates/etc/sensu/ssl/key.pem b/roles/sensu-server/templates/etc/sensu/ssl/key.pem new file mode 100644 index 0000000..f222b0c --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/ssl/key.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ sensu.server.rabbitmq.ssl.key }} diff --git a/roles/sensu-server/templates/etc/sensu/uchiwa.json b/roles/sensu-server/templates/etc/sensu/uchiwa.json new file mode 100644 index 0000000..0fb47b0 --- /dev/null +++ b/roles/sensu-server/templates/etc/sensu/uchiwa.json @@ -0,0 +1,32 @@ +#jinja2:trim_blocks: False + +{% macro sensu_datacenter(sensu_dc) %} + { + "name": "{{ sensu_dc.name|default(sensu_dc.hostname) }}", + "host": "{{ sensu_dc.hostname }}", + "ssl": false, + "port": {{ sensu_dc.port|default(sensu.api.port) }}, + "user": "{{ sensu_dc.username|default(sensu.api.username) }}", + "pass": "{{ sensu_dc.password }}", + "path": "", + "timeout": {{ sensu_dc.timeout|default(5000) }} + } +{% endmacro %} + +{ + "sensu": [ + {% for sensu_dc in sensu.dashboard.datacenters %} + {{ sensu_datacenter(sensu_dc) }}{% if not loop.last %},{% endif %} + {% endfor %} + ], + "uchiwa": { +{% if sensu.dashboard.username is defined and sensu.dashboard.password is defined -%} + "user": "{{ sensu.dashboard.username }}", + "pass": "{{ sensu.dashboard.password }}", +{% endif -%} + "host": "{{ sensu.dashboard.host }}", + "port": {{ sensu.dashboard.port }}, + "stats": 10, + "refresh": {{ sensu.dashboard.refresh }} + } +} diff --git a/roles/sensu-server/templates/serverspec/sensu-server-handler_spec.rb b/roles/sensu-server/templates/serverspec/sensu-server-handler_spec.rb new file mode 100644 index 0000000..1280536 --- /dev/null +++ b/roles/sensu-server/templates/serverspec/sensu-server-handler_spec.rb @@ -0,0 +1,26 @@ +# {{ ansible_managed }} + +require 'spec_helper' +{% for key,value in sensu.server.handlers|dictsort %} +{% if value.enabled is defined %} +{% if value.enabled %} + +describe file('/etc/sensu-server/conf.d/handlers/{{ key }}.json') do + it { should be_file } +end +{% if value.uri is defined %} +describe file('/etc/sensu-server/conf.d/handlers/{{ key }}.json') do + its(:content) { should contain("{{ value.uri }}") } +end +{% endif %} +{% endif %} +{% endif %} +{% endfor %} +{% for key,value in sensu.server.handlers.hijack|dictsort %} +{% if value|length > 0 %} + +describe file('/etc/sensu-server/conf.d/handlers/{{ key }}_hijack.json') do + it { should be_file } +end +{% endif %} +{% endfor %} diff --git a/roles/sensu-server/templates/serverspec/sensu-server_spec.rb b/roles/sensu-server/templates/serverspec/sensu-server_spec.rb new file mode 100644 index 0000000..557660e --- /dev/null +++ b/roles/sensu-server/templates/serverspec/sensu-server_spec.rb @@ -0,0 +1,27 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('uchiwa') do + it { should be_installed } +end + +describe service('uchiwa') do + it { should be_enabled } +end + +describe service('sensu-api') do + it { should be_enabled } +end + +describe service('sensu-server') do + it { should be_enabled } +end + +describe port("{{ sensu.api.port }}") do + it { should be_listening } +end + +describe port("{{ sensu.dashboard.port }}") do + it { should be_listening } +end diff --git a/roles/serverspec/defaults/main.yml b/roles/serverspec/defaults/main.yml new file mode 100644 index 0000000..bafcfb1 --- /dev/null +++ b/roles/serverspec/defaults/main.yml @@ -0,0 +1,11 @@ +--- +serverspec: + enabled: True + version: 2.37.1 + version_locked_gems: + - name: net-ssh + version: 2.9.4 + - name: specinfra + - name: rspec-support + - name: rspec-core + - name: rspec diff --git a/roles/serverspec/handlers/main.yml b/roles/serverspec/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/serverspec/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/serverspec/meta/main.yml b/roles/serverspec/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/serverspec/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/serverspec/tasks/checks.yml b/roles/serverspec/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/serverspec/tasks/main.yml b/roles/serverspec/tasks/main.yml new file mode 100644 index 0000000..4015402 --- /dev/null +++ b/roles/serverspec/tasks/main.yml @@ -0,0 +1,44 @@ +--- +- name: install version-locked gems + gem: name="{{ item.name }}" version="{{ item.version | default(omit) }}" + user_install=no + with_items: "{{ serverspec.version_locked_gems }}" + register: result + until: result|succeeded + retries: 5 + +- name: install serverspec gem + gem: name=serverspec version="{{ serverspec.version }}" + user_install=no + register: result + until: result|succeeded + retries: 5 + +- name: install serverspec-extended-types gem + gem: name=serverspec-extended-types version=0.0.3 + user_install=no + register: result + until: result|succeeded + retries: 5 + +- name: ensure serverspec directory exists + file: dest=/etc/serverspec/spec/localhost state=directory + owner=root mode=0755 recurse=true + +- name: serverspec fixtures + file: dest=/etc/serverspec/spec/fixtures state=directory + owner=root mode=0755 + +- name: serverspec rakefile + template: src=etc/serverspec/Rakefile + dest=/etc/serverspec/Rakefile mode=0755 + +- name: serverspec helper + template: src=etc/serverspec/spec/spec_helper.rb + dest=/etc/serverspec/spec/spec_helper.rb mode=0755 + +- name: monitoring common serverspec checks + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/etc/serverspec/spec/localhost/* diff --git a/roles/serverspec/tasks/metrics.yml b/roles/serverspec/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/serverspec/tasks/serverspec.yml b/roles/serverspec/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/serverspec/templates/etc/serverspec/Rakefile b/roles/serverspec/templates/etc/serverspec/Rakefile new file mode 100644 index 0000000..3ef66a3 --- /dev/null +++ b/roles/serverspec/templates/etc/serverspec/Rakefile @@ -0,0 +1,29 @@ +# {{ ansible_managed }} + +require 'rake' +require 'rspec/core/rake_task' + +task :spec => 'spec:all' +task :default => :spec + +namespace :spec do + targets = [] + Dir.glob('./spec/*').each do |dir| + next unless File.directory?(dir) + target = File.basename(dir) + target = "_#{target}" if target == "default" + targets << target + end + + task :all => targets + task :default => :all + + targets.each do |target| + original_target = target == "_default" ? target[1..-1] : target + desc "Run serverspec tests to #{original_target}" + RSpec::Core::RakeTask.new(target.to_sym) do |t| + ENV['TARGET_HOST'] = original_target + t.pattern = "spec/#{original_target}/*_spec.rb" + end + end +end diff --git a/roles/serverspec/templates/etc/serverspec/spec/spec_helper.rb b/roles/serverspec/templates/etc/serverspec/spec/spec_helper.rb new file mode 100644 index 0000000..600e6d6 --- /dev/null +++ b/roles/serverspec/templates/etc/serverspec/spec/spec_helper.rb @@ -0,0 +1,6 @@ +# {{ ansible_managed }} + +require 'serverspec' +require 'serverspec_extended_types' + +set :backend, :exec diff --git a/roles/source-install/defaults/main.yml b/roles/source-install/defaults/main.yml new file mode 100644 index 0000000..9519fc2 --- /dev/null +++ b/roles/source-install/defaults/main.yml @@ -0,0 +1,13 @@ +--- +source_install: + name: source-install + virtualenv: {} + # EXAMPLE + # path: /opt/venv/ + # requirements: /opt/git//master/requirements.txt + # extra_index: http://pypi-mirror/private/index/simple + git: [] + system_deps: [] + pip_packages: [] + alternatives: [] + cleanup: [] diff --git a/roles/source-install/tasks/checks.yml b/roles/source-install/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/source-install/tasks/main.yml b/roles/source-install/tasks/main.yml new file mode 100644 index 0000000..3c94118 --- /dev/null +++ b/roles/source-install/tasks/main.yml @@ -0,0 +1,84 @@ +--- +- name: "{{ source_install.name }} system deps" + apt: + pkg: "{{ item }}" + with_items: "{{ source_install.system_deps }}" + +- name: "create {{ source_install.name }} git repo path" + file: + dest: "{{ item.path }}" + state: directory + owner: "{{ item.owner|default('root') }}" + with_items: "{{ source_install.git }}" + +- name: "fetch {{ source_install.name }} repo" + git: + repo: "{{ item.repo }}" + dest: "{{ item.path }}" + version: "{{ item.rev }}" + update: yes + force: yes + accept_hostkey: True + with_items: "{{ source_install.git }}" + +- name: "symlink for {{ source_install.name }} git repo" + file: + dest: "{{ item.symlink }}" + src: "{{ item.path }}" + state: link + when: item.symlink is defined + with_items: "{{ source_install.git }}" + +- name: "create {{ source_install.name }} virtualenvs" + file: + dest: "{{ item.path }}" + state: directory + owner: "{{ item.owner|default('root') }}" + when: item.path is defined + with_items: "{{ source_install.virtualenvs }}" + +- name: "create dedicated virtualenv for {{ source_install.name }} for pip packages" + command: virtualenv --no-site-packages --no-wheel "{{ item.path }}" + args: + creates: "{{ item.path }}/bin/activate" + when: item.path is defined + with_items: "{{ source_install.virtualenvs }}" + +- name: "install {{ source_install.name }} requirements" + pip: + requirements: "{{ item.requirements }}" + virtualenv: "{{ item.path }}" + when: item.path is defined and item.requirements is defined + with_items: "{{ source_install.virtualenvs }}" + +- name: "install {{ source_install.name }} pips with venvs" + pip: + name: "{{ item }}" + virtualenv: "{{ source_install.pip_virtualenv | default(omit) }}" + extra_args: "--extra-index-url {{ source_install.pip_extra_index | default(omit) }}" + with_items: "{{ source_install.pip_packages }}" + +- name: "{{ source_install.name }} update-alternatives" + alternatives: + name: "{{ item.name }}" + path: "{{ item.path }}" + link: "{{ item.link }}" + with_items: "{{ source_install.alternatives }}" + +- name: "cleanup {{ source_install.name }}" + file: + dest: "{{ item }}" + state: absent + with_items: "{{ source_install.cleanup }}" + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/source-install/tasks/metrics.yml b/roles/source-install/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/source-install/tasks/serverspec.yml b/roles/source-install/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/squid/README.md b/roles/squid/README.md new file mode 100644 index 0000000..eec2bfc --- /dev/null +++ b/roles/squid/README.md @@ -0,0 +1,7 @@ +### To purge a cached asset from Squid + +- On Squid Host: +`squidclient -m PURGE http://URL.of.Site/ABC.txt` + +- From remote host: +`http_proxy=http://proxy.$DC.bbg:3128 curl -X PURGE http://URL.of.Site/ABC.txt` diff --git a/roles/squid/defaults/main.yml b/roles/squid/defaults/main.yml new file mode 100644 index 0000000..9eb7511 --- /dev/null +++ b/roles/squid/defaults/main.yml @@ -0,0 +1,32 @@ +--- +squid: + offline_mode: false + port: 3128 + path: + home: /etc/squid3 + log: /var/log/squid3 + cache: /data/squid3 + allowed_networks: + - all + proxy_domains: [] + blacklist_packages: [] + default_deny: True + cache_dir: + size: 40000 + upstream_proxy: [] + # - host: proxy.openstack.bbg + # port: 3128 + # type: parent or sibling + config: + debug_options: + - ALL,1 + cache_mem: 200 MB + max_obj_size: 512 MB + max_obj_size_in_mem: 10240 KB + read_ahead_gap: 128 KB + safe_ports_only: True + negative_ttl: 600 seconds + firewall: + - port: 3128 + protocol: tcp + src: 0.0.0.0/0 diff --git a/roles/squid/handlers/main.yml b/roles/squid/handlers/main.yml new file mode 100644 index 0000000..d3d43ef --- /dev/null +++ b/roles/squid/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: reload squid + service: name=squid3 state=restarted diff --git a/roles/squid/meta/main.yml b/roles/squid/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/squid/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/squid/tasks/checks.yml b/roles/squid/tasks/checks.yml new file mode 100644 index 0000000..69ef43c --- /dev/null +++ b/roles/squid/tasks/checks.yml @@ -0,0 +1,5 @@ +--- +# SQD012 +- name: install squid process check + sensu_check_dict: name="check-squid-process" check="{{ sensu_checks.squid.check_squid_process }}" + notify: restart sensu-client missing ok diff --git a/roles/squid/tasks/main.yml b/roles/squid/tasks/main.yml new file mode 100644 index 0000000..6900008 --- /dev/null +++ b/roles/squid/tasks/main.yml @@ -0,0 +1,110 @@ +--- +- name: install squid packages + apt: + pkg: "{{ item }}" + with_items: + - squid + - calamaris + +- name: set fact for squid config dir + set_fact: + squid_config_dir: /etc/squid3 + squid_service_name: squid3 + when: ansible_distribution_version == "14.04" + +- name: set fact for squid config dir + set_fact: + squid_config_dir: /etc/squid + squid_service_name: squid + when: ansible_distribution_version != "14.04" + +- name: squid configuration + template: src=etc/squid3/squid.conf + dest=/etc/squid3/squid.conf + notify: + - reload squid + +- name: create squid directories + file: + dest: "{{ item }}" + state: directory + recurse: true + owner: proxy + group: proxy + mode: 0755 + with_items: + - "{{ squid.path.home }}" + - "{{ squid.path.log }}" + - "{{ squid.path.cache }}" + +- name: check if squid dirs exist + stat: path="{{ squid.path.cache }}/00" + register: squidcache + +- block: + - name: stop squid to build cache dirs + service: + name: "{{ squid_service_name }}" + state: stopped + + - name: build squid cache dirs + command: /usr/sbin/{{ squid_service_name }} -z + + - name: wait a few seconds for squid to be ready to start + pause: + seconds: 5 + + - name: start squid after building cache dirs + service: + name: "{{ squid_service_name }}" + state: started + enabled: yes + when: squidcache.stat.isdir is not defined + +- name: squid allowed networks acl + template: src=etc/squid3/allowed-networks-src.acl + dest=/etc/squid3/allowed-networks-src.acl + notify: + - reload squid + +- name: squid allowed domains acl + template: src=etc/squid3/allowed-domains-dst.acl + dest=/etc/squid3/allowed-domains-dst.acl + notify: + - reload squid + +- name: remove antiquated mirror domain acl + file: path=/etc/squid3/mirror-dstdomain.acl + state=absent + +- name: squid blacklist package acl + template: src=etc/squid3/pkg-blacklist-regexp.acl + dest=/etc/squid3/pkg-blacklist-regexp.acl + notify: + - reload squid + +- meta: flush_handlers + +- name: ensure squid is running + service: name=squid3 state=started enabled=yes + +- name: allow squid traffic + ufw: rule=allow + to_port={{ item.port }} + src={{ item.src }} + proto={{ item.protocol }} + with_items: "{{ squid.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/squid/tasks/metrics.yml b/roles/squid/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/squid/tasks/serverspec.yml b/roles/squid/tasks/serverspec.yml new file mode 100644 index 0000000..3853b36 --- /dev/null +++ b/roles/squid/tasks/serverspec.yml @@ -0,0 +1,5 @@ +- name: serverspec tests squid tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/squid/templates/etc/squid3/allowed-domains-dst.acl b/roles/squid/templates/etc/squid3/allowed-domains-dst.acl new file mode 100644 index 0000000..122140c --- /dev/null +++ b/roles/squid/templates/etc/squid3/allowed-domains-dst.acl @@ -0,0 +1,29 @@ +# {{ ansible_managed }} + +# allowed-domains-dst.acl +# +# network destinations that are allowed by this cache + +# domains from squid.proxy_domains +{% for domain in squid.proxy_domains %} +{{ domain }} +{% endfor %} + +# default ubuntu and ubuntu country archive mirrors +.archive.ubuntu.com +ports.ubuntu.com +security.ubuntu.com +ddebs.ubuntu.com +mirrors.ubuntu.com +keyserver.ubuntu.com + +#official third party repositories +.archive.canonical.com +extras.ubuntu.com + +# default changelogs location, this includes changelogs and the meta-release +# file that has information about new ubuntu versions +changelogs.ubuntu.com + +# additional destinations can be added to the directory: +# /etc/squid3/mirror-dstdomain.acl.d diff --git a/roles/squid/templates/etc/squid3/allowed-networks-src.acl b/roles/squid/templates/etc/squid3/allowed-networks-src.acl new file mode 100644 index 0000000..3c75002 --- /dev/null +++ b/roles/squid/templates/etc/squid3/allowed-networks-src.acl @@ -0,0 +1,13 @@ +# {{ ansible_managed }} + +# allowed-networks-src.acl +# +# network sources that you want to allow access to the cache + +# private networks +{% for network in squid.allowed_networks %} +{{ network }} +{% endfor %} + +# additional non-private networks can be added to the directory: +# /etc/squid3/allowed-networks-src.acl.d diff --git a/roles/squid/templates/etc/squid3/pkg-blacklist-regexp.acl b/roles/squid/templates/etc/squid3/pkg-blacklist-regexp.acl new file mode 100644 index 0000000..7d9a666 --- /dev/null +++ b/roles/squid/templates/etc/squid3/pkg-blacklist-regexp.acl @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +# /etc/squid/pkg-blacklist-regexp.acl +# +# packages that should be not allowed for download, one binary packagename +# per line +# + +{% for package in squid.blacklist_packages %} +{{ package }} +{% endfor %} diff --git a/roles/squid/templates/etc/squid3/squid.conf b/roles/squid/templates/etc/squid3/squid.conf new file mode 100644 index 0000000..bb46473 --- /dev/null +++ b/roles/squid/templates/etc/squid3/squid.conf @@ -0,0 +1,127 @@ +# {{ ansible_managed }} + +# WELCOME TO SQUID PROXY +# ---------------------- +# +# This config file is a version of a squid proxy file optimized +# as a configuration for a caching proxy for IBM Bluemix Private Cloud systems. +# +# More information about squid and its configuration can be found here +# http://www.squid-cache.org/ and in the FAQ + +# settings that you may want to customize +# --------------------------------------- + +{% if squid.offline_mode|bool %} +offline_mode on +{% endif %} +debug_options {{ squid.config.debug_options|join(' ') }} + +acl allowed_networks src "{{ squid.path.home }}/allowed-networks-src.acl" + +acl bbg dstdomain .bbg .blueboxgrid.com .openstack.bbg .openstack.blueboxgrid.com +{% if squid.proxy_domains|count > 0 -%} +acl allowed_domains dstdomain "{{ squid.path.home }}/allowed-domains-dst.acl" +{% endif -%} + +# this contains the package blacklist +acl blockedpkgs urlpath_regex "{{ squid.path.home }}/pkg-blacklist-regexp.acl" + +http_port {{ squid.port }} + +# ------------------------------------------------- +# settings below probably do not need customization + +server_persistent_connections on +retry_on_error on +reload_into_ims on +pipeline_prefetch on +read_ahead_gap {{ squid.config.read_ahead_gap }} + +# cache 404s, 503s, etc. +negative_ttl {{ squid.config.negative_ttl }} + +# user visible name +visible_hostname {{ ansible_nodename }} + +{% if squid.upstream_proxy|count > 0 -%} +# use upstream proxy cache +{% for proxy in squid.upstream_proxy %} +cache_peer {{ proxy.host }} {{ proxy.type|default("parent") }} {{ proxy.port }} {{ proxy.udp_port|default("0") }} no-query no-digest no-netdb-exchange +{% endfor -%} +always_direct allow bbg +always_direct deny all +never_direct allow allowed_domains +never_direct deny all +{% endif -%} + +cache_access_log {{ squid.path.log }}/access.log +cache_log {{ squid.path.log }}/cache.log +cache_store_log {{ squid.path.log }}/store.log + +cache_effective_user proxy +cache_effective_group proxy + +# tweaks to speed things up +max_stale 10 years +cache_mem {{ squid.config.cache_mem }} + +# we need a big cache, some debs are huge +maximum_object_size {{ squid.config.max_obj_size }} +maximum_object_size_in_memory {{ squid.config.max_obj_size_in_mem }} + +cache_dir aufs {{ squid.path.cache }} {{ squid.cache_dir.size }} 16 256 + +# pid +pid_filename /var/run/squid3.pid + +# refresh pattern for packages +# 10080 = 1 week, 129600 = 3 months, 525950 minutes = 1 year +# refresh_pattern +refresh_pattern \.(bz2|gz|xz|deb|ddeb|udeb)$ 129600 100% 525950 ignore-auth ignore-no-store ignore-private # apt-mirror packages +refresh_pattern \.(deb|gz|img|iso|squashfs|tgz|zip)$ 129600 100% 525950 ignore-auth ignore-no-store ignore-private # file-mirror +refresh_pattern \.(gem|rz)$ 129600 100% 525950 ignore-auth ignore-no-store ignore-private # gem-mirror +refresh_pattern \.(bz2|egg|gz|whl|tgz|tar.gz|zip)$ 129600 100% 525950 ignore-auth ignore-no-store ignore-private # pypi-mirror +refresh_pattern \.(bz2|gz|rpm|srpm|xml)$ 129600 100% 525950 ignore-auth ignore-no-store ignore-private # yum-mirror +refresh_pattern -i \/(dependencies|\?) 10080 100% 525950 ignore-auth ignore-no-store ignore-private # gem-mirror dynamic content +refresh_pattern -i \/pypi\/\+simple 10080 100% 525950 ignore-auth ignore-no-store ignore-private # pypi-mirror dynamic content + +# usually refresh Packages and Release files (file indexes) +# 24 hours = 1440 minutes +refresh_pattern \/keys\/ 1440 20% 10080 ignore-auth ignore-no-store ignore-private refresh-ims +refresh_pattern \/Release(|\.gpg)$ 1440 20% 10080 ignore-auth ignore-no-store ignore-private refresh-ims +refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 1440 20% 10080 ignore-auth ignore-no-store ignore-private refresh-ims +refresh_pattern \/(Translation-.*)(|\.lzma|\.bz2|\.gz|\.xz)$ 1440 20% 10080 ignore-auth ignore-no-store ignore-private refresh-ims +refresh_pattern \/InRelease$ 1440 20% 10080 ignore-auth ignore-no-store ignore-private refresh-ims + +# handle meta-release and changelogs.ubuntu.com special +refresh_pattern changelogs.ubuntu.com/* 0 01% 1 + +# cache all responses by default for 24 hours +refresh_pattern . 0 20% 1440 + +acl Purge method PURGE +http_access allow localhost Purge +http_access allow allowed_networks Purge +http_access deny Purge + +{% if squid.config.safe_ports_only|bool -%} +# only allow connects to ports for http, https +acl Safe_ports port 80 443 563 11371 + +# only allow ports we trust +http_access deny !Safe_ports +{% endif -%} + +# do not allow to download from the pkg blacklist +http_access deny blockedpkgs + +# allow access from our network and localhost +http_access allow allowed_networks + +# allow access to listed domains +http_access allow bbg +http_access allow allowed_domains + +# And finally deny all other access to this proxy +http_access deny all diff --git a/roles/squid/templates/serverspec/squid_spec.rb b/roles/squid/templates/serverspec/squid_spec.rb new file mode 100644 index 0000000..03b8a7f --- /dev/null +++ b/roles/squid/templates/serverspec/squid_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe package('squid') do + it { should be_installed.by('apt') } #SQD001 +end + +describe file('/etc/squid3/squid.conf') do + it { should be_file } #SQD002 +end + +describe file('/usr/sbin/squid3') do + it { should be_file } #SQD003 +end + +describe file('{{ squid.path.cache }}') do + it { should be_mode 755 } #SQD004 + it { should be_owned_by 'proxy' } #SQD005 + it { should be_grouped_into 'proxy' } #SQD006 + it { should be_directory } #SQD007 +end + +describe file('/etc/squid3/allowed-networks-src.acl') do + it { should be_file } #SQD008 +end + +describe file('/etc/squid3/allowed-domains-dst.acl') do + it { should be_file } #SQD009 +end + +describe file('/etc/squid3/pkg-blacklist-regexp.acl') do + it { should be_file } #SQD010 +end + +describe port('{{ squid.port }}') do + it { should be_listening } #SQD011 +end + +describe service('squid3') do + it { should be_enabled } #SQD012 +end + +{% for network in squid.allowed_networks %} +describe file('/etc/squid3/allowed-networks-src.acl') do + it(:content) { should contain('{{ network }}').after('# private networks') } #SQD013 +end +{% endfor %} + +{% for domain in squid.proxy_domains %} +describe file('/etc/squid3/allowed-domains-dst.acl') do + it(:content) { should contain('{{ domain }}').after('# domains from squid.proxy_domains') } #SQD014 +end +{% endfor %} diff --git a/roles/sshagentmux/defaults/main.yml b/roles/sshagentmux/defaults/main.yml new file mode 100644 index 0000000..f2a7647 --- /dev/null +++ b/roles/sshagentmux/defaults/main.yml @@ -0,0 +1,16 @@ +--- +sshagentmux: + enabled: false + auth_db: /root/authorization_proxy.db + auth_socket: /var/run/authorization_proxy.sock + virtualenv_path: /opt/venv/sshagentmux + logs: + - paths: + - /var/log/authorization_proxy.log + fields: + tags: audit,auth,sshagentmux + +users_to_add: {} +user_groups: {} +sorted_authorizations: {} +sorted_identities: {} diff --git a/roles/sshagentmux/handlers/main.yml b/roles/sshagentmux/handlers/main.yml new file mode 100644 index 0000000..74ef6c0 --- /dev/null +++ b/roles/sshagentmux/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart authorization_proxy + action: service name=authorization_proxy state=restarted + +- name: reload authorization_proxy + action: service name=authorization_proxy state=reloaded diff --git a/roles/sshagentmux/meta/main.yml b/roles/sshagentmux/meta/main.yml new file mode 100644 index 0000000..2f446a6 --- /dev/null +++ b/roles/sshagentmux/meta/main.yml @@ -0,0 +1,8 @@ +--- +dependencies: + - role: git-repos + - role: logging-config + service: sshagentmux + logdata: "{{ sshagentmux.logs }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/sshagentmux/tasks/authorizations.yml b/roles/sshagentmux/tasks/authorizations.yml new file mode 100644 index 0000000..5ab4a45 --- /dev/null +++ b/roles/sshagentmux/tasks/authorizations.yml @@ -0,0 +1,25 @@ +--- +- set_fact: + current_user: "{{ item }}" + +- name: determine existing users + command: sqlite3 {{ sshagentmux.auth_db }} + "SELECT username, GROUP_CONCAT(identity_id) FROM authorizations + GROUP BY username ORDER BY username" + register: existing_authorizations + +# combine() below creates a dict with the username as the key with associated identity_ids as a list for value +- name: sort existing users + set_fact: + sorted_authorizations: "{{ sorted_authorizations|default({}) | combine({item.split('|')[0]: item.split('|')[1].split(',')}) }}" + with_items: "{{ existing_authorizations.stdout_lines }}" + +- name: add users to authorizations + command: sqlite3 {{ sshagentmux.auth_db }} + "INSERT INTO authorizations + VALUES ('{{ current_user.key }}', (SELECT rowid FROM identities WHERE name='{{ item }}'));" + when: user_groups[item] is defined and + user_groups[item].ssh_keys is defined and + (current_user.key not in sorted_authorizations or sorted_identities[item]['rowid'] not in sorted_authorizations[current_user.key]) + with_items: "{{ (current_user.value.groups | default([])) | union([current_user.value.primary_group]) | unique }}" + notify: reload authorization_proxy diff --git a/roles/sshagentmux/tasks/checks.yml b/roles/sshagentmux/tasks/checks.yml new file mode 100644 index 0000000..93124e1 --- /dev/null +++ b/roles/sshagentmux/tasks/checks.yml @@ -0,0 +1,5 @@ +--- +# SAM009 +- name: install authorization_proxy process check + sensu_check_dict: name="check-authorization_proxy-process" check="{{ sensu_checks.sshauthmux.check_authorization_proxy_process }}" + notify: restart sensu-client missing ok diff --git a/roles/sshagentmux/tasks/main.yml b/roles/sshagentmux/tasks/main.yml new file mode 100644 index 0000000..05f661c --- /dev/null +++ b/roles/sshagentmux/tasks/main.yml @@ -0,0 +1,47 @@ +--- +- name: install system packages + apt: pkg={{ item }} + with_items: + - sqlite3 + +- name: install sshagentmux + pip: name=sshagentmux virtualenv="{{ sshagentmux.virtualenv_path }}" + +- name: update-alternatives + alternatives: name={{ item }} + path={{ sshagentmux.virtualenv_path }}/bin/{{ item }} + link=/usr/local/bin/{{ item }} + with_items: + - authorization_proxy + - sshagentmux + +- name: configure authorization_proxy service + template: src=etc/init/authorization_proxy.conf + dest=/etc/init/authorization_proxy.conf + mode=0644 + notify: restart authorization_proxy + +- name: configure authorization_proxy profile.d + template: src=etc/profile.d/authorization_proxy.sh + dest=/etc/profile.d/authorization_proxy.sh + mode=0755 + +- meta: flush_handlers + +- name: start and enable authorization_proxy service + service: name=authorization_proxy state=started enabled=yes + +- include: ssh_keys.yml + tags: users + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/sshagentmux/tasks/metrics.yml b/roles/sshagentmux/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/sshagentmux/tasks/serverspec.yml b/roles/sshagentmux/tasks/serverspec.yml new file mode 100644 index 0000000..b4c14d0 --- /dev/null +++ b/roles/sshagentmux/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests sshagentmux tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/sshagentmux/tasks/ssh_agent.yml b/roles/sshagentmux/tasks/ssh_agent.yml new file mode 100644 index 0000000..fa9a0eb --- /dev/null +++ b/roles/sshagentmux/tasks/ssh_agent.yml @@ -0,0 +1,14 @@ +--- +- set_fact: + current_group: "{{ item }}" + +- name: ask for passphrase for ssh key + pause: + prompt: "enter passphrase for {{ current_group.key }} ssh key" + register: passphrase + when: current_group.value.ssh_keys is defined and + current_group.value.ssh_keys.enable_passphrase|default('False')|bool + +- name: add ssh keys to authorization_proxy ssh agent session + shell: echo {{ passphrase.user_input|default("") }} | SSH_AUTH_SOCK={{ sshagentmux.auth_socket }} ssh-add /root/.ssh/{{ current_group.key }}-id_rsa + when: current_group.value.ssh_keys is defined diff --git a/roles/sshagentmux/tasks/ssh_keys.yml b/roles/sshagentmux/tasks/ssh_keys.yml new file mode 100644 index 0000000..e618f9f --- /dev/null +++ b/roles/sshagentmux/tasks/ssh_keys.yml @@ -0,0 +1,52 @@ +--- +- name: "create /root/.ssh path" + file: + dest: /root/.ssh + state: directory + owner: root + +- name: write public keys to system + copy: + content: "{{ item.value.ssh_keys.public }}" + dest: /root/.ssh/{{ item.key }}-id_rsa.pub + when: item.value.ssh_keys is defined and + item.value.ssh_keys.private is defined and + item.value.ssh_keys.public is defined + with_dict: "{{ user_groups }}" + +- name: write private keys to system + copy: + content: "{{ item.value.ssh_keys.private }}" + dest: /root/.ssh/{{ item.key }}-id_rsa + mode: 0400 + when: item.value.ssh_keys is defined and + item.value.ssh_keys.private is defined + with_dict: "{{ user_groups }}" + +- include: ssh_agent.yml + with_dict: "{{ user_groups }}" + tags: ['ssh-agent'] + +- name: determine existing keys + command: sqlite3 {{ sshagentmux.auth_db }} + "SELECT name, rowid, key_digest FROM identities + ORDER BY name" + register: existing_identities + +- name: sort existing keys + set_fact: + sorted_identities: "{{ sorted_identities|default({}) | combine({item.split('|')[0]: {'rowid': item.split('|')[1], 'key_digest': item.split('|')[2]}}) }}" + with_items: "{{ existing_identities.stdout_lines }}" + +- name: add ssh key fingerprints to identities + command: sqlite3 {{ sshagentmux.auth_db }} + "INSERT OR REPLACE INTO identities + VALUES ('{{ item.value.ssh_keys.fingerprint }}', '{{ item.key }}');" + when: item.value.ssh_keys is defined and + item.value.ssh_keys.fingerprint is defined and + (item.key not in sorted_identities or sorted_identities[item.key]['key_digest'] != item.value.ssh_keys.fingerprint) + with_dict: "{{ user_groups }}" + notify: reload authorization_proxy + +- include: authorizations.yml + with_dict: "{{ users_to_add }}" diff --git a/roles/sshagentmux/templates/etc/init/authorization_proxy.conf b/roles/sshagentmux/templates/etc/init/authorization_proxy.conf new file mode 100644 index 0000000..d43a402 --- /dev/null +++ b/roles/sshagentmux/templates/etc/init/authorization_proxy.conf @@ -0,0 +1,15 @@ +# {{ ansible_managed }} + +description "SSH Authorization Proxy" +author "Dustin Lundquist" + +start on runlevel [2345] +stop on runlevel [!2345] + +expect daemon +respawn + +setuid root +setgid root + +exec /usr/local/bin/authorization_proxy --socket {{ sshagentmux.auth_socket }} --logfile /var/log/authorization_proxy.log --pidfile /var/run/authorization_proxy.pid start diff --git a/roles/sshagentmux/templates/etc/profile.d/authorization_proxy.sh b/roles/sshagentmux/templates/etc/profile.d/authorization_proxy.sh new file mode 100644 index 0000000..cfe064c --- /dev/null +++ b/roles/sshagentmux/templates/etc/profile.d/authorization_proxy.sh @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +# Apparently in .profile and .rc context the parent pid changes when using backticks and eval, so work around it by dumping environment to a new file and souring it +AGENT_ENV=$(mktemp) +/usr/local/bin/sshagentmux --socket {{ sshagentmux.auth_socket }} > ${AGENT_ENV} +. ${AGENT_ENV} +rm ${AGENT_ENV} diff --git a/roles/sshagentmux/templates/serverspec/sshagentmux_spec.rb b/roles/sshagentmux/templates/serverspec/sshagentmux_spec.rb new file mode 100644 index 0000000..86818ab --- /dev/null +++ b/roles/sshagentmux/templates/serverspec/sshagentmux_spec.rb @@ -0,0 +1,19 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('sqlite3') do + it { should be_installed } #SAM001 +end + +describe file('{{ sshagentmux.virtualenv_path }}') do + it { should be_directory } #SAM002 +end + +describe file('/etc/init/authorization_proxy.conf') do + it { should be_mode 644} #SAM005 +end + +describe service('authorization_proxy') do + it { should be_enabled } +end diff --git a/roles/support-tools/defaults/main.yml b/roles/support-tools/defaults/main.yml new file mode 100644 index 0000000..4312ea0 --- /dev/null +++ b/roles/support-tools/defaults/main.yml @@ -0,0 +1,45 @@ +--- +support_tools: + enabled: false + system_deps: + - libffi-dev + - libxml2-dev + - libxslt-dev + - libssl-dev + cron: + minute: '0' + hour: '*' + + git: [] +# EXAMPLE +# - name: sitecontroller_envs +# path: /opt/git/sitecontroller-envs/master +# owner: "{{ admin_user }}" +# repo: "{{ git_repos.sitecontroller_envs }}" +# rev: master +# auto_update: true + + virtualenvs: [] +# EXAMPLE +# - name: ursula-master +# path: /opt/venv/ursula/master +# owner: "{{ admin_user }}" +# requirements: /opt/git/ursula/master/requirements.txt + + alternatives: [] +# EXAMPLE +# - name: sitecontroller +# path: /opt/venv/sitecontroller/master/bin/ursula +# link: /usr/local/bin/sitecontroller + + pip_packages: [] +# EXAMPLE +# - name: ursula-cli +# path: /opt/venv/ursula-cli/master +# location: "https://pypi-mirror.openstack.blueboxgrid.com/bluebox/openstack" +# owner: "{{ admin_user }}" +# private: False + + cleanup: [] +# EXAMPLE +# - /opt/git/ursula-infra diff --git a/roles/support-tools/meta/main.yml b/roles/support-tools/meta/main.yml new file mode 100644 index 0000000..8664307 --- /dev/null +++ b/roles/support-tools/meta/main.yml @@ -0,0 +1,12 @@ +--- +dependencies: + - role: git-repos + - role: source-install + source_install: + name: support-tools + git: "{{ support_tools.git }}" + system_deps: "{{ support_tools.system_deps }}" + virtualenvs: "{{ support_tools.virtualenvs }}" + pip_packages: "{{ support_tools.pip_packages }}" + alternatives: "{{ support_tools.alternatives }}" + cleanup: "{{ support_tools.cleanup }}" diff --git a/roles/support-tools/tasks/checks.yml b/roles/support-tools/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/support-tools/tasks/main.yml b/roles/support-tools/tasks/main.yml new file mode 100644 index 0000000..233978b --- /dev/null +++ b/roles/support-tools/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: create git_pull binary + template: + src: git_pull + dest: /usr/sbin/git_pull + owner: root + group: root + mode: 0700 + +- name: allow blueboxadmin to run git_pull + template: + src: git_pull.sudoers + dest: /etc/sudoers.d/git_pull + owner: root + group: root + mode: 0700 + +- name: set up cronjob to run git_pull hourly + cron: + name: git_pull + minute: "{{ support_tools.cron.minute }}" + hour: "{{ support_tools.cron.hour }}" + cron_file: "ansible_git_pull_{{ item.name }}" + job: "/usr/sbin/git_pull {{ item.name }} 2>&1 | logger -t git_pull" + user: root + with_items: "{{ support_tools.git }}" + when: item.auto_update|default('false')|bool + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/support-tools/tasks/metrics.yml b/roles/support-tools/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/support-tools/tasks/serverspec.yml b/roles/support-tools/tasks/serverspec.yml new file mode 100644 index 0000000..787adfc --- /dev/null +++ b/roles/support-tools/tasks/serverspec.yml @@ -0,0 +1,7 @@ +--- +- name: install support-tools serverspec tests + template: + src: "{{ item }}" + dest: /etc/serverspec/spec/localhost/ + mode: 0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/support-tools/templates/git_pull b/roles/support-tools/templates/git_pull new file mode 100644 index 0000000..e2feed3 --- /dev/null +++ b/roles/support-tools/templates/git_pull @@ -0,0 +1,37 @@ +#!/bin/bash + +# {{ ansible_managed }} + +# lulz what is this? you cna't source a socket. +# source {{ sshagentmux.auth_socket }} + +declare -a errors + +case "$1" in +{% for repo in support_tools.git %} + {{ repo.name }}) + echo Updating {{ repo.path }} to {{ repo.rev }} + cd {{ repo.path }} + git pull origin {{ repo.rev }} + (( $? )) && errors+=( {{ repo.name }} ) + ;; +{% endfor %} + all) +{% for repo in support_tools.git %} + echo Updating {{ repo.path }} to {{ repo.rev }} + cd {{ repo.path }} + git pull origin {{ repo.rev }} + (( $? )) && errors+=( {{ repo.name }} ) +{% endfor %} + ;; + *) + errors+=( "$1" ) + echo "Usage: git_pull repo_name" + echo "valid repo_names: all {% for repo in support_tools.git %}{{ repo.name }} {% endfor %}" +esac + +if [ {{ "${#errors[@]}" }} -ne 0 ]; then + echo '{"name": "git_pull", "ttl": 7200, "output": "git_pull failed for repo(s): '${errors[@]}'", "status": 1}' | nc localhost 3030 +else + echo '{"name": "git_pull", "ttl": 7200, "output": "git_pull successful", "status": 0}' | nc localhost 3030 +fi diff --git a/roles/support-tools/templates/git_pull.sudoers b/roles/support-tools/templates/git_pull.sudoers new file mode 100644 index 0000000..a2d092d --- /dev/null +++ b/roles/support-tools/templates/git_pull.sudoers @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +blueboxadmin ALL=NOPASSWD: /usr/sbin/git_pull diff --git a/roles/support-tools/templates/serverspec/support-tools_spec.rb b/roles/support-tools/templates/serverspec/support-tools_spec.rb new file mode 100644 index 0000000..b1c4890 --- /dev/null +++ b/roles/support-tools/templates/serverspec/support-tools_spec.rb @@ -0,0 +1,31 @@ +# {{ ansible_managed }} +require 'spec_helper' + +{% for item in support_tools.git %} +describe file('{{ item.path }}') do + it { should be_owned_by '{{ item.owner|default(admin_user) }}' } #SPT001 + it { should be_directory } #SPT002 +end +{% endfor %} + +{% for venv in support_tools.virtualenvs %} +describe file('{{ venv.path }}') do + it { should be_owned_by '{{ venv.owner }}' } #SPT003 + it { should be_directory } #SPT004 +end +{% endfor %} + +describe file('/usr/sbin/git_pull') do + it { should be_mode 700 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_file } +end + +describe file('/etc/sudoers.d/git_pull') do + it { should be_mode 700 } #SPT005 + it { should be_owned_by 'root' } #SPT006 + it { should be_grouped_into 'root' } #SPT007 + it { should be_file } #SPT008 + its(:content) { should contain('blueboxadmin ALL=NOPASSWD: /usr/sbin/git_pull') } #SPT009 +end diff --git a/roles/ttyspy-client/handlers/main.yml b/roles/ttyspy-client/handlers/main.yml new file mode 100644 index 0000000..1897e2c --- /dev/null +++ b/roles/ttyspy-client/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart ttyspyd + service: + name: ttyspyd + state: restarted diff --git a/roles/ttyspy-client/meta/main.yml b/roles/ttyspy-client/meta/main.yml new file mode 100644 index 0000000..cd60dc2 --- /dev/null +++ b/roles/ttyspy-client/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: ttyspy-common + - role: sensu-check diff --git a/roles/ttyspy-client/tasks/checks.yml b/roles/ttyspy-client/tasks/checks.yml new file mode 100644 index 0000000..f66bd8d --- /dev/null +++ b/roles/ttyspy-client/tasks/checks.yml @@ -0,0 +1,5 @@ +--- +- name: install ttyspyd process check + sensu_check_dict: name="check-ttyspyd-process" + check="{{ sensu_checks.ttyspyd.check_ttyspyd_process }}" + notify: restart sensu-client missing ok diff --git a/roles/ttyspy-client/tasks/main.yml b/roles/ttyspy-client/tasks/main.yml new file mode 100644 index 0000000..0a3c316 --- /dev/null +++ b/roles/ttyspy-client/tasks/main.yml @@ -0,0 +1,62 @@ +--- +- name: install ttyspy client + apt: + deb: "{{ ttyspy.client.package }}" + +- name: configure ttyspy + template: + src: etc/ttyspy.conf + dest: /etc/ttyspy.conf + owner: ttyspy + group: ttyspy + mode: 0644 + +- name: ttyspy client directory + file: + path: /etc/ttyspy/client + owner: ttyspy + group: ttyspy + state: directory + mode: 0750 + +- name: install client certs & key + template: + src: "{{ item }}" + dest: /etc/ttyspy/client + owner: ttyspy + group: ttyspy + mode: 0640 + with_fileglob: ../templates/etc/ttyspy/client/* + +# TLS doesn't play nicely with IPs +- name: configure ttyspy server IP & hostname in /etc/hosts + lineinfile: + dest: "/etc/hosts" + line: "{{ ttyspy.server.ip }} {{ ttyspy.server.host }}" + +- name: configure ttyspyd service + template: + src: etc/init/ttyspyd.conf + dest: /etc/init/ttyspyd.conf + mode: 0644 + notify: restart ttyspyd + +- meta: flush_handlers + +- name: start ttyspyd service + service: + name: ttyspyd + state: started + enabled: yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/ttyspy-client/tasks/metrics.yml b/roles/ttyspy-client/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/ttyspy-client/tasks/serverspec.yml b/roles/ttyspy-client/tasks/serverspec.yml new file mode 100644 index 0000000..2177950 --- /dev/null +++ b/roles/ttyspy-client/tasks/serverspec.yml @@ -0,0 +1,7 @@ +--- +- name: install ttyspy-client serverspec tests + template: + src: "{{ item }}" + dest: /etc/serverspec/spec/localhost/ + mode: 0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/ttyspy-client/templates/etc/init/ttyspyd.conf b/roles/ttyspy-client/templates/etc/init/ttyspyd.conf new file mode 100644 index 0000000..bf86a11 --- /dev/null +++ b/roles/ttyspy-client/templates/etc/init/ttyspyd.conf @@ -0,0 +1,12 @@ +# {{ ansible_managed }} + +description "ttyspyd" +author "Michael Sambol" + +start on runlevel [2345] +stop on runlevel [!2345] + +setuid root +setgid root + +exec /usr/sbin/ttyspyd -f diff --git a/roles/ttyspy-client/templates/etc/ttyspy.conf b/roles/ttyspy-client/templates/etc/ttyspy.conf new file mode 100644 index 0000000..fb54af0 --- /dev/null +++ b/roles/ttyspy-client/templates/etc/ttyspy.conf @@ -0,0 +1,8 @@ +# {{ ansible_managed }} + +endpoint https://{{ ttyspy.server.host }}:{{ ttyspy.server.port }}/transcript +ca_path /etc/ttyspy/client/ca.pem +cert_path /etc/ttyspy/client/cert.pem +key_path /etc/ttyspy/client/key.pem +socket /var/run/ttyspy.sock +username ttyspy diff --git a/roles/ttyspy-client/templates/etc/ttyspy/client/ca.pem b/roles/ttyspy-client/templates/etc/ttyspy/client/ca.pem new file mode 100644 index 0000000..de173e4 --- /dev/null +++ b/roles/ttyspy-client/templates/etc/ttyspy/client/ca.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ ttyspy.common.ssl.ca_cert }} diff --git a/roles/ttyspy-client/templates/etc/ttyspy/client/cert.pem b/roles/ttyspy-client/templates/etc/ttyspy/client/cert.pem new file mode 100644 index 0000000..d5c3fce --- /dev/null +++ b/roles/ttyspy-client/templates/etc/ttyspy/client/cert.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ ttyspy.client.ssl.cert }} diff --git a/roles/ttyspy-client/templates/etc/ttyspy/client/key.pem b/roles/ttyspy-client/templates/etc/ttyspy/client/key.pem new file mode 100644 index 0000000..2cd42ea --- /dev/null +++ b/roles/ttyspy-client/templates/etc/ttyspy/client/key.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ ttyspy.client.ssl.key }} diff --git a/roles/ttyspy-client/templates/serverspec/ttyspy-client_spec.rb b/roles/ttyspy-client/templates/serverspec/ttyspy-client_spec.rb new file mode 100644 index 0000000..d6dd8d3 --- /dev/null +++ b/roles/ttyspy-client/templates/serverspec/ttyspy-client_spec.rb @@ -0,0 +1,31 @@ +# {{ ansible_managed }} +require 'spec_helper' + +describe package('ttyspy-client') do + it { should be_installed } +end + +describe service('ttyspyd') do + it { should be_enabled } +end + +describe file('/etc/ttyspy.conf') do + it { should be_mode 644 } + it { should be_owned_by 'ttyspy' } + it { should be_grouped_into 'ttyspy' } + it { should be_file } +end + +describe file('/etc/ttyspy/client') do + it { should be_mode 750 } + it { should be_owned_by 'ttyspy' } + it { should be_grouped_into 'ttyspy' } + it { should be_directory } +end + +describe file('/etc/init/ttyspyd.conf') do + it { should be_mode 644 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_file } +end diff --git a/roles/ttyspy-common/defaults/main.yml b/roles/ttyspy-common/defaults/main.yml new file mode 100644 index 0000000..9908ece --- /dev/null +++ b/roles/ttyspy-common/defaults/main.yml @@ -0,0 +1,29 @@ +--- +ttyspy: + common: + dependencies: + - autoconf + - libcurl4-openssl-dev + ssl: + ca_cert: ~ + + client: + enabled: false + package: https://github.com/IBM/ttyspy/releases/download/packages/ttyspy-client_0.0.3_amd64.deb + ssl: + cert: ~ + key: ~ + + server: + enabled: false + host: server.test + ip: 127.0.0.1 + port: 8090 + transcript_path: /opt/ttyspy-server/transcripts + transcript_glob: /transcripts/*/*/*/*/*/transcript_* + firewall: + friendly_networks: + - 0.0.0.0/0 + ssl: + cert: ~ + key: ~ diff --git a/roles/ttyspy-common/meta/main.yml b/roles/ttyspy-common/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/ttyspy-common/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/ttyspy-common/tasks/checks.yml b/roles/ttyspy-common/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/ttyspy-common/tasks/main.yml b/roles/ttyspy-common/tasks/main.yml new file mode 100644 index 0000000..07f97f6 --- /dev/null +++ b/roles/ttyspy-common/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: install dependencies + apt: + pkg: "{{ item }}" + with_items: "{{ ttyspy.common.dependencies }}" + +- name: ttyspy group + group: + name: ttyspy + state: present + +- name: ttyspy user + user: + name: ttyspy + groups: ttyspy + home: /nonexistent + shell: /bin/false + system: true + comment: ttyspy + +- name: ttyspy directory + file: + path: /etc/ttyspy + owner: ttyspy + group: ttyspy + state: directory + mode: 0755 + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/ttyspy-common/tasks/metrics.yml b/roles/ttyspy-common/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/ttyspy-common/tasks/serverspec.yml b/roles/ttyspy-common/tasks/serverspec.yml new file mode 100644 index 0000000..8c3c0a0 --- /dev/null +++ b/roles/ttyspy-common/tasks/serverspec.yml @@ -0,0 +1,7 @@ +--- +- name: install ttyspy-common serverspec tests + template: + src: "{{ item }}" + dest: /etc/serverspec/spec/localhost/ + mode: 0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/ttyspy-common/templates/serverspec/ttyspy-common_spec.rb b/roles/ttyspy-common/templates/serverspec/ttyspy-common_spec.rb new file mode 100644 index 0000000..1884aba --- /dev/null +++ b/roles/ttyspy-common/templates/serverspec/ttyspy-common_spec.rb @@ -0,0 +1,16 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +{% for pkg in ttyspy.common.dependencies %} +describe package('{{ pkg }}') do + it { should be_installed } +end +{% endfor %} + +describe file('/etc/ttyspy') do + it { should be_mode 755 } + it { should be_owned_by 'ttyspy' } + it { should be_grouped_into 'ttyspy' } + it { should be_directory } +end diff --git a/roles/ttyspy-server/handlers/main.yml b/roles/ttyspy-server/handlers/main.yml new file mode 100644 index 0000000..aa87e25 --- /dev/null +++ b/roles/ttyspy-server/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart ttyspy-receiver + service: + name: ttyspy-receiver + state: restarted diff --git a/roles/ttyspy-server/meta/main.yml b/roles/ttyspy-server/meta/main.yml new file mode 100644 index 0000000..cd60dc2 --- /dev/null +++ b/roles/ttyspy-server/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: ttyspy-common + - role: sensu-check diff --git a/roles/ttyspy-server/tasks/checks.yml b/roles/ttyspy-server/tasks/checks.yml new file mode 100644 index 0000000..4cf9553 --- /dev/null +++ b/roles/ttyspy-server/tasks/checks.yml @@ -0,0 +1,5 @@ +--- +- name: install ttyspy-receiver process check + sensu_check_dict: name="check-ttyspy-receiver-process" + check="{{ sensu_checks.ttyspy_server.check_ttyspy_receiver_process }}" + notify: restart sensu-client missing ok diff --git a/roles/ttyspy-server/tasks/compression.yml b/roles/ttyspy-server/tasks/compression.yml new file mode 100644 index 0000000..1563915 --- /dev/null +++ b/roles/ttyspy-server/tasks/compression.yml @@ -0,0 +1,16 @@ +--- +- name: python script to compress transcripts + template: + src: etc/ttyspy/compression.py + dest: /etc/ttyspy/compression.py + owner: root + group: root + mode: 0744 + +- name: cron to call python script daily + template: + src: etc/cron.daily/ttyspy_compression + dest: /etc/cron.daily/ttyspy_compression + owner: root + group: root + mode: 0755 diff --git a/roles/ttyspy-server/tasks/main.yml b/roles/ttyspy-server/tasks/main.yml new file mode 100644 index 0000000..34c8b1b --- /dev/null +++ b/roles/ttyspy-server/tasks/main.yml @@ -0,0 +1,73 @@ +--- +- name: install ttyspy server + apt: + deb: "{{ ttyspy.server.package }}" + +- name: ttyspy transcript directory + file: + path: "{{ ttyspy.server.transcript_path }}" + owner: ttyspy + group: ttyspy + state: directory + mode: 0755 + +- name: ttyspy server directory + file: + path: /etc/ttyspy/server + owner: ttyspy + group: ttyspy + state: directory + mode: 0750 + +- name: install server certs & key + template: + src: "{{ item }}" + dest: /etc/ttyspy/server + owner: ttyspy + group: ttyspy + mode: 0640 + with_fileglob: ../templates/etc/ttyspy/server/* + +- name: allow ttyspy traffic + ufw: + rule: allow + to_port: "{{ ttyspy.server.port }}" + src: "{{ item }}" + proto: tcp + with_items: "{{ ttyspy.server.firewall.friendly_networks }}" + tags: + - firewall + +- name: ttyspy-receiver service + upstart_service: + name: ttyspy-receiver + cmd: /usr/bin/ttyspy_receiver + args: "-ca /etc/ttyspy/server/ca.pem + -cert /etc/ttyspy/server/cert.pem + -key /etc/ttyspy/server/key.pem + -store {{ ttyspy.server.transcript_path }}" + user: ttyspy + notify: restart ttyspy-receiver + +- meta: flush_handlers + +- name: start ttyspy-receiver service + service: + name: ttyspy-receiver + state: started + enabled: yes + +- include: compression.yml + tags: ttyspy-compression + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/ttyspy-server/tasks/metrics.yml b/roles/ttyspy-server/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/ttyspy-server/tasks/serverspec.yml b/roles/ttyspy-server/tasks/serverspec.yml new file mode 100644 index 0000000..012fc36 --- /dev/null +++ b/roles/ttyspy-server/tasks/serverspec.yml @@ -0,0 +1,7 @@ +--- +- name: install ttyspy-server serverspec tests + template: + src: "{{ item }}" + dest: /etc/serverspec/spec/localhost/ + mode: 0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/ttyspy-server/templates/etc/cron.daily/ttyspy_compression b/roles/ttyspy-server/templates/etc/cron.daily/ttyspy_compression new file mode 100644 index 0000000..6855e3f --- /dev/null +++ b/roles/ttyspy-server/templates/etc/cron.daily/ttyspy_compression @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +/usr/bin/python /etc/ttyspy/compression.py diff --git a/roles/ttyspy-server/templates/etc/ttyspy/compression.py b/roles/ttyspy-server/templates/etc/ttyspy/compression.py new file mode 100644 index 0000000..08787f3 --- /dev/null +++ b/roles/ttyspy-server/templates/etc/ttyspy/compression.py @@ -0,0 +1,10 @@ +# {{ ansible_managed }} + +import os +import glob + +transcripts = glob.glob('{{ ttyspy.server.transcript_glob }}') + +for transcript in transcripts: + if ".xz" not in transcript: + os.system('xz ' + transcript) diff --git a/roles/ttyspy-server/templates/etc/ttyspy/server/ca.pem b/roles/ttyspy-server/templates/etc/ttyspy/server/ca.pem new file mode 100644 index 0000000..de173e4 --- /dev/null +++ b/roles/ttyspy-server/templates/etc/ttyspy/server/ca.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ ttyspy.common.ssl.ca_cert }} diff --git a/roles/ttyspy-server/templates/etc/ttyspy/server/cert.pem b/roles/ttyspy-server/templates/etc/ttyspy/server/cert.pem new file mode 100644 index 0000000..b18fb17 --- /dev/null +++ b/roles/ttyspy-server/templates/etc/ttyspy/server/cert.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ ttyspy.server.ssl.cert }} diff --git a/roles/ttyspy-server/templates/etc/ttyspy/server/key.pem b/roles/ttyspy-server/templates/etc/ttyspy/server/key.pem new file mode 100644 index 0000000..b1b6461 --- /dev/null +++ b/roles/ttyspy-server/templates/etc/ttyspy/server/key.pem @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ ttyspy.server.ssl.key }} diff --git a/roles/ttyspy-server/templates/serverspec/ttyspy-server_spec.rb b/roles/ttyspy-server/templates/serverspec/ttyspy-server_spec.rb new file mode 100644 index 0000000..53bcdbb --- /dev/null +++ b/roles/ttyspy-server/templates/serverspec/ttyspy-server_spec.rb @@ -0,0 +1,43 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('ttyspy-server') do + it { should be_installed } +end + +describe service('ttyspy-receiver') do + it { should be_enabled } +end + +describe file('{{ ttyspy.server.transcript_path }}') do + it { should be_mode 755 } + it { should be_owned_by 'ttyspy' } + it { should be_grouped_into 'ttyspy' } + it { should be_directory } +end + +describe file('/etc/ttyspy/server') do + it { should be_mode 750 } + it { should be_owned_by 'ttyspy' } + it { should be_grouped_into 'ttyspy' } + it { should be_directory } +end + +describe port('{{ ttyspy.server.port }}') do + it { should be_listening } +end + +describe file('/etc/ttyspy/compression.py') do + it { should be_mode 744 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_file } +end + +describe file('/etc/cron.daily/ttyspy_compression') do + it { should be_mode 755 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_file } +end diff --git a/roles/users/defaults/main.yml b/roles/users/defaults/main.yml new file mode 100644 index 0000000..3d24a5d --- /dev/null +++ b/roles/users/defaults/main.yml @@ -0,0 +1,54 @@ +--- +users: {} + + # EXAMPLE + # + # blueboxadmin: + # bastion_user: false + # primary_group: blueboxadmin + # groups: + # - admin + # home: /home/blueboxadmin + # createhome: yes + # shell: /bin/bash + # public_keys: + # - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA4rAIuN7EoPdU8iDPnp27zd+hXsbTE1NEIAQFblG0IywG2B522pivpxE/v1BmtaIVas1APRFDsn5SMGrDOiVNZGz/MdIdJOPjza29WyXgb5w9I329I/XKF5/NEkXDajqzHQheHZ0NSQFFqrlW+N7t6KdKkFP0heAnOLtXJIXrJso04Ew/o/NX6qJFvDY8pVMUeQVloX5zFuHwq+N2JjJIEDS89mmNfqThoAR0KZ/jKQnjNhCdKVurS20Sxft4HI6Zjm7YZMXJO5a+TL+nYEq+JEzLL+PdKcBf4BVpr6MLO/R3d5nxGAtdhgXUSvEDT2bCFWc66KBzNtJTzDKcVn2KcQ== blueboxadmin@yama-1.blueboxgrid.com + # + # exampleuser: + # bastion_user: true + # primary_group: sitecontroller + # home: /home/bastionuser + # createhome: yes + # shell: /bin/bash + # uid: 1999 + # yubikey: + # public_id: 111 + # serial_number: 111 + # aes_key: 111 + # private_id: 111 + # public_keys: + # - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA4rAIuN7EoPdU8iDPnp27zd+hXsbTE1NEIAQFblG0IywG2B522pivpxE/v1BmtaIVas1APRFDsn5SMGrDOiVNZGz/MdIdJOPjza29WyXgb5w9I329I/XKF5/NEkXDajqzHQheHZ0NSQFFqrlW+N7t6KdKkFP0heAnOLtXJIXrJso04Ew/o/NX6qJFvDY8pVMUeQVloX5zFuHwq+N2JjJIEDS89mmNfqThoAR0KZ/jKQnjNhCdKVurS20Sxft4HI6Zjm7YZMXJO5a+TL+nYEq+JEzLL+PdKcBf4BVpr6MLO/R3d5nxGAtdhgXUSvEDT2bCFWc66KBzNtJTzDKcVn2KcQ== blueboxadmin@yama-1.blueboxgrid.com + +user_groups: {} + + # EXAMPLE + # + # admin: + # system: yes + # blueboxadmin: + # system: yes + # ssh_keys: + # enable_passphrase: false + # fingerprint: ~ + # private: ~ + # public: ~ + # sitecontroller: + # system: no + # ssh_keys: + # enable_passphrase: false + # fingerprint: ~ + # private: ~ + # public: ~ + +_users: + manage_authorized_keys: False diff --git a/roles/users/handlers/main.yml b/roles/users/handlers/main.yml new file mode 100644 index 0000000..9d6619d --- /dev/null +++ b/roles/users/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: place authorized keys checksums + shell: sha256sum ~{{ item.key }}/.ssh/authorized_keys | cut -d' ' -f 1 > /etc/serverspec/spec/fixtures/{{ item.key }}_keys.checksum + with_dict: "{{ users_to_add }}" + when: serverspec.enabled|default("True")|bool diff --git a/roles/users/tasks/main.yml b/roles/users/tasks/main.yml new file mode 100644 index 0000000..46ff6e4 --- /dev/null +++ b/roles/users/tasks/main.yml @@ -0,0 +1,74 @@ +--- +# needed when not running playbooks/add-bastion-users.yml +- name: set users_to_add when not defined + set_fact: + users_to_add: "{{ users }}" + when: users_to_add is not defined + +- name: create groups + group: + name: "{{ item.key }}" + system: "{{ item.value.system | default(omit) }}" + state: present + with_dict: "{{ user_groups }}" + +- name: create users + user: + name: "{{ item.key }}" + group: "{{ item.value.primary_group | default(omit) }}" + groups: "{{ item.value.groups | default() | join(',') or omit }}" + home: "{{ item.value.home | default(omit) }}" + createhome: "{{ item.value.createhome | default(omit) }}" + uid: "{{ item.value.uid | default(omit) }}" + shell: "{{ item.value.shell | default('/bin/bash') }}" + comment: "{{ item.value.comment | default(omit) }}" + with_dict: "{{ users_to_add }}" + +- block: + + - name: create root-managed authorized keys directory + file: + path: /etc/ssh/authorized_keys + owner: root + group: root + state: directory + mode: 0755 + + - name: add root-managed authorized keys + template: + src: authorized_keys + dest: "/etc/ssh/authorized_keys/{{ item.key }}.keys" + owner: root + group: root + mode: 0644 + with_dict: "{{ users_to_add }}" + + when: _users.manage_authorized_keys|bool + +- block: + + - name: ensure user has .ssh path + file: + path: "~{{ item.key }}/.ssh" + owner: "{{ item.key }}" + group: "{{ item.key }}" + state: directory + mode: 0700 + with_dict: "{{ users_to_add }}" + + - name: add ssh authorized keys + template: + src: authorized_keys + dest: "~{{ item.key }}/.ssh/authorized_keys" + owner: root + group: "{{ item.key }}" + mode: 0644 + with_dict: "{{ users_to_add }}" + notify: + - place authorized keys checksums + + when: not _users.manage_authorized_keys|bool + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/users/tasks/serverspec.yml b/roles/users/tasks/serverspec.yml new file mode 100644 index 0000000..b91d447 --- /dev/null +++ b/roles/users/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for users role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/users/templates/authorized_keys b/roles/users/templates/authorized_keys new file mode 100644 index 0000000..3834da9 --- /dev/null +++ b/roles/users/templates/authorized_keys @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +{% for key in item.value.public_keys|sort %} +{{ key }} +{% endfor %} diff --git a/roles/users/templates/serverspec/users_spec.rb b/roles/users/templates/serverspec/users_spec.rb new file mode 100644 index 0000000..5b1d627 --- /dev/null +++ b/roles/users/templates/serverspec/users_spec.rb @@ -0,0 +1,20 @@ +# {{ ansible_managed }} + +require 'spec_helper' +require 'etc' + +{% if not _users.manage_authorized_keys %} +{% for key, value in users.iteritems() %} +describe file(File.join(Etc.getpwnam("{{ key }}").dir, '.ssh/authorized_keys')) do + its(:sha256sum) { should eq File.read('/etc/serverspec/spec/fixtures/{{ key }}_keys.checksum').strip } #OPS055 +end +{% endfor %} +{% else %} +{% for key, value in users.iteritems() %} +describe file('/etc/ssh/authorized_keys/{{ key }}.keys') do + it { should be_owned_by 'root' } + it { should be_grouped_into {{ key }} } + it { should be_mode 640 } +end +{% endfor %} +{% endif %} diff --git a/roles/varnish/defaults/main.yml b/roles/varnish/defaults/main.yml new file mode 100644 index 0000000..4fa1365 --- /dev/null +++ b/roles/varnish/defaults/main.yml @@ -0,0 +1,9 @@ +--- +varnish: + host: "127.0.0.1" + port: 8080 + enabled: True + backends: + - name: default + host: "127.0.0.1" + port: 9292 diff --git a/roles/varnish/handlers/main.yml b/roles/varnish/handlers/main.yml new file mode 100644 index 0000000..5c7854d --- /dev/null +++ b/roles/varnish/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart varnish + service: name=varnish state=restarted diff --git a/roles/varnish/meta/main.yml b/roles/varnish/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/varnish/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/varnish/tasks/checks.yml b/roles/varnish/tasks/checks.yml new file mode 100644 index 0000000..d08a02b --- /dev/null +++ b/roles/varnish/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install varnish process check + sensu_check_dict: name="check-varnish-process" check="{{ sensu_checks.varnish.check_varnish_process }}" + notify: restart sensu-client missing ok diff --git a/roles/varnish/tasks/main.yml b/roles/varnish/tasks/main.yml new file mode 100644 index 0000000..c926f77 --- /dev/null +++ b/roles/varnish/tasks/main.yml @@ -0,0 +1,32 @@ +--- +- name: install varnish + apt: pkg={{ item }} + with_items: + - varnish + +- name: configure varnish defaults + template: src=etc/default/varnish dest=/etc/default/varnish + owner=root mode=0644 + notify: restart varnish + +- name: configure varnish + template: src=etc/varnish/default.vcl dest=/etc/varnish/default.vcl + owner=root mode=0644 + notify: restart varnish + +- meta: flush_handlers + +- name: ensure varnish is running + service: name=varnish state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/varnish/tasks/metrics.yml b/roles/varnish/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/varnish/tasks/serverspec.yml b/roles/varnish/tasks/serverspec.yml new file mode 100644 index 0000000..cb2821c --- /dev/null +++ b/roles/varnish/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for varnish role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/varnish/templates/etc/default/varnish b/roles/varnish/templates/etc/default/varnish new file mode 100644 index 0000000..7cff15d --- /dev/null +++ b/roles/varnish/templates/etc/default/varnish @@ -0,0 +1,8 @@ +START=yes +NFILES=131072 +MEMLOCK=82000 +DAEMON_OPTS="-a {{ varnish.host }}:{{ varnish.port }} \ + -T localhost:6082 \ + -f /etc/varnish/default.vcl \ + -S /etc/varnish/secret \ + -s malloc,256m" diff --git a/roles/varnish/templates/etc/varnish/default.vcl b/roles/varnish/templates/etc/varnish/default.vcl new file mode 100644 index 0000000..693dc6e --- /dev/null +++ b/roles/varnish/templates/etc/varnish/default.vcl @@ -0,0 +1,21 @@ +{% for backend in varnish.backends %} +backend {{ backend.name }} { + .host = "{{ backend.host }}"; + .port = "{{ backend.port }}"; +} +{% endfor %} + +sub vcl_fetch { + if (beresp.ttl <= 0s || + beresp.http.Set-Cookie || + beresp.http.Vary == "*") { + /* + * Mark as "Hit-For-Pass" for the next 10s + */ + set beresp.ttl = 10 s; + return (hit_for_pass); + } + set beresp.ttl = 6h; + set beresp.grace = 12h; + return (deliver); +} diff --git a/roles/varnish/templates/serverspec/varnish_spec.yml b/roles/varnish/templates/serverspec/varnish_spec.yml new file mode 100644 index 0000000..24e4a11 --- /dev/null +++ b/roles/varnish/templates/serverspec/varnish_spec.yml @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('varnish') do + it { should be_installed } +end + +describe service('varnish') do + it { should be_enabled } +end diff --git a/roles/vault/defaults/main.yml b/roles/vault/defaults/main.yml new file mode 100644 index 0000000..17a4291 --- /dev/null +++ b/roles/vault/defaults/main.yml @@ -0,0 +1,24 @@ +--- +vault: + version: 0.5.2 + download: + url: https://releases.hashicorp.com/vault/0.5.2/vault_0.5.2_linux_amd64.zip + sha256sum: 7517b21d2c709e661914fbae1f6bf3622d9347b0fe9fc3334d78a01d1e1b4ec2 + bind_interface: lo + bind_port: 8200 + bin_path: /opt/vault/bin + archive_path: /opt/vault/archives + config_path: /opt/vault/etc + config_file: /opt/vault/etc/vault.hcl + enabled: True + backend: + store: consul + scheme: http + telemetry: + enabled: False + statsd_address: 127.0.0.1 + statsd_port: 8125 + tls: + enabled: False + cert_file: '/etc/ssl/certs/vault.crt' + key_file: '/etc/ssl/private/vault.key' diff --git a/roles/vault/handlers/main.yml b/roles/vault/handlers/main.yml new file mode 100644 index 0000000..769e575 --- /dev/null +++ b/roles/vault/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart vault server + service: name=vault state=restarted + +- name: stop vault server + service: name=vault state=stopped diff --git a/roles/vault/meta/main.yml b/roles/vault/meta/main.yml new file mode 100644 index 0000000..3d679a6 --- /dev/null +++ b/roles/vault/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: consul diff --git a/roles/vault/tasks/main.yml b/roles/vault/tasks/main.yml new file mode 100644 index 0000000..40124c8 --- /dev/null +++ b/roles/vault/tasks/main.yml @@ -0,0 +1,36 @@ +--- +- name: make vault directories + file: dest={{ item }} state=directory owner=root + with_items: + - "{{ vault.bin_path }}" + - "{{ vault.archive_path }}" + - "{{ vault.config_path }}" + +- name: download vault + get_url: url={{ vault.download.url }} + dest="{{ vault.archive_path }}/vault-{{ vault.version }}.zip" + sha256sum={{ vault.download.sha256sum }} + +- stat: path={{ vault.bin_path }}/vault + register: vault_binary + +- name: unzip vault binary + unarchive: src="{{ vault.archive_path }}/vault-{{ vault.version }}.zip" dest={{ vault.bin_path }} copy=no + when: vault_binary.stat.exists == False + +- name: link vault binary to path + file: src={{ vault.bin_path }}/vault dest=/usr/local/bin/vault state=link + +- name: configure vault server + template: src=etc/vault.hcl dest={{ vault.config_file }} + notify: restart vault server + +- name: vault service + upstart_service: name=vault + cmd={{ vault.bin_path }}/vault + args="server -config={{ vault.config_file }}" + user=root + notify: restart vault server + +- name: start vault service + service: name=vault state=started enabled=yes diff --git a/roles/vault/templates/etc/vault.hcl b/roles/vault/templates/etc/vault.hcl new file mode 100644 index 0000000..ab9b9d9 --- /dev/null +++ b/roles/vault/templates/etc/vault.hcl @@ -0,0 +1,21 @@ +backend "{{ vault.backend.store }}" { + address = "{{ vault.backend.address|default("127.0.0.1") }}:{{ vault.backend.port|default("8500") }}" + path = "{{ vault.backend.path|default("vault") }}" +} + +listener "tcp" { + address = "{{ hostvars[inventory_hostname]['ansible_' + vault.bind_interface].ipv4.address }}:{{ vault.bind_port }}" + {% if vault.tls.enabled %} + tls_disable = 0 + tls_cert_file = "{{ vault.tls.cert_file }}" + tls_key_file = "{{ vault.tls.key_file }}" + {% else %} + tls_disable = 1 + {% endif %} +} + +{% if vault.telemetry.enabled %} +telemetry { + statsd_address = "{{ statsd_address }}:{{ statsd_port }}" +} +{% endif %} diff --git a/roles/yum_mirror/defaults/main.yml b/roles/yum_mirror/defaults/main.yml new file mode 100644 index 0000000..5aa53b9 --- /dev/null +++ b/roles/yum_mirror/defaults/main.yml @@ -0,0 +1,58 @@ +--- +yum_mirror: + enabled: True + pull_files: False + cron: 0 6 * * * + apache: + http_redirect: False + servername: mirror01.local + serveraliases: + - mirror01 + port: 80 + ip: '*' + ssl: + enabled: False + port: 443 + ip: '*' + name: sitecontroller + cert: ~ + key: ~ + intermediate: ~ + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + - port: 443 + protocol: tcp + src: 0.0.0.0/0 + path: /opt/yum-mirror + htpasswd_location: /opt/yum-mirror/etc + repositories: {} + # EXAMPLE + # elastic: + # description: elastic + # enabled: True + # url: http://sensu.global.ssl.fastly.net/yum/$basearch/ + # key_url: https://artifacts.elastic.co/GPG-KEY-elasticsearch + # archs: + # - arch: x86_64 + # - arch: i686 + # sensu: + # description: sensu + # enabled: True + # gpgcheck: False + # archs: + # - arch: x86_64 + # url: http://sensu.global.ssl.fastly.net/yum/el/7/x86_64/ + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/apache2/yum_mirror-access.log + fields: + tags: mirror,apache_access + - paths: + - /var/log/apache2/yum_mirror-error.log + fields: + tags: mirror,apache_error + logging: + forwarder: filebeat diff --git a/roles/yum_mirror/meta/main.yml b/roles/yum_mirror/meta/main.yml new file mode 100644 index 0000000..59a2805 --- /dev/null +++ b/roles/yum_mirror/meta/main.yml @@ -0,0 +1,17 @@ +--- +dependencies: + - role: bbg-ssl + name: "{{ yum_mirror.apache.ssl.name }}" + ssl_cert: "{{ yum_mirror.apache.ssl.cert }}" + ssl_key: "{{ yum_mirror.apache.ssl.key }}" + ssl_intermediate: "{{ yum_mirror.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: yum_mirror.apache.ssl.enabled + tags: ['bbg-ssl'] + - role: apache + - role: logging-config + service: yum_mirror + logdata: "{{ yum_mirror.logs }}" + forward_type: "{{ yum_mirror.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/yum_mirror/tasks/checks.yml b/roles/yum_mirror/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/yum_mirror/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/yum_mirror/tasks/main.yml b/roles/yum_mirror/tasks/main.yml new file mode 100644 index 0000000..926c8e5 --- /dev/null +++ b/roles/yum_mirror/tasks/main.yml @@ -0,0 +1,145 @@ +--- +- name: install dependencies + apt: + name: "{{ item }}" + state: present + with_items: + - yum + - createrepo + - yum-utils + +- name: create yum-mirror user + user: + name: yum-mirror + comment: yum-mirror + shell: /bin/false + system: yes + home: "{{ yum_mirror.path }}" + +- name: create yum-mirror repo config directory + file: + dest: "/etc/yum/{{ item }}" + state: directory + owner: yum-mirror + mode: 0755 + recurse: true + with_items: + - repos.d + - repo-manager + +- name: lay down yum.conf + template: + src: "etc/yum/yum.conf" + dest: "/etc/yum/yum.conf" + owner: yum-mirror + +- name: configure yum repos + template: + src: "etc/yum/repos.d/mirror-template" + dest: "/etc/yum/repos.d/{{ item.key }}.repo" + owner: yum-mirror + with_dict: "{{ yum_mirror.repositories }}" + +- name: sync and create repo shell script + template: + src: "etc/yum/repo-manager/repo-manager-template.sh" + dest: "/etc/yum/repo-manager/{{ item.key }}.sh" + owner: yum-mirror + mode: 0755 + with_dict: "{{ yum_mirror.repositories }}" + +- name: repo manager script cron jobs + template: + src: "etc/cron.d/yum_mirror" + dest: "/etc/cron.d/yum_mirror" + +- name: keys path + file: + dest: "{{ yum_mirror.path }}/keys" + state: directory + mode: 0755 + owner: yum-mirror + tags: yum-mirror-keys + +- name: download repo keys + get_url: + url: "{{ item.value.key_url }}" + dest: "{{ yum_mirror.path }}/keys/{{ item.key }}.key" + mode: 0644 + when: item.value.key_url is defined + with_dict: "{{ yum_mirror.repositories }}" + tags: yum-mirror-keys + +- name: create yum mirror htpasswd location + file: + name: "{{ yum_mirror.htpasswd_location }}" + state: directory + owner: yum-mirror + +- name: create per repo mirror directory + file: + dest: "{{ yum_mirror.path }}/mirror/{{ item.key }}" + state: directory + mode: 0755 + owner: yum-mirror + with_dict: "{{ yum_mirror.repositories }}" + +- name: create per repo .htpasswd + htpasswd: + name: "{{ item.value.username }}" + password: "{{ item.value.password }}" + path: "{{ yum_mirror.htpasswd_location }}/{{ item.key }}.htpasswd" + with_dict: "{{ yum_mirror.repositories }}" + when: item.value.username is defined and item.value.password is defined + +- name: add main apache vhost + template: + src: "etc/apache2/sites-available/yum_mirror" + dest: "/etc/apache2/sites-available/yum_mirror.conf" + notify: + - restart apache + tags: yum-mirror-apache-config + +- name: enable repo vhost + apache2_site: + state: enabled + name: yum_mirror + notify: + - restart apache + +- meta: flush_handlers + +- name: ensure apache is running + service: + name: apache2 + state: started + enabled: yes + +- name: pull repo files + shell: "/bin/bash /etc/yum/repo-manager/{{ item.key }}.sh" + become: true + become_user: yum-mirror + when: yum_mirror.pull_files and "{{ item.value.enabled }}" + with_dict: "{{ yum_mirror.repositories }}" + +- name: allow yum-mirror traffic + ufw: + rule: allow + to_port: "{{ item.port }}" + src: "{{ item.src }}" + proto: "{{ item.protocol }}" + with_items: "{{ yum_mirror.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/yum_mirror/tasks/metrics.yml b/roles/yum_mirror/tasks/metrics.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/yum_mirror/tasks/metrics.yml @@ -0,0 +1 @@ +--- diff --git a/roles/yum_mirror/tasks/serverspec.yml b/roles/yum_mirror/tasks/serverspec.yml new file mode 100644 index 0000000..202981b --- /dev/null +++ b/roles/yum_mirror/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for yum-mirror role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/yum_mirror/templates/etc/apache2/sites-available/yum_mirror b/roles/yum_mirror/templates/etc/apache2/sites-available/yum_mirror new file mode 100644 index 0000000..e0b505b --- /dev/null +++ b/roles/yum_mirror/templates/etc/apache2/sites-available/yum_mirror @@ -0,0 +1,67 @@ +# {{ ansible_managed }} + +{% macro virtualhost() %} + ServerAdmin openstack@bluebox.net + ServerName {{ yum_mirror.apache.servername }} + ServerAlias {{ yum_mirror.apache.serveraliases|join(" ") }} + DocumentRoot {{ yum_mirror.path }}/mirror + ErrorLog ${APACHE_LOG_DIR}/yum_mirror-error.log + CustomLog ${APACHE_LOG_DIR}/yum_mirror-access.log combined + Alias /keys {{ yum_mirror.path }}/keys + + + Options +Indexes +SymLinksIfOwnerMatch + AllowOverride None + Require all granted + + + + Options Indexes + AllowOverride None + Require all granted + + +{% for key, value in yum_mirror.repositories.iteritems() %} + Alias /{{ key }} {{ yum_mirror.path }}/mirror/{{ key }} + +{% if value.username is defined and value.password is defined %} + Options +Indexes +SymLinksIfOwnerMatch + AuthType Basic + AuthName "Restricted Content" + AuthUserFile {{ yum_mirror.htpasswd_location }}/{{ key }}.htpasswd + Require valid-user +{% else %} + Options +Indexes +SymLinksIfOwnerMatch + Require all granted +{% endif %} + +{% endfor %} +{% endmacro %} + +{% if yum_mirror.apache.ssl.enabled|bool and yum_mirror.apache.http_redirect|bool %} + + ServerName {{ yum_mirror.apache.servername }} + ServerAlias {{ yum_mirror.apache.serveraliases|join(" ") }} + RewriteEngine on + RewriteCond %{HTTPS} !=on + RewriteRule ^(.*)$ https://%{HTTP_HOST}:{{ yum_mirror.apache.ssl.port }}$1 [R=301,L] + +{% else %} + +{{ virtualhost() }} + +{% endif %} + +{% if yum_mirror.apache.ssl.enabled|bool %} + +{{ apache.ssl.settings | indent(4,true) }} + SSLCertificateFile /etc/ssl/certs/{{ yum_mirror.apache.ssl.name|default('sitecontroller') }}.crt + SSLCertificateKeyFile /etc/ssl/private/{{ yum_mirror.apache.ssl.name|default('sitecontroller') }}.key +{% if bbg_ssl.intermediate or yum_mirror.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ yum_mirror.apache.ssl.name|default('sitecontroller') }}-intermediate.crt +{% endif %} +{% else %} + +{% endif %} +{{ virtualhost() }} + diff --git a/roles/yum_mirror/templates/etc/cron.d/yum_mirror b/roles/yum_mirror/templates/etc/cron.d/yum_mirror new file mode 100644 index 0000000..675122c --- /dev/null +++ b/roles/yum_mirror/templates/etc/cron.d/yum_mirror @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +{% for key,value in yum_mirror.repositories|dictsort() %} +# {{ key }} -- yum repo qualifier +{{ value.cron|default(yum_mirror.cron) }} yum-mirror /bin/bash /etc/yum/repo-manager/{{ key }}.sh + +{% endfor %} + + diff --git a/roles/yum_mirror/templates/etc/yum/repo-manager/repo-manager-template.sh b/roles/yum_mirror/templates/etc/yum/repo-manager/repo-manager-template.sh new file mode 100644 index 0000000..8761785 --- /dev/null +++ b/roles/yum_mirror/templates/etc/yum/repo-manager/repo-manager-template.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# {{ ansible_managed }} + +{% for _arch in item.value.archs %} +reposync -c /etc/yum/yum.conf --repoid="{{ item.key }}-{{ (item.value.release|default('el/7')) | replace('/','') }}-{{ _arch.arch }}" --download_path="{{ yum_mirror.path }}/mirror/{{ item.key }}/{{ item.value.release|default('el/7') }}/{{ _arch.basearch|default(_arch.arch) }}" --arch="{{ _arch.arch }}" --norepopath +yum --disablerepo=* --enablerepo="{{ item.key }}-{{ (item.value.release|default('el/7')) | replace('/','') }}-{{ _arch.arch }}" makecache +curl -Lso /var/cache/yum/{{ item.key }}-{{ (item.value.release|default('el/7')) | replace('/','') }}-{{ _arch.arch }}/repomd.xml.asc {{ _arch.url|default(item.value.url) }}/{{ item.value.release|default('el/7') }}/{{ _arch.basearch|default(_arch.arch) }}/repodata/repomd.xml.asc +ln -sfn /var/cache/yum/{{ item.key }}-{{ (item.value.release|default('el/7')) | replace('/','') }}-{{ _arch.arch }} {{ yum_mirror.path }}/mirror/{{ item.key }}/{{ item.value.release|default('el/7') }}/{{ _arch.basearch|default(_arch.arch) }}/repodata + +{% endfor %} diff --git a/roles/yum_mirror/templates/etc/yum/repos.d/mirror-template b/roles/yum_mirror/templates/etc/yum/repos.d/mirror-template new file mode 100644 index 0000000..c69b865 --- /dev/null +++ b/roles/yum_mirror/templates/etc/yum/repos.d/mirror-template @@ -0,0 +1,19 @@ +# {{ ansible_managed }} +{%- macro yum_repo_config(repo, _arch) %} +[{{ repo.key }}-{{ (repo.value.release|default('el/7')) | replace('/','') }}-{{ _arch.arch }}] +name={{ repo.value.description }} - {{ repo.value.release|default('el/7') }} - {{ _arch.arch }} +baseurl={{ _arch.url|default(repo.value.url) }}/{{ repo.value.release|default('el/7') }}/{{ _arch.basearch|default(_arch.arch) }} +enabled={{ repo.value.enabled|default(False)|bool|int }} +{% if repo.value.key_url is defined %} +gpgkey={{ repo.value.key_url }} +{% endif %} +repo_gpgcheck=0 +gpgcheck={{ repo.value.gpgcheck|default(False)|bool|int }} +autorefresh=1 +metadata_expire=300 +{%- endmacro %} + +{% for _arch in item.value.archs %} +{{ yum_repo_config(item, _arch) }} + +{% endfor %} diff --git a/roles/yum_mirror/templates/etc/yum/yum.conf b/roles/yum_mirror/templates/etc/yum/yum.conf new file mode 100644 index 0000000..a8675ef --- /dev/null +++ b/roles/yum_mirror/templates/etc/yum/yum.conf @@ -0,0 +1,10 @@ +# {{ ansible_managed }} + +[main] +cachedir=/var/cache/yum +reposdir=/etc/yum/repos.d +debuglevel=2 +logfile=/var/log/yum/yum.log +gpgcheck=0 +plugins=1 +#keepcache=0 diff --git a/roles/yum_mirror/templates/serverspec/yum_mirror_spec.rb b/roles/yum_mirror/templates/serverspec/yum_mirror_spec.rb new file mode 100644 index 0000000..28441f3 --- /dev/null +++ b/roles/yum_mirror/templates/serverspec/yum_mirror_spec.rb @@ -0,0 +1,63 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('yum') do + it { should be_installed } +end + +describe package('yum-utils') do + it { should be_installed } +end + +describe package('createrepo') do + it { should be_installed } +end + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end + +describe file('/etc/yum/yum.conf') do + it { should be_file } +end + +describe file('/etc/cron.d/yum_mirror') do + it { should be_file } +end + +describe file('{{ yum_mirror.path }}/mirror') do + it { should be_directory } +end + +describe file('{{ yum_mirror.path }}/keys') do + it { should be_directory } +end + +{% for key, value in yum_mirror.repositories.iteritems() %} +{% if value.key_url is defined %} +describe file('{{ yum_mirror.path }}/keys/{{ key }}.key') do + it { should be_file } +end +{% endif %} +{% endfor %} + +describe file('/etc/apache2/sites-available/yum_mirror.conf') do + it { should be_file } +end + +describe file('/etc/apache2/sites-enabled/yum_mirror.conf') do + it { should be_symlink } +end + +describe port('{{ yum_mirror.apache.port }}') do + it { should be_listening } +end + +describe iptables do + it { should have_rule('-p tcp -m tcp --dport {{ yum_mirror.apache.port }} -j ACCEPT') } +end diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..9c85c39 --- /dev/null +++ b/site.yml @@ -0,0 +1,240 @@ +--- +- name: ensure connectivity to all nodes + hosts: all:!vyatta-* + gather_facts: false + pre_tasks: + - name: make sure python is installed + raw: "[[ -e /usr/bin/python ]] || apt-get update && apt-get -y install python" + changed_when: false + tasks: + - action: ping + tags: ['ping'] + - action: setup + tags: ['ping'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + max_fail_percentage: 0 + +- name: sitecontroller base + hosts: all:!vyatta-* + roles: + - role: _sitecontroller + tags: ['sc', 'sitecontroller'] + environment: "{{ env_vars|default({}) }}" + tags: ['always'] + any_errors_fatal: true + +- name: common items + hosts: all:!vyatta-* + roles: + - role: common + tags: ['common'] + - role: manage-disks + tags: ['manage-disks'] + when: manage_disks.enabled|default("False")|bool + - role: dnsmasq + tags: ['dnsmasq'] + when: dnsmasq.enabled|default("False")|bool + - role: sensu-client + tags: ['sensu-client'] + when: sensu.client.enabled|default("True")|bool + - role: logging + tags: ['logging'] + when: logging.enabled|default("True")|bool + - role: collectd + tags: ['collectd'] + when: collectd.enabled|default("False")|bool + - role: docker + tags: ['docker'] + when: docker.enabled|default("False")|bool + - role: netdata + tags: ['netdata'] + when: netdata.enabled|default("False")|bool + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: bastion server + hosts: bastion + serial: 1 + roles: + - role: ttyspy-client + tags: ['ttyspy-client'] + when: ttyspy.client.enabled|default("False")|bool + - role: sshagentmux + tags: ['sshagentmux'] + when: sshagentmux.enabled|default("False")|bool +# - role: support-tools +# tags: ['support-tools'] +# when: support_tools.enabled|default("False")|bool +# - role: bastion +# tags: ['bastion-role'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: ttyspy server + hosts: ttyspy-server + roles: + - role: ttyspy-server + tags: ['ttyspy-server'] + when: ttyspy.server.enabled|default("False")|bool + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: ipsec + hosts: ipsec + roles: + - role: ipsec + tags: ['ipsec'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: squid server + hosts: squid + roles: + - role: squid + tags: ['squid'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: netdata dashboard + hosts: netdata_dashboard + roles: + - role: netdata-dashboard + tags: ['netdata-dashboard'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: mirror server + hosts: mirror + roles: + - role: apt-mirror + tags: ['apt-mirror', 'debmirror'] + when: apt_mirror.enabled|default('True')|bool + - role: pypi-mirror + tags: ['pypi-mirror'] + when: pypi_mirror.enabled|default('True')|bool + - role: gem-mirror + tags: ['gem-mirror'] + when: gem_mirror.enabled|default('True')|bool + - role: file-mirror + tags: ['file-mirror'] + when: file_mirror.enabled|default('True')|bool + - role: yum_mirror + tags: ['yum-mirror'] + when: yum_mirror.enabled|default('True')|bool + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: elk + hosts: elk + roles: + - role: elasticsearch + tags: ['elasticsearch'] + - role: logstash + tags: ['logstash'] + - role: kibana + tags: ['kibana'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: rally benchmark host + hosts: rally + roles: + - rally + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: percona server + hosts: percona + roles: + - role: percona + tags: ['percona'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: monitor server + hosts: monitor + roles: + - role: rabbitmq + tags: ['rabbitmq'] + - role: sensu-server + tags: ['sensu-server'] + when: sensu.server.enabled|default("True")|bool + - role: graphite + tags: ['graphite'] + when: graphite.enabled|default("True")|bool + - role: grafana + tags: ['grafana'] + when: grafana.enabled|default("True")|bool + - role: flapjack + tags: ['flapjack'] + when: flapjack.enabled|default("False")|bool + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: pxe server + hosts: pxe + roles: + - role: pxe + tags: ['pxe'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: consul + hosts: consul + roles: + - role: consul + tags: ['consul'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: vault + hosts: vault + roles: + - role: vault + tags: ['vault'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: openid_proxy + hosts: openid_proxy + roles: + - role: openid_proxy + tags: ['openid-proxy'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: netman server + hosts: netman + roles: + - role: harden + tags: ['harden'] + - role: postfix-simple + tags: ['postfix-simple'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: ipmi proxy server + hosts: ipmi-proxy + roles: + - role: ipmi-proxy + tags: ['ipmi-proxy'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: boxpanel worker queue + hosts: bpw-queue + serial: 1 + roles: + - role: rabbitmq + tags: ['rabbitmq'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + +- name: security + hosts: all:!vyatta-* + roles: + - role: security + tags: ['security', 'always'] + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/vagrant.yml b/vagrant.yml new file mode 100644 index 0000000..dac16f6 --- /dev/null +++ b/vagrant.yml @@ -0,0 +1,50 @@ +default: + memory: 512 + cpus: 1 + +vms: + bastion: + ip_address: 172.16.0.2 + allinone: + ip_address: 172.16.0.10 + memory: 4096 + cpus: 2 + aptly: + ip_address: 172.16.0.12 + rally: + ip_address: 172.16.0.14 + monitor: + ip_address: 172.16.0.15 + squid: + ip_address: 172.16.0.16 + percona: + ip_address: 172.16.0.17 + memory: 1024 + mirror: + ip_address: 172.16.0.18 + pxe: + ip_address: 172.16.0.19 + netman: + ip_address: 172.16.0.20 + +ansible: + groups: + aptly: ["allinone", "aptly"] + squid: ["allinone", "squid"] + graphite: ["allinone", "monitor"] + sensu: ["allinone", "monitor"] + rabbitmq: ["allinone", "monitor"] + percona_primary: ["allinone", "percona"] + percona_secondary: [] + percona_arbiter: [] + percona_backup: ["allinone", "percona"] + percona:children: ["percona_primary", "percona_secondary"] + rally: ["rally"] + elasticsearch: ["elk"] + logstash: ["elk"] + kibana: ["elk"] + mirror: ["pypi_mirror", "apt_mirror", "aptly"] + consul: ["consul", "consul2"] + pxe: ["pxe"] + bastion: ["bastion"] + netman: []

Ly^JGeMVRW;Rrx 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 Ri>p^v?-|1`S4zS&DESeX`J<`SXPc-5Qp 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 SU+#kc6y;SfW?eA3|Nj6XlH|bv literal 0 HcmV?d00001 diff --git a/docs/post_deploy_validation.md b/docs/post_deploy_validation.md new file mode 100644 index 0000000..a129806 --- /dev/null +++ b/docs/post_deploy_validation.md @@ -0,0 +1,48 @@ +# Validating a SoftLayer Remote Site Controller + +##### 1. Run network tests + +``` +$ ursula ../sitecontroller-envs/remote-$DC playbooks/healthcheck.yml +``` + +##### 2. Ensure datacenter is visible in control + +Go to https://control.XXXXX.com/ and verify your datacenter is listed in "Remote Locations". + +##### 3. Can reach PagerDuty + +ssh to `monitor01` and `elk01`, stop `sensu-client`, and ensure central and remote Sensu servers were notified. + +``` +$ ssh -F ../sitecontroller-envs/remote-$DC/ssh_config monitor01 +$ service sensu-client stop + +$ ssh -F ../sitecontroller-envs/remote-$DC/ssh_config elk01 +$ service sensu-client stop +``` +Proceed to https://control.XXXXX.com/ and click Sensu under "Locations" and "Remote Locations". +You should see an alert in both places for the downed clients. Don't forget to start them when you're done. +``` +$ service sensu-client start +``` +Go to https://control.XXXXX.com/flapjack and verify your datacenter is listed in "Entities". + +##### 4. Can reach Grafana + +Go to https://control.XXXXX.com/ and click on the "Grafana" link for your datacenter. +You should see the Grafana dashboard, and Graphite should be listed as a data source. + +##### 5. Can reach Kibana + +Go to https://control.XXXXX.com/ and click on the "Kibana" link for your datacenter. +You should see a bunch of logs. + +##### 6. Can reach Uchiwa + +Go to https://control.XXXXX.com/ and click on the "Sensu" link for your datacenter. +There should be no error messages. Click on "Datacenter" on the left and you should see your datacenter as "connected". + +##### 7. Central Uchiwa can reach remote sensu-api + +Go to https://control.XXXXX.com/sensu/#/datacenters and verify your datacenter is listed and "connected". diff --git a/docs/sitecontroller_backup_strategies.md b/docs/sitecontroller_backup_strategies.md new file mode 100644 index 0000000..bbbea12 --- /dev/null +++ b/docs/sitecontroller_backup_strategies.md @@ -0,0 +1,37 @@ +# SiteController Backup Strategies + +## Central Site Controller + +Currently the Central Site Controller is not the source of truth for any data. It should be able to be regenerated from those other data sources. + +### Bastion + +* SSH Keys - These are stored in an encrypted keepass vault in GHE + +### Mirror + +* sitecontroller-envs has dicts of data which tells it how to collect the appropriate materials + +### IPMI Proxy + +* data for servers is polled from BoxPanel + +### Control Proxy + +* sitecontroller-envs has a dict of data which it uses to build out the control proxy. + +## Remote Site Controller + +### Bootstrapper + +* contents of bootstrapper ( squid, pxe, etc ) is generated by sitecontroller-envs data + +### ELK + +* ELK configs are generated by sitecontroller-envs data +* ELK LOG Data is replicated across multiple servers. + +### Monitoring Data + +* sensu, graphite, etc settings are generated by sitecontroller-envs +* graphite data is not backed up as it is not considered critical diff --git a/envs/example/bastion/bastion-users.yml b/envs/example/bastion/bastion-users.yml new file mode 100644 index 0000000..be5e9fc --- /dev/null +++ b/envs/example/bastion/bastion-users.yml @@ -0,0 +1,74 @@ +$ANSIBLE_VAULT;1.1;AES256 +34626462393236363232316530346665643334646530333337323262356538346663626565653031 +3431666463653634653434333839346663383664363635620a356466643733616364646361616562 +65353133303739643530393261303534363637656139613536313839623632336434663362666465 +3534363737636536390a346230383863373637333531643836313839613761313030636133386462 +39396630376239396239393931356464333733383630383530306137323037336666643162393534 +65643461646565646138373732656664393863356634303830313061623864373538653538343265 +31306230363031643138643630373132623435353434666261626430636364373766386565643435 +61663135313035323033383133663832393438306162666437316661653763353135323733346430 +33343262643766333062316136323438623731633938343034396631346433303030646132626135 +35623665623362306536326438656364343938346136623235303364623130393765643634626663 +63353465323762303834363866343264636235626261636266653737373731363131666431343162 +65613835386566336339643661653830666531346662366662313737386231386538326130636166 +38643365313830633032356166613032383465653131326161386633363034396162306635376362 +32386639356537626133396361623138343039343662353236393662653261623765376365326439 +32383335323564303462323337626533373531373063363134366338386332633365666239383862 +65313432373639643930356638363966663337326662323666396564313833336364623138356530 +34343731366637356661376433326138343433643063393364396132333366383063393561616439 +62643662616436626430653531656434653166396433393230386534643831626136643533646265 +34373764613561383438663438396437633637336335626132616231613862636365666133633061 +64386435326332623937363864616430643134396666663361623034336665663166326434326431 +65646134333534303232623636366264376663383563653932643733336433303131366462656364 +38376565363337306236323664306363646561616533656231663734393364663331303430353563 +37323835373032363532333732326266383366393065396662393932323039373630306461326665 +32333539666133623336363437643766393039333863623635313537356363663636323037303833 +65613232356238636135353433313236366631313965373439656266656332626364346132353830 +39376565376366633731333932383461313536353439386632663861656161333134333262663234 +66633263373166666564663531643165333061323034353734346533666438373935653636353636 +37313135376561653563376563306361613532396534383031623863616335383238616539393035 +62353066353765633634636636643136656563626632333965373063633831376133633631333232 +30653362663866383330636537613365366630306531393332313336363263653165633431663361 +36373139303765356661373261613566376338613035396139313438613733393963353637383864 +33386661343739383765336362623834313437326634326565396230616166323463333638663239 +63333765626633613830393664653665363937343666376432666438396432663666383630303864 +65623634343732616365356437323762653763656634306237343864313863653261303432666231 +37636262303834373136643165646438636137383165313363316664663533303737396337356530 +66363765383263363437653631643636633330326362336432333666343233313862373931353833 +32306133623335616534316132616435626164386661656661616162326161623039663863303663 +39616430633734623161306632303235633966356161366362316432653132303665323032363561 +64623961366662326432643966363865333064376161343335313039623362363735633531396462 +62626635383036353036383563353033303963333533643564616430663630343266613264666432 +39353036613564393730633430363831383531316261306665366432643061396534336332373364 +39343663616638343736333034316339313737363631613165656233383036336430393363393139 +65663131386135343236323462323031656536316563383930366439656530623636663635626165 +31666366396330396465383666633163363933613566653433623164356165343330653964346565 +63613135323235343937636133303534613639383362326664386363623632366339383337366237 +38616435306363643432376464626431333765353061363361373034646339313461316539336666 +64323762666630326238313932643933656662643965313737383137666430386662343339333236 +30393436653532336432393033643833613831636438616562303935666662386236323931343835 +38633137353538363337343435663137343363366265353937396336333964323264333233653662 +63616535306436363664316364353239636633366163376638643532646566303564366639353965 +35326665363866656131613663653334343765353234666432653535663665373465343039396432 +66346461366562303666336536366565636230336561316436623462343665663761646131613665 +32363464633833383331613261643936333039663838383337393065626662373135383163383361 +61623431353764623933346666653730623937346662356664393138653739373266386262323861 +65373131376537393230616531303233663735383566613232316434366535313962636233623966 +30646637313534616163643661343766643033363662623464643139316433393061396463353532 +36323930316330313836346336396666363461343030653430326462393435323833373366636331 +63623366373134666432336362373237356436376535616435376562636264653939353331303864 +62343032383231646237343538303632383639396332303635346536663937616537613031646630 +32613966353839346235333763626337643262323662623838363530646639353561363135343630 +62363563323337663837633434623765363461316562633966396632643439353436643264646636 +39323632383734646164616336636164666431333061316562373532376338396132353034386233 +39376461633961383361333466313132613662643866343538613438633230316566626436663638 +36393134313438636564376331643361383764373133626366333935363163626331643332646634 +64356565633565613266633636353363633564633266336337343637336437383037376536633336 +37613539663431323363333463636461386366393530346366386338323130323133643761356131 +63666336373438653264373736323465343961356435333835343535333264326534313032656538 +31623464633038313639316533323333353638623039313235623233323332643463613363356137 +63306435663062383335666234343062623830643062363634383562353764393764653064366130 +39623437333064336464643864393635316263383434336335646632653337363939663030333232 +62363834393939303737333137643737383834613234326130366332636434366133336338363461 +64663039616232633735336463643863636636616465373330343934333332613563373131643938 +64373231336531333963313232666332313363313261373733356165643062363064 diff --git a/envs/example/bastion/group_vars/all.yml b/envs/example/bastion/group_vars/all.yml new file mode 100644 index 0000000..7f664de --- /dev/null +++ b/envs/example/bastion/group_vars/all.yml @@ -0,0 +1,377 @@ +--- +public_device_interface: eth1 +private_device_interface: eth0 +public_interface: ansible_eth1 +private_interface: ansible_eth0 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +common: + ssh: + ghe_authorized_keys: + enabled: False + api_url: ~ # ex: https://github.ghe.com/api/v3 + api_user: ~ + api_pass: ~ + +bastion: + backdoor_user: vagrant + ssh_port: 22 + force_commands: + - /usr/bin/ttyspy + - /usr/bin/ssh-ip-check + - /usr/bin/ssh-mosh-filter + users: ~ + +yama_utils: + enabled: true + +yubiauthd: + enabled: true + skipped_users: + - root + - "{{ admin_user }}" + hosts: + - name: bastion01 + ip: "{{ hostvars['bastion01'][public_interface]['ipv4']['address']|default('172.16.0.150') }}" + - name: bastion02 + ip: "{{ hostvars['bastion02'][public_interface]['ipv4']['address']|default('172.16.0.151') }}" + sync_socket_secret: thisisabadsecret + firewall: + friendly_networks: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + +ttyspy: + common: + ssl: + ca_cert: | + -----BEGIN CERTIFICATE----- + MIIFETCCAvmgAwIBAgIBATANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdUZXN0 + IENBMB4XDTE1MTAwNTIyNDMwMFoXDTI1MTAwNTIyNDMwMFowEjEQMA4GA1UEAxMH + VGVzdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALaSaxQLH42A + UAoXi/kpW1p4I4Do2A6oyyeaaP3SejcnNb5bW3VFu1RK2aAfeSLgB3URtBN57W4x + D7f41fkqGYYIwS9D2iDKRGRBuofTjNeOYs6m95eLs3Erbz96oCPm5T1IyP6G36Ye + Gt9ut+IaGMiUP4ocyxJnf78YeIfDdCQEV19k9C4GEEFwfeyEIscId0VrQy1cSRjN + 7Tyht1WYCXUJu8ye7D5NUtMLACV7ZB5OxgZc8vaoFoFErUD76WmZa2sHe8dopa1r + Waadixjx+2t6w4zYQj70g0X79m9uB8TDa2a+nIspEYJOO9cMQEOHRNVrofSEGTTv + 5c/eh+s5i4WkN8NBqgCEkqczV3lJMThi/mmkpVmtJ1X3Vh2SB6SihBg09ESfngsS + odX504FCFQLjR2hI1B1Ofd6DszVkCyp7G063Dpa7QvA90P79BbjhAKOkBAykRH68 + HiPCDD61DarrpjVJ7Nlr0A4R0WjNgiF945EZv0ZvzOSW5qqJhOR8WbBG2NmK5UcU + iX1FQUt7Wq1MXb46nAc/N5JLkM5vMYuw2dZ4Ny5Nbxx7hMDHPCGQ/ltIRd1w+kdG + 40a/ln29/LakjHQ1FSOpCqhD11lLfAxvn0SbIHuSfOxMX4rEGYBDsU05CXAvnTlC + AJdrNxqr7M0ll+BeBVIIPn8Vtl0uDjt3AgMBAAGjcjBwMA8GA1UdEwEB/wQFMAMB + Af8wHQYDVR0OBBYEFGsa+1qlRsk+vMyh2RQsenxX2IArMAsGA1UdDwQEAwIBBjAR + BglghkgBhvhCAQEEBAMCAAcwHgYJYIZIAYb4QgENBBEWD3hjYSBjZXJ0aWZpY2F0 + ZTANBgkqhkiG9w0BAQsFAAOCAgEAppnxty/kqH6mDTEB1/1m3IEju9hMDf6sD37k + ogN2D3NBJC/CQDNfqz6/0wtAF5CSUSSZ919MtZG/wEKcVbNzuoYNERrpS51OjiCS + pgV4U/mPFvZlNFD1iMtaYGnOjLJagK1W2NfW2fGuV0mhmvIckqjjQPEjWcYRL8i/ + 4E6jbnQzuSUrpLumZBFQSjZkfPyeo7jYll1b4LM+/K8omzGA6xfbLbIWUvLSzl2R + 4YUW/ezJWTONEh3jrFWUXxrNwDaIVPEMBm5+V7/L/cvQKdt2SH67lbSFHSWvfND8 + 3wNHRvRyTCtfVDoPGa2otuY29SoSOTWi63QSZQVu9th4wcPtXwQTeQx34bUfdwei + Xjxd1F7Ux0IkPv90GUixxYOXv2O7Sjhjtw+68DFYd5Yiec30692aEghzMThe1yLe + rImozA7iV4jih7OTaiIDKdCfvs2GcKHfI+nx9cCaCRYPe+9dB79yEXpDRxGii68X + 904ga0g0p1035FTicZ2btECFIN7H3zceylDe9D95WETP0ENB0q8qHs+LjiCNuqiv + 2KzUW4HW7eA8HBoNzxsRZCj3eu+SeY+5LzSu4j/4nhp2Q6pOlbOKVHDjVmnZIZt6 + 8cLTIKuMbD0pxJzSrrHeYh5fdV4j0xJub6MxROEQPbPn0oH1RjfYwRUohg2ibq64 + NojcNto= + -----END CERTIFICATE----- + client: + enabled: true + ssl: + cert: | + -----BEGIN CERTIFICATE----- + MIIFEjCCAvqgAwIBAgIBAjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdUZXN0 + IENBMB4XDTE1MTAwNTIyNDQwMFoXDTE2MTAwNDIyNDQwMFowFjEUMBIGA1UEAxML + Y2xpZW50LnRlc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDkiiXQ + E3v0thvOjzXLlNA6+uy3+2cKYRJ/AGJZAwpbjmMg+M1Ql2ZN7RbMBSdAXDH1RXxh + xTjWJMboj1XFsX/Mf8taiOTgSUN/sEm3HLFVnXC2fbZOjZV9DfuEjKSzWfm8TLiU + pmbQ9R1ZxVO9Y50hWC5hmA2lDth1QU9L2IoeYBR8qqM6heorxem2a5NyI6Cc8bPQ + ydBo3zhVUAP4Gnhq0CEYFY8sCrJKemVlfJfhQ/sbDRwQ/PsL0inl1qwHrcKWTzVe + t9vOFRdeOfdN6qa2iIHeLjjJ/NV7V0jBuALJUGIWz9NBYhZL5AArhDwVS9lSL9p7 + sLwYa4rc2Zo079sv7UVBj+7UJO1upgnHziKdhZX/Be1gOdKHzlrR3/Rr910tWXuE + /6S/TUffekmKgPhr8+AMT5PLKMVaB16urHB0jf+hLVAJS6xaenJvt6f7eoYUGMRN + OJQMlFW3WMKjMveUDurxi9v9XKrRwx5f8geiI6ANNZfV/WEhJeME+C+Jne3k37sj + vKjb2W1eap6ftP9yPS4d8m1M8ChJUnawBR/xAAbuivsBlz6eY4YZ5fsGL5mIX7Gs + LeyoGYCb8fc2XJ2MR4sqoQqwD5V5BOfJR8hpMA7sMXj5+ICqtZJq+tesgd/daXxZ + OSIKcGbv8yoI+GYK8mgjfQhXRe9gApSHW/Q4GwIDAQABo28wbTAMBgNVHRMBAf8E + AjAAMB0GA1UdDgQWBBQaDK+1R3zzYUV7es6VWg+HsvH+tzALBgNVHQ8EBAMCBLAw + EQYJYIZIAYb4QgEBBAQDAgWgMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNh + dGUwDQYJKoZIhvcNAQELBQADggIBAIdLFwQx0ZCX1PJhN22mGEYGpV1rq7Osixbz + vX5K48wxX/gJG76vYUnkhAt/cm9H9yOtUglHdYXlLvt5grR/rPPKZ60iMBud0n47 + SGmojQVCaWdwbKMjehSz6N8NWT/LG4lmahIeiv8IPpmV6cfsQLdWS+AeqtqmFSNA + 1lVLBdoLYvN0i/CGQBtRnTprmp1CbV9MFUDb5JSR4XhxtqfCxZQFcAovk4TlgmFS + 9WHfkXcFU56LypQHvzIqa7QEm2z3QuqHpA1S56L+1+MLxdNzNthS6fnKY6EbEGQN + 0rQyEgebk858DpeMdJA708H4vgr5TR7eIPIlAJUvF0QMJKqgQ/yMdH5+XmFYqbDZ + nHvQNvXMY4l2DiaxgqVBlpUeKbjDqURTLNXXZJYEWhpz1iVXx66C2q+Uh0+JA3g9 + 9VJsIq0LDrBDoNw3GvDX/WKUItRKs5BD24T/7xHs8z8AfSPlEX3ChtMhZYqJK/C3 + 8UbQV6MASZTLoO4S+66Gq3Fra3VZXvFGjMLlXGzRCMZpw/1TeolGc84XjsGUpaq3 + 99JK4m+OlIKyDFqC9090S7WyEV/pyy0VEAEupob4voAGGA0EjV6Ytx9XaarJgqBy + XS6tqm19L6OtSmCbapndz0KBUhejFgNSLUqY/YMO2onayoAYXnUiskfb1cKQax8F + rNKZQ9So + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + 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 + -----END RSA PRIVATE KEY----- + server: + enabled: true + host: server.test + ip: "{{ hostvars['ttyspy01'][public_interface]['ipv4']['address']|default('172.16.0.152') }}" + port: 8090 + transcript_path: /tmp/transcripts + firewall: + friendly_networks: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + ssl: + cert: | + -----BEGIN CERTIFICATE----- + MIIFEjCCAvqgAwIBAgIBAzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdUZXN0 + IENBMB4XDTE1MTAwNTIyNDQwMFoXDTE2MTAwNDIyNDQwMFowFjEUMBIGA1UEAxML + c2VydmVyLnRlc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCxHM2K + 0b3A1yk1IY4Bwvq/g+8HDsI1kRVdpkZOljE8JWEXBcotV9NoRnpKWImLRthPfBcm + TmOXaBDmS2rx4hW/jwca3V5dSSSXkfx8DEWMmUhX3dxBMerbjtMyQLy+8HKAZPGQ + xRNmEhV/qwS5ZxEKI+dsB2zlyepnF8S3IE5kKJQFOZVZv59WZFgaxv7XbFyTeJJf + qrcpULnzzI7AwBXt3JxCxM5U/ABzAlzVOvH/tOweHYIjtm6yQCs1KQStumjh7L4X + BVjU/YPfGy+Fqky60mruZEU1JvpISsjO67QsmcnYWR3vQI4K2L6fo6OCGhOkdNsb + 3z6LWlk+pzbB5rRKsMv7FJkqdFDfOb2mF+SPUVU0hAx5e0XiNfJsPUpqlzF8dALW + M26vEpUDemEEYwW4MQEEBnvL4E8oXXwTExFvHAvCuqu3wdCBufn5qCVTir/wT+v0 + aYqsR6QhgscIMCzZipyjeLs7yfW4rAMgLUE4xz8ZIJfSKr5+zeDwoYBs7d9y/8v6 + g4j3CTNMfraSbdKbLBRvvVk/+ruBVnGl/NPd5z7XtlBtfDV/aHqzj4n6frR7UmA4 + +VewY5idzdxgu0T+6mlqhY1s83EZsu+gNl0hAmAvMdp7UptfwWnDr/Waqj0CJHfq + alp1cdRZh6ceRbTIkpBemIxXAKW27i+/ScAh9wIDAQABo28wbTAMBgNVHRMBAf8E + AjAAMB0GA1UdDgQWBBRkwxTJ3QzxIU2qleVkIIaa6Y6otTALBgNVHQ8EBAMCBeAw + EQYJYIZIAYb4QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNh + dGUwDQYJKoZIhvcNAQELBQADggIBAICHzCY8omXOgmGFRQxSQMZ9KCbaLDVQ6duN + ysniO0imXdy0qGb+b0HVP1gnTv7moq8YrIA50TeyfdGZkBXpT6pA/hc6Q11aw2Bq + FaDSSj0otCpxSuur0b52G1p2IhFjymFeuCQwkkPZ+1YXtAY9evE7iq6ItFkGEs/K + FgSIffFggOHFfoHWz9QhAMtE28VZOieYbpsyR7YnZHANkkJfjK6FNJGE4LrwuvlG + SR/wvFeecmWMsRVsWdi0Jb9PHF9JW2gJVFl6fvHCZleg9LtEoVwSseK7mIy0VU1A + JlYVx7Z2lcxYVpkDk0JQlUsOSD0C+lyTwjwfCN/UX6PjaSMMiAP4h+dOBVFLsp6S + c1qeZrA5Dgsv/9efTPXxb2uEaSg2bNqvn0ivmbcu0TfDALCAKUtzTvM7lO6IVijp + HKGm46ZuvOVLIv4oZmaAg/1lC4J9HZPzGMABR+63lTEhcR8DqtZ5MtJ9clD618Z8 + dg9jEgpapUdao+nat3jbVr+mh7m9WFfd4H/8d++xxfjcEEUSPhH8JT1voSzZr4cn + gBjSiohrW/FpcDNeCTsoE847wldkpHblD5khwtTmDDwrIRAD3Q0i+EBAkhsk/dY3 + +LEbD1Ab8lmSMiLGDONctQxFdWEWSyR/btki8Q0NG8fAFy0qzPIymCF2aVMtxX24 + /xdoyOms + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + 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 + -----END RSA PRIVATE KEY----- + +support_tools: + enabled: true + system_deps: + - libxml2-dev + - libxslt-dev + - libssl-dev + git: + - name: ursula-master + path: /opt/git/ursula/master + owner: "{{ admin_user }}" + repo: https://github.com/blueboxgroup/ursula.git + rev: master + virtualenvs: + - name: ursula-master + path: /opt/venv/ursula/master + owner: "{{ admin_user }}" + requirements: /opt/git/ursula/master/requirements.txt + alternatives: + - name: ursula + path: /opt/venv/ursula/master/bin/ursula + link: /usr/local/bin/ursula + cleanup: [] + +sshagentmux: + enabled: true + auth_socket: /var/run/authorization_proxy.sock + +users: + exampleuser: + primary_group: sitecontroller + groups: + - exampleuser + home: /home/bastionuser + createhome: yes + shell: /bin/bash + uid: 1999 + yubikey: + public_id: 111 + serial_number: 111 + aes_key: 111 + private_id: 111 + public_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA4rAIuN7EoPdU8iDPnp27zd+hXsbTE1NEIAQFblG0IywG2B522pivpxE/v1BmtaIVas1APRFDsn5SMGrDOiVNZGz/MdIdJOPjza29WyXgb5w9I329I/XKF5/NEkXDajqzHQheHZ0NSQFFqrlW+N7t6KdKkFP0heAnOLtXJIXrJso04Ew/o/NX6qJFvDY8pVMUeQVloX5zFuHwq+N2JjJIEDS89mmNfqThoAR0KZ/jKQnjNhCdKVurS20Sxft4HI6Zjm7YZMXJO5a+TL+nYEq+JEzLL+PdKcBf4BVpr6MLO/R3d5nxGAtdhgXUSvEDT2bCFWc66KBzNtJTzDKcVn2KcQ== blueboxadmin@yama-1.blueboxgrid.com + + +user_groups: + exampleuser: + system: yes + admin: + system: yes + blueboxadmin: + system: yes + ssh_keys: + enable_passphrase: false + fingerprint: "64:f3:60:f0:33:ed:8b:a3:af:33:c3:c1:e6:c8:41:bf" + private: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA34Tz/TDvjmcZkDiWmbm6Zz4jx1bl4rcEiSpbxYRqyXvxbGkG + e+YoDyy5s+M/tKaP+kAYDbXwJM6NnLq8q4YsEeqszjDE3+qgH54TE8EoT1VoPDkl + iUiE3a0d1DLZT8oX7x8y+q+39qQFN2sjTf39VgMpbWy6zOH7Ok9hpJXMItezuX9B + sMOfHE3TgZdYnRlc9tRmI58+igw1E2reNqHBxWu2c4FdydoakTXgUmcLLheKW0xC + m0NG9Cy5oAdq79ZJruuQQxTy9W71xm6W4EreDMCZrR3JWBvH2Ahv2JcDdj8BEB2g + iYzwzSqM4D3l44jza2BULgoXf58JS/8B7LkryQIDAQABAoIBAE6ba1M3yofCKnNV + 82DMuIlmiR6pqN86jhXPF8c28nc5Z4ZAyU75ek0b5ZMl8FmP2kKgF9V6jqHjIlpk + McYAwa7rYSqCbDpzQSzdYsgnvg1oc+f6EQFex5tOLpdZ6qLs583oov0WnxPWSx9a + Rmg/UsDVC9S30FoNf1TaZfSD2e7GVOCohXSXheQnRs5AkovlieoBy8NbaRj9+ey3 + hTsbtAsMk7WiazHpP/Fl7LwAXSHRgtFl+s1dbSYbpBLWdabB54j7m75x/hJRWzlW + zgmTdGSpLOnPeKe9TJtIkWzkqx0+XMbk1D4FUZVpo2D8whpHJxZNUdzY1UpqTu6M + frGsQIECgYEA8vqxh7Pw3S32TyMmkpL6oNziccyKe7U+xurulmccrPD6dhpMjOXR + 0ErPBIklxiUsAEZNIdBP4G2Of1xU0OWnWH9Xpk69FI7K7/XPmOoNeC3lYp08IUtb + jvs2C7F6ir+cSpEjU1PrT9hIKTk6XO13Nx291pd6xxUz1UUfdyxv220CgYEA639M + jEH9BsFSghYHM41GCMp2/+xfLLN9sTdPB7b8ElsOMD2Xne1QJ9uHfXyDi5Ba07aI + GbxgIoKcVlxIf/JJFdomeiXzf3PjDOTE8Pv/wMVhjQsfkCfgEunqVKF9amLTeTbl + Ype2modJXe+yuo4AqN8inz3CvTsP15rfZRh9XE0CgYA6xnOdPOS9y/lx6THScOVZ + djT8jBrPk+csnPW7whOIrf4YBYKQ7qLTPNVY5ogRpzo+ksLjtA0uX7IBkucdZQAX + Ay7DlvZb+7KRWyeteKhrcsazFQ/PifgK3S+Uooje+TyoOPWPmZQpS0shVauNgQ++ + cF5Ug236rGFObJsQ69ne/QKBgAaxxLRL/+xcPIM0Kxo9DtubHczipEX6CD2sa9O1 + UO8YpYubhJ7Zqyim5fAcRQUHon1YOAA7SaRRgC44S1tPwOIdJHDeeVCLM84fBrYv + A7MwKTjAMzJ+37DJ835aZN1MV+SfOeAWnftAk0ZXVQZWmRAz36EVOV71udqQLX+L + Na0VAoGBAOyAT/XPSDjMaWMxkf0/cuGrPDotG/2qlPkFzykTurYRXfB+gs/0wM9q + V64jR8VQpxfyljtdEEIbNnYiiHcu3WGDK6zu0w3LzYFdKawKxJezL7vbC3X4qgil + 3KsipSty93s+kZ265SMqby2itnryoMSURt5PniUFlBq5BBrzeZde + -----END RSA PRIVATE KEY----- + public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDfhPP9MO+OZxmQOJaZubpnPiPHVuXitwSJKlvFhGrJe/FsaQZ75igPLLmz4z+0po/6QBgNtfAkzo2curyrhiwR6qzOMMTf6qAfnhMTwShPVWg8OSWJSITdrR3UMtlPyhfvHzL6r7f2pAU3ayNN/f1WAyltbLrM4fs6T2Gklcwi17O5f0Gww58cTdOBl1idGVz21GYjnz6KDDUTat42ocHFa7ZzgV3J2hqRNeBSZwsuF4pbTEKbQ0b0LLmgB2rv1kmu65BDFPL1bvXGbpbgSt4MwJmtHclYG8fYCG/YlwN2PwEQHaCJjPDNKozgPeXjiPNrYFQuChd/nwlL/wHsuSvJ dummmy-key-2 + sitecontroller: + system: no + ssh_keys: + enable_passphrase: false + fingerprint: "c6:a2:23:58:cf:c7:7e:c7:63:87:0f:4c:ce:be:7a:eb" + private: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEA1x6zzX/OSjNwqPuQTJGyXqIB+OBzNTxCXQZUYrb9c9ZjC7rS + yvqpnpfNq2iSag+TSkyZmz+rSOVey94YBf8KY9tI3PXwMFnyoSIsTChmDpSiNx9b + /IswRWHMjWmGo0oxD0cVsRIDPH4cd9DMPpbYgY9062E7nCBgv4b+xerul3sjlGzN + WN3tWFJ0BgzbvWKM9R6fFMgXOrIz2ASyVepCUizndh4m0DU3Dsj7utEnVawMUWN8 + asWZx6XvxlEghQixEhIp2gFOhe3vm+GGgR0BzCfmQqget+jYs7FlW3Vx2he8izAv + yfBOV9PfpfEv0mgCd266rijAFIZycj9xONuvNwIDAQABAoIBAQCkQkNU9PQV4HVz + 8rLaZJ3oeJg861XNHngmBAFHZybc3qS+fica6o++E3fuHGlAJyh2oUrhKpqljM73 + qFx7p6TNXtGiNwDySpxjwW5FsMtM8t1ybbWVfsqbD/RbPmqaILqZSdQcYv6poDoo + mvx6BkDHnTzPxmz36Bk35eKAScVpUC0SDVX9AiK+/4cIna2UvSB3XpTOtaXNHqIQ + 4gJzO8A7EF6u4CyGD8ycB49Y1w/+0gLVpH7p9aIgZrJTxr4bloJTnTrALzEly4p0 + MzIB9DtSFpTv0yx+f3acix38gRehhjQtxLk1+/goODQU3j/7JK2ZiH5a0uDM8zEY + tELDC4mBAoGBAO0xUrNHfJnY5x9dDRd5RLIEP8Bd0GRnCNxfAtwH5iKHJlXHekj+ + zFHkGZJqFCC9ej6i0Mh7tnfZRQUBvVBjw6kslAKXD+x9vDq1qiqVxxwJeFihX8fj + xk5GWCS3I3ienEWPZMu83sZC4YJN2px5IO9BZGoXj9WN0MLH0ejmYH3XAoGBAOgt + VXbYMjt1VwVtawBCywvazmq+0A85AvZ2Ak0LSS2+mgZ/fUlzgRbKOP8Myh4gmYAh + ME35vOb85IpVn2AF02v4HcBOTNBvHluXNgxieqHICYsfCUVqnmBA1UucpCGNGVt7 + UGof+FdUBYGMLLEIfLGsjNj/MmjasGqyRLZBKG2hAoGAZfQPgjQ2IMMVBWwv1mkv + 1/zvkjZA/wcyzdahGgbjKvBA0BpAO+QZ2xFa1I54PGJ1izrc13AlzHo9qptGzqkz + TyJ0NHDOTW72W53+mPNsdGa1rhMfYoJLmRWviYiW3KAAt/2c694xO7M/z4y7bQq7 + 11uaV+fs0XR1yWOunJd53l0CgYAl9NNV/H4pzkMNtheaEVFUfM7mEI+/DVj4pc42 + fjPWcKSJj2oSCfn+mcy7lYGtbzLpCYP2G2/Qa2OJYfoOHqWzrvpeJ+7S3HegDZZe + a/MUY7l7rvU7DfUaUz8Lf24ttf2BQSWiU9urmybTSPE9d9+6xDS6fO3mymmw57fn + +7r6QQKBgQCEhCWm6PdAhixliu4u4Fh+DS8RzxTYYeY5d1f3D8PFdWUW45ILdN9+ + Tvgu9ijpqfu+4faO5UVODPU/GpF8sd8UhTRtrsQof/OzdBP6cbT7OsNYg6fQd4XL + rNd+I4++Fjjcm36/6uS0Uk80ASZTlsJd8IGAJzG2+KlTilZWo4VC/A== + -----END RSA PRIVATE KEY----- + public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXHrPNf85KM3Co+5BMkbJeogH44HM1PEJdBlRitv1z1mMLutLK+qmel82raJJqD5NKTJmbP6tI5V7L3hgF/wpj20jc9fAwWfKhIixMKGYOlKI3H1v8izBFYcyNaYajSjEPRxWxEgM8fhx30Mw+ltiBj3TrYTucIGC/hv7F6u6XeyOUbM1Y3e1YUnQGDNu9Yoz1Hp8UyBc6sjPYBLJV6kJSLOd2HibQNTcOyPu60SdVrAxRY3xqxZnHpe/GUSCFCLESEinaAU6F7e+b4YaBHQHMJ+ZCqB636NizsWVbdXHaF7yLMC/J8E5X09+l8S/SaAJ3brquKMAUhnJyP3E42683 dummy-key diff --git a/envs/example/bastion/hosts b/envs/example/bastion/hosts new file mode 100644 index 0000000..1478ef7 --- /dev/null +++ b/envs/example/bastion/hosts @@ -0,0 +1,6 @@ +[bastion] +bastion01 +bastion02 + +[ttyspy-server] +ttyspy01 diff --git a/envs/example/bastion/vagrant.yml b/envs/example/bastion/vagrant.yml new file mode 100644 index 0000000..cbb2cc0 --- /dev/null +++ b/envs/example/bastion/vagrant.yml @@ -0,0 +1,17 @@ +default: + memory: 1024 + cpus: 1 + +vms: + bastion01: + ip_address: + - 172.16.0.150 + - 172.16.1.150 + bastion02: + ip_address: + - 172.16.0.151 + - 172.16.1.151 + ttyspy01: + ip_address: + - 172.16.0.152 + - 172.16.1.152 diff --git a/envs/example/centralcontroller/group_vars/all.yml b/envs/example/centralcontroller/group_vars/all.yml new file mode 100644 index 0000000..22f6e46 --- /dev/null +++ b/envs/example/centralcontroller/group_vars/all.yml @@ -0,0 +1,98 @@ +--- +public_interface: ansible_eth1 +private_interface: ansible_eth2 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +common: + firewall: + friendly_networks: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + ssh: + allow_from: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +rabbitmq: + host: "{{ hostvars[groups['rabbitmq'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}" + cluster: False + erlang_cookie: E9HGSG7Fs8UmSSQ6 + users: + - username: admin + password: w4HLrz8DHtB84shd + vhost: / + - username: sensu + password: m2KNhjrmxgjXB2ue + vhost: /sensu + - username: graphite + password: 6L2wyT9NXC6qZhQH + vhost: /graphite + firewall: + - port: 5671 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 5672 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +sensu: + host: "{{ hostvars[groups['sensu'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}" + rabbitmq: + host: "{{ rabbitmq.host }}" + username: sensu + password: m2KNhjrmxgjXB2ue + vhost: /sensu + hostgroup: sensu + dashboard: + host: "{{ private_ipv4.address }}" + firewall: + - port: 80 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 443 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +openid_proxy: + locations: + sensu: + proxy: "http://{{ sensu.host }}:3000/" + url: "/sensu/" + ipmi: + proxy: "http://{{ ipmi_proxy.host }}:{{ ipmi_proxy.apache.port }}/ipmi/" + url: "/ipmi/" + config: + - RequestHeader: "set X-Proxy-Remote-User %{REMOTE_USER}e env=REMOTE_USER" + firewall: + - port: 80 + protocol: tcp + src: + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 443 + protocol: tcp + src: + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + +ipmi_proxy: + datacenters: + - name: dc01 + data_center_uuid: 7cd69350-35e8-4677-98fe-aad60fc9e191 + backend_source_ip: 172.16.0.16 + host: "{{ hostvars[groups['ipmi-proxy'][0]][private_interface]['ipv4']['address']|default('172.16.1.104') }}" + ip_pool: + - 172.16.1.21 + - 172.16.1.22 + apache: + port: 80 + allow_from: + - 127.0.0.1 + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +monitoring_common: + service_owner: development + +dnsmasq: + interface: lo + server: 127.0.0.1 + dns: + hosts: + - name: bastion01.example.bbg + ip: "{{ hostvars['bastion01']['ipv4']['address']|default('172.16.0.101') }}" + - name: monitor01.example.bbg + ip: "{{ hostvars['monitor01']['ipv4']['address']|default('172.16.0.103') }}" + firewall: [] diff --git a/envs/example/centralcontroller/hosts b/envs/example/centralcontroller/hosts new file mode 100644 index 0000000..716274b --- /dev/null +++ b/envs/example/centralcontroller/hosts @@ -0,0 +1,14 @@ +[bastion] +bastion01 + +[openid_proxy] +openid-proxy01 + +[ipmi-proxy] +ipmi-proxy01 + +[sensu] +monitor01 + +[rabbitmq] +monitor01 diff --git a/envs/example/centralcontroller/vagrant.yml b/envs/example/centralcontroller/vagrant.yml new file mode 100644 index 0000000..bdf9c6d --- /dev/null +++ b/envs/example/centralcontroller/vagrant.yml @@ -0,0 +1,23 @@ +default: + memory: 512 + cpus: 1 + +vms: + bastion01: + ip_address: + - 172.16.0.101 + - 172.16.1.101 + memory: 1024 + openid-proxy01: + ip_address: + - 172.16.0.102 + - 172.16.1.102 + monitor01: + ip_address: + - 172.16.0.103 + - 172.16.1.103 + memory: 1536 + ipmi-proxy01: + ip_address: + - 172.16.0.104 + - 172.16.1.104 diff --git a/envs/example/ci/group_vars/all.yml b/envs/example/ci/group_vars/all.yml new file mode 100644 index 0000000..7eb212a --- /dev/null +++ b/envs/example/ci/group_vars/all.yml @@ -0,0 +1,93 @@ +--- +public_interface: ansible_eth1 +private_interface: ansible_eth2 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +etc_hosts: + - name: jenkins01 + ip: "{{ hostvars['jenkins01'][private_interface]['ipv4']['address']|default('172.16.0.101') }}" + +dnsmasq: + enabled: true + interface: lo + server: 127.0.0.1 + dns: + hosts: + - name: jenkins01.example.bbg + ip: "{{ hostvars['jenkins01'][public_interface]['ipv4']['address']|default('172.16.0.101') }}" + firewall: [] + +jenkins: + admin_username: bluebox-ci-jenkins + admin_password: 0e03beb1b3e69a6d2f2ba784ebde417a4ab3cdd7 + security: + github: + orgs: + - bluebox-ci + admins: + - bluebox-ci-jenkins + client_id: ab79f626fd6e466bdbe9 + client_secret: 5e9a0cbb9a6659294a507a8f0d5927ec1f0681c3 + jjb: + git: + source: + repo: https://github.com/blueboxgroup/jenkins-job-builder.git + rev: master + path: /usr/local/src/jjb + jobs: + repo: https://github.com/bluebox-ci/jenkins-jobs.git + rev: master + path: /var/lib/jenkins/jjb-jobs + + # we're not testing the apache role at the moment + apache: + enabled: false + +ci: + stackrc: + path: ~jenkins/jenkins.stackrc + user: jenkins + group: jenkins + +sensu: + client: + enable_checks: False + +secrets: + openstack: + os_username: ~ + os_auth_url: ~ + os_project_id: ~ + os_password: ~ + cd: + ssh: + private_key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEApLGq0EuyeqqbsQTfLSiVsFo9VNvRX6HvfWLqydmB7lvwg2lp + SQ1+RuE+BNWLxuGALZrS9BlVr4VI9uPHcq+kqrH8qcdwyd+N3y4a5ThwqXq+675G + f3A68TsC30F+7NmqAEK0fvkUMGjNddPuV/tIz8t2wKicu425DEPEgHGqpkNIVn0j + PsGwxYteGvp/FS8ofzcbQTaLSOUJ8sNdtfQ/jGRIIQyAHXMPOjKY7hZz8Fnr/kZm + RdOG69IGRMe4HsRu6RM/qPoNFurxiX7sTwkhluH9w0Nll2/yuZrxxua1ExlXCdhT + ittwxGYrQSjzpaeqaBq/GMrXQNm7TgYlEvtbjQIDAQABAoIBAB2y1CSfod2w6kZv + pHcWPHgCrdChmw3tu6wWrFQd3upGtgZcednsjvrlHzPr9jq061JN+wB9mQ//rvqm + dI7f0YfedLuA9XvRzlmSjNMM7HQDaG5Gb5wHdvmNNKVhwCuhzKAjZz0GWXepTnOS + 9gXzkFu4Sv/O6ASCN/5YMhUfajB044UliScjZ+Acx6EL1uLQJd92mMV0tdP2AAvH + Cxbl9NGz3ne1lQgGFYnURTxFjX7sjN6MaJ1PzyjiA1cMLc2qxR45kTPNLvHUfpx5 + R4JBqJw3W+/v+jejeCes0hWyOjdo3X8yG84A+XN1LwvM1aWye/wdd6j7fw9Dl1wC + 8v3wq5kCgYEAz83nmrP9HfQG7CFeMbU5z8pmJrWN89ze8X6/0T3RMJOJhMA344sC + QsgiGyXsNFRYo1c7shzKq8bmQ6U6CJHMk6ubZwWyfqfEu9qGzfI9w63FP/67bsky + 1dR3mxWYXb3Ux5BgzoWIvLq1lAFZxf4zQqnVP3YRRRadkMzM74DsIBMCgYEAyuQl + y4GHHlTRQ4knVGEP+crbm5nLF7t7uCY5YZ+VDKPWpM0CQkAlCO1pSC29/Ms6FfCf + BODkMaWRKlxTT2spyqs8VPKCokEmzoI32SQlHWd9ANkcmsBAfyDnVyhwhoU/QVxG + dJlBu2/VXJxlDBETPKqdRwBz0Z4nvz5pXRACSd8CgYA8v84gbNPnaMqJR2v5Dijb + dSkN0e+wxfYrFUnQSskX8Vm3hFYSYDYF/enyk9CMr8fF4J+j/0TEmP4B8pCXpr/u + v7FLmd/HpRYfcNAN7u5dgTRz8+0pgwoodteMb/C+HtvQh4W1elYyDkc9AHQVLK4h + o/KifcqorubbSPP0ZHUjHwKBgFmT51f1ZTmwCwB9Yp7vh8UqmlEV0sQ0o4gHYFhI + w97jy1XRqcikV3WclFooz/P3qjqblSnrPPTqe7AvOR0cXEQ93BJJTAHlMAQHHC9D + tRtJYLhqIyXz8cXvOhSSJzRNSkNKQVUjgREHbDVKW5a7RzoGvg/mFDydTkEyhgRv + mAMTAoGBAJgrJugytVGupVbLaiP15cHRWDX5JHPp1YJAgmnUJbe8YBwiElk8QvhR + v3PUXTYDaApwuokmSWZ2y+FHNZq8vn8x8LRwWIAwmav8LQkzxE2y7dWNEs96bhqE + Ql1IWH4KZ/RTvGieqEpVuHrXEJqY+2a1BaZ6v3N8nFRkMsSewFDU + -----END RSA PRIVATE KEY----- + public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCksarQS7J6qpuxBN8tKJWwWj1U29Ffoe99YurJ2YHuW/CDaWlJDX5G4T4E1YvG4YAtmtL0GVWvhUj248dyr6Sqsfypx3DJ343fLhrlOHCper7rvkZ/cDrxOwLfQX7s2aoAQrR++RQwaM110+5X+0jPy3bAqJy7jbkMQ8SAcaqmQ0hWfSM+wbDFi14a+n8VLyh/NxtBNotI5Qnyw1219D+MZEghDIAdcw86MpjuFnPwWev+RmZF04br0gZEx7gexG7pEz+o+g0W6vGJfuxPCSGW4f3DQ2WXb/K5mvHG5rUTGVcJ2FOK23DEZitBKPOlp6poGr8YytdA2btOBiUS+1uN" diff --git a/envs/example/ci/group_vars/cd-masters.yml b/envs/example/ci/group_vars/cd-masters.yml new file mode 100644 index 0000000..6762964 --- /dev/null +++ b/envs/example/ci/group_vars/cd-masters.yml @@ -0,0 +1,24 @@ +--- +jenkins: + apache: + github_hooks_only: true + ssh_keys: + - name: id_rsa + key: '{{ cd.ssh.private_key }}' + +cd: + secrets: + stackrcs: + - name: mycloud + os_username: mycloud-user + os_password: asdf + os_project_name: mycloud + os_project_id: asdfghjkl + os_auth_url: https://mycloud.local:5000/v2.0 + os_availability_zone: nova + os_storage_url: https://mycloud.local:8090/v1 + +ci: + users: + jenkins: + home: /var/lib/jenkins diff --git a/envs/example/ci/group_vars/imagebuilder.yml b/envs/example/ci/group_vars/imagebuilder.yml new file mode 100644 index 0000000..99a9975 --- /dev/null +++ b/envs/example/ci/group_vars/imagebuilder.yml @@ -0,0 +1,10 @@ +--- +ci: + users: + dib: + home: /home/dib + + stackrc: + path: ~dib/stackrc + user: dib + group: dib diff --git a/envs/example/ci/group_vars/jenkins-masters.yml b/envs/example/ci/group_vars/jenkins-masters.yml new file mode 100644 index 0000000..6e77300 --- /dev/null +++ b/envs/example/ci/group_vars/jenkins-masters.yml @@ -0,0 +1,5 @@ +--- +ci: + users: + jenkins: + home: /var/lib/jenkins diff --git a/envs/example/ci/heat_stack.yml b/envs/example/ci/heat_stack.yml new file mode 100644 index 0000000..2357485 --- /dev/null +++ b/envs/example/ci/heat_stack.yml @@ -0,0 +1,90 @@ +heat_template_version: 2013-05-23 + +description: HOT template for CI + +parameters: + image: + type: string + description: Name of image to use for servers + default: ubuntu-14.04 + flavor: + type: string + description: Flavor to use for servers + default: m1.small + net_id: + type: string + description: ID of Neutron network into which servers get deployed + default: ba0fdd03-72b5-41eb-bb67-fef437fd6cb4 + +resources: + + server_security_group: + type: OS::Neutron::SecurityGroup + properties: + description: Security group for SC testing. + name: test-security-group + rules: + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 22 + port_range_max: 22 + - remote_ip_prefix: 0.0.0.0/0 + protocol: icmp + + sc_ssh_key: + type: OS::Nova::KeyPair + properties: + save_private_key: true + name: sitecontroller + + jenkins01: + type: OS::Nova::Server + properties: + name: jenkins01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + cdmaster01: + type: OS::Nova::Server + properties: + name: cdmaster01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + imagebuilder01: + type: OS::Nova::Server + properties: + name: imagebuilder01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + +outputs: + jenkins01: + description: IP address of jenkins01 in provider network + value: { get_attr: [ jenkins01, first_address ] } + cdmaster01: + description: IP address of cdmaster01 in provider network + value: { get_attr: [ cdmaster01, first_address ] } + imagebuilder01: + description: IP address of imagebuilder01 in provider network + value: { get_attr: [ imagebuilder01, first_address ] } + private_key: + description: Private key + value: { get_attr: [ sc_ssh_key, private_key ] } + diff --git a/envs/example/ci/hosts b/envs/example/ci/hosts new file mode 100644 index 0000000..d6b0223 --- /dev/null +++ b/envs/example/ci/hosts @@ -0,0 +1,18 @@ +[jenkins-masters] +jenkins01 + +[jenkins-slaves] +jenkins01 + +[cd-targets] +jenkins01 + +[cd-masters] +cdmaster01 + +[imagebuilder] +imagebuilder01 + +[sshproxy] +jenkins01 + diff --git a/envs/example/ci/vagrant.yml b/envs/example/ci/vagrant.yml new file mode 100644 index 0000000..5344837 --- /dev/null +++ b/envs/example/ci/vagrant.yml @@ -0,0 +1,23 @@ +default: + memory: 512 + cpus: 1 + +vms: + jenkins01: + ip_address: + - 172.16.0.101 + - 172.16.1.101 + memory: 512 + cdmaster01: + ip_address: + - 172.16.0.102 + - 172.16.1.102 + memory: 512 + imagebuilder01: + ip_address: + - 172.16.0.103 + - 172.16.1.103 + memory: 256 + +cd: + target_user: vagrant diff --git a/envs/example/ci/vars_heat.yml b/envs/example/ci/vars_heat.yml new file mode 100644 index 0000000..8fe2319 --- /dev/null +++ b/envs/example/ci/vars_heat.yml @@ -0,0 +1,14 @@ +--- +datacenter: ci +public_interface: ansible_eth0 +private_interface: ansible_eth0 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" +admin_user: ubuntu + +common: + users: + - name: ubuntu + #pass: + public_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsHndVMf3mu6THq7Fde7TC2SjfRlfPpBfQbMwA4HS44NljWOBuFGUyE3roRxSvGxEamPH79TXKURegLZEuh1l92ADrDEU4SpcHgUjIfyQwH5SP0Y2/uKRKpj26MbCx8yCyV9ra7YpLYvIFzxiLtp7xN2zu53mvhxHzj1SK7YkkAvmYa7At2yTBsyBu7+MTGtYCpPC1YsP7IZbc900HwwffBJo011puySHxV4xWi8lxqG43lqx0d1BILITMPXXR6QzOciB5wfsTHMTf6o4/Hzk4URjKLIbEfr1lby8rE+aKWEN2GuSuwrw7XERQuSr1PRi5pJWvLNfbyOT9TzO8DOkf diff --git a/envs/example/consul/group_vars/all b/envs/example/consul/group_vars/all new file mode 100644 index 0000000..a30d28a --- /dev/null +++ b/envs/example/consul/group_vars/all @@ -0,0 +1,4 @@ +consul: + bind_interface: "{{ public_interface }}" + bootstrap_expect: 3 + client_address: "0.0.0.0" diff --git a/envs/example/consul/hosts b/envs/example/consul/hosts new file mode 100644 index 0000000..af328dc --- /dev/null +++ b/envs/example/consul/hosts @@ -0,0 +1,4 @@ +[consul] +consul-01 +consul-02 +consul-03 diff --git a/envs/example/consul/vagrant.yml b/envs/example/consul/vagrant.yml new file mode 100644 index 0000000..8a19a06 --- /dev/null +++ b/envs/example/consul/vagrant.yml @@ -0,0 +1,11 @@ +default: + memory: 512 + cpus: 1 + +vms: + consul-01: + ip_address: 172.16.0.22 + consul-02: + ip_address: 172.16.0.23 + consul-03: + ip_address: 172.16.0.24 diff --git a/envs/example/defaults.yml b/envs/example/defaults.yml new file mode 100644 index 0000000..eadea2a --- /dev/null +++ b/envs/example/defaults.yml @@ -0,0 +1,324 @@ +--- +site_abrv: example +stack_name: example +datacenter: example +deploy_type: example +public_device_interface: eth1 +private_device_interface: eth0 +public_interface: ansible_eth1 +private_interface: ansible_eth0 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" +admin_user: blueboxadmin + +env_vars: {} + +sitecontroller: + apt: + force_cache_update: true +# python: +# pypi_mirror: https://pypi-mirror.example.com/root/pypi +# trusted_host: pypi-mirror.example.com +# ruby: +# gem_sources: +# - https://gem-mirror.example.com +# ubuntu_mirror: https://apt-mirror.example.com/trusty/ubuntu + +secrets: + sensu: + server: + rabbitmq: + admin: w4HLrz8DHtB84shd + +common: + sysdig: + enabled: False + firewall: + friendly_networks: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + ssh: + allow_from: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + disable_dns: True + users: + - name: blueboxadmin + #pass: + public_keys: [] + + hwraid: + enabled: false + +sshagentmux: + enabled: False + +database: + host: 172.16.0.17 + port: 3306 + users: + graphite: + databases: + - graphite + username: graphite + password: graphite + host: '172.16.0.%' + grafana: + databases: + - grafana + username: grafana + password: grafana + host: '172.16.0.%' + +serverspec: + enabled: True + +sensu: + client: + enable_metrics: true + enable_checks: true + +openid_proxy: + locations: + root: + path: "/" + auth_type: "openid-connect" + sensu: + proxy: "http://172.16.0.15:3000/" + auth_type: "openid-connect" + firewall: + - port: 80 + protocol: tcp + src: + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 443 + protocol: tcp + src: + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + +logging: + enabled: true + follow: + global_fields: + customer_id: "0" + cluster_name: "vagrant" + forward: + host: 172.16.0.13 + +logstash: + filters: + - template: filter-drop-empty + - template: filter-tags + - template: filter-syslog + - template: filter-openstack + outputs: + - name: elasticsearch + config: + hosts: "127.0.0.1:9200" + index: logstash-%{+YYYY.MM.dd} + manage_template: false + firewall: + - port: 1514 + protocol: tcp + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 1515 + protocol: tcp + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 4560 + protocol: tcp + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 4561 + protocol: tcp + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + +apt_repos: + docker: + repo: http://apt.dockerproject.org/repo/ + key_url: http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF76221572C52609D + hwraid: + repo: http://hwraid.le-vert.net/ubuntu + key_url: http://hwraid.le-vert.net/debian/hwraid.le-vert.net.gpg.key + sensu: + repo: http://repositories.sensuapp.org/apt + key_url: http://repositories.sensuapp.org/apt/pubkey.gpg + percona: + repo: http://repo.percona.com/apt + key_url: http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9334A25F8507EFA5 + rabbitmq: + repo: http://www.rabbitmq.com/debian + key_url: https://www.rabbitmq.com/rabbitmq-release-signing-key.asc + erlang: + repo: http://packages.erlang-solutions.com/debian + key_url: https://packages.erlang-solutions.com/debian/erlang_solutions.asc +# aptly: +# repo: https://apt-mirror.openstack.blueboxgrid.com/aptly/squeeze +# key_url: https://apt-mirror.openstack.blueboxgrid.com/keys/aptly.key + elasticsearch: + repo: http://artifacts.elastic.co/packages/5.x/apt/ + key_url: https://artifacts.elastic.co/GPG-KEY-elasticsearch + logstash: + repo: http://artifacts.elastic.co/packages/5.x/apt + key_url: https://artifacts.elastic.co/GPG-KEY-elasticsearch + kibana: + repo: http://artifacts.elastic.co/packages/5.x/apt + key_url: https://artifacts.elastic.co/GPG-KEY-elasticsearch + jenkins: + repo: https://pkg.jenkins.io/debian + key_url: https://pkg.jenkins.io/debian/jenkins.io.key + flapjack: + repo: http://packages.flapjack.io/deb/v1/ + key_url: http://pgp.mit.edu/pks/lookup?op=get&search=0x8406B0E3803709B6 + grafana: + repo: http://packagecloud.io/grafana/stable/debian/ + key_url: https://packagecloud.io/gpg.key + filebeat: + repo: http://packages.elastic.co/beats/apt + key_url: http://packages.elastic.co/GPG-KEY-elasticsearch + sensu_checks: + repo: ~ + key_url: ~ + +pxe_files: True + +pxe: + enable_server: True + servers: + - name: test + mac: 08:00:27:09:4C:2B + ipmi: 10.254.18.27 + os: trusty + preseed: default_preseed.cfg + network: ~ + +dnsmasq: + interface: lo + server: 127.0.0.1 + dns: + hosts: [] + +# creates http://172.16.0.18:8098/git/mirror/noVNC.git +git_server: + web: + port: 8098 + path: /srv/git + authorized_keys: [] + mirrors: + - name: noVNC + url: https://github.com/kanaka/noVNC.git + +pypi_mirror: + cron: false + +consul: + bind: "{{ private_ipv4.address }}" + bootstrap_expect: 1 + +kibana: + force_config: true + +elasticsearch: + restart_on_config: true # enable in dev for easier testing, disable in prod + +grafana: + dashboards: + path: /usr/share/grafana/public/dashboards + public: + enabled: + - bbc-standard-sla.json + - bbc-basic-host.json + - cleversafe-standard-sla.json + - bbc-ceph-usage.json + +percona: + root_password: asdf + galera_version: 3.x + client_version: 5.6 + server_version: 5.6 + sst_auth_user: sst_admin + sst_auth_password: asdf + wsrep_cluster_name: example-sitecontroller + ip: "{{ hostvars[inventory_hostname][private_interface]['ipv4']['address'] }}" + +git_repos: + ursula: https://github.com/blueboxgroup/ursula.git + +support_tools: + enabled: True + git: + - name: ursula-master + path: /opt/git/ursula/master + owner: "{{ admin_user }}" + repo: "{{ git_repos.ursula }}" + rev: master + auto_update: true + virtualenvs: + - name: ursula-master + path: /opt/venv/ursula/master + owner: "{{ admin_user }}" + requirements: /opt/git/ursula/master/requirements.txt + alternatives: + - name: ursula + path: /opt/venv/ursula/master/bin/ursula + link: /usr/local/bin/ursula + +bbg_ssl: + intermediate: ~ + cert: | + -----BEGIN CERTIFICATE----- + MIIDbjCCAlagAwIBAgIJANyptbhIpO8TMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV + BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX + aWRnaXRzIFB0eSBMdGQwHhcNMTUwMTE0MTg0MTQyWhcNMTYwMTE0MTg0MTQyWjBF + MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 + ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEA1MWu8kJ20FCLbBIMqgTlKCOL54X06C0bERm3wIOwu6dk35s7uy78I2pt + dA2sbwLeIMiJHKY85eBNI+pMZGNsRYajl3BZRvSWcjO6DHGN8k0ljf6gvzAlzyG0 + 2Pz0Th1R2fveOE0fNZcT/JqbKLFb/Cu3GoaC/wUAbRK36qgzQkX4hQD0QVylhdmS + 82Fsr6H4fl7iybn7w1HwA2DG4MuJRjCeskujfz0ch7/BBdON84SDVGcemHkO45R/ + c6n49jnTHiJ5CJXsZz4uH8lEs0Q6CW2GMtVbPiXfJ8TLBdSgMp6atVMCMNsa0Pg1 + I5hXAqknZf9Uc3KWdBRkufJrwHnFuwIDAQABo2EwXzAPBgNVHREECDAGhwSsEAAN + MB0GA1UdDgQWBBQzjGYx1ss5NQuRGAf3nk7KenvLyTAfBgNVHSMEGDAWgBQzjGYx + 1ss5NQuRGAf3nk7KenvLyTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IB + AQBAcq4Fe5+5jeRBqS0R9EApBeQZfCV+h88UpzAsWWhxicft1BUiftMzAwAE0VnG + xTB96jmlmHXjSR8ugCED4A6wkjW3mDo5SmkWLQBCY1EHSUdIgVbhK4zxP4TLhGbD + 54+nGAsRLM1Hb5UlI1uCa4E/1gdeLUd41vSfKc8/A133Rl1CMpFLHuMgE1VgVQkU + ElkHVD8xQlOrza/yMT1eGg4tbR4ukqjdC4vOWqGPmajlR+gk/sJ2Ut1CzN4fcmtj + yqcwxri9aiDB1mimS+m/SjKvPf6lV5bDRtQWXbCPAWzq8gKvv2PqoNyyZOxbqJhM + eb0+9ULVAGAYtcZuNHgs0bH+ + -----END CERTIFICATE----- + key: | + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDUxa7yQnbQUIts + EgyqBOUoI4vnhfToLRsRGbfAg7C7p2Tfmzu7Lvwjam10DaxvAt4gyIkcpjzl4E0j + 6kxkY2xFhqOXcFlG9JZyM7oMcY3yTSWN/qC/MCXPIbTY/PROHVHZ+944TR81lxP8 + mpsosVv8K7cahoL/BQBtErfqqDNCRfiFAPRBXKWF2ZLzYWyvofh+XuLJufvDUfAD + YMbgy4lGMJ6yS6N/PRyHv8EF043zhINUZx6YeQ7jlH9zqfj2OdMeInkIlexnPi4f + yUSzRDoJbYYy1Vs+Jd8nxMsF1KAynpq1UwIw2xrQ+DUjmFcCqSdl/1RzcpZ0FGS5 + 8mvAecW7AgMBAAECggEAeP6BYdpR3lwvLKGG+hgWiCDOqjYO8wjTX4IUcDFzCwNB + 5bZM3UD2uN0IqPotmGM1Fcdz0QrnjoFi3I2cK2ouY8sQtEl7O1JTS1YG8pSQd71P + IdQubQpgNc2hHdOayeD6bs8/qxyQJtVm1DrHCPjyqg/h6/+Z9pNNjrkaRSKpI4HF + Jtuj8iRHV9yWDoNU1eWO6qzoEU1fNE48XVefpLpVDJx0u4Ih8/BHguTBH1DO5qQt + sNkBUNh09Fy3ZqM2085ZMmi+W+EqtNJyYsB7J2324Bga7Voo71s9HNDZWLWHTIYW + jaPBRvn4mjlVua1o8Dd3/RQ2JVzwiv/aZ47IGw3cgQKBgQDwco2IduJkWHnOyPKZ + +uo4xmYBa8UswBMqmfPaTijZ4naG9J34LjZFtvsJhSqOrdKEYbP3ONz3UwvOFUQg + kzC/F5TJidDzNXPiLd+3NpssofnVwLKdZUwORkLoBph7oAY2fImaoWIGfPsHyam5 + o4RWbsyhxKozFyBn2NBIyxQ0EQKBgQDiiN6rsHusVdH5OreO6Z8RvB9ULUH8seB+ + f1oYiHSfPZQ4fkwsB8k9gF6yJOb/Kob3FSoc5GRpd4TK9DXHTZh41Q3/CSwveYYa + 5BJlupXGgAzu6vFuyUxAw/TclZFD9uAjrlfu8lbRpBHzKZtJnJQtgNPFflH6ZxbY + ljlNO4f5CwKBgA42l0szy9omqLyigES94k6M28bFuhgVGozwIMwMxrlqe5sqppPf + F3IziM9dQdDBUaplpB+/CsDL9eyusSJD0SPanv7y2Jkn1bvO/mR0I+QVhxEtnOFU + 9ZP6b0YL7cORCAz8e53aYFMF3EjvkMracZ4yWoJNf8oZWd8Jn/ZNmtohAoGAN/yF + s78BQb1QEJ2PYhWhB5wLzh0FUvOPPRQyax/GWti4OiIUp0khVj7UqIhwQp37DzO+ + 3bcgjeRJAHPMmr41sZ9OPzrAHdeV6i110oiDnbRl/eI42x2K5/LGIIIijb6E9KyQ + 9PAVvugiu4sL4ux8vqY5MHUgw5cY0VyHOuw8lbMCgYBPZtqsJs+zrkZj9HD+lQvo + N2CQtonrnIwqaorgbZVr1PDBJo0AHBraQRt/0Xpzu4Q/rjkHxlhESjW17Ax3JS1s + gRwuu+SIjud/7fZcGy8to7MbdJrWiYFUOlQF6F/kEIq3bsHmJWbi1FgTdeQkkEl6 + DFeUkc2q0uaO1lZIAnMA4w== + -----END PRIVATE KEY----- diff --git a/envs/example/elk/group_vars/all.yml b/envs/example/elk/group_vars/all.yml new file mode 100644 index 0000000..cefd5ff --- /dev/null +++ b/envs/example/elk/group_vars/all.yml @@ -0,0 +1,97 @@ +--- +public_interface: ansible_eth1 +private_interface: ansible_eth2 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +elasticsearch: + config: + cluster.name: "elk_{{ stack_name }}" + #index.number_of_replicas: 1 + discovery.zen.minimum_master_nodes: "{{ (groups['elasticsearch']|length / 2)|int + (groups['elasticsearch']|length % 2 > 0)|int }}" + discovery.zen.ping.unicast.hosts: "['{{ hostvars['elk01'][private_interface]['ipv4']['address'] }}','{{ hostvars['elk02'][private_interface]['ipv4']['address'] }}']" + gateway.expected_nodes: "{{ groups['elasticsearch']|length }}" + gateway.recover_after_nodes: "{{ (groups['elasticsearch']|length / 2)|int + (groups['elasticsearch']|length % 2 > 0)|int }}" + cluster.routing.allocation.disk.watermark.low: 10gb + cluster.routing.allocation.disk.watermark.high: 5gb + node.name: "{{ ansible_hostname }}" + path.conf: /etc/elasticsearch + path.data: /var/lib/elasticsearch + path.logs: /var/log/elasticsearch + http.bind_host: "0.0.0.0" + network.publish_host: "{{ public_ipv4.address }}" + curator: + tasks: + - action: delete + older: 2 + hour: 2 + - action: close + older: 2 + hour: 2 + minute: 15 + - action: optimize + params: "--max_num_segments 1" + older: 2 + hour: 3 + minute: 0 + - action: bloom + state: absent + firewall: + - port: 9200 + src: + - "{{ hostvars['elk01'][private_interface]['ipv4']['address'] }}/32" + - "{{ hostvars['elk02'][private_interface]['ipv4']['address'] }}/32" + - port: 9300 + src: + - "{{ hostvars['elk01'][private_interface]['ipv4']['address'] }}/32" + - "{{ hostvars['elk02'][private_interface]['ipv4']['address'] }}/32" + heap_size: 512m + restart_on_config: true + +logging: + forward: + host: "{{ hostvars['elk01'][private_interface]['ipv4']['address'] }}" + +apache: + listen: + - 80 + - 443 + +openid_proxy: + locations: + root: + path: "/" + kibana: + proxy: "http://127.0.0.1:5601/" + url: "/kibana/" + elasticsearch: + proxy: "http://127.0.0.1:9200/" + url: "/kibana/elasticsearch/" + hq: + proxy: "http://127.0.0.1:9200/_plugin/HQ/" + url: "/hq/?url=http://ip_of_host/kibana/elasticsearch/" + head: + proxy: "http://127.0.0.1:9200/_plugin/head/" + url: "/head/?base_uri=http://ip_of_host/kibana/elasticsearch/" + bigdesk: + proxy: "http://127.0.0.1:9200/_plugin/bigdesk/" + url: "/bigdesk/?endpoint=http://ip_of_host/kibana/elasticsearch/&connect=true" + admin: + apache: + state: present + users: + - username: admin + password: admin + listen: + admin_ip: "*" + admin_port: 80 + admin_port_ssl: 443 + firewall: + - port: 80 + protocol: tcp + src: + - 0.0.0.0/0 + - port: 443 + protocol: tcp + src: + - 0.0.0.0/0 diff --git a/envs/example/elk/heat_stack.yml b/envs/example/elk/heat_stack.yml new file mode 100644 index 0000000..f883a0f --- /dev/null +++ b/envs/example/elk/heat_stack.yml @@ -0,0 +1,117 @@ +heat_template_version: 2013-05-23 + +description: HOT template for ELK Nodes + +parameters: + image: + type: string + description: Name of image to use for servers + default: ubuntu-14.04 + flavor: + type: string + description: Flavor to use for servers + default: m1.medium + net_id: + type: string + description: ID of Neutron network into which servers get deployed + default: 54992b8d-6d53-4f90-9956-78cea7c01003 + floating_ip_pool: + type: string + description: name of floating ip pool to use + default: external + +resources: + + server_security_group: + type: OS::Neutron::SecurityGroup + properties: + description: Security group for SC testing. + name: test-security-group + rules: + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 22 + port_range_max: 22 + - remote_ip_prefix: 0.0.0.0/0 + protocol: icmp + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 80 + port_range_max: 80 + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 443 + port_range_max: 443 + - remote_ip_prefix: 192.168.0.0/22 + protocol: icmp + - remote_ip_prefix: 192.168.0.0/22 + protocol: tcp + port_range_min: 1 + port_range_max: 65535 + - remote_ip_prefix: 192.168.0.0/22 + protocol: udp + port_range_min: 1 + port_range_max: 65535 + + sc_ssh_key: + type: OS::Nova::KeyPair + properties: + save_private_key: true + name: sitecontroller + + elk01: + type: OS::Nova::Server + properties: + name: elk01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + elk01_floating_ip: + type: OS::Nova::FloatingIP + properties: + pool: { get_param: floating_ip_pool } + + elk01_fip_association: + type: OS::Nova::FloatingIPAssociation + properties: + floating_ip: { get_resource: elk01_floating_ip } + server_id: { get_resource: elk01 } + + elk02: + type: OS::Nova::Server + properties: + name: elk02 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + elk02_floating_ip: + type: OS::Nova::FloatingIP + properties: + pool: { get_param: floating_ip_pool } + + elk02_fip_association: + type: OS::Nova::FloatingIPAssociation + properties: + floating_ip: { get_resource: elk02_floating_ip } + server_id: { get_resource: elk02 } + +outputs: + elk01: + description: IP address of elk01 in provider network + value: { get_attr: [ elk01_floating_ip, ip ] } + elk02: + description: IP address of elk02 in provider network + value: { get_attr: [ elk02_floating_ip, ip ] } + private_key: + description: Private key + value: { get_attr: [ sc_ssh_key, private_key ] } diff --git a/envs/example/elk/hosts b/envs/example/elk/hosts new file mode 100644 index 0000000..24dd729 --- /dev/null +++ b/envs/example/elk/hosts @@ -0,0 +1,20 @@ +[elk:children] +elasticsearch +logstash +kibana + +[elasticsearch] +elk01 +elk02 + +[logstash] +elk01 +elk02 + +[kibana] +elk01 +elk02 + +[openid_proxy] +elk01 +elk02 diff --git a/envs/example/elk/vagrant.yml b/envs/example/elk/vagrant.yml new file mode 100644 index 0000000..829c2c8 --- /dev/null +++ b/envs/example/elk/vagrant.yml @@ -0,0 +1,18 @@ +default: + memory: 2048 + cpus: 2 +# LULZ WAT? needed a xenial box that had old style ethernet naming, as the newer predictive network +# names are affected by the number of networks a person has configured in virtualbox +# making it inconsistent across development machines. + box_url: https://atlas.hashicorp.com/cloudtoad/boxes/xenial_server_base/versions/1.2/providers/virtualbox.box + box_name: envimation/ubuntu-xenial + +vms: + elk01: + ip_address: + - 172.16.0.13 + - 172.16.1.13 + elk02: + ip_address: + - 172.16.0.14 + - 172.16.1.14 diff --git a/envs/example/elk/vars_heat.yml b/envs/example/elk/vars_heat.yml new file mode 100644 index 0000000..ca7681f --- /dev/null +++ b/envs/example/elk/vars_heat.yml @@ -0,0 +1,22 @@ +--- +datacenter: ci +public_interface: ansible_eth0 +private_interface: ansible_eth0 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" +admin_user: blueboxadmin + +common: + users: + - name: blueboxadmin + #pass: + public_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsHndVMf3mu6THq7Fde7TC2SjfRlfPpBfQbMwA4HS44NljWOBuFGUyE3roRxSvGxEamPH79TXKURegLZEuh1l92ADrDEU4SpcHgUjIfyQwH5SP0Y2/uKRKpj26MbCx8yCyV9ra7YpLYvIFzxiLtp7xN2zu53mvhxHzj1SK7YkkAvmYa7At2yTBsyBu7+MTGtYCpPC1YsP7IZbc900HwwffBJo011puySHxV4xWi8lxqG43lqx0d1BILITMPXXR6QzOciB5wfsTHMTf6o4/Hzk4URjKLIbEfr1lby8rE+aKWEN2GuSuwrw7XERQuSr1PRi5pJWvLNfbyOT9TzO8DOkf + sysdig: + enabled: False + firewall: + friendly_networks: [] + ssh: + allow_from: + - "0.0.0.0/0" + disable_dns: True diff --git a/envs/example/ipmi-proxy/group_vars/all.yml b/envs/example/ipmi-proxy/group_vars/all.yml new file mode 100644 index 0000000..41d2d8d --- /dev/null +++ b/envs/example/ipmi-proxy/group_vars/all.yml @@ -0,0 +1,59 @@ +--- +public_interface: ansible_eth1 +private_interface: ansible_eth2 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +common: + firewall: + friendly_networks: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + ssh: + allow_from: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +ipmi_proxy: + datacenters: + - name: lab01 + data_center_uuid: 7cd69350-35e8-4677-98fe-aad60fc9e191 + backend_source_ip: 172.16.0.16 + - name: lab02 + data_center_uuid: eb1e05be-a1f5-46a8-9808-cbb033979f4f + backend_source_ip: 172.16.0.16 + ip_pool: + - 172.16.1.21 + - 172.16.1.22 + apache: + allow_from: + - 127.0.0.1 + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + +apache: + listen: + - 80 + - 443 + - 81 + - 444 + - 82 + - 445 + - 83 + - 446 + - 8091 + +openid_proxy: + locations: + ipmi: + proxy: "http://127.0.0.1:8091/" + url: "/ipmi/" + config: + - RequestHeader: "set X-Proxy-Remote-User %{REMOTE_USER}e env=REMOTE_USER" + firewall: + - port: 80 + protocol: tcp + src: + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 443 + protocol: tcp + src: + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" diff --git a/envs/example/ipmi-proxy/hosts b/envs/example/ipmi-proxy/hosts new file mode 100644 index 0000000..9df7aee --- /dev/null +++ b/envs/example/ipmi-proxy/hosts @@ -0,0 +1,5 @@ +[ipmi-proxy] +ipmi-proxy01 + +[openid_proxy] +ipmi-proxy01 diff --git a/envs/example/ipmi-proxy/vagrant.yml b/envs/example/ipmi-proxy/vagrant.yml new file mode 100644 index 0000000..c035f4c --- /dev/null +++ b/envs/example/ipmi-proxy/vagrant.yml @@ -0,0 +1,9 @@ +default: + memory: 512 + cpus: 1 + +vms: + ipmi-proxy01: + ip_address: + - 172.16.0.16 + - 172.16.1.16 diff --git a/envs/example/ipsec/README.md b/envs/example/ipsec/README.md new file mode 100644 index 0000000..b60b16b --- /dev/null +++ b/envs/example/ipsec/README.md @@ -0,0 +1,122 @@ +IPSEC Example Environment +------------------------- + +Environment conists of 4 servers ... two IPSEC servers that create a point-to-point VPN over eth1. Each server has a second network on eth2. There are two regular servers with no ansible roles `test1` and `test2`. These are on the second networks of each of the IPSEC servers. + +If using StrongSwan, both the package and service name will be `strongswan` (default in the environment). If using OpenSwan, the ipsec.implementation.package will be `openswan` and the ipsec.implementation.service will be `ispec` (look in the comments below for configuration). + +Bring up the environment: + +``` +$ ursula --vagrant envs/example/ipsec site.yml +``` + +Set up static routes on the test servers: + +``` +$ vagrant ssh test1 -c "sudo ip route add 172.16.20.0/24 via 172.16.10.100" +$ vagrant ssh test2 -c "sudo ip route add 172.16.10.0/24 via 172.16.20.100" +``` + +Run tcpdump on one of the ipsec servers and leave it running: + +``` +$ vagrant ssh ipsec-server -c 'sudo tcpdump -n -i eth1 esp or udp port 500 or udp port 4500' +``` + +and then try to ping from one test server to the other from another terminal window: + +``` +$ vagrant ssh test2 -c 'ping 172.16.10.200' +PING 172.16.10.200 (172.16.10.200) 56(84) bytes of data. +64 bytes from 172.16.10.200: icmp_seq=1 ttl=62 time=0.933 ms +64 bytes from 172.16.10.200: icmp_seq=2 ttl=62 time=1.06 ms +``` + +In the tcpdump session you should see the following: + +``` +21:42:57.120282 IP 172.16.0.101.4500 > 172.16.0.100.4500: UDP-encap: ESP(spi=0xf7893654,seq=0x8), length 132 +21:42:57.120680 IP 172.16.0.100.4500 > 172.16.0.101.4500: UDP-encap: ESP(spi=0xda5b24b4,seq=0x8), length 132 +21:42:58.124660 IP 172.16.0.101.4500 > 172.16.0.100.4500: UDP-encap: ESP(spi=0xf7893654,seq=0x9), length 132 +``` + +woot! this means the VPN is working. + + + + + + diff --git a/envs/example/ipsec/group_vars/all b/envs/example/ipsec/group_vars/all new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/envs/example/ipsec/group_vars/all @@ -0,0 +1 @@ +--- diff --git a/envs/example/ipsec/host_vars/ipsec-client.yaml b/envs/example/ipsec/host_vars/ipsec-client.yaml new file mode 100644 index 0000000..e8557db --- /dev/null +++ b/envs/example/ipsec/host_vars/ipsec-client.yaml @@ -0,0 +1,38 @@ +--- +ipsec: + implementation: + package: strongswan + service: strongswan + nat_enabled: False + config: + nat_traversal: "yes" + virtual_private: "%v4:{{ hostvars['ipsec-client'][private_interface]['ipv4']['network'] }}/{{ hostvars['ipsec-client'][private_interface]['ipv4']['netmask'] }}" + connections: + example: + authby: "secret" + auto: "start" + compress: "no" + dpdaction: restart + dpddelay: 30 + dpdtimeout: 120 + esp: "aes256-sha256-modp2048" + forceencaps: "yes" + ike: "aes256-sha256-modp2048" + ikelifetime: 86400s + keyingtries: "%forever" + keylife: 3600s + leftupdown: "/etc/ipsec.d/ipsec-notify.sh" + mark: 500 + type: "tunnel" + rekeymargin: 540s + rekey: "yes" + left: "{{ hostvars['ipsec-client'][public_interface]['ipv4']['address'] }}" + leftsubnet: "{{ hostvars['ipsec-client'][private_interface]['ipv4']['network'] }}/{{ hostvars['ipsec-client'][private_interface]['ipv4']['netmask'] }}" + left_vti_ip: "169.254.0.1" + right: "{{ hostvars['ipsec-server'][public_interface]['ipv4']['address'] }}" + rightsubnet: "{{ hostvars['ipsec-server'][private_interface]['ipv4']['network'] }}/{{ hostvars['ipsec-server'][private_interface]['ipv4']['netmask'] }}" + right_vti_ip: "169.254.0.2" + sharedkeys: + example: + remote_ip: "{{ hostvars['ipsec-server'][public_interface]['ipv4']['address'] }}" + key: "dfgffk4ltjk3jkl234t234t" diff --git a/envs/example/ipsec/host_vars/ipsec-server.yml b/envs/example/ipsec/host_vars/ipsec-server.yml new file mode 100644 index 0000000..dcbfa64 --- /dev/null +++ b/envs/example/ipsec/host_vars/ipsec-server.yml @@ -0,0 +1,36 @@ +--- +ipsec: + implementation: + package: strongswan + service: strongswan + nat_enabled: False + config: + nat_traversal: "yes" + virtual_private: "%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12,%v4:!{{ hostvars['ipsec-client'][private_interface]['ipv4']['network'] }}/{{ hostvars['ipsec-client'][private_interface]['ipv4']['netmask'] }}" + connections: + example: + authby: "secret" + auto: "start" + compress: "no" + dpdaction: restart + dpddelay: 30 + dpdtimeout: 120 + esp: "aes256-sha256-modp2048" + forceencaps: "yes" + ike: "aes256-sha256-modp2048" + ikelifetime: 86400s + keyingtries: "%forever" + keylife: 3600s + leftupdown: "/etc/ipsec.d/ipsec-notify.sh" + mark: 500 + type: "tunnel" + rekeymargin: 540s + rekey: "yes" + left: "{{ hostvars['ipsec-server'][public_interface]['ipv4']['address'] }}" + leftsubnet: "{{ hostvars['ipsec-server'][private_interface]['ipv4']['network'] }}/{{ hostvars['ipsec-server'][private_interface]['ipv4']['netmask'] }}" + right: "{{ hostvars['ipsec-client'][public_interface]['ipv4']['address'] }}" + rightsubnet: "{{ hostvars['ipsec-client'][private_interface]['ipv4']['network'] }}/{{ hostvars['ipsec-client'][private_interface]['ipv4']['netmask'] }}" + sharedkeys: + example: + remote_ip: "{{ hostvars['ipsec-client'][public_interface]['ipv4']['address'] }}" + key: "dfgffk4ltjk3jkl234t234t" diff --git a/envs/example/ipsec/hosts b/envs/example/ipsec/hosts new file mode 100644 index 0000000..b1fdaf4 --- /dev/null +++ b/envs/example/ipsec/hosts @@ -0,0 +1,3 @@ +[ipsec] +ipsec-server +ipsec-client diff --git a/envs/example/ipsec/vagrant.yml b/envs/example/ipsec/vagrant.yml new file mode 100644 index 0000000..1c4c373 --- /dev/null +++ b/envs/example/ipsec/vagrant.yml @@ -0,0 +1,17 @@ +default: + memory: 512 + cpus: 1 + +vms: + ipsec-server: + ip_address: + - 172.16.0.100 + - 172.16.10.100 + ipsec-client: + ip_address: + - 172.16.0.101 + - 172.16.20.100 + test1: + ip_address: 172.16.10.200 + test2: + ip_address: 172.16.20.200 diff --git a/envs/example/jenkins/group_vars/all b/envs/example/jenkins/group_vars/all new file mode 100644 index 0000000..68ec972 --- /dev/null +++ b/envs/example/jenkins/group_vars/all @@ -0,0 +1,9 @@ +--- +public_interface: ansible_eth2 +private_interface: ansible_eth1 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" +apache: + listen: + - 80 + - 443 diff --git a/envs/example/jenkins/hosts b/envs/example/jenkins/hosts new file mode 100644 index 0000000..8b19f6d --- /dev/null +++ b/envs/example/jenkins/hosts @@ -0,0 +1,2 @@ +[jenkins] +jenkins-01 diff --git a/envs/example/jenkins/vagrant.yml b/envs/example/jenkins/vagrant.yml new file mode 100644 index 0000000..2ecf3a9 --- /dev/null +++ b/envs/example/jenkins/vagrant.yml @@ -0,0 +1,9 @@ +default: + memory: 1024 + cpus: 1 + +vms: + jenkins-01: + ip_address: + - 172.16.0.30 + - 172.16.1.30 diff --git a/envs/example/minibootstrapper/README.md b/envs/example/minibootstrapper/README.md new file mode 100644 index 0000000..5a3400f --- /dev/null +++ b/envs/example/minibootstrapper/README.md @@ -0,0 +1,9 @@ +# Mini Bootstrapper + +Designed to create a Vagrant VM with just enough tooling to bootstrap a bootstrapper. + +Once booted and running you need to swap the Virtualbox networking for `adapter 3` to bridged on the physical network interface of your laptop. + +It runs a pxeboot server and a squid proxy. As long as it has internet access ( or VPN tether to the mirrors ) it should be able to get the bootstrapper installed. + +There are some tasks in the `pxe` role for setting the mini bootstrapper up to be able to `NAT` traffic. These are featured flagged off by default, but turned on for the environment. They are not idempotent, but that's okay because this is a disposable VM. diff --git a/envs/example/minibootstrapper/group_vars/all b/envs/example/minibootstrapper/group_vars/all new file mode 100644 index 0000000..40ae4cc --- /dev/null +++ b/envs/example/minibootstrapper/group_vars/all @@ -0,0 +1,142 @@ +--- +common: + firewall: + friendly_networks: + - 0.0.0.0/0 + +squid: + port: 3128 + allowed_networks: + - 0.0.0.0/0 + proxy_domains: + - apt-mirror.openstack.blueboxgrid.com + - file-mirror.openstack.blueboxgrid.com + - pypi-mirror.openstack.blueboxgrid.com + - gem-mirror.openstack.blueboxgrid.com + - github.com + blacklist_packages: [] + cache_dir: + size: 40000 + +pxe: + tftpboot_path: /opt/pxe/tftpboot + tftp_interface: "{{ private_device_interface }}" + tftp_server: "{{ private_ipv4.address }}" + dhcp_ranges: + - tag: default + range: 172.16.1.50,172.16.1.55,255.255.255.0,2h + gateway: 172.16.1.125 + dns: 8.8.8.8 + nat: + enabled: True + interface_in: eth2 + interface_out: eth0 + # for serial_com: COM1=0, COM2=1, and COM3=2. Overrideable on each host. + serial_com: ~ + ks_interface: auto + mirror_http_proxy: http://172.16.1.125:3128 + # LOCAL with Public Mirrors + mirror_http_hostname: archive.ubuntu.com + mirror_http_directory: /ubuntu/ + # LOCAL with Internal BBC Mirrors + # mirror_http_hostname: apt-mirror.openstack.bbg + # mirror_http_directory: /archive.ubuntu.com/ubuntu + # LOCAL with Public BBC Mirrors + # mirror_http_directory: /archive.ubuntu.com/ubuntu + + timeout: 100 + os: + - name: trusty + kernel: https://file-mirror.openstack.blueboxgrid.com/pxe/trusty/linux + bootloader: https://file-mirror.openstack.blueboxgrid.com/pxe/trusty/initrd.gz +# mirror_http_hostname: apt-mirror.openstack.blueboxgrid.com +# mirror_http_directory: /archive.ubuntu.com/ubuntu + mirror_http_proxy: http://172.16.1.125:3128 + root_password: password + kernel_image: + ntp_server: + packages: + - curl + - openssh-server + - build-essential + - sudo + - wget + ssh_pub_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC5slg9GeZdOxYljn0dDqk3/ZaYLRtO7ll9d8kZQTV0c2jfndHPsvokJhHkBmKcL9cdR1VCow9KpzthIXaCVcMlKaps4jDKJQJKoWSlN7ZxM8ZlMwDVNV8gp+L65JYK8yEqDk4lPdjBDzMrC2aFscvhJWBIPoKbXZrrXtuOVNwjLSGSV0hD8O0AzMgsL/dNJM2dI/WPhdP0VVvNicLBZ5089HSgQCj6zKpQnHgtHzsCcLp4Oklo9BFftWgS4E+1Xc0hdMLumrK8ZLBy2j4U7UZWgv/p8oOsqGuC4AN480Z/u0hWhAGHDy/P/OjOIyaPm9BDH5EqXwzymYiwN73S7yX1Z5yGfVXKliaMKSqrwUJX+zbkXHO0UZqwyxqKADE+/QAjHCvI2QOecNfDQroNyBF1K+43NKHnI0i+pepqgDVN/7nrf73c+lr5wEpMF7+454bN/pLK6ylGUl4XduG+U22hIdo57eYuP86e1Cu3c035iYwLNbAY6q8su5jDn1/BElLQpLQmyzZBb/91qPR2rQ+lt98gWMK78xVV4+ywA9Yj+HzB4fzRgQrmC3O+kgMxczXbP5SLulZTr0Uq0zoPMgS4OL8YsdVTKSVA3SitfkOjOrS5b18Zk58EV0aOiAsNxGr5yU8fAqBHi05zy7V42t0wK4k80yOG7NaWRG3tWfvCvQ== test key + servers: + - name: bootstrapper + mac: A0:36:9F:7D:02:2C + serial_com: ~ + os: trusty + preseed: vagrant_preseed.cfg + network: | + auto lo + iface lo inet loopback + + auto eth0 + iface eth0 inet manual + bond-master bond0 + + auto eth1 + iface eth1 inet manual + bond-master bond0 + + auto bond0 + iface bond0 inet static + address 10.254.19.11 + netmask 255.255.255.0 + bond-mode 4 + bond-miimon 100 + bond-lacp-rate 1 + bond-slaves eth0 eth1 + + auto bond0.104 + iface bond0.104 inet static + address 10.254.22.11 + gateway 10.254.22.1 + netmask 255.255.255.0 + vlan-raw-device bond0 + root_password: password + firstuser: blueboxadmin + firstpass: password + packages: + - curl + - openssh-server + - build-essential + - sudo + - wget + - vim + - vlan + - ipmitool + +apt_mirror: + enabled: False +pypi_mirror: + enabled: False +gem_mirror: + enabled: False +file_mirror: + enabled: False + +dnsmasq: + enabled: True + interface: eth2 + firewall: + - port: 53 + src: 0.0.0.0 + to_ip: any + proto: any + dns: + # PUBLIC DNS + servers: + - 8.8.8.8 + - 8.8.4.4 + +sensu: + enabled: False + +collectd: + enabled: False + +serverspec: + enabled: False diff --git a/envs/example/minibootstrapper/hosts b/envs/example/minibootstrapper/hosts new file mode 100644 index 0000000..15ef16b --- /dev/null +++ b/envs/example/minibootstrapper/hosts @@ -0,0 +1,5 @@ +[mirror] +minibs + +[pxe] +minibs diff --git a/envs/example/minibootstrapper/vagrant.yml b/envs/example/minibootstrapper/vagrant.yml new file mode 100644 index 0000000..097dd31 --- /dev/null +++ b/envs/example/minibootstrapper/vagrant.yml @@ -0,0 +1,9 @@ +default: + memory: 512 + cpus: 1 + +vms: + minibs: + ip_address: + - 172.16.0.125 + - 172.16.1.125 diff --git a/envs/example/mirror/group_vars/all.yml b/envs/example/mirror/group_vars/all.yml new file mode 100644 index 0000000..7a2b578 --- /dev/null +++ b/envs/example/mirror/group_vars/all.yml @@ -0,0 +1,138 @@ +--- +common: + firewall: + friendly_networks: [] + sysdig: + enabled: False + +sensu: + enabled: False + +collectd: + enabled: False + +apache: + listen: + - 80 + - 443 + - 81 + - 444 + - 82 + - 445 + - 83 + - 446 + +apt_mirror: + protected_repos: ['packagecloud.io'] + repositories: + hwraid: + key_url: http://hwraid.le-vert.net/debian/hwraid.le-vert.net.gpg.key + type: deb + url: http://hwraid.le-vert.net/ubuntu + username: apt + password: apt + distributions: + - precise + components: + - main + aptly: + url: http://repo.aptly.info/ + key_url: http://pgp.mit.edu/pks/lookup?op=get&search=0xE083A3782A194991 + distributions: + - squeeze + components: + - main + architectures: + - deb + - deb-i386 + apache: + port: 80 + ssl: + enabled: True + port: 443 + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + - port: 443 + protocol: tcp + src: 0.0.0.0/0 + +pypi_mirror: + repos: + bluebox_private: + username: bluebox + password: nopenopenope + index: private + mirror: + url: https://fdec6132ec19dc59011548774d514af7005c494495107ff9:@packagecloud.io/blueboxcloud + cache_expiry: 300 + apache: + port: 81 + ip: "{{ private_ipv4.address }}" + ssl: + enabled: True + port: 444 + firewall: + - port: 81 + protocol: tcp + src: 0.0.0.0/0 + - port: 444 + protocol: tcp + src: 0.0.0.0/0 + +gem_mirror: + ip: 127.0.0.1 + port: 9292 + apache: + port: 82 + ip: "{{ private_ipv4.address }}" + ssl: + enabled: True + port: 445 + firewall: + - port: 82 + protocol: tcp + src: 0.0.0.0/0 + - port: 445 + protocol: tcp + src: 0.0.0.0/0 + +file_mirror: + files: + - name: 'ubuntu-14.04-server-cloudimg-amd64.manifest' + path: 'ubuntu' + url: 'http://cloud-images.ubuntu.com/releases/14.04/release/ubuntu-14.04-server-cloudimg-amd64.manifest' + #sha256sum: 88a864aae6f67ec7c5c5baeda973ee25054c5fcfd570acbe25ef4c655e8b01ee + auth: + - path: ubuntu + username: ubuntu + password: ubuntu + apache: + ip: "{{ private_ipv4.address }}" + port: 83 + ssl: + enabled: True + port: 446 + firewall: + - port: 83 + protocol: tcp + src: 0.0.0.0/0 + - port: 446 + protocol: tcp + src: 0.0.0.0/0 + +squid: + port: 3128 + allowed_networks: + - 172.16.1.115/24 + - 0.0.0.0/0 + proxy_domains: + - apt-mirror.openstack.blueboxgrid.com + - file-mirror.openstack.blueboxgrid.com + - pypi-mirror.openstack.blueboxgrid.com + - gem-mirror.openstack.blueboxgrid.com + - github.com + blacklist_packages: [] + cache_dir: + size: 40000 diff --git a/envs/example/mirror/heat_stack.yml b/envs/example/mirror/heat_stack.yml new file mode 100644 index 0000000..4615412 --- /dev/null +++ b/envs/example/mirror/heat_stack.yml @@ -0,0 +1,81 @@ +heat_template_version: 2013-05-23 + +description: HOT template for sitecontroller mirror server + +parameters: + image: + type: string + description: Name of image to use for servers + default: ubuntu-14.04 + flavor: + type: string + description: Flavor to use for servers + default: m1.xlarge + net_id: + type: string + description: ID of Neutron network into which servers get deployed + default: ba0fdd03-72b5-41eb-bb67-fef437fd6cb4 + floating_ip_pool: + type: string + description: name of floating ip pool to use + default: external + +resources: + + server_security_group: + type: OS::Neutron::SecurityGroup + properties: + description: Security group for SC Registry + name: sc-mirror + rules: + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 22 + port_range_max: 22 + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 80 + port_range_max: 80 + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 81 + port_range_max: 81 + - remote_ip_prefix: 0.0.0.0/0 + protocol: icmp + + sc_ssh_key: + type: OS::Nova::KeyPair + properties: + save_private_key: true + name: sc-mirror + + mirror01_floating_ip: + type: OS::Nova::FloatingIP + properties: + pool: { get_param: floating_ip_pool } + + mirror01: + type: OS::Nova::Server + properties: + name: mirror01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + mirror01_fip_association: + type: OS::Nova::FloatingIPAssociation + properties: + floating_ip: { get_resource: mirror01_floating_ip } + server_id: { get_resource: mirror01 } + +outputs: + mirror01: + description: IP address of mirror01 in provider network + value: { get_attr: [ mirror01_floating_ip, ip ] } + private_key: + description: Private key + value: { get_attr: [ sc_ssh_key, private_key ] } diff --git a/envs/example/mirror/hosts b/envs/example/mirror/hosts new file mode 100644 index 0000000..6d03419 --- /dev/null +++ b/envs/example/mirror/hosts @@ -0,0 +1,2 @@ +[mirror] +mirror01 diff --git a/envs/example/mirror/vagrant.yml b/envs/example/mirror/vagrant.yml new file mode 100644 index 0000000..7b25142 --- /dev/null +++ b/envs/example/mirror/vagrant.yml @@ -0,0 +1,9 @@ +default: + memory: 512 + cpus: 1 + +vms: + mirror01: + ip_address: + - 172.16.0.115 + - 172.16.1.115 diff --git a/envs/example/mirror/vars_heat.yml b/envs/example/mirror/vars_heat.yml new file mode 100644 index 0000000..c94f623 --- /dev/null +++ b/envs/example/mirror/vars_heat.yml @@ -0,0 +1,127 @@ +--- +datacenter: tardis +public_interface: ansible_eth0 +private_interface: ansible_eth0 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +common: + ssh: + allow_from: + - "0.0.0.0/0" + sysdig: + enabled: False + +pypi_mirror: + web: + servername: mirror.tardis.openstack.blueboxgrid.com + port: 81 + +apt_mirror: + web: + servername: mirror.tardis.openstack.blueboxgrid.com + port: 80 + precise: True + trusty: False + repositories: + docker: + key_url: 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xD8576A8BA88D21E9' + type: deb + url: 'https://get.docker.com/ubuntu' + distributions: + - docker + components: + - main + bbg_ubuntu: + url: 'http://repo.openstack.blueboxgrid.com/ubuntu/' + key_url: 'http://repo.openstack.blueboxgrid.com/blue_box_cloud.gpg.key' + type: deb + distributions: + - precise + - trusty + components: + - main + blueboxcloud_giftwrap: + url: 'https://packagecloud.io/blueboxcloud/giftwrap/ubuntu/' + key_url: 'https://packagecloud.io/gpg.key' + type: deb + distributions: + - precise + - trusty + components: + - main + hwraid: + key_url: http://hwraid.le-vert.net/debian/hwraid.le-vert.net.gpg.key + type: deb + url: http://hwraid.le-vert.net/ubuntu + distributions: + - precise + components: + - main + sensu: + url: 'http://repos.sensuapp.org/apt' + key_url: 'http://repos.sensuapp.org/apt/pubkey.gpg' + type: deb + distributions: + - sensu + components: + - main + haproxy: + url: 'http://ppa.launchpad.net/vbernat/haproxy-1.5/ubuntu' + key_url: 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xCFFB779AADC995E4F350A060505D97A41C61B9CD' + type: deb + distributions: + - precise + - trusty + components: + - main + bbg_openstack_ppa: + url: 'http://ppa.launchpad.net/blueboxgroup/openstack/ubuntu' + key_url: 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xC37BA5F849DE63CB' + type: deb + distributions: + - precise + components: + - main + percona: + url: 'http://repo.percona.com/apt' + key_url: 'https://www.percona.com/redir/downloads/RPM-GPG-KEY-percona' + type: deb + distributions: + - precise + - trusty + components: + - main + rabbitmq: + url: 'http://www.rabbitmq.com/debian/' + key_url: 'https://www.rabbitmq.com/rabbitmq-signing-key-public.asc' + type: deb + distributions: + - testing + components: + - main + cloud_archive: + url: 'http://ubuntu-cloud.archive.canonical.com/ubuntu' + type: deb + distributions: + - precise-updates/icehouse + components: + - main + erlang: + url: 'http://packages.erlang-solutions.com/debian' + key_url: 'https://packages.erlang-solutions.com/debian/erlang_solutions.asc' + type: deb + distributions: + - precise + - trusty + components: + - contrib + ceph: + url: 'http://ceph.com/debian-hammer' + key_url: 'https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/release.asc' + type: deb + distributions: + - trusty + - precise + components: + - main diff --git a/envs/example/monitor/group_vars/all.yml b/envs/example/monitor/group_vars/all.yml new file mode 100644 index 0000000..58802ff --- /dev/null +++ b/envs/example/monitor/group_vars/all.yml @@ -0,0 +1,169 @@ +--- +site_abrv: example +stack_name: example +datacenter: example +public_interface: ansible_eth2 +private_interface: ansible_eth1 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +serverspec: + enabled: True + +apache: + listen: + - 80 + - 8081 + +database: + host: "{{ hostvars[groups['percona'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}" + port: 3306 + users: + graphite: + databases: + - graphite + username: graphite + password: graphite + host: '%' + grafana: + databases: + - grafana + username: grafana + password: grafana + host: '%' + +graphite: + amqp: + enabled: False + verbose: False + host: 172.16.0.15 + port: 5672 + vhost: /graphite + user: graphite + password: graphite + exchange: metrics + metric_name_in_body: True + +rabbitmq: + users: + - username: admin + password: admin + vhost: / + - username: sensu + password: sensu + vhost: /sensu + - username: graphite + password: graphite + vhost: /graphite + +sensu: + client: + rabbitmq: + host: 172.16.0.15 + username: sensu + password: sensu + vhost: /sensu + hostgroup: sensu + server: + rabbitmq: + host: 172.16.0.15 + username: sensu + password: sensu + vhost: /sensu + hostgroup: sensu + handlers: + default: + - flapjack_http + flapjack: + enabled: False + host: 127.0.0.1 + port: 6380 + flapjack_http: + enabled: True + uri: http://127.0.0.1:3090 + ttl: -1 # -1 disables, otherwise httpbroker re-sends last received state until ttl expires + service_owner: development + api: + bind_ip: 0.0.0.0 + firewall: + - port: 4567 + src: "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + dashboard: + host: 0.0.0.0 + datacenters: + - hostname: localhost + password: admin + - hostname: monitor.local + password: admin + firewall: + - port: 3000 + src: "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + +flapjack: + dashboard: + base_url: "http://monitor.local/flapjack/" + api: + base_url: "http://monitor.local/flapjack_api/" + receivers: + httpbroker: + enabled: True + debug: True + firewall: + - port: 3080 + src: "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 3081 + src: "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + - port: 3090 + src: "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + +openid_proxy: + http_redirect: False + apache: + servername: monitor.local + listen: + ip: "0.0.0.0" + port: 80 + firewall: + - port: 80 + protocol: tcp + src: + - any + locations: + root: + path: "/" + sensu: + proxy: "http://127.0.0.1:3000/" + url: "/sensu/" + flapjack: + proxy: "http://127.0.0.1:3080/" + url: "/flapjack/" + flapjack_api: + proxy: "http://127.0.0.1:3081/" + grafana: + proxy: "http://127.0.0.1:3001/" + url: "/grafana/" + ssl: + enabled: False + +grafana: + host: "{{ hostvars[groups['grafana'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}" + server: + http_port: 3001 + http_addr: 0.0.0.0 + root_url: "/grafana/" + security: + enabled: True + admin_user: admin + admin_password: admin + anonymous: true + secret_key: nopenopenopenope + + database: + type: mysql + host: "{{ database.host }}:3306" + name: "{{ database.users.grafana.database }}" + user: "{{ database.users.grafana.username }}" + password: "{{ database.users.grafana.password }}" + firewall: + - port: 5671 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" diff --git a/envs/example/monitor/hosts b/envs/example/monitor/hosts new file mode 100644 index 0000000..b68bebd --- /dev/null +++ b/envs/example/monitor/hosts @@ -0,0 +1,23 @@ +[sensu] +monitor + +[graphite] +monitor + +[percona] +monitor + +[percona_primary] +monitor + +[rabbitmq] +monitor + +[grafana] +monitor + +[flapjack] +monitor + +[openid_proxy] +monitor diff --git a/envs/example/monitor/vagrant.yml b/envs/example/monitor/vagrant.yml new file mode 100644 index 0000000..af11819 --- /dev/null +++ b/envs/example/monitor/vagrant.yml @@ -0,0 +1,9 @@ +default: + memory: 2048 + cpus: 2 + +vms: + monitor: + ip_address: + - 172.16.0.15 + - 172.16.1.15 diff --git a/envs/example/netdata/group_vars/all.yml b/envs/example/netdata/group_vars/all.yml new file mode 100644 index 0000000..affd966 --- /dev/null +++ b/envs/example/netdata/group_vars/all.yml @@ -0,0 +1,19 @@ +--- +common: + firewall: + friendly_networks: [] + sysdig: + enabled: False + +sensu: + enabled: False + +collectd: + enabled: False + +netdata: + enabled: true + firewall: + allow_from: + - any + package: /vagrant/netdata_1.5.1_amd64.deb diff --git a/envs/example/netdata/heat_stack.yml b/envs/example/netdata/heat_stack.yml new file mode 100644 index 0000000..4615412 --- /dev/null +++ b/envs/example/netdata/heat_stack.yml @@ -0,0 +1,81 @@ +heat_template_version: 2013-05-23 + +description: HOT template for sitecontroller mirror server + +parameters: + image: + type: string + description: Name of image to use for servers + default: ubuntu-14.04 + flavor: + type: string + description: Flavor to use for servers + default: m1.xlarge + net_id: + type: string + description: ID of Neutron network into which servers get deployed + default: ba0fdd03-72b5-41eb-bb67-fef437fd6cb4 + floating_ip_pool: + type: string + description: name of floating ip pool to use + default: external + +resources: + + server_security_group: + type: OS::Neutron::SecurityGroup + properties: + description: Security group for SC Registry + name: sc-mirror + rules: + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 22 + port_range_max: 22 + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 80 + port_range_max: 80 + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 81 + port_range_max: 81 + - remote_ip_prefix: 0.0.0.0/0 + protocol: icmp + + sc_ssh_key: + type: OS::Nova::KeyPair + properties: + save_private_key: true + name: sc-mirror + + mirror01_floating_ip: + type: OS::Nova::FloatingIP + properties: + pool: { get_param: floating_ip_pool } + + mirror01: + type: OS::Nova::Server + properties: + name: mirror01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + mirror01_fip_association: + type: OS::Nova::FloatingIPAssociation + properties: + floating_ip: { get_resource: mirror01_floating_ip } + server_id: { get_resource: mirror01 } + +outputs: + mirror01: + description: IP address of mirror01 in provider network + value: { get_attr: [ mirror01_floating_ip, ip ] } + private_key: + description: Private key + value: { get_attr: [ sc_ssh_key, private_key ] } diff --git a/envs/example/netdata/hosts b/envs/example/netdata/hosts new file mode 100644 index 0000000..43c4731 --- /dev/null +++ b/envs/example/netdata/hosts @@ -0,0 +1,2 @@ +[netdata] +netdata01 diff --git a/envs/example/netdata/vagrant.yml b/envs/example/netdata/vagrant.yml new file mode 100644 index 0000000..6c22551 --- /dev/null +++ b/envs/example/netdata/vagrant.yml @@ -0,0 +1,9 @@ +default: + memory: 512 + cpus: 1 + +vms: + netdata01: + ip_address: + - 172.16.0.115 + - 172.16.1.115 diff --git a/envs/example/netdata/vars_heat.yml b/envs/example/netdata/vars_heat.yml new file mode 100644 index 0000000..c94f623 --- /dev/null +++ b/envs/example/netdata/vars_heat.yml @@ -0,0 +1,127 @@ +--- +datacenter: tardis +public_interface: ansible_eth0 +private_interface: ansible_eth0 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +common: + ssh: + allow_from: + - "0.0.0.0/0" + sysdig: + enabled: False + +pypi_mirror: + web: + servername: mirror.tardis.openstack.blueboxgrid.com + port: 81 + +apt_mirror: + web: + servername: mirror.tardis.openstack.blueboxgrid.com + port: 80 + precise: True + trusty: False + repositories: + docker: + key_url: 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xD8576A8BA88D21E9' + type: deb + url: 'https://get.docker.com/ubuntu' + distributions: + - docker + components: + - main + bbg_ubuntu: + url: 'http://repo.openstack.blueboxgrid.com/ubuntu/' + key_url: 'http://repo.openstack.blueboxgrid.com/blue_box_cloud.gpg.key' + type: deb + distributions: + - precise + - trusty + components: + - main + blueboxcloud_giftwrap: + url: 'https://packagecloud.io/blueboxcloud/giftwrap/ubuntu/' + key_url: 'https://packagecloud.io/gpg.key' + type: deb + distributions: + - precise + - trusty + components: + - main + hwraid: + key_url: http://hwraid.le-vert.net/debian/hwraid.le-vert.net.gpg.key + type: deb + url: http://hwraid.le-vert.net/ubuntu + distributions: + - precise + components: + - main + sensu: + url: 'http://repos.sensuapp.org/apt' + key_url: 'http://repos.sensuapp.org/apt/pubkey.gpg' + type: deb + distributions: + - sensu + components: + - main + haproxy: + url: 'http://ppa.launchpad.net/vbernat/haproxy-1.5/ubuntu' + key_url: 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xCFFB779AADC995E4F350A060505D97A41C61B9CD' + type: deb + distributions: + - precise + - trusty + components: + - main + bbg_openstack_ppa: + url: 'http://ppa.launchpad.net/blueboxgroup/openstack/ubuntu' + key_url: 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xC37BA5F849DE63CB' + type: deb + distributions: + - precise + components: + - main + percona: + url: 'http://repo.percona.com/apt' + key_url: 'https://www.percona.com/redir/downloads/RPM-GPG-KEY-percona' + type: deb + distributions: + - precise + - trusty + components: + - main + rabbitmq: + url: 'http://www.rabbitmq.com/debian/' + key_url: 'https://www.rabbitmq.com/rabbitmq-signing-key-public.asc' + type: deb + distributions: + - testing + components: + - main + cloud_archive: + url: 'http://ubuntu-cloud.archive.canonical.com/ubuntu' + type: deb + distributions: + - precise-updates/icehouse + components: + - main + erlang: + url: 'http://packages.erlang-solutions.com/debian' + key_url: 'https://packages.erlang-solutions.com/debian/erlang_solutions.asc' + type: deb + distributions: + - precise + - trusty + components: + - contrib + ceph: + url: 'http://ceph.com/debian-hammer' + key_url: 'https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/release.asc' + type: deb + distributions: + - trusty + - precise + components: + - main diff --git a/envs/example/netman/group_vars/all b/envs/example/netman/group_vars/all new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/envs/example/netman/group_vars/all @@ -0,0 +1 @@ +--- diff --git a/envs/example/netman/hosts b/envs/example/netman/hosts new file mode 100644 index 0000000..7045e0e --- /dev/null +++ b/envs/example/netman/hosts @@ -0,0 +1,11 @@ +[harden] +netman-01 + +[postfix-simple] +netman-01 + +[packagecloud] +netman-01 + +[netman] +netman-01 diff --git a/envs/example/netman/vagrant.yml b/envs/example/netman/vagrant.yml new file mode 100644 index 0000000..220c3d8 --- /dev/null +++ b/envs/example/netman/vagrant.yml @@ -0,0 +1,9 @@ +default: + memory: 512 + cpus: 1 + +vms: + netman-01: + ip_address: + - 172.16.0.20 + - 172.16.1.20 diff --git a/envs/example/sitecontroller/group_vars/all.yml b/envs/example/sitecontroller/group_vars/all.yml new file mode 100644 index 0000000..7630aad --- /dev/null +++ b/envs/example/sitecontroller/group_vars/all.yml @@ -0,0 +1,218 @@ +--- +public_interface: ansible_eth1 +private_interface: ansible_eth2 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +etc_hosts: + - name: bastion01 + ip: "{{ hostvars['bastion01'][private_interface]['ipv4']['address']|default('172.16.0.101') }}" + - name: bootstrap01 + ip: "{{ hostvars['bootstrap01'][private_interface]['ipv4']['address']|default('172.16.0.102') }}" + - name: monitor01 + ip: "{{ hostvars['monitor01'][private_interface]['ipv4']['address']|default('172.16.0.103') }}" + - name: elk01 + ip: "{{ hostvars['elk01'][private_interface]['ipv4']['address']|default('172.16.0.104') }}" + +sitecontroller: + python: + pypi_mirror: https://pypi-mirror.openstack.blueboxgrid.com/root/pypi + trusted_host: pypi-mirror.openstack.blueboxgrid.com + ruby: + gem_sources: + - https://gem-mirror.openstack.blueboxgrid.com + ubuntu_mirror: https://apt-mirror.openstack.blueboxgrid.com/trusty/ubuntu + +common: + sysdig: + enabled: False + firewall: + friendly_networks: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + ssh: + ghe_authorized_keys: + enabled: False + api_url: ~ # ex: https://github.ghe.com/api/v3 + api_user: ~ # ex: username + api_pass: ~ # ex: password + allow_from: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + ntpd: + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 3.pool.ntp.org + - 4.pool.ntp.org + peers: + - "{{ hostvars['bastion01'][private_interface]['ipv4']['address']|default('172.16.0.101') }}" + - "{{ hostvars['bootstrap01'][private_interface]['ipv4']['address']|default('172.16.0.102') }}" + - "{{ hostvars['monitor01'][private_interface]['ipv4']['address']|default('172.16.0.103') }}" + - "{{ hostvars['elk01'][private_interface]['ipv4']['address']|default('172.16.0.104') }}" + clients: + - ip: 172.16.0.0 + netmask: 255.255.225.0 + - ip: 172.16.1.0 + netmask: 255.255.225.0 + sudoers: + - name: blueboxadmin + args: + - "ALL=NOPASSWD: /usr/sbin/tcpdump" + +users: + blueboxadmin: + primary_group: blueboxadmin + groups: + - admin + home: /home/blueboxadmin + createhome: yes + shell: /bin/bash + public_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA4rAIuN7EoPdU8iDPnp27zd+hXsbTE1NEIAQFblG0IywG2B522pivpxE/v1BmtaIVas1APRFDsn5SMGrDOiVNZGz/MdIdJOPjza29WyXgb5w9I329I/XKF5/NEkXDajqzHQheHZ0NSQFFqrlW+N7t6KdKkFP0heAnOLtXJIXrJso04Ew/o/NX6qJFvDY8pVMUeQVloX5zFuHwq+N2JjJIEDS89mmNfqThoAR0KZ/jKQnjNhCdKVurS20Sxft4HI6Zjm7YZMXJO5a+TL+nYEq+JEzLL+PdKcBf4BVpr6MLO/R3d5nxGAtdhgXUSvEDT2bCFWc66KBzNtJTzDKcVn2KcQ== demokey + +user_groups: + admin: + system: yes + blueboxadmin: + system: yes + ssh_keys: + enable_passphrase: false + fingerprint: "64:f3:60:f0:33:ed:8b:a3:af:33:c3:c1:e6:c8:41:bf" + private: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA34Tz/TDvjmcZkDiWmbm6Zz4jx1bl4rcEiSpbxYRqyXvxbGkG + e+YoDyy5s+M/tKaP+kAYDbXwJM6NnLq8q4YsEeqszjDE3+qgH54TE8EoT1VoPDkl + iUiE3a0d1DLZT8oX7x8y+q+39qQFN2sjTf39VgMpbWy6zOH7Ok9hpJXMItezuX9B + sMOfHE3TgZdYnRlc9tRmI58+igw1E2reNqHBxWu2c4FdydoakTXgUmcLLheKW0xC + m0NG9Cy5oAdq79ZJruuQQxTy9W71xm6W4EreDMCZrR3JWBvH2Ahv2JcDdj8BEB2g + iYzwzSqM4D3l44jza2BULgoXf58JS/8B7LkryQIDAQABAoIBAE6ba1M3yofCKnNV + 82DMuIlmiR6pqN86jhXPF8c28nc5Z4ZAyU75ek0b5ZMl8FmP2kKgF9V6jqHjIlpk + McYAwa7rYSqCbDpzQSzdYsgnvg1oc+f6EQFex5tOLpdZ6qLs583oov0WnxPWSx9a + Rmg/UsDVC9S30FoNf1TaZfSD2e7GVOCohXSXheQnRs5AkovlieoBy8NbaRj9+ey3 + hTsbtAsMk7WiazHpP/Fl7LwAXSHRgtFl+s1dbSYbpBLWdabB54j7m75x/hJRWzlW + zgmTdGSpLOnPeKe9TJtIkWzkqx0+XMbk1D4FUZVpo2D8whpHJxZNUdzY1UpqTu6M + frGsQIECgYEA8vqxh7Pw3S32TyMmkpL6oNziccyKe7U+xurulmccrPD6dhpMjOXR + 0ErPBIklxiUsAEZNIdBP4G2Of1xU0OWnWH9Xpk69FI7K7/XPmOoNeC3lYp08IUtb + jvs2C7F6ir+cSpEjU1PrT9hIKTk6XO13Nx291pd6xxUz1UUfdyxv220CgYEA639M + jEH9BsFSghYHM41GCMp2/+xfLLN9sTdPB7b8ElsOMD2Xne1QJ9uHfXyDi5Ba07aI + GbxgIoKcVlxIf/JJFdomeiXzf3PjDOTE8Pv/wMVhjQsfkCfgEunqVKF9amLTeTbl + Ype2modJXe+yuo4AqN8inz3CvTsP15rfZRh9XE0CgYA6xnOdPOS9y/lx6THScOVZ + djT8jBrPk+csnPW7whOIrf4YBYKQ7qLTPNVY5ogRpzo+ksLjtA0uX7IBkucdZQAX + Ay7DlvZb+7KRWyeteKhrcsazFQ/PifgK3S+Uooje+TyoOPWPmZQpS0shVauNgQ++ + cF5Ug236rGFObJsQ69ne/QKBgAaxxLRL/+xcPIM0Kxo9DtubHczipEX6CD2sa9O1 + UO8YpYubhJ7Zqyim5fAcRQUHon1YOAA7SaRRgC44S1tPwOIdJHDeeVCLM84fBrYv + A7MwKTjAMzJ+37DJ835aZN1MV+SfOeAWnftAk0ZXVQZWmRAz36EVOV71udqQLX+L + Na0VAoGBAOyAT/XPSDjMaWMxkf0/cuGrPDotG/2qlPkFzykTurYRXfB+gs/0wM9q + V64jR8VQpxfyljtdEEIbNnYiiHcu3WGDK6zu0w3LzYFdKawKxJezL7vbC3X4qgil + 3KsipSty93s+kZ265SMqby2itnryoMSURt5PniUFlBq5BBrzeZde + -----END RSA PRIVATE KEY----- + public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDfhPP9MO+OZxmQOJaZubpnPiPHVuXitwSJKlvFhGrJe/FsaQZ75igPLLmz4z+0po/6QBgNtfAkzo2curyrhiwR6qzOMMTf6qAfnhMTwShPVWg8OSWJSITdrR3UMtlPyhfvHzL6r7f2pAU3ayNN/f1WAyltbLrM4fs6T2Gklcwi17O5f0Gww58cTdOBl1idGVz21GYjnz6KDDUTat42ocHFa7ZzgV3J2hqRNeBSZwsuF4pbTEKbQ0b0LLmgB2rv1kmu65BDFPL1bvXGbpbgSt4MwJmtHclYG8fYCG/YlwN2PwEQHaCJjPDNKozgPeXjiPNrYFQuChd/nwlL/wHsuSvJ dummmy-key-2 + +monitoring_common: + service_owner: development + +dnsmasq: + enabled: true + interface: lo + server: 127.0.0.1 + dns: + hosts: + - name: bastion01.example.bbg + ip: "{{ hostvars['bastion01'][public_interface]['ipv4']['address']|default('172.16.0.101') }}" + - name: bootstrap01.example.bbg + ip: "{{ hostvars['bootstrap01'][public_interface]['ipv4']['address']|default('172.16.0.102') }}" + - name: monitor01.example.bbg + ip: "{{ hostvars['monitor01'][public_interface]['ipv4']['address']|default('172.16.0.103') }}" + firewall: [] + +sensu: + enabled: true + host: monitor01 + client: + rabbitmq: + host: monitor01 + username: sensu + password: m2KNhjrmxgjXB2ue + vhost: /sensu + hostgroup: sensu + server: + instances: 0 + rabbitmq: + host: monitor01 + username: sensu + password: m2KNhjrmxgjXB2ue + vhost: /sensu + hostgroup: sensu + handlers: + default: + - flapjack_http + flapjack: + enabled: False + host: 127.0.0.1 + port: 6380 + flapjack_http: + enabled: True + uri: http://127.0.0.1:3090 + ttl: -1 # -1 disables, otherwise httpbroker re-sends last received state until ttl expires + service_owner: development + dashboard: + host: "{{ private_ipv4.address }}" + firewall: + - port: 80 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 443 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +rabbitmq: + host: monitor01 + cluster: False + erlang_cookie: E9HGSG7Fs8UmSSQ6 + users: + - username: admin + password: "{{ secrets.sensu.server.rabbitmq.admin }}" + vhost: / + - username: sensu + password: m2KNhjrmxgjXB2ue + vhost: /sensu + - username: graphite + password: 6L2wyT9NXC6qZhQH + vhost: /graphite + firewall: + - port: 5671 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 5672 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +consul: + bind_interface: eth0 + +logging: + enabled: true + forward: + host: "{{ hostvars['elk01'][private_interface]['ipv4']['address']|default('172.16.0.104') }}" + logstash_forwarder: + enabled: true + filebeat: + enabled: true + tls: + ca_cert: | + -----BEGIN CERTIFICATE----- + MIIDbjCCAlagAwIBAgIJAM0BGcYd3vMHMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV + BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX + aWRnaXRzIFB0eSBMdGQwHhcNMTUwMTE0MTk0NzMxWhcNMTYwMTE0MTk0NzMxWjBF + MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 + ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEAxRMHhTXHqEnH9mPUtreyJARa7820HOBf9Fe5zZNnIYXIhx7GpaxZp1pV + vcg2fB9iSUw81Oz+/Y91cyUJID1TF0RNHap/n7bVEu0kMHBbozScQcsE4/zljgX5 + gqSECXpCZ9KQAX3WNBbFGHV/QrJTZA1Teb/Ne77vwTcOVLa++1BRyKc88kLeWS11 + n5IMqWAytLS+1TtvcI+9iMKTb4udKo8UE0ojOMz/xpHbeVBumWBB0Uh8tlv2Bv2r + Qeflib9Djdbj5mj5BPyO2cSUy9blg51vK3wg7XImVpqqTaa97w938I9XICNhvxxm + iS5u4juu5Xuy0Fy0WBWgnaKC7zAwQQIDAQABo2EwXzAPBgNVHREECDAGhwSsEAAN + MB0GA1UdDgQWBBRo6C9RWMTxDhYgBFR701yvGlnw7TAfBgNVHSMEGDAWgBRo6C9R + WMTxDhYgBFR701yvGlnw7TAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IB + AQAYiC7dnmG/HloCPvmnqJ8nuGvHjExF1yaSQbbeCdpS3RcfRi4FvhvQsiQoO4aL + EYVzTEfamy9xD6GDREcQ0ex6b91b2aS71b46S/snzoEwqyc51Fikf93qUGW8Y5hT + /TTsu35mRUkCsOW9JBTE8hHuxaqC4sXvEgVTSQxrfLu9vqQ34c+PWh6sS0nA7aYA + 8GS4HLwpwQNfhO8feQgyLljFrYMSe0iHiLWg2y8FB2ZjWgC/ZX9lY5YiBIe5K9A2 + ToXF6Rn3zaU1rZEQ0vZOK+MdAXNoG1L9jqSRgrSoD92VXKsU0WHorFCN5NLknRlh + S+3FRsZsB9xRpd7jnrShLnVh + -----END CERTIFICATE----- diff --git a/envs/example/sitecontroller/group_vars/bastion.yml b/envs/example/sitecontroller/group_vars/bastion.yml new file mode 100644 index 0000000..3859182 --- /dev/null +++ b/envs/example/sitecontroller/group_vars/bastion.yml @@ -0,0 +1,326 @@ +--- +bastion: + ssh_port: 22 + force_commands: [] + #- /usr/bin/ttyspy + #- /usr/bin/ssh-ip-check + #- /usr/bin/ssh-mosh-filter + +yama_utils: + enabled: true + +yubiauthd: + enabled: true + skipped_users: + - root + - "{{ admin_user }}" + +users: + exampleuser: + primary_group: sitecontroller + groups: + - exampleuser + home: /home/bastionuser + createhome: yes + shell: /bin/bash + uid: 1999 + yubikey: + public_id: 111 + serial_number: 111 + aes_key: 111 + private_id: 111 + public_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA4rAIuN7EoPdU8iDPnp27zd+hXsbTE1NEIAQFblG0IywG2B522pivpxE/v1BmtaIVas1APRFDsn5SMGrDOiVNZGz/MdIdJOPjza29WyXgb5w9I329I/XKF5/NEkXDajqzHQheHZ0NSQFFqrlW+N7t6KdKkFP0heAnOLtXJIXrJso04Ew/o/NX6qJFvDY8pVMUeQVloX5zFuHwq+N2JjJIEDS89mmNfqThoAR0KZ/jKQnjNhCdKVurS20Sxft4HI6Zjm7YZMXJO5a+TL+nYEq+JEzLL+PdKcBf4BVpr6MLO/R3d5nxGAtdhgXUSvEDT2bCFWc66KBzNtJTzDKcVn2KcQ== blueboxadmin@yama-1.blueboxgrid.com + +user_groups: + exampleuser: + system: yes + sitecontroller: + system: no + ssh_keys: + enable_passphrase: false + fingerprint: "c6:a2:23:58:cf:c7:7e:c7:63:87:0f:4c:ce:be:7a:eb" + private: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEA1x6zzX/OSjNwqPuQTJGyXqIB+OBzNTxCXQZUYrb9c9ZjC7rS + yvqpnpfNq2iSag+TSkyZmz+rSOVey94YBf8KY9tI3PXwMFnyoSIsTChmDpSiNx9b + /IswRWHMjWmGo0oxD0cVsRIDPH4cd9DMPpbYgY9062E7nCBgv4b+xerul3sjlGzN + WN3tWFJ0BgzbvWKM9R6fFMgXOrIz2ASyVepCUizndh4m0DU3Dsj7utEnVawMUWN8 + asWZx6XvxlEghQixEhIp2gFOhe3vm+GGgR0BzCfmQqget+jYs7FlW3Vx2he8izAv + yfBOV9PfpfEv0mgCd266rijAFIZycj9xONuvNwIDAQABAoIBAQCkQkNU9PQV4HVz + 8rLaZJ3oeJg861XNHngmBAFHZybc3qS+fica6o++E3fuHGlAJyh2oUrhKpqljM73 + qFx7p6TNXtGiNwDySpxjwW5FsMtM8t1ybbWVfsqbD/RbPmqaILqZSdQcYv6poDoo + mvx6BkDHnTzPxmz36Bk35eKAScVpUC0SDVX9AiK+/4cIna2UvSB3XpTOtaXNHqIQ + 4gJzO8A7EF6u4CyGD8ycB49Y1w/+0gLVpH7p9aIgZrJTxr4bloJTnTrALzEly4p0 + MzIB9DtSFpTv0yx+f3acix38gRehhjQtxLk1+/goODQU3j/7JK2ZiH5a0uDM8zEY + tELDC4mBAoGBAO0xUrNHfJnY5x9dDRd5RLIEP8Bd0GRnCNxfAtwH5iKHJlXHekj+ + zFHkGZJqFCC9ej6i0Mh7tnfZRQUBvVBjw6kslAKXD+x9vDq1qiqVxxwJeFihX8fj + xk5GWCS3I3ienEWPZMu83sZC4YJN2px5IO9BZGoXj9WN0MLH0ejmYH3XAoGBAOgt + VXbYMjt1VwVtawBCywvazmq+0A85AvZ2Ak0LSS2+mgZ/fUlzgRbKOP8Myh4gmYAh + ME35vOb85IpVn2AF02v4HcBOTNBvHluXNgxieqHICYsfCUVqnmBA1UucpCGNGVt7 + UGof+FdUBYGMLLEIfLGsjNj/MmjasGqyRLZBKG2hAoGAZfQPgjQ2IMMVBWwv1mkv + 1/zvkjZA/wcyzdahGgbjKvBA0BpAO+QZ2xFa1I54PGJ1izrc13AlzHo9qptGzqkz + TyJ0NHDOTW72W53+mPNsdGa1rhMfYoJLmRWviYiW3KAAt/2c694xO7M/z4y7bQq7 + 11uaV+fs0XR1yWOunJd53l0CgYAl9NNV/H4pzkMNtheaEVFUfM7mEI+/DVj4pc42 + fjPWcKSJj2oSCfn+mcy7lYGtbzLpCYP2G2/Qa2OJYfoOHqWzrvpeJ+7S3HegDZZe + a/MUY7l7rvU7DfUaUz8Lf24ttf2BQSWiU9urmybTSPE9d9+6xDS6fO3mymmw57fn + +7r6QQKBgQCEhCWm6PdAhixliu4u4Fh+DS8RzxTYYeY5d1f3D8PFdWUW45ILdN9+ + Tvgu9ijpqfu+4faO5UVODPU/GpF8sd8UhTRtrsQof/OzdBP6cbT7OsNYg6fQd4XL + rNd+I4++Fjjcm36/6uS0Uk80ASZTlsJd8IGAJzG2+KlTilZWo4VC/A== + -----END RSA PRIVATE KEY----- + public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXHrPNf85KM3Co+5BMkbJeogH44HM1PEJdBlRitv1z1mMLutLK+qmel82raJJqD5NKTJmbP6tI5V7L3hgF/wpj20jc9fAwWfKhIixMKGYOlKI3H1v8izBFYcyNaYajSjEPRxWxEgM8fhx30Mw+ltiBj3TrYTucIGC/hv7F6u6XeyOUbM1Y3e1YUnQGDNu9Yoz1Hp8UyBc6sjPYBLJV6kJSLOd2HibQNTcOyPu60SdVrAxRY3xqxZnHpe/GUSCFCLESEinaAU6F7e+b4YaBHQHMJ+ZCqB636NizsWVbdXHaF7yLMC/J8E5X09+l8S/SaAJ3brquKMAUhnJyP3E42683 dummy-key + +sshagentmux: + enabled: true + auth_socket: /var/run/authorization_proxy.sock + +ttyspy: + common: + ssl: + ca_cert: | + -----BEGIN CERTIFICATE----- + MIIFETCCAvmgAwIBAgIBATANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdUZXN0 + IENBMB4XDTE1MTAwNTIyNDMwMFoXDTI1MTAwNTIyNDMwMFowEjEQMA4GA1UEAxMH + VGVzdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALaSaxQLH42A + UAoXi/kpW1p4I4Do2A6oyyeaaP3SejcnNb5bW3VFu1RK2aAfeSLgB3URtBN57W4x + D7f41fkqGYYIwS9D2iDKRGRBuofTjNeOYs6m95eLs3Erbz96oCPm5T1IyP6G36Ye + Gt9ut+IaGMiUP4ocyxJnf78YeIfDdCQEV19k9C4GEEFwfeyEIscId0VrQy1cSRjN + 7Tyht1WYCXUJu8ye7D5NUtMLACV7ZB5OxgZc8vaoFoFErUD76WmZa2sHe8dopa1r + Waadixjx+2t6w4zYQj70g0X79m9uB8TDa2a+nIspEYJOO9cMQEOHRNVrofSEGTTv + 5c/eh+s5i4WkN8NBqgCEkqczV3lJMThi/mmkpVmtJ1X3Vh2SB6SihBg09ESfngsS + odX504FCFQLjR2hI1B1Ofd6DszVkCyp7G063Dpa7QvA90P79BbjhAKOkBAykRH68 + HiPCDD61DarrpjVJ7Nlr0A4R0WjNgiF945EZv0ZvzOSW5qqJhOR8WbBG2NmK5UcU + iX1FQUt7Wq1MXb46nAc/N5JLkM5vMYuw2dZ4Ny5Nbxx7hMDHPCGQ/ltIRd1w+kdG + 40a/ln29/LakjHQ1FSOpCqhD11lLfAxvn0SbIHuSfOxMX4rEGYBDsU05CXAvnTlC + AJdrNxqr7M0ll+BeBVIIPn8Vtl0uDjt3AgMBAAGjcjBwMA8GA1UdEwEB/wQFMAMB + Af8wHQYDVR0OBBYEFGsa+1qlRsk+vMyh2RQsenxX2IArMAsGA1UdDwQEAwIBBjAR + BglghkgBhvhCAQEEBAMCAAcwHgYJYIZIAYb4QgENBBEWD3hjYSBjZXJ0aWZpY2F0 + ZTANBgkqhkiG9w0BAQsFAAOCAgEAppnxty/kqH6mDTEB1/1m3IEju9hMDf6sD37k + ogN2D3NBJC/CQDNfqz6/0wtAF5CSUSSZ919MtZG/wEKcVbNzuoYNERrpS51OjiCS + pgV4U/mPFvZlNFD1iMtaYGnOjLJagK1W2NfW2fGuV0mhmvIckqjjQPEjWcYRL8i/ + 4E6jbnQzuSUrpLumZBFQSjZkfPyeo7jYll1b4LM+/K8omzGA6xfbLbIWUvLSzl2R + 4YUW/ezJWTONEh3jrFWUXxrNwDaIVPEMBm5+V7/L/cvQKdt2SH67lbSFHSWvfND8 + 3wNHRvRyTCtfVDoPGa2otuY29SoSOTWi63QSZQVu9th4wcPtXwQTeQx34bUfdwei + Xjxd1F7Ux0IkPv90GUixxYOXv2O7Sjhjtw+68DFYd5Yiec30692aEghzMThe1yLe + rImozA7iV4jih7OTaiIDKdCfvs2GcKHfI+nx9cCaCRYPe+9dB79yEXpDRxGii68X + 904ga0g0p1035FTicZ2btECFIN7H3zceylDe9D95WETP0ENB0q8qHs+LjiCNuqiv + 2KzUW4HW7eA8HBoNzxsRZCj3eu+SeY+5LzSu4j/4nhp2Q6pOlbOKVHDjVmnZIZt6 + 8cLTIKuMbD0pxJzSrrHeYh5fdV4j0xJub6MxROEQPbPn0oH1RjfYwRUohg2ibq64 + NojcNto= + -----END CERTIFICATE----- + client: + enabled: true + ssl: + cert: | + -----BEGIN CERTIFICATE----- + MIIFEjCCAvqgAwIBAgIBAjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdUZXN0 + IENBMB4XDTE1MTAwNTIyNDQwMFoXDTE2MTAwNDIyNDQwMFowFjEUMBIGA1UEAxML + Y2xpZW50LnRlc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDkiiXQ + E3v0thvOjzXLlNA6+uy3+2cKYRJ/AGJZAwpbjmMg+M1Ql2ZN7RbMBSdAXDH1RXxh + xTjWJMboj1XFsX/Mf8taiOTgSUN/sEm3HLFVnXC2fbZOjZV9DfuEjKSzWfm8TLiU + pmbQ9R1ZxVO9Y50hWC5hmA2lDth1QU9L2IoeYBR8qqM6heorxem2a5NyI6Cc8bPQ + ydBo3zhVUAP4Gnhq0CEYFY8sCrJKemVlfJfhQ/sbDRwQ/PsL0inl1qwHrcKWTzVe + t9vOFRdeOfdN6qa2iIHeLjjJ/NV7V0jBuALJUGIWz9NBYhZL5AArhDwVS9lSL9p7 + sLwYa4rc2Zo079sv7UVBj+7UJO1upgnHziKdhZX/Be1gOdKHzlrR3/Rr910tWXuE + /6S/TUffekmKgPhr8+AMT5PLKMVaB16urHB0jf+hLVAJS6xaenJvt6f7eoYUGMRN + OJQMlFW3WMKjMveUDurxi9v9XKrRwx5f8geiI6ANNZfV/WEhJeME+C+Jne3k37sj + vKjb2W1eap6ftP9yPS4d8m1M8ChJUnawBR/xAAbuivsBlz6eY4YZ5fsGL5mIX7Gs + LeyoGYCb8fc2XJ2MR4sqoQqwD5V5BOfJR8hpMA7sMXj5+ICqtZJq+tesgd/daXxZ + OSIKcGbv8yoI+GYK8mgjfQhXRe9gApSHW/Q4GwIDAQABo28wbTAMBgNVHRMBAf8E + AjAAMB0GA1UdDgQWBBQaDK+1R3zzYUV7es6VWg+HsvH+tzALBgNVHQ8EBAMCBLAw + EQYJYIZIAYb4QgEBBAQDAgWgMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNh + dGUwDQYJKoZIhvcNAQELBQADggIBAIdLFwQx0ZCX1PJhN22mGEYGpV1rq7Osixbz + vX5K48wxX/gJG76vYUnkhAt/cm9H9yOtUglHdYXlLvt5grR/rPPKZ60iMBud0n47 + SGmojQVCaWdwbKMjehSz6N8NWT/LG4lmahIeiv8IPpmV6cfsQLdWS+AeqtqmFSNA + 1lVLBdoLYvN0i/CGQBtRnTprmp1CbV9MFUDb5JSR4XhxtqfCxZQFcAovk4TlgmFS + 9WHfkXcFU56LypQHvzIqa7QEm2z3QuqHpA1S56L+1+MLxdNzNthS6fnKY6EbEGQN + 0rQyEgebk858DpeMdJA708H4vgr5TR7eIPIlAJUvF0QMJKqgQ/yMdH5+XmFYqbDZ + nHvQNvXMY4l2DiaxgqVBlpUeKbjDqURTLNXXZJYEWhpz1iVXx66C2q+Uh0+JA3g9 + 9VJsIq0LDrBDoNw3GvDX/WKUItRKs5BD24T/7xHs8z8AfSPlEX3ChtMhZYqJK/C3 + 8UbQV6MASZTLoO4S+66Gq3Fra3VZXvFGjMLlXGzRCMZpw/1TeolGc84XjsGUpaq3 + 99JK4m+OlIKyDFqC9090S7WyEV/pyy0VEAEupob4voAGGA0EjV6Ytx9XaarJgqBy + XS6tqm19L6OtSmCbapndz0KBUhejFgNSLUqY/YMO2onayoAYXnUiskfb1cKQax8F + rNKZQ9So + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + 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 + -----END RSA PRIVATE KEY----- + server: + enabled: True + host: server.test + ip: 127.0.0.1 + port: 8090 + transcript_path: /tmp/transcripts + firewall: + friendly_networks: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" + ssl: + cert: | + -----BEGIN CERTIFICATE----- + MIIFEjCCAvqgAwIBAgIBAzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDEwdUZXN0 + IENBMB4XDTE1MTAwNTIyNDQwMFoXDTE2MTAwNDIyNDQwMFowFjEUMBIGA1UEAxML + c2VydmVyLnRlc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCxHM2K + 0b3A1yk1IY4Bwvq/g+8HDsI1kRVdpkZOljE8JWEXBcotV9NoRnpKWImLRthPfBcm + TmOXaBDmS2rx4hW/jwca3V5dSSSXkfx8DEWMmUhX3dxBMerbjtMyQLy+8HKAZPGQ + xRNmEhV/qwS5ZxEKI+dsB2zlyepnF8S3IE5kKJQFOZVZv59WZFgaxv7XbFyTeJJf + qrcpULnzzI7AwBXt3JxCxM5U/ABzAlzVOvH/tOweHYIjtm6yQCs1KQStumjh7L4X + BVjU/YPfGy+Fqky60mruZEU1JvpISsjO67QsmcnYWR3vQI4K2L6fo6OCGhOkdNsb + 3z6LWlk+pzbB5rRKsMv7FJkqdFDfOb2mF+SPUVU0hAx5e0XiNfJsPUpqlzF8dALW + M26vEpUDemEEYwW4MQEEBnvL4E8oXXwTExFvHAvCuqu3wdCBufn5qCVTir/wT+v0 + aYqsR6QhgscIMCzZipyjeLs7yfW4rAMgLUE4xz8ZIJfSKr5+zeDwoYBs7d9y/8v6 + g4j3CTNMfraSbdKbLBRvvVk/+ruBVnGl/NPd5z7XtlBtfDV/aHqzj4n6frR7UmA4 + +VewY5idzdxgu0T+6mlqhY1s83EZsu+gNl0hAmAvMdp7UptfwWnDr/Waqj0CJHfq + alp1cdRZh6ceRbTIkpBemIxXAKW27i+/ScAh9wIDAQABo28wbTAMBgNVHRMBAf8E + AjAAMB0GA1UdDgQWBBRkwxTJ3QzxIU2qleVkIIaa6Y6otTALBgNVHQ8EBAMCBeAw + EQYJYIZIAYb4QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNh + dGUwDQYJKoZIhvcNAQELBQADggIBAICHzCY8omXOgmGFRQxSQMZ9KCbaLDVQ6duN + ysniO0imXdy0qGb+b0HVP1gnTv7moq8YrIA50TeyfdGZkBXpT6pA/hc6Q11aw2Bq + FaDSSj0otCpxSuur0b52G1p2IhFjymFeuCQwkkPZ+1YXtAY9evE7iq6ItFkGEs/K + FgSIffFggOHFfoHWz9QhAMtE28VZOieYbpsyR7YnZHANkkJfjK6FNJGE4LrwuvlG + SR/wvFeecmWMsRVsWdi0Jb9PHF9JW2gJVFl6fvHCZleg9LtEoVwSseK7mIy0VU1A + JlYVx7Z2lcxYVpkDk0JQlUsOSD0C+lyTwjwfCN/UX6PjaSMMiAP4h+dOBVFLsp6S + c1qeZrA5Dgsv/9efTPXxb2uEaSg2bNqvn0ivmbcu0TfDALCAKUtzTvM7lO6IVijp + HKGm46ZuvOVLIv4oZmaAg/1lC4J9HZPzGMABR+63lTEhcR8DqtZ5MtJ9clD618Z8 + dg9jEgpapUdao+nat3jbVr+mh7m9WFfd4H/8d++xxfjcEEUSPhH8JT1voSzZr4cn + gBjSiohrW/FpcDNeCTsoE847wldkpHblD5khwtTmDDwrIRAD3Q0i+EBAkhsk/dY3 + +LEbD1Ab8lmSMiLGDONctQxFdWEWSyR/btki8Q0NG8fAFy0qzPIymCF2aVMtxX24 + /xdoyOms + -----END CERTIFICATE----- + key: | + -----BEGIN RSA PRIVATE KEY----- + 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 + -----END RSA PRIVATE KEY----- + +support_tools: + enabled: true + system_deps: + - libxml2-dev + - libxslt-dev + - libssl-dev + git: + - name: ursula-master + path: /opt/git/ursula/master + owner: "{{ admin_user }}" + repo: http://github.com/blueboxgroup/ursula.git + rev: master + virtualenvs: + - name: ursula-master + path: /opt/venv/ursula/master + owner: "{{ admin_user }}" + requirements: /opt/git/ursula/master/requirements.txt + alternatives: + - name: ursula + path: /opt/venv/ursula/master/bin/ursula + link: /usr/local/bin/ursula + cleanup: [] + +ipmi_proxy: + datacenters: + - name: dc01 + data_center_uuid: 7cd69350-35e8-4677-98fe-aad60fc9e191 + backend_source_ip: "{{ hostvars[groups['bastion'][0]][private_interface]['ipv4']['address']|default('172.16.1.101') }}" + ip_pool: + - "{{ hostvars[groups['bastion'][0]][private_interface]['ipv4']['address']|default('172.16.1.101') }}" + apache: + allow_from: + - 127.0.0.1 + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - "{{ public_ipv4.network }}/{{ public_ipv4.netmask }}" diff --git a/envs/example/sitecontroller/group_vars/bootstrap.yml b/envs/example/sitecontroller/group_vars/bootstrap.yml new file mode 100644 index 0000000..9ad9c0e --- /dev/null +++ b/envs/example/sitecontroller/group_vars/bootstrap.yml @@ -0,0 +1,211 @@ +--- +apache: + listen: + - 80 + - 443 + - 81 + - 444 + - 82 + - 445 + - 83 + - 446 + +apt_mirror: + debmirror: + + repositories: + ddebs: + host: ddebs.ubuntu.com + path: / + method: http + exclude_regex: '.' + include_regex: 'linux/linux-image-.*generic-dbgsym' + arch: amd64,i386 + distributions: 'trusty,trusty-security,trusty-updates' + sections: 'main,restricted,universe,multiverse' + key_url: http://ddebs.ubuntu.com/dbgsym-release-key.asc + ceph: + host: download.ceph.com + path: ceph/debian-hammer + method: rsync + arch: amd64,i386 + distributions: trusty,precise + sections: main + key_url: http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xE84AC2C0460F3994 + mongodb: + host: repo.mongodb.org + path: apt/ubuntu + method: http + arch: amd64,i386 + distributions: trusty/mongodb-org + sections: 3.0/multiverse + ignore_missing_release: true + download_files: + - dists/trusty/mongodb-org/3.0/Release + - dists/trusty/mongodb-org/3.0/Release.gpg + + distros: + trusty: + enabled: True + host: us.archive.ubuntu.com + path: ubuntu + method: rsync + arch: amd64,i386 + distributions: trusty,trusty-security,trusty-updates,trusty-backports + sections: main,main/debian-installer,restricted,restricted/debian-installer,universe,universe/debian-installer,multiverse,multiverse/debian-installer + precise: + enabled: True + host: us.archive.ubuntu.com + path: ubuntu + method: rsync + arch: amd64,i386 + distributions: precise,precise-security,precise-updates,precise-backports + sections: main,main/debian-installer,restricted,restricted/debian-installer,universe,universe/debian-installer,multiverse,multiverse/debian-installer + power8_trusty: + enabled: False + host: ports.ubuntu.com + path: ubuntu-ports + method: rsync + arch: ppc64el + distributions: trusty,trusty-security,trusty-updates,trusty-backports + sections: main,main/debian-installer,restricted,restricted/debian-installer,universe,universe/debian-installer,multiverse,multiverse/debian-installer + aliases: + - /power8_trusty/ubuntu + armhf_xenial: + enabled: False + host: ports.ubuntu.com + path: ubuntu-ports + method: rsync + arch: armhf + distributions: xenial,xenial-security,xenial-updates,xenial-backports + sections: main,main/debian-installer,restricted,restricted/debian-installer,universe,universe/debian-installer,multiverse,multiverse/debian-installer + + apache: + port: 80 + ssl: + enabled: True + port: 443 + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + - port: 443 + protocol: tcp + src: 0.0.0.0/0 + +yum_mirror: + repositories: + elastic: + description: elastic + enabled: True + url: https://artifacts.elastic.co/packages/5.x/yum + key_url: https://artifacts.elastic.co/GPG-KEY-elasticsearch + archs: + - arch: x86_64 +# sensu: +# description: sensu +# enabled: True +# gpgcheck: False +# archs: +# - arch: x86_64 +# url: http://sensu.global.ssl.fastly.net/yum/el/7/x86_64/ +# chef: +# description: chef +# enabled: True +# key_url: https://downloads.chef.io/packages-chef-io-public.key +# archs: +# - arch: x86_64 +# url: https://packages.chef.io/repos/yum/stable/el/7/x86_64/ + +pypi_mirror: + repos: + bluebox_private: + username: bluebox + password: nopenopenope + index: private + mirror: + url: https://fdec6132ec19dc59011548774d514af7005c494495107ff9:@packagecloud.io/blueboxcloud + cache_expiry: 300 + bluebox_openstack: + username: bluebox + index: openstack + mirror: + url: https://packagecloud.io/blueboxcloud + cache_expiry: 300 + apache: + port: 81 + ssl: + enabled: False + port: 444 + firewall: + - port: 81 + protocol: tcp + src: 0.0.0.0/0 + - port: 444 + protocol: tcp + src: 0.0.0.0/0 + +gem_mirror: + ip: 127.0.0.1 + port: 9292 + apache: + enabled: true + servername: mirror01.local + port: 82 + ssl: + enabled: True + port: 445 + firewall: + - port: 82 + protocol: tcp + src: 0.0.0.0/0 + - port: 445 + protocol: tcp + src: 0.0.0.0/0 + +squid: + port: 3128 + allowed_networks: + - 127.0.0.1/32 + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + proxy_domains: + - github.com + - codeload.github.com + - .openstack.blueboxgrid.com + blacklist_packages: [] + cache_dir: + size: 40000 + +file_mirror: + files: + - name: bluebox.ico + path: horizon-branding + url: http://file-mirror.openstack.blueboxgrid.com/horizon-branding/bluebox.ico + sha256sum: 72e0318a9806300478c6dddc2591f7cab04c54957b9424d2a11eb08156820c38 + auth: + - path: ubuntu + username: ubuntu + password: ubuntu + apache: + enabled: true + servername: mirror01.local + port: 83 + ssl: + enabled: True + port: 446 + firewall: + - port: 83 + protocol: tcp + src: 0.0.0.0/0 + - port: 446 + protocol: tcp + src: 0.0.0.0/0 + +pxe: + os: + - name: trusty + kernel: http://archive.ubuntu.com/ubuntu/dists/trusty-updates/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux + bootloader: http://archive.ubuntu.com/ubuntu/dists/trusty-updates/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz + root_password: password + kernel_image: + ntp_server: diff --git a/envs/example/sitecontroller/group_vars/elk.yml b/envs/example/sitecontroller/group_vars/elk.yml new file mode 100644 index 0000000..aec9d33 --- /dev/null +++ b/envs/example/sitecontroller/group_vars/elk.yml @@ -0,0 +1,120 @@ +--- +apache: + listen: + - 80 + - 443 + - 1080 + - 10443 + +elasticsearch: + config: + action.disable_delete_all_indices: true + bootstrap.mlockall: true + cluster.name: elk_example + cluster.routing.allocation.node_concurrent_recoveries: 4 + cluster.routing.allocation.node_initial_primaries_recoveries: 4 + discovery.zen.minimum_master_nodes: "{{ (groups['elasticsearch']|length / 2)|int + (groups['elasticsearch']|length % 2 > 0)|int }}" + discovery.zen.ping.multicast.enabled: false + discovery.zen.ping.unicast.hosts: ["{{ hostvars[groups['elasticsearch'][0]][private_interface]['ipv4']['address']|default('172.16.1.104') }}"] + gateway.expected_nodes: "{{ groups['elasticsearch']|length }}" + gateway.recover_after_nodes: "{{ (groups['elasticsearch']|length / 2)|int + (groups['elasticsearch']|length % 2 > 0)|int }}" + index.number_of_replicas: 0 + indices.breaker.fielddata.limit: 75% + indices.fielddata.cache.expire: 1w + indices.fielddata.cache.size: 60% + indices.recovery.concurrent_streams: 4 + indices.recovery.max_bytes_per_sec: 75mb + indices.store.throttle.max_bytes_per_sec: 150mb + node.name: "{{ ansible_hostname }}" + network.publish_host: "{{ private_ipv4.address }}" + path.conf: /etc/elasticsearch + path.data: /var/lib/elasticsearch + path.logs: /var/log/elasticsearch + path.work: /tmp/elasticsearch + firewall: + - port: 9200 + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 9300 + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + heap_size: 512m + restart_on_config: true + curator: + tasks: + - action: delete + older: 90 + hour: 2 + - action: close + older: 90 + hour: 2 + minute: 15 + - action: optimize + params: "--max_num_segments 1" + older: 2 + hour: 3 + minute: 0 + +openid_proxy: + http_redirect: true + apache: + ssl: + enabled: true + name: openid_proxy + sslproxy: + enabled: true + location_prefixes: [] + locations: + cleversafe: + proxy: https://10.143.14.201/ + url: "/cleversafe/" + config: + - ProxyPassReverseCookiePath: "/ /cleversafe/qa/" + - RequestHeader: "set X-Forwarded-Path /cleversafe/qa/" + - RequestHeader: 'set Authorization "Basic YWRtaW46U200cnRjbDB1ZCE="' + ipmi: + proxy: "http://127.0.0.1:8091/" + url: "/ipmi/" + config: + - RequestHeader: "set X-Proxy-Remote-User %{REMOTE_USER}e env=REMOTE_USER" + kibana: + proxy: "http://127.0.0.1:5601/" + url: "/kibana/" + es: + proxy: "http://127.0.0.1:9200/" + hq: + proxy: "http://127.0.0.1:9200/_plugin/hq/" + url: "/hq/?url=http://elk01.local/es/" + head: + proxy: "http://127.0.0.1:9200/_plugin/head/" + url: "/head/?base_uri=http://elk01.local/es/" + sensu: + proxy: "http://monitor01.local:3000/" + url: "/sensu/" + flapjack: + proxy: "http://monitor01.local:3080/" + url: "/flapjack/" + flapjack_api: + proxy: "http://monitor01.local:3081/" + grafana: + proxy: "http://{{ hostvars[groups['grafana'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}:3001/" + url: "/grafana/" + config: + - RequestHeader: unset Authorization + firewall: + - port: 80 + protocol: tcp + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 443 + protocol: tcp + src: + - "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 1080 + protocol: tcp + src: + - 0.0.0.0/0 + - port: 10443 + protocol: tcp + src: + - 0.0.0.0/0 diff --git a/envs/example/sitecontroller/group_vars/monitor.yml b/envs/example/sitecontroller/group_vars/monitor.yml new file mode 100644 index 0000000..d25322a --- /dev/null +++ b/envs/example/sitecontroller/group_vars/monitor.yml @@ -0,0 +1,70 @@ +--- +apache: + listen: + - 8081 + +database: + host: "{{ hostvars[groups['percona'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}" + port: 3306 + users: + graphite: + database: graphite + username: graphite + password: b82tPZ9W7Kjsba38 + host: '%' + grafana: + database: grafana + username: grafana + password: QKBRt8Zz4j937dQS + host: '%' + +graphite: + host: "{{ hostvars[groups['graphite'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}" + amqp: + enabled: False + verbose: False + host: "{{ rabbitmq.host }}" + port: 5672 + vhost: /graphite + user: graphite + password: 6L2wyT9NXC6qZhQH + exchange: metrics + metric_name_in_body: True + +grafana: + host: "{{ hostvars[groups['grafana'][0]][private_interface]['ipv4']['address']|default('172.16.1.103') }}" + server: + http_port: 3001 + http_addr: 0.0.0.0 + root_url: "/grafana/" + security: + enabled: false + anonymous: true + admin_user: admin + admin_password: admin + secret_key: dsfdgrgrgrfewfdewgfreGregvre + database: + type: mysql + path: "/usr/share/grafana/data/grafana.db" + host: "{{ database.host }}:3306" + name: "{{ database.users.grafana.database }}" + user: "{{ database.users.grafana.username }}" + password: "{{ database.users.grafana.password }}" + firewall: + - port: 5671 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + +flapjack: + dashboard: + base_url: "http://elk01.local/flapjack/" + api: + base_url: "http://elk01.local/flapjack_api/" + receivers: + httpbroker: + enabled: True + debug: True + firewall: + - port: 3080 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + - port: 3081 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" diff --git a/envs/example/sitecontroller/heat_stack.yml b/envs/example/sitecontroller/heat_stack.yml new file mode 100644 index 0000000..0f873be --- /dev/null +++ b/envs/example/sitecontroller/heat_stack.yml @@ -0,0 +1,133 @@ +heat_template_version: 2013-05-23 + +description: HOT template to sitecontroller + +parameters: + image: + type: string + description: Name of image to use for servers + default: ubuntu-14.04 + flavor: + type: string + description: Flavor to use for servers + default: m1.medium + net_id: + type: string + description: ID of Neutron network into which servers get deployed + default: d9d929b6-6797-4247-85f4-b620e392c391 + floating_ip_pool: + type: string + description: name of floating ip pool to use + default: external + ssh_key_name: + type: string + description: name of the ssh keypair + default: sitecontroller + secgroup_name: + type: string + description: name of the security group + default: sitecontroller-test-security-group + +resources: + + server_security_group: + type: OS::Neutron::SecurityGroup + properties: + description: Security group for SC testing. + name: { get_param: secgroup_name } + rules: + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 22 + port_range_max: 22 + - remote_ip_prefix: 0.0.0.0/0 + protocol: icmp + - remote_ip_prefix: 0.0.0.0/0 + protocol: tcp + port_range_min: 9300 + port_range_max: 9300 + + sc_ssh_key: + type: OS::Nova::KeyPair + properties: + save_private_key: true + name: { get_param: ssh_key_name } + + bastion01_floating_ip: + type: OS::Nova::FloatingIP + properties: + pool: { get_param: floating_ip_pool } + + bastion01: + type: OS::Nova::Server + properties: + name: bastion01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + bastion01_fip_association: + type: OS::Nova::FloatingIPAssociation + properties: + floating_ip: { get_resource: bastion01_floating_ip } + server_id: { get_resource: bastion01 } + + bootstrap01: + type: OS::Nova::Server + properties: + name: bootstrap01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + monitor01: + type: OS::Nova::Server + properties: + name: monitor01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + + elk01: + type: OS::Nova::Server + properties: + name: elk01 + image: { get_param: image } + flavor: { get_param: flavor } + key_name: { get_resource: sc_ssh_key } + user_data_format: RAW + security_groups: [{ get_resource: server_security_group }] + networks: + - network: { get_param: net_id } + +outputs: + floating_ip: + description: Floating IP address of bastion01 + value: { get_attr: [ bastion01_floating_ip, ip ] } + bastion01: + description: IP address of bastion01 in provider network + value: { get_attr: [ bastion01, first_address ] } + bootstrap01: + description: IP address of elk01 in provider network + value: { get_attr: [ bootstrap01, first_address ] } + monitor01: + description: IP address of monitor01 in provider network + value: { get_attr: [ monitor01, first_address ] } + elk01: + description: IP address of elk01 in provider network + value: { get_attr: [ elk01, first_address ] } + private_key: + description: Private key + value: { get_attr: [ sc_ssh_key, private_key ] } diff --git a/envs/example/sitecontroller/host_vars/bastion01.yml b/envs/example/sitecontroller/host_vars/bastion01.yml new file mode 100644 index 0000000..b21ad06 --- /dev/null +++ b/envs/example/sitecontroller/host_vars/bastion01.yml @@ -0,0 +1,73 @@ +--- +common: + ssh: + allow_from: + - any + ssh_host_rsa_key: + public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLVohq7MeGCq4kU9NBPNk/XUbK0W3sy6GEbapsGYi1qxvx+7gNpYblfUtwrfMyXp1ICuJei8NoXkpXsgnC4EOmPU1HBnwkyVXJmyaBn4u19LjZQ2KrmrH0LC8d3mf3fSTFrj27QHbJk48pnVo7NkgM3ImKXEtUvtrieBYY88lFyHZIW4WuFkMlnmHaxSpSiVBuDcemxzHD0MieHkB2pbA84f2LFI2Gmd1248vnmAC65Qce35ngfkpzktgnhl0Q7CaOY16tdJog0FBsjXtwfUf0nruTKkAwWtbj+g+Rc4Vh8DQ+LqYL7k+tMD/wc6WZ0wp4py9bouSsL0pbwiMCxsyV + private: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAy1aIauzHhgquJFPTQTzZP11GytFt7MuhhG2qbBmItasb8fu4 + DaWG5X1LcK3zMl6dSAriXovDaF5KV7IJwuBDpj1NRwZ8JMlVyZsmgZ+LtfS42UNi + q5qx9CwvHd5n930kxa49u0B2yZOPKZ1aOzZIDNyJilxLVL7a4ngWGPPJRch2SFuF + rhZDJZ5h2sUqUolQbg3Hpscxw9DInh5AdqWwPOH9ixSNhpndduPL55gAuuUHHt+Z + 4H5Kc5LYJ4ZdEOwmjmNerXSaINBQbI17cH1H9J67kypAMFrW4/oPkXOFYfA0Pi6m + C+5PrTA/8HOlmdMKeKcvW6LkrC9KW8IjAsbMlQIDAQABAoIBAQDAZemAmviTuWlb + EEH1GZlKXn0MjauggnEa+BVoYaS8mOJBTnex2ezRGoQWY1GZWHMj5sCYy1AjYgyf + ++NGlKMOGjUTvmwa9rKIp5iA0DEHAz8gvKURULePHXubnITtZWk07xCULIP5afjt + XGkzW8JDAS3XUEsrLce4v/3QsGPC9SIO5n4Rior3yr6BCAEp7ZEFPd85tyEeYgAA + dkdcegDKg1OdtLll2atTZQywXO9OFNwtuMjonZ5Xi9gKXDh3tNbwby1B8GUNl12u + PhgYJpCkRKJ2OK/SGJO/mv79OlEbR+kRuSqUoYtwK4cqcWdqCt23L5jqeGL3AfDp + j7AW7KKlAoGBAOvFzQb4azzwcDmeo+WIGOiQHrysKgoCFCoiioJSxvLAxf/LZRyE + By8rOQ5qZRafCMrm75rX/l8E5468r/es8KZyxofL3QnAIYxn46Yn97WrgwAMj/zb + QJewHxr38xtcPH1Y/oIZIDaoKT7LgvyxmgVbrEnogNN3cTjCNB65Q05LAoGBANzI + YWKZk5s/CSZdndsaOlRP3Oa1nK1Wk/lwjJ0n8GdwzjnT6Z8DcZqrXAFB7SKc4JKv + /iMd9NdvI5IVM/1MxqbBgPdkHz+z1ljCIwSa397ez81EBEHG5s8U0M9NusMc1kEl + MTZoLA/KOVf0TV6e4u78PLrYbH2LGk21vYS2QASfAoGBALPF7843zvT5VGjq6IeU + 3YE+muE72RYmfZ+fFMPIQEBK8u5W9TLoDN5Pc8LlJ003WCn2Ko6D0UY8ZB93Cims + 6RXRgEV9EX5kzG/Vq0Q/R2Zzb4CzE3s25qqCtUUH+ItNKiZdnDow1Fo4oLJr1OW+ + ufUJ8HWuXcRgV6lykvE2S/a/AoGAPjVvyzp+rsWqLFdAfVX8jXkbQx51ERpOA4DE + /hscz+inEwA02Ys83VnfSLNsv834MRzJvNdZ/8HfAfBbf8m8R4xKbGqXq3lesg+x + kkCZR9D8OFgr4uTKcBrrYx4Bu6xKanyySyQ7Fg/i7Hd3vWuDgdcsvXDx0MX7GV5W + EijQDP0CgYBdz5VmUa8bkKqQSVjfKJVX5oBtw+FasHx3LSogkCUPmJhinznSSIs/ + u5mTA8uK+HG5nMvzhaJdr6nMe2AtQB1rBDWceTmL34rhnDajyqWtmTw9fWWlnsZR + rbgwnNnBoiK6Osp9KK9fSOptvcrjT/Y3eYlAsZE+JFonK5ofXPm8/Q== + -----END RSA PRIVATE KEY----- + +_users: + ### TODO: this kills the CI, specifically due to how keys get handled. + ### manage_authorized_keys: True + manage_authorized_keys: False + +manage_disks: + enabled: True + defaults: + pesize: 4 + loopback: + enabled: True + file: /opt/loopback + device: /dev/loop2 + size: 1G + volume_groups: + - name: test + pvs: + - /dev/loop2 + logical_volumes: + - name: test + volume_group: test + size: 500m + filesystem: ext4 +# filesystem_opts: + mount_point: /mnt/test +# mount_opts: + luks_volumes: + - name: crypttest + volume_group: test + size: 500m + filesystem: ext4 + mount_point: /mnt/crypttest + mount_opts: defaults + password_file: none + crypttab_opts: luks + +luks_passphrase: paulczar diff --git a/envs/example/sitecontroller/hosts b/envs/example/sitecontroller/hosts new file mode 100644 index 0000000..5206b2a --- /dev/null +++ b/envs/example/sitecontroller/hosts @@ -0,0 +1,86 @@ +########### +# control # +########### + +[bastion] +bastion01 + +[ttyspy-server] +bastion01 + +# Disable in dev +# [ipmi-proxy] +# bastion01 + +########################## +# remote site controller # +########################## + +[elk:children] +logstash +kibana +elasticsearch + +[monitor:children] +sensu +graphite +percona +percona_primary +rabbitmq +grafana + +[bootstrap:children] +mirror +pxe + +[pxe] +bootstrap01 + +[squid] +bootstrap01 + +[mirror] +bootstrap01 + +[openid_proxy] +elk01 + +[elasticsearch] +elk01 + +[logstash] +elk01 + +[kibana] +elk01 + +[sensu] +monitor01 + +[graphite] +monitor01 + +[percona] +monitor01 + +[percona_primary] +monitor01 + +[rabbitmq] +monitor01 + +[grafana] +monitor01 + +[flapjack] +monitor01 + +[consul] + +[consul_server] + +[vault] + +[vyatta-sitecontroller] + +[docker] diff --git a/envs/example/sitecontroller/vagrant.yml b/envs/example/sitecontroller/vagrant.yml new file mode 100644 index 0000000..622eace --- /dev/null +++ b/envs/example/sitecontroller/vagrant.yml @@ -0,0 +1,26 @@ +default: + memory: 512 + cpus: 1 + gui: false + +vms: + bastion01: + ip_address: + - 172.16.0.101 + - 172.16.1.101 + memory: 1024 + bootstrap01: + ip_address: + - 172.16.0.102 + - 172.16.1.102 + memory: 1024 + monitor01: + ip_address: + - 172.16.0.103 + - 172.16.1.103 + memory: 1536 + elk01: + ip_address: + - 172.16.0.104 + - 172.16.1.104 + memory: 1536 diff --git a/envs/example/sitecontroller/vars_heat.yml b/envs/example/sitecontroller/vars_heat.yml new file mode 100644 index 0000000..8fe2319 --- /dev/null +++ b/envs/example/sitecontroller/vars_heat.yml @@ -0,0 +1,14 @@ +--- +datacenter: ci +public_interface: ansible_eth0 +private_interface: ansible_eth0 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" +admin_user: ubuntu + +common: + users: + - name: ubuntu + #pass: + public_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsHndVMf3mu6THq7Fde7TC2SjfRlfPpBfQbMwA4HS44NljWOBuFGUyE3roRxSvGxEamPH79TXKURegLZEuh1l92ADrDEU4SpcHgUjIfyQwH5SP0Y2/uKRKpj26MbCx8yCyV9ra7YpLYvIFzxiLtp7xN2zu53mvhxHzj1SK7YkkAvmYa7At2yTBsyBu7+MTGtYCpPC1YsP7IZbc900HwwffBJo011puySHxV4xWi8lxqG43lqx0d1BILITMPXXR6QzOciB5wfsTHMTf6o4/Hzk4URjKLIbEfr1lby8rE+aKWEN2GuSuwrw7XERQuSr1PRi5pJWvLNfbyOT9TzO8DOkf diff --git a/envs/example/swiftbrowser/group_vars/all.yml b/envs/example/swiftbrowser/group_vars/all.yml new file mode 100644 index 0000000..ecaa809 --- /dev/null +++ b/envs/example/swiftbrowser/group_vars/all.yml @@ -0,0 +1,19 @@ +--- +public_interface: ansible_eth1 +private_interface: ansible_eth2 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" + +sensu: + client: + enable_checks: False + + +secrets: + swiftbrowser: + os_password: PASSWORD + + +apache: + listen: + - 80 diff --git a/envs/example/swiftbrowser/hosts b/envs/example/swiftbrowser/hosts new file mode 100644 index 0000000..2061120 --- /dev/null +++ b/envs/example/swiftbrowser/hosts @@ -0,0 +1,2 @@ +[swiftbrowser] +swiftbrowser01 diff --git a/envs/example/swiftbrowser/vagrant.yml b/envs/example/swiftbrowser/vagrant.yml new file mode 100644 index 0000000..02005d2 --- /dev/null +++ b/envs/example/swiftbrowser/vagrant.yml @@ -0,0 +1,10 @@ +default: + memory: 512 + cpus: 1 + +vms: + swiftbrowser01: + ip_address: + - 172.16.0.101 + - 172.16.1.101 + memory: 512 diff --git a/envs/example/vagrant.yml b/envs/example/vagrant.yml new file mode 100644 index 0000000..a130f56 --- /dev/null +++ b/envs/example/vagrant.yml @@ -0,0 +1,34 @@ +--- +datacenter: vagrant +public_interface: ansible_eth1 +private_interface: ansible_eth2 +public_device_interface: eth1 +private_device_interface: eth2 +public_ipv4: "{{ hostvars[inventory_hostname][public_interface]['ipv4'] }}" +private_ipv4: "{{ hostvars[inventory_hostname][private_interface]['ipv4'] }}" +admin_user: vagrant + +common: + mdns: + enabled: True + ssh: + allow_from: + - "0.0.0.0/0" + users: + - name: vagrant + #pass: + public_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTwSNZ+ECq3FUZ31VCxzDxVyPD6rtKmRYz6LhpiQWwhyhS+4KRkyRrQ85Xz7Y2p5HsrY7ej2maWs2x5LetDmpv0ow+JLtGGrp/jdZh0QrGwewdpZ48qPr7rgwxck5DSXVEu5L/SyEbtw1TYVnHmhVQuxuKeu/zEQklFrCwwLzbMHMpF7+SJdfh3n4vx5rIZqAzlWn9Y+1E7C1LKR9WfkXqz1xL5mGb7ZEka/m+GqFflR8iW82wOG9CryDXPryBRKt82Oa3F0BpUJe94Q9X/BP0l6zc7eoTijQMmwD8KQbvf0buBJrjOXf2qtc0X2RhyBfxz4fxOaLHekyQOt0eHsxEzoRtc938ahKrG0rbRKqVJISMwi9eOOHmwRRqeZeX2OTTsZRyzjPNLtcoWxLqgRmSkRa0c9p88biB4+kLoj7uT93kTcpwYUvXx4UEMmw3tezYfaMGSFLbtW7V0FyS5AwIiAp6RvOX2fewmiAVkSbbCGeeSlOSVvbUC6RLjC+jjNJVct+p8+XvMcG9m5R0iweFKO57RG6JDjnn7dFt//i0OvJFZlZq/E0tFS/+NqvPtT93FvVmXCUDDnmrhS1pQl2D7RmrOp8/X3f4G/kdJ+ZXI7xcX/IBKA2Y/Yn9fUlB4Ozojk6bLXE1xMywEF2uuvL0F6CIA3rh16Xl2PAiJ7OeCQ== dev01 default site key + +#env_vars: +# http_proxy: "http://10.0.2.2:3128" +# https_proxy: "http://10.0.2.2:3128" +# no_proxy: localhost,127.0.0.0/8,10.0.0.0/8,172.0.0.0/8 + +#sitecontroller: +# pypi_mirror: http://10.0.2.2:3141/root/pypi +# easy_install_mirror: http://10.0.2.2:3141/root/pypi/+simple/ +# pip_trusted: 10.0.2.2 +# gem_sources: +# - http://gem-mirror.openstack.bbg diff --git a/hosts b/hosts new file mode 100644 index 0000000..ce65d2a --- /dev/null +++ b/hosts @@ -0,0 +1,11 @@ +[jenkins] +173.247.105.49 + +[graphite] +graphite.openstack.blueboxgrid.com + +[repo] +repo.openstack.blueboxgrid.com + +[rally] +rally.openstack.blueboxgrid.com diff --git a/library/apache2_site.py b/library/apache2_site.py new file mode 100644 index 0000000..ad5f4f2 --- /dev/null +++ b/library/apache2_site.py @@ -0,0 +1,89 @@ +#!/usr/bin/python +#coding: utf-8 -*- + +# (c) 2013-2014, Christian Berendt +# +# This site is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +DOCUMENTATION = ''' +--- +module: apache2_site +version_added: 1.6 +short_description: enables/disables a site for the Apache2 webserver +description: + - Enables or disables a specified site for the Apache2 webserver. +options: + name: + description: + - name of the site to enable/disable + required: true + state: + description: + - indicate the desired state of the resource + choices: ['enabled', 'disabled', 'present', 'absent'] + default: enabled + +''' + +EXAMPLES = ''' +# enables the Apache2 site "default" +- apache2_site: state=enabled name=default + +# disables the Apache2 site "default" +- apache2_site: state=disabled name=default +''' + +import re + +def _disable_site(site): + name = site.params['name'] + a2dissite_binary = site.get_bin_path("a2dissite") + result, stdout, stderr = site.run_command("%s %s" % (a2dissite_binary, name)) + + if re.match(r'.*' + name + r' already disabled.*', stdout, re.S): + site.exit_json(changed = False, result = "Success") + elif result != 0: + site.fail_json(msg="Failed to disable site %s: %s" % (name, stdout)) + else: + site.exit_json(changed = True, result = "Disabled") + +def _enable_site(site): + name = site.params['name'] + a2dissite_binary = site.get_bin_path("a2ensite") + result, stdout, stderr = site.run_command("%s %s" % (a2dissite_binary, name)) + + if re.match(r'.*' + name + r' already enabled.*', stdout, re.S): + site.exit_json(changed = False, result = "Success") + elif result != 0: + site.fail_json(msg="Failed to enable site %s: %s" % (name, stdout)) + else: + site.exit_json(changed = True, result = "Enabled") + +def main(): + site = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + state = dict(default='enabled', choices=['disabled', 'enabled', 'present', 'absent']) + ), + ) + + if site.params['state'] == 'enabled' or site.params['state'] == 'present': + _enable_site(site) + + if site.params['state'] == 'disabled' or site.params['state'] == 'absent': + _disable_site(site) + +# import site snippets +from ansible.module_utils.basic import * +main() diff --git a/library/binary.py b/library/binary.py new file mode 100644 index 0000000..2d564b3 --- /dev/null +++ b/library/binary.py @@ -0,0 +1,91 @@ +#!/usr/bin/python + +DOCUMENTATION = ''' +--- +module: binary +version_added: 0.1 +short_description: outputs binary files safely +description: + - Will safely output base64 encoded binaries. +options: + base64: + description: + - Base64 encoded binary file + required: true + dest: + description: + - Path of the file to write to + required: true + owner: + description: + - owner of the file + required: false + group: + description: + - group of the file + required: false +''' + +EXAMPLES = ''' +- binary: base64="{{ my_base64_binary }}" dest=/var/lib/secret.key +''' + +import base64 +import md5 +import os +import pwd +import grp + + +def main(): + conf = AnsibleModule( + argument_spec = dict( + base64 = dict(required=True), + dest = dict(required=True), + owner = dict(required=False, default=None), + group = dict(required=False, default=None), + ) + ) + + binary = base64.b64decode(conf.params['base64']) + dest = conf.params['dest'] + md5sum = md5.md5(binary).hexdigest() + current_md5sum = None + changed = False + diff = {'before': {}, 'after': {}} + + if os.path.exists(dest): + with open(dest, 'rb') as f: + current_md5sum = md5.md5(f.read()).hexdigest() + + if current_md5sum != md5sum: + with open(dest, 'wb') as f: + f.write(binary) + changed = True + diff['before']['md5'] = current_md5sum + diff['after']['md5'] = md5sum + + if conf.params['owner']: + uid = pwd.getpwnam(conf.params['owner']).pw_uid + current_uid = os.stat(dest).st_uid + if uid != current_uid: + os.chown(dest, uid, -1) + changed = True + diff['before']['owner'] = pwd.getpwuid(current_uid).pw_name + diff['after']['owner'] = conf.params['owner'] + + if conf.params['group']: + gid = grp.getgrnam(conf.params['group']).gr_gid + current_gid = os.stat(dest).st_gid + if gid != current_gid: + os.chown(dest, -1, gid) + changed = True + diff['before']['group'] = grp.getgrgid(current_gid).gr_name + diff['after']['group'] = conf.params['group'] + + conf.exit_json(changed=changed, result=diff) + + +# import site snippets +from ansible.module_utils.basic import * +main() diff --git a/library/jenkins.py b/library/jenkins.py new file mode 100644 index 0000000..f0a5009 --- /dev/null +++ b/library/jenkins.py @@ -0,0 +1,210 @@ +#!/usr/bin/python + +DOCUMENTATION = ''' +--- +module: jenkins_safe_restart +version_added: 0.1 +short_description: restarts jenkins safely +description: + - Will restart a jenkins server, allowing jobs to finish, and waiting for the server to fully start up before continuing. +options: + task: + description: + - The task to perform. + choices: + - wait_for_startup + - restart + - install_plugin + required: true + url: + description: + - URL of the jenkins server + required: true + username: + description: + - username for access + required: false + password: + description: + - password for access + required: false + password: + description: + - password for access + required: false + plugin: + description: + - plugin to install + required: false + version: + description: + - plugin version + required: false + shutdown_check_delay: + description: + - seconds to delay between checking for complete shutdown + required: false +''' + +EXAMPLES = ''' +# safely restart a running jenkins instance +- jenkins: url=http://127.0.0.1:8080/ username=admin password=admnin123 task=restart +''' + +import base64 +import time +import urllib2 +import os +from ansible.module_utils.urls import * + + +def _get_installed_plugin_version(plugin): + path = '/var/lib/jenkins/plugins/%s/META-INF/MANIFEST.MF' % plugin + if os.path.exists(path): + version_line = [l for l in open(path, 'r').readlines() if l.startswith('Plugin-Version')][0] + return version_line.split(': ')[1].strip() + + +def main(): + conf = AnsibleModule( + argument_spec = dict( + url = dict(required=True), + task = dict( + required=True, + choices=['restart', 'wait_for_startup', 'install_plugin']), + username = dict(required=False, default=None), + password = dict(required=False, default=None), + plugin = dict(required=False, default=None), + version = dict(required=False, default="latest"), + plugin_install_retries = dict(required=False, default=5), + plugin_install_timeout = dict(required=False, default=10), + shutdown_check_delay = dict( + type='int', required=False, default=1), + ) + ) + try: + _deal_with_jenkins(conf) + except urllib2.HTTPError as e: + conf.fail_json(msg="Jenkins failed.\n\n%s\n\n%s\n" % (e.msg, e.read())) + + +def _deal_with_jenkins(conf): + headers={} + task = conf.params['task'] + jenkins_url = conf.params['url'] + prepare_shutdown_url = jenkins_url + 'quietDown' + restart_url = jenkins_url + 'restart' + jobs_check_url = jenkins_url + 'computer/api/xml?xpath=//busyExecutors' + username = conf.params['username'] + password = conf.params['password'] + shutdown_check_delay = conf.params['shutdown_check_delay'] + + # first, detect if jenkins is running with security turned on or not + # because we might be working with a brand new jenkins install that + # has not had the security settings applied yet + + + + # ansible < 2.0 does not do basic auth properly, got to handle it here + if username is not None and password is not None: + headers["Authorization"] = "Basic %s" % base64.b64encode( + "%s:%s" % (username, password)) + + if task in ('install_plugin', 'restart'): + # check if security is enabled or not + try: + res = open_url(jenkins_url) + if res.getcode() == 200: + del(headers["Authorization"]) + except: + pass + + if task == 'install_plugin': + install_timeout = float(conf.params['plugin_install_timeout']) + install_retries = int(conf.params['plugin_install_retries']) + plugin = conf.params['plugin'] + version = conf.params['version'] + plugin_request_headers = headers.copy() + plugin_request_headers['Content-Type'] = 'text/xml' + + if version == 'latest': + plugin_download_url = 'http://updates.jenkins-ci.org/latest/%s.hpi' % plugin + else: + plugin_download_url = 'http://updates.jenkins-ci.org/download/plugins/%s/%s/%s.hpi' % (plugin, version, plugin) + + plugin_path = '/var/lib/jenkins/plugins/%s.jpi' % plugin + + # always unpin the plugin, we want to control versions + if os.path.exists(plugin_path + '.pinned'): + os.unlink(plugin_path + '.pinned') + + current_version = _get_installed_plugin_version(plugin) + + if version == 'latest' or current_version != version: + plugin_request = open_url(plugin_download_url) + with open(plugin_path, 'wb') as pfp: + pfp.write(plugin_request.fp.read()) + conf.exit_json(changed=True, result="Jenkins plugin %s uploaded" % plugin) + + conf.exit_json(changed=False, result="Jenkins plugin %s already installed" % plugin) + + if task == 'restart': + try: + # sometimes, jenkins is started up enough to do things, but + # not started up enough to prepare to shutdown, which makes + # the prepare shutdown url gives a 500. So, retry it after a delay + open_url(prepare_shutdown_url, headers=headers, method='POST', timeout=120) + except: + time.sleep(60) + open_url(prepare_shutdown_url, headers=headers, method='POST', timeout=120) + # if it doesn't work after letting jenkins warm up after jenkins + # appears to be working, then just fail. + + # Wait until there are no jobs running + jobs_running = True + while jobs_running: + try: + res = open_url(jobs_check_url, headers=headers) + output = ''.join(res.readlines()) + jobs_running = '0' not in output + except Exception as e: + pass + time.sleep(shutdown_check_delay) + + try: + open_url(restart_url, headers=headers, method='POST') + except: + # Jenkins will immediately shutdown and restart. It also redirects + # to the index page. That means there are many errors we might have + # to handle here, socket errors, httplib errors, urllib2 errors etc. + # so if we've got this far we can be reasonably confident that jenkins + # is working, and any errors here are due to it not being available + # during some stage of the restart. So, catch and ignore everything. + # This will not be needed with ansible 2, because we can tell + # open_url not to follow the redirect. + pass + finally: + # Now we need to wait for jenkins to fully start up. + task = 'wait_for_startup' + + if task == 'wait_for_startup': + # any non-error response code is enough to let us know jenkins is running. + response_code = 503 + while response_code > 499: + try: + # don't send the auth headers, a 403 is an expected response + # and shows jenkins is running + response_code = open_url(jenkins_url).getcode() + time.sleep(1) + except Exception as e: + if hasattr(e, 'code'): + response_code = e.code + time.sleep(1) + + conf.exit_json(changed=True, result="Jenkins running") + + +# import site snippets +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +main() diff --git a/library/logrotate.py b/library/logrotate.py new file mode 100644 index 0000000..8cab923 --- /dev/null +++ b/library/logrotate.py @@ -0,0 +1,183 @@ +#!/usr/bin/python +#coding: utf-8 -*- + +# Copyright (c) 2014 John Dewey +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os +import hashlib + + +DOCUMENTATION = """ +--- +module: logrotate +version_added: 1.6 +short_description: Manages logrotate configuration files. +description: + - Installs the logrotate package. + - Manages logrotate configuration files. +options: + name: + description: + - Name of the logrotate config file. + required: true + path: + description: + - Path of the file to manage. + required: true + options: + description: + - A dict with logrotate options. + default: + - daily + - missingok + - rotate 8 + - compress + - delaycompress + - copytruncate + - notifempty + required: false + config_dir: + description: + - Directory containing config files. + default: /etc/logrotate.d + required: false + state: + description: + - Indicate the desired state of the resource. + choices: ['present', 'absent'] + default: present +requirements: ["logrotate"] +""" + +EXAMPLES = """ +# Rotate the Apache2 logs daily while keeping 8 +- logrotate: name=apache2 path=/var/log/apache2/*.log + args: + options: + - daily + - rotate 8 + +# Rotate the Apache2 logs while adding a postrotate script +# Add a config file with the create directive. +- logrotate: name=apache2 path=/var/log/apache2/*.log + args: + options: + - daily + - rotate 8 + - postrotate + - exec script + - endscript + +# Remove the Apache2 config file +- logrotate: name=apache2 path=/var/log/apache2/*.log state=absent +""" + +TEMPLATE = """ +# Generated by Ansible. +# Local modifications will be overwritten. + +{path} +{{ + {options} +}} +""" + + +def _compare_config(config, config_path): + current_run_md5 = hashlib.md5(config).hexdigest() + on_disk_md5 = None + with open(config_path) as f: + data = f.read() + on_disk_md5 = hashlib.md5(data).hexdigest() + + return on_disk_md5 == current_run_md5 + + +def _write_config(config, config_path): + with open(config_path, 'w') as f: + f.write(config) + + +def _get_config_path(module): + name = module.params.get('name') + config_dir = module.params.get('config_dir') + + return os.path.join(config_dir, name) + + +def _get_config(module): + path = module.params.get('path') + options = module.params.get('options') + + return TEMPLATE.format(path=path, + options='\n '.join(options)) + + +def _add_config(module): + config_path = _get_config_path(module) + config = _get_config(module) + if os.path.isfile(config_path): + if not _compare_config(config, config_path): + _write_config(config, config_path) + module.exit_json(changed=True, result="Enabled") + module.exit_json(changed=False, result="Success") + else: + _write_config(config, config_path) + module.exit_json(changed=True, result="Enabled") + + +def _remove_config(module): + config_path = _get_config_path(module) + if os.path.isfile(config_path): + os.remove(config_path) + module.exit_json(changed=True, result="Disabled") + else: + module.exit_json(changed=False, result="Success") + + +def main(): + module = AnsibleModule( + argument_spec = dict( + name = dict(required=True), + path = dict(required=True), + options = dict(type='list', + default=['daily', + 'missingok', + 'rotate 8', + 'compress', + 'delaycompress', + 'copytruncate', + 'notifempty']), + config_dir = dict(default='/etc/logrotate.d'), + state = dict(default='present', choices=['absent', 'present']), + ), + ) + + if module.params.get('state') == 'present': + _add_config(module) + + if module.params.get('state') == 'absent': + _remove_config(module) + + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +main() diff --git a/library/rabbitmq_user.py b/library/rabbitmq_user.py new file mode 100644 index 0000000..0adc222 --- /dev/null +++ b/library/rabbitmq_user.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2013, Chatham Financial +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: rabbitmq_user +short_description: Adds or removes users to RabbitMQ +description: + - Add or remove users to RabbitMQ and assign permissions +version_added: "1.1" +author: '"Chris Hoffman (@chrishoffman)"' +options: + user: + description: + - Name of user to add + required: true + default: null + aliases: [username, name] + password: + description: + - Password of user to add. + - To change the password of an existing user, you must also specify + C(force=yes). + required: false + default: null + tags: + description: + - User tags specified as comma delimited + required: false + default: null + permissions: + description: + - a list of dicts, each dict contains vhost, configure_priv, write_priv, and read_priv, + and represents a permission rule for that vhost. + - This option should be preferable when you care about all permissions of the user. + - You should use vhost, configure_priv, write_priv, and read_priv options instead + if you care about permissions for just some vhosts. + required: false + default: [] + vhost: + description: + - vhost to apply access privileges. + - This option will be ignored when permissions option is used. + required: false + default: / + node: + description: + - erlang node name of the rabbit we wish to configure + required: false + default: rabbit + version_added: "1.2" + configure_priv: + description: + - Regular expression to restrict configure actions on a resource + for the specified vhost. + - By default all actions are restricted. + - This option will be ignored when permissions option is used. + required: false + default: ^$ + write_priv: + description: + - Regular expression to restrict configure actions on a resource + for the specified vhost. + - By default all actions are restricted. + - This option will be ignored when permissions option is used. + required: false + default: ^$ + read_priv: + description: + - Regular expression to restrict configure actions on a resource + for the specified vhost. + - By default all actions are restricted. + - This option will be ignored when permissions option is used. + required: false + default: ^$ + force: + description: + - Deletes and recreates the user. + required: false + default: "no" + choices: [ "yes", "no" ] + state: + description: + - Specify if user is to be added or removed + required: false + default: present + choices: [present, absent] +''' + +EXAMPLES = ''' +# Add user to server and assign full access control on / vhost. +# The user might have permission rules for other vhost but you don't care. +- rabbitmq_user: user=joe + password=changeme + vhost=/ + configure_priv=.* + read_priv=.* + write_priv=.* + state=present +# Add user to server and assign full access control on / vhost. +# The user doesn't have permission rules for other vhosts +- rabbitmq_user: user=joe + password=changeme + permissions=[{vhost='/', configure_priv='.*', read_priv='.*', write_priv='.*'}] + state=present +''' + +class RabbitMqUser(object): + def __init__(self, module, username, password, tags, permissions, + node, bulk_permissions=False): + self.module = module + self.username = username + self.password = password + self.node = node + if not tags: + self.tags = list() + else: + self.tags = tags.split(',') + + self.permissions = permissions + self.bulk_permissions = bulk_permissions + + self._tags = None + self._permissions = [] + self._rabbitmqctl = module.get_bin_path('rabbitmqctl', True) + + def _exec(self, args, run_in_check_mode=False): + if not self.module.check_mode or (self.module.check_mode and run_in_check_mode): + cmd = [self._rabbitmqctl, '-q'] + if self.node is not None: + cmd.extend(['-n', self.node]) + rc, out, err = self.module.run_command(cmd + args, check_rc=True) + return out.splitlines() + return list() + + def get(self): + users = self._exec(['list_users'], True) + + for user_tag in users: + if '\t' not in user_tag: + continue + + user, tags = user_tag.split('\t') + + if user == self.username: + for c in ['[',']',' ']: + tags = tags.replace(c, '') + + if tags != '': + self._tags = tags.split(',') + else: + self._tags = list() + + self._permissions = self._get_permissions() + return True + return False + + def _get_permissions(self): + perms_out = self._exec(['list_user_permissions', self.username], True) + + perms_list = list() + for perm in perms_out: + vhost, configure_priv, write_priv, read_priv = perm.split('\t') + if not self.bulk_permissions: + if vhost == self.permissions[0]['vhost']: + perms_list.append(dict(vhost=vhost, configure_priv=configure_priv, + write_priv=write_priv, read_priv=read_priv)) + break + else: + perms_list.append(dict(vhost=vhost, configure_priv=configure_priv, + write_priv=write_priv, read_priv=read_priv)) + return perms_list + + def add(self): + if self.password is not None: + self._exec(['add_user', self.username, self.password]) + else: + self._exec(['add_user', self.username, '']) + self._exec(['clear_password', self.username]) + + def delete(self): + self._exec(['delete_user', self.username]) + + def set_tags(self): + self._exec(['set_user_tags', self.username] + self.tags) + + def set_permissions(self): + for permission in self._permissions: + if permission not in self.permissions: + cmd = ['clear_permissions', '-p'] + cmd.append(permission['vhost']) + cmd.append(self.username) + self._exec(cmd) + for permission in self.permissions: + if permission not in self._permissions: + cmd = ['set_permissions', '-p'] + cmd.append(permission['vhost']) + cmd.append(self.username) + cmd.append(permission['configure_priv']) + cmd.append(permission['write_priv']) + cmd.append(permission['read_priv']) + self._exec(cmd) + + def has_tags_modifications(self): + return set(self.tags) != set(self._tags) + + def has_permissions_modifications(self): + return self._permissions != self.permissions + +def main(): + arg_spec = dict( + user=dict(required=True, aliases=['username', 'name']), + password=dict(default=None), + tags=dict(default=None), + permissions=dict(default=list(), type='list'), + vhost=dict(default='/'), + configure_priv=dict(default='^$'), + write_priv=dict(default='^$'), + read_priv=dict(default='^$'), + force=dict(default='no', type='bool'), + state=dict(default='present', choices=['present', 'absent']), + node=dict(default=None) + ) + module = AnsibleModule( + argument_spec=arg_spec, + supports_check_mode=True + ) + + username = module.params['user'] + password = module.params['password'] + tags = module.params['tags'] + permissions = module.params['permissions'] + vhost = module.params['vhost'] + configure_priv = module.params['configure_priv'] + write_priv = module.params['write_priv'] + read_priv = module.params['read_priv'] + force = module.params['force'] + state = module.params['state'] + node = module.params['node'] + + bulk_permissions = True + if permissions == []: + perm = { + 'vhost': vhost, + 'configure_priv': configure_priv, + 'write_priv': write_priv, + 'read_priv': read_priv + } + permissions.append(perm) + bulk_permissions = False + + rabbitmq_user = RabbitMqUser(module, username, password, tags, permissions, + node, bulk_permissions=bulk_permissions) + + changed = False + if rabbitmq_user.get(): + if state == 'absent': + rabbitmq_user.delete() + changed = True + else: + if force: + rabbitmq_user.delete() + rabbitmq_user.add() + rabbitmq_user.get() + changed = True + + if rabbitmq_user.has_tags_modifications(): + rabbitmq_user.set_tags() + changed = True + + if rabbitmq_user.has_permissions_modifications(): + rabbitmq_user.set_permissions() + changed = True + elif state == 'present': + rabbitmq_user.add() + rabbitmq_user.set_tags() + rabbitmq_user.set_permissions() + changed = True + + module.exit_json(changed=changed, user=username, state=state) + +# import module snippets +from ansible.module_utils.basic import * +main() diff --git a/library/sensu_check.py b/library/sensu_check.py new file mode 100644 index 0000000..eee60aa --- /dev/null +++ b/library/sensu_check.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014, Blue Box Group, Inc. +# Copyright 2014, Craig Tracey +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import traceback + +from hashlib import md5 +from jinja2 import Environment + +def main(): + + module = AnsibleModule( + argument_spec=dict( + name=dict(default=None, required=True), + use_sudo=dict(required=False, type='bool', default=False), + auto_resolve=dict(required=False, type='bool', default=True), + interval=dict(required=False, default=30), + occurrences=dict(required=False, default=2), + plugin=dict(required=True), + args=dict(required=False, default=''), + prefix=dict(required=False, default=''), + env_vars=dict(required=False, default=''), + command=dict(required=False, default=''), + service_owner=dict(required=False, default=None), + handlers=dict(required=False, default='default'), + tags=dict(required=False, type='list'), + dependencies=dict(required=False, type='list'), + check_dir=dict(default='/etc/sensu/conf.d/checks', required=False), + state=dict(default='present', required=False, choices=['present','absent']) + ) + ) + + if module.params['state'] == 'present': + try: + changed = False + check_path = '%s/%s.json' % (module.params['check_dir'], module.params['name']) + command = module.params['command'] + if not command: + command = '%s %s' % (module.params['plugin'], module.params['args']) + if module.params['env_vars']: + env_vars = module.params['env_vars'].replace(":","=").replace(","," ") + command = '%s %s' % (env_vars, command) + if module.params['prefix']: + command = '%s %s' % (module.params['prefix'], command) + if module.params['use_sudo']: + command = "sudo %s" % (command) + check=dict({ + 'checks': { + module.params['name']: { + 'command': command, + 'standalone': True, + 'handlers': module.params['handlers'].split(','), + 'interval': int(module.params['interval']), + 'occurrences': int(module.params['occurrences']), + 'auto_resolve': module.params['auto_resolve'], + 'service_owner': module.params['service_owner'], + 'tags': module.params['tags'], + 'dependencies': module.params['dependencies'] + } + } + }) + + if os.path.isfile(check_path): + with open(check_path) as fh: + if json.load(fh) == check: + module.exit_json(changed=False, result="ok") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="changed") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="created") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="creating the check failed: %s %s" % (e,formatted_lines)) + + else: + try: + changed = False + check_path = '%s/%s.json' % (module.params['check_dir'], module.params['name']) + if os.path.isfile(check_path): + os.remove(check_path) + module.exit_json(changed=True, result="changed") + else: + module.exit_json(changed=False, result="ok") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="removing the check failed: %s %s" % (e,formatted_lines)) + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +main() diff --git a/library/sensu_check_dict.py b/library/sensu_check_dict.py new file mode 100644 index 0000000..58125b7 --- /dev/null +++ b/library/sensu_check_dict.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014, Blue Box Group, Inc. +# Copyright 2014, Craig Tracey +# Copyright 2016, Paul Czarkowski +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import traceback + +from hashlib import md5 +from jinja2 import Environment + +def main(): + + module = AnsibleModule( + argument_spec=dict( + name=dict(default=None, required=True), + check_dir=dict(default='/etc/sensu/conf.d/checks', required=False), + state=dict(default='present', required=False, choices=['present','absent']), + check=dict(type='dict', required=True) + ) + ) + + if module.params['state'] == 'present': + try: + changed = False + check_path = '%s/%s.json' % (module.params['check_dir'], module.params['name']) + check=dict({ + 'checks': { + module.params['name']: module.params['check'] + } + }) + + + if os.path.isfile(check_path): + with open(check_path) as fh: + if json.load(fh) == check: + module.exit_json(changed=False, result="ok") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="changed") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="created") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="creating the check failed: %s %s" % (e,formatted_lines)) + + else: + try: + changed = False + check_path = '%s/%s.json' % (module.params['check_dir'], module.params['name']) + if os.path.isfile(check_path): + os.remove(check_path) + module.exit_json(changed=True, result="changed") + else: + module.exit_json(changed=False, result="ok") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="removing the check failed: %s %s" % (e,formatted_lines)) + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +main() diff --git a/library/sensu_metrics_check.py b/library/sensu_metrics_check.py new file mode 100644 index 0000000..ee922c3 --- /dev/null +++ b/library/sensu_metrics_check.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014, Blue Box Group, Inc. +# Copyright 2014, Craig Tracey +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import traceback + +from hashlib import md5 +from jinja2 import Environment + +def main(): + + module = AnsibleModule( + argument_spec=dict( + name=dict(default=None, required=True), + use_sudo=dict(required=False, type='bool', default=False), + plugin=dict(default=None, required=True), + args=dict(default='', required=False), + handlers=dict(required=False, default='metrics'), + service_owner=dict(required=False, default=None), + check_dir=dict(default='/etc/sensu/conf.d/checks', required=False), + prefix=dict(default='', required=False), + interval=dict(default=60, required=False), + state=dict(default='present', required=False, choices=['present','absent']) + ) + ) + + if module.params['state'] == 'present': + try: + changed = False + check_path = '%s/%s.json' % (module.params['check_dir'], module.params['name']) + command = '%s %s' % ( module.params['plugin'], module.params['args'] ) + if module.params['prefix']: + command = '%s %s' % (module.params['prefix'], command) + if module.params['use_sudo']: + command = "sudo %s" % (command) + check=dict({ + 'checks': { + module.params['name']: { + 'type': 'metric', + 'command': command, + 'standalone': True, + 'interval': int(module.params['interval']), + 'handlers': module.params['handlers'].split(','), + 'service_owner': module.params['service_owner'] + } + } + }) + + if os.path.isfile(check_path): + with open(check_path) as fh: + if json.load(fh) == check: + module.exit_json(changed=False, result="ok") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="changed") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="created") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="creating the check failed: %s %s" % (e,formatted_lines)) + + else: + try: + changed = False + check_path = '%s/%s.json' % (module.params['check_dir'], module.params['name']) + if os.path.isfile(check_path): + os.remove(check_path) + module.exit_json(changed=True, result="changed") + else: + module.exit_json(changed=False, result="ok") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="removing the check failed: %s %s" % (e,formatted_lines)) + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +main() diff --git a/library/sensu_process_check.py b/library/sensu_process_check.py new file mode 100644 index 0000000..91d782b --- /dev/null +++ b/library/sensu_process_check.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014, Blue Box Group, Inc. +# Copyright 2014, Craig Tracey +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import traceback + +def main(): + + module = AnsibleModule( + argument_spec=dict( + service=dict(default=None, required=True), + short_service_name=dict(required=False), + warn_over=dict(default=15, required=False), + crit_over=dict(default=30, required=False), + interval=dict(default=30, required=False), + occurrences=dict(default=2, required=False), + service_owner=dict(required=False, default=None), + handlers=dict(required=False, default='default'), + tags=dict(required=False, type='list'), + dependencies=dict(required=False, type='list'), + check_dir=dict(default='/etc/sensu/conf.d/checks', required=False), + state=dict(default='present', required=False, choices=['present','absent']) + ) + ) + + if module.params['state'] == 'present': + try: + changed = False + if not module.params['short_service_name']: + short_service_name = os.path.basename(module.params['service']) + check_path = "%s/%s-service.json" % ( module.params['check_dir'], short_service_name) + command = "check-procs.rb -p %s -w %s -c %s -W 1 -C 1" % (module.params['service'], module.params['warn_over'], module.params['crit_over'] ) + notification = "unexpected number of %s processes" % ( module.params['service'] ) + check=dict({ + 'checks': { + short_service_name: { + 'command': command, + 'standalone': True, + 'handlers': [ 'default' ], + 'interval': int(module.params['interval']), + 'notification': notification, + 'occurrences': int(module.params['occurrences']), + 'tags': module.params['tags'], + 'dependencies': module.params['dependencies'] + } + } + }) + + if os.path.isfile(check_path): + with open(check_path) as fh: + if json.load(fh) == check: + module.exit_json(changed=False, result="ok") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="changed") + else: + with open(check_path, "w") as fh: + fh.write(json.dumps(check, indent=4)) + module.exit_json(changed=True, result="created") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="creating the check failed: %s %s" % (e,formatted_lines)) + + else: + try: + changed = False + check_path = '%s/%s.json' % (module.params['check_dir'], module.params['name']) + if os.path.isfile(check_path): + os.remove(check_path) + module.exit_json(changed=True, result="changed") + else: + module.exit_json(changed=False, result="ok") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="removing the check failed: %s %s" % (e,formatted_lines)) + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +main() diff --git a/library/systemd_service.py b/library/systemd_service.py new file mode 100755 index 0000000..dfbd4e8 --- /dev/null +++ b/library/systemd_service.py @@ -0,0 +1,191 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014, Blue Box Group, Inc. +# Copyright 2014, Craig Tracey +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import traceback + +from hashlib import md5 +from jinja2 import Environment + +INIT_CONFIG_TEMPLATE = """ +[Unit] +{% if description -%} +Description={{ description }} +{% endif %} + +{% if default_deps -%} +DefaultDependencies={{ default_deps }} +{% endif %} + +{% if after -%} +After={{ after }} +{% else -%} +After=network.target syslog.target +{% endif %} + +{% if before -%} +Before={{ before }} +{% endif %} + +{% if wants -%} +Wants={{ wants }} +{% endif %} + +[Install] +{% if wanted_by -%} +WantedBy={{ wanted_by }} +{% endif %} +##### Alias={{ name }}.service + +[Service] +{% if service_type -%} +Type={{ service_type }} +{% endif %} + +{% if pidfile -%} +PIDFile={{ pidfile }} +{% endif %} + +{% if env_vars -%} +Environment={{ env_vars | join(' ') }} +{% endif %} +{% if environment_file -%} +EnvironmentFile={{ environment_file }} +{% endif %} + +{% if chdir -%} +WorkingDirectory={{ chdir }} +{% endif %} + +{% if restart -%} +Restart={{ restart }} +{% endif %} + +# Start main service +ExecStart={{ cmd }} {{ args }} + +{% if prestart_script -%} +ExecStartPre={{ prestart_script }} +{% endif %} + +#ExecStop= + +#ExecStopPost= + +{% if reload_script -%} +ExecReload={{ reload_script }} +{% endif %} + +{% if user -%} +User={{ user }} +{% endif %} +{% if group -%} +Group={{ group }} +{% endif %} + +TimeoutStartSec=120 +TimeoutStopSec=120 + +""" + +def main(): + + module = AnsibleModule( + argument_spec=dict( + after=dict(default=None), + args=dict(default=None), + before=dict(default=None), + chdir=dict(default=None), + cmd=dict(default=None, required=True), + config_dirs=dict(default=None), + config_files=dict(default=None), + default_deps=dict(default=None), + description=dict(default=None), + env_vars=dict(default=None, type='list'), + group=dict(default=None), + name=dict(default=None, required=True), + path=dict(default=None), + pidfile=dict(default=None), + prestart_script=dict(default=None), + reload_script=dict(default=None), + restart=dict(default="on-failure"), + service_type=dict(default="simple"), + state=dict(default='present'), + user=dict(default=None), + wanted_by=dict(default=None), + wants=dict(default=None) + ) + ) + + try: + changed = False + service_path = None + if not module.params['path']: + service_path = '/etc/systemd/system/%s.service' % module.params['name'] + else: + service_path = module.params['path'] + + if module.params['state'] == 'absent': + if os.path.exists(service_path): + os.remove(service_path) + changed = True + if not changed: + module.exit_json(changed=False, result="ok") + else: + os.system('systemctl daemon-reload') + module.exit_json(changed=True, result="changed") + + args = ' ' + if module.params['args'] or module.params['config_dirs'] or \ + module.params['config_files']: + if module.params['args']: + args += module.params['args'] + + if module.params['config_dirs']: + for directory in module.params['config_dirs'].split(','): + args += '--config-dir %s ' % directory + + if module.params['config_files']: + for filename in module.params['config_files'].split(','): + args += '--config-file %s ' % filename + + template_vars = module.params + template_vars['args'] = args + + env = Environment().from_string(INIT_CONFIG_TEMPLATE) + rendered_service = env.render(template_vars) + + if os.path.exists(service_path): + file_hash = md5(open(service_path, 'rb').read()).hexdigest() + template_hash = md5(rendered_service).hexdigest() + if file_hash == template_hash: + module.exit_json(changed=False, result="ok") + + with open(service_path, "w") as fh: + fh.write(rendered_service) + os.system('systemctl daemon-reload') + module.exit_json(changed=True, result="created") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="creating the service failed: %s" % (str(e))) + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * + +main() diff --git a/library/upstart_service.py b/library/upstart_service.py new file mode 100644 index 0000000..458c777 --- /dev/null +++ b/library/upstart_service.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014, Blue Box Group, Inc. +# Copyright 2014, Craig Tracey +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import traceback + +from hashlib import md5 +from jinja2 import Environment + +UPSTART_TEMPLATE = """ +start on {{ start_on }} +stop on {{ stop_on }} + +{% if description -%} +description {{ description }} +{% endif -%} + +{% if envs -%} +{% for env in envs %} +env {{ env }} +{% endfor %} +{% endif -%} + +{% if prestart_script -%} +pre-start script + {{ prestart_script }} +end script +{% endif -%} + +{% if respawn -%} +respawn +{% endif -%} + +exec start-stop-daemon --start --chuid {{ user }} {{ pidfile }} --exec {{ cmd }} {{ args }} +""" + +def main(): + + module = AnsibleModule( + argument_spec=dict( + name=dict(default=None, required=True), + cmd=dict(default=None, required=True), + args=dict(default=None), + user=dict(default=None, required=True), + config_dirs=dict(default=None), + config_files=dict(default=None), + description=dict(default=None), + envs=dict(default=None, required=False, type='list'), + state=dict(default='present'), + start_on=dict(default='runlevel [2345]'), + stop_on=dict(default='runlevel [!2345]'), + prestart_script=dict(default=None), + respawn=dict(default=True), + path=dict(default=None), + pidfile=dict(default=None) + ) + ) + + try: + changed = False + service_path = None + if not module.params['path']: + service_path = '/etc/init/%s.conf' % module.params['name'] + else: + service_path = module.params['path'] + + symlink = os.path.join('/etc/init.d/', module.params['name']) + + if module.params['state'] == 'absent': + if os.path.exists(service_path): + os.remove(service_path) + changed = True + if os.path.exists(symlink): + os.remove(symlink) + changed = True + if not changed: + module.exit_json(changed=False, result="ok") + else: + module.exit_json(changed=True, result="changed") + + pidfile = '' + if module.params['pidfile'] and len(module.params['pidfile']): + pidfile = '--make-pidfile --pidfile %s' % module.params['pidfile'] + + args = '' + if module.params['args'] or module.params['config_dirs'] or \ + module.params['config_files']: + args = '-- ' + if module.params['args']: + args += module.params['args'] + + if module.params['config_dirs']: + for directory in module.params['config_dirs'].split(','): + args += '--config-dir %s ' % directory + + if module.params['config_files']: + for filename in module.params['config_files'].split(','): + args += '--config-file %s ' % filename + + template_vars = module.params + template_vars['pidfile'] = pidfile + template_vars['args'] = args + + env = Environment().from_string(UPSTART_TEMPLATE) + rendered_service = env.render(template_vars) + + if os.path.exists(service_path): + file_hash = md5(open(service_path, 'rb').read()).hexdigest() + template_hash = md5(rendered_service).hexdigest() + if file_hash == template_hash: + module.exit_json(changed=False, result="ok") + + with open(service_path, "w") as fh: + fh.write(rendered_service) + + if not os.path.exists(symlink): + os.symlink('/lib/init/upstart-job', symlink) + + module.exit_json(changed=True, result="created") + except Exception as e: + formatted_lines = traceback.format_exc() + module.fail_json(msg="creating the service failed: %s" % (str(e))) + +# this is magic, see lib/ansible/module_common.py +from ansible.module_utils.basic import * +main() diff --git a/library/xml_configuration.py b/library/xml_configuration.py new file mode 100644 index 0000000..b56e1cc --- /dev/null +++ b/library/xml_configuration.py @@ -0,0 +1,90 @@ +#!/usr/bin/python + +DOCUMENTATION = ''' +--- +module: xml_configuration +version_added: 0.1 +short_description: sets values in xml files +description: + - Given an xml file, an xpath selector, and a value, this will set the value in the file. +options: + file: + description: + - path of the xml file to work with + required: true + xpath: + description: + - xpath of the element to set, must match a single element + required: true + value: + description: + - the value to set + required: true +''' + +EXAMPLES = ''' +# sets the first widget to 1 inside the widgets element +- xml_configuration: xpath=/widgets/widget[1] value=1 +''' + +import xml.etree.ElementTree as ET + +def main(): + conf = AnsibleModule( + argument_spec = dict( + file = dict(required=True), + xpath = dict(required=True), + value = dict(required=True), + ), + supports_check_mode=True + ) + + doc = ET.parse(conf.params['file']) + changed = False + report = '' + elements = doc.findall(conf.params['xpath']) + + # because we create elements if they are not found, we + # must only match 1 or 0 + assert len(elements) < 2 + + val = str(conf.params['value']) + + if len(elements) == 1: + element = elements[0] + current_value = element.text + + # normalise None into empty string for easy value testing + if current_value is None: + current_value = '' + + if current_value != val: + changed = True + old = current_value + element.text = val + report = '%s set to %s, was %s' % (conf.params['xpath'], val, old) + else: + current_element = doc.getroot() + path = conf.params['xpath'].split('/')[1:] + for segment in path: + next = current_element.findall(segment) + if not next: + segment = segment.split('[')[0] # get rid of index matcher + # create the element, set it as current element + current_element = ET.SubElement(current_element, segment) + else: + current_element = next[0] + current_element.text = val + changed = True + report = '%s added and set to %s' % (conf.params['xpath'], val) + + if changed and not conf.check_mode: + doc.write(conf.params['file'], encoding='UTF-8', xml_declaration=True) + + conf.exit_json(changed=changed, result=report) + + + +# import site snippets +from ansible.module_utils.basic import * +main() diff --git a/playbooks/add-bastion-users.yml b/playbooks/add-bastion-users.yml new file mode 100644 index 0000000..086fd91 --- /dev/null +++ b/playbooks/add-bastion-users.yml @@ -0,0 +1,26 @@ +# Use this playbook as a shortcut to add users to bastion hosts +# +# NOTE: Users must still be defined in sitecontroller-envs/bastion-users.yml +# before this playbook is executed. +# +# ex: ursula ../sitecontroller-envs/control-wdc04 playbooks/add-bastion-users.yml +# --ask-vault-pass --ask-su-pass +# -e "@../sitecontroller-envs/bastion-users.yml" +# -e "usernames=sambol,myles" +--- +- name: add bastion users + hosts: bastion + serial: 1 + vars: + users_to_add: {} + + pre_tasks: + - name: extract users from users dict + set_fact: + users_to_add: "{{ users_to_add | combine( {item: users[item]} ) }}" + with_items: "{{ usernames.split(',') }}" + + roles: + - role: users + - role: sshagentmux + - role: bastion diff --git a/playbooks/apt-mirror-debmirror-conversion.yml b/playbooks/apt-mirror-debmirror-conversion.yml new file mode 100644 index 0000000..e8b13d9 --- /dev/null +++ b/playbooks/apt-mirror-debmirror-conversion.yml @@ -0,0 +1,32 @@ +--- +- name: apt-mirror repo move from host/path to key + hosts: mirror + any_errors_fatal: true + environment: "{{ env_vars|default({}) }}" + roles: + - apt-mirror + tasks: + - name: find which mirrors exist on disk + stat: + path: "{{ apt_mirror.path }}/mirror/{{ item.value.host }}/{{ item.value.path }}" + with_dict: "{{ apt_mirror.debmirror.repositories|combine(apt_mirror.debmirror.distros) }}" + register: reg_mirror_exist + + - debug: var=reg_mirror_exist + + - name: move each repo mirror from hostname/path to key + command: "time mv -fn --strip-trailing-slashes {{ apt_mirror.path }}/mirror/{{ item.item.value.host }}/{{ item.item.value.path }} {{ apt_mirror.path }}/mirror/{{ item.item.key }}" + with_items: "{{ reg_mirror_exist.results }}" + when: item.stat.exists|bool + register: reg_move_repo + + - debug: var=reg_move_repo + + - name: create per repo mirror directory, set permissions + file: + dest: "{{ apt_mirror.path }}/mirror/{{ item.key }}" + state: directory + owner: apt-mirror + recurse: true + with_dict: "{{ apt_mirror.debmirror.repositories|combine(apt_mirror.debmirror.distros) }}" + diff --git a/playbooks/central-cutover.yml b/playbooks/central-cutover.yml new file mode 100644 index 0000000..20e132d --- /dev/null +++ b/playbooks/central-cutover.yml @@ -0,0 +1,64 @@ +# Run this playbook against a remote SC to cut over its central SC. +# This playbook should be run AFTER site.yml. +# +# Example: +# ursula ../sitecontroller-envs/remote-sjc01 site.yml +# ursula ../sitecontroller-envs/remote-sjc01 playbooks/central-cutover.yml +--- +- name: central cutover - common + hosts: all:!vyatta-* + tasks: + - name: flush dnsmasq cache + command: killall -HUP dnsmasq + + - name: force stop for various services + service: + name: "{{ item }}" + state: stopped + must_exist: false + with_items: + - dnsmasq + - ntp + - sensu-client + + - name: pause for 30 seconds + pause: + seconds: 30 + + - name: force start for various services + service: + name: "{{ item }}" + state: started + must_exist: false + with_items: + - dnsmasq + - ntp + - sensu-client + + +- name: central cutover - bootstrap + hosts: bootstrap + tasks: + - name: force restart for various services + service: + name: "{{ item }}" + state: restarted + must_exist: false + with_items: + - squid + +- name: central cutover - monitor + hosts: monitor + tasks: + - name: restart sensu-server + shell: kill -9 `cat /var/run/sensu/sensu*.pid | xargs` + + - name: force restart for various services + service: + name: "{{ item }}" + state: started + must_exist: false + with_items: + - sensu-server + - sensu-api + - sensu-client diff --git a/playbooks/delete-bastion-users.yml b/playbooks/delete-bastion-users.yml new file mode 100644 index 0000000..14aab70 --- /dev/null +++ b/playbooks/delete-bastion-users.yml @@ -0,0 +1,54 @@ +# Use this playbook to delete a user from bastion hosts +# +# ex: ursula ../sitecontroller-envs/control-wdc04 playbooks/delete-bastion-users.yml --ask-su-pass -e "username=zsais" +--- +- name: delete bastion users + hosts: bastion + tasks: + + - fail: + msg: "Please specify username with -e" + run_once: true + when: username is undefined + + - name: warn user + run_once: true + pause: + prompt: "Warning: This action is destructive. Type 'dangerous' to continue" + register: warning + + - name: fail if user does not accept warning + run_once: true + fail: + msg: Play cancelled + when: warning.user_input != "dangerous" + + - name: kill processes belonging to users + shell: "pkill -u {{ username }}" + register: pkill_result + failed_when: pkill_result.rc > 2 + + - name: remove yubiauth files + file: + path: "/var/yubiauth/users/{{ username }}" + state: absent + + - name: remove users from yubiauthd database + command: sqlite3 /var/lib/yubiauthd.sqlite + "DELETE FROM identities WHERE username LIKE '{{ username }}';" + + - name: remove users from sshagentmux database + command: sqlite3 /root/authorization_proxy.db + "DELETE FROM authorizations WHERE username LIKE '{{ username }}';" + + - name: remove users from sudoers + lineinfile: + dest: /etc/sudoers + state: absent + regexp: "^{{ username }}" + + - name: delete users from system + user: + name: "{{ username }}" + state: absent + remove: no diff --git a/playbooks/elk-cluster-upgrade.yml b/playbooks/elk-cluster-upgrade.yml new file mode 100644 index 0000000..6c333eb --- /dev/null +++ b/playbooks/elk-cluster-upgrade.yml @@ -0,0 +1,182 @@ +--- +- name: stop ingestion and queries + hosts: logstash:kibana + tasks: + - name: stop logstash + service: + name: logstash + state: stopped + + - name: stop apache + service: + name: apache2 + state: stopped + + - name: stop kibana + service: + name: kibana + state: stopped + must_exist: false + +- name: upgrade logstash and kibana + hosts: logstash:kibana + vars: + kibana: + config: false + pre_tasks: + - name: remove kibana upstart script + file: + path: /etc/init/kibana.conf + state: absent + roles: + - role: kibana + tags: ['kibana'] + - role: logstash + tags: ['logstash'] + +- name: cluster upgrade elasticsearch + hosts: elasticsearch + vars: + elasticsearch: + start_on_config: false + wait_for_listening: false + pre_tasks: + - block: + - name: disable elasticsearch shard allocation + uri: + url: http://localhost:9200/_cluster/settings + method: PUT + body: '{ "persistent": { "cluster.routing.allocation.enable": "none" } }' + body_format: json + return_content: yes + status_code: 200 + register: es_disable_shard_alloc + failed_when: false + run_once: true + + - name: request a synced flush + uri: + url: http://localhost:9200/_flush/synced + method: POST + return_content: yes + status_code: 200 + register: es_sync_flush + failed_when: false + run_once: true + + - name: safely shutdown local elasticsearch node + service: + name: elasticsearch + state: stopped + + - name: wait for elasticsearch to stop listening on localhost:9200 + wait_for: port=9200 state=stopped delay=0 + + - name: remove elasticsearch startup config (replaced during upgrade) + file: + path: /etc/default/elasticsearch + state: absent + + - name: remove elasticsearch plugins (replaced during upgrade) + file: + path: /usr/share/elasticsearch/plugins + state: absent + + - name: remove old sensu-client checks + sensu_check: + name: "{{ item.name }}" + plugin: "{{ item.plugin }}" + state: absent + with_items: + - name: check-es-insert-rate + plugin: check-es-insert-rate.rb + + - name: remove old sensu-client metrics + sensu_metrics_check: + name: "{{ item.name }}" + plugin: "{{ item.plugin }}" + state: absent + with_items: + - name: es-cluster-metrics + plugin: es-cluster-metrics.rb + - name: es-node-graphite + plugin: es-node-graphite.rb + roles: + - role: elasticsearch + tags: ['elasticsearch'] + post_tasks: + - name: overwrite modified elasticsearch service script with package script + copy: + src: /etc/init.d/elasticsearch.dpkg-dist + dest: /etc/init.d/elasticsearch + remote_src: True + failed_when: false # ignore failures, as the src likely does not exist + + - name: remove elasticsearch package service script backup + file: + path: /etc/init.d/elasticsearch.dpkg-dist + state: absent + + - name: start elasticsearch since it likely failed in the role or was disabled + service: + name: elasticsearch + state: started + + - name: wait for elasticsearch to start listening on localhost:9200 + wait_for: port=9200 state=started delay=0 + + - name: start apache + service: name=apache2 state=started + + - name: wait 60 seconds for all shards to recover/cluster status yellow or green, retry 10 times + action: uri + args: + url: http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=60s + method: GET + status_code: 200 + timeout: 120 + register: es_shard_recovery_status + until: es_shard_recovery_status.json.status == "yellow" or es_shard_recovery_status.json.status == "green" + retries: 10 + delay: 10 + + - name: enable elasticsearch shard allocation (and recovery) + uri: + url: http://localhost:9200/_cluster/settings + method: PUT + body: '{ "persistent": { "cluster.routing.allocation.enable" : "all" } }' + body_format: json + return_content: yes + status_code: 200 + timeout: 30 + register: es_disable_shard_alloc + run_once: true + + - name: wait 60 seconds for all shards to recover/cluster status green, retry 60 times + action: uri + args: + url: http://localhost:9200/_cluster/health?wait_for_status=green&timeout=60s + method: GET + status_code: 200 + timeout: 120 + register: es_shard_recovery_status + until: es_shard_recovery_status.json.status == "green" + retries: 10 + delay: 10 + + +- name: start ingestion and queries + hosts: logstash:kibana + tasks: + - name: start logstash + service: + name: logstash + state: started + + - name: start kibana since it likely failed in the role or was disabled + service: + name: kibana + state: started + + - name: wait for kibana to start listening on localhost:5601 + wait_for: port=5601 state=started delay=0 diff --git a/playbooks/elk-curator.yml b/playbooks/elk-curator.yml new file mode 100644 index 0000000..5b2feec --- /dev/null +++ b/playbooks/elk-curator.yml @@ -0,0 +1,41 @@ +# Use this playbook to delete elasticsearch indices older than a specified value +# +# ex: ursula ../sitecontroller-envs/remote-sjc01 playbooks/elk-curator.yml -e "older_than=60" +--- +- name: elk curator + hosts: elasticsearch + run_once: true + tasks: + + - fail: + msg: "Please specify older_than with -e" + when: older_than is undefined + + - name: warn user + run_once: true + pause: + prompt: "Warning: This action is destructive. Type 'dangerous' to continue" + register: warning + + - name: fail if user does not accept warning + fail: + msg: Play cancelled + when: warning.user_input != "dangerous" + + - name: install curator action file + template: + src: templates/delete_indices.yml + dest: /tmp/delete_indices.yml + owner: root + group: root + mode: 0644 + + - name: remove old indices + command: "curator --config /etc/elasticsearch/curator.yml /tmp/delete_indices.yml" + environment: + MASTER_ONLY: False + + - name: remove curator action file + file: + path: /tmp/delete_indices.yml + state: absent diff --git a/playbooks/elk-rolling-restart.yml b/playbooks/elk-rolling-restart.yml new file mode 100644 index 0000000..dcf2faf --- /dev/null +++ b/playbooks/elk-rolling-restart.yml @@ -0,0 +1,139 @@ +# To do a rolling cluster RESTART of Elasticsearch +# ursula playbooks/elk-rolling-restart.yml +# ex: ursula ../sitecontroller-envs/remote-zelda.tor01 playbooks/elk-rolling-restart.yml +# +# To do a rolling cluster REBOOT of ELK hosts +# ursula playbooks/elk-rolling-restart.yml --extra-vars 'reboot=true' +# ex: ursula ../sitecontroller-envs/remote-zelda.tor01 playbooks/elk-rolling-restart.yml --extra-vars 'reboot=true' +--- +- name: rolling restart elasticsearch cluster + hosts: elasticsearch:logstash:kibana + serial: 1 + vars: + reboot: false + tasks: + # stop ingestion and queries + - block: + - name: stop logstash + service: + name: logstash + state: stopped + must_exist: false + + - name: stop apache + service: + name: apache2 + state: stopped + must_exist: false + + - name: stop kibana + service: + name: kibana + state: stopped + must_exist: false + + # block to make requests but also ignore failures if elasticsearch is not running + - block: + - name: disable elasticsearch shard allocation + uri: + url: http://localhost:9200/_cluster/settings + method: PUT + body: '{ "persistent": { "cluster.routing.allocation.enable": "none" } }' + body_format: json + return_content: yes + status_code: 200 + register: es_disable_shard_alloc + failed_when: false + run_once: true + + - name: request a synced flush + uri: + url: http://localhost:9200/_flush/synced + method: POST + return_content: yes + status_code: 200 + register: es_sync_flush + failed_when: false + run_once: true + + - name: safely shutdown local elasticsearch node + service: + name: elasticsearch + state: stopped + + - name: wait for elasticsearch to stop listening on localhost:9200 + wait_for: port=9200 state=stopped delay=0 + + + - block: + - name: reboot + command: /sbin/reboot + + - name: waiting for server to come back + local_action: wait_for host={{ inventory_hostname }} + state=started + sudo: false + when: reboot|default("False")|bool + + + - name: start elasticsearch + service: + name: elasticsearch + state: started + + - name: wait for elasticsearch to start listening on localhost:9200 + wait_for: port=9200 state=started delay=0 + + - name: wait 60 seconds for all shards to recover/cluster status yellow or green, retry 10 times + action: uri + args: + url: http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=60s + method: GET + status_code: 200 + timeout: 120 + register: es_shard_recovery_status + until: es_shard_recovery_status.json.status == "yellow" or es_shard_recovery_status.json.status == "green" + retries: 10 + delay: 10 + + - name: enable elasticsearch shard allocation (and recovery) + uri: + url: http://localhost:9200/_cluster/settings + method: PUT + body: '{ "persistent": { "cluster.routing.allocation.enable" : "all" } }' + body_format: json + return_content: yes + status_code: 200 + timeout: 30 + register: es_disable_shard_alloc + run_once: true + + - name: wait 60 seconds for all shards to recover/cluster status green, retry 60 times + action: uri + args: + url: http://localhost:9200/_cluster/health?wait_for_status=green&timeout=60s + method: GET + status_code: 200 + timeout: 120 + register: es_shard_recovery_status + until: es_shard_recovery_status.json.status == "green" + retries: 10 + delay: 10 + + # start ingestion and queries + - block: + - name: start logstash + service: + name: logstash + state: started + + - name: start kibana since it likely failed in the role or was disabled + service: + name: kibana + state: started + + - name: wait for kibana to start listening on localhost:5601 + wait_for: port=5601 state=started delay=0 + + - name: start apache + service: name=apache2 state=started diff --git a/playbooks/elk-stats.yml b/playbooks/elk-stats.yml new file mode 100644 index 0000000..26f02e7 --- /dev/null +++ b/playbooks/elk-stats.yml @@ -0,0 +1,48 @@ +--- +################### +# To make a report: +# 1) +# - WITH comparison of previous stats, run +# ursula playbooks/elk-stats.yml +# +# or, +# +# - WITHOUT comparison of previous stats, run +# ursula playbooks/elk-stats.yml -e prev_stats=False +# 2) +# Then run python playbooks/stats-to-spreadsheet.py +# This will create the sitecontroller/elk-stats/report.xls spreadsheet +# +# OPTIONAL: days_ago (configure how many days ago to compare to) +# NOTE: elk-stats.yml playbook should be run against all sites, +# then stats-to-spreadsheet.py should be run +################### +- name: gather elk stats + hosts: elk01 + serial: 1 + tasks: + - name: identify current stats file + shell: ls /opt/sitecontroller/elk-stats-output/stats-summary-* | tail -n1 + register: current_stats + + - name: gather current stats file + fetch: + src: "{{ item }}" + dest: "../elk-stats/{{ site_abrv }}-current_stats.json" + fail_on_missing: yes + flat: yes + with_items: "{{ current_stats.stdout }}" + + - block: + - name: identify previous stats file + shell: "ls /opt/sitecontroller/elk-stats-output/stats-summary-* | tail -n{{ days_ago | default(1) | int() * 4 + 1}} | head -n1" + register: previous_stats + + - name: gather previous stats file + fetch: + src: "{{ item }}" + dest: "../elk-stats/{{ site_abrv }}-previous_stats.json" + fail_on_missing: yes + flat: yes + with_items: "{{ previous_stats.stdout }}" + when: "{{ prev_stats | default(True) | bool }}" diff --git a/playbooks/full-remote-restart.yml b/playbooks/full-remote-restart.yml new file mode 100644 index 0000000..08ce20c --- /dev/null +++ b/playbooks/full-remote-restart.yml @@ -0,0 +1,50 @@ +--- +- name: restart monitor + hosts: monitor01 + serial: 1 + tasks: + - name: reboot + command: shutdown -r now + async: 0 + poll: 0 + failed_when: False + + - name: wait for host to come back + local_action: wait_for host={{ inventory_hostname }} state=started + sudo: false + +- name: restart bootstrap + hosts: bootstrap01 + serial: 1 + tasks: + - name: reboot + command: shutdown -r now + async: 0 + poll: 0 + failed_when: False + + - name: wait for host to come back + local_action: wait_for host={{ inventory_hostname }} state=started + sudo: false + +- name: restart elasticsearch + hosts: elasticsearch + serial: 1 + tasks: + - name: reboot + command: shutdown -r now + async: 0 + poll: 0 + failed_when: False + + - name: wait for host to come back + local_action: wait_for host={{ inventory_hostname }} state=started + sudo: false + + - name: wait for elasticsearch to start listening on localhost:9200 + wait_for: port=9200 state=started delay=5 + + - name: wait 60 seconds for all shards to recover/cluster status green, retry 10 times + uri: "url=http://localhost:9200/_cluster/health?wait_for_status=green&timeout=60s method=GET + status_code=200" + retries: 10 diff --git a/playbooks/healthcheck.yml b/playbooks/healthcheck.yml new file mode 100644 index 0000000..c95f672 --- /dev/null +++ b/playbooks/healthcheck.yml @@ -0,0 +1,22 @@ +--- +- name: force gather facts for tagged, or limit runs + hosts: all + gather_facts: force + tags: ['always'] + +- name: network tests + hosts: all + tasks: + - name: check default route exists + shell: ip route | grep default + + - name: check dns resolution + command: host mirror.openstack.bbg + + - name: check network mtu 1410 - no fragementation + command: ping -M do -s 1410 -c 3 -t 5 mirror.openstack.bbg + + - name: check network mtu fragmentation - yes fragmentation + command: ping -M want -s 8992 -c 3 -t 5 mirror.openstack.bbg + environment: "{{ env_vars|default({}) }}" + diff --git a/playbooks/pxe-config.yml b/playbooks/pxe-config.yml new file mode 100644 index 0000000..c41bca1 --- /dev/null +++ b/playbooks/pxe-config.yml @@ -0,0 +1,21 @@ +# This playbook allows the user to install pxe config files. +# It is "safe" and minimal in that it only installs/removes pxe config files. +# +# It is first recommended that you run the playbook with pxe_files +# set to false. This will remove existing config files from the pxe +# server (who knows what someone left there!). +# +# $ ursula ../sitecontroller-envs/remote-$DC playbooks/pxe-config.yml -e 'pxe_files=false' +# +# Make sure your hosts are in pxe.servers and run the playbook with the +# flag enabled. +# +# $ ursula ../sitecontroller-envs/remote-$DC playbooks/pxe-config.yml -e 'pxe_files=true' +--- +- name: pxe config + hosts: pxe + vars: + pxe: + enable_server: False + roles: + - role: pxe diff --git a/playbooks/remove-grafana-datasources.yml b/playbooks/remove-grafana-datasources.yml new file mode 100644 index 0000000..1281599 --- /dev/null +++ b/playbooks/remove-grafana-datasources.yml @@ -0,0 +1,33 @@ +# Use this playbook if you want to delete certain grafana datasources +# You must know the names of the datasources before hand +# ex: ursula ../sitecontroller-envs/control-iad01/ playbooks/remove_grafana_datasources.yml --extra-vars 'datasources_to_remove=["fra02","hkg02","lon02"]' + +--- +- name: remove grafana datasources + hosts: grafana + tasks: + - name: check if grafana datasources exist + uri: + url: http://127.0.0.1:{{ grafana.server.http_port|default("3001") }}/api/datasources + method: GET + user: "{{ grafana.security.admin_user|default("admin") }}" + password: "{{ grafana.security.admin_password|default("admin") }}" + force_basic_auth: yes + return_content: yes + register: grafana_api_datasources + + - name: create list of currently configured datasources + set_fact: + configured_datasources: "{{ configured_datasources | default({}) | combine({item.name: item.id}) }}" + with_items: "{{ grafana_api_datasources.content|from_json }}" + + - name: remove specified grafana datasources + uri: + url: http://127.0.0.1:{{ grafana.server.http_port|default("3001") }}/api/datasources/{{ configured_datasources[item] }} + method: DELETE + user: "{{ grafana.security.admin_user }}" + password: "{{ grafana.security.admin_password }}" + force_basic_auth: yes + return_content: yes + with_items: "{{ datasources_to_remove }}" + when: "{{ item in configured_datasources }}" diff --git a/playbooks/sensu-server-force-restart.yml b/playbooks/sensu-server-force-restart.yml new file mode 100644 index 0000000..e722609 --- /dev/null +++ b/playbooks/sensu-server-force-restart.yml @@ -0,0 +1,24 @@ +--- +- name: sensu server force restart + hosts: sensu + tasks: + - name: kill sensu-server + command: pkill -9 sensu-server + + - name: remove sensu-server pid file + file: + path: /var/run/sensu/sensu-server.pid + state: absent + + - name: start sensu-server + service: + name: sensu-server + state: started + enabled: yes + + - name: start sensu-api + service: + name: sensu-api + state: started + enabled: yes + diff --git a/playbooks/sensu-server-health.yml b/playbooks/sensu-server-health.yml new file mode 100644 index 0000000..9cd5a97 --- /dev/null +++ b/playbooks/sensu-server-health.yml @@ -0,0 +1,33 @@ +# Use this playbook to retrieve info from sensu-server +# +# EXAMPLE +# +# $ ursula ../sitecontroller-envs/remote-$DC playbooks/sensu-server-health.yml +# +# OUTPUT: /tmp/sensu-server-health.txt +--- +- name: sensu-server health + hosts: monitor + tasks: + + - name: query sensu-api + uri: + url: http://localhost:4567/info + user: sensu + password: "{{ secrets.sensu.server.rabbitmq.admin }}" + register: sensu_info + + - local_action: file + args: + path: /tmp/sensu-server-health.txt + state: touch + + - local_action: lineinfile + args: + line: "{{ site_abrv }}:" + dest: /tmp/sensu-server-health.txt + + - local_action: lineinfile + args: + line: " {{ sensu_info.json.transport | to_yaml(default_flow_style=False, indent=2) | indent(width=2) }}\n" + dest: /tmp/sensu-server-health.txt diff --git a/playbooks/stats-to-spreadsheet.py b/playbooks/stats-to-spreadsheet.py new file mode 100644 index 0000000..12d7740 --- /dev/null +++ b/playbooks/stats-to-spreadsheet.py @@ -0,0 +1,114 @@ +#!/usr/bin/python + +# +# Usage: stats-to-excel.py +# options: +# -a = compile for all sites for stats that have been collected prior +# -c = compare previous stats with current stats +# +# -s = target a particular site report +# + +import subprocess +import os +import sys, getopt +import pandas +import json + +site_names=[] +site = "example_site" +outfile = "elk-stats/report.xls" +compare = False +all_sites = False + +def find_files(): + global site_names + if all_sites: + command = "ls elk-stats/*current_stats.json | awk -F\- '{print $3}'" + else: + command = "ls elk-stats/remote-%s-current_stats.json | awk -F\- '{print $3}'" % site + search = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE).stdout.read() + site_names.extend(str(search).split('\n')) + site_names = site_names[:-1] + return + +def collect(): + report_data = {} + for sitename in site_names: + report_data[sitename] = \ + { + "Days Remaining - Current": None, + "Last Host Count - Current": None, + "Last Index Size - Current": None, + "Retention Days": None + } + curr_filename = "elk-stats/remote-" + sitename + "-current_stats.json" + with open(curr_filename, 'r') as curr_file: + curr_file_stats = json.load(curr_file) + report_data[sitename]["Days Remaining - Current"] = \ + float(curr_file_stats["Days Remaining Until Max Capacity Reached"]) + report_data[sitename]["Last Host Count - Current"] = \ + float(curr_file_stats["Last Host Count"]) + report_data[sitename]["Last Index Size - Current"] = \ + float(curr_file_stats["Last Index Size"]) + report_data[sitename]["Retention Days"] = \ + float(curr_file_stats["Retention Days"]) + if compare: + prev_filename = "elk-stats/remote-" + sitename + "-previous_stats.json" + with open(prev_filename, 'r') as prev_file: + prev_file_stats = json.load(prev_file) + report_data[sitename]["Days Remaining - Previous"] = \ + float(prev_file_stats["Days Remaining Until Max Capacity Reached"]) + adjust = check_value(report_data[sitename]["Days Remaining - Previous"]) + report_data[sitename]["Days Remaining - ROC"] = \ + (float(curr_file_stats["Days Remaining Until Max Capacity Reached"])/\ + (float(prev_file_stats["Days Remaining Until Max Capacity Reached"]) + adjust)) + report_data[sitename]["Last Host Count - Previous"] = \ + float(prev_file_stats["Last Host Count"]) + adjust = check_value(report_data[sitename]["Last Host Count - Previous"]) + report_data[sitename]["Last Host Count - ROC"] = \ + (float(curr_file_stats["Last Host Count"])/\ + (float(prev_file_stats["Last Host Count"]) + adjust)) + report_data[sitename]["Last Index Size - Previous"] = \ + float(prev_file_stats["Last Index Size"]) + adjust = check_value(report_data[sitename]["Last Index Size - Previous"]) + report_data[sitename]["Last Index Size - ROC"] = \ + (float(curr_file_stats["Last Index Size"])/\ + (float(prev_file_stats["Last Index Size"]) + adjust)) + return report_data + +def convert(data): + print("\n%s\n") % data + pandas.DataFrame.from_dict(data,orient='index').sort_index(axis=1).to_excel(outfile) + return + +def check_value(value): + if value == 0.0: + return 1.0 + else: + return 0.0 + +def main(argv): + global site, compare, all_sites + try: + opts, args = getopt.getopt(argv,"hs:ca",["site=","compare","all-sites"]) + except getopt.GetoptError: + print '\nCommand line options for "stats-to-spreadsheet.py":\n\t-s SITE\t\tCompiles report for site specified.\n\t-a\t\tCompiles report for all sites.\n\t-c\t\tIncludes previous stats for comparison.\n' + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print '\nCommand line options for "stats-to-spreadsheet.py":\n\t-s SITE\tCompiles report for site specified.\n\t-a\tCompiles report for all sites.\n\t-c\tIncludes previous stats for comparison.\n' + sys.exit() + elif opt in ("-s", "--site"): + site = arg + elif opt in ("-a", "--all-sites"): + all_sites = True + elif opt in ("-c", "--compare"): + compare = True + find_files() + raw_data = collect() + convert(raw_data) + return + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/playbooks/templates/authorized_keys b/playbooks/templates/authorized_keys new file mode 100644 index 0000000..3834da9 --- /dev/null +++ b/playbooks/templates/authorized_keys @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +{% for key in item.value.public_keys|sort %} +{{ key }} +{% endfor %} diff --git a/playbooks/templates/delete_indices.yml b/playbooks/templates/delete_indices.yml new file mode 100644 index 0000000..44c403d --- /dev/null +++ b/playbooks/templates/delete_indices.yml @@ -0,0 +1,17 @@ +--- +actions: + 1: + action: delete_indices + description: Delete indices older than specified number of days. + filters: + - filtertype: pattern + kind: prefix + value: logstash- + exclude: + - filtertype: age + source: name + direction: older + timestring: '%Y.%m.%d' + unit: days + unit_count: {{ older_than }} + exclude: diff --git a/playbooks/ucs-bmc-verify.py b/playbooks/ucs-bmc-verify.py new file mode 100755 index 0000000..5b581d9 --- /dev/null +++ b/playbooks/ucs-bmc-verify.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python2 +import paramiko +import optparse +import time +import yaml +import sys + + +class Ucs_bmc: + def __init__(self, client=None, shell=None): + self.shell = shell + self.client = client + self.fixkeys = [] + + def connect(self, options): + # Create instance of SSHClient object + self.client = paramiko.SSHClient() + + # Automatically add untrusted hosts (make sure okay for security policy in your environment) + self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # initiate SSH connection + self.client.connect(options.ip, username=options.username, password=options.password) + print "--- SSH connection established to %s ---" % options.ip + + + # Use invoke_shell to establish an 'interactive session' + self.shell = self.client.invoke_shell(term='vt102') + self.shell.setblocking(0) + print "--- Interactive SSH session established ---" + + output = self.recv(1024) + # print output + + return self.shell + + + def recv(self, recv_bytes, wait=1): + # Wait for the command to complete + # time.sleep(wait) + time_check = 0 + # print "### recv: start" + while not self.shell.recv_ready(): + time.sleep(1) + time_check+=1 + # print "### recv: loop" + if time_check >= 10: + print 'time out'#TODO: add exception here + return + + output = self.shell.recv(recv_bytes) + # print "### recv: output" + # print output + + return output + + + def run_command(self, command, wait=1, recv_bytes=50000, yml=False): + # Now let's try to send the cimc a command + self.shell.send(command + " \n") + + output = self.recv(recv_bytes, wait=wait) + + # strip off first line of output, our command + fline = output.find('\n') + output = output[fline+1:] + # print "### run_command: ["+output+"]" + # print output + + # if we didn't actually get anything back, try again + attempt = 0 + while output == "": + output = self.recv(recv_bytes, wait=wait) + attempt+=1 + if attempt > 10: + print "ERROR: could not get data for command: "+command + break + + # strip off last line of output, cli prompt + lline = output.rfind('\n') + output = output[:lline] + + if yml: + output = yaml.load(output) + return output + + + def check_settings(self, name, expect, actual): + if actual is None: + print "ERROR: No actual data from CIMC for %s" % name + return + diffkeys = [k for k in expect if expect[k] != actual[k]] + if len(diffkeys) > 0: + print "ERROR: %s does not match expectations" % name + for k in diffkeys: + err = name + '[' + k + ']: ' + expect[k] + ' != ' + actual[k] + self.fixkeys.append(err) + print err + else: + print "OK: %s" % name + + + def disable_paging(self): + '''Disable paging on a Cisco router''' + self.shell.send("terminal length 0\n") + time.sleep(1) + + # Clear the buffer on the screen + output = self.shell.recv(1000) + + return output + + +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option('-i', '--ip',dest="ip", + help="[Mandatory] UCSS IP Address") + parser.add_option('-u', '--username',dest="username", + help="[Mandatory] Account Username for UCS Login") + parser.add_option('-p', '--password',dest="password", + help="[Mandatory] Account Password for UCS Login") + + (options, args) = parser.parse_args() + + if not options.ip: + parser.print_help() + parser.error("Provide UCS IP Address") + if not options.username: + parser.print_help() + parser.error("Provide UCS Username") + if not options.password: + options.password=getpassword("UCS Password") + + ucs = Ucs_bmc() + shell = ucs.connect(options) + + # Turn off paging + # disable_paging(shell) + + # all output in yaml, for easy parsing + ucs.run_command("set cli output yaml") + + # check ipmi + ipmi = ucs.run_command("show ipmi detail", yml=True) + e_ipmi = { + "enabled": True, + "privilege-level": "admin" + } + ucs.check_settings('ipmi', e_ipmi, ipmi) + + # check sol + sol = ucs.run_command("show sol detail", yml=True) + e_sol = { + "enabled": True, + "baud-rate": 115200, + "comport": "com0" + } + ucs.check_settings('sol', e_sol, sol) + + # check bios + ucs.run_command("top") + bios = ucs.run_command("show bios detail", yml=True, wait=2) + e_bios = { + "boot-order": "HDD,PXE", + "secure-boot": "disabled", + "act-boot-mode": "Legacy" + } + ucs.check_settings('bios', e_bios, bios) + + # check advanced bios + ucs.run_command("top") + ucs.run_command("scope bios") + ucs.run_command("scope advanced") + bios_adv = ucs.run_command("show detail", yml=True, wait=2) + e_bios_adv = { + "AllLomPortControl": "Disabled" + } + ucs.check_settings('bios advanced', e_bios_adv, bios_adv) + + # check power-cap-config + ucs.run_command("top") + ucs.run_command("scope chassis") + ucs.run_command("scope power-cap-config") + pcc = ucs.run_command("show detail", yml=True) + e_pcc = { + "run-pow-char-at-boot": False + } + ucs.check_settings('power-cap-config', e_pcc, pcc) + + if len(ucs.fixkeys) > 0: + print "ERROR: Found items to be fixed." + for fix in ucs.fixkeys: + print fix + exit(1) diff --git a/playbooks/ucs-sanity.yml b/playbooks/ucs-sanity.yml new file mode 100644 index 0000000..9f8ce92 --- /dev/null +++ b/playbooks/ucs-sanity.yml @@ -0,0 +1,48 @@ +--- +- name: build list of ipmi hosts + hosts: pxe + become: false + sudo: false + gather_facts: false + tasks: + - name: load all known ipmi devices into ipmi group + add_host: + name: "ipmi-{{ item.name }}" + ip: "{{ item.ipmi }}" + user: admin + pass: admin + groups: ipmi + when: "{{ item.ipmi is defined }}" + with_items: "{{ pxe.servers }}" + delegate_to: localhost + + - debug: + var: groups.ipmi + delegate_to: localhost + +#- name: check ucs ipmi +# hosts: localhost +# connection: local +# gather_facts: false +# environment: +# TERM: vt102 +# tasks: +# - name: verify ucs cimc settings +# command: /home/blueboxadmin/watson-wuwp5C/sitecontroller/playbooks/ucs-bmc-verify.py -i {{ hostvars[item].ip }} -u {{ hostvars[item].user }} -p {{ hostvars[item].pass }} +# register: cimc_settings +# with_items: groups.ipmi +# +# - name: print results from settings check +# debug: +# var: "{{ item }}" +# with_items: cimc_settings.results | failed + +- name: check ucs ipmi + hosts: ipmi + gather_facts: false + environment: + TERM: vt102 + tasks: + - name: verify ucs cimc settings + local_action: command /home/blueboxadmin/watson-wuwp5C/sitecontroller/playbooks/ucs-bmc-verify.py -i {{ hostvars[inventory_hostname].ip }} -u {{ hostvars[inventory_hostname].user }} -p {{ hostvars[inventory_hostname].pass }} + register: cimc_settings diff --git a/playbooks/update-apt-mirror.yml b/playbooks/update-apt-mirror.yml new file mode 100644 index 0000000..c255751 --- /dev/null +++ b/playbooks/update-apt-mirror.yml @@ -0,0 +1,38 @@ +# To update all mirrors, run: +# ursula playbooks/update-apt-mirror.yml +# ex: ursula ../sitecontroller-envs/control-iad01 playbooks/update-apt-mirror.yml +# +# To update individual mirrors, run: +# ursula playbooks/update-apt-mirror.yml -t apt-mirror-split --extra-vars 'repos=,' +# ex: ursula ../sitecontroller-envs/control-iad01 playbooks/update-apt-mirror.yml -t apt-mirror-split --extra-vars 'repos=blueboxcloud_misc' + +--- +- name: update apt-mirror + hosts: mirror + any_errors_fatal: true + environment: "{{ env_vars|default({}) }}" + tasks: + - name: run apt-mirror + command: /usr/bin/apt-mirror + become: true + become_user: apt-mirror + register: apt_mirror + when: repos is not defined + + - debug: msg="{{ apt_mirror.stdout }}" + when: not apt_mirror|skipped + + - name: update targeted apt-mirror(s) + command: /usr/bin/apt-mirror /etc/apt/mirror.list.d/{{ item }} + become: true + become_user: apt-mirror + with_items: + - "{{ (repos|default('')).split(',') |default([]) }}" + register: apt_mirror + tags: apt-mirror-split + when: repos is defined + + - debug: msg="{{ item.stdout }}" + tags: apt-mirror-split + with_items: "{{ apt_mirror.results }}" + when: repos is defined diff --git a/playbooks/update-yum-mirror.yml b/playbooks/update-yum-mirror.yml new file mode 100644 index 0000000..c89fa36 --- /dev/null +++ b/playbooks/update-yum-mirror.yml @@ -0,0 +1,34 @@ +# To update all mirrors, run: +# ursula playbooks/update-yum-mirror.yml +# ex: ursula ../sitecontroller-envs/control-iad01 playbooks/update-yum-mirror.yml +# +# To update individual mirrors, run: +# ursula playbooks/update-yum-mirror.yml --extra-vars 'repos=,' +# ex: ursula ../sitecontroller-envs/control-iad01 playbooks/update-yum-mirror.yml --extra-vars 'repos=blueboxcloud_misc' + +--- +- name: update yum-mirror + hosts: mirror + any_errors_fatal: true + environment: "{{ env_vars|default({}) }}" + tasks: + - name: update targeted yum-mirror(s) + shell: "/bin/bash /etc/yum/repo-manager/{{ item }}.sh" + become: true + become_user: yum-mirror + with_items: + - "{{ (repos|default('')).split(',') |default([]) }}" + register: yum_mirror_out + when: repos is defined + + - name: update all yum-mirror(s) + shell: "/bin/bash /etc/yum/repo-manager/{{ item.key }}.sh" + become: true + become_user: yum-mirror + when: repos is not defined and {{ item.value.enabled }} + with_dict: "{{ yum_mirror.repositories }}" + + - debug: + msg: "{{ item.stdout }}" + when: repos is defined + with_items: "{{ yum_mirror_out.results }}" diff --git a/playbooks/upgrades/elasticsearch.yml b/playbooks/upgrades/elasticsearch.yml new file mode 100644 index 0000000..349e52c --- /dev/null +++ b/playbooks/upgrades/elasticsearch.yml @@ -0,0 +1,27 @@ +--- +- name: upgrade elasticsearch + hosts: elasticsearch + serial: 1 + tasks: + # TODO: Make this not insanely fragile + - name: disable shard allocation routing + uri: "url=http://localhost:9200/_cluster/settings method=PUT + body='{ \"transient\" : { \"cluster.routing.allocation.enable\" : \"none\" } }' + status_code=200" + run_once: true + + - name: install latest elasticsearch + apt: pkg=elasticsearch state=latest + + - name: restart elasticsearch + service: name=elasticsearch state=restarted + + - name: wait until elasticsearch is listening + wait_for: port=9200 + + # TODO: Make this not insanely fragile + - name: enable shard allocation routing + uri: "url=http://localhost:9200/_cluster/settings method=PUT + body='{ \"transient\" : { \"cluster.routing.allocation.enable\" : \"all\" } }' + status_code=200" + run_once: true diff --git a/playbooks/validation.yml b/playbooks/validation.yml new file mode 100644 index 0000000..00c98e3 --- /dev/null +++ b/playbooks/validation.yml @@ -0,0 +1,37 @@ +--- +# Check Site Controller services are up, running, functional, and compliant. +# Pre-Validation: +# Run manual connectivity checks, but not serverspecs +# ex: ursula playbooks/validation.yml -e 'ping_test=True,specs=False' +# Post-Validation: +# Run serverspecs by default, all else optional +# ex: ursula playbooks/validation.yml -e 'ping_test=True' + +- name: validate networking from SC to CC + # Ensure all networking is established and reachable + hosts: all:!vyatta-* + tasks: + - name: validate networking to SC - standard MTU + command: ping -M do -s 1410 -c 3 -t 5 "{{ item.ip }}" + with_items: "{{ etc_hosts }}" + when: ping_test|default("False")|bool + +- name: validate networking from SC to CC + # Ensure all networking is established and reachable + hosts: all:!vyatta-* + serial: 1 + tasks: + - name: central sitecontroller reachable + command: ping -M do -s 1371 -c 3 -t 5 mirror.openstack.bbg + when: ping_test|default("False")|bool + + - name: update apt cache + apt: update_cache=yes cache_valid_time=3600 + +- name: run serverspec + hosts: all:!vyatta-* + tasks: + - name: run serverspec + command: /usr/bin/ruby -S rspec /etc/serverspec/spec/localhost + chdir=/etc/serverspec/ + when: specs|default("True")|bool diff --git a/playbooks/vyatta-add-ssh-keys.sh b/playbooks/vyatta-add-ssh-keys.sh new file mode 100644 index 0000000..9fde907 --- /dev/null +++ b/playbooks/vyatta-add-ssh-keys.sh @@ -0,0 +1,34 @@ +#!/bin/bash +VCMD="/opt/vyatta/sbin/vyatta-cfg-cmd-wrapper" +rc=0 + +$VCMD begin + +if [ "$($VCMD show system login user vyatta authentication public-keys net-dev-deployer)" != "Configuration under specified path is empty" ]; then + $VCMD delete system login user vyatta authentication public-keys net-dev-deployer +fi +if [ "$($VCMD show system login user vyatta authentication public-keys control-iad01)" != "Configuration under specified path is empty" ]; then + $VCMD delete system login user vyatta authentication public-keys control-iad01 +fi +if [ "$($VCMD show system login user vyatta authentication public-keys iad01)" != "Configuration under specified path is empty" ]; then + $VCMD delete system login user vyatta authentication public-keys iad01 +fi +$VCMD set system login user vyatta authentication public-keys old-control-iad01 key AAAAB3NzaC1yc2EAAAADAQABAAACAQDwHi6xBKiWTJSHvmKUrqCshFIngHhsRCiMHqK8/JwMDO/68dtVJgBXcMYHd3/6KbFNmI4a1A+ZbcKrr0BGFtknlCw8kIHpHg9yU9+4AJtu14RUjRwjel0607a4JvRxta1ik5iFBC8AuSSCmS+aITtdkhZ7YFiHDQXQ04Igui4bTQeINCgCNs8k9VqzL+cJWwzBnMUWGZD8NgVTCF+tRC9M7joHIxMeGrPFHn8FIKweWx5qUZ9dLOPs+fzfVbRR2WKCvkQd6APsvSIY1fq7aXICqOViZdZ++b63b/5pCxBeNyi1XwzYVwC7aLp7EILgm69V3wPk4ovtgHPdqSP2Vi8UeSkXS1fs46szUMIVq4WWE+0ihzgpETnRK+lyxUdmgsCsmvUj7B3Ngs4dwdfQEoDjlQ1IVHCXM5KPYDZACJHs1XVJ2b4KykfUoimpn54A5RxrNBHje/tEt2A4smRV9yW3d68iEwltsHBfy44C8ZyR7eeM+aHmHKkkJnRD8af6xPLzbHbQkIG14hl4l1q+AoVZKLoY+GyztbF1r2c61oR0prw75jMVeTqibzPpKL5rnZNFrp9yTjE7U8WcTn1MINzj90jkSXn1ur/um9kF24hdGS7AnBqGRicVDvTbsMbCRY2R9WyPNOedIz9djJCQ4WxZwS+pDM8aIVZ6VPJA58FDXw== +$VCMD set system login user vyatta authentication public-keys old-control-iad01 type ssh-rsa +$VCMD set system login user vyatta authentication public-keys control-iad01 key AAAAB3NzaC1yc2EAAAADAQABAAACAQDd+ygfi+29xsMBFba5Y8c0BZED1Qfxbvp3KLrWgcW4ldHCOx3UvkQDCoGdBea9oJFB/8s7XkDXdwUTuYoO+zBNrSIKU8e49SVQCCiedn1Nw75+rpKtgRviu1h/VFPy9Yit4SSCxiA0rKD9rRqNGQU0U9GZHvTJS9YBTLyxUYKH2stuIiVwJcLQIqdI31sMNU4LiU5HJQ+9WOJvW+hdOom0aHvy1autp9zQ9Xa+/yxw9P0QNDTjx6XW2KyIkQAQ0czqxQEmxZFm5+h5hD1b8dxv5GVlZMlmptT6W1/umo7iloVckGCks/zN5sH67JPglQWPEhX8C41L+WDzJ2uitNp7uw46VyfUH1vvyAN5jAuAUyp8C4uCNi99dJgAHQh501UPAghmw8zu57Qtr8zE/p1/rt9s/5FnVd5m8qZNc0pJH2aIvkhTU3kEAvHdUqeVUPTFkJE7rm1Bcs6hTIv5+6brnw6OPMQjlmrFzOscIQaetfrXzuZ1tR4yO5SAwQGlCVDAFr9+ZknF4jr8GQ4byHLJDPwjpKer3gS88CD5YfjfIaqtbmC7WwGVv+ZpqzlI31HtiNympTcxdukFVqO9Okd9JlexqnxKLI3SZwvGLdOYtHYC+nrJYrYCWdGextIoQqkXSLWzCL4EZTu1uhdWTR1zAMMYNU9GWXbk1ZxjVBZfWw== +$VCMD set system login user vyatta authentication public-keys control-iad01 type ssh-rsa +$VCMD set system login user vyatta authentication public-keys sc-backlog-1 key AAAAB3NzaC1yc2EAAAADAQABAAACAQDMg2tpcKRcBqlKRLQZ3pLEYb3VFZ59vvN/Ykh1bZRrD1ifOcnH0PY1KOIdyR417PhrVU0oL/cxcYpJ/QSg2+cPxLiCDAMlagogbHV4wdv1wqHn90DO1Vy0Gw6dfu+3MnETu2JVkFs48drESeNITvaTNobRXZ81YUIelMk2GSNnAWSINxVkRZNARphIKAhvG3g1idFDFTlHJ/tMJfZaJ2I+utBpfySZgUVwWvCW8Fsmv/+5vHDjNlhiTQRGyfoxOo7I4WRUXk1ez8gZvW69OSplbZlSvs6LzDTbsG31upNktjUVXTazbPE0aPkA4wcxlYOxkvTZIcBUtXDD6n7WJiZnlRTk7etRN6+oaDLg2XiI/3Uvp6HLlNK8WW8LJPjYHiCQf0q9hkxzqIMkMaGdyOlJmgTKg/ZwAuT4Z9wuiylVt8CeUsdhVcwxtkmpCvLxqXNPTfMNMudMgekRQYrepqI2OEi7J2dtD/QzFjqbGlE2ZeoscUq4l+5WOGs7X9pExeDyfEzAc/h3hgMexb5FNUPbY+iodhRuSB96rSDPXqKH8w+E/bjB9RJoPnWhQHX8J2eMrLjO4gqhA2f0/tjayEBhj6PJdxnp8VRtSXPzXWQAfEQVlTTV44eiy+8tKXW0c7XY8ec9bbs9or6PFTNpLpqvpNi3wS54JCCYnZssfd4MRw== +$VCMD set system login user vyatta authentication public-keys sc-backlog-1 type ssh-rsa +$VCMD set system login user vyatta authentication public-keys sc-backlog-2 key AAAAB3NzaC1yc2EAAAADAQABAAACAQDHVTyx6Jn7lNmHSY8j/9x0P7GAcEoUJ/OXUz+WzAk+JDbawkGAkc59aoieIugFZGJ12RoC2tNQXWMJ/7OjrRQIx9UXizwnJB2aVfojNs5NDBeMNpH0gDu12s1PPEezhND6Bh8Lp1hhS4/4cO1zQ5Rc0lpRGpfiS45Qm43VWuTP4PDzWJD260veywtchOC30RNcTDLYVNrUAKe0DjISAe+E+9KbxzqQL0xUtoeMHOTOw983V9sHxwwP941Ftf54KfUR6mgm0G4zqXN9B0j0soQuY51fgsV268NA4ZJ/XXrMjgRc+IiHFPdTAf7VcsIo3InRrcHLKUv1RAZzneeXw0uBhma3RoriHATuo0t24FqPKda908kkX4xAtyiqtSyUHIAulqGCixXhkvlOjKuSJx2tqaTYvGMXyXIfaCb2mXChciMKfCvc0z+32U0TWFvhgyRFHwvpxsZxDr0OVZ4NFJRxmw0BL6rQNJ6wv92HWsozsl4wPSLr4MFNyNHT1dd3zvd/RNXw4fmbKo7KvtLNexHzXoaA8mDs/wNP7R92AauwwiOfIAgJ6gD/32623arEt/jRplFcTxWW6UeSOMsGkJBWMhaLLUAanLzv6FJc2puttrAXpP3FfBIU5ktrIqDC1VwXY+XhEd2/vuYpQOsyzKfQiFkXt0yavk/RfAtjPMguVQ== +$VCMD set system login user vyatta authentication public-keys sc-backlog-2 type ssh-rsa +$VCMD set system login user vyatta authentication public-keys sc-backlog-3 key AAAAB3NzaC1yc2EAAAADAQABAAACAQCsm4MoS6mPp9/y8yt27nyRRGk+8di6ic1E/z0mE5YwFfLciRQ/wxGGpJR0YGr2WK/5Drs0bxNtE4zEQiw+Sz/ynaoZZXmvjl606kaW0HdoeZzygCcfWH7A/AK1c5S5EItbERtDwAJCOXlwHoBk2FRd7J2v0otM4Ml2lw1Uvo2r7TC6WvDvhknbD3QIWB7cJu1tG9JSaHWNkKCGFUYvfxzyqut1wJt+bEd/EWs1YhT2afHi6IRuv951KU3RhI0SJ5nQkTQrIIENyZr9u3OV6D54yLImHaDeHzXw90+MRao4KudWnqI4DxIfcexKwjev7ZYdpPMzXJm1xpwnBkml1hVNvEfcSrK5XSZZTFfL5sbaedtNg0EQEQcpCfEO5V7Re5/f8i7Fvrniim2XzZfRq6aWY//EOlin8LxUSksg5+WSUYYR14YkDeGmu6p8r7SfaFhfKOPRKcfb60nGm+yrGg1QV6gmVlNlFvk/ewWdapw0JU1VNM3EPdMQxTREs8SAEHsFTWVSll0SrfqvQL4jLUhUwVxVitl86RqkWp2NY9VmsVXInT2AwIf70It0unByFV8YEp68DMTLbmIPWHaa5EWCQqCg3NEd95lE2i0vp+QzUh59YxsEpjWqwkIdTRvNhoSVpgoYW7E7IYqzt/svAXDJbnCDx1fiFilRyqr3iUb3Ww== +$VCMD set system login user vyatta authentication public-keys sc-backlog-3 type ssh-rsa +$VCMD set system login user vyatta authentication public-keys sc-backlog-4 key AAAAB3NzaC1yc2EAAAADAQABAAACAQDNotu40xXw/NrIcqyjYz1KqlHlSdl2kFY+JjBn9S0oL+NarKvQbZBJp76aa96CJfkcxMfzVxOBw46UM1m0/OOV7Rzqb4vFeTtLM1xo4lC/Dhm55tDVDa1cil0Ow9WXHSwBju6CgabvKauyQguBsnv5ogR7J9QBSQiQ4p3A8mL5J2dwn2OqlGMzA5+RYR2esi0D9ShjdJ9fmnxzm6tNfjJFk04B8ObFodO0Lq7pLpPqH6Q2kjFPyDRuFmVSFDiWl/E95hrpdMgx1gPGVBi87wWDD/OQnfngSeYQoGyU9BvNbqOUDIdaROs76ug+wtrD+CnI+cYBk/OePEUl9vdeXf3tBv9SWg2yVImHh+52T82uXbWpSwceIQh5aTIEiVE1aCRa6fj77GMALqK2p0p+HoYaF36UOSca2gA/+5lt5eGTl/1hbim7rAItch4u+c84AS1vedizoy7h1gFQaLtEfN2EmU8dpPD10xwAQj+SG+jQLPhzBQv8yJ3kZB8Qbog/q22vKsZyjLqx/jPWcDIE8D71WEuuz/hUEiYLOlGXoxHAK9HFjHRU4Qgi/OtvhJwrmNF8mRS8IrvL7etrBqApVF4b0wqXu/8w/zulP0AO9BsVsGI5Yvf/j4ISbQf193P16T+14RHL6pqGpSIA6JXm8ehX7VY+1N/cnmvvk0GD4iBW6Q== +$VCMD set system login user vyatta authentication public-keys sc-backlog-4 type ssh-rsa +$VCMD set system login user vyatta authentication public-keys sc-backlog-5 key AAAAB3NzaC1yc2EAAAADAQABAAACAQC5d/IgqblmhcV3URHfSK+ROM1Gwu270muhgJ5BvtAtzRiHkV3EHzBAji1qVeoff6XDq1LWkI3vDWpeiWDAEg0dBXZpNLVgj8NXKTadWUJdcCdT9oy1NADk7hTKwO+dD+012HDVroUuU4mgep84z+ookcv9HobHm6UEbJGHR1HmUnJL5HVmumvZK84QtNIfQ77MfK6hxCuVdZ3uoO95nm2t6G0Lrf47LUZQP6YA9nCYxw8KzXZgS+HpOoy0KAv27j10smdCmrJ55jwJ32aM3hbkA261ExpMP7u3XfxjzPhIwgGq80g8FzoSQIB2HchUPTa/Ne5Zy/bBFPNTXFF1HcGzd/RdBmb/6muTzQHA2Pgx7YnvuEQ/PX2VSzdpY35HrzJ/DBO47NTA3APrC5fjgCn8iQdn0+7yvnqtSsYlKSEnhS7s78dIdw7eNUGzXZoOjKAvKx7OWz/8cgQi19r6UYEUV9axQb1DE2oWbYAVrnZIMzHCJkCw/VL8qOLcgWb9+2Jf7IwRO7OAz85MQ+vYtAqqQyLGJe0kowP58o9fcBF4d7DWJEGQx+h8PR2OZTmCmVBukoH9U/gLwXoq4wLy8iIVZCS63gVTEmNbQOHTHfgShBrNt8DUL8jnrqIiY8UT3J8vOzRmSecp4CXua16tGxwNXYapqnDjwrglGFfsdhYfzw== +$VCMD set system login user vyatta authentication public-keys sc-backlog-5 type ssh-rsa +$VCMD commit_with_error +$VCMD save +$VCMD end + +exit $rc diff --git a/playbooks/vyatta-set-authorized-keys.yml b/playbooks/vyatta-set-authorized-keys.yml new file mode 100644 index 0000000..801f2c2 --- /dev/null +++ b/playbooks/vyatta-set-authorized-keys.yml @@ -0,0 +1,19 @@ +--- +- name: assure connectivity to all nodes + hosts: vyatta-sitecontroller + gather_facts: false + tasks: + - action: ping + environment: "{{ env_vars|default({}) }}" + any_errors_fatal: true + max_fail_percentage: 0 + +- name: update authorized_keys + hosts: vyatta-sitecontroller + gather_facts: false + tasks: + - name: run script to set ssh keys + script: vyatta-add-ssh-keys.sh + any_errors_fatal: true + max_fail_percentage: 0 + diff --git a/playbooks/whisper-resize.yml b/playbooks/whisper-resize.yml new file mode 100644 index 0000000..0b30733 --- /dev/null +++ b/playbooks/whisper-resize.yml @@ -0,0 +1,9 @@ +#playbook to change retention of whisper +--- +- name: change whisper retention + hosts: monitor + tasks: + - name: find and change tap retention + shell: find {{ graphite.path.home }}/storage/whisper/stats -name \*.wsp -path "*/ds*/tap*" -print0 | sudo -u graphite xargs -0 -I% -P40 whisper-resize.py --nobackup % 60s:7d + - name: find and change whisper files + shell: find {{ graphite.path.home }}/storage/whisper/stats -name \*.wsp -path "*/ds*/*" -not -path "*/ds*/tap*" -print0 | sudo -u graphite xargs -0 -I% -P40 whisper-resize.py --nobackup % 60s:90d diff --git a/plugins/callbacks/timestamp.py b/plugins/callbacks/timestamp.py new file mode 100644 index 0000000..01ef1b4 --- /dev/null +++ b/plugins/callbacks/timestamp.py @@ -0,0 +1,105 @@ +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import time +from ansible.plugins.callback import CallbackBase + + +def secs_to_str(seconds): + # http://bytes.com/topic/python/answers/635958-handy-short-cut-formatting-elapsed-time-floating-point-seconds + def rediv(ll, b): + return list(divmod(ll[0], b)) + ll[1:] + + numbers = tuple(reduce(rediv, [[seconds * 1000, ], 1000, 60, 60])) + return "%d:%02d:%02d.%03d" % numbers + + +def fill_str(string, fchar="*"): + if len(string) == 0: + width = 79 + else: + string = "%s " % string + width = 79 - len(string) + + if width < 3: + width = 3 + filler = fchar * width + + return "%s%s " % (string, filler) + + +class CallbackModule(CallbackBase): + def __init__(self, *args, **kwargs): + self.count = 0 + self.stats = {} + self.current = None + self.tn = self.t0 = time.time() + super(CallbackModule, self).__init__(*args, **kwargs) + + def v2_playbook_on_task_start(self, task, is_conditional): + self.timestamp() + + if self.current is not None: + # Record the running time of the last executed task + self.stats[self.current] = time.time() - self.stats[self.current] + + # Record the start time of the current task + self.current = task.get_name() + self.stats[self.current] = time.time() + self.count += 1 + + def v2_playbook_on_setup(self): + self.timestamp() + + def v2_playbook_on_play_start(self, play): + self.timestamp() + self._display.display(fill_str("", fchar="=")) + + def v2_playbook_on_stats(self, play): + self.timestamp() + self._display.display(fill_str("", fchar="=")) + self._display.display("Total tasks: %d" % self.count) + self._display.display(fill_str("", fchar="=")) + self._display.display("Slowest 25 Tasks") + self._display.display(fill_str("", fchar="=")) + # Record the timing of the very last task + if self.current is not None: + self.stats[self.current] = time.time() - self.stats[self.current] + + # Sort the tasks by their running time + results = sorted( + self.stats.items(), + key=lambda value: value[1], + reverse=True, + ) + + # Print the timings + for name, elapsed in results[:25]: + name = '{0} '.format(name) + elapsed = ' {0:.02f}s'.format(elapsed) + self._display.display("{0:-<70}{1:->9}".format(name, elapsed)) + + def timestamp(self): + time_current = time.strftime('%A %d %B %Y %H:%M:%S %z') + time_elapsed = secs_to_str(time.time() - self.tn) + time_total_elapsed = secs_to_str(time.time() - self.t0) + self._display.display( + fill_str( + '%s (%s) %s' % (time_current, + time_elapsed, + time_total_elapsed) + ) + ) + self.tn = time.time() diff --git a/plugins/filters/filters.py b/plugins/filters/filters.py new file mode 100644 index 0000000..6cc8d19 --- /dev/null +++ b/plugins/filters/filters.py @@ -0,0 +1,40 @@ +from urlparse import urlsplit + + +def consul_server_ips(hostvars, groups, consul_group='consul'): + consul_ips = set() + for host in groups[consul_group]: + interface = hostvars[host]['consul']['bind_interface'] + ip = hostvars[host]['ansible_' + interface]['ipv4']['address'] + consul_ips.add(ip) + return sorted(list(consul_ips)) + + +def urlparse(url, index): + return urlsplit(url)[index] + + +def domain(url): + return urlsplit(url)[0] + '://' + urlsplit(url)[1] + + +def hostname(url): + return urlsplit(url).hostname + + +def list_contains_url(_list, url): + intersection = [r for r in _list if r in url] + return intersection + + +class FilterModule(object): + ''' ursula infra filters ''' + + def filters(self): + return { + 'consul_server_ips': consul_server_ips, + 'urlparse': urlparse, + 'domain': domain, + 'hostname': hostname, + 'list_contains_url': list_contains_url + } diff --git a/plugins/filters/jenkins_filters.py b/plugins/filters/jenkins_filters.py new file mode 100644 index 0000000..b843df9 --- /dev/null +++ b/plugins/filters/jenkins_filters.py @@ -0,0 +1,54 @@ +import base64 +import struct +from hashlib import sha256 +from Crypto.Cipher import AES + + +MAGIC = '::::MAGIC::::' +PAD_BLOCK_LEN = 16 +KEY_LEN = 16 + + +def jenkins_encrypt(value, master_key, secret_key_base64): + ''' filter to encrypt values that we insert into xml config files ''' + # if we're passed dicts from a shell command, get the stdouts + if type(master_key) == dict: + master_key = master_key['stdout'] + if type(secret_key_base64) == dict: + secret_key_base64 = secret_key_base64['stdout'] + # get the hash of the master key, then throw away half of it + # because woo yay java encryption export restrictions + hashed_master_key = sha256(master_key).digest()[:KEY_LEN] + # get the raw encrypted secret key. It's base64 to begin + # with because we read it from the host through a shell command + secret_key_encrypted = base64.decodestring(secret_key_base64) + # decrypt the secret key + cipher = AES.new(hashed_master_key, AES.MODE_ECB) + secret_key_full = cipher.decrypt(secret_key_encrypted) + # make sure we have a valid secret key by checking the jenkins + # magic token is in there, and hard fail if we don't + assert MAGIC in secret_key_full + # remove the magic token and padding + secret_key = secret_key_full[:-KEY_LEN] + # and truncacte because woo yay java again + secret_key = secret_key[:KEY_LEN] + # now we have all the keys in the right forms, we need to add + # the magic token to the value we want to encrypt and pad it + # for pkcs7 compliance + stored_value = value + MAGIC + pad_len = PAD_BLOCK_LEN - (len(stored_value) % PAD_BLOCK_LEN) + stored_value = stored_value + struct.pack('B', pad_len) * pad_len + # finally, encrypt with the secret key and return a base64 encoded + # version of the encrypted value + cipher = AES.new(secret_key, AES.MODE_ECB) + encrypted_value = cipher.encrypt(stored_value) + return base64.encodestring(encrypted_value).strip() + + +class FilterModule(object): + ''' jenkins utility filters ''' + + def filters(self): + return { + 'jenkins_encrypt': jenkins_encrypt, + } diff --git a/plugins/vars/default_vars.py b/plugins/vars/default_vars.py new file mode 100644 index 0000000..825cb1b --- /dev/null +++ b/plugins/vars/default_vars.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# (c) 2014, Craig Tracey +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +import collections +import os +import yaml + +from ansible.constants import DEFAULTS, get_config, load_config_file + + +def deep_update_dict(d, u): + for k, v in u.iteritems(): + if isinstance(v, collections.Mapping): + r = deep_update_dict(d.get(k, {}), v) + d[k] = r + else: + d[k] = u[k] + return d + + +class VarsModule(object): + + def __init__(self, inventory): + self.inventory = inventory + self.inventory_basedir = inventory.basedir() + + def _get_defaults(self): + p, cfg_path = load_config_file() + defaults_file = get_config(p, DEFAULTS, 'var_defaults_file', + 'ANSIBLE_VAR_DEFAULTS_FILE', None) + print "Using defaults.yml: %s" % defaults_file + if not defaults_file: + return None + + ursula_env = os.environ.get('URSULA_ENV', '') + defaults_path = os.path.join(ursula_env, defaults_file) + if os.path.exists(defaults_path): + with open(defaults_path) as fh: + return yaml.safe_load(fh) + return None + + def run(self, host, vault_password=None): + + default_vars = self._get_defaults() + # This call to the variable_manager will get the variables of + # a given host, with the variable precedence already sorted out. + # This references some "private" like objects and may need to be + # adjusted in the future if/when this all gets overhauled. + # See also https://github.com/ansible/ansible/pull/17067 + inv_vars = self.inventory._variable_manager.get_vars( + loader=self.inventory._loader, host=host) + if default_vars: + return deep_update_dict(default_vars, inv_vars) + return inv_vars diff --git a/requirements-es-stats.txt b/requirements-es-stats.txt new file mode 100644 index 0000000..935a85a --- /dev/null +++ b/requirements-es-stats.txt @@ -0,0 +1,5 @@ +-r requirements.txt +pandas +xlwt +numpy +humanfriendly diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3a01209 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +urllib3>=1.19.1 +six>=1.10.0 +jinja2==2.8.1 +ansible>=2.3,<2.4 +python-heatclient +requests[security] +-e git://github.com/blueboxgroup/ursula-cli.git@master#egg=ursula-cli diff --git a/roles/_blank/defaults/main.yml b/roles/_blank/defaults/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/_blank/defaults/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/_blank/handlers/main.yml b/roles/_blank/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/_blank/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/_blank/meta/main.yml b/roles/_blank/meta/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/_blank/meta/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/_blank/tasks/checks.yml b/roles/_blank/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/_blank/tasks/main.yml b/roles/_blank/tasks/main.yml new file mode 100644 index 0000000..111103e --- /dev/null +++ b/roles/_blank/tasks/main.yml @@ -0,0 +1,14 @@ +--- + + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/_blank/tasks/metrics.yml b/roles/_blank/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/_blank/tasks/serverspec.yml b/roles/_blank/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/_sitecontroller/defaults/main.yml b/roles/_sitecontroller/defaults/main.yml new file mode 100644 index 0000000..eea7840 --- /dev/null +++ b/roles/_sitecontroller/defaults/main.yml @@ -0,0 +1,10 @@ +--- +sitecontroller: + apt: + force_cache_update: false + do_upgrade: false + python: + pypi_mirror: ~ + ruby: + gem_sources: ~ + ubuntu_mirror: ~ diff --git a/roles/_sitecontroller/tasks/checks.yml b/roles/_sitecontroller/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/_sitecontroller/tasks/main.yml b/roles/_sitecontroller/tasks/main.yml new file mode 100644 index 0000000..43df416 --- /dev/null +++ b/roles/_sitecontroller/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: ubuntu apt sources list + template: src=etc/apt/sources.list dest=/etc/apt/sources.list + when: sitecontroller.ubuntu_mirror + +- name: force-update apt cache + apt: update_cache=yes cache_valid_time=1 + when: sitecontroller.apt.force_cache_update|bool + register: result + until: result|succeeded + retries: 5 + +- name: update apt cache + apt: update_cache=yes cache_valid_time=3600 + register: result + until: result|succeeded + retries: 5 + +- name: do apt upgrade + apt: upgrade=yes + when: sitecontroller.apt.do_upgrade + register: result + until: result|succeeded + retries: 5 + +- name: root user pip config directory + file: dest=/root/.pip state=directory + when: sitecontroller.python.pypi_mirror + +- name: pip config file + template: src=python/pip.conf dest=/root/.pip/pip.conf + when: sitecontroller.python.pypi_mirror + +- name: easyinstall config file + template: src=python/pydistutils.cfg dest=/root/.pydistutils.cfg + when: sitecontroller.python.pypi_mirror + +- name: set up gem sources if needed + template: src=ruby/gemrc dest=/root/.gemrc + when: sitecontroller.ruby.gem_sources + +- name: nuke gem sources if unused + file: dest=/root/.gemrc state=absent + when: not sitecontroller.ruby.gem_sources + +- name: nuke user_install gems for root + file: + path: /root/.gem + state: absent + +- name: clean out sensu checks + shell: "rm -f /etc/sensu/conf.d/checks/*" + failed_when: false + when: delete_all_sensu_checks|default('False')|bool diff --git a/roles/_sitecontroller/tasks/metrics.yml b/roles/_sitecontroller/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/_sitecontroller/tasks/serverspec.yml b/roles/_sitecontroller/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/_sitecontroller/templates/etc/apt/sources.list b/roles/_sitecontroller/templates/etc/apt/sources.list new file mode 100644 index 0000000..5682896 --- /dev/null +++ b/roles/_sitecontroller/templates/etc/apt/sources.list @@ -0,0 +1,44 @@ +# {{ ansible_managed }} + +# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to +# newer versions of the distribution. +deb {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }} main restricted +# deb-src {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }} main restricted + +# # Major bug fix updates produced after the final release of the +# # distribution. +deb {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-updates main restricted +# deb-src {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-updates main restricted + +# # N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu +# # team. Also, please note that software in universe WILL NOT receive any +# # review or updates from the Ubuntu security team. +deb {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }} universe +# deb-src {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }} universe +deb {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-updates universe +# deb-src {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-updates universe + +# # N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu +# # team, and may not be under a free licence. Please satisfy yourself as to +# # your rights to use the software. Also, please note that software in +# # multiverse WILL NOT receive any review or updates from the Ubuntu +# # security team. +deb {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }} multiverse +# deb-src {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }} multiverse +deb {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-updates multiverse +# deb-src {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-updates multiverse + +# # N.B. software from this repository may not have been tested as +# # extensively as that contained in the main release, although it includes +# # newer versions of some applications which may provide useful features. +# # Also, please note that software in backports WILL NOT receive any review +# # or updates from the Ubuntu security team. +deb {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-backports main restricted universe multiverse +# deb-src {{ sitecontroller.ubuntu_mirror }}/ {{ ansible_distribution_release }}-backports main restricted universe multiverse + +deb {{ sitecontroller.ubuntu_mirror }} {{ ansible_distribution_release }}-security main restricted +# deb-src {{ sitecontroller.ubuntu_mirror }} {{ ansible_distribution_release }}-security main restricted +deb {{ sitecontroller.ubuntu_mirror }} {{ ansible_distribution_release }}-security universe +# deb-src {{ sitecontroller.ubuntu_mirror }} {{ ansible_distribution_release }}-security universe +deb {{ sitecontroller.ubuntu_mirror }} {{ ansible_distribution_release }}-security multiverse +# deb-src {{ sitecontroller.ubuntu_mirror }} {{ ansible_distribution_release }}-security multiverse diff --git a/roles/_sitecontroller/templates/python/pip.conf b/roles/_sitecontroller/templates/python/pip.conf new file mode 100644 index 0000000..cb01120 --- /dev/null +++ b/roles/_sitecontroller/templates/python/pip.conf @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +[global] +index-url = {{ sitecontroller.python.pypi_mirror|default('') }}/+simple/ +trusted-host = {{ sitecontroller.python.trusted_host|default('')}} diff --git a/roles/_sitecontroller/templates/python/pydistutils.cfg b/roles/_sitecontroller/templates/python/pydistutils.cfg new file mode 100644 index 0000000..875905b --- /dev/null +++ b/roles/_sitecontroller/templates/python/pydistutils.cfg @@ -0,0 +1,4 @@ +# {{ ansible_managed }} + +[easy_install] +index-url = {{ sitecontroller.python.pypi_mirror }}/+simple/ diff --git a/roles/_sitecontroller/templates/ruby/gemrc b/roles/_sitecontroller/templates/ruby/gemrc new file mode 100644 index 0000000..e4bf0ed --- /dev/null +++ b/roles/_sitecontroller/templates/ruby/gemrc @@ -0,0 +1,12 @@ +# {{ ansible_managed }} + +--- +:backtrace: false +:benchmark: false +:bulk_threshold: 1000 +:sources: +{% for gem_source in sitecontroller.ruby.gem_sources %} +- {{ gem_source }} +{% endfor %} +:update_sources: true +:verbose: true diff --git a/roles/apache/defaults/main.yml b/roles/apache/defaults/main.yml new file mode 100644 index 0000000..5abc7f0 --- /dev/null +++ b/roles/apache/defaults/main.yml @@ -0,0 +1,26 @@ +--- +apache: + modules: + - libapache2-mod-wsgi + listen: [] + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/apache2/access.log + fields: + tags: apache_access + - paths: + - /var/log/apache2/error.log + fields: + tags: apache_error + logging: + forwarder: filebeat + ssl: + settings: | + SSLEngine on + SSLProtocol All -SSLv2 -SSLv3 + SSLHonorCipherOrder On + SSLCompression off + # Add six earth month HSTS header for all users... + Header always set Strict-Transport-Security "max-age=15768000" + SSLCipherSuite 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:CAMELLIA256-SHA:AES256-SHA:CAMELLIA128-SHA:AES128-SHA' diff --git a/roles/apache/handlers/main.yml b/roles/apache/handlers/main.yml new file mode 100644 index 0000000..b9369b5 --- /dev/null +++ b/roles/apache/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: reload apache + service: name=apache2 state=reloaded + +- name: restart apache + service: name=apache2 state=restarted + +- name: stop apache + service: name=apache2 state=stopped diff --git a/roles/apache/meta/main.yml b/roles/apache/meta/main.yml new file mode 100644 index 0000000..a61ff5a --- /dev/null +++ b/roles/apache/meta/main.yml @@ -0,0 +1,8 @@ +--- +dependencies: + - role: logging-config + service: apache + logdata: "{{ apache.logs }}" + forward_type: "{{ apache.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/apache/tasks/checks.yml b/roles/apache/tasks/checks.yml new file mode 100644 index 0000000..8d8c82d --- /dev/null +++ b/roles/apache/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install apache process check + sensu_check_dict: name="check-apache-process" check="{{ sensu_checks.apache.check_apache_process }}" + notify: restart sensu-client missing ok diff --git a/roles/apache/tasks/main.yml b/roles/apache/tasks/main.yml new file mode 100644 index 0000000..c0bd980 --- /dev/null +++ b/roles/apache/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: install apache package + apt: + pkg: apache2 + register: result + until: result|succeeded + retries: 5 + +- name: install www-browser + apt: + pkg: lynx-cur + register: result + until: result|succeeded + retries: 5 + +- name: install apache module packages + apt: + pkg: "{{ item }}" + register: result + until: result|succeeded + retries: 5 + with_items: "{{ apache.modules }}" + +- name: enable apache modules + apache2_module: + name: "{{ item }}" + with_items: + - headers + - ssl + - rewrite + notify: reload apache + +- name: install passlib pip module + pip: + name: passlib + register: result + until: result|succeeded + retries: 5 + +- name: disable default vhost + file: dest=/etc/apache2/sites-enabled/{{ item }} + state=absent + with_items: + - 000-default + - 000-default.conf + - default + - default.conf + notify: stop apache + +- name: configure apache listen ports + template: src=etc/apache2/ports.conf + dest=/etc/apache2/ports.conf + notify: reload apache + +- name: include ports.conf + lineinfile: dest=/etc/apache2/apache2.conf + line="Include ports.conf" + regexp="^Include\sports\.conf$" + state=present + notify: reload apache + +- meta: flush_handlers + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/apache/tasks/metrics.yml b/roles/apache/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/apache/tasks/serverspec.yml b/roles/apache/tasks/serverspec.yml new file mode 100644 index 0000000..b2dce55 --- /dev/null +++ b/roles/apache/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for apache role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/apache/templates/etc/apache2/ports.conf b/roles/apache/templates/etc/apache2/ports.conf new file mode 100644 index 0000000..5bf55eb --- /dev/null +++ b/roles/apache/templates/etc/apache2/ports.conf @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +{% for port in apache.listen %} +Listen {{ port }} +{% endfor %} diff --git a/roles/apache/templates/serverspec/apache_spec.rb b/roles/apache/templates/serverspec/apache_spec.rb new file mode 100644 index 0000000..991240d --- /dev/null +++ b/roles/apache/templates/serverspec/apache_spec.rb @@ -0,0 +1,31 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end + +describe package('lynx-cur') do + it { should be_installed } +end + +['000-default', '000-default.conf', 'default', 'default.conf'].each do |file| + describe file("/etc/apache/sites-enabled/#{file}") do + it { should_not exist } + end +end + +describe file('/etc/apache2/ports.conf') do + it { should be_file } +end + +{% for module in apache.modules %} +describe package('{{ module }}') do + it { should be_installed } +end +{% endfor %} diff --git a/roles/apt-mirror/defaults/main.yml b/roles/apt-mirror/defaults/main.yml new file mode 100644 index 0000000..c9720dd --- /dev/null +++ b/roles/apt-mirror/defaults/main.yml @@ -0,0 +1,41 @@ +--- +apt_mirror: + apache: + http_redirect: False + servername: mirror01.local + serveraliases: + - mirror01 + port: 80 + ip: '*' + ssl: + enabled: False + port: 443 + ip: '*' + name: sitecontroller + cert: ~ + key: ~ + intermediate: ~ + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + - port: 443 + protocol: tcp + src: 0.0.0.0/0 + path: /opt/apt-mirror + htpasswd_location: /opt/apt-mirror/etc + debmirror: + repositories: {} + distros: {} + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/apache2/apt_mirror-access.log + fields: + tags: mirror,apache_access + - paths: + - /var/log/apache2/apt_mirror-error.log + fields: + tags: mirror,apache_error + logging: + forwarder: filebeat diff --git a/roles/apt-mirror/meta/main.yml b/roles/apt-mirror/meta/main.yml new file mode 100644 index 0000000..8e5ec46 --- /dev/null +++ b/roles/apt-mirror/meta/main.yml @@ -0,0 +1,17 @@ +--- +dependencies: + - role: bbg-ssl + name: "{{ apt_mirror.apache.ssl.name }}" + ssl_cert: "{{ apt_mirror.apache.ssl.cert }}" + ssl_key: "{{ apt_mirror.apache.ssl.key }}" + ssl_intermediate: "{{ apt_mirror.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: apt_mirror.apache.ssl.enabled + tags: ['bbg-ssl'] + - role: apache + - role: logging-config + service: apt_mirror + logdata: "{{ apt_mirror.logs }}" + forward_type: "{{ apt_mirror.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/apt-mirror/tasks/checks.yml b/roles/apt-mirror/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/apt-mirror/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/apt-mirror/tasks/debmirror.yml b/roles/apt-mirror/tasks/debmirror.yml new file mode 100644 index 0000000..4a3a579 --- /dev/null +++ b/roles/apt-mirror/tasks/debmirror.yml @@ -0,0 +1,79 @@ +--- +- name: install debmirror + apt: + name: debmirror + state: present + +- name: create keys path + file: + dest: "{{ apt_mirror.path }}/keys" + state: directory + mode: 0755 + +- name: download repo keys + get_url: + url: "{{ item.value.key_url }}" + dest: "{{ apt_mirror.path }}/keys/{{ item.key }}.key" + mode: 0644 + when: proxy_env is not defined and item.value.key_url is defined + with_dict: "{{ apt_mirror.debmirror.repositories }}" + +- name: download repo keys via proxy + get_url: + url: "{{ item.value.key_url }}" + dest: "{{ apt_mirror.path }}/keys/{{ item.key }}.key" + mode: 0644 + environment: proxy_env + when: proxy_env is defined and item.value.key_url is defined + with_dict: "{{ apt_mirror.debmirror.repositories }}" + +- name: add mirror keys to local trust store + apt_key: + url: "{{ item.value.key_url }}" + when: item.value.key_url is defined + with_dict: "{{ apt_mirror.debmirror.repositories }}" + +- name: create per repo mirror directory + file: + dest: "{{ apt_mirror.path }}/mirror/{{ item.key }}" + state: directory + owner: apt-mirror + with_dict: "{{ apt_mirror.debmirror.repositories|combine(apt_mirror.debmirror.distros) }}" + +- name: create apt mirror htpasswd location + file: + name: "{{ apt_mirror.htpasswd_location }}" + state: directory + owner: apt-mirror + +- name: create per repo .htpasswd + htpasswd: + name: "{{ item.value.username }}" + password: "{{ item.value.password }}" + path: "{{ apt_mirror.path }}/mirror/{{ item.key }}/.htpasswd" + with_dict: "{{ apt_mirror.debmirror.repositories|combine(apt_mirror.debmirror.distros) }}" + when: item.value.username is defined and item.value.username is defined + +- name: create debmirror cron directory + file: + dest: "{{ apt_mirror.path }}/cron" + state: directory + +- name: debmirror cron script + template: + src: debmirror.sh + dest: "{{ apt_mirror.path }}/cron/debmirror.sh" + mode: 0755 + +- name: debmirror crontab + template: + src: etc/cron.d/debmirror + dest: /etc/cron.d/debmirror + +- name: remove antiquated cron jobs + file: + path: "{{ item }}" + state: absent + with_items: + - /etc/cron.d/partial-apt + - /etc/cron.d/apt-mirror diff --git a/roles/apt-mirror/tasks/main.yml b/roles/apt-mirror/tasks/main.yml new file mode 100644 index 0000000..47ca80a --- /dev/null +++ b/roles/apt-mirror/tasks/main.yml @@ -0,0 +1,67 @@ +--- +- name: create mirror user + user: + name: apt-mirror + comment: apt-mirror + shell: /bin/false + system: yes + home: /nonexistent + +- name: create mirror directories + file: + dest: "{{ item }}" + state: directory + recurse: true + owner: apt-mirror + group: apt-mirror + with_items: + - "{{ apt_mirror.path }}" + - "{{ apt_mirror.path }}/mirror" + +- include: debmirror.yml + when: (apt_mirror.debmirror.repositories|combine(apt_mirror.debmirror.distros)) | length > 0 + +- name: add main apache vhost + template: + src: etc/apache2/sites-available/apt_mirror + dest: /etc/apache2/sites-available/apt_mirror.conf + notify: + - restart apache + tags: apt-mirror-apache-config + +- name: enable repo vhost + apache2_site: + state: enabled + name: apt_mirror + notify: + - restart apache + +- meta: flush_handlers + +- name: ensure apache is running + service: + name: apache2 + state: started + enabled: yes + +- name: allow apt-mirror traffic + ufw: + rule: allow + to_port: "{{ item.port }}" + src: "{{ item.src }}" + proto: "{{ item.protocol }}" + with_items: "{{ apt_mirror.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/apt-mirror/tasks/metrics.yml b/roles/apt-mirror/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/apt-mirror/tasks/serverspec.yml b/roles/apt-mirror/tasks/serverspec.yml new file mode 100644 index 0000000..16c0b05 --- /dev/null +++ b/roles/apt-mirror/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for apt-mirror role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/apt-mirror/templates/debmirror.sh b/roles/apt-mirror/templates/debmirror.sh new file mode 100644 index 0000000..200ccf1 --- /dev/null +++ b/roles/apt-mirror/templates/debmirror.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +{% for key, value in (apt_mirror.debmirror.repositories|combine(apt_mirror.debmirror.distros)).iteritems() %} +{% if value.enabled is not defined or value.enabled|bool %} +debmirror -v --keyring /etc/apt/trusted.gpg \ + --method={{ value.method }} {% if value.method != "rsync" %} --rsync-extra=none {% endif %} \ + {% if value.exclude_regex is defined %} --exclude='{{ value.exclude_regex }}' {% endif %} \ + {% if value.include_regex is defined %} --include='{{ value.include_regex }}' {% endif %} \ + {% if value.upstream_username is defined %} --user='{{ value.upstream_username }}' {% endif %} \ + {% if value.upstream_password is defined %} --passwd='{{ value.upstream_password }}' {% endif %} \ + {% if value.ignore_missing_release is defined and value.ignore_missing_release|bool %} --ignore-missing-release {% endif %} \ + --arch {{ value.arch }} --no-source --getcontents \ + --host {{ value.host }} --root {{ value.path }} \ + --dist {{ value.distributions }} \ + --section {{ value.sections }} \ + {{ apt_mirror.path }}/mirror/{{ key }} + +{% if value.download_files is defined %} +{% for file in value.download_files %} +curl -o {{ apt_mirror.path }}/mirror/{{ key }}/{{ file }} {{ value.method }}://{{ value.host }}/{{ value.path }}/{{ file }} +{% endfor %} +{% endif %} + +{% endif %} +{% endfor %} diff --git a/roles/apt-mirror/templates/etc/apache2/htaccess b/roles/apt-mirror/templates/etc/apache2/htaccess new file mode 100644 index 0000000..187da0d --- /dev/null +++ b/roles/apt-mirror/templates/etc/apache2/htaccess @@ -0,0 +1,6 @@ +# {{ ansible_managed }} + +AuthType Basic +AuthName "Authentication Required" +AuthUserFile "{{ apt_mirror.path }}/mirror/{{ item.value.path|default(item.value.url|urlparse(1)) }}/.htpasswd" +Require valid-user diff --git a/roles/apt-mirror/templates/etc/apache2/sites-available/apt_mirror b/roles/apt-mirror/templates/etc/apache2/sites-available/apt_mirror new file mode 100644 index 0000000..50fedaa --- /dev/null +++ b/roles/apt-mirror/templates/etc/apache2/sites-available/apt_mirror @@ -0,0 +1,77 @@ +# {{ ansible_managed }} + +{% macro virtualhost() %} + ServerAdmin openstack@bluebox.net + ServerName {{ apt_mirror.apache.servername }} + ServerAlias {{ apt_mirror.apache.serveraliases|join(" ") }} + DocumentRoot {{ apt_mirror.path }}/mirror + ErrorLog ${APACHE_LOG_DIR}/apt_mirror-error.log + CustomLog ${APACHE_LOG_DIR}/apt_mirror-access.log combined + FileETag MTime Size + Header set Cache-Control public + Alias /keys {{ apt_mirror.path }}/keys + + + Options +Indexes +SymLinksIfOwnerMatch + AllowOverride None + Require all granted + + + + Options Indexes + AllowOverride None + Require all granted + + +{% for key,value in (apt_mirror.debmirror.repositories|combine(apt_mirror.debmirror.distros)).iteritems()|sort %} +{% set mirror_path = apt_mirror.path ~"/mirror/"~ key %} + Alias /{{ key }}/{{ value.path.rstrip("/") }} {{ mirror_path }} +{% if value.aliases is defined %} +{% for value in value.aliases %} + Alias {{ value.rstrip("/") }} {{ mirror_path }} +{% endfor %} +{% endif %} + Alias /{{ key }} {{ mirror_path }} + Alias /{{ value.host }}/{{ value.path.rstrip("/") }} {{ mirror_path }} + +{% if value.username is defined and value.password is defined %} + Options +Indexes +SymLinksIfOwnerMatch + AuthType Basic + AuthName "Restricted Content" + AuthUserFile {{ apt_mirror.htpasswd_location }}/{{ key }}.htpasswd + Require valid-user +{% else %} + Options +Indexes +SymLinksIfOwnerMatch + Require all granted +{% endif %} + +{% endfor %} +{% endmacro %} + +{% if apt_mirror.apache.ssl.enabled|bool and apt_mirror.apache.http_redirect|bool %} + + ServerName {{ apt_mirror.apache.servername }} + ServerAlias {{ apt_mirror.apache.serveraliases|join(" ") }} + RewriteEngine on + RewriteCond %{HTTPS} !=on + RewriteRule ^(.*)$ https://%{HTTP_HOST}:{{ apt_mirror.apache.ssl.port }}$1 [R=301,L] + +{% else %} + +{{ virtualhost() }} + +{% endif %} + +{% if apt_mirror.apache.ssl.enabled|bool %} + + {{ apache.ssl.settings }} + SSLCertificateFile /etc/ssl/certs/{{ apt_mirror.apache.ssl.name|default('sitecontroller') }}.crt + SSLCertificateKeyFile /etc/ssl/private/{{ apt_mirror.apache.ssl.name|default('sitecontroller') }}.key +{% if bbg_ssl.intermediate or apt_mirror.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ apt_mirror.apache.ssl.name|default('sitecontroller') }}-intermediate.crt +{% endif %} +{% else %} + +{% endif %} +{{ virtualhost() }} + diff --git a/roles/apt-mirror/templates/etc/cron.d/debmirror b/roles/apt-mirror/templates/etc/cron.d/debmirror new file mode 100644 index 0000000..4ba0636 --- /dev/null +++ b/roles/apt-mirror/templates/etc/cron.d/debmirror @@ -0,0 +1,5 @@ +# {{ ansible_managed }} +# +# Regular cron jobs for all packages that use debmirror syncing +# +0 6 * * * apt-mirror /opt/apt-mirror/cron/debmirror.sh diff --git a/roles/apt-mirror/templates/serverspec/apt_mirror_spec.rb b/roles/apt-mirror/templates/serverspec/apt_mirror_spec.rb new file mode 100644 index 0000000..a2465d4 --- /dev/null +++ b/roles/apt-mirror/templates/serverspec/apt_mirror_spec.rb @@ -0,0 +1,35 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('debmirror') do + it { should be_installed } +end + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end + +describe file('{{ apt_mirror.path }}/keys') do + it { should be_directory } +end + +describe file('/etc/apache2/sites-available/apt_mirror.conf') do + it { should be_file } +end + +describe file('/etc/apache2/sites-enabled/apt_mirror.conf') do + it { should be_symlink } +end + +describe port('{{ apt_mirror.apache.port }}') do + it { should be_listening } +end + +describe iptables do + it { should have_rule('-p tcp -m tcp --dport {{ apt_mirror.apache.port }} -j ACCEPT') } +end diff --git a/roles/apt-repos/defaults/main.yml b/roles/apt-repos/defaults/main.yml new file mode 100644 index 0000000..5f06c6c --- /dev/null +++ b/roles/apt-repos/defaults/main.yml @@ -0,0 +1,5 @@ +--- +apt_repos: {} + # add repos specific to your project in your -envs variables +purge_repos: [] + # - repo: 'deb https://cb1a8630e683a470ec2db3ff8aac8152d95d6535b9b42b98:@packagecloud.io/blueboxcloud/misc/ubuntu/ trusty main' diff --git a/roles/apt-repos/tasks/checks.yml b/roles/apt-repos/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/apt-repos/tasks/main.yml b/roles/apt-repos/tasks/main.yml new file mode 100644 index 0000000..7e09035 --- /dev/null +++ b/roles/apt-repos/tasks/main.yml @@ -0,0 +1,42 @@ +--- +- name: remove deprecated apt repos + apt_repository: repo="{{ item.repo }}" state=absent update_cache=yes + with_items: "{{ purge_repos }}" + when: purge_repos is defined + +- name: add any dependent repository keys from url + apt_key: url="{{ item.key_url }}" + with_items: "{{ repos }}" + register: result + until: result|succeeded + retries: 5 + when: repos is defined and item.key_url is defined + +# things like keyrings may come as packages vs. keys +- name: add any dependent repository key packages + apt: pkg="{{ item.key_package }}" + with_items: "{{ repos }}" + register: result + until: result|succeeded + retries: 5 + when: repos is defined and item.key_package is defined + +- name: add any dependent repositories + apt_repository: repo="{{ item.repo }}" update_cache=yes mode=0644 + with_items: "{{ repos }}" + register: result + until: result|succeeded + retries: 5 + when: repos is defined + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/apt-repos/tasks/metrics.yml b/roles/apt-repos/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/apt-repos/tasks/serverspec.yml b/roles/apt-repos/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/bastion/defaults/main.yml b/roles/bastion/defaults/main.yml new file mode 100644 index 0000000..f4f1c3b --- /dev/null +++ b/roles/bastion/defaults/main.yml @@ -0,0 +1,30 @@ +--- +yama_utils: + enabled: false + dependencies: + - libdbi-perl + - libdbd-mysql-perl + - python-daemon + +yubiauthd: + enabled: false + package: https://github.com/blueboxgroup/yubiauthd/releases/download/1.0.1/yubiauthd_1.0.1_all.deb + hosts: [] + skipped_users: + - root + dependencies: + - sqlite3 + - libev-perl + - libanyevent-perl + - libdbd-sqlite3-perl + - libdigest-hmac-perl + - libdigest-sha-perl + - libauth-yubikey-decrypter-perl + - libio-socket-ip-perl + log_path: /var/log/yubiauthd.log + auth_socket: /var/run/yubiauthd.sock + sync_socket_secret: nopenopenopenopenopenopenope + sync_port: 32 + firewall: + friendly_networks: + - 0.0.0.0/0 diff --git a/roles/bastion/files/utils/ssh-ip-check b/roles/bastion/files/utils/ssh-ip-check new file mode 100755 index 0000000..46eec1e --- /dev/null +++ b/roles/bastion/files/utils/ssh-ip-check @@ -0,0 +1,382 @@ +#!/usr/bin/perl -T +# +# This script is called by SSH when users log into the box. Its job +# is to check the connecting IP of the connecting user, and if we've never +# seen that user connect over the given IP, force a second factor for +# authentication. At the present time, that second factor is yubikeys-- +# and this requires this script to check in with yubiauthd using the 'yubiauth' +# command-line client. +# +# Obviously, since a failure in this script means nobody can log in over +# SSH, it's important this thing react well in a partial failure +# scenario, eh. The most likely partial failure is for yubiauthd to be +# not running. In this case, the usual most-appropriate response is to +# fall back to single-factor auth with a warning. +# +# For this command to be used, the sshd_config should be updated with a line +# like the following: +# +# ForceCommand /usr/bin/ssh-ip-check + +# Before anything else, we want to prevent any funny business around signals being +# used to background or terminate this process unexpectedly. +$SIG{TERM} = 'IGNORE'; +$SIG{INT} = sub { kill SIGKILL, $$; }; +$SIG{HUP} = 'IGNORE'; +$SIG{TSTP} = 'IGNORE'; + +# Signal handler for alarm conditions... +local $SIG{ALRM} = sub { + do_timeout(); +}; + +use strict; +use Sys::Syslog; + +use constant { + IPDIR => '.ipauth', + AUTHTIMEOUT => 60, + AUTHATTEMPTS => 3, + REAUTHTIME => 43200, # 12 hours + YUBIAUTH => '/usr/bin/yubiauth', + YADPID => '/var/run/yubiauthd.pid', + CONFIGFILE => '/etc/ssh-ip-check.conf' +}; + +# Global variables +my $user; +my $ip; + +# Set of users which are exempt from second factor auth +my $skipped = {}; + +sub parse_config($) { + my $filename = shift; + + open(CONFIG, '<', $filename) + or return undef; + + while (my $line = ) { + chomp($line); + + # Trim comments and trailing whitespace + $line =~ s/\s*(?:#.*)?$//; + + # Skip blank lines + next unless $line; + + # Trim leading whitespace + $line =~ s/^\s*//; + + # Add account to skipped hash + $skipped->{$line} = 1; + } + close(CONFIG); + + # Successfully parsed config file + return 1; +} + +sub check_environment() { + # Start our syslog... + openlog("ssh-ip-check", "nofatal", 'authpriv'); + + # Set the PATH to something safe so we can exec() in taint mode + $ENV{'PATH'} = "/usr/bin:/bin"; +} + +# This cleans up IPv6 addresses. We only care about the most significant /64 in this case +# so we don't have to re-auth every time an IP changes on a local subnet (eg. due to +# privacy extensions being enabled on the client machine), but only if the client +# changes subnet. Also, I know there's a Net::IP perl module which could do this, +# but since we don't want to include a whole lot of package dependencies-- and that one's +# a big one-- and we only really need this single function, we're coding it ourselves here. +# +# We're also not checking that the IP address we're getting is a valid IPv6 address. Because +# we're populating this from an environment variable, and because we can implicitly trust +# that environment variable not to be populated with malicious data, this should be OK. +sub ipv6_subnet($) { + my $ip = shift; + my @result; + + my @quads = split(/:/,$ip,-1); + + # Start with an empty array + for (my $i = 0; $i<8; $i++) { + push @result, "0"; + } + + # Fill the first half of the quad (up to the double-colon) + my $i = 0; + while (($i < $#quads) && ($quads[$i] ne '')) { + # sprintf and hex here eliminate leading zeros. + $result[$i] = sprintf("%x",hex($quads[$i])); + $i++; + } + + # Fill the last half of the quad (up to the double-colon) + my $i = $#quads; + my $offset = 7 - $i; + while (($i > 0) && ($quads[$i] ne '')) { + # sprintf and hex here eliminate leading zeros. + $result[$i + $offset] = sprintf("%x",hex($quads[$i])); + $i--; + } + + return $result[0] . "-" . $result[1] . "-" . $result[2] . "-" . $result[3]; +} + +# Checks to see whether this user and IP have authed recently. Returns true if so. +sub ip_is_authed($$) { + my $ip = shift; + my $user = shift; + + # If it's an IPv6 address, get the /64 subnet + $ip = ipv6_subnet($ip) if ($ip =~ /:/); + + # Sanitize home directory... + my $home; + if ($ENV{'HOME'} =~ /^([\w\/]+)$/) { + $home = $1; + } else { + syslog + syslog('warning', $user . ": Invalid home directory. Cannot auth."); + print "Invalid home directory. Cannot auth.\n"; + closelog(); + exit 2; + } + + if (! -d $home . "/" . IPDIR) { + mkdir $home . "/" . IPDIR; + return undef; + } + + if ( -f $home . "/" . IPDIR . "/$ip") { + # Make sure we're not too old. + my $age = (stat($home . "/" . IPDIR . "/$ip"))[9]; + return 1 if ($age > time() - REAUTHTIME); + } + return undef; +} + +# This returns true if our account is on our "skipped" list for 2 factor auth. +sub skipped_account($) { + my $user = shift; + return defined($skipped->{$user}); +} + +# Checks to see whether yubiauthd is running. Returns true +# if so, undef if not. +sub yubiauthd_alive() { + return undef unless (-f YADPID); + open (YPID,"<" . YADPID) || return undef; + my $pid = ; + close(YPID); + chomp($pid); + if ($pid =~ /^(\d+)$/) { + $pid = $1; + } else { + return undef; + } + my $sigs = kill 0, $pid; + # Root can signal, others can't, but they can see the pid is alive with the right error string + return 1 if ((0 != $sigs) || ("Operation not permitted" eq $!)); + return undef; +} + +# This actually authenticates an unknown IP / user +sub auth_ip_user($$) { + my $ip = shift; + my $user = shift; + my $code; + + # Don't wait for that code indefinitely + alarm AUTHTIMEOUT; + + print "Press yubikey for 3 seconds: "; + $code = ; + chomp($code); + + # Turn off that alarm + alarm 0; + + return undef if ("" eq $code); + + # Make taint check happy, and make sure we've got something that looks like a yubicode + if ($code =~ m/^([cbdefghijklnrtuv]{44})$/) { + $code = $1; + } else { + syslog('info', "$user : $ip : Yubikey authentication failure"); + return undef; + } + + my $sret = system(YUBIAUTH, $code); # Need to check return value of system() call, as SIGINT gets sent to child + my $retval = $? >> 8; + + if ((0 == $retval) && (0 == $sret)) { + syslog('info', "$user : $ip : Successful IP authentication"); + # Cache the successful auth... + + # If it's an IPv6 address, get the /64 subnet + $ip = ipv6_subnet($ip) if ($ip =~ /:/); + + # Sanitize home directory + my $home; + if ($ENV{'HOME'} =~ /^([\w\/]+)$/) { + $home = $1; + } + + # This is the equivalent of a "touch" command without having to spawn a new process. + open(AUTH,">" . $home . "/" . IPDIR . "/$ip") || syslog('warning', "$user : $ip : Unable to cache yubi auth: $!"); + close(AUTH); + return 1; + } else { + # Log the failure + syslog('info', "$user : $ip : Yubikey authentication failure"); + } + + # If we are here, we didn't auth successfully. + return undef; +} + +# This is what we do if there's a timeout authenticating... +sub do_timeout { + syslog('warning', "$user: $ip : Timed out while waiting for yubi code"); + closelog(); + exit 3; +} + +# This just extracts the client IP address from the SSH_CONNECTION environment variable. +sub get_ip { + my $remote_addr; + my $remote_port; + my $server_addr; + my $server_port; + + if (defined $ENV{'SSH_CONNECTION'} and $ENV{'SSH_CONNECTION'} =~ + m/^([:\.\da-f]{2,39})\s+(\d{2,5})\s+([:\.\da-f]{2,39})\s+(\d{2,5})$/i) { + $remote_addr = $1; + $remote_port = $2; + $server_addr = $3; + $server_port = $4; + return $remote_addr; + } + return undef; +} + +# Prints the motd without having to spawn an external process. +sub print_motd { + open (MOTD, ") { + print $line; + } + close(MOTD); +} + +# This takes care of "doing the right thing" after a successful authentication +sub post_auth { + my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'}; + + # Allow an additional access control filter be specified as a + # ssh-ip-check argument + if (@ARGV) { + $cmd = shift @ARGV; + # Make taint checking happy. + if ($cmd =~ /^(.+)$/) { + $cmd = $1; + } + closelog(); + exec($cmd, @ARGV); + die("exec($cmd) failed: $!"); + } + + my $shell; + # Make taint checking happy... + if ($ENV{'SHELL'} =~ /^([\w\/]+)$/) { + $shell = $1; + } + + # Do we even have a command to run? + unless (defined($cmd)) { + print_motd(); + closelog(); + exec($shell,"-l"); + die("exec($shell) failed: $!"); + } + + # Make taint checking happy. Blindly accept any commands the user typed. + if ($cmd =~ /^(.+)$/) { + $cmd = $1; + } + + closelog(); + exec($shell,"-c",$cmd); + die("exec($shell -c $cmd) failed: $!"); +} + +sub main { + check_environment(); + $user = `/usr/bin/whoami`; + chomp($user); + + # Make taint mode happy + if ($user =~ /^(.+)$/) { + $user = $1; + } + + # See if yubiauthd is working. Fall back to single-factor auth with warning + # if not, eh. + if (!yubiauthd_alive) { + print "\n!!!! WARNING! yubiauthd not running! Two-factor auth unavailable! !!!!\n\n"; + syslog('warning', $user . ": Skipping 2-factor auth check because yubiauthd is not running"); + closelog(); + post_auth(); + } + + unless ($ip = get_ip()) { + syslog('warning', $user . ": Unable to parse connecting IP"); + closelog(); + exit 2; + } + + # Are we already authed? + if (ip_is_authed($ip,$user)) { + syslog('info', "$user : $ip : Cached IP authentication found"); + closelog(); + post_auth(); + } + + # Parse config and add users to skipped + my $config_file = CONFIGFILE; + my $config = parse_config($config_file); + + # Display warning if there is an error in parsing config, but continue + if (!$config) { + print "\nUnable to parse config file. Skipped users may not work properly.\n\n"; + syslog('warning', $user . ": Unable to parse config file. Skipped users may not work properly."); + closelog(); + } + # Config successfully loaded, check if second factor can be skipped + else { + # Skipping auth for this user? + if (skipped_account($user)) { + syslog('info', "$user : $ip : IP authentication skipped"); + closelog(); + post_auth(); + } + } + + # If we are here, we need to auth... + for (my $i = 0; $i < AUTHATTEMPTS ; $i++) { + post_auth() if (auth_ip_user($ip,$user)); + } + + # If we are here, we didn't successfully auth. + syslog('warning', "$user : $ip : Too many failed yubikey authentication attempts"); + closelog(); + exit 1; +} + + +main(); +# vim: ai si ts=4 sw=4 et diff --git a/roles/bastion/handlers/main.yml b/roles/bastion/handlers/main.yml new file mode 100644 index 0000000..68a9e92 --- /dev/null +++ b/roles/bastion/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart yubiauthd + service: + name: yubiauthd + state: restarted diff --git a/roles/bastion/meta/main.yml b/roles/bastion/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/bastion/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/bastion/tasks/checks.yml b/roles/bastion/tasks/checks.yml new file mode 100644 index 0000000..29d9791 --- /dev/null +++ b/roles/bastion/tasks/checks.yml @@ -0,0 +1,6 @@ +--- +- name: install yubiauthd process check + sensu_check_dict: name="check-yubiauthd-process" + check="{{ sensu_checks.yubiauthd.check_yubiauthd_process }}" + when: yubiauthd.enabled|bool + notify: restart sensu-client missing ok diff --git a/roles/bastion/tasks/main.yml b/roles/bastion/tasks/main.yml new file mode 100644 index 0000000..728c2a4 --- /dev/null +++ b/roles/bastion/tasks/main.yml @@ -0,0 +1,24 @@ +--- +# needed when not running playbooks/add-bastion-users.yml +- name: set users_to_add when not defined + set_fact: + users_to_add: "{{ users }}" + when: users_to_add is not defined + +- include: yubiauthd.yml + when: yubiauthd.enabled|default("False")|bool + +- include: utils.yml + when: yama_utils.enabled|default("False")|bool + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/bastion/tasks/metrics.yml b/roles/bastion/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/bastion/tasks/serverspec.yml b/roles/bastion/tasks/serverspec.yml new file mode 100644 index 0000000..b88728d --- /dev/null +++ b/roles/bastion/tasks/serverspec.yml @@ -0,0 +1,14 @@ +--- +- name: install yubiauthd serverspec tests + template: + src: serverspec/yubiauthd_spec.rb + dest: /etc/serverspec/spec/localhost/yubiauthd_spec.rb + mode: 0755 + when: yubiauthd.enabled|bool + +- name: install yama-utils serverspec tests + template: + src: serverspec/yama-utils_spec.rb + dest: /etc/serverspec/spec/localhost/yama-utils_spec.rb + mode: 0755 + when: yama_utils.enabled|bool diff --git a/roles/bastion/tasks/utils.yml b/roles/bastion/tasks/utils.yml new file mode 100644 index 0000000..6417ab0 --- /dev/null +++ b/roles/bastion/tasks/utils.yml @@ -0,0 +1,19 @@ +--- +- name: install dependencies + apt: + pkg: "{{ item }}" + with_items: "{{ yama_utils.dependencies }}" + +- name: install yama-utils + copy: + src: utils/ssh-ip-check + dest: /usr/bin/ssh-ip-check + mode: 0755 + +- name: configure ssh-ip-check + template: + src: etc/ssh-ip-check.conf + dest: /etc/ssh-ip-check.conf + owner: root + group: root + mode: 0644 diff --git a/roles/bastion/tasks/yubiauthd.yml b/roles/bastion/tasks/yubiauthd.yml new file mode 100644 index 0000000..fffc0db --- /dev/null +++ b/roles/bastion/tasks/yubiauthd.yml @@ -0,0 +1,60 @@ +--- +- name: install dependencies + apt: + pkg: "{{ item }}" + with_items: "{{ yubiauthd.dependencies }}" + +- name: install yubiauthd + apt: + deb: "{{ yubiauthd.package }}" + +- name: configure yubiauthd + template: + src: etc/yubiauthd.conf + dest: /etc/yubiauthd.conf + owner: root + group: root + mode: 0640 + +- name: allow yubiauthd sync traffic + ufw: + rule: allow + to_port: "{{ yubiauthd.sync_port }}" + src: "{{ item }}" + proto: any + with_items: "{{ yubiauthd.firewall.friendly_networks }}" + tags: + - firewall + +- name: configure yubiauthd service + template: + src: etc/init/yubiauthd.conf + dest: /etc/init/yubiauthd.conf + mode: 0644 + notify: restart yubiauthd + +- meta: flush_handlers + +- name: start yubiauthd service + service: + name: yubiauthd + state: started + enabled: yes + +# The yubiauthd database gets created upon starting the service +- name: ensure yubiauthd database has correct perms + file: + path: /var/lib/yubiauthd.sqlite + state: touch + owner: root + group: root + mode: 0600 + +- name: add or update users in yubiauthd database + command: sqlite3 /var/lib/yubiauthd.sqlite + "INSERT OR REPLACE INTO identities(public_id, serial_number, username, aes_key, uid) + VALUES('{{ item.value.yubikey.public_id }}','{{ item.value.yubikey.serial_number }}', + '{{ item.key }}','{{ item.value.yubikey.aes_key }}','{{ item.value.yubikey.private_id }}');" + when: item.value.yubikey is defined + with_dict: "{{ users_to_add }}" + tags: users diff --git a/roles/bastion/templates/etc/init/yubiauthd.conf b/roles/bastion/templates/etc/init/yubiauthd.conf new file mode 100644 index 0000000..3cdfc86 --- /dev/null +++ b/roles/bastion/templates/etc/init/yubiauthd.conf @@ -0,0 +1,15 @@ +# {{ ansible_managed }} + +description "yubiauthd" +author "Michael Sambol" + +start on runlevel [2345] +stop on runlevel [!2345] + +expect daemon +respawn + +setuid root +setgid root + +exec /usr/sbin/yubiauthd diff --git a/roles/bastion/templates/etc/ssh-ip-check.conf b/roles/bastion/templates/etc/ssh-ip-check.conf new file mode 100644 index 0000000..d722014 --- /dev/null +++ b/roles/bastion/templates/etc/ssh-ip-check.conf @@ -0,0 +1,6 @@ +# {{ ansible_managed }} + +# List of accounts that skip 2 factor auth +{% for user in yubiauthd.skipped_users %} +{{ user }} +{% endfor %} diff --git a/roles/bastion/templates/etc/yubiauthd.conf b/roles/bastion/templates/etc/yubiauthd.conf new file mode 100644 index 0000000..17d7b09 --- /dev/null +++ b/roles/bastion/templates/etc/yubiauthd.conf @@ -0,0 +1,22 @@ +# {{ ansible_managed }} + +# YubiAuthd configuration + +log_file {{ yubiauthd.log_path }} + +sqlite_store /var/lib/yubiauthd.sqlite + +# Alternatively you can use a directory based store +#file_store /var/yubiauth/ + +auth_socket {{ yubiauthd.auth_socket }} + +{% if groups['bastion'][1] is defined %} +# Provides sync mechanism between yama pairs +sync_socket 0.0.0.0:{{ yubiauthd.sync_port }} +{% for host in yubiauthd.hosts %} +{% if hostvars[inventory_hostname][public_interface]['ipv4']['address'] != host.ip %} + peer {{ host.ip }}:{{ yubiauthd.sync_port }} {{ yubiauthd.sync_socket_secret }} +{% endif %} +{% endfor %} +{% endif %} diff --git a/roles/bastion/templates/serverspec/yama-utils_spec.rb b/roles/bastion/templates/serverspec/yama-utils_spec.rb new file mode 100644 index 0000000..a589fbb --- /dev/null +++ b/roles/bastion/templates/serverspec/yama-utils_spec.rb @@ -0,0 +1,13 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +{% for pkg in yama_utils.dependencies %} +describe package('{{ pkg }}') do + it { should be_installed } +end +{% endfor %} + +describe package('yama-utils') do + it { should be_installed } +end diff --git a/roles/bastion/templates/serverspec/yubiauthd_spec.rb b/roles/bastion/templates/serverspec/yubiauthd_spec.rb new file mode 100644 index 0000000..9b7a3f1 --- /dev/null +++ b/roles/bastion/templates/serverspec/yubiauthd_spec.rb @@ -0,0 +1,44 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +{% for pkg in yubiauthd.dependencies %} +describe package('{{ pkg }}') do + it { should be_installed } +end +{% endfor %} + +describe package('yubiauthd') do + it { should be_installed } +end + +describe service('yubiauthd') do + it { should be_enabled } +end + +describe file('/etc/yubiauthd.conf') do + it { should be_mode 640 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_file } +end + +describe file('/etc/init/yubiauthd.conf') do + it { should be_mode 644 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_file } +end + +describe file('/var/lib/yubiauthd.sqlite') do + it { should be_mode 600 } + it { should be_owned_by 'root' } + it { should be_grouped_into 'root' } + it { should be_file } +end + +{% if groups['bastion'][1] is defined %} +describe port('{{ yubiauthd.sync_port }}') do + it { should be_listening } +end +{% endif %} diff --git a/roles/bbg-ssl/defaults/main.yml b/roles/bbg-ssl/defaults/main.yml new file mode 100644 index 0000000..7119bad --- /dev/null +++ b/roles/bbg-ssl/defaults/main.yml @@ -0,0 +1,6 @@ +--- +bbg_ssl: + intermediate: ~ + cert: ~ + key: ~ + ca_cert: ~ diff --git a/roles/bbg-ssl/handlers/main.yml b/roles/bbg-ssl/handlers/main.yml new file mode 100644 index 0000000..f42d634 --- /dev/null +++ b/roles/bbg-ssl/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: update ca certificates + shell: update-ca-certificates diff --git a/roles/bbg-ssl/tasks/checks.yml b/roles/bbg-ssl/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/bbg-ssl/tasks/main.yml b/roles/bbg-ssl/tasks/main.yml new file mode 100644 index 0000000..29d257f --- /dev/null +++ b/roles/bbg-ssl/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: install ssl cert package + apt: pkg=ssl-cert + +- name: create ssl-key group + group: name=ssl-key state=present + +- name: install ssl key + template: src=etc/ssl/private/sitecontroller.key + dest=/etc/ssl/private/{{ name|default('sitecontroller') }}.key + owner=root group=ssl-key mode=0640 + when: ssl_key or bbg_ssl.key + +- name: install ssl cert + template: src=etc/ssl/certs/sitecontroller.crt + dest=/etc/ssl/certs/{{ name|default('sitecontroller') }}.crt + when: ssl_cert or bbg_ssl.cert + +- name: install ssl intermediate cert + template: src=etc/ssl/certs/intermediate.crt + dest=/etc/ssl/certs/{{ name|default('sitecontroller') }}-intermediate.crt + when: ssl_intermediate or bbg_ssl.intermediate + +- name: install ssl ca cert + template: src=usr/local/share/ca-certificates/ca_cert.crt + dest=/usr/local/share/ca-certificates/{{ name|default('sitecontroller-ca_cert') }}.crt + when: ssl_ca_cert or bbg_ssl.ca_cert + notify: update ca certificates + +- name: remove antiquated ca certs + file: + path: "{{ item }}" + state: absent + with_items: + - /usr/local/share/ca-certificates/bbg/bbg-root-ca.crt + - /usr/local/share/ca-certificates/logging-forward.crt + notify: update ca certificates + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/bbg-ssl/tasks/metrics.yml b/roles/bbg-ssl/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/bbg-ssl/tasks/serverspec.yml b/roles/bbg-ssl/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/bbg-ssl/templates/etc/ssl/certs/intermediate.crt b/roles/bbg-ssl/templates/etc/ssl/certs/intermediate.crt new file mode 100644 index 0000000..7129825 --- /dev/null +++ b/roles/bbg-ssl/templates/etc/ssl/certs/intermediate.crt @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{% if ssl_intermediate %}{{ ssl_intermediate }}{% else %}{{ bbg_ssl.intermediate }}{% endif %} diff --git a/roles/bbg-ssl/templates/etc/ssl/certs/sitecontroller.crt b/roles/bbg-ssl/templates/etc/ssl/certs/sitecontroller.crt new file mode 100644 index 0000000..c81bdba --- /dev/null +++ b/roles/bbg-ssl/templates/etc/ssl/certs/sitecontroller.crt @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{% if ssl_cert %}{{ ssl_cert }}{% else %}{{ bbg_ssl.cert }}{% endif %} diff --git a/roles/bbg-ssl/templates/etc/ssl/private/sitecontroller.key b/roles/bbg-ssl/templates/etc/ssl/private/sitecontroller.key new file mode 100644 index 0000000..db7146e --- /dev/null +++ b/roles/bbg-ssl/templates/etc/ssl/private/sitecontroller.key @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{% if ssl_key %}{{ ssl_key }}{% else %}{{ bbg_ssl.key }}{% endif %} diff --git a/roles/bbg-ssl/templates/usr/local/share/ca-certificates/ca_cert.crt b/roles/bbg-ssl/templates/usr/local/share/ca-certificates/ca_cert.crt new file mode 100644 index 0000000..4bfef21 --- /dev/null +++ b/roles/bbg-ssl/templates/usr/local/share/ca-certificates/ca_cert.crt @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{% if ssl_ca_cert %}{{ ssl_ca_cert }}{% else %}{{ bbg_ssl.ca_cert }}{% endif %} diff --git a/roles/collectd/defaults/main.yml b/roles/collectd/defaults/main.yml new file mode 100644 index 0000000..da096b2 --- /dev/null +++ b/roles/collectd/defaults/main.yml @@ -0,0 +1,21 @@ +--- +collectd: + client_name: "{{ ansible_hostname }}" + plugin_conf_dir: /etc/collectd/plugins + interval: 10 + timeout: 2 + threads: 5 + graphite_prefix: "stats." + plugins: + amqp: + enabled: True + verbose: False + host: 172.16.0.15 + port: 5672 + vhost: /graphite + user: graphite + pass: graphite + exchange: metrics + logfile: + file: /var/log/collectd.log + level: info diff --git a/roles/collectd/handlers/main.yml b/roles/collectd/handlers/main.yml new file mode 100644 index 0000000..a083b00 --- /dev/null +++ b/roles/collectd/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart collectd + service: name=collectd state=restarted diff --git a/roles/collectd/meta/main.yml b/roles/collectd/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/collectd/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/collectd/tasks/checks.yml b/roles/collectd/tasks/checks.yml new file mode 100644 index 0000000..fa490fe --- /dev/null +++ b/roles/collectd/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install sensu check + sensu_check_dict: name="check-collectd-process" check="{{ sensu_checks.collectd.check_collectd_process }}" + notify: restart sensu-client missing ok diff --git a/roles/collectd/tasks/main.yml b/roles/collectd/tasks/main.yml new file mode 100644 index 0000000..8beae43 --- /dev/null +++ b/roles/collectd/tasks/main.yml @@ -0,0 +1,44 @@ +--- +- name: create collectd user + user: name=collectd comment=collectd shell=/bin/false + system=yes home=/nonexistent + +- name: install collectd packages + apt: name={{ item }} state=present update_cache=yes + with_items: + - collectd + - collectd-utils + +- name: create config dirs + file: dest={{ collectd.plugin_conf_dir }} state=directory owner=root mode=0755 + +- name: configure collectd + template: src=etc/collectd/collectd.conf dest=/etc/collectd/collectd.conf + notify: + - restart collectd + +- name: configure collectd plugins + template: src=etc/collectd/plugins/{{ item }}.conf dest={{ collectd.plugin_conf_dir }}/{{ item }}.conf + with_items: + - amqp + - system + - logfile + notify: + - restart collectd + +- meta: flush_handlers + +- name: ensure collectd is running + service: name=collectd state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/collectd/tasks/metrics.yml b/roles/collectd/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/collectd/tasks/serverspec.yml b/roles/collectd/tasks/serverspec.yml new file mode 100644 index 0000000..b29929f --- /dev/null +++ b/roles/collectd/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for collectd role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/collectd/templates/etc/collectd/collectd.conf b/roles/collectd/templates/etc/collectd/collectd.conf new file mode 100644 index 0000000..580863e --- /dev/null +++ b/roles/collectd/templates/etc/collectd/collectd.conf @@ -0,0 +1,18 @@ +# {{ ansible_managed }} + +Hostname {{ collectd.client_name|default(ansible_hostname) }} +FQDNLookup false + +BaseDir "/var/lib/collectd" +PluginDir "/usr/lib/collectd" + +{% if collectd.typesdb is defined %} +TypesDB {% for path in collectd.typesdb %} "{{ path }} " {% endfor %} +{% else %} +TypesDB "/usr/share/collectd/types.db" "/etc/collectd/my_types.db" +{% endif %} + +Interval {{ collectd.interval }} +Timeout {{ collectd.timeout }} +ReadThreads {{ collectd.threads }} +Include "{{ collectd.plugin_conf_dir }}/*.conf" diff --git a/roles/collectd/templates/etc/collectd/plugins/amqp.conf b/roles/collectd/templates/etc/collectd/plugins/amqp.conf new file mode 100644 index 0000000..f984234 --- /dev/null +++ b/roles/collectd/templates/etc/collectd/plugins/amqp.conf @@ -0,0 +1,18 @@ +# {{ ansible_managed }} + +LoadPlugin amqp + + + + Host "{{ collectd.plugins.amqp.host }}" + Port "{{ collectd.plugins.amqp.port }}" + VHost "{{ collectd.plugins.amqp.vhost }}" + User "{{ collectd.plugins.amqp.user }}" + Password "{{ collectd.plugins.amqp.pass }}" + Exchange "{{ collectd.plugins.amqp.exchange }}" + Format "Graphite" + Persistent true + GraphitePrefix "{{ collectd.graphite_prefix }}" + GraphiteEscapeChar "-" + + diff --git a/roles/collectd/templates/etc/collectd/plugins/logfile.conf b/roles/collectd/templates/etc/collectd/plugins/logfile.conf new file mode 100644 index 0000000..a6c700e --- /dev/null +++ b/roles/collectd/templates/etc/collectd/plugins/logfile.conf @@ -0,0 +1,9 @@ +# {{ ansible_managed }} + +LoadPlugin "logfile" + + + LogLevel "{{ collectd.plugins.logfile.level }}" + File "{{ collectd.plugins.logfile.file }}" + Timestamp true + diff --git a/roles/collectd/templates/etc/collectd/plugins/system.conf b/roles/collectd/templates/etc/collectd/plugins/system.conf new file mode 100644 index 0000000..d6fd4b5 --- /dev/null +++ b/roles/collectd/templates/etc/collectd/plugins/system.conf @@ -0,0 +1,49 @@ +# {{ ansible_managed }} + +LoadPlugin aggregation +LoadPlugin cpu +LoadPlugin disk +LoadPlugin interface +LoadPlugin load +LoadPlugin memory +LoadPlugin network +LoadPlugin processes +LoadPlugin swap +LoadPlugin vmem + +############################################################################## +# Plugin configuration # +#----------------------------------------------------------------------------# +# In this section configuration stubs for each plugin are provided. A desc- # +# ription of those options is available in the collectd.conf(5) manual page. # +############################################################################## + + + + Plugin "cpu" + Type "cpu" + GroupBy "Host" + GroupBy "TypeInstance" + CalculateSum true + CalculateAverage true + + + + + Disk "/^[hs]d[a-f][0-9]?$/" + IgnoreSelected false + + + + Interface "{{ private_device_interface }}" + IgnoreSelected false + + + + ReportByDevice false + ReportBytes true + + + + Verbose false + diff --git a/roles/collectd/templates/serverspec/collectd_spec.rb b/roles/collectd/templates/serverspec/collectd_spec.rb new file mode 100644 index 0000000..0624303 --- /dev/null +++ b/roles/collectd/templates/serverspec/collectd_spec.rb @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('collectd') do + it { should be_installed } +end + +describe service('collectd') do + it { should be_enabled } +end diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml new file mode 100644 index 0000000..877f039 --- /dev/null +++ b/roles/common/defaults/main.yml @@ -0,0 +1,167 @@ +--- +common: + locale: en_US.UTF-8 + packages: + - ack-grep + - acl + - apt-transport-https + - build-essential + - cdpr + - curl + - dnsutils + - dstat + - ethtool + - git + - htop + - ifenslave + - iotop + - iperf + - libffi-dev + - libssl-dev + - logrotate + - ltrace + - lvm2 + - mtr + - netcat + - ntp + - pstack + - pv + - python2.7 + - python2.7-dev + - python-httplib2 + - python-jinja2 + - python-mysqldb + - python-pip + - python-software-properties + - smem + - socat + - sshpass + - sysstat + - tcpdump + - tmux + - tree + - unzip + - vim + - whois + + python: + packages: +# - name: pip +# state: latest +# - name: setuptools +# state: latest + - name: pytz + version: 2016.6.1 + - name: pyparsing + version: 2.1.10 + - name: six + version: 1.10.0 + - name: pyopenssl + version: 16.2.0 + - name: cryptography + - name: idna + - name: certifi + - name: pyasn1 + - name: ndg-httpsclient + - name: virtualenv + - name: "urllib3>=1.19" + - name: "requests[security]" + skip_serverspec: True + ntpd: + enabled: True + servers: + - 0.ubuntu.pool.ntp.org + - 1.ubuntu.pool.ntp.org + - 2.ubuntu.pool.ntp.org + - 3.ubuntu.pool.ntp.org + - ntp.ubuntu.com + peers: [] + clients: + - ip: 172.16.1.115 + netmask: 255.255.255.0 + + firewall: + forwarding: False + friendly_networks: [] + + mdns: + enabled: False + + ssh: + allow_from: + - "{{ ansible_default_ipv4.network }}/{{ ansible_default_ipv4.netmask }}" + disable_dns: True + max_sessions: 25 + + client_alive: + interval: 120 + countmax: 15 + + private_keys: [] + ssh_host_rsa_key: + public: ~ + private: ~ + ghe_authorized_keys: + enabled: False + api_url: ~ # ex: https://api.github.com + api_user: ~ + api_pass: ~ + + sysdig: + enabled: True + + hwraid: + enabled: True + add_clients: + - tw-cli + - megacli + remove_clients: [] + + logs: + # FILEBEAT + - paths: + - /var/log/syslog + document_type: syslog + fields: + tags: syslog + - paths: + - /var/log/apt/history.log + fields: + tags: apt + - paths: + - /var/log/auth.log + document_type: syslog + fields: + tags: auth,audit,archive + # LOGSTASH FORWARDER + #- paths: + # - /var/log/syslog + # fields: + # type: syslog + # tags: syslog + + logging: + forwarder: filebeat # or 'logstash-forwarder' + + sudoers: + - name: blueboxadmin + args: + - "ALL=NOPASSWD: /usr/sbin/tcpdump" + + shell_customization: + enabled: true + git_prompt: true + tmux_fix_ssh: true + + data_dirs: + - path: /data + owner: root + group: root + +bastion: + backdoor_user: sitecontroller + ssh_port: 22 + force_commands: [] + #- /usr/bin/ttyspy + #- /usr/bin/ssh-ip-check + #- /usr/bin/ssh-mosh-filter diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml new file mode 100644 index 0000000..d8fffe2 --- /dev/null +++ b/roles/common/handlers/main.yml @@ -0,0 +1,21 @@ +--- +- name: update timezone + command: dpkg-reconfigure --frontend noninteractive tzdata + +- name: update locales + command: dpkg-reconfigure --frontend noninteractive locales + +- name: reload-sshd + service: name=ssh state=reloaded + +- name: flush time + shell: > + service ntp stop + ntpdate -s {{ common.ntpd.servers[0]|quote }} + service ntp start + +- name: restart ntpd service + service: name=ntp state=restarted + +- name: update apt index + apt: update_cache=yes diff --git a/roles/common/meta/main.yml b/roles/common/meta/main.yml new file mode 100644 index 0000000..51cd0b9 --- /dev/null +++ b/roles/common/meta/main.yml @@ -0,0 +1,25 @@ +--- +dependencies: + - role: runtime/ruby + tags: ruby + - role: runtime/python + tags: python + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.hwraid.repo }} trusty main' + key_url: '{{ apt_repos.hwraid.key_url }}' + purge_repos: + - repo: 'deb {{ apt_repos.hwraid.repo }} precise main' + - repo: 'deb http://apt-mirror.openstack.blueboxgrid.com/sysdig/stable/deb/ stable-$(ARCH)/' + when: common.hwraid.enabled|bool + - role: serverspec + tags: ['serverspec'] + when: serverspec.enabled|default("True")|bool + - role: logging-config + service: common + logdata: "{{ common.logs }}" + forward_type: "{{ common.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check + - role: users + tags: users diff --git a/roles/common/tasks/checks.yml b/roles/common/tasks/checks.yml new file mode 100644 index 0000000..effbf75 --- /dev/null +++ b/roles/common/tasks/checks.yml @@ -0,0 +1,58 @@ +--- +- name: install ssh process check + sensu_check_dict: name="check-sshd-process" check="{{ sensu_checks.common.check_sshd_process }}" + notify: restart sensu-client missing ok + +- name: install ntp process check + sensu_check_dict: name="check-ntp-process" check="{{ sensu_checks.common.check_ntp_process }}" + notify: restart sensu-client missing ok + when: common.ntpd.enabled|bool + +- name: install disk space check + sensu_check_dict: name=check-disk-space check="{{ sensu_checks.common.check_disk_space }}" + notify: restart sensu-client missing ok + +# below needs dictifying + +- name: cpu check + sensu_check: name=cpu plugin=check-cpu.rb args='-w 80 -c 90' service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok + +- name: disk check + sensu_check: name=disk plugin=check-disk.rb service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok + +- name: ntp-offset check + sensu_check: name=ntp-offset plugin=check-ntp.rb service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok + +- name: syslog check + sensu_check: name=syslog-socket plugin=check-syslog-socket.rb + service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok + +- name: memory check + sensu_check: name=memory plugin=check-mem.sh + args="-w {{ monitoring_common.checks.memory.warning }} -c {{ monitoring_common.checks.memory.critical }}" + service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok + +- name: add sensu to adm group so it can read log files + user: name=sensu groups=adm append=yes + +- name: log files greater than 1gb check + sensu_check: name=log_file_size plugin=check-for-large-files.sh + args='-d /var/log -s 1G' use_sudo=true + service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok + +- name: raid check + sensu_check: name=check-raid plugin=check-raid.sh interval=300 occurrences=1 + args="-z {{ monitoring_common.checks.raid.severity }}" + service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok + +- name: kernel-options + sensu_check: name=kernel-options plugin=check-kernel-options.rb state=absent + service_owner={{ monitoring_common.service_owner }} + notify: restart sensu-client missing ok diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 0000000..23e076e --- /dev/null +++ b/roles/common/tasks/main.yml @@ -0,0 +1,96 @@ +--- +- name: set utc timezone + template: src=etc/timezone dest=/etc/timezone owner=root group=root mode=0644 + notify: + - update timezone + +- name: add locale + locale_gen: name={{ common.locale }} state=present + notify: + - update locales + +- name: set locale + command: update-locale LANG={{ common.locale }} LC_ALL={{ common.locale }} + +- name: configure /etc/hosts + template: + src: ../templates/etc/hosts + dest: /etc/hosts + when: etc_hosts is defined + tags: etc_hosts + +- name: install common packages + apt: pkg={{ item }} + with_items: "{{ common.packages }}" + +- name: install system-wide python packages + pip: + name: "{{ item.name }}" + version: "{{ item.version|default(omit) }}" + state: "{{ item.state|default(omit) }}" + with_items: "{{ common.python.packages }}" + register: result + until: result|succeeded + retries: 5 + tags: python + +- name: create generic data directories + file: path={{ item.path }} owner={{ item.owner }} group={{ item.group }} + state=directory mode=0755 + with_items: "{{ common.data_dirs }}" + when: common.data_dirs|count > 0 + +- name: create /opt/sitecontroller paths + file: dest=/opt/sitecontroller/{{ item }} state=directory + recurse=yes + with_items: + - scripts + - sensu-plugins + tags: elk-stats + +- include: ssh.yml + tags: ssh + +- include: sudoers.yml + tags: sudoers + +- include: ufw.yml + tags: + - firewall + - common-ufw + +- name: install raid utilities + apt: pkg={{ item }} + with_items: "{{ common.hwraid.add_clients }}" + when: common.hwraid.enabled + tags: hwraid + +- name: uninstall raid utilities + apt: pkg={{ item }} state=absent + with_items: "{{ common.hwraid.remove_clients }}" + when: common.hwraid.enabled + tags: hwraid + +- name: install mdns tooling + apt: pkg=avahi-daemon + when: common.mdns.enabled|bool + +- include: ntpd.yml + when: common.ntpd.enabled|bool + tags: ntpd + +- include: shell_customization.yml + when: common.shell_customization.enabled|bool + tags: shell-customization + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/common/tasks/metrics.yml b/roles/common/tasks/metrics.yml new file mode 100644 index 0000000..7538956 --- /dev/null +++ b/roles/common/tasks/metrics.yml @@ -0,0 +1,30 @@ +--- +- name: vmstat metrics check + sensu_metrics_check: name=vmstat-metrics plugin=vmstat-metrics.rb + args='--scheme {{ monitoring_common.graphite.host_prefix }}.system.vmstat' + notify: restart sensu-client missing ok + +- name: load metrics check + sensu_metrics_check: name=load-metrics plugin=load-metrics.rb + args='--scheme {{ monitoring_common.graphite.host_prefix }}.system.load' + notify: restart sensu-client missing ok + +- name: memory metrics check + sensu_metrics_check: name=memory-metrics plugin=memory-metrics.rb + args='--scheme {{ monitoring_common.graphite.host_prefix }}.system.memory' + notify: restart sensu-client missing ok + +- name: network metrics check + sensu_metrics_check: name=network-metrics plugin=metrics-net.rb + args='--scheme {{ monitoring_common.graphite.host_prefix }}.system.network' + notify: restart sensu-client missing ok + +- name: disk usage metrics check + sensu_metrics_check: name=disk-usage-metrics plugin=metrics-disk-usage.rb + args='-f --scheme {{ monitoring_common.graphite.host_prefix }}.system.disk-usage' + notify: restart sensu-client missing ok + +- name: disk metrics check + sensu_metrics_check: name=disk-metrics plugin=metrics-disk.rb + args='--scheme {{ monitoring_common.graphite.host_prefix }}.system.disk' + notify: restart sensu-client missing ok diff --git a/roles/common/tasks/ntpd.yml b/roles/common/tasks/ntpd.yml new file mode 100644 index 0000000..518fdef --- /dev/null +++ b/roles/common/tasks/ntpd.yml @@ -0,0 +1,36 @@ +--- +- name: add ntp packages + apt: pkg={{ item }} state=present + with_items: + - ntp + - ntpdate + register: result + until: result|succeeded + retries: 5 + notify: + - flush time + +- meta: flush_handlers + +- name: ntpd configuration + template: src=etc/ntp.conf + dest=/etc/ntp.conf + notify: + - restart ntpd service + +- name: firewall for ntp tcp + ufw: rule=allow to_port=123 proto=tcp src={{ item.ip }}/{{ item.netmask }} + with_items: "{{ common.ntpd.clients }}" + tags: + - firewall + +- name: firewall for ntp udp + ufw: rule=allow to_port=123 proto=udp src={{ item.ip }}/{{ item.netmask }} + with_items: "{{ common.ntpd.clients }}" + tags: + - firewall + +- meta: flush_handlers + +- name: ensure ntp service is running + service: name=ntp state=started enabled=yes diff --git a/roles/common/tasks/serverspec.yml b/roles/common/tasks/serverspec.yml new file mode 100644 index 0000000..9824971 --- /dev/null +++ b/roles/common/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for common role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/common/tasks/shell_customization.yml b/roles/common/tasks/shell_customization.yml new file mode 100644 index 0000000..3de7c27 --- /dev/null +++ b/roles/common/tasks/shell_customization.yml @@ -0,0 +1,31 @@ +--- +- name: install bash prompt customizations + template: + src: etc/profile.d/prompt.sh + dest: /etc/profile.d/prompt.sh + +- name: install tmux fix ssh + template: + src: etc/profile.d/tmux_fix_ssh.sh + dest: /etc/profile.d/tmux_fix_ssh.sh + when: common.shell_customization.tmux_fix_ssh|bool + +- name: override admin_user bashrc + template: + src: admin_user/bashrc + dest: ~{{admin_user}}/.bashrc + +- name: override admin_user bash_profile + template: + src: admin_user/bashrc + dest: ~{{admin_user}}/.bash_profile + +- name: override admin user gitconfig + template: + src: admin_user/gitconfig + dest: ~{{admin_user}}/.gitconfig + +- name: override admin user tmux conf + template: + src: admin_user/tmux.conf + dest: ~{{admin_user}}/.tmux.conf diff --git a/roles/common/tasks/ssh.yml b/roles/common/tasks/ssh.yml new file mode 100644 index 0000000..61b7120 --- /dev/null +++ b/roles/common/tasks/ssh.yml @@ -0,0 +1,54 @@ +--- +- name: install GHE authorized keys app + template: + src: bin/ghe_authorized_keys.py + dest: /usr/local/bin/ghe_authorized_keys.py + owner: root + group: root + mode: 0755 + when: common.ssh.ghe_authorized_keys.enabled|bool + +- name: configure ssh settings + template: src=etc/ssh/sshd_config + dest=/etc/ssh/sshd_config + owner=root + group=root + mode=0644 + notify: + - reload-sshd + +- name: add ssh private keys + template: + src: ssh-private-key + dest: "{{ item.dest }}" + owner: "{{ item.owner|default('root') }}" + group: "{{ item.group|default('root') }}" + mode: 0600 + with_items: "{{ common.ssh.private_keys}}" + +- name: lay down ssh host rsa public key + template: + src: "etc/ssh/ssh_host_rsa_key.pub" + dest: "/etc/ssh/ssh_host_rsa_key.pub" + mode: 644 + owner: root + group: root + when: common.ssh.ssh_host_rsa_key.public + notify: + - reload-sshd + +- name: lay down ssh host rsa private key + template: + src: "etc/ssh/ssh_host_rsa_key" + dest: "/etc/ssh/ssh_host_rsa_key" + mode: 600 + owner: root + group: root + when: common.ssh.ssh_host_rsa_key.private + notify: + - reload-sshd + +- meta: flush_handlers + +- name: ensure ssh service is running + service: name=ssh state=started enabled=yes diff --git a/roles/common/tasks/sudoers.yml b/roles/common/tasks/sudoers.yml new file mode 100644 index 0000000..fd926de --- /dev/null +++ b/roles/common/tasks/sudoers.yml @@ -0,0 +1,32 @@ +--- +- name: ensure admin user has sudo access + template: + src: etc/sudoers.d/admin_user + dest: /etc/sudoers.d/admin_user + owner: root + group: root + mode: 0700 + when: (groups['bastion'] is not defined or inventory_hostname not in groups['bastion']) and + (groups['ttyspy-server'] is not defined or inventory_hostname not in groups['ttyspy-server']) + +- name: ensure blueboxadmin does not have sudo access + file: + dest: "/etc/sudoers.d/admin_user" + state: absent + when: (groups['bastion'] is defined and inventory_hostname in groups['bastion']) or + (groups['ttyspy-server'] is defined and inventory_hostname in groups['ttyspy-server']) + +- name: write general sudoers file + template: + src: etc/sudoers.d/blueboxcloud + dest: /etc/sudoers.d/blueboxcloud + owner: root + group: root + mode: 0744 + +- name: configure sudoers + template: src=etc/sudoers + dest=/etc/sudoers + owner=root + group=root + mode=0440 diff --git a/roles/common/tasks/ufw.yml b/roles/common/tasks/ufw.yml new file mode 100644 index 0000000..a382f34 --- /dev/null +++ b/roles/common/tasks/ufw.yml @@ -0,0 +1,22 @@ +--- +- name: install ufw + apt: pkg=ufw + register: result + until: result|succeeded + retries: 5 + +- name: Permit SSH + ufw: rule=allow to_port=22 proto=tcp src={{ item }} + with_items: "{{ common.ssh.allow_from }}" + +- name: Permit unrestricted access from remainder of cluster + ufw: rule=allow from_ip={{ item }} proto=any + with_items: "{{ common.firewall.friendly_networks }}" + +- name: Do not enforce forwarding rules with UFW + lineinfile: dest=/etc/default/ufw regexp="^DEFAULT_FORWARD_POLICY" + line="DEFAULT_FORWARD_POLICY=\"ACCEPT\"" + when: common.firewall.forwarding|bool + +- name: Enable UFW (after configuring SSH access) + ufw: state=enabled diff --git a/roles/common/templates/admin_user/bash_profile b/roles/common/templates/admin_user/bash_profile new file mode 100644 index 0000000..b7c44be --- /dev/null +++ b/roles/common/templates/admin_user/bash_profile @@ -0,0 +1,3 @@ +# .bashrc is only read by a shell that's both interactive and non-login +# let's fix that by _always_ reading bashrc +[[ -r ~/.bashrc ]] && . ~/.bashrc diff --git a/roles/common/templates/admin_user/bashrc b/roles/common/templates/admin_user/bashrc new file mode 100644 index 0000000..374e04a --- /dev/null +++ b/roles/common/templates/admin_user/bashrc @@ -0,0 +1,89 @@ +# ~/.bashrc: executed by bash(1) for non-login shells. +# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) +# for examples + +# If not running interactively, don't do anything +case $- in + *i*) ;; + *) return;; +esac + +# don't put duplicate lines or lines starting with space in the history. +# See bash(1) for more options +HISTCONTROL=ignoreboth + +# append to the history file, don't overwrite it +shopt -s histappend + +# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) +HISTSIZE=1000 +HISTFILESIZE=2000 + +# check the window size after each command and, if necessary, +# update the values of LINES and COLUMNS. +shopt -s checkwinsize + +# If set, the pattern "**" used in a pathname expansion context will +# match all files and zero or more directories and subdirectories. +#shopt -s globstar +# +PS1_HEAD='${debian_chroot:+($debian_chroot)}\u@\h:\w' +PS1_TAIL='\$ ' + +{% if common.shell_customization.git_prompt|bool %} +PS1_GIT="\$(prompt_git)" +{% endif %} + +PS1=${PS1_HEAD}${PS1_GIT}${PS1_TAIL} + +# make less more friendly for non-text input files, see lesspipe(1) +[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" + +# set variable identifying the chroot you work in (used in the prompt below) +if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then + debian_chroot=$(cat /etc/debian_chroot) +fi + +# enable color support of ls and also add handy aliases +if [ -x /usr/bin/dircolors ]; then + test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" + alias ls='ls --color=auto' + #alias dir='dir --color=auto' + #alias vdir='vdir --color=auto' + + alias grep='grep --color=auto' + alias fgrep='fgrep --color=auto' + alias egrep='egrep --color=auto' +fi + +# some more ls aliases +alias ll='ls -alF' +alias la='ls -A' +alias l='ls -CF' + +# Add an "alert" alias for long running commands. Use like so: +# sleep 10; alert +alias alert='notify-send --urgency=low -i "$([ $? = 0 ] && echo terminal || echo error)" "$(history|tail -n1|sed -e '\''s/^\s*[0-9]\+\s*//;s/[;&|]\s*alert$//'\'')"' + +# Alias definitions. +# You may want to put all your additions into a separate file like +# ~/.bash_aliases, instead of adding them here directly. +# See /usr/share/doc/bash-doc/examples in the bash-doc package. + +if [ -f ~/.bash_aliases ]; then + . ~/.bash_aliases +fi + +# enable programmable completion features (you don't need to enable +# this, if it's already enabled in /etc/bash.bashrc and /etc/profile +# sources /etc/bash.bashrc). +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi + +export HISTSIZE=100000 +export EDITOR=vim diff --git a/roles/common/templates/admin_user/gitconfig b/roles/common/templates/admin_user/gitconfig new file mode 100644 index 0000000..d6b6ac4 --- /dev/null +++ b/roles/common/templates/admin_user/gitconfig @@ -0,0 +1,5 @@ +[user] + email = sitecontroller@bluebox.net + name = Bastion {{ datacenter }} +[merge] + tool = vimdiff diff --git a/roles/common/templates/admin_user/tmux.conf b/roles/common/templates/admin_user/tmux.conf new file mode 100644 index 0000000..361a0f8 --- /dev/null +++ b/roles/common/templates/admin_user/tmux.conf @@ -0,0 +1 @@ +set-option -g history-limit 50000 diff --git a/roles/common/templates/bin/ghe_authorized_keys.py b/roles/common/templates/bin/ghe_authorized_keys.py new file mode 100644 index 0000000..d53bf43 --- /dev/null +++ b/roles/common/templates/bin/ghe_authorized_keys.py @@ -0,0 +1,30 @@ +#!/usr/bin/python + +import sys +import requests +import json + + +if len(sys.argv) == 2: + ssh_user = sys.argv[1] + + user_url = "{{ common.ssh.ghe_authorized_keys.api_url }}/users/%s" % ssh_user + key_url = "%s/keys" % user_url + api_user = '{{ common.ssh.ghe_authorized_keys.api_user }}' + api_key = '{{ common.ssh.ghe_authorized_keys.api_pass }}' + + user_info = requests.get(user_url,auth=(api_user, api_key)) + if(user_info.ok): + user = json.loads(user_info.content) + if not user['suspended_at']: + myResponse = requests.get(key_url,auth=(api_user, api_key)) + if(myResponse.ok): + keys = json.loads(myResponse.content) + for key in keys: + print key['key'] + else: + print "user %s is disabled" % ssh_user + raise +else: + print "you must provide a username" + raise diff --git a/roles/common/templates/etc/hosts b/roles/common/templates/etc/hosts new file mode 100644 index 0000000..46a6d8a --- /dev/null +++ b/roles/common/templates/etc/hosts @@ -0,0 +1,19 @@ +# {{ ansible_managed }} + +127.0.0.1 localhost.localdomain localhost +{% if hostname is defined and domain is defined %} +127.0.1.1 {{ hostname }}.{{ domain }} {{ hostname.split('.')[0] }} {{ hostname }} +{% else %} +127.0.1.1 {{ ansible_fqdn }} {{ ansible_hostname }} {{ ansible_nodename }} +{% endif %} + +# The following lines are desirable for IPv6 capable hosts +::1 ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + +{% for entry in etc_hosts -%} +{{ entry.ip }} {{ entry.name }} +{% endfor %} diff --git a/roles/common/templates/etc/ntp.conf b/roles/common/templates/etc/ntp.conf new file mode 100644 index 0000000..1a49a6f --- /dev/null +++ b/roles/common/templates/etc/ntp.conf @@ -0,0 +1,48 @@ +# {{ ansible_managed }} + +# /etc/ntp.conf, configuration for ntpd; see ntp.conf(5) for help +# Manage by ANSIBLE + +driftfile /var/lib/ntp/ntp.drift + +# Enable this if you want statistics to be logged. +#statsdir /var/log/ntpstats/ + +statistics loopstats peerstats clockstats +filegen loopstats file loopstats type day enable +filegen peerstats file peerstats type day enable +filegen clockstats file clockstats type day enable + +# Specify one or more NTP servers. + +# iburst + +{% for server in common.ntpd.servers %} + +server {{ server }} iburst + +{% endfor %} + +# if cannot access other servers, try to keep own time +server 127.127.1.0 +fudge 127.127.1.0 stratum 10 + +{% for peer in common.ntpd.peers %} +peer {{ peer }} +{% endfor %} + +# Access control configuration; see /usr/share/doc/ntp-doc/html/accopt.html for +# details. The web page +# might also be helpful. +# +# Note that "restrict" applies to both servers and clients, so a configuration +# that might be intended to block requests from certain clients could also end +# up blocking replies from your own upstream servers. + +{% for client in common.ntpd.clients %} +restrict {{ client.ip }} mask {{ client.netmask }} nomodify notrap +{% endfor %} + +# Local users may interrogate the ntp server more closely. +restrict 127.0.0.1 +restrict ::1 diff --git a/roles/common/templates/etc/profile.d/prompt.sh b/roles/common/templates/etc/profile.d/prompt.sh new file mode 100644 index 0000000..acc306a --- /dev/null +++ b/roles/common/templates/etc/profile.d/prompt.sh @@ -0,0 +1,21 @@ +#!/bin/bash +{% if common.shell_customization.git_prompt|bool %} +prompt_git() { + local branchName=''; + + # Check if the current directory is in a Git repository. + if [ $(git rev-parse --is-inside-work-tree &>/dev/null; echo "${?}") == '0' ]; then + + # Get the short symbolic ref. + # If HEAD isn’t a symbolic ref, get the short SHA for the latest commit + # Otherwise, just give up. + branchName="$(git symbolic-ref --quiet --short HEAD 2> /dev/null || \ + git rev-parse --short HEAD 2> /dev/null || \ + echo '(unknown)')"; + + echo -e " [${1}${branchName}]"; + else + return; + fi; +} +{% endif %} diff --git a/roles/common/templates/etc/profile.d/tmux_fix_ssh.sh b/roles/common/templates/etc/profile.d/tmux_fix_ssh.sh new file mode 100644 index 0000000..cf87285 --- /dev/null +++ b/roles/common/templates/etc/profile.d/tmux_fix_ssh.sh @@ -0,0 +1,10 @@ +fixssh() { + if [ -n "$TMUX" ]; then + echo "fixing tmux ssh-agent" + eval $(tmux show-env \ + |sed -n 's/^\(SSH_[^=]*\)=\(.*\)/export \1="\2"/p') + else + echo "not in tmux, something else is broken." + fi +} + diff --git a/roles/common/templates/etc/ssh/ssh_host_rsa_key b/roles/common/templates/etc/ssh/ssh_host_rsa_key new file mode 100644 index 0000000..0cf926f --- /dev/null +++ b/roles/common/templates/etc/ssh/ssh_host_rsa_key @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ common.ssh.ssh_host_rsa_key.private }} diff --git a/roles/common/templates/etc/ssh/ssh_host_rsa_key.pub b/roles/common/templates/etc/ssh/ssh_host_rsa_key.pub new file mode 100644 index 0000000..1afb423 --- /dev/null +++ b/roles/common/templates/etc/ssh/ssh_host_rsa_key.pub @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ common.ssh.ssh_host_rsa_key.public }} diff --git a/roles/common/templates/etc/ssh/sshd_config b/roles/common/templates/etc/ssh/sshd_config new file mode 100644 index 0000000..e14526c --- /dev/null +++ b/roles/common/templates/etc/ssh/sshd_config @@ -0,0 +1,126 @@ +# {{ ansible_managed }} + +# Package generated configuration file +# See the sshd_config(5) manpage for details + +# What ports, IPs and protocols we listen for +{% if groups['bastion'] is defined and inventory_hostname in groups['bastion'] %} +Port {{ bastion.ssh_port }} +{% else %} +Port 22 +{% endif %} +# Use these options to restrict which interfaces/protocols sshd will bind to +#ListenAddress :: +#ListenAddress 0.0.0.0 +Protocol 2 +# HostKeys for protocol version 2 +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key +#Privilege Separation is turned on for security +UsePrivilegeSeparation yes + +# Lifetime and size of ephemeral version 1 server key +KeyRegenerationInterval 3600 +ServerKeyBits 1024 + +# Logging +SyslogFacility AUTH +LogLevel INFO + +# Authentication: +LoginGraceTime 120 +PermitRootLogin no +StrictModes yes + +RSAAuthentication yes +PubkeyAuthentication yes +AuthorizedKeysFile {% if _users.manage_authorized_keys|bool %}/etc/ssh/authorized_keys/%u.keys{% else %}.ssh/authorized_keys{% endif %} + +# Don't read the user's ~/.rhosts and ~/.shosts files +IgnoreRhosts yes +# For this to work you will also need host keys in /etc/ssh_known_hosts +RhostsRSAAuthentication no +# similar for protocol version 2 +HostbasedAuthentication no +# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication +#IgnoreUserKnownHosts yes + +# To enable empty passwords, change to yes (NOT RECOMMENDED) +PermitEmptyPasswords no + +# Change to yes to enable challenge-response passwords (beware issues with +# some PAM modules and threads) +ChallengeResponseAuthentication no + +# Kerberos options +#KerberosAuthentication no +#KerberosGetAFSToken no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes + +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd yes +PrintLastLog yes +TCPKeepAlive yes +#UseLogin no + +MaxAuthTries 15 +MaxStartups 100 +MaxSessions {{ common.ssh.max_sessions }} +#Banner /etc/issue.net + +# Allow client to pass locale environment variables +#AcceptEnv +PermitUserEnvironment no +GatewayPorts no + +Subsystem sftp /usr/lib/openssh/sftp-server + +Ciphers aes128-ctr,aes192-ctr,aes256-ctr +MACs hmac-sha1,hmac-ripemd160 + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the ChallengeResponseAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via ChallengeResponseAuthentication may bypass +# the setting of "PermitRootLogin without-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and ChallengeResponseAuthentication to 'no'. +UsePAM yes +{% if common.ssh.disable_dns %} +UseDNS no +{% else %} +UseDNS yes +{% endif %} +GSSAPIAuthentication no +PasswordAuthentication no + +# Send keep-alives and disconnect inactive clients after 30 minutes +ClientAliveInterval {{ common.ssh.client_alive.interval }} +ClientAliveCountMax {{ common.ssh.client_alive.countmax }} + +{% if groups['bastion'] is defined and inventory_hostname in groups['bastion'] %} +# The remaining values are exclusive to Bastion hosts + +AllowAgentForwarding yes + +{% if bastion.force_commands | length > 0 %} +# Backdoor for {{ bastion.backdoor_user }} user +Match User *,!{{ bastion.backdoor_user }} + ForceCommand {{ bastion.force_commands | join(" ") }} +{% endif %} +{% endif %} + +{% if common.ssh.ghe_authorized_keys.enabled | bool %} +AuthorizedKeysCommand /usr/local/bin/ghe_authorized_keys.py +AuthorizedKeysCommandUser nobody +{% endif %} diff --git a/roles/common/templates/etc/sudoers b/roles/common/templates/etc/sudoers new file mode 100644 index 0000000..188591e --- /dev/null +++ b/roles/common/templates/etc/sudoers @@ -0,0 +1,35 @@ +# {{ ansible_managed }} +# +# This file MUST be edited with the 'visudo' command as root. +# +# Please consider adding local content in /etc/sudoers.d/ instead of +# directly modifying this file. +# +# See the man page for details on how to write a sudoers file. +# +Defaults env_reset +Defaults exempt_group=sudo +Defaults mail_badpass +Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/sensu/plugins" +# Pass user's existing ssh-agent into sudo. +# Example: Allow ansible (using sudo) to access user's forwarded agent to clone private repo. +Defaults env_keep+=SSH_AUTH_SOCK + +# Host alias specification + +# User alias specification + +# Cmnd alias specification + +# User privilege specification +root ALL=(ALL:ALL) ALL + +# Members of the admin group may gain root privileges +%admin ALL=(ALL) ALL + +# Allow members of group sudo to execute any command +%sudo ALL=(ALL) NOPASSWD:ALL + +# See sudoers(5) for more information on "#include" directives: + +#includedir /etc/sudoers.d diff --git a/roles/common/templates/etc/sudoers.d/admin_user b/roles/common/templates/etc/sudoers.d/admin_user new file mode 100644 index 0000000..27a6e99 --- /dev/null +++ b/roles/common/templates/etc/sudoers.d/admin_user @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ admin_user }} ALL=(ALL) NOPASSWD:ALL diff --git a/roles/common/templates/etc/sudoers.d/blueboxcloud b/roles/common/templates/etc/sudoers.d/blueboxcloud new file mode 100644 index 0000000..06016d2 --- /dev/null +++ b/roles/common/templates/etc/sudoers.d/blueboxcloud @@ -0,0 +1,7 @@ +# {{ ansible_managed }} + +{% for sudoer in common.sudoers|sort %} +{% for arg in sudoer.args|sort %} +{{ sudoer.name }} {{ arg }} +{% endfor %} +{% endfor %} diff --git a/roles/common/templates/etc/timezone b/roles/common/templates/etc/timezone new file mode 100644 index 0000000..7f39493 --- /dev/null +++ b/roles/common/templates/etc/timezone @@ -0,0 +1 @@ +Etc/UTC diff --git a/roles/common/templates/serverspec/common_spec.rb b/roles/common/templates/serverspec/common_spec.rb new file mode 100644 index 0000000..b717fd5 --- /dev/null +++ b/roles/common/templates/serverspec/common_spec.rb @@ -0,0 +1,288 @@ +# {{ ansible_managed }} + +require 'spec_helper' +require 'etc' + +### tasks/mail.yml + +{% for pkg in common.packages %} +describe package('{{ pkg }}') do + it { should be_installed } #OPS001 +end +{% endfor %} + +describe file('/etc/timezone') do + it { should be_file } #OPS002 + it { should contain 'Etc/UTC' } #OPS003 +end + +{% if common.hwraid.enabled %} +{% for pkg in common.hwraid.add_clients %} +describe package('{{ pkg }}') do + it { should be_installed } #OPS004 +end +{% endfor %} + +{% for pkg in common.hwraid.remove_clients %} +describe package('{{ pkg }}') do + it { should_not be_installed } #OPS005 +end +{% endfor %} +{% endif %} + +{% if common.mdns.enabled|bool %} +describe package('avahi-daemon') do + it { should be_installed } #OPS008 +end +{% else %} +describe package('avahi-daemon') do + it { should_not be_installed } #OPS009 +end +{% endif %} + +{% if common.python.proxy_url|default('False')|bool %} +describe file('/root/.pip/pip.conf') do + it { should be_file } #OPS010 + it { should contain '{{ common.pip.proxy_url }}' } #OPS011 +end +{% endif %} + +{% for pkg in common.python.packages %} +{% if not pkg.skip_serverspec|default('False')|bool %} +describe package('{{ pkg.name.split('>')[0] }}') do + it { should be_installed.by('pip') } #OPS012 +end +{% endif %} +{% endfor %} + +describe file('/etc/sudoers') do + its(:content) { should match /^Defaults\s+env_keep\+=SSH_AUTH_SOCK/ } #OPS014 +end + +describe service('ssh') do + it { should be_enabled } +end + +describe file('/etc/ssh/sshd_config') do + its(:content) { should match /^PasswordAuthentication no/ } #OPS015 + its(:content) { should_not match /^PasswordAuthentication yes/ } #OPS016 + its(:content) { should match /^PermitRootLogin no/ } #OPS017 + its(:content) { should_not match /^PermitRootLogin yes/ } #OPS018 + its(:content) { should match /^PermitEmptyPasswords no/ } #OPS019 + its(:content) { should_not match /^PermitEmptyPasswords yes/ } #OPS020 + its(:content) { should match /^PubkeyAuthentication yes/ } #OPS021 + its(:content) { should_not match /^PubkeyAuthentication no/ } #OPS022 + its(:content) { should match /^RSAAuthentication yes/ } #OPS023 + its(:content) { should_not match /^RSAAuthentication no/ } #OPS024 + its(:content) { should match /^HostbasedAuthentication no/ } #OPS025 + its(:content) { should_not match /^HostbasedAuthentication yes/ } #OPS026 + its(:content) { should match /^IgnoreRhosts yes/ } #OPS027 + its(:content) { should_not match /^IgnoreRhosts no/ } #OPS028 + its(:content) { should match /^PrintMotd yes/ } #OPS029 + its(:content) { should_not match /^PrintMotd no/ } #OPS030 + its(:content) { should match /^PermitUserEnvironment no/ } #OPS031 + its(:content) { should_not match /^PermitUserEnvironment yes/ } #OPS032 + its(:content) { should match /^StrictModes yes/ } #OPS033 + its(:content) { should_not match /^StrictModes no/ } #OPS034 + its(:content) { should match /^ServerKeyBits 1024/ } #OPS035 + its(:content) { should match /^TCPKeepAlive yes/ } #OPS036 + its(:content) { should_not match /^TCPKeepAlive no/ } #OPS037 + its(:content) { should match /^LoginGraceTime 120/ } #OPS038 + its(:content) { should match /^MaxStartups 100/ } #OPS039 + its(:content) { should match /^LogLevel INFO/ } #OPS040 + its(:content) { should match /^MaxAuthTries 15/ } #OPS041 + its(:content) { should match /^KeyRegenerationInterval 3600/ } #OPS042 + its(:content) { should match /^Protocol 2/ } #OPS043 + its(:content) { should match /^GatewayPorts no/ } #OPS044 + its(:content) { should_not match /^GatewayPorts yes/ } #OPS045 + its(:content) { should match /^UsePAM yes/ } #OPS046 + its(:content) { should_not match /^UsePAM no/ } #OPS047 + its(:content) { should match /^Ciphers aes128-ctr,aes192-ctr,aes256-ctr/ } #OPS092 + its(:content) { should match /^MACs hmac-sha1,hmac-ripemd160/ } #OPS093 +{% if common.ssh.disable_dns|bool %} + its(:content) { should match /^UseDNS no/ } #OPS048 + its(:content) { should_not match /^UseDNS yes/ } #OPS049 +{% else %} + its(:content) { should match /^UseDNS yes/ } #OPS050 + its(:content) { should_not match /^UseDNS no/ } #OPS051 +{% endif %} +end + +{% for item in common.ssh.private_keys %} +describe file('{{ item.dest }}') do + it { should be_owned_by '{{ item.owner|default('root') }}' } #OPS052 + it { should be_grouped_into '{{ item.group|default('root') }}' } #OPS053 + it { should be_mode 600 } #OPS054 +end +{% endfor %} + +describe package('ufw') do + it { should be_installed } #OPS056 +end + +describe command('ufw status') do + its(:stdout) { should match /Status: active/ } #OPS057 +end + +# FIX THIS TO BE BETTERER +#describe command("ufw show added | sed '1d'") do +#{% for item in common.ssh.allow_from %} +# {% if item == "0.0.0.0/0" %} +# its(:stdout) { should match /^ufw allow 22\/tcp$/ } +# {% else %} +# its(:stdout) { should match /^ufw allow 22\/tcp/ } +# {% endif %} +#{% endfor %} +#end + +# TEST FOR THIS +#- name: Permit unrestricted access from remainder of cluster +# ufw: rule=allow from_ip={{ item }} proto=any +# with_items: common.firewall.friendly_networks + + +describe file('/etc/default/ufw') do +{% if common.firewall.forwarding|bool %} + its(:content) { should match /^DEFAULT_FORWARD_POLICY="ACCEPT"/ } #OPS058 +{% else %} + its(:content) { should_not match /^DEFAULT_FORWARD_POLICY="ACCEPT"/ } #OPS059 +{% endif %} +end + +{% if common.ntpd.enabled|bool %} + +['ntp','ntpdate'].each do |pkg| + describe package(pkg) do + it { should be_installed } #OPS060 + end +end + +describe service('ntp') do + it { should be_enabled } +end + +{% endif %} + +describe file('/etc/pam.d/login') do + its(:content) { should contain /^@include common-auth/ } #OPS062 + its(:content) { should contain /^@include common-account/ } #OPS063 + its(:content) { should contain /^@include common-session/ } #OPS064 + its(:content) { should contain /^@include common-password/ } #OPS065 +end + +describe file('/etc/pam.d/common-password') do + its(:content) { should contain /^password\t\[success=1 default=ignore\]\tpam_unix\.so obscure sha512/ } #OPS066 +end + +describe file('/etc/adduser.conf') do + its(:content) { should match /^DIR_MODE=([0-7][0-5][0-5]|0[0-7][0-5][0-5])/ } #OPS067 +end + +files = ['.rhosts','.netrc'] +files.each do |file| + describe file ("~root/#{file}") do + it { should_not exist } #OPS068 + end +end + +files = ['bin', 'boot', 'dev', 'etc', 'home','lib', + 'lib64', 'lost+found', 'media', 'mnt', 'opt', 'proc', 'root', + 'run', 'sbin', 'srv', 'sys', 'usr', 'var'] +files.each do |file| + describe file("/#{file}/") do + it { should be_directory } #OPS069 + it { should be_mode '[0-7][0-7][0-5]' } #OPS070 + end +end + +files = ['bin', 'games', 'include', 'lib', 'local', 'sbin', 'share', 'src'] +files.each do |file| + describe file("/usr/#{file}/") do + it { should be_directory } #OPS071 + it { should be_mode '[0-7][0-7][0-5]' } #OPS072 + end +end + +describe file('/etc/security/opasswd') do + it { should exist } #OPS073 + it { should be_mode 600 } #OPS074 +end + +describe file('/etc/shadow') do + it { should exist } #OPS075 + it { should be_mode 640 } #OPS076 +end + +files = ['backups', 'cache', 'lib', 'local', + 'log', 'mail', 'opt', 'spool'] +files.each do |file| + describe file("/var/#{file}/") do + it { should be_directory } #OPS077 + it { should be_mode '[0-2]*[0-7][0-7][0-5]' } #OPS078 + end +end + +describe file('/var/tmp/') do + it { should be_directory } #OPS079 +end + +files = ['syslog', 'auth.log'] +files.each do |file| + describe file ("/var/log/#{file}") do + it { should exist } #OPS080 + it { should be_mode '[0-7][0-5][0-5]' } #OPS081 + it { should be_owned_by 'syslog' } #OPS082 + end +end + +describe file('/tmp/') do + it { should be_directory } #OPS083 +end + +files = ['/etc/init/', '/var/spool/cron/', '/etc/cron.d/', '/etc/cron.d/sysstat', '/etc/init.d/', '/etc/rc0.d/', + '/etc/rc1.d/', '/etc/rc2.d/','/etc/rc3.d/','/etc/rc4.d/','/etc/rc5.d/','/etc/rc6.d/','/etc/rcS.d/'] +files.each do |file| + describe file("#{file}") do + it { should be_mode '[0-7][0-7][0-5]' } #OPS084 + end +end + +files = ['/', '/usr', '/etc', '/etc/security/opasswd', + '/etc/shadow', '/var', '/var/tmp', '/var/log', + '/var/log/wtmp', '/tmp'] +files.each do |file| + describe file(file) do + it { should be_owned_by 'root' } #OPS085 + end +end + +#describe file('/etc/login.defs') do +# its(:content) { should match /^PASS_MAX_DAYS 90/ } +# its(:content) { should_not match /^PASS_MAX_DAYS 99999/ } +# its(:content) { should match /^PASS_MIN_DAYS 1/ } +# its(:content) { should_not match /^PASS_MIN_DAYS 0/ } +#end + +describe file('/etc/sudoers.d/blueboxcloud') do + it { should be_mode 744 } #OPS086 + it { should be_owned_by 'root' } #OPS087 + it { should be_grouped_into 'root' } #OPS088 + it { should be_file } #OPS089 +{% for sudoer in common.sudoers %} +{% for arg in sudoer.args %} + its(:content) { should contain('{{ sudoer.name }} {{ arg }}'.gsub(/[()]/){ |c| "\\" << c }) } #OPS090 +{% endfor -%} +{% endfor %} +end + +{% if groups['bastion'] is defined and inventory_hostname in groups['bastion'] %} +describe command('grep -r "blueboxadmin\sALL=(ALL)\sNOPASSWD:ALL" /etc/sudoers.d/') do + its(:stdout) { should_not contain('blueboxadmin') } #OPS091 +end +{% endif %} + +{% if groups['ttyspy-server'] is defined and inventory_hostname in groups['ttyspy-server'] %} +describe command('grep -r "blueboxadmin\sALL=(ALL)\sNOPASSWD:ALL" /etc/sudoers.d/') do + its(:stdout) { should_not contain('blueboxadmin') } #OPS091 +end +{% endif %} diff --git a/roles/common/templates/ssh-private-key b/roles/common/templates/ssh-private-key new file mode 100644 index 0000000..011c993 --- /dev/null +++ b/roles/common/templates/ssh-private-key @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +{{ item.private_key }} diff --git a/roles/consul/defaults/main.yml b/roles/consul/defaults/main.yml new file mode 100644 index 0000000..2151843 --- /dev/null +++ b/roles/consul/defaults/main.yml @@ -0,0 +1,29 @@ +--- +consul: + version: 0.6.4 + download: + url: https://releases.hashicorp.com/consul/0.6.4/consul_0.6.4_linux_amd64.zip + sha256sum: abdf0e1856292468e2c9971420d73b805e93888e006c76324ae39416edcf0627 + webui: + version: 0.5.0 + download: + url: https://dl.bintray.com/mitchellh/consul/0.5.0_web_ui.zip + sha256sum: 0081d08be9c0b1172939e92af5a7cf9ba4f90e54fae24a353299503b24bb8be9 + bind_interface: '{{ private_device_interface }}' + bootstrap_expect: 1 + join_at_start: true + disable_remote_exec: true + bin_path: /opt/consul/bin + archive_path: /opt/consul/archives + config_path: /opt/consul/etc + config_file: /opt/consul/etc/consul.json + data_path: /opt/consul/data + domain: + is_server: true + is_ui: false + log_level: "INFO" + rejoin_after_leave: true + leave_on_terminate: false + client_address: "127.0.0.1" + node_name: "{{ ansible_nodename }}" + datacenter: "default" diff --git a/roles/consul/meta/main.yml b/roles/consul/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/consul/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/consul/tasks/checks.yml b/roles/consul/tasks/checks.yml new file mode 100644 index 0000000..e84a2a0 --- /dev/null +++ b/roles/consul/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install consul process check + sensu_check_dict: name="check-consul-process" check="{{ sensu_checks.consul.check_consul_process }}" + notify: restart sensu-client missing ok diff --git a/roles/consul/tasks/main.yml b/roles/consul/tasks/main.yml new file mode 100644 index 0000000..713f21c --- /dev/null +++ b/roles/consul/tasks/main.yml @@ -0,0 +1,79 @@ +--- +- name: create consul user + user: name=consul comment=consul shell=/bin/false + system=yes home=/nonexistent + +- name: make consul directories + file: dest={{ item }} state=directory owner=consul + with_items: + - "{{ consul.bin_path }}" + - "{{ consul.archive_path }}" + - "{{ consul.config_path }}" + - "{{ consul.config_path }}/conf.d" + - "{{ consul.data_path }}" + +- name: download consul + get_url: url={{ consul.download.url }} + dest="{{ consul.archive_path }}/consul-{{ consul.version }}.zip" + sha256sum={{ consul.download.sha256sum }} + +- stat: path={{ consul.bin_path }}/consul + register: consul_binary + +- name: unzip consul binary + unarchive: src="{{ consul.archive_path }}/consul-{{ consul.version }}.zip" dest={{ consul.bin_path }} copy=no + when: consul_binary.stat.exists == False + +- name: link consul binary to path + file: src={{ consul.bin_path }}/consul dest=/usr/local/bin/consul state=link + +- name: configure consul + template: src=etc/consul.json dest={{ consul.config_file }} owner=consul + notify: restart consul server + +- name: allow access to consul tcp ports + ufw: rule=allow src={{ hostvars[item.0]['ansible_' + consul.bind_interface].ipv4.address }} to_port={{ item.1 }} proto=tcp + with_nested: + - "{{ groups.consul }}" + - - 8500 + - 8400 + - 8301 + - 8302 + - 8300 + - 8600 + tags: + - firewall + +- name: allow access to consul udp ports + ufw: rule=allow src={{ hostvars[item.0]['ansible_' + consul.bind_interface].ipv4.address }} to_port={{ item.1 }} proto=udp + with_nested: + - "{{ groups.consul }}" + - - 8301 + - 8302 + - 8600 + tags: + - firewall + +- name: consul service + upstart_service: name=consul + cmd={{ consul.bin_path }}/consul + args="agent {% if consul.is_server %}-bootstrap-expect {{ groups.consul_server|length }}{% endif %} -config-dir {{ consul.config_path }}/conf.d -config-file={{ consul.config_file }}" + user=consul + notify: restart consul server + +- meta: flush_handlers + +- name: start consul service + service: name=consul state=started enabled=yes + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/consul/tasks/metrics.yml b/roles/consul/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/consul/tasks/serverspec.yml b/roles/consul/tasks/serverspec.yml new file mode 100644 index 0000000..be4681a --- /dev/null +++ b/roles/consul/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for consul-server role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/consul/templates/etc/consul.json b/roles/consul/templates/etc/consul.json new file mode 100644 index 0000000..2b9d77e --- /dev/null +++ b/roles/consul/templates/etc/consul.json @@ -0,0 +1,36 @@ +{ +{% if consul.join_at_start is defined and consul.join_at_start %} + "start_join": {{ hostvars|consul_server_ips(groups)|to_nice_json }}, +{% endif %} +{% if consul.disable_remote_exec is defined and consul.disable_remote_exec %} + "disable_remote_exec": {{ "true" if consul.disable_remote_exec else "false" }}, +{% endif %} + "domain": "{{ consul.domain }}", + "data_dir": "{{ consul.data_path }}", + "log_level": "{{ consul.log_level }}", + "node_name": "{{ consul.node_name }}", + "server": {{ "true" if consul.is_server else "false" }}, + "client_addr": "{{ consul.client_address }}", + "bind_addr": "{{ hostvars[inventory_hostname]['ansible_' + consul.bind_interface]['ipv4']['address'] }}", +{% if consul.advertise_address is defined %} + "advertise_addr": "{{ consul.advertise_address }}", +{% endif %} + "datacenter": "{{ consul.datacenter }}", +{% if consul.is_server and consul.bootstrap is defined %} + "bootstrap": {{ "true" if consul.bootstrap else "false" }}, +{% endif %} +{% if consul.is_server and consul.bootstrap_expect is defined %} + "bootstrap_expect": {{ consul.bootstrap_expect }}, +{% endif %} +{% if consul.encrypt_key is defined %} + "encrypt": "{{ consul.encrypt_key }}", +{% endif %} +{% if consul.watches is defined %} + "watches": {{ consul.watches|to_nice_json }}, +{% endif %} +{% if consul.encrypt is defined %} + "encrypt": "{{ consul.encrypt }}", +{% endif %} + "rejoin_after_leave": {{ "true" if consul.rejoin_after_leave else "false" }}, + "leave_on_terminate": {{ "true" if consul.leave_on_terminate else "false" }} +} diff --git a/roles/consul/templates/serverspec/consul-server_rb.yml b/roles/consul/templates/serverspec/consul-server_rb.yml new file mode 100644 index 0000000..379da19 --- /dev/null +++ b/roles/consul/templates/serverspec/consul-server_rb.yml @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('consul') do + it { should be_installed } +end + +describe service('consul') do + it { should be_enabled } +end diff --git a/roles/dnsmasq/defaults/main.yml b/roles/dnsmasq/defaults/main.yml new file mode 100644 index 0000000..e9f6c82 --- /dev/null +++ b/roles/dnsmasq/defaults/main.yml @@ -0,0 +1,19 @@ +--- +dnsmasq: + interface: ~ + except_interface: ~ + server: "{{ private_ipv4.address }}" + addn_hosts: [] + dns: + servers: + - 8.8.8.8 + - 8.8.4.4 + hosts: + - name: test + ip: 127.0.0.1 + firewall: [] +# EXAMPLE +# - port: 53 +# src: 0.0.0.0 +# to_ip: any +# proto: any diff --git a/roles/dnsmasq/handlers/main.yml b/roles/dnsmasq/handlers/main.yml new file mode 100644 index 0000000..460fe4a --- /dev/null +++ b/roles/dnsmasq/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart dnsmasq + service: name=dnsmasq state=restarted diff --git a/roles/dnsmasq/meta/main.yml b/roles/dnsmasq/meta/main.yml new file mode 100644 index 0000000..f8a5101 --- /dev/null +++ b/roles/dnsmasq/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: sensu-check diff --git a/roles/dnsmasq/tasks/checks.yml b/roles/dnsmasq/tasks/checks.yml new file mode 100644 index 0000000..b314df8 --- /dev/null +++ b/roles/dnsmasq/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install dnsmasq process check + sensu_check_dict: name="check-dnsmasq-process" check="{{ sensu_checks.dnsmasq.check_dnsmasq_process }}" + notify: restart sensu-client missing ok diff --git a/roles/dnsmasq/tasks/main.yml b/roles/dnsmasq/tasks/main.yml new file mode 100644 index 0000000..83f1652 --- /dev/null +++ b/roles/dnsmasq/tasks/main.yml @@ -0,0 +1,59 @@ +--- +- name: install dnsmasq prereqs + apt: pkg={{ item }} state=installed + with_items: + - dnsmasq + +- name: ensure /etc/dnsmasq.d exists + file: dest=/etc/dnsmasq.d state=directory mode=0755 + +- name: dnsmasq config + lineinfile: dest=/etc/dnsmasq.conf + line="conf-dir=/etc/dnsmasq.d" + notify: restart dnsmasq + +- name: create dnsmasq server config + template: src=etc/dnsmasq.d/server.conf dest=/etc/dnsmasq.d/server.conf + notify: restart dnsmasq + +- name: ubuntu default config + template: src=etc/default/dnsmasq dest=/etc/default/dnsmasq + notify: restart dnsmasq + +- name: create dnsmasq hosts file + template: src=etc/hosts.dnsmasq dest=/etc/hosts.dnsmasq + with_items: + - server.conf + notify: restart dnsmasq + +- name: create dnsmasq unmanaged hosts files + copy: dest="{{ item }}" owner=dnsmasq content="" force=no + with_items: "{{ dnsmasq.addn_hosts }}" + +- name: create resolvconf + template: src=etc/resolv.conf dest=/etc/resolv.conf + +- meta: flush_handlers + +- name: start dnsmasq services + service: name=dnsmasq state=started enabled=yes + +- name: permit access to dnsmasq + ufw: rule=allow + port="{{ item.port }}" src="{{ item.src }}" + to_ip="{{ item.to_ip }}" proto="{{ item.proto }}" + with_items: "{{ dnsmasq.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/dnsmasq/tasks/metrics.yml b/roles/dnsmasq/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/dnsmasq/tasks/serverspec.yml b/roles/dnsmasq/tasks/serverspec.yml new file mode 100644 index 0000000..3855b4e --- /dev/null +++ b/roles/dnsmasq/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for dnsmasq role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/dnsmasq/templates/etc/default/dnsmasq b/roles/dnsmasq/templates/etc/default/dnsmasq new file mode 100644 index 0000000..630f8c8 --- /dev/null +++ b/roles/dnsmasq/templates/etc/default/dnsmasq @@ -0,0 +1,34 @@ +# This file has five functions: +# 1) to completely disable starting dnsmasq, +# 2) to set DOMAIN_SUFFIX by running `dnsdomainname` +# 3) to select an alternative config file +# by setting DNSMASQ_OPTS to --conf-file= +# 4) to tell dnsmasq to read the files in /etc/dnsmasq.d for +# more configuration variables. +# 5) to stop the resolvconf package from controlling dnsmasq's +# idea of which upstream nameservers to use. +# For upgraders from very old versions, all the shell variables set +# here in previous versions are still honored by the init script +# so if you just keep your old version of this file nothing will break. + +#DOMAIN_SUFFIX=`dnsdomainname` +#DNSMASQ_OPTS="--conf-file=/etc/dnsmasq.alt" + +# Whether or not to run the dnsmasq daemon; set to 0 to disable. +ENABLED=1 + +# By default search this drop directory for configuration options. +# Libvirt leaves a file here to make the system dnsmasq play nice. +# Comment out this line if you don't want this. The dpkg-* are file +# endings which cause dnsmasq to skip that file. This avoids pulling +# in backups made by dpkg. +CONFIG_DIR=/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new + +# If the resolvconf package is installed, dnsmasq will use its output +# rather than the contents of /etc/resolv.conf to find upstream +# nameservers. Uncommenting this line inhibits this behaviour. +# Not that including a "resolv-file=" line in +# /etc/dnsmasq.conf is not enough to override resolvconf if it is +# installed: the line below must be uncommented. +IGNORE_RESOLVCONF=yes + diff --git a/roles/dnsmasq/templates/etc/dnsmasq.d/server.conf b/roles/dnsmasq/templates/etc/dnsmasq.d/server.conf new file mode 100644 index 0000000..7e4d496 --- /dev/null +++ b/roles/dnsmasq/templates/etc/dnsmasq.d/server.conf @@ -0,0 +1,22 @@ +# {{ ansible_managed }} + +### server specific config items go here + +bind-interfaces +strict-order + +{% if dnsmasq.interface %} +interface={{ dnsmasq.interface }} +{% endif %} +{% if dnsmasq.except_interface %} +no-dhcp-interface={{ dnsmasq.except_interface }} +{% endif %} + +{% for server in dnsmasq.dns.servers %} +server={{ server }} +{% endfor %} + +addn-hosts=/etc/hosts.dnsmasq +{% for hosts_file in dnsmasq.addn_hosts %} +addn-hosts={{ hosts_file }} +{% endfor %} diff --git a/roles/dnsmasq/templates/etc/hosts.dnsmasq b/roles/dnsmasq/templates/etc/hosts.dnsmasq new file mode 100644 index 0000000..f90394c --- /dev/null +++ b/roles/dnsmasq/templates/etc/hosts.dnsmasq @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +{% for host in dnsmasq.dns.hosts %} +{{ host.ip }} {{ host.name }} +{% endfor %} diff --git a/roles/dnsmasq/templates/etc/resolv.conf b/roles/dnsmasq/templates/etc/resolv.conf new file mode 100644 index 0000000..c938eb8 --- /dev/null +++ b/roles/dnsmasq/templates/etc/resolv.conf @@ -0,0 +1,3 @@ +# {{ ansible_managed }} + +nameserver 127.0.0.1 diff --git a/roles/dnsmasq/templates/serverspec/dnsmasq_spec.rb b/roles/dnsmasq/templates/serverspec/dnsmasq_spec.rb new file mode 100644 index 0000000..ca30cfb --- /dev/null +++ b/roles/dnsmasq/templates/serverspec/dnsmasq_spec.rb @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe package('dnsmasq') do + it { should be_installed } +end + +describe service('dnsmasq') do + it { should be_enabled } +end diff --git a/roles/docker/defaults/main.yml b/roles/docker/defaults/main.yml new file mode 100644 index 0000000..1771157 --- /dev/null +++ b/roles/docker/defaults/main.yml @@ -0,0 +1,3 @@ +--- +docker: + allow: [] diff --git a/roles/docker/meta/main.yml b/roles/docker/meta/main.yml new file mode 100644 index 0000000..8736650 --- /dev/null +++ b/roles/docker/meta/main.yml @@ -0,0 +1,6 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.docker.repo }} ubuntu-trusty main' + key_url: '{{ apt_repos.docker.key_url }}' diff --git a/roles/docker/tasks/checks.yml b/roles/docker/tasks/checks.yml new file mode 100644 index 0000000..3a001d2 --- /dev/null +++ b/roles/docker/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install docker process check + sensu_check_dict: name="check-docker-process" check="{{ sensu_checks.docker.check_docker_process }}" + notify: restart sensu-client missing ok diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml new file mode 100644 index 0000000..c456469 --- /dev/null +++ b/roles/docker/tasks/main.yml @@ -0,0 +1,25 @@ +--- +- name: install docker + apt: pkg=docker-engine + +- name: add users to docker group + user: name={{ item }} groups=docker append=yes + with_items: "{{ docker.allow }}" + +- name: enable and start the docker daemon + service: + name: docker + enabled: yes + state: started + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/docker/tasks/metrics.yml b/roles/docker/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/docker/tasks/serverspec.yml b/roles/docker/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/elasticsearch/defaults/main.yml b/roles/elasticsearch/defaults/main.yml new file mode 100644 index 0000000..3cd9427 --- /dev/null +++ b/roles/elasticsearch/defaults/main.yml @@ -0,0 +1,75 @@ +--- +elasticsearch: + version: 5.4.1 + wait_for_listening: true # sometimes need to skip, like upgrades + start_on_config: true # sane defaults to avoid blowing up the cluster in prod + restart_on_config: false # sane defaults to avoid blowing up the cluster in prod + config: + cluster.name: elasticsearch + node.name: "{{ ansible_hostname }}" + path.logs: /var/log/elasticsearch + path.data: /var/lib/elasticsearch + path.conf: /etc/elasticsearch + cluster.routing.allocation.disk.watermark.low: 100gb + cluster.routing.allocation.disk.watermark.high: 25gb + http.max_header_size: 64kb + firewall: [] + max_open_files: 65536 + heap_size: 512m + home_dir: /usr/share/elasticsearch + plugin_dir: /usr/share/elasticsearch/plugins + plugins: + - name: x-pack + file: x-pack-5.4.1.zip + url: http://artifacts.elastic.co/downloads/packs/x-pack/x-pack-5.4.1.zip + sha256sum: 7a93565b8d4af2d7d4dc804245543a4f38b2b31f8d0395dad792b7ea05d0088c + config: + xpack.security.enabled: false + sensu_plugins: + - name: elasticsearch + version: 1.0.18 + - name: elasticsearch + version: 2.0 + - name: sensu-plugins-elasticsearch + version: 1.0.0 + curator: + actions: + delete_indices: + enabled: True + description: "Delete indices older than specified number of days." + older: 90 + hour: 2 + minute: 0 + forcemerge: + enabled: True + description: "Force Merge indices older than days specified to max_num_seg. per shard." + options: + max_num_segments: 1 + older: 2 + hour: 3 + minute: 0 + delete_snapshots: + enabled: False + description: "Delete snapshots from the repository older than specified number of days." + options: + repository: logstash + older: 90 + hour: 4 + minute: 0 + snapshot: + enabled: False + description: "Snapshot selected indices to specified repository." + options: + repository: logstash + wait_for_completion: False + older: 1 + hour: 5 + minute: 0 + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/elasticsearch/*.log + fields: + tags: elk,elasticsearch + logging: + forwarder: filebeat diff --git a/roles/elasticsearch/handlers/main.yml b/roles/elasticsearch/handlers/main.yml new file mode 100644 index 0000000..4b66739 --- /dev/null +++ b/roles/elasticsearch/handlers/main.yml @@ -0,0 +1,17 @@ +--- +- name: start elasticsearch-if-start-enabled + service: + name: elasticsearch + state: started + when: elasticsearch.start_on_config|bool + +- name: restart elasticsearch-if-restart-enabled + service: + name: elasticsearch + state: restarted + when: elasticsearch.restart_on_config|bool + +- name: stop elasticsearch + service: + name: elasticsearch + state: stopped diff --git a/roles/elasticsearch/meta/main.yml b/roles/elasticsearch/meta/main.yml new file mode 100644 index 0000000..9db16bc --- /dev/null +++ b/roles/elasticsearch/meta/main.yml @@ -0,0 +1,13 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.elasticsearch.repo }} stable main' + key_url: '{{ apt_repos.elasticsearch.key_url }}' + - role: runtime/java + - role: logging-config + service: elasticsearch + logdata: "{{ elasticsearch.logs }}" + forward_type: "{{ elasticsearch.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/elasticsearch/tasks/checks.yml b/roles/elasticsearch/tasks/checks.yml new file mode 100644 index 0000000..f29e565 --- /dev/null +++ b/roles/elasticsearch/tasks/checks.yml @@ -0,0 +1,31 @@ +--- +- name: install sensu plugins + gem: + name: "{{ item.name }}" + version: "{{ item.version|default(omit) }}" + executable: /opt/sensu/embedded/bin/gem + user_install: no + with_items: "{{ elasticsearch.sensu_plugins }}" + register: result + until: result|succeeded + retries: 5 + +- name: install sensu checks + sensu_check_dict: name="{{ item.name }}" check="{{ item.check }}" + with_items: + - name: check-es-cluster-status + check: "{{ sensu_checks.elasticsearch.check_es_cluster_status }}" + - name: check-es-node-status + check: "{{ sensu_checks.elasticsearch.check_es_node_status }}" + - name: check-es-file-descriptors + check: "{{ sensu_checks.elasticsearch.check_es_file_descriptors }}" + - name: check-es-heap + check: "{{ sensu_checks.elasticsearch.check_es_heap }}" + - name: check-es-circuit-breakers + check: "{{ sensu_checks.elasticsearch.check_es_circuit_breakers }}" + notify: restart sensu-client missing ok + +# ELS016 +- name: install elasticsearch process check + sensu_check_dict: name="check-elasticsearch-process" check="{{ sensu_checks.elasticsearch.check_elasticsearch_process }}" + notify: restart sensu-client missing ok diff --git a/roles/elasticsearch/tasks/curator.yml b/roles/elasticsearch/tasks/curator.yml new file mode 100644 index 0000000..0ee1727 --- /dev/null +++ b/roles/elasticsearch/tasks/curator.yml @@ -0,0 +1,35 @@ +- name: install elasticsearch curator + pip: name=elasticsearch-curator version=4.1.2 + register: result + until: result|succeeded + retries: 5 + +- name: install curator symlink + file: state=link + src=/usr/local/bin/curator + dest=/usr/bin/curator + +- name: configure curator + template: src=etc/elasticsearch/curator.yml + dest=/etc/elasticsearch/curator.yml + +- name: configure curator actions + template: src=etc/elasticsearch/action.yml + dest="/etc/elasticsearch/{{ item.key }}.yml" + when: "item.value.enabled|bool" + with_dict: "{{ elasticsearch.curator.actions }}" + +- name: remove old crontab tasks + cron: name=curator-{{ item }} state=absent + with_items: + - delete + - close + - optimize + +- name: install cron tasks + cron: name=curator-{{item.key}} state={{ item.state|default('present') }} + hour="{{ item.value.hour|default('0') }}" minute="{{ item.value.minute|default('0') }}" + user="root" cron_file="curator" + job="curator --config /etc/elasticsearch/curator.yml /etc/elasticsearch/{{ item.key }}.yml" + when: "item.value.enabled|bool" + with_dict: "{{ elasticsearch.curator.actions }}" diff --git a/roles/elasticsearch/tasks/main.yml b/roles/elasticsearch/tasks/main.yml new file mode 100644 index 0000000..6c70bc4 --- /dev/null +++ b/roles/elasticsearch/tasks/main.yml @@ -0,0 +1,142 @@ +--- +- name: install elasticsearch + apt: + pkg: "elasticsearch={{ elasticsearch.version }}" + register: result + until: result|succeeded + retries: 5 + notify: + - restart elasticsearch-if-restart-enabled + +- name: install supporting pip packages + pip: + name: pyes + register: result + until: result|succeeded + retries: 5 + +- name: Configuring open file limits + lineinfile: dest=/etc/security/limits.conf regexp='^elasticsearch - nofile {{ elasticsearch.max_open_files }}' insertafter=EOF line='elasticsearch - nofile {{ elasticsearch.max_open_files }}' + when: elasticsearch.max_open_files is defined +- lineinfile: dest=/etc/security/limits.conf regexp='^elasticsearch - memlock {{ elasticsearch.max_locked_memory }}' insertafter=EOF line='elasticsearch - memlock {{ elasticsearch.max_locked_memory }}' + when: elasticsearch.max_locked_memory is defined +- lineinfile: dest=/etc/pam.d/su regexp='^session required pam_limits.so' insertafter=EOF line='session required pam_limits.so' +- lineinfile: dest=/etc/pam.d/common-session regexp='^session required pam_limits.so' insertafter=EOF line='session required pam_limits.so' +- lineinfile: dest=/etc/pam.d/common-session-noninteractive regexp='^session required pam_limits.so' insertafter=EOF line='session required pam_limits.so' +- lineinfile: dest=/etc/pam.d/sudo regexp='^session required pam_limits.so' insertafter=EOF line='session required pam_limits.so' + +#- name: install elasticsearch service +# template: +# src: etc/init.d/elasticsearch +# dest: /etc/init.d/elasticsearch +# mode: 0755 +# notify: +# - restart elasticsearch-if-restart-enabled + +- name: configure elasticsearch + template: src={{ item }} + dest=/{{ item }} mode=0644 + with_items: + - etc/elasticsearch/elasticsearch.yml + - etc/default/elasticsearch + notify: + - restart elasticsearch-if-restart-enabled + +- name: create elasticsearch data path + file: + path: "{{ elasticsearch.config['path.data'] }}" + owner: elasticsearch + group: elasticsearch + state: directory + mode: 0700 + when: elasticsearch.config['path.data'] is string + +- name: create elasticsearch data paths (from list) + file: + dest: "{{ item }}" + owner: elasticsearch + group: elasticsearch + state: directory + mode: 0700 + with_items: "{{ elasticsearch.config['path.data'] }}" + when: not elasticsearch.config['path.data'] is string + +- name: make elasticsearch plugin directory + file: + dest: /opt/elasticsearch-plugins + mode: 0755 + state: directory + tags: elasticsearch-plugins + +- name: get list of installed elasticsearch plugins + command: /usr/share/elasticsearch/bin/elasticsearch-plugin list + register: installed_plugins + changed_when: false + tags: elasticsearch-plugins + +- name: download elasticsearch plugin files via proxy + get_url: + url: "{{ item.url }}" + dest: "/opt/elasticsearch-plugins/{{ item.file }}" + mode: 0644 + sha256sum: "{{ item.sha256sum|default(omit) }}" + force: "{{ item.force|default(omit) }}" + with_items: "{{ elasticsearch.plugins }}" + register: result + until: result|succeeded + retries: 5 + when: + - item.name not in installed_plugins.stdout_lines + - item.url is defined + tags: elasticsearch-plugins + +- name: install elasticsearch plugins + shell: "yes | /usr/share/elasticsearch/bin/elasticsearch-plugin -s install file:///opt/elasticsearch-plugins/{{ item.file }}" + with_items: "{{ elasticsearch.plugins }}" + when: item.name not in installed_plugins.stdout_lines + tags: elasticsearch-plugins + notify: + - restart elasticsearch-if-restart-enabled + +- name: log rotation hint + template: src=etc/logrotate.d/elasticsearch dest=/etc/logrotate.d/elasticsearch mode=0644 + +- name: cron to delete logs rotated by elasticsearch + template: src=etc/cron.daily/elasticsearch dest=/etc/cron.daily/elasticsearch mode=0755 + +- name: allow elasticsearch traffic + ufw: rule=allow to_port={{ item.0.port }} src={{ item.1|default('127.0.0.1') }} + with_subelements: + - "{{ elasticsearch.firewall }}" + - src + tags: + - firewall + +- name: start elasticsearch + service: + name: elasticsearch + enabled: yes + notify: start elasticsearch-if-start-enabled + +- meta: flush_handlers + +- name: wait until elasticsearch is listening + wait_for: port=9200 + when: elasticsearch.wait_for_listening|bool + +- include: curator.yml + tags: curator + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec + +- meta: flush_handlers diff --git a/roles/elasticsearch/tasks/metrics.yml b/roles/elasticsearch/tasks/metrics.yml new file mode 100644 index 0000000..3b0523f --- /dev/null +++ b/roles/elasticsearch/tasks/metrics.yml @@ -0,0 +1,54 @@ +--- +- name: lay down elk-stats prediction files + template: src=opt/sitecontroller/{{ item }} + dest=/opt/sitecontroller/{{ item }} + mode=0775 owner=root group=sensu + with_items: + - scripts/elk-stats-collection.py + - scripts/elk-stats.py + - sensu-plugins/elk-stats-metrics.rb + tags: elk-stats + +- name: designate folder for elk stat file collection + file: + dest: /opt/sitecontroller/elk-stats-output + owner: root + group: sensu + state: directory + mode: 0775 + recurse: yes + tags: elk-stats + +- name: setup cronjob for elk-stats-collection + cron: + name: elk-stats-collection + job: '/usr/bin/python /opt/sitecontroller/scripts/elk-stats-collection.py' + minute: "0" + hour: "0,6,12,18" + tags: elk-stats + +- name: install sensu metrics checks + sensu_metrics_check: name="{{ item.name }}" plugin="{{ item.plugin }}" + interval="{{ item.interval|default(60) }}" args="{{ item.args|default('') }} + --scheme {{ monitoring_common.graphite.host_prefix }}.elasticsearch" + service_owner={{ monitoring_common.service_owner }} + with_items: + - name: metrics-es-cluster + plugin: metrics-es-cluster.rb + - name: metrics-es-node-graphite + plugin: metrics-es-node-graphite.rb + - name: elk-stats-metrics + plugin: /opt/sitecontroller/sensu-plugins/elk-stats-metrics.rb + interval: 21600 + args: "--retention {{ elasticsearch.curator.actions.delete_indices.older }}" + notify: restart sensu-client missing ok + tags : elk-stats + +- name: set up log rotation for elk-stats + logrotate: name=elk-stats path=/opt/sitecontroller/elk-stats-output/stats-summary* + args: + options: + - maxage 90 + - "rotate 0" + - missingok + tags: elk-stats diff --git a/roles/elasticsearch/tasks/serverspec.yml b/roles/elasticsearch/tasks/serverspec.yml new file mode 100644 index 0000000..360bc95 --- /dev/null +++ b/roles/elasticsearch/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests elasticsearch tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/elasticsearch/templates/etc/cron.daily/elasticsearch b/roles/elasticsearch/templates/etc/cron.daily/elasticsearch new file mode 100644 index 0000000..1465449 --- /dev/null +++ b/roles/elasticsearch/templates/etc/cron.daily/elasticsearch @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +find '{{ elasticsearch.config['path.logs'] }}' -type f -mtime +30 -name '*.log.*' -delete diff --git a/roles/elasticsearch/templates/etc/default/elasticsearch b/roles/elasticsearch/templates/etc/default/elasticsearch new file mode 100644 index 0000000..1b745b1 --- /dev/null +++ b/roles/elasticsearch/templates/etc/default/elasticsearch @@ -0,0 +1,35 @@ +# {{ ansible_managed }} + +# Run Elasticsearch as this user ID and group ID +ES_USER=elasticsearch +ES_GROUP=elasticsearch + +#ES_HEAP_SIZE={{ elasticsearch.heap_size }} + +# Heap new generation +#ES_HEAP_NEWSIZE= + +# max direct memory +#ES_DIRECT_SIZE= + +MAX_OPEN_FILES={{ elasticsearch.max_open_files }} + +# Maximum locked memory size. Set to "unlimited" if you use the +# bootstrap.mlockall option in elasticsearch.yml. You must also set +# ES_HEAP_SIZE. +MAX_LOCKED_MEMORY=unlimited + +# Maximum number of VMA (Virtual Memory Areas) a process can own +MAX_MAP_COUNT=262144 + +# Elasticsearch log directory +LOG_DIR={{ elasticsearch.config['path.logs'] }} + +# Elasticsearch configuration directory +CONF_DIR={{ elasticsearch.config['path.conf'] }} + +# Additional Java OPTS +ES_JAVA_OPTS="-Xms{{ elasticsearch.heap_size }} -Xmx{{ elasticsearch.heap_size }}" + +# Configure restart on package upgrade (true, every other setting will lead to not restarting) +RESTART_ON_UPGRADE=false diff --git a/roles/elasticsearch/templates/etc/elasticsearch/action.yml b/roles/elasticsearch/templates/etc/elasticsearch/action.yml new file mode 100644 index 0000000..1777d21 --- /dev/null +++ b/roles/elasticsearch/templates/etc/elasticsearch/action.yml @@ -0,0 +1,27 @@ +--- +actions: + 1: + action: {{ item.key }} + description: {{ item.value.description }} +{% if item.value.options is defined %} + options: +{% for key,value in item.value.options|dictsort %} + {{ key }}: {{ value }} +{% endfor %} +{% endif %} + filters: + - filtertype: pattern + kind: prefix +{% if item.key == 'delete_snapshots' %} + value: snapshot- +{% else %} + value: logstash- +{% endif %} + exclude: + - filtertype: age + source: name + direction: older + timestring: '%Y.%m.%d' + unit: days + unit_count: {{ item.value.older }} + exclude: diff --git a/roles/elasticsearch/templates/etc/elasticsearch/curator.yml b/roles/elasticsearch/templates/etc/elasticsearch/curator.yml new file mode 100644 index 0000000..88dd220 --- /dev/null +++ b/roles/elasticsearch/templates/etc/elasticsearch/curator.yml @@ -0,0 +1,20 @@ +--- +client: + hosts: + - 127.0.0.1 + port: 9200 + url_prefix: + use_ssl: False + certificate: + client_cert: + client_key: + ssl_no_validate: False + http_auth: + timeout: 30 + master_only: ${MASTER_ONLY:True} + +logging: + loglevel: INFO + logfile: + logformat: default + blacklist: ['elasticsearch', 'urllib3'] diff --git a/roles/elasticsearch/templates/etc/elasticsearch/elasticsearch.yml b/roles/elasticsearch/templates/etc/elasticsearch/elasticsearch.yml new file mode 100644 index 0000000..98cce4d --- /dev/null +++ b/roles/elasticsearch/templates/etc/elasticsearch/elasticsearch.yml @@ -0,0 +1,10 @@ +# {{ ansible_managed }} + +##################### ElasticSearch Configuration ##################### + +{{ elasticsearch.config | to_yaml(default_flow_style=False) }} + +{% for plugin in elasticsearch.plugins %}{% if plugin.config is defined %} +# ElasticSearch Plugin {{ plugin.name }} Config +{{ plugin.config | to_yaml(default_flow_style=False) }} +{% endif %}{% endfor %} diff --git a/roles/elasticsearch/templates/etc/init.d/elasticsearch b/roles/elasticsearch/templates/etc/init.d/elasticsearch new file mode 100644 index 0000000..9567301 --- /dev/null +++ b/roles/elasticsearch/templates/etc/init.d/elasticsearch @@ -0,0 +1,224 @@ +#!/bin/sh +# +# /etc/init.d/elasticsearch -- startup script for Elasticsearch +# +# Written by Miquel van Smoorenburg . +# Modified for Debian GNU/Linux by Ian Murdock . +# Modified for Tomcat by Stefan Gybas . +# Modified for Tomcat6 by Thierry Carrez . +# Additional improvements by Jason Brittain . +# Modified by Nicolas Huray for Elasticsearch . +# +### BEGIN INIT INFO +# Provides: elasticsearch +# Required-Start: $network $remote_fs $named +# Required-Stop: $network $remote_fs $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Starts elasticsearch +# Description: Starts elasticsearch using start-stop-daemon +### END INIT INFO + +PATH=/bin:/usr/bin:/sbin:/usr/sbin +NAME=elasticsearch +DESC="Elasticsearch Server" +DEFAULT=/etc/default/$NAME + +if [ `id -u` -ne 0 ]; then + echo "You need root privileges to run this script" + exit 1 +fi + + +. /lib/lsb/init-functions + +if [ -r /etc/default/rcS ]; then + . /etc/default/rcS +fi + +# The following variables can be overwritten in $DEFAULT + +# Run Elasticsearch as this user ID and group ID +ES_USER=elasticsearch +ES_GROUP=elasticsearch + +# The first existing directory is used for JAVA_HOME (if JAVA_HOME is not defined in $DEFAULT) +JDK_DIRS="/usr/lib/jvm/java-8-oracle/ /usr/lib/jvm/j2sdk1.8-oracle/ /usr/lib/jvm/jdk-7-oracle-x64 /usr/lib/jvm/java-7-oracle /usr/lib/jvm/j2sdk1.7-oracle/ /usr/lib/jvm/java-7-openjdk /usr/lib/jvm/java-7-openjdk-amd64/ /usr/lib/jvm/java-7-openjdk-armhf /usr/lib/jvm/java-7-openjdk-i386/ /usr/lib/jvm/default-java" + +# Look for the right JVM to use +for jdir in $JDK_DIRS; do + if [ -r "$jdir/bin/java" -a -z "${JAVA_HOME}" ]; then + JAVA_HOME="$jdir" + fi +done +export JAVA_HOME + +# Directory where the Elasticsearch binary distribution resides +ES_HOME=/usr/share/$NAME + +# Heap size defaults to 256m min, 1g max +# Set ES_HEAP_SIZE to 50% of available RAM, but no more than 31g +#ES_HEAP_SIZE=2g + +# Heap new generation +#ES_HEAP_NEWSIZE= + +# max direct memory +#ES_DIRECT_SIZE= + +# Additional Java OPTS +#ES_JAVA_OPTS= + +# Maximum number of open files +MAX_OPEN_FILES=65535 + +# Maximum amount of locked memory +#MAX_LOCKED_MEMORY= + +# Elasticsearch log directory +LOG_DIR=/var/log/$NAME + +# Elasticsearch work directory +WORK_DIR=/tmp/$NAME + +# Elasticsearch configuration directory +CONF_DIR=/etc/$NAME + +# Maximum number of VMA (Virtual Memory Areas) a process can own +MAX_MAP_COUNT=262144 + +# Path to the GC log file +#ES_GC_LOG_FILE=/var/log/elasticsearch/gc.log +# End of variables that can be overwritten in $DEFAULT + +# overwrite settings from default file +if [ -f "$DEFAULT" ]; then + . "$DEFAULT" +fi + +# Define other required variables +PID_FILE="$PID_DIR/$NAME.pid" +DAEMON=$ES_HOME/bin/elasticsearch +DAEMON_OPTS="-d -p $PID_FILE --default.path.home=$ES_HOME --default.path.logs=$LOG_DIR --default.path.work=$WORK_DIR --default.path.conf=$CONF_DIR" + +export ES_HEAP_SIZE +export ES_HEAP_NEWSIZE +export ES_DIRECT_SIZE +export ES_JAVA_OPTS + +# Check DAEMON exists +test -x $DAEMON || exit 0 + +checkJava() { + if [ -x "$JAVA_HOME/bin/java" ]; then + JAVA="$JAVA_HOME/bin/java" + else + JAVA=`which java` + fi + + if [ ! -x "$JAVA" ]; then + echo "Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME" + exit 1 + fi +} + +case "$1" in + start) + checkJava + + if [ -n "$MAX_LOCKED_MEMORY" -a -z "$ES_HEAP_SIZE" ]; then + log_failure_msg "MAX_LOCKED_MEMORY is set - ES_HEAP_SIZE must also be set" + exit 1 + fi + + log_daemon_msg "Starting $DESC" + + pid=`pidofproc -p $PID_FILE elasticsearch` + if [ -n "$pid" ] ; then + log_begin_msg "Already running." + log_end_msg 0 + exit 0 + fi + + # Prepare environment + mkdir -p "$LOG_DIR" "$WORK_DIR" && chown "$ES_USER":"$ES_GROUP" "$LOG_DIR" "$WORK_DIR" + + # Ensure that the PID_DIR exists (it is cleaned at OS startup time) + if [ -n "$PID_DIR" ] && [ ! -e "$PID_DIR" ]; then + mkdir -p "$PID_DIR" && chown "$ES_USER":"$ES_GROUP" "$PID_DIR" + fi + if [ -n "$PID_FILE" ] && [ ! -e "$PID_FILE" ]; then + touch "$PID_FILE" && chown "$ES_USER":"$ES_GROUP" "$PID_FILE" + fi + + if [ -n "$MAX_OPEN_FILES" ]; then + ulimit -n $MAX_OPEN_FILES + fi + + if [ -n "$MAX_LOCKED_MEMORY" ]; then + ulimit -l $MAX_LOCKED_MEMORY + fi + + if [ -n "$MAX_MAP_COUNT" -a -f /proc/sys/vm/max_map_count ]; then + sysctl -q -w vm.max_map_count=$MAX_MAP_COUNT + fi + + # Start Daemon + start-stop-daemon --start -b --user "$ES_USER" -c "$ES_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS + return=$? + if [ $return -eq 0 ] + then + i=0 + timeout=10 + # Wait for the process to be properly started before exiting + until { cat "$PID_FILE" | xargs kill -0; } >/dev/null 2>&1 + do + sleep 1 + i=$(($i + 1)) + if [ $i -gt $timeout ]; then + log_end_msg 1 + exit 1 + fi + done + else + log_end_msg $return + fi + ;; + stop) + log_daemon_msg "Stopping $DESC" + + if [ -f "$PID_FILE" ]; then + start-stop-daemon --stop --pidfile "$PID_FILE" \ + --user "$ES_USER" \ + --retry=TERM/20/KILL/5 >/dev/null + if [ $? -eq 1 ]; then + log_progress_msg "$DESC is not running but pid file exists, cleaning up" + elif [ $? -eq 3 ]; then + PID="`cat $PID_FILE`" + log_failure_msg "Failed to stop $DESC (pid $PID)" + exit 1 + fi + rm -f "$PID_FILE" + else + log_progress_msg "(not running)" + fi + log_end_msg 0 + ;; + status) + status_of_proc -p $PID_FILE elasticsearch elasticsearch && exit 0 || exit $? + ;; + restart|force-reload) + if [ -f "$PID_FILE" ]; then + $0 stop + sleep 1 + fi + $0 start + ;; + *) + log_success_msg "Usage: $0 {start|stop|restart|force-reload|status}" + exit 1 + ;; +esac + +exit 0 + diff --git a/roles/elasticsearch/templates/etc/logrotate.d/elasticsearch b/roles/elasticsearch/templates/etc/logrotate.d/elasticsearch new file mode 100644 index 0000000..92f5c62 --- /dev/null +++ b/roles/elasticsearch/templates/etc/logrotate.d/elasticsearch @@ -0,0 +1,2 @@ +# Due to limitations of log4j 1.2, files are rotated by elasticsearch +# but deleted by a cron job. See /etc/cron.daily/elasticsearch diff --git a/roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats-collection.py b/roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats-collection.py new file mode 100644 index 0000000..39f64cc --- /dev/null +++ b/roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats-collection.py @@ -0,0 +1,181 @@ +#!/usr/bin/python + +import datetime +import math +import json + +from collections import OrderedDict +from elasticsearch import Elasticsearch +es = Elasticsearch(timeout=30) + +index_data = {} +DATAFILE = '/opt/sitecontroller/elk-stats-output/elk-stats.json' +# determine what indices we have to work with +ls_indices = es.indices.get(index="logstash-2*") +cs_indices = es.indices.get(index="cleversafe-2*") +num_of_days = len(ls_indices) +# dict of all indices +all_indices = [] +all_indices.extend(ls_indices) +all_indices.extend(cs_indices) +all_indices = sorted(all_indices, key=lambda t: t[-10:]) +cluster_data = es.cluster.stats() +used_bytes = cluster_data["nodes"]["fs"]["total_in_bytes"] -\ + cluster_data["nodes"]["fs"]["free_in_bytes"] +reserve_total = used_bytes + cluster_data["nodes"]["fs"]["available_in_bytes"] + + +def construct_data(): + first_day = None + # for each index, get the stats we care about + for index in all_indices: + data = es.indices.stats(index=index) + + # filter out indices of absurdly small size + if data["indices"][index]["total"]["store"]["size_in_bytes"]\ + < 1000000: + continue + + # initialize index data set + date_title = index[-10:] + if date_title not in index_data: + index_data[date_title] = \ + { + "date": None, + "day_index": None, + "indices":{}, + "log(x)": None, + "total_debug_ratio": 0, + "total_host_count": 0, + "total_num_of_all_messages": 0, + "total_num_of_debug_messages": 0, + "total_size": 0, + "weekday": None + } + + # get day index num. + day = datetime.datetime.strptime( + (date_title).replace(".", "/"), '%Y/%m/%d' + ) + if first_day is None: + first_day = day + day_index = (day - first_day).days + 1 + weekday = day.isoweekday() + + # index date, number, and weekday, respectively + index_data[date_title]["date"] = date_title.replace(".", "/") + index_data[date_title]["day_index"] = day_index + index_data[date_title]["weekday"] = weekday + + # index log(x) function of index number + # Addition of 4 came about to avoid vertical asymptote, and shift + # right, giving overall smoothness. + # The division of the cubed root of x gave way to a function with + # horizontal asymptote at 0, continuous growth or reduction that + # diminishes over time, but is never eliminated. + # In my opinion, better than simple linear analysis of variables + # since this will adjust, rather than over or under predict. + index_data[date_title]["log(x)"] = \ + (math.log10(index_data[date_title]["day_index"] + 4)) / \ + (math.pow(index_data[date_title]["day_index"], 1.0/3.0)) + + index_data[date_title]["indices"][index] = {} + + # size in bytes of index + index_data[date_title]["indices"][index]["size"] = \ + data["indices"][index]["total"]["store"]["size_in_bytes"] + index_data[date_title]["total_size"] += \ + index_data[date_title]["indices"][index]["size"] + + # ES queries to find host count on given day + # Searches placed in 'queries' long-to-short (in time) + # Timeout relative to size of cluster + timeout = 10.0 + ((used_bytes / math.pow(1024, 4.0)) * 2.5) + # queries: + # 1) all messages, 2) "DEBUG" messages, 3) "TRACE" messages, 4) host # + # msearch requires empy queries at beginning and between each. + queries = [ + {}, + { + "query": { + "match_all": {} + }, + "timeout":timeout + }, + {}, + { + "query": { + "match": { + "message": "DEBUG" + } + }, + "timeout": timeout + }, + {}, + { + "query": { + "match": { + "message": "TRACE" + } + }, + "timeout": timeout + }, + {}, + { + "aggs": { + "host_count": { + "cardinality": { + "field": "host" + } + } + }, + "timeout": timeout + } + ] + data = es.msearch(index=str(index), body=queries, search_type="query_then_fetch") + + # the responses from the multiple queries + data = data["responses"] + num_of_all_messages = data[0]['hits']['total'] + + # Debug messages + Trace messages + index_data[date_title]["indices"][index]["num_of_debug_messages"] = \ + data[1]['hits']['total'] + data[2]['hits']['total'] + index_data[date_title]["total_num_of_debug_messages"] += \ + index_data[date_title]["indices"][index]["num_of_debug_messages"] + + # index host count + index_data[date_title]["indices"][index]["host_count"] = \ + data[3]["aggregations"]["host_count"]["value"] + index_data[date_title]["total_host_count"] += \ + index_data[date_title]["indices"][index]["host_count"] + + # All messages + index_data[date_title]["indices"][index]["num_of_all_messages"] = num_of_all_messages + index_data[date_title]["total_num_of_all_messages"] += \ + index_data[date_title]["indices"][index]["num_of_all_messages"] + + debug_ratio = \ + float(index_data[date_title]["indices"][index]["num_of_debug_messages"]) / \ + float(num_of_all_messages) + index_data[date_title]["indices"][index]["debug_ratio"] = debug_ratio + index_data[date_title]["total_debug_ratio"] = \ + float(index_data[date_title]["total_num_of_debug_messages"]) / \ + float(index_data[date_title]["total_num_of_all_messages"]) + + cluster_data = {} + cluster_data["reserve_total"] = reserve_total + cluster_data["used_bytes"] = used_bytes + # it puts the index in its file... + with open(DATAFILE, 'w') as f: + json.dump([index_data,cluster_data], f, sort_keys=True, indent=4, + separators=(',', ': ')) + return + + +def main(): + construct_data() + + +if __name__ == "__main__": + main() diff --git a/roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats.py b/roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats.py new file mode 100644 index 0000000..b374e17 --- /dev/null +++ b/roles/elasticsearch/templates/opt/sitecontroller/scripts/elk-stats.py @@ -0,0 +1,257 @@ +#!/usr/bin/python + +import json +import math +import numpy as np +import os +import sys, getopt + +from collections import OrderedDict + +matrixA = None +matrixB = None +coeffs = None +reserve_total = None +used_bytes = None +last_index = None +last_weekday = None +last_host_count = None +last_debug_ratio = None +last_num_all_msg = None + +desired_hostnum = 50 +days_ahead = 90 +retention_days = 270 + +def calculate_coeffs(): + global matrixA, matrixB, coeffs, reserve_total, used_bytes, \ + last_debug_ratio, last_num_all_msg + # retrieve file that was created in collection script + with open('/opt/sitecontroller/elk-stats-output/elk-stats.json', 'r') as f: + data = json.load(f) + reserve_total = data[1]["reserve_total"] + used_bytes = data[1]["used_bytes"] + data = sorted(data[0].iteritems(), key=lambda x: x[1]) + row_length = len(data) + matrixA = np.ones(shape=(row_length, 5)) + matrixB = np.zeros(shape=(row_length, 1)) + is_singular = True + for i, index in enumerate(data): + matrixA[i][1] = data[i][1]["day_index"] + matrixA[i][2] = data[i][1]["weekday"] + matrixA[i][3] = data[i][1]["total_host_count"] + matrixA[i][4] = data[i][1]["log(x)"] + matrixB[i] = data[i][1]["total_size"] + if is_singular is True and i != 0: + if matrixA[i][3] != matrixA[i-1][3]: + is_singular = False + + last_debug_ratio = data[row_length-2][1]["total_debug_ratio"] + last_num_all_msg = data[row_length-2][1]["total_num_of_all_messages"] + if is_singular and row_length > 2: + if matrixB[0] <= matrixB[1]: + matrixA[0][3] -= 0.01 + else: + matrixA[0][3] += 0.01 + + # remove current day's index; it isn't complete + matrixA = matrixA[:-1] + matrixB = matrixB[:-1] + matrixAt = np.transpose(matrixA) + matrixAtA = np.dot(matrixAt, matrixA) + matrixAtB = np.dot(matrixAt, matrixB) + matrixInverseAtA = np.linalg.inv(matrixAtA) + + # these are the magic numbers + coeffs = np.dot(matrixInverseAtA, matrixAtB) + + return matrixA, matrixB, coeffs + + +def predict(): + global last_index, last_weekday, last_host_count + day_index_vector_sum = 0 + weekday_vector_sum = 0 + function_vector_sum = 0 + current_sum = 0 + last_index = matrixA[-1, 1] + last_weekday = matrixA[-1, 2] + last_host_count = matrixA[-1, 3] + + # create data points for future indices :: days_ahead, retention, last_index + i = 0 + num_of_predicted = 0 + if (last_index + days_ahead - retention_days) < last_index: + while (matrixA[i,1] <= (last_index + days_ahead - retention_days)): + i += 1 + else: + i = last_index + days_ahead - retention_days + for j in range(int(i), int(last_index + days_ahead + 1)): + if j < matrixB.size: + current_sum += matrixB[j] + else: + day_index_vector_sum += j + weekday_vector_sum += ((j + last_weekday) % 7) + 1 + function_vector_sum += (math.log10(j + 4) / math.pow(j, 1.0/3.0)) + num_of_predicted += 1 + + # (1) prediction of total bytes used based on last_host_count + # days into the future + future_sum = \ + current_sum + \ + coeffs[0] * num_of_predicted + \ + coeffs[1] * day_index_vector_sum + \ + coeffs[2] * weekday_vector_sum + \ + coeffs[3] * last_host_count * num_of_predicted + \ + coeffs[4] * function_vector_sum + # This is a fail-safe should the prediction be negative (impossible). + # The trend which leads to negative values implies the sizes are only + # being reduced. To avoid these nonsensical values, we can at least + # say that they will be less than or equal to the current total size + # of the cluster. To reduce risk, we assume that it will remain constant, + # it current projected local maximum. + days_at_future_point = retention_days + if retention_days > days_ahead + last_index: + days_at_future_point = days_ahead + last_index + if future_sum[0] < (days_at_future_point * (math.pow(1024.0, 3.0) / 10.0)): + future_sum = np.array([used_bytes]) + + # (2) prediction of number of hosts we are able to maintain with current + # storage + host_projection = \ + reserve_total - current_sum - \ + coeffs[0] * num_of_predicted - \ + coeffs[1] * day_index_vector_sum - \ + coeffs[2] * weekday_vector_sum - \ + coeffs[4] * function_vector_sum + host_projection = host_projection / (coeffs[3] * num_of_predicted) + # A nonnegative number is the only realistic value here + if host_projection < 0: + host_projection = np.array([0]) + + # Fail-safe for unusually large value for hosts sustainable. + # Currently set at 1000, subject to change if/when we're amazing. + if host_projection > 1000: + host_projection = np.array([1000]) + + # (3) prediction of total bytes used based on desired_hostnum + # (similar to 1, kept separate for output) + host_sum = \ + current_sum + \ + coeffs[0] * num_of_predicted + \ + coeffs[1] * day_index_vector_sum + \ + coeffs[2] * weekday_vector_sum + \ + coeffs[3] * desired_hostnum * num_of_predicted + \ + coeffs[4] * function_vector_sum + # same fail-safe case as (1) + if host_sum < (days_at_future_point * (math.pow(1024.0, 3.0) / 8.0)): + host_sum = np.array([used_bytes]) + + # (4) prediction of day left until max capacity will be reached + remaining_sum = reserve_total - used_bytes + j = last_index + 1 # pointer for last predicted index number + i = 0 # pointer for first index or + # last index minus retention (existing or predicted) + + while (remaining_sum > 0): # calculates rolling total + if j > retention_days: + if i < matrixB.size: + remaining_sum += matrixB[i] + else: + remaining_sum += evaluate_point(i) + i += 1 + remaining_sum -= evaluate_point(j) + j += 1 + if j > 1000: + remaining_sum = "1000" + break + if remaining_sum != "1000": + remaining_sum = j - last_index - 2 + + return future_sum, host_projection, host_sum, remaining_sum + + +def evaluate_point(p): # evaluates prediction function at single point + evaluated_point = ( + coeffs[0] + \ + coeffs[1] * p + \ + coeffs[2] * (((p + last_weekday) % 7) + 1) + \ + coeffs[3] * last_host_count + \ + coeffs[4] * (math.log10(p + 4) / math.pow(p, 1.0/3.0)) + ) + return evaluated_point + + +def main(argv): + global desired_hostnum, days_ahead, retention_days + days = "" + hosts = "" + outfile = "/opt/sitecontroller/elk-stats-output/elk-stats-summary.json" + retention = "" + try: + opts, args = getopt.getopt(argv,"hd:n:f:r:",["days=","hosts=","outfile=","retention="]) + except getopt.GetoptError: + print 'elk-stats.py -d -n \ +-r -f ' + sys.exit(2) + for opt, arg in opts: + if opt == '-h': + print 'elk-stats.py -d -n \ +-r -f ' + sys.exit() + elif opt in ("-d", "--days"): + days = arg + elif opt in ("-n", "--hosts"): + hosts = arg + elif opt in ("-r", "--retention"): + retention = arg + elif opt in ("-f", "--file"): + outfile = arg + + # calculate coefficients for prediction model + calculate_coeffs() + + # number of hosts to predict size with + if days is not "": + days_ahead = int(days) + if hosts is not "": + desired_hostnum = int(hosts) + if retention is not "": + retention_days = int(retention) + if outfile[-5:] != ".json": + outfile = outfile + '.json' + # calculate estimations using prediction model + future_sum, host_projection, host_sum, remaining_sum = \ + predict() + terabyte = math.pow(1024, 4.0) + gigabyte = math.pow(1024, 3.0) + future_sum = future_sum[0] / terabyte + host_sum = host_sum[0] / terabyte + output = [ + ("Current Projected Size", future_sum), + ("Desired-Host Projected Size", host_sum), + ("Max Hosts Sustainable", host_projection[0]), + ("Days Remaining Until Max Capacity Reached", remaining_sum), + ("Coefficients", coeffs.flatten().tolist()), + ("Last Index Number", last_index), + ("Last Weekday", last_weekday), + ("Last Host Count", last_host_count), + ("Last Index Size", matrixB[-1][0] / gigabyte), + ("Last Number of All Messages in a Day", last_num_all_msg), + ("Last Debug Ratio", last_debug_ratio), + ("Retention Days", retention_days) + ] + + # output json and stdout summary + summary = {} + for pair in output: + summary[pair[0]] = pair[1] + with open(outfile, 'w') as f: + json.dump(summary, f, indent=4, sort_keys=True, separators=(',', ':')) + f.close() + + return summary + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/roles/elasticsearch/templates/opt/sitecontroller/sensu-plugins/elk-stats-metrics.rb b/roles/elasticsearch/templates/opt/sitecontroller/sensu-plugins/elk-stats-metrics.rb new file mode 100644 index 0000000..811063a --- /dev/null +++ b/roles/elasticsearch/templates/opt/sitecontroller/sensu-plugins/elk-stats-metrics.rb @@ -0,0 +1,54 @@ +#! /usr/bin/env ruby +#metrics for elk-stats prediction + +require 'sensu-plugin/metric/cli' +require 'socket' +require 'json' + +class ELKPrediction < Sensu::Plugin::Metric::CLI::Graphite + option :scheme, + description: 'Metric naming scheme, text to prepend to .$parent.$child', + long: '--scheme SCHEME', + default: Socket.gethostname.to_s + option :days, + description: 'Days to predict ahead of time.', + long: '--days-ahead DAYS', + default: 90 + option :hosts, + description: 'Number of hosts to predict size with.', + long: '--hosts', + default: 50 + option :retention, + description: 'Retention rate set in Elasticsearch configuration.', + long: '--retention RETENTION', + default: 270 + + + def run + timestamp = Time.now.to_i + filename = "/opt/sitecontroller/elk-stats-output/stats-summary-%s.json" % timestamp + statement = "/usr/bin/python /opt/sitecontroller/scripts/elk-stats.py -d %s -n %s -f %s -r %s" \ + % [config[:days], config[:hosts], filename, config[:retention]] + result = `#{statement}` + file = File.read(filename) + data_hash = JSON.parse(file) + projection = data_hash["Current Projected Size"] + desired_host_projection = data_hash["Desired-Host Projected Size"] + hosts_sustainable = data_hash["Max Hosts Sustainable"] + days_remaining = data_hash["Days Remaining Until Max Capacity Reached"] + last_index_size = data_hash["Last Index Size"] + last_num_all_msg = data_hash["Last Number of All Messages in a Day"] + last_debug_ratio = data_hash["Last Debug Ratio"] + last_host_count = data_hash["Last Host Count"] + + output [config[:scheme], 'projection'].join('.'), projection, timestamp + output [config[:scheme], 'desired_host_projection'].join('.'), desired_host_projection, timestamp + output [config[:scheme], 'hosts_sustainable'].join('.'), hosts_sustainable, timestamp + output [config[:scheme], 'days_remaining'].join('.'), days_remaining, timestamp + output [config[:scheme], 'last_index_size'].join('.'), last_index_size, timestamp + output [config[:scheme], 'last_num_all_msg'].join('.'), last_num_all_msg, timestamp + output [config[:scheme], 'last_debug_ratio'].join('.'), last_debug_ratio, timestamp + output [config[:scheme], 'last_host_count'].join('.'), last_host_count, timestamp + exit + end +end diff --git a/roles/elasticsearch/templates/serverspec/elasticsearch_spec.rb b/roles/elasticsearch/templates/serverspec/elasticsearch_spec.rb new file mode 100644 index 0000000..550713a --- /dev/null +++ b/roles/elasticsearch/templates/serverspec/elasticsearch_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe package('elasticsearch') do + it { should be_installed.by('apt') } #ELS001 +end + +describe file('/etc/security/limits.conf') do +{% if elasticsearch.max_open_files is defined %} + its(:content) { should contain('elasticsearch - nofile {{ elasticsearch.max_open_files }}') } #ELS002 +{% endif %} +{% if elasticsearch.max_locked_memory is defined %} + its(:content) { should contain('elasticsearch - memlock {{ elasticsearch.max_locked_memory }}') } #ELS003 +{% endif %} +end + +files = ['su','common-session', 'common-session-noninteractive', 'sudo'] +files.each do |file| + describe file("/etc/pam.d/#{ file }") do + it { should be_file } #ELS004 + its(:content) { should contain('session required pam_limits.so') } #ELS005 + end +end + +describe file('/etc/init.d/elasticsearch') do + it { should be_file } #ELS006 +end + +describe file('/etc/elasticsearch/elasticsearch.yml') do + it { should be_mode 644 } #ELS008 + it { should be_file } #ELS009 +end + +describe file('/etc/default/elasticsearch') do + it { should be_mode 644 } #ELS010 + it { should be_file } #ELS011 +end + +{% for item in elasticsearch.firewall %} +describe port('{{ item.port }}') do + it { should be_listening } #ELS012 +end +{% endfor %} + +describe package('elasticsearch-curator') do + it { should be_installed.by('pip') } #ELS013 +end + +describe file('/usr/bin/curator') do + it { should be_file } #ELS014 + it { should be_linked_to '/usr/local/bin/curator' } #ELS015 +end + +describe package('elasticsearch') do + it { should be_installed } +end + +describe service('elasticsearch') do + it { should be_enabled } +end diff --git a/roles/file-mirror/defaults/main.yml b/roles/file-mirror/defaults/main.yml new file mode 100644 index 0000000..7fddff6 --- /dev/null +++ b/roles/file-mirror/defaults/main.yml @@ -0,0 +1,61 @@ +--- +file_mirror: + mirror_location: '/opt/file_mirror/files' + +# Files to mirror + files: [] + +# Files/Directories to remove +# should be relative path from `mirror_location` + remove: [] + +# Swift files to mirror +# ENV vars to push to swift + swift: + clouds: {} +# softlayer: +# ST_AUTH: https://dal05.objectstorage.softlayer.net/auth/v1.0/ +# ST_USER: dsr235r3jdk32rtf23 +# ST_KEY: cfgfrfjkj4lk2gt423 +# Swift objects to donwload + objects: [] +# - file: secfixdb.Ubuntu14.04 +# container: lssecfix +# cloud: softlayer + auth: [] + apache: + enabled: true + http_redirect: False + servername: mirror01.local + serveraliases: + - mirror01 + port: 80 + ip: '*' + ssl: + enabled: False + port: 443 + ip: '*' + name: sitecontroller + cert: ~ + key: ~ + intermediate: ~ + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/upstart/file_mirror.log + fields: + tags: mirror,file-mirror + - paths: + - /var/log/apache2/file_mirror-access.log + fields: + tags: mirror,apache_access + - paths: + - /var/log/apache2/file_mirror-error.log + fields: + tags: mirror,apache_error + logging: + forwarder: filebeat diff --git a/roles/file-mirror/meta/main.yml b/roles/file-mirror/meta/main.yml new file mode 100644 index 0000000..1ac7588 --- /dev/null +++ b/roles/file-mirror/meta/main.yml @@ -0,0 +1,17 @@ +--- +dependencies: + - role: bbg-ssl + name: "{{ file_mirror.apache.ssl.name }}" + ssl_cert: "{{ file_mirror.apache.ssl.cert }}" + ssl_key: "{{ file_mirror.apache.ssl.key }}" + ssl_intermediate: "{{ file_mirror.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: file_mirror.apache.ssl.enabled + tags: ['bbg-ssl'] + - role: apache + - role: logging-config + service: file_mirror + logdata: "{{ file_mirror.logs }}" + forward_type: "{{ file_mirror.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/file-mirror/tasks/apache.yml b/roles/file-mirror/tasks/apache.yml new file mode 100644 index 0000000..bf853ed --- /dev/null +++ b/roles/file-mirror/tasks/apache.yml @@ -0,0 +1,23 @@ +--- +- name: enable apache mods for file-mirror + apache2_module: name={{ item }} + with_items: + - proxy_http + - rewrite + - headers + +- name: add file_mirror apache vhost + template: src=etc/apache2/sites-available/file_mirror + dest=/etc/apache2/sites-available/file_mirror.conf + notify: + - restart apache + +- name: enable file_mirror vhost + apache2_site: state=enabled name=file_mirror + notify: + - restart apache + +- meta: flush_handlers + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes diff --git a/roles/file-mirror/tasks/checks.yml b/roles/file-mirror/tasks/checks.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/file-mirror/tasks/checks.yml @@ -0,0 +1 @@ +--- diff --git a/roles/file-mirror/tasks/main.yml b/roles/file-mirror/tasks/main.yml new file mode 100644 index 0000000..2519cc8 --- /dev/null +++ b/roles/file-mirror/tasks/main.yml @@ -0,0 +1,99 @@ +--- +- name: install python swift client + pip: name=python-swiftclient + +- name: create mirror user + user: name=mirror comment=mirror shell=/bin/false + system=yes home=/nonexistent + +- name: create file mirror location + file: dest={{ file_mirror.mirror_location }} state=directory + owner=mirror recurse=true + +- name: remove unwanted files + file: dest="{{ file_mirror.mirror_location }}/{{ item }}" state=absent + with_items: "{{ file_mirror.remove }}" + +- name: ensure file mirror path exists for web files + file: dest="{{ file_mirror.mirror_location }}/{{ item.path|default('misc') }}" + state=directory + owner=mirror recurse=true + with_items: "{{ file_mirror.files }}" + +- name: ensure file mirror path exists for swift files + file: dest="{{ file_mirror.mirror_location }}/{{ item.cloud }}/{{ item.container }}" + state=directory + owner=mirror recurse=true + with_items: "{{ file_mirror.swift.objects }}" + +- name: create .htpasswd + htpasswd: name={{ item.username }} password={{ item.password }} + path="{{ file_mirror.mirror_location }}/{{ item.path }}/.htpasswd" + with_items: "{{ file_mirror.auth }}" + +- name: create .htaccess + template: src=etc/apache2/htaccess + dest="{{ file_mirror.mirror_location }}/{{ item.path }}/.htaccess" + with_items: "{{ file_mirror.auth }}" + +- name: download files via proxy + get_url: + url: "{{ item.url }}" + dest: "{{ file_mirror.mirror_location }}/{{ item.path|default('misc') }}/{{ item.name }}" + mode: 0644 + sha256sum: "{{ item.sha256sum|default(omit) }}" + force: "{{ item.force|default(omit) }}" + with_items: "{{ file_mirror.files }}" + environment: proxy_env + when: proxy_env is defined + +- name: download files directly + get_url: + url: "{{ item.url }}" + dest: "{{ file_mirror.mirror_location }}/{{ item.path|default('misc') }}/{{ item.name }}" + mode: 0644 + sha256sum: "{{ item.sha256sum|default(omit) }}" + force: "{{ item.force|default(omit) }}" + with_items: "{{ file_mirror.files }}" + when: proxy_env is not defined + +- name: download files from swift (force) + command: "swift download {{ item.container }} {{ item.file }}" + args: + chdir: "{{ file_mirror.mirror_location }}/{{ item.cloud }}/{{ item.container }}" + when: item.force|default("False")|bool + with_items: "{{ file_mirror.swift.objects }}" + environment: "{{ file_mirror.swift['clouds'][item.cloud] }}" + +- name: download files from swift + command: "swift download {{ item.container }} {{ item.file }}" + args: + chdir: "{{ file_mirror.mirror_location }}/{{ item.cloud }}/{{ item.container }}" + creates: "{{ file_mirror.mirror_location }}/{{ item.cloud }}/{{ item.container }}/{{ item.file }}" + when: not item.force|default("False")|bool + with_items: "{{ file_mirror.swift.objects }}" + environment: "{{ file_mirror.swift['clouds'][item.cloud] }}" + +- include: apache.yml + when: file_mirror.apache.enabled|bool + +- name: allow file-mirror traffic + ufw: rule=allow + to_port={{ item.port }} + src={{ item.src }} + proto={{ item.protocol }} + with_items: "{{ file_mirror.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/file-mirror/tasks/metrics.yml b/roles/file-mirror/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/file-mirror/tasks/serverspec.yml b/roles/file-mirror/tasks/serverspec.yml new file mode 100644 index 0000000..d9184d3 --- /dev/null +++ b/roles/file-mirror/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for file-mirror role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/file-mirror/templates/etc/apache2/htaccess b/roles/file-mirror/templates/etc/apache2/htaccess new file mode 100644 index 0000000..10b437a --- /dev/null +++ b/roles/file-mirror/templates/etc/apache2/htaccess @@ -0,0 +1,6 @@ +# {{ ansible_managed }} + +AuthType Basic +AuthName "Authentication Required" +AuthUserFile "{{ file_mirror.mirror_location }}/{{ item.path }}/.htpasswd" +Require valid-user diff --git a/roles/file-mirror/templates/etc/apache2/sites-available/file_mirror b/roles/file-mirror/templates/etc/apache2/sites-available/file_mirror new file mode 100644 index 0000000..c58907e --- /dev/null +++ b/roles/file-mirror/templates/etc/apache2/sites-available/file_mirror @@ -0,0 +1,48 @@ +# {{ ansible_managed }} + +{% macro virtualhost() %} + ServerAdmin openstack@bluebox.net + ServerName {{ file_mirror.apache.servername }} + ServerAlias {{ file_mirror.apache.serveraliases|join(" ") }} + DocumentRoot {{ file_mirror.mirror_location }} + ErrorLog ${APACHE_LOG_DIR}/file_mirror-error.log + CustomLog ${APACHE_LOG_DIR}/file_mirror-access.log combined + FileETag MTime Size + Header set Cache-Control public + RewriteEngine On + RewriteCond {{ file_mirror.mirror_location }}/%{REQUEST_FILENAME} !-f + RewriteCond {{ file_mirror.mirror_location }}/%{REQUEST_FILENAME} !-d + + Options Indexes + AllowOverride All + Require all granted + +{% endmacro %} + +{% if file_mirror.apache.ssl.enabled|bool and file_mirror.apache.http_redirect|bool %} + + ServerName {{ file_mirror.apache.servername }} + ServerAlias {{ file_mirror.apache.serveraliases|join(" ") }} + RewriteEngine on + RewriteCond %{HTTPS} !=on + RewriteRule ^(.*)$ https://%{HTTP_HOST}:{{ file_mirror.apache.ssl.port }}$1 [R=301,L] + +{% else %} + +{{ virtualhost() }} + +{% endif %} + +{% if file_mirror.apache.ssl.enabled|bool %} + + {{ apache.ssl.settings }} + SSLCertificateFile /etc/ssl/certs/{{ file_mirror.apache.ssl.name|default('sitecontroller') }}.crt + SSLCertificateKeyFile /etc/ssl/private/{{ file_mirror.apache.ssl.name|default('sitecontroller') }}.key +{% if bbg_ssl.intermediate or file_mirror.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ file_mirror.apache.ssl.name|default('sitecontroller') }}-intermediate.crt +{% endif %} +{% else %} + +{% endif %} +{{ virtualhost() }} + diff --git a/roles/file-mirror/templates/serverspec/file_mirror_spec.rb b/roles/file-mirror/templates/serverspec/file_mirror_spec.rb new file mode 100644 index 0000000..765ec61 --- /dev/null +++ b/roles/file-mirror/templates/serverspec/file_mirror_spec.rb @@ -0,0 +1,42 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe user('mirror') do + it { should exist } + it { should have_home_directory '/nonexistent' } + it { should have_login_shell '/bin/false' } +end + +['proxy_http', 'rewrite', 'headers'].each do |file| + describe file("/etc/apache2/mods-available/#{file}.load") do + it { should exist } + end + describe file("/etc/apache2/mods-enabled/#{file}.load") do + it { should be_symlink } + end +end + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end + +describe file('/etc/apache2/sites-available/file_mirror.conf') do + it { should be_file } +end + +describe file('/etc/apache2/sites-enabled/file_mirror.conf') do + it { should be_symlink } +end + +describe port('{{ file_mirror.apache.port }}') do + it { should be_listening } +end + +describe iptables do + it { should have_rule('-p tcp -m tcp --dport {{ file_mirror.apache.port }} -j ACCEPT') } +end diff --git a/roles/flapjack/defaults/main.yml b/roles/flapjack/defaults/main.yml new file mode 100644 index 0000000..ab823ca --- /dev/null +++ b/roles/flapjack/defaults/main.yml @@ -0,0 +1,69 @@ +--- +flapjack: + firewall: + - port: 3080 + src: 127.0.0.1 + - port: 3081 + src: 127.0.0.1 + - port: 3090 + src: 127.0.0.1 + when: flapjack.receivers.httpbroker.enabled|bool + dashboard: + address: 0.0.0.0 + port: 3080 + base_url: "http://monitor.local:3080/" + auto_refresh: 120 + api: + address: 0.0.0.0 + port: 3081 + base_url: "http://monitor.local:3081/" + receivers: + httpbroker: + enabled: True + port: 3090 + db: 0 + interval: 10s + debug: False + notifications: + pagerduty: + enabled: no + slack: + enabled: no + account_sid: "webhookbot" + endpoint: "https://hooks.slack.com/services/xxx/yyy/" + auth_token: "zzzzz" + email: + enabled: no + from: "Flapjack Notification " + host: smtp.example.com + port: 25 + starttls: false + auth: + enabled: false + type: ~ + username: ~ + password: ~ + jabber: + enabled: false + server: ~ + port: 5222 + username: ~ + password: ~ + rooms: [] + webhook: + enabled: false + hooks: [] + # EXAMPLE: + # - url: "http://127.0.0.1/flapjack_notification" + # timeout: 30 + # "out-of-band" end-to-end testing, used for monitoring other instances of + # flapjack to ensure that they are running correctly + oobetet: + enabled: false + redis: + host: 127.0.0.1 + port: 6380 + db: 0 + logrotate: + frequency: daily + rotations: 7 diff --git a/roles/flapjack/handlers/main.yml b/roles/flapjack/handlers/main.yml new file mode 100644 index 0000000..47f41ed --- /dev/null +++ b/roles/flapjack/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: restart flapjack + service: name=flapjack state=restarted + +- name: reload flapjack + service: name=flapjack state=reloaded + +- name: restart flapjack-httpbroker + service: name=flapjack-httpbroker state=restarted + +- name: restart postfix + service: name=postfix state=restarted diff --git a/roles/flapjack/meta/main.yml b/roles/flapjack/meta/main.yml new file mode 100644 index 0000000..403724b --- /dev/null +++ b/roles/flapjack/meta/main.yml @@ -0,0 +1,7 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.flapjack.repo }} trusty main' + key_url: '{{ apt_repos.flapjack.key_url }}' + - role: sensu-check diff --git a/roles/flapjack/tasks/checks.yml b/roles/flapjack/tasks/checks.yml new file mode 100644 index 0000000..6b1bdb5 --- /dev/null +++ b/roles/flapjack/tasks/checks.yml @@ -0,0 +1,9 @@ +--- +- name: install flapjack process check + sensu_check_dict: name="check-flapjack-process" check="{{ sensu_checks.flapjack.check_flapjack_process }}" + notify: restart sensu-client missing ok + +- name: install flapjack process check + sensu_check_dict: name="check-flapjack-httpbroker-process" check="{{ sensu_checks.flapjack.check_flapjack_httpbroker_process }}" + notify: restart sensu-client missing ok + when: flapjack.receivers.httpbroker.enabled|bool diff --git a/roles/flapjack/tasks/email.yml b/roles/flapjack/tasks/email.yml new file mode 100644 index 0000000..bbeec94 --- /dev/null +++ b/roles/flapjack/tasks/email.yml @@ -0,0 +1,6 @@ +- name: install postfix for flapjack email routing + apt: pkg=postfix + +- name: configure postfix to only listen on loopback + lineinfile: dest=/etc/postfix/main.cf regexp="^inet_interfaces" line="inet_interfaces = loopback-only" + notify: restart postfix diff --git a/roles/flapjack/tasks/main.yml b/roles/flapjack/tasks/main.yml new file mode 100644 index 0000000..7535906 --- /dev/null +++ b/roles/flapjack/tasks/main.yml @@ -0,0 +1,87 @@ +--- +- name: set 14.04 ruby version + set_fact: ruby_version=2.0 + when: ansible_distribution_version == "14.04" + +- name: set 16.04 ruby version + set_fact: ruby_version=2.3 + when: ansible_distribution_version == "16.04" + +- name: install system packages + apt: pkg={{ item }} + with_items: + - ruby{{ ruby_version }} + - ruby{{ ruby_version }}-dev + - flapjack + - flapjack-admin + +- name: install flapjack-diner gem + gem: + name: flapjack-diner + version: '~> 1.4.0' + executable: /usr/bin/gem{{ ruby_version }} + user_install: no + register: result + until: result|succeeded + retries: 5 + +- name: create config dirs + file: dest=/etc/flapjack state=directory owner=root mode=0755 + +- name: install config + template: src=etc/flapjack/flapjack_config.yaml + dest=/etc/flapjack/flapjack_config.yaml + notify: + - restart flapjack + +- name: set up log rotation for flapjack + logrotate: name=flapjack path=/var/log/flapjack/*.log + args: + options: + - "{{ flapjack.logrotate.frequency }}" + - "rotate {{ flapjack.logrotate.rotations }}" + - missingok + - compress + - copytruncate + - notifempty + +- name: httpbroker receiver service + upstart_service: name=flapjack-httpbroker + cmd=/opt/flapjack/bin/httpbroker-nocache + args="--port={{ flapjack.receivers.httpbroker.port }} --interval={{ flapjack.receivers.httpbroker.interval }} --database={{ flapjack.receivers.httpbroker.db }}{% if flapjack.receivers.httpbroker.debug %} --debug{% endif %}" + user=flapjack + notify: restart flapjack-httpbroker + when: flapjack.receivers.httpbroker.enabled|bool + +- meta: flush_handlers + +- name: ensure redis-flapjack is running + service: name=redis-flapjack state=started enabled=yes + +- name: ensure flapjack is running + service: name=flapjack state=started enabled=yes + +- name: ensure flapjack httpbroker receiver is running + service: name=flapjack-httpbroker state=started enabled=yes + when: flapjack.receivers.httpbroker.enabled|bool + +- name: permit flapjack traffic + ufw: rule=allow to_port={{ item.port }} proto=tcp src={{ item.src }} + with_items: "{{ flapjack.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec + +- include: email.yml + when: flapjack.notifications.email.enabled|bool diff --git a/roles/flapjack/tasks/metrics.yml b/roles/flapjack/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/flapjack/tasks/serverspec.yml b/roles/flapjack/tasks/serverspec.yml new file mode 100644 index 0000000..1d45d7f --- /dev/null +++ b/roles/flapjack/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests flapjack tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/flapjack/templates/etc/flapjack/flapjack_config.yaml b/roles/flapjack/templates/etc/flapjack/flapjack_config.yaml new file mode 100644 index 0000000..3c55755 --- /dev/null +++ b/roles/flapjack/templates/etc/flapjack/flapjack_config.yaml @@ -0,0 +1,207 @@ +# {{ ansible_managed }} + +--- +production: + pid_dir: /var/run/flapjack/ + log_dir: /var/log/flapjack/ + daemonize: yes + logger: + level: INFO + syslog_errors: yes + redis: + host: {{ flapjack.redis.host }} + port: {{ flapjack.redis.port }} + db: {{ flapjack.redis.db }} + # Processes monitoring events off the *events* queue (a redis list) and decides + # what actions to take (generate notification event, record state changes, etc) + processor: + enabled: yes + queue: events + notifier_queue: notifications + archive_events: true + events_archive_maxage: 10800 + # Flapjack sets scheduled maintenance on new check results so contacts aren't + # notified as soon as Flapjack becomes aware of an entity to notify on. + # This is useful is cases where your monitoring starts checking something + # before it is completely provisioned + # Value parsed by https://github.com/hpoydar/chronic_duration + # You can disable this setting by specifying "0 seconds". + new_check_scheduled_maintenance_duration: 0 seconds + new_check_scheduled_maintenance_ignore_tags: + - bypass_ncsm + logger: + level: INFO + syslog_errors: yes + # Processes notification events off the *notifications* queue (a redis list) and + # works out who to notify, and on which media, and with what kind of notification + # message. It then creates jobs for the various notification gateways below. + notifier: + enabled: yes + queue: notifications + pagerduty_queue: pagerduty_notifications + slack_queue: slack_notifications + email_queue: email_notifications + jabber_queue: jabber_notifications + webhook_queue: webhook_notifications + notification_log_file: /var/log/flapjack/notification.log + default_contact_timezone: UTC + logger: + level: INFO + syslog_errors: yes + gateways: + {%- if flapjack.notifications.pagerduty.enabled|bool %} + # Sends notifications to and accepts acknowledgements from [PagerDuty](http://www.pagerduty.com/) + # (NB: contacts will need to have a registered PagerDuty account to use this) + pagerduty: + enabled: {{ flapjack.notifications.pagerduty.enabled }} + # the redis queue this pikelet will look for notifications on + queue: pagerduty_notifications + logger: + level: INFO + syslog_errors: yes + # location of custom alert templates + templates: + alert.text: '/etc/flapjack/templates/pagerduty.erb' + {% endif %} + {%- if flapjack.notifications.slack.enabled|bool %} + # Sends notifications via Slack + slack: + enabled: {{ flapjack.notifications.slack.enabled }} + queue: slack_notifications + account_sid: {{ flapjack.notifications.slack.account_sid }} + endpoint: {{ flapjack.notifications.slack.endpoint }} + auth_token: {{ flapjack.notifications.slack.auth_token }} + from: "" + logger: + level: INFO + syslog_errors: yes + # location of custom alert templates + #templates: + # rollup.text: '/etc/flapjack/templates/slack/rollup.text.erb' + # alert.text: '/etc/flapjack/templates/slack/alert.text.erb' + {% endif %} + {%- if flapjack.notifications.email.enabled|bool %} + # Generates email notifications + email: + enabled: {{ flapjack.notifications.email.enabled }} + # the redis queue this pikelet will look for notifications on + queue: email_notifications + logger: + level: INFO + syslog_errors: yes + # these values are passed directly through to EventMachine::Protocols::SmtpClient configuration, + # and can be omitted if the defaults are acceptable + smtp_config: + from: {{ flapjack.notifications.email.from }} + # reply_to: "flapjack@support.example" + host: {{ flapjack.notifications.email.host }} + port: {{ flapjack.notifications.email.port }} + starttls: {{ flapjack.notifications.email.starttls }} + {% if flapjack.notifications.email.auth.enabled|bool %} + auth: + type: {{ flapjack.notifications.email.auth.type }} + username: {{ flapjack.notifications.email.auth.username }} + password: {{ flapjack.notifications.email.auth.password }} + {% endif %} + # location of custom alert templates + #templates: + # rollup_subject.text: '/etc/flapjack/templates/email/rollup_subject.text.erb' + # alert_subject.text: '/etc/flapjack/templates/email/alert_subject.text.erb' + # rollup.text: '/etc/flapjack/templates/email/rollup.text.erb' + # alert.text: '/etc/flapjack/templates/email/alert.text.erb' + # rollup.html: '/etc/flapjack/templates/email/rollup.html.erb' + # alert.html: '/etc/flapjack/templates/email/alert.html.erb' + {% endif %} + {%- if flapjack.notifications.jabber.enabled|bool %} + # Connects to an XMPP (jabber) server, sends notifications (to rooms and individuals), + # handles acknowledgements from jabber users and other commands. + jabber: + enabled: {{ flapjack.notifications.jabber.enabled }} + # the redis queue this pikelet will look for notifications on + queue: jabber_notifications + server: {{ flapjack.notifications.jabber.server }} + port: {{ flapjack.notifications.jabber.port }} + jabberid: {{ flapjack.notifications.jabber.username }} + password: {{ flapjack.notifications.jabber.password }} + alias: "flapjack" + # List of strings that this pikelet user will respond to + identifiers: + - "@flapjack" + # the Multi-User Chats the pikelet should join and announce to + rooms: {{ flapjack.notifications.jabber.rooms }} + logger: + level: INFO + syslog_errors: yes + # location of custom alert templates + #templates: + # rollup.text: '/etc/flapjack/templates/jabber/rollup.text.erb' + # alert.text: '/etc/flapjack/templates/jabber/alert.text.erb' + {% endif %} + {%- if flapjack.notifications.webhook.enabled|bool %} + webhook: + enabled: {{ flapjack.notifications.webhook.enabled }} + queue: webhook_notifications + hooks: {{ flapjack.notifications.webhook.enabled }} + logger: + level: INFO + syslog_errors: yes + {% endif %} + # Browsable web interface + web: + enabled: yes + bind_address: {{ flapjack.dashboard.address }} + port: {{ flapjack.dashboard.port }} + timeout: 300 + # Seconds between auto_refresh of entities/checks pages. Set to 0 to disable + auto_refresh: {{ flapjack.dashboard.auto_refresh }} + access_log: "/var/log/flapjack/web_access.log" + # Main URL for this service eg: "http://flapjack.example/" + base_url: {{ flapjack.dashboard.base_url }} + # URL for accessing the JSONAPI service from the browser + api_url: {{ flapjack.api.base_url }} + # Full path to location of logo file, e.g. /etc/flapjack/custom_logo.png + #logo_image_path: "/etc/flapjack/web/custom_logo/flapjack-2013-notext-transparent-300-300.png" + show_exceptions: false + logger: + level: INFO + syslog_errors: yes + # HTTP API server + jsonapi: + enabled: yes + bind_address: {{ flapjack.api.address }} + port: {{ flapjack.api.port }} + timeout: 300 + access_log: "/var/log/flapjack/jsonapi_access.log" + # Main URL for this service eg: "http://flapjack.example:3081/" + base_url: {{ flapjack.api.base_url }} + logger: + level: INFO + syslog_errors: yes + {% if flapjack.notifications.oobetet.enabled|bool %} + # "out-of-band" end-to-end testing, used for monitoring other instances of + # flapjack to ensure that they are running correctly + oobetet: + enabled: no + server: "jabber.example.com" + port: 5222 + # jabberid, password, alias, rooms: see the jabber pikelet + jabberid: "flapjacktest@jabber.example.com" + password: "nuther-good-password" + alias: "flapjacktest" + # The check oobetet should watch for the state change + watched_check: "PING" + # The entitiy that the check should be associated with + watched_entity: "foo.example.com" + # The maximum amount of time allowed to pass between state changes on that check + max_latency: 300 + # The API key for a service in PagerDuty that the oobetet will use to alert you + pagerduty_contact: "11111111111111111111111111111111" + # Jabber rooms to join + rooms: + - "flapjacktest@conference.jabber.example.com" + - "gimp@conference.jabber.example.com" + - "log@conference.jabber.example.com" + logger: + level: INFO + syslog_errors: yes + {% endif %} diff --git a/roles/flapjack/templates/etc/flapjack/templates/pagerduty.erb b/roles/flapjack/templates/etc/flapjack/templates/pagerduty.erb new file mode 100644 index 0000000..045db55 --- /dev/null +++ b/roles/flapjack/templates/etc/flapjack/templates/pagerduty.erb @@ -0,0 +1,14 @@ +<%= @alert.type_sentence_case %>: "<%= @alert.check %>" on <%= @alert.entity -%> +<% unless ['acknowledgement', 'test'].include?(@alert.notification_type) -%> + is <%= @alert.state_title_case -%> +<% end -%> +<% if ['acknowledgement'].include?(@alert.type) -%> + has been acknowledged, unscheduled maintenance created for <%= time_period_in_words(@alert.acknowledgement_duration) -%> +<% end -%> +<% if @alert.summary && !@alert.summary.empty? -%> +, <%= @alert.summary -%> +<% end %> +--- +[Flapjack: https://control.openstack.blueboxgrid.com/flapjack/check?entity=<%= @alert.entity -%>&check=<%= @alert.check -%>] --- +[Sensu: https://control.openstack.blueboxgrid.com/sensu/#/events?q=<%= @alert.entity -%>] + diff --git a/roles/flapjack/templates/serverspec/flapjack_spec.rb b/roles/flapjack/templates/serverspec/flapjack_spec.rb new file mode 100644 index 0000000..fa83f51 --- /dev/null +++ b/roles/flapjack/templates/serverspec/flapjack_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe package('flapjack') do + it { should be_installed } +end + +describe package('flapjack-admin') do + it { should be_installed } +end + +describe file('/etc/flapjack') do + it { should be_mode 755 } + it { should be_owned_by 'root' } + it { should be_directory } +end + +describe file('/etc/flapjack/flapjack_config.yaml') do + it { should be_file } +end + +describe service('flapjack') do + it { should be_enabled } +end + +describe service('redis-flapjack') do + it { should be_enabled } +end + +{% if flapjack.receivers.httpbroker.enabled %} +describe service('flapjack-httpbroker') do + it { should be_enabled } +end +{% endif %} + +{% for item in flapjack.firewall %} +describe port('{{ item.port }}') do + it { should be_listening } +end +{% endfor %} diff --git a/roles/gem-mirror/defaults/main.yml b/roles/gem-mirror/defaults/main.yml new file mode 100644 index 0000000..98d3ab9 --- /dev/null +++ b/roles/gem-mirror/defaults/main.yml @@ -0,0 +1,58 @@ +--- +gem_mirror: + bin_location: '/usr/local/bin' + home: '/opt/gem_mirror' + config_location: '/opt/gem_mirror/config' + mirror_location: '/opt/gem_mirror/files' + ruby_gems_url: https://rubygems.org/ + worker_processes: 16 # !!!update the sensu-check/defaults/main.yml too!!! + rubygems_proxy: true + allow_remote_failure: true + timeout: 30 # seconds + host: 127.0.0.1 + port: 9292 + packages: + - name: rack + version: '1.6.6' + - name: sinatra + version: '1.4.8' + - name: geminabox + version: '0.13.5' + - name: unicorn + version: '5.3.0' + apache: + enabled: true + http_redirect: False + servername: mirror01.local + serveraliases: + - mirror01 + port: 80 + ip: '*' + ssl: + enabled: False + port: 443 + ip: '*' + name: sitecontroller + cert: ~ + key: ~ + intermediate: ~ + firewall: + - port: 80 + protocol: tcp + src: 0.0.0.0/0 + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/upstart/gem_mirror.log + fields: + tags: mirror,gem-mirror + - paths: + - /var/log/apache2/gem_mirror-access.log + fields: + tags: mirror,apache_access + - paths: + - /var/log/apache2/gem_mirror-error.log + fields: + tags: mirror,apache_error + logging: + forwarder: filebeat diff --git a/roles/gem-mirror/handlers/main.yml b/roles/gem-mirror/handlers/main.yml new file mode 100644 index 0000000..26d7250 --- /dev/null +++ b/roles/gem-mirror/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart gem_mirror server + service: name=gem_mirror state=restarted + +- name: stop gem_mirror server + service: name=gem_mirror state=stopped diff --git a/roles/gem-mirror/meta/main.yml b/roles/gem-mirror/meta/main.yml new file mode 100644 index 0000000..ab3ad6a --- /dev/null +++ b/roles/gem-mirror/meta/main.yml @@ -0,0 +1,20 @@ +--- +dependencies: + - role: bbg-ssl + name: "{{ gem_mirror.apache.ssl.name }}" + ssl_cert: "{{ gem_mirror.apache.ssl.cert }}" + ssl_key: "{{ gem_mirror.apache.ssl.key }}" + ssl_intermediate: "{{ gem_mirror.apache.ssl.intermediate }}" + ssl_ca_cert: ~ + when: gem_mirror.apache.ssl.enabled + tags: ['bbg-ssl'] + - role: apache + - role: logging-config + service: gem_mirror + logdata: "{{ gem_mirror.logs }}" + forward_type: "{{ gem_mirror.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: varnish + tags: ['gem-mirror', 'varnish'] + when: varnish.enabled|default('True')|bool + - role: sensu-check diff --git a/roles/gem-mirror/tasks/apache.yml b/roles/gem-mirror/tasks/apache.yml new file mode 100644 index 0000000..00e35e8 --- /dev/null +++ b/roles/gem-mirror/tasks/apache.yml @@ -0,0 +1,23 @@ +--- +- name: enable apache mods for gem-mirror + apache2_module: name={{ item }} + with_items: + - proxy_http + - rewrite + - headers + +- name: add gem_mirror apache vhost + template: src=etc/apache2/sites-available/gem_mirror + dest=/etc/apache2/sites-available/gem_mirror.conf + notify: + - restart apache + +- name: enable gem_mirror vhost + apache2_site: state=enabled name=gem_mirror + notify: + - restart apache + +- meta: flush_handlers + +- name: ensure apache is running + service: name=apache2 state=started enabled=yes diff --git a/roles/gem-mirror/tasks/checks.yml b/roles/gem-mirror/tasks/checks.yml new file mode 100644 index 0000000..c0b7a92 --- /dev/null +++ b/roles/gem-mirror/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: install gem-mirror process check + sensu_check_dict: name="check-gem-mirror-process" check="{{ sensu_checks.gem_mirror.check_gem_mirror_process }}" + notify: restart sensu-client missing ok diff --git a/roles/gem-mirror/tasks/main.yml b/roles/gem-mirror/tasks/main.yml new file mode 100644 index 0000000..5eb10d5 --- /dev/null +++ b/roles/gem-mirror/tasks/main.yml @@ -0,0 +1,80 @@ +--- +- name: create mirror user + user: name=mirror comment=mirror shell=/bin/false + system=yes home=/nonexistent + +- name: upgrade rubygems under ruby 2.0 + gem: + name: rubygems-update + executable: /usr/bin/gem2.0 + user_install: no + until: result|succeeded + retries: 5 + +- name: refresh rubygems system after upgrade + command: update_rubygems + until: result|succeeded + retries: 5 + +- name: install geminabox and other dependencies under ruby 2.0 + gem: + name: "{{ item.name }}" + version: "{{ item.version }}" + executable: /usr/bin/gem2.0 + user_install: no + with_items: "{{ gem_mirror.packages }}" + register: result + until: result|succeeded + retries: 5 + +- name: create gem mirror config path + file: name="{{ gem_mirror.config_location }}" state=directory + owner=mirror recurse=true + +- name: create gem mirror files location + file: name="{{ gem_mirror.mirror_location }}" state=directory + owner=mirror recurse=true + +- name: geminabox config + template: src=opt/gem_mirror/config.ru + dest="{{ gem_mirror.config_location }}/config.ru" + +- name: unicorn config + template: src=opt/gem_mirror/unicorn.rb + dest="{{ gem_mirror.config_location }}/unicorn.rb" + +- name: gem_mirror service + upstart_service: name=gem_mirror + cmd="{{ gem_mirror.bin_location }}/unicorn" + args="--env production -c {{ gem_mirror.config_location }}/unicorn.rb {{ gem_mirror.config_location }}/config.ru" + user=mirror + notify: restart gem_mirror server + +- meta: flush_handlers + +- name: ensure gem_mirror is running + service: name=gem_mirror state=started enabled=yes + +- include: apache.yml + when: gem_mirror.apache.enabled|bool + +- name: allow gem-mirror traffic + ufw: rule=allow + to_port={{ item.port }} + src={{ item.src }} + proto={{ item.protocol }} + with_items: "{{ gem_mirror.firewall }}" + tags: + - firewall + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/gem-mirror/tasks/metrics.yml b/roles/gem-mirror/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/gem-mirror/tasks/serverspec.yml b/roles/gem-mirror/tasks/serverspec.yml new file mode 100644 index 0000000..e73da76 --- /dev/null +++ b/roles/gem-mirror/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec checks for gem-mirror role + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/gem-mirror/templates/etc/apache2/sites-available/gem_mirror b/roles/gem-mirror/templates/etc/apache2/sites-available/gem_mirror new file mode 100644 index 0000000..f8f24a9 --- /dev/null +++ b/roles/gem-mirror/templates/etc/apache2/sites-available/gem_mirror @@ -0,0 +1,48 @@ +# {{ ansible_managed }} + +{% macro virtualhost() %} + ServerAdmin openstack@bluebox.net + ServerName {{ gem_mirror.apache.servername }} + ServerAlias {{ gem_mirror.apache.serveraliases|join(" ") }} + DocumentRoot {{ gem_mirror.config_location }} + ErrorLog ${APACHE_LOG_DIR}/gem_mirror-error.log + CustomLog ${APACHE_LOG_DIR}/gem_mirror-access.log combined + FileETag MTime Size + Header set Cache-Control public + ProxyRequests Off + ProxyPass / http://{{ varnish.host if varnish.enabled else gem_mirror.host }}:{{ varnish.port if varnish.enabled else gem_mirror.port }}/ + ProxyPassReverse / http://{{ varnish.host if varnish.enabled else gem_mirror.host }}:{{ varnish.port if varnish.enabled else gem_mirror.port }}/ + + Options Indexes + AllowOverride None + Require all granted + +{% endmacro %} + +{% if gem_mirror.apache.ssl.enabled|bool and gem_mirror.apache.http_redirect|bool %} + + ServerName {{ gem_mirror.apache.servername }} + ServerAlias {{ gem_mirror.apache.serveraliases|join(" ") }} + RewriteEngine on + RewriteCond %{HTTPS} !=on + RewriteRule ^(.*)$ https://%{HTTP_HOST}:{{ gem_mirror.apache.ssl.port }}$1 [R=301,L] + +{% else %} + +{{ virtualhost() }} + +{% endif %} + +{% if gem_mirror.apache.ssl.enabled|bool %} + + {{ apache.ssl.settings }} + SSLCertificateFile /etc/ssl/certs/{{ gem_mirror.apache.ssl.name|default('sitecontroller') }}.crt + SSLCertificateKeyFile /etc/ssl/private/{{ gem_mirror.apache.ssl.name|default('sitecontroller') }}.key +{% if bbg_ssl.intermediate or gem_mirror.apache.ssl.intermediate %} + SSLCertificateChainFile /etc/ssl/certs/{{ gem_mirror.apache.ssl.name|default('sitecontroller') }}-intermediate.crt +{% endif %} +{% else %} + +{% endif %} +{{ virtualhost() }} + diff --git a/roles/gem-mirror/templates/opt/gem_mirror/config.ru b/roles/gem-mirror/templates/opt/gem_mirror/config.ru new file mode 100644 index 0000000..323c87a --- /dev/null +++ b/roles/gem-mirror/templates/opt/gem_mirror/config.ru @@ -0,0 +1,11 @@ +# {{ ansible_managed }} + +require "rubygems" +require "geminabox" + +Geminabox.data = "{{ gem_mirror.mirror_location }}" +Geminabox.rubygems_proxy = {{ gem_mirror.rubygems_proxy | lower }} +Geminabox.allow_remote_failure = {{ gem_mirror.allow_remote_failure | lower }} +Geminabox.ruby_gems_url = "{{ gem_mirror.ruby_gems_url }}" + +run Geminabox::Server diff --git a/roles/gem-mirror/templates/opt/gem_mirror/unicorn.rb b/roles/gem-mirror/templates/opt/gem_mirror/unicorn.rb new file mode 100644 index 0000000..714f731 --- /dev/null +++ b/roles/gem-mirror/templates/opt/gem_mirror/unicorn.rb @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +worker_processes {{ gem_mirror.worker_processes }} +working_directory "{{ gem_mirror.home }}" +listen "{{ gem_mirror.host }}:{{ gem_mirror.port }}" +timeout {{ gem_mirror.timeout }} +pid "{{ gem_mirror.home }}/unicorn.pid" +preload_app true + +if(GC.respond_to?(:copy_on_write_friendly=)) + GC.copy_on_write_friendly = true +end diff --git a/roles/gem-mirror/templates/serverspec/gem_mirror_spec.rb b/roles/gem-mirror/templates/serverspec/gem_mirror_spec.rb new file mode 100644 index 0000000..470054f --- /dev/null +++ b/roles/gem-mirror/templates/serverspec/gem_mirror_spec.rb @@ -0,0 +1,59 @@ +# {{ ansible_managed }} + +require 'spec_helper' + +describe user('mirror') do + it { should exist } + it { should have_home_directory '/nonexistent' } + it { should have_login_shell '/bin/false' } +end + +['proxy_http', 'rewrite', 'headers'].each do |file| + describe file("/etc/apache2/mods-available/#{file}.load") do + it { should exist } + end + describe file("/etc/apache2/mods-enabled/#{file}.load") do + it { should be_symlink } + end +end + +describe file('{{ gem_mirror.mirror_location }}') do + it { should be_directory } + it { should be_owned_by 'mirror' } +end + +describe service('gem_mirror') do + it { should be_enabled } +end + +describe port('{{ gem_mirror.port }}') do + it { should be_listening.on('{{ gem_mirror.host }}').with('tcp') } +end + +describe port('{{ gem_mirror.apache.port }}') do + it { should be_listening } +end + +describe package('apache2') do + it { should be_installed } +end + +describe service('apache2') do + it { should be_enabled } +end + +describe file('{{ gem_mirror.config_location }}/config.ru') do + it { should be_file } +end + +describe file('/etc/apache2/sites-available/gem_mirror.conf') do + it { should be_file } +end + +describe file('/etc/apache2/sites-enabled/gem_mirror.conf') do + it { should be_symlink } +end + +describe iptables do + it { should have_rule('-p tcp -m tcp --dport {{ gem_mirror.apache.port }} -j ACCEPT') } +end diff --git a/roles/git-repos/defaults/main.yml b/roles/git-repos/defaults/main.yml new file mode 100644 index 0000000..2f51a5d --- /dev/null +++ b/roles/git-repos/defaults/main.yml @@ -0,0 +1,4 @@ +--- +git_repos: + ursula: git@github.com:blueboxgroup/ursula.git + ursula_cli: git@github.com:blueboxgroup/ursula-cli.git diff --git a/roles/gpg/defaults/main.yml b/roles/gpg/defaults/main.yml new file mode 100644 index 0000000..7af7646 --- /dev/null +++ b/roles/gpg/defaults/main.yml @@ -0,0 +1,4 @@ +--- +gpg: + secret_key_id: '9A83C9E2' + secret_key_passphrase: 'S9JgkdQnv9PHXXc' diff --git a/roles/gpg/files/blueboxcloud.asc b/roles/gpg/files/blueboxcloud.asc new file mode 100644 index 0000000..523fec0 --- /dev/null +++ b/roles/gpg/files/blueboxcloud.asc @@ -0,0 +1,59 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.14 (GNU/Linux) + +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 +QXx32eYDB6G6EkJ372LvE7JiIvaMh9EVfw5s +=TYfL +-----END PGP PRIVATE KEY BLOCK----- diff --git a/roles/gpg/files/blueboxcloud.key b/roles/gpg/files/blueboxcloud.key new file mode 100644 index 0000000..781fc6b --- /dev/null +++ b/roles/gpg/files/blueboxcloud.key @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.14 (GNU/Linux) + +mQENBFSJ/z4BCAC1vwQ3Yh2a4nstngZIOH9P1VUXZCugSYLOZhyd6XH4R49lr82h +gNEZCFo3C7+ohIh9RFCrh4WHCgEw5lmitrxJLStCpWIttkOwZuKbuXKFtkBXS6MV +YLnBvCdzPyxzg7s+WEY1VihnfGGZQpmiQGuY4a7OHK+1Q591kbyvfislQ9mShyOx +dhawOCa7RM4vVam7toCP4VcMbhOjQiyxovGExy34AUu/K9lq+WcA3yXTEigc08hx +Q4jO1KfA6Nt2z3ixUhRi7iUdFbEZ/Vt113AvTYO9uGpHg8m04PXk/oMyWOoS/KI3 +6WEeQ3iHcy3hjCeOfLjZU02BeJ2IubHSy289ABEBAAG0KkJsdWUgQm94IENsb3Vk +IDxvcGVuc3RhY2stZW5nQGJsdWVib3gubmV0PokBOAQTAQIAIgUCVIn/PgIbAwYL +CQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQKRsjSJqDyeITswf/cCRliEPbnvbc +uEoGy60DERywNvooT80N7MZsIInbHBKaC973sX5fsTfxR7fNkPAGKRKuO0/vqugl +JJZxxPczZUDSJe8wiwECSFirRL8Qnj2J5+9zKjCJd9oE+UcbucLlVP9oO+kxIYoJ +E/8j3VOK/ggjXJseRWOXE+Q+0DRq7/aI7aGIqbuO0sdDPf4sJPPW/4E526knUdf4 +sHRdCncWJPBBwvdnkbWXA/EBg7s2Ptutq/Hqbhhb8vxZhkwSr6X7Ry8McozK2y/S +XLeKmYX0a9AaRDstI/7KROFKpN+5KEOC+pQpNHHcp3PhFq1kQpRkoM7OHRMF5KI1 +BzmZZEqnZ7kBDQRUif8+AQgA1Obc7WcPeyyO1wA5v4Yhs6bP6ObaW7qkh9gAE1Qy +Gchk0IQmWALCv6fbI78MQYFoi1cAKo63M2ZGjexBrxl3ytPGgNlY5EYTuDn0a8wr +kzrNO/LJrjWoKh2LWoahFZ0KgiIzsiAfpw0rRY1an+ws3cPBrxBHyQkHFA0heByu +KIHDBOUcSrofUJSElaDSarYI9FSM/nEsHII+uR12EiEjhi5taCRwmIc2FAHaS18r +ez3YODLnbXOj0WRzKMLq60v5EHREwtwLAwTFBmqpAtn/ou0dyTqCsT0tCtByCxnu +VK6YM3B9JjGQOXS9eRg+yaAX+lGKS4uRW0MID8tAb+5/+QARAQABiQEfBBgBAgAJ +BQJUif8+AhsMAAoJECkbI0iag8niBboH/j4IIROzYw4oYZAJDjvKYa95TA/+ptAR +ezGADxoT9CbAraez7QyM3PRMeSjs0zF8eZ4yN+fP+lXu9EBxnvqgxCX+nZzohdh1 +zdYElOw6dkBTt7BUla+CHFaPhwb1+CqYhh7JBn694EF7O+IuR1gSuGZ6OBGqA1eE +e2Jd2vl2NkKdZGWg+954r0rjGU4d6UWhWJhsm7VhosmwNtKM+uN1TZOZ5NCERSVj +vCiHuWD2JKumz8DBWhpsRAc3M9g62FTe/W54b/QjyBziL6bA+HyshekakU+9LnE7 +E5deYKYPTRi/T/Sln/lBfHfZ5gMHoboSQnfvYu8TsmIi9oyH0RV/Dmw= +=F8z8 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/roles/gpg/tasks/checks.yml b/roles/gpg/tasks/checks.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/gpg/tasks/main.yml b/roles/gpg/tasks/main.yml new file mode 100644 index 0000000..a49044d --- /dev/null +++ b/roles/gpg/tasks/main.yml @@ -0,0 +1,35 @@ +--- +- name: install gpg + apt: pkg=gnupg + +- name: check whether repo key exists + shell: 'gpg --list-secret-keys | grep -q "{{ gpg.secret_key_id }}"' + register: gpg_key_exists + failed_when: False + +- name: copy secret key to host + copy: src=blueboxcloud.asc dest=/tmp/blueboxcloud.asc + when: gpg_key_exists.rc != 0 + +- name: gpg import repo key + shell: 'gpg --allow-secret-key-import --import /tmp/blueboxcloud.asc' + when: gpg_key_exists.rc != 0 + +- name: remove gpg key + file: dest=/tmp/blueboxcloud.asc state=absent + when: gpg_key_exists.rc != 0 + +- name: create default keys file + command: creates="/root/.gnupg/trustedkeys.gpg" cp /usr/share/keyrings/ubuntu-archive-keyring.gpg /root/.gnupg/trustedkeys.gpg + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/gpg/tasks/metrics.yml b/roles/gpg/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/gpg/tasks/serverspec.yml b/roles/gpg/tasks/serverspec.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml new file mode 100644 index 0000000..a24b3a3 --- /dev/null +++ b/roles/grafana/defaults/main.yml @@ -0,0 +1,46 @@ +--- +grafana: + version: ~ + graphite_url: http://172.16.0.17:8080 + elasticsearch_url: http://172.16.0.13:9200 + server: + http_port: 3001 + http_addr: 0.0.0.0 + security: + enabled: false + anonymous: false + admin_user: admin + admin_password: admin + basic: true + secret_key: dsfdgrgrgrfewfdewgfreGregvre + database: + type: sqlite3 + path: "/usr/share/grafana/data/grafana.db" + host: "127.0.0.1:3306" + name: "grafana" + user: "grafana" + password: "grafana" + dashboards: + path: ~ + public: + enabled: [] + disabled: [] + firewall: + - port: 5671 + src: "{{ private_ipv4.network }}/{{ private_ipv4.netmask }}" + logs: + # See logging-config/defaults/main.yml for filebeat vs. logstash-forwarder example + - paths: + - /var/log/grafana/* + fields: + tags: grafana + logging: + forwarder: filebeat + logrotate: + frequency: 'daily' + rotations: 7 + remote_graphite: + datasources: + - name: graphite + hostname: 127.0.0.1 + port: 8081 diff --git a/roles/grafana/handlers/main.yml b/roles/grafana/handlers/main.yml new file mode 100644 index 0000000..02db294 --- /dev/null +++ b/roles/grafana/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart grafana + service: name=grafana-server state=restarted diff --git a/roles/grafana/meta/main.yml b/roles/grafana/meta/main.yml new file mode 100644 index 0000000..c8c1948 --- /dev/null +++ b/roles/grafana/meta/main.yml @@ -0,0 +1,12 @@ +--- +dependencies: + - role: apt-repos + repos: + - repo: 'deb {{ apt_repos.grafana.repo }} wheezy main' + key_url: '{{ apt_repos.grafana.key_url }}' + - role: logging-config + service: grafana + logdata: "{{ grafana.logs }}" + forward_type: "{{ grafana.logging.forwarder }}" + when: logging.enabled|default("True")|bool + - role: sensu-check diff --git a/roles/grafana/tasks/checks.yml b/roles/grafana/tasks/checks.yml new file mode 100644 index 0000000..5b8f205 --- /dev/null +++ b/roles/grafana/tasks/checks.yml @@ -0,0 +1,4 @@ +--- +- name: GFN015 install grafana-server process check + sensu_check_dict: name="check-grafana-process" check="{{ sensu_checks.grafana.check_grafana_process }}" + notify: restart sensu-client missing ok diff --git a/roles/grafana/tasks/datasources.yml b/roles/grafana/tasks/datasources.yml new file mode 100644 index 0000000..4c33865 --- /dev/null +++ b/roles/grafana/tasks/datasources.yml @@ -0,0 +1,43 @@ +--- +- name: set grafana request url + set_fact: + grafana_url: http://127.0.0.1:{{ grafana.server.http_port|default("3001") }}/api/datasources + +- name: check if grafana datasources exist + uri: + url: "{{ grafana_url }}" + method: GET + user: "{{ grafana.security.admin_user }}" + password: "{{ grafana.security.admin_password }}" + force_basic_auth: yes + return_content: yes + register: grafana_api_datasources + +- name: create list of currently configured datasources + set_fact: + configured_datasources: "{{ configured_datasources | default([]) + [item.name|lower] }}" + with_items: "{{ grafana_api_datasources.content|from_json }}" + +- name: create initial grafana datasources + uri: + url: "{{ grafana_url }}" + method: POST + user: "{{ grafana.security.admin_user }}" + password: "{{ grafana.security.admin_password }}" + force_basic_auth: yes + body: "{{ lookup('template', 'datasources.json')}}" + body_format: json + with_items: "{{ grafana.remote_graphite.datasources }}" + when: "{{ not grafana_api_datasources.content|from_json }}" + +- name: add new grafana datasources + uri: + url: "{{ grafana_url }}" + method: POST + user: "{{ grafana.security.admin_user }}" + password: "{{ grafana.security.admin_password }}" + force_basic_auth: yes + body: "{{ lookup('template', 'datasources.json')}}" + body_format: json + with_items: "{{ grafana.remote_graphite.datasources }}" + when: "{{ grafana_api_datasources.content|from_json and item.name|lower not in configured_datasources }}" diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml new file mode 100644 index 0000000..8266c31 --- /dev/null +++ b/roles/grafana/tasks/main.yml @@ -0,0 +1,81 @@ +--- +- name: create grafana user + user: name=grafana groups=adm home=/usr/share/grafana + comment=grafana shell=/bin/false + system=yes + +- name: grafana log directory + file: dest=/var/log/grafana + owner=grafana group=adm mode=0775 + state=directory + +- name: create grafana packages dir + file: dest=/usr/share/grafana/packages owner=grafana + state=directory + +- name: grafana default configuration + template: src=etc/default/grafana-server dest=/etc/default/grafana-server + notify: + - restart grafana + +- name: fix grafana service + template: src=etc/init.d/grafana-server dest=/etc/init.d/grafana-server + mode=0755 + +- name: install grafana + apt: name="{{ 'grafana=' ~ grafana.version if grafana.version else 'grafana' }}" + +- name: grafana configuration + template: src=etc/grafana/grafana.ini dest=/etc/grafana/grafana.ini + notify: + - restart grafana + +- name: set up log rotation for grafana + logrotate: name=grafana path=/var/log/grafana/grafana*.log + args: + options: + - "{{ grafana.logrotate.frequency }}" + - "rotate {{ grafana.logrotate.rotations }}" + - missingok + - copytruncate + - compress + - notifempty + +- name: enabled grafana dashboards + template: src=dashboards/{{item}} dest=/usr/share/grafana/public/dashboards/{{item}} + with_items: "{{ grafana.dashboards.public.enabled }}" + notify: + - restart grafana + +- name: disabled grafana dashboards + file: path=/usr/share/grafana/public/dashboards/{{ item }} state=absent + with_items: "{{ grafana.dashboards.public.disabled }}" + notify: + - restart grafana + +- meta: flush_handlers + +- name: ensure grafana is running + service: name=grafana-server state=started enabled=yes + +- name: grafana firewall rules + ufw: rule=allow to_port={{ item.port }} proto=tcp src={{ item.src }} + with_items: "{{ grafana.firewall }}" + tags: + - firewall + +- include: datasources.yml + when: grafana.remote_graphite.datasources|length > 0 + tags: grafana-datasources + +- include: checks.yml + when: sensu.client.enable_checks|default('True')|bool + tags: sensu-checks + +- include: metrics.yml + when: sensu.client.enable_metrics|default('True')|bool + tags: sensu-metrics + +- include: serverspec.yml + when: serverspec.enabled|default("True")|bool + tags: serverspec diff --git a/roles/grafana/tasks/metrics.yml b/roles/grafana/tasks/metrics.yml new file mode 100644 index 0000000..e69de29 diff --git a/roles/grafana/tasks/serverspec.yml b/roles/grafana/tasks/serverspec.yml new file mode 100644 index 0000000..69bac06 --- /dev/null +++ b/roles/grafana/tasks/serverspec.yml @@ -0,0 +1,6 @@ +--- +- name: serverspec tests grafana tech specs + template: src={{ item }} + dest=/etc/serverspec/spec/localhost/ + mode=0755 + with_fileglob: ../templates/serverspec/* diff --git a/roles/grafana/templates/dashboards/bbc-basic-host.json b/roles/grafana/templates/dashboards/bbc-basic-host.json new file mode 100644 index 0000000..6821820 --- /dev/null +++ b/roles/grafana/templates/dashboards/bbc-basic-host.json @@ -0,0 +1,882 @@ +{ + "id": 1, + "title": "System Metrics", + "originalTitle": "System Metrics", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "25px", + "panels": [ + { + "cacheTimeout": null, + "colorBackground": true, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "editable": true, + "error": false, + "format": "percent", + "id": 12, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 1, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "targets": [ + { + "target": "stats.{bbc,sc}.$cloud_instance.$node_name.vmstat.cpu.user" + } + ], + "thresholds": "0,80,90", + "title": "used cpu (%)", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": true, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "editable": true, + "error": false, + "format": "bytes", + "id": 13, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 2, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "targets": [ + { + "target": "stats.{bbc,sc}.$cloud_instance.$node_name.memory.freeWOBuffersCaches" + } + ], + "thresholds": "0,1024,4096", + "title": "free memory (M)", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": true, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "editable": true, + "error": false, + "format": "bps", + "id": 14, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 2, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "targets": [ + { + "target": "maxSeries(scale(perSecond(stats.{bbc,sc}.$cloud_instance.$node_name.network.*.{rx_bytes,tx_bytes}), 8))" + } + ], + "thresholds": "0,350000000,500000000", + "title": "NIF max traffic (bps)", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + } + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 7, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "one", + "yaxis": 1 + } + ], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.load_avg.*)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Load", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 2, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "waiting", + "yaxis": 1 + } + ], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.vmstat.cpu.*)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "vmstat CPU (%)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "percent", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 10, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "waiting", + "yaxis": 1 + }, + { + "alias": "usedWOBuffersCaches", + "yaxis": 1 + }, + { + "alias": "asPercent(usedWOBuffersCaches, total)", + "yaxis": 2 + }, + { + "alias": "usedPercentage", + "yaxis": 2 + } + ], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.memory.total)" + }, + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.memory.usedWOBuffersCaches)" + }, + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.memory.swapUsed)" + }, + { + "target": "alias(asPercent(#B,#A),\"usedPercentage\")" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "memory", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "bytes", + "percent" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 5, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.vmstat.procs.*)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "vmstat procs", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 6, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.vmstat.swap.*)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "vmstat si/so", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 3, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.vmstat.io.*)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "vmstat IO (blocks)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 8, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByMetric(stats.{bbc,sc}.$cloud_instance.$node_name.vmstat.system.*)" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "vmstat system", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 9, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(scale(perSecond(stats.{bbc,sc}.$cloud_instance.$node_name.network.*.rx_bytes), 8), 5)", + "textEditor": false + } + ], + "timeFrom": null, + "timeShift": null, + "title": "network traffic in (bps)", + "tooltip": { + "shared": true, + "value_type": "individual" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "bps", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 11, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(scale(perSecond(stats.{bbc,sc}.$cloud_instance.$node_name.network.*.tx_bytes), 8), 5)", + "textEditor": false + } + ], + "timeFrom": null, + "timeShift": null, + "title": "network traffic out (bps)", + "tooltip": { + "shared": true, + "value_type": "individual" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "bps", + "short" + ] + } + ], + "title": "New row" + } + ], + "nav": [ + { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + } + ], + "time": { + "from": "now-2d", + "to": "now" + }, + "templating": { + "list": [ + { + "allFormat": "glob", + "current": { + "text": "hitachi-systems-tok", + "value": "hitachi-systems-tok" + }, + "datasource": null, + "includeAll": false, + "multi": false, + "multiFormat": "glob", + "name": "cloud_instance", + "options": [ + { + "text": "hitachi-systems-tok", + "value": "hitachi-systems-tok" + }, + { + "text": "lixil-tok", + "value": "lixil-tok" + } + ], + "query": "stats.{bbc,sc}.*", + "refresh": true, + "type": "query" + }, + { + "allFormat": "glob", + "current": { + "text": "ds0006", + "value": "ds0006" + }, + "datasource": null, + "includeAll": false, + "multi": false, + "multiFormat": "glob", + "name": "node_name", + "options": [ + { + "text": "ds0006", + "value": "ds0006" + }, + { + "text": "ds0007", + "value": "ds0007" + }, + { + "text": "ds0008", + "value": "ds0008" + }, + { + "text": "openstack", + "value": "openstack" + } + ], + "query": "stats.{bbc,sc}.$cloud_instance.*", + "refresh": true, + "type": "query" + } + ] + }, + "annotations": { + "list": [] + }, + "refresh": false, + "schemaVersion": 6, + "version": 1 +} diff --git a/roles/grafana/templates/dashboards/bbc-ceph-usage.json b/roles/grafana/templates/dashboards/bbc-ceph-usage.json new file mode 100644 index 0000000..e918ae5 --- /dev/null +++ b/roles/grafana/templates/dashboards/bbc-ceph-usage.json @@ -0,0 +1,699 @@ +{ + "id": 3, + "title": "Ceph Usage Monitoring", + "originalTitle": "Ceph Usage Monitoring", + "tags": [], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": "100px", + "panels": [ + { + "content": "

5fWI?U|*a#IY)2-oZKYhAXAh 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 U&-`oXfIrtXb$>3oZ2jQ>0XceWqW}N^ literal 0 HcmV?d00001 diff --git a/docs/logging.dot b/docs/logging.dot new file mode 100644 index 0000000..a33abab --- /dev/null +++ b/docs/logging.dot @@ -0,0 +1,54 @@ +digraph G { + label="Site Controller Logging" + labelloc="t"; + rankdir="LR"; + clusterrank="local"; + overlap=scale; + normalize=true; + edge [fontsize=11]; + nodesep=0.5; + ranksep=2; + dpi = 128; + + user [label="Support / Engineering User", shape=Mdiamond]; + pagerduty [label="PagerDuty\n(Unused)", shape=Msquare]; + + subgraph cluster_central_controller { + label = "Central Site Controller"; + rank = same; + + cc_apache [label = "BBG Auth Proxy\n(Apache)"]; + } + + subgraph cluster_site_controller { + label = "Remote Site Controller"; + rank = same; + + sc_apache [label = "Apache"]; + sc_kibana [label="Kibana"]; + sc_elasticsearch [label="Elasticsearch"]; + sc_logstash [label="Logstash"]; + sc_log_shipper [label="Log Shipper\n(Logstash Forwarder)"]; + + sc_apache -> sc_kibana [label="localhost:5601/tcp"]; + sc_kibana -> sc_elasticsearch [label="localhost:9200/tcp"]; + sc_logstash -> sc_elasticsearch [label="localhost:9200/tcp"]; + sc_log_shipper -> sc_logstash [label="TLS logs.$DC.bbg:4560/tcp"]; + } + + subgraph cluster_openstack { + label = "OpenStack"; + rank = same; + + os_error [label="OpenStack Error", shape=Mdiamond]; + os_log_shipper [label="Log Shipper\n(Logstash Forwarder)"]; + + os_error -> os_log_shipper; + } + + os_log_shipper -> sc_logstash [label="TLS logs.$DC.bbg:4560/tcp"]; + + cc_apache -> sc_apache [label="VPN: HTTP logs.$DC.bbg:80/tcp"]; + + user -> cc_apache [label="Internet: HTTPS control.openstack.blueboxgrid.com:443/tcp"]; +} diff --git a/docs/logging.png b/docs/logging.png new file mode 100644 index 0000000000000000000000000000000000000000..ebd251f2262039efe7708e2271b4ccff071b5f06 GIT binary patch literal 188325 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⪻p0^$ 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