bundle agent cfe_internal_enterprise_mission_portal { meta: "description" string => "Manage mission portal configuration"; methods: policy_server:: "Apache Configuration" -> { "CFEngine Enterprise", "Mission Portal" } usebundle => cfe_internal_enterprise_mission_portal_apache; "Apache Service" -> { "CFEngine Enterprise", "Mission Portal" } usebundle => cfe_internal_webserver("on"), handle => "cfe_internal_management_web_server", comment => "Manage Apache Web server (on/off)"; } bundle agent apachectl_patched_for_upgrade # @brief Ensure that apacehctl is patched so that it is able to re-start services # # @description This bundle addresses an issue where upgrades fail trying to stop # httpd. Versions prior to 3.20.0, 3.18.2, and 3.15.6 need to have apachectl # patched to make it wait for processes to shut down before exiting before # upgrading binaries to 3.20.0, 3.18.2, or 3.15.6. CFEngine packages for # versions after 3.20.0, 3.18.2, 3.15.6 ship with this patched apachectl which # should make this bundle a no-op. # # TODO Redact when 3.21.0 is the oldest supported version # # NOTE The rendering of apachectl is done slightly differently for different # versions of CFEngine. We need to maintain support for 3.15.x and many macros # were not available at 3.15.0, so we use classes instead of macros. # # Versions prior to CFEngine 3.20.0 do not create files by default when managed # by a template, if the file is absent but it's presence was desired the use of # create => "true" or some other machinery is necessary. # # When 3.15.0 (Currently oldest supported version) was released, 3.10.0 was supported. { vars: "considered_versions" slist => { "cfengine_3_10", "cfengine_3_11", "cfengine_3_12", "cfengine_3_13", "cfengine_3_14", "cfengine_3_15", "cfengine_3_16", "cfengine_3_17", "cfengine_3_18" }; classes: "_running_cfengine_version_where_templated_files_automatically_created" or => { @(considered_versions) }; "_running_cfengine_version_where_templated_files_NOT_automatically_created" not => or( @(considered_versions) ); files: _running_cfengine_version_where_templated_files_automatically_created:: "$(sys.workdir)/httpd/bin/apachectl" edit_template => "$(this.promise_dirname)/templates/apachectl.mustache", handle => "apachectl_content_pre_create_default_templated_files", template_method => "mustache", template_data => parsejson( '{ "cfengine_enterprise_mission_portal_httpd_dir": "$(sys.workdir)/httpd" }'); _running_cfengine_version_where_templated_files_NOT_automatically_created:: "$(sys.workdir)/httpd/bin/apachectl" create => "true", edit_template => "$(this.promise_dirname)/templates/apachectl.mustache", handle => "apachectl_content_post_create_default_templated_files", template_method => "mustache", template_data => parsejson( '{ "cfengine_enterprise_mission_portal_httpd_dir": "$(sys.workdir)/httpd" }'); cfengine:: "$(sys.workdir)/httpd/bin/apachectl" handle => "apachectl_perms", perms => mog( "0755", "root", "root" ); } bundle agent cfe_internal_enterprise_mission_portal_apache # @brief Manage Apache instance that runs Mission Portal { vars: policy_server.enterprise_edition:: "template" string => "$(this.promise_dirname)/templates/httpd.conf.mustache", comment => "The template used to render the apache config file."; "config" string => "$(sys.workdir)/httpd/conf/httpd.conf", comment => "This is the location of the apache config file."; "staged_config" string => "$(config).staged", comment => "This is the temporary file we will render the config and valid against before deploying to the final $(config) location."; # TODO: Consider collecting the classes and variables used based on a tag # to prepare a smaller subset of data "data" data => datastate(); methods: "apachectl patched for resilient stop " -> { "ENT-8823" } usebundle => apachectl_patched_for_upgrade; "Stage Apache Config" usebundle => file_make_mustache( $(staged_config), $(template), @(data) ), comment => "We render the config to a staging location so that it can be validated before deploying the final apache config so that we don't end up with a broken service."; "Manage Final Apache Config" usebundle => mission_portal_apache_from_stage( $(config), $(staged_config) ); reports: DEBUG|DEBUG_cfe_internal_enterprise_mission_portal_apache:: "DEBUG $(this.bundle): Should stage '$(staged_config)' from '$(template)'."; classes: (policy_server|am_policy_hub).enterprise_edition:: "mission_portal_index_php_redirect_enabled" expression => isgreaterthan(countlinesmatching(".*config\['index_page'\] = \"\".*", "$(cfe_internal_hub_vars.docroot)/application/config/config.php"), 0), comment => "We need to know if index_page config in Mission Portal is empty to enable redirect from /index.php/path to /path in httpd.conf"; } bundle agent mission_portal_apache_from_stage(config, staged_config) # @brief Make sure the live Apache config is based on a staged config that has # been validated. # @param config Path to the live config # @param staged_config Path to the staged configuration # # **Example:** # # ```cf3 # bundle agent example # { # vars: # # "config" # string => "$(sys.workdir)/httpd/conf/httpd.conf", # comment => "This is the location of the apache config file."; # # "staged_config" # string => "$(config).staged", # comment => "This is the temporary file we will render the config and # valid against before deploying to the final $(config) # location."; # # methods: # # "Manage Final Apache Config" # usebundle => mission_portal_apache_from_stage( $(config), $(staged_config) ); # } #``` { meta: "description" string => "Configure apache based on successfully staged config"; classes: "systemd_supervised" expression => returnszero("$(paths.systemctl) -q is-enabled cf-apache > /dev/null 2>&1", "useshell"), if => fileexists( $(paths.systemctl) ); vars: "validate_config" string => "$(sys.workdir)/httpd/bin/httpd -t -f $(staged_config)"; # The location of the apache pid file moved from httpd/logs/httpd.pid to # httpd/httpd.pid in 3.15.5, 3.18.1 and, 3.19.0 "httpd_pid_file" -> { "ENT-7966" } string => ifelse( classmatch( "cfengine_3_1[0-4]" ), "$(sys.workdir)/httpd/logs/httpd.pid", classmatch( "cfengine_3_15_[0-4]" ), "$(sys.workdir)/httpd/logs/httpd.pid", "cfengine_3_18_0", "$(sys.workdir)/httpd/logs/httpd.pid", "$(sys.workdir)/httpd/httpd.pid" ); files: "$(config)" create => "true", comment => "Without an apache config, Mission Mortal and Enterprise API services will not work"; "$(config)" -> { "ENT-9686" } copy_from => local_dcp( $(staged_config) ), handle => "cfapache_httpd_conf_from_staged", if => and( or( "apache_stop_after_new_staged_config_repaired", not( fileexists( "$(httpd_pid_file)" ) ), isnewerthan( $(config), $(staged_config) ) ), returnszero("$(validate_config) > /dev/null 2>&1 ", "useshell")), classes => results("bundle", "mission_portal_apache_config"), comment => "We make sure that the deployed config is a copy of the staged config if the staged config passes a syntax check. We redirect the output to keep policy runs clean and not generate lots of unnecessary email."; commands: !systemd_supervised:: "LD_LIBRARY_PATH=$(sys.workdir)/lib:$LD_LIBRARY_PATH $(sys.workdir)/httpd/bin/apachectl" args => "stop", if => and( returnszero("$(validate_config) > /dev/null 2>&1 ", "useshell"), isnewerthan( $(staged_config), $(config) ), fileexists( "$(httpd_pid_file)" ) ), contain => in_shell, classes => results( "bundle", "apache_stop_after_new_staged_config" ), comment => concat( "We have to stop apache before trying to start with a", "new config, or the new config could prevent apache from stopping."); "LD_LIBRARY_PATH=$(sys.workdir)/lib:$LD_LIBRARY_PATH $(sys.workdir)/httpd/bin/apachectl" args => "start", if => and( "mission_portal_apache_config_repaired", "apache_stop_after_new_staged_config_repaired"), contain => in_shell, comment => concat( "We start apache after the new valid config is in ", "place only if we have stopped apache already."); "LD_LIBRARY_PATH=$(sys.workdir)/lib:$LD_LIBRARY_PATH $(sys.workdir)/httpd/bin/apachectl" -> { "ENT-9686" } args => "restart", if => and( "mission_portal_apache_config_repaired", not( "apache_stop_after_new_staged_config_repaired" ) ), contain => in_shell, comment => "We restart apache after the new valid config is in place"; services: systemd_supervised:: "cf-apache" service_policy => "stop", if => and( returnszero("$(validate_config) > /dev/null 2>&1 ", "useshell"), isnewerthan( $(staged_config), $(config) ) ), classes => results( "bundle", "apache_stop_after_new_staged_config" ), comment => concat( "We have to stop apache before trying to start with a", "new config, or the new config could prevent apache from stopping."); "cf-apache" service_policy => "start", if => and( "mission_portal_apache_config_repaired", "apache_stop_after_new_staged_config_repaired"), comment => concat( "We start apache after the new valid config is in ", "place only if we have stopped apache already."); "cf-apache" -> { "ENT-9686" } service_policy => "restart", if => and( "mission_portal_apache_config_repaired", not( "apache_stop_after_new_staged_config_repaired" ) ), comment => "We restart apache after the new valid config is in place"; reports: DEBUG|DEBUG_mission_portal_apache_from_stage:: "DEBUG $(this.bundle): '$(config)' should be a copy of '$(staged_config)' because it validates with '$(validate_config)'" if => returnszero("$(validate_config) > /dev/null 2>&1 ", "useshell"); "DEBUG $(this.bundle): '$(config)' should *not* be a copy of '$(staged_config)' because it failed to validate with '$(validate_config)'" if => not(returnszero("$(validate_config) > /dev/null 2>&1 ", "useshell")); } ################################################################## # # cfe_internal_webserver(on/off) # ################################################################## bundle agent cfe_internal_webserver(state) { meta: "description" string => "Manage the Mission Portal webserver state"; classes: "on" expression => strcmp("$(state)","on"), comment => "Check if a keyword \"on\" is inputs", handle => "cfe_internal_webserver_classes_on"; "off" expression => strcmp("$(state)","off"), comment => "Check if a keyword \"off\" is inputs", handle => "cfe_internal_webserver_classes_off"; # processes: am_policy_hub.on:: ".*$(sys.workdir)/httpd/bin/httpd.*" restart_class => "start_cfe_httpd", comment => "Check if CFE httpd process exists or not", handle => "cfe_internal_webserver_processes_start_cfe_httpd"; # commands: start_cfe_httpd:: "LD_LIBRARY_PATH=$(sys.workdir)/lib:$LD_LIBRARY_PATH $(sys.workdir)/httpd/bin/apachectl start" comment => "Start CFE httpd process if not exist", classes => kept_successful_command, handle => "cfe_internal_webserver_commands_start_cfe_httpd", contain => in_shell; } bundle agent cfe_enterprise_selfsigned_cert # @brief Generate a self signed certificate for Mission Portal # # By default this bundle will only regenerate a certificate if one of the # necessary files is missing. To force certificate regeneration run with the # class '_cfe_enterprise_selfsigned_cert_regenerate_certificate' defined. { meta: "description" string => "Generate a self signed SSL certificate for secure hub communications."; vars: policy_server:: "SSLCertificateFile" string => "$(cfe_internal_hub_vars.SSLCertificateFile)"; "SSLCertificateKeyFile" string => "$(cfe_internal_hub_vars.SSLCertificateKeyFile)"; "SSLCertificatePasswdKeyFile" string => "$(cfe_internal_hub_vars.SSLCertificatePasswdKeyFile)"; "SSLCertificateKeySize" string => "$(cfe_internal_hub_vars.SSLCertificateKeySize)"; "SSLCertificateSigningRequest" string => "$(cfe_internal_hub_vars.SSLCertificateSigningRequest)"; "SSLCnf" string => "$(cfe_internal_hub_vars.SSLCnf)"; "SSLCertificateDaysValid" string => "$(cfe_internal_hub_vars.SSLCertificateDaysValid)"; "SSLCertificateFileSymlink" string => "$(sys.workdir)/ssl/cert.pem"; "openssl" string => "$(sys.workdir)/bin/openssl"; "_generated_files" slist => { $(SSLCertificateFile), $(SSLCertificateKeyFile), $(SSLCertificatePasswdKeyFile), $(SSLCertificateSigningRequest), }; classes: "_cfe_enterprise_selfsigned_cert_regenerate_certificate" not => filesexist( @(_generated_files) ), comment => "We only want to automatically regenerate the certificate if one of the files does not exist."; commands: policy_server._cfe_enterprise_selfsigned_cert_regenerate_certificate:: "$(openssl)" args => "genrsa -passout pass:x -out $(SSLCertificatePasswdKeyFile) $(SSLCertificateKeySize)"; # Strip password from key in ${CFENGINE_MP_PASS_KEY} and produce ${CFENGINE_MP_KEY} "$(openssl)" args => "rsa -passin pass:x -in $(SSLCertificatePasswdKeyFile) -out $(SSLCertificateKeyFile)"; # Generate a CSR in ${CFENGINE_MP_CSR} with key ${CFENGINE_MP_KEY} "$(openssl)" args => 'req -utf8 -sha256 -nodes -new -subj "/CN=$(sys.fqhost)" -key $(SSLCertificateKeyFile) -out $(SSLCertificateSigningRequest) -config $(SSLCnf)'; # Generate CRT "$(openssl)" args => "x509 -req -days $(SSLCertificateDaysValid) -in $(SSLCertificateSigningRequest) -signkey $(SSLCertificateKeyFile) -out $(SSLCertificateFile)"; files: "$(SSLCertificateFile)" -> { "jira:ENT-760" } perms => mog("644", "root", "root"), comment => "The SSL Certificate File needs to be readable so that it can be read by other services like Mission Portal."; "$(SSLCertificateKeyFile)" perms => mog("600", "root", "root"), comment => "The SSL Certificate Key should only be readable by root."; "$(SSLCertificatePasswdKeyFile)" perms => mog("600", "root", "root"), comment => "The SSL Certificate Key should only be readable by root."; "$(SSLCertificateFileSymlink)" -> { "jira:ENT-760" } link_from => ln_s( $(SSLCertificateFile) ), move_obstructions => "true", comment => "Mission Portal reads the certificate from this stable location, so it must always point to the current certificate."; reports: DEBUG|DEBUG_cfe_enterprise_selfsigned_cert:: "DEBUG $(this.bundle): Certificate Generation Requested" if => "_cfe_enterprise_selfsigned_cert_regenerate_certificate"; "DEBUG $(this.bundle): No Certificate Generation Requested" if => "!_cfe_enterprise_selfsigned_cert_regenerate_certificate"; }