diff --git a/extensions/appdynamics/__init__.py b/extensions/appdynamics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/appdynamics/extension.py b/extensions/appdynamics/extension.py new file mode 100644 index 000000000..03ac79957 --- /dev/null +++ b/extensions/appdynamics/extension.py @@ -0,0 +1,213 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +"""AppDynamics Extension + +Downloads, installs and configures the AppDynamics agent for PHP +""" +import os +import os.path +import logging + + +_log = logging.getLogger('appdynamics') + + +#DEFAULTS = { +# 'APPDYNAMICS_HOST': 's3-us-west-2.amazonaws.com/niksappd', +# 'APPDYNAMICS_VERSION': '4.1.1.0', +# 'APPDYNAMICS_PACKAGE': 'appdynamics-php-agent-x64-linux-{APPDYNAMICS_VERSION}.tar.gz', +# 'APPDYNAMICS_DOWNLOAD_URL': 'https://{APPDYNAMICS_HOST}/php_agent/' +# 'archive/{APPDYNAMICS_VERSION}/{APPDYNAMICS_PACKAGE}', +#} + +DEFAULTS = { +'APPDYNAMICS_HOST': 'packages.appdynamics.com', +'APPDYNAMICS_VERSION': '4.1.5.0', +'APPDYNAMICS_PACKAGE': 'appdynamics-php-agent-x64-linux-{APPDYNAMICS_VERSION}.tar.bz2', +'APPDYNAMICS_DOWNLOAD_URL': 'https://{APPDYNAMICS_HOST}/' + 'php/{APPDYNAMICS_VERSION}/{APPDYNAMICS_PACKAGE}', +} + +class AppDynamicsInstaller(object): + def __init__(self, ctx): + self._log = _log + self._ctx = ctx + self._detected = False + self.app_name = None + self.account_access_key = None + try: + self._log.info("Initializing") + if ctx['PHP_VM'] == 'php': + self._merge_defaults() + self._load_service_info() + self._load_php_info() + self._load_appdynamics_info() + except Exception: + self._log.exception("Error installing AppDynamics! " + "AppDynamics will not be available.") + + def _merge_defaults(self): + for key, val in DEFAULTS.iteritems(): + if key not in self._ctx: + self._ctx[key] = val + + def _load_service_info(self): + self._log.info("Loading AppDynamics service info.") + services = self._ctx.get('VCAP_SERVICES', {}) + service_defs = services.get('appdynamics', []) + if len(service_defs) == 0: + self._log.info("AppDynamics services with tag appdynamics not detected.") + self._log.info("Looking for tag app-dynamics service.") + service_defs = services.get('app-dynamics', []) + if len(service_defs) == 0: + self._log.info("AppDynamics services with tag app-dynamics not detected.") + self._log.info("Looking for Appdynamics user-provided service.") + service_defs = services.get('user-provided', []) + if len(service_defs) == 0: + self._log.info("AppDynamics services not detected.") + if len(service_defs) > 1: + self._log.warn("Multiple AppDynamics services found, " + "credentials from first one.") + if len(service_defs) > 0: + service = service_defs[0] + creds = service.get('credentials', {}) + self.account_access_key = creds.get('account-access-key', None) + if self.account_access_key: + self._log.debug("AppDynamics service detected.") + self._detected = True + + def _load_appdynamics_info(self): + vcap_app = self._ctx.get('VCAP_APPLICATION', {}) + self.app_name = vcap_app.get('name', None) + self._log.debug("App Name [%s]", self.app_name) + + if 'APPDYNAMICS_LICENSE' in self._ctx.keys(): + if self._detected: + self._log.warn("Detected a AppDynamics Service & Manual Key," + " using the manual key.") + self.license_key = self._ctx['APPDYNAMICS_LICENSE'] + self._detected = True + + if self._detected: + appdynamics_so_name = 'appdynamics-%s%s.so' % ( + self._php_api, (self._php_zts and 'zts' or '')) + self.appdynamics_so = os.path.join('@{HOME}', 'appdynamics', + 'agent', self._php_arch, + appdynamics_so_name) + self._log.debug("PHP Extension [%s]", self.appdynamics_so) + self.log_path = os.path.join('@{HOME}', 'logs', + 'appdynamics-daemon.log') + self._log.debug("Log Path [%s]", self.log_path) + self.daemon_path = os.path.join( + '@{HOME}', 'appdynamics', 'daemon', + 'appdynamics-daemon.%s' % self._php_arch) + self._log.debug("Daemon [%s]", self.daemon_path) + self.socket_path = os.path.join('@{HOME}', 'appdynamics', + 'daemon.sock') + self._log.debug("Socket [%s]", self.socket_path) + self.pid_path = os.path.join('@{HOME}', 'appdynamics', + 'daemon.pid') + self._log.debug("Pid File [%s]", self.pid_path) + + def _load_php_info(self): + self.php_ini_path = os.path.join(self._ctx['BUILD_DIR'], + 'php', 'etc', 'php.ini') + self._php_extn_dir = self._find_php_extn_dir() + self._php_api, self._php_zts = self._parse_php_api() + self._php_arch = self._ctx.get('APPDYNAMICS_ARCH', 'x64') + self._log.debug("PHP API [%s] Arch [%s]", + self._php_api, self._php_arch) + + def _find_php_extn_dir(self): + with open(self.php_ini_path, 'rt') as php_ini: + for line in php_ini.readlines(): + if line.startswith('extension_dir'): + (key, val) = line.strip().split(' = ') + return val.strip('"') + + def _parse_php_api(self): + tmp = os.path.basename(self._php_extn_dir) + php_api = tmp.split('-')[-1] + php_zts = (tmp.find('non-zts') == -1) + return php_api, php_zts + + def should_install(self): + return self._detected + +# Extension Methods +def preprocess_commands(ctx): + + service = ctx.get('VCAP_SERVICES', {}) + service_defs = service.get('appdynamics', []) + detected = False + if len(service_defs) == 0: + _log.info("AppDynamics services with tag appdynamics not detected.") + _log.info("Looking for tag app-dynamics service.") + service_defs = service.get('app-dynamics', []) + if len(service_defs) == 0: + _log.info("AppDynamics services with tag app-dynamics not detected.") + _log.info("Looking for Appdynamics user-provided service.") + cups_service_defs = service.get('user-provided', []) + + if len(cups_service_defs) == 0: + _log.info("AppDynamics services not detected.") + else: + cups_svc = cups_service_defs.get('name', []) + if cups_svc == "appdynamics" || cups_svc == "app-dynamics": + _log.info("AppDynamics cups services detected.") + detected = True + + if len(service_defs) > 0: + _log.debug("AppDynamics service detected.") + detected = True + + if detected == True: + exit_code = os.system("echo preprocess_commands: AppDynamics agent configuration") + return [[ 'echo', '" in preprocess;"'], + ['env'], + [ 'chmod', ' -R 755 /home/vcap/app'], + [ 'chmod', ' 777 ./app/appdynamics/appdynamics-php-agent/logs'], + [ 'export', ' APP_TIERNAME=`echo $VCAP_APPLICATION | sed -e \'s/.*application_name.:.//g;s/\".*application_uri.*//g\' `'], + [ 'if [ -z $application_name ]; then export APP_NAME=$APP_TIERNAME && APP_TIERNAME=$APP_TIERNAME-tier; else export APP_NAME=$application_name; fi'], + [ 'export', ' APP_HOSTNAME=$APP_TIERNAME-`echo $VCAP_APPLICATION | sed -e \'s/.*instance_index.://g;s/\".*host.*//g\' | sed \'s/,//\' `'], + [ 'export', ' AD_ACCOUNT_NAME=`echo $VCAP_SERVICES | sed -e \'s/.*account-name.:.//g;s/\".*port.*//g\' `'], + [ 'export', ' AD_ACCOUNT_ACCESS_KEY=`echo $VCAP_SERVICES | sed -e \'s/.*account-access-key.:.//g;s/\".*host-name.*//g\' `'], + [ 'export', ' AD_CONTROLLER=`echo $VCAP_SERVICES | sed -e \'s/.*host-name.:.//g;s/\".*ssl-enabled.*//g\' `'], + [ 'export', ' AD_PORT=`echo $VCAP_SERVICES | sed -e \'s/.*port.:.//g;s/\".*account-access-key.*//g\' `'], + [ 'export', ' sslenabled=`echo $VCAP_SERVICES | sed -e \'s/.*ssl-enabled.:.//g;s/\".*.*//g\'`'], + [ 'if [ $sslenabled == \"true\" ] ; then export sslflag=-s ; fi; '], + [ 'echo sslflag set to $sslflag' ], + [ 'PATH=$PATH:./app/php/bin/ ./app/appdynamics/appdynamics-php-agent/install.sh $sslflag -i ./app/appdynamics/phpini -a=$AD_ACCOUNT_NAME@$AD_ACCOUNT_ACCESS_KEY $AD_CONTROLLER $AD_PORT $APP_NAME $APP_TIERNAME $APP_HOSTNAME' ], + [ 'cat', ' /home/vcap/app/appdynamics/phpini/appdynamics_agent.ini >> /home/vcap/app/php/etc/php.ini'], + [ 'cat', ' /home/vcap/app/appdynamics/phpini/appdynamics_agent.ini'], + [ 'echo', '"done preprocess"'], + ['env']] + else: + return () + +def service_commands(ctx): + return {} + + +def service_environment(ctx): + return {} + +def compile(install): + appdynamics = AppDynamicsInstaller(install.builder._ctx) + if appdynamics.should_install(): + _log.info("Installing AppDynamics") + install.package('APPDYNAMICS') + _log.info("AppDynamics Installed.") + return 0 diff --git a/manifest.yml b/manifest.yml index 28b226c5b..60e4cf677 100644 --- a/manifest.yml +++ b/manifest.yml @@ -15,6 +15,9 @@ exclude_files: - php_buildpack-*v* url_to_dependency_map: + - match: appdynamics-php-agent-x64-linux-(\d+\.\d+\.\d+\.\d+) + name: appdynamics + version: "$1" - match: newrelic-php5-(\d+\.\d+\.\d+\.\d+)-linux name: newrelic version: "$1" @@ -26,6 +29,12 @@ url_to_dependency_map: version: "$1" dependencies: + - name: appdynamics + version: 4.1.1.0 + uri: https://s3-us-west-2.amazonaws.com/niksappd/appdynamics-php-agent-x64-linux-4.1.1.0.tar.gz + cf_stacks: + - cflinuxfs2 + md5: cd9dbe7e3cc51031db1e429fbc934b64 - name: newrelic version: 4.23.3.111 uri: https://download.newrelic.com/php_agent/archive/4.23.3.111/newrelic-php5-4.23.3.111-linux.tar.gz diff --git a/tests/test_appdynamics.py b/tests/test_appdynamics.py new file mode 100644 index 000000000..f5c3dd921 --- /dev/null +++ b/tests/test_appdynamics.py @@ -0,0 +1,276 @@ +import os +import os.path +import tempfile +import shutil +import json +from nose.tools import eq_ +from nose.tools import with_setup +from build_pack_utils import utils +from common.integration import ErrorHelper +from common.components import BuildPackAssertHelper +from common.components import HttpdAssertHelper +from common.components import PhpAssertHelper +from common.components import NoWebServerAssertHelper +from common.components import NewRelicAssertHelper +from common.components import HhvmAssertHelper +from common.components import DownloadAssertHelper +from common.base import BaseCompileApp + + +newrelic = utils.load_extension('extensions/newrelic') + + +class TestNewRelic(object): + def setUp(self): + self.build_dir = tempfile.mkdtemp('build-') + self.php_dir = os.path.join(self.build_dir, 'php', 'etc') + os.makedirs(self.php_dir) + shutil.copy('defaults/config/php/5.4.x/php.ini', self.php_dir) + + def tearDown(self): + if os.path.exists(self.build_dir): + shutil.rmtree(self.build_dir) + + def testDefaults(self): + nr = newrelic.NewRelicInstaller(utils.FormattedDict({ + 'BUILD_DIR': self.build_dir, + 'PHP_VM': 'php' + })) + eq_(True, 'APPDYNAMICS_HOST' in nr._ctx.keys()) + eq_(True, 'APPDYNAMICS_VERSION' in nr._ctx.keys()) + eq_(True, 'APPDYNAMICS_PACKAGE' in nr._ctx.keys()) + eq_(True, 'APPDYNAMICS_DOWNLOAD_URL' in nr._ctx.keys()) + eq_(True, 'APPDYNAMICS_STRIP' in nr._ctx.keys()) + + def testShouldNotInstall(self): + nr = newrelic.NewRelicInstaller(utils.FormattedDict({ + 'BUILD_DIR': self.build_dir + })) + eq_(False, nr.should_install()) + + @with_setup(setup=setUp, teardown=tearDown) + def testShouldInstall(self): + ctx = utils.FormattedDict({ + 'BUILD_DIR': self.build_dir, + 'APPDYNAMICS_LICENSE': 'JUNK_LICENSE', + 'VCAP_APPLICATION': { + 'name': 'app-name-1' + }, + 'PHP_VM': 'php' + }) + nr = newrelic.NewRelicInstaller(ctx) + eq_(True, nr.should_install()) + eq_('x64', nr._php_arch) + eq_('@{HOME}/php/lib/php/extensions/no-debug-non-zts-20100525', + nr._php_extn_dir) + eq_(False, nr._php_zts) + eq_('20100525', nr._php_api) + eq_('@{HOME}/newrelic/agent/x64/newrelic-20100525.so', nr.newrelic_so) + eq_('app-name-1', nr.app_name) + eq_('JUNK_LICENSE', nr.license_key) + eq_('@{HOME}/logs/newrelic-daemon.log', nr.log_path) + eq_('@{HOME}/newrelic/daemon/newrelic-daemon.x64', nr.daemon_path) + eq_('@{HOME}/newrelic/daemon.sock', nr.socket_path) + eq_('@{HOME}/newrelic/daemon.pid', nr.pid_path) + + @with_setup(setup=setUp, teardown=tearDown) + def testShouldInstallService(self): + ctx = utils.FormattedDict({ + 'BUILD_DIR': self.build_dir, + 'VCAP_SERVICES': { + 'newrelic': [{ + 'name': 'newrelic', + 'label': 'newrelic', + 'tags': ['Monitoring'], + 'plan': 'standard', + 'credentials': {'licenseKey': 'LICENSE'}}] + }, + 'VCAP_APPLICATION': { + 'name': 'app-name-1' + }, + 'PHP_VM': 'php' + }) + nr = newrelic.NewRelicInstaller(ctx) + eq_(True, nr.should_install()) + eq_('x64', nr._php_arch) + eq_('@{HOME}/php/lib/php/extensions/no-debug-non-zts-20100525', + nr._php_extn_dir) + eq_(False, nr._php_zts) + eq_('20100525', nr._php_api) + eq_('@{HOME}/newrelic/agent/x64/newrelic-20100525.so', nr.newrelic_so) + eq_('app-name-1', nr.app_name) + eq_('LICENSE', nr.license_key) + eq_('@{HOME}/logs/newrelic-daemon.log', nr.log_path) + eq_('@{HOME}/newrelic/daemon/newrelic-daemon.x64', nr.daemon_path) + eq_('@{HOME}/newrelic/daemon.sock', nr.socket_path) + eq_('@{HOME}/newrelic/daemon.pid', nr.pid_path) + + @with_setup(setup=setUp, teardown=tearDown) + def testShouldInstallServiceAndManual(self): + ctx = utils.FormattedDict({ + 'BUILD_DIR': self.build_dir, + 'VCAP_SERVICES': { + 'newrelic': [{ + 'name': 'newrelic', + 'label': 'newrelic', + 'tags': ['Monitoring'], + 'plan': 'standard', + 'credentials': {'licenseKey': 'LICENSE'}}] + }, + 'APPDYNAMICS_LICENSE': 'LICENSE2', + 'VCAP_APPLICATION': { + 'name': 'app-name-2' + }, + 'PHP_VM': 'php' + }) + nr = newrelic.NewRelicInstaller(ctx) + eq_(True, nr.should_install()) + eq_('x64', nr._php_arch) + eq_('@{HOME}/php/lib/php/extensions/no-debug-non-zts-20100525', + nr._php_extn_dir) + eq_(False, nr._php_zts) + eq_('20100525', nr._php_api) + eq_('@{HOME}/newrelic/agent/x64/newrelic-20100525.so', nr.newrelic_so) + eq_('app-name-2', nr.app_name) + eq_('LICENSE2', nr.license_key) + eq_('@{HOME}/logs/newrelic-daemon.log', nr.log_path) + eq_('@{HOME}/newrelic/daemon/newrelic-daemon.x64', nr.daemon_path) + eq_('@{HOME}/newrelic/daemon.sock', nr.socket_path) + eq_('@{HOME}/newrelic/daemon.pid', nr.pid_path) + + @with_setup(setup=setUp, teardown=tearDown) + def testModifyPhpIni(self): + ctx = utils.FormattedDict({ + 'BUILD_DIR': self.build_dir, + 'APPDYNAMICS_LICENSE': 'JUNK_LICENSE', + 'VCAP_APPLICATION': { + 'name': 'app-name-1' + }, + 'PHP_VM': 'php' + }) + nr = newrelic.NewRelicInstaller(ctx) + nr.modify_php_ini() + with open(os.path.join(self.php_dir, 'php.ini'), 'rt') as php_ini: + lines = php_ini.readlines() + eq_(True, lines.index('extension=%s\n' % nr.newrelic_so) >= 0) + eq_(True, lines.index('[newrelic]\n') >= 0) + eq_(True, lines.index('newrelic.license=JUNK_LICENSE\n') >= 0) + eq_(True, lines.index('newrelic.appname=%s\n' % nr.app_name) >= 0) + + +class TestNewRelicCompiled(BaseCompileApp): + def __init__(self): + self.app_name = 'app-1' + + def setUp(self): + BaseCompileApp.setUp(self) + os.environ['APPDYNAMICS_LICENSE'] = 'JUNK_LICENSE' + os.environ['VCAP_APPLICATION'] = json.dumps({ + 'name': 'app-name-1' + }) + + def test_with_httpd_and_newrelic(self): + # helpers to confirm the environment + bp = BuildPackAssertHelper() + nr = NewRelicAssertHelper() + httpd = HttpdAssertHelper() + php = PhpAssertHelper() + # set web server to httpd, since that's what we're expecting here + self.opts.set_web_server('httpd') + # run the compile step of the build pack + output = ErrorHelper().compile(self.bp) + # confirm downloads + DownloadAssertHelper(3, 2).assert_downloads_from_output(output) + # confirm start script + bp.assert_start_script_is_correct(self.build_dir) + httpd.assert_start_script_is_correct(self.build_dir) + php.assert_start_script_is_correct(self.build_dir) + # confirm bp utils installed + bp.assert_scripts_are_installed(self.build_dir) + bp.assert_config_options(self.build_dir) + # check env & proc files + httpd.assert_contents_of_procs_file(self.build_dir) + httpd.assert_contents_of_env_file(self.build_dir) + php.assert_contents_of_procs_file(self.build_dir) + php.assert_contents_of_env_file(self.build_dir) + # webdir exists + httpd.assert_web_dir_exists(self.build_dir, self.opts.get_webdir()) + # check php & httpd installed + httpd.assert_files_installed(self.build_dir) + php.assert_files_installed(self.build_dir) + nr.assert_files_installed(self.build_dir) + + def test_with_httpd_hhvm_and_newrelic(self): + # helpers to confirm the environment + bp = BuildPackAssertHelper() + nr = NewRelicAssertHelper() + httpd = HttpdAssertHelper() + hhvm = HhvmAssertHelper() + # set web server to httpd, since that's what we're expecting here + self.opts.set_php_vm('hhvm') + self.opts.set_hhvm_download_url( + '{DOWNLOAD_URL}/hhvm/{HHVM_VERSION}/{HHVM_PACKAGE}') + self.opts.set_web_server('httpd') + # run the compile step of the build pack + output = ErrorHelper().compile(self.bp) + # confirm downloads + DownloadAssertHelper(2, 2).assert_downloads_from_output(output) + # confirm start script + bp.assert_start_script_is_correct(self.build_dir) + httpd.assert_start_script_is_correct(self.build_dir) + hhvm.assert_start_script_is_correct(self.build_dir) + # confirm bp utils installed + bp.assert_scripts_are_installed(self.build_dir) + bp.assert_config_options(self.build_dir) + # check env & proc files + httpd.assert_contents_of_procs_file(self.build_dir) + httpd.assert_contents_of_env_file(self.build_dir) + hhvm.assert_contents_of_procs_file(self.build_dir) + hhvm.assert_contents_of_env_file(self.build_dir) + # webdir exists + httpd.assert_web_dir_exists(self.build_dir, self.opts.get_webdir()) + # check php & httpd installed + httpd.assert_files_installed(self.build_dir) + hhvm.assert_files_installed(self.build_dir) + # Test NewRelic should not be installed w/HHVM + nr.is_not_installed(self.build_dir) + + +class TestNewRelicWithApp5(BaseCompileApp): + def __init__(self): + self.app_name = 'app-5' + + def setUp(self): + BaseCompileApp.setUp(self) + os.environ['APPDYNAMICS_LICENSE'] = 'JUNK_LICENSE' + os.environ['VCAP_APPLICATION'] = json.dumps({ + 'name': 'app-name-1' + }) + + def test_standalone(self): + # helpers to confirm the environment + bp = BuildPackAssertHelper() + php = PhpAssertHelper() + none = NoWebServerAssertHelper() + nr = NewRelicAssertHelper() + # no web server + self.opts.set_web_server('none') + # run the compile step of the build pack + output = ErrorHelper().compile(self.bp) + # confirm downloads + DownloadAssertHelper(2, 1).assert_downloads_from_output(output) + # confirm httpd and nginx are not installed + none.assert_no_web_server_is_installed(self.build_dir) + # confirm start script + bp.assert_start_script_is_correct(self.build_dir) + php.assert_start_script_is_correct(self.build_dir) + # confirm bp utils installed + bp.assert_scripts_are_installed(self.build_dir) + # check env & proc files + none.assert_contents_of_procs_file(self.build_dir) + php.assert_contents_of_env_file(self.build_dir) + # webdir exists + none.assert_no_web_dir(self.build_dir, self.opts.get_webdir()) + # check php cli installed + none.assert_files_installed(self.build_dir) + nr.assert_files_installed(self.build_dir)