Ambari插件开发 Apache Ambari项目旨在通过开发用于配置,管理和监控Apache Hadoop集群的软件,使Hadoop管理更简单。Ambari提供了一个直观的,易于使用的Hadoop管理Web UI,由其RESTful API支持。但有时候有一些自定义非官方的需求,这时就需要基于ambari做二次开发了。
Ambari是支持服务扩展的,只需要,编写自定义服务相关配置、脚本,然后扔到它的资源目录,重启服务器就可以了。听起来很简单。它的服务组织结构是分Stack / Service /Component 3层的 。
开发的一个java 服务插件定义目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [daxiang@ark1 common-services]$ pwd  /var/lib/ambari-server/resources/common-services [daxiang@ark1 common-services]$ tree ARK_DATA_API/ ARK_DATA_API/ └── 0.0.1     ├── alerts.json     ├── configuration     │   ├── ark-data-api-env.xml     │   └── ark-data-api-server.xml     ├── metainfo.xml     └── package         ├── archive.zip         ├── scripts         │   ├── ark_app_api.py         │   ├── params.py         │   └── status_params.py         └── templates             └── application.properties.j2 5 directories, 9 files 
其中 ARK_DATA_API 为服务名
metainfo.xml:服务定义描述文件 
alerts.json: 定义了告警的配置 
configuration:目录配置了服务允许所需要的环境参数 和 运行参数,这些参数可以通过ambari界面进行管理和修改。 
package:包括scripts 和 templates,scripts 定义了从安装、配置、启动、停止和状态的脚本,我这里是python。template 程序运行的配置文件,通过ambari 界面管理的参数 通过这个模版进行替换,生成程序的最终配置,供程序运行使用 
 
metainfo.xml 配置说明(我的服务比较简单,就借用参考文章 的做下记录)
ambari-server/resources/common-services/{A_SERVICE}/metainfo.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <?xml version="1.0"?> <metainfo >     <schemaVersion > 2.0</schemaVersion >      <services >          <service >              <name > ELASTICSEARCH</name >              <displayName > ElasticSearch</displayName >              <comment > ElasticSearch service</comment >              <version > 1.4.0</version >              <components >                  <component >                      <name > ELASTICSEARCH</name >                      <displayName > ElasticSearch</displayName >                      <category > MASTER</category >                      <cardinality > 1</cardinality >                      <commandScript >                          <script > scripts/master.py</script >                          <scriptType > PYTHON</scriptType >                          <timeout > 600</timeout >                      </commandScript >                  </component >                  <component >                      <name > ELASTICSEARCH_NODE</name >                      <displayName > ElasticSearchNode</displayName >                      <category > SLAVE</category >                      <cardinality > ALL</cardinality >                       <auto-deploy >                           <enabled > false</enabled >                      </auto-deploy >                      <commandScript >                          <script > scripts/slave.py</script >                          <scriptType > PYTHON</scriptType >                          <timeout > 600</timeout >                      </commandScript >                                       </component >              </components >              <osSpecifics >                  <osSpecific >                    <osFamily > any</osFamily >                    <packages >                      <package >                        <name > elasticsearch</name >                      </package >                    </packages >                  </osSpecific >              </osSpecifics >              <requiredServices >                  <service > GANGLIA</service >               </requiredServices >              <configuration-dependencies >                <config-type > elasticsearch-env</config-type >              </configuration-dependencies >              <monitoringService > false</monitoringService >           </service >      </services >  </metainfo > 
服务和组件名字一定要大写,不然后面你会全部重新来过的。不要问我为什么知道,我重试了N次。 
package节点的name 很重要。yum install [name]  
category节点组件类型可以有MASTER,SLAVE,CLIENT几种,那p2p集群咋办? 
MASTER不是必须得有,只有SLAVE也可以 
cardinality节点是安装基数。1+ 最少一个 ,  0-1 最多一个 ,ALL 全部 
 
一个基础的主配置脚本如下:
master.py
master.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import  sysfrom  resource_management import  *class  Master (Script) :  def  install (self, env) :     print  'Install the ES Master' ;   def  stop (self, env) :     print  'Stop the ES Master' ;   def  start (self, env) :     print  'Start the ES Master' ;         def  status (self, env) :     print  'Status of the ES Master' ;   def  configure (self, env) :     print  'Configure the ES Master' ; if  __name__ == "__main__" :  Master().execute() 
举例(一个自定义jar 服务) OK,现在贴上我的配置。单jar包,已经打成rpm包,运行jar包时使用了ambari生成的配置文件,对项目中的配置文件进行覆盖。
1 2 java -jar $jar_file  --spring.config.location=classpath:/,$conf_file  
服务描述文件:metainfo.xml:  
我这metainfo.xml里面的关键信息包括(信息还是非常少的,少了依赖、集群等等等):scripts/ark_app_api.py;ark-data-api*
metainfo.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?xml version="1.0"?> <metainfo > 	<schemaVersion > 2.0</schemaVersion >  	<services >  		<service >  			<name > ARK_DATA_API</name >  			<displayName > Ark data_api</displayName >  			<comment > 方舟data-api</comment >  			<version > 0.0.1</version >  			<components >  				<component >  					<name > ARK_DATA_API</name >  					<displayName > Ark data_api</displayName >  					<category > MASTER</category >  					<cardinality > 1</cardinality >  					<timelineAppid > arkb</timelineAppid >  					<commandScript >  						<script > scripts/ark_app_api.py</script >  						<scriptType > PYTHON</scriptType >  						<timeout > 600</timeout >  					</commandScript >  				</component >  			</components >  			<osSpecifics >  				<osSpecific >  					<osFamily > any</osFamily >  					<packages >  						<package >  							<name > ark-data-api*</name >  							<skipUpgrade > true</skipUpgrade >  						</package >  					</packages >  				</osSpecific >  			</osSpecifics >  		</service >  	</services >  </metainfo > 
其中scripts/ark_app_api.py:也是5个方法,分别是install 、configure、start、stop、status。
install:通过ambari安装 yum install ark-data-api* 
configure: 安装完成后,做的一些配置信息,比如根据模版信息生成相关文件、生成相关的文件夹,改变用户组和权限等等 
start: 如何启动服务。我这是先检测服务允许状态,再通过脚本启动(这个脚本我是打包到rpm包里面的,安装时就放到相关的文件夹中了。而且脚本有维护pid操作) 
stop: 如何关停服务。我这也是脚本 
status: 通过检测pid文件查询服务状态。(上面的服务启停脚本维护了pid文件) 
 
插件主脚本:ark_app_api.py 参考 
package/scripts/ark_app_api.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import  randomimport  sysfrom  resource_management import  *from  resource_management.libraries.script.script import  Scriptfrom  resource_management.libraries.functions import  get_unique_id_and_datefrom  resource_management.libraries.functions import  conf_selectfrom  resource_management.libraries.functions import  stack_selectfrom  resource_management.libraries.functions import  StackFeaturefrom  resource_management.libraries.functions.version import  compare_versions, format_stack_versionfrom  resource_management.libraries.functions.stack_features import  check_stack_featurefrom  resource_management.libraries.functions.security_commons import  build_expectations, \  cached_kinit_executor, get_params_from_filesystem, validate_security_config_properties, \   FILE_TYPE_JAAS_CONF from  resource_management import  *from  resource_management.core.logger import  Loggerfrom  resource_management.core.resources.system import  Executefrom  resource_management.libraries.functions.check_process_status import  check_process_statusfrom  resource_management.libraries.functions.format import  formatfrom  resource_management.libraries.functions.validate import  call_and_match_outputfrom  pickle import  STOPfrom  time import  sleepimport  ambari_simplejson as  jsonclass  Master (Script) :         def  install (self, env) :         import  params         self.install_packages(env)         Execute("chown -R {0}:{1} {2}" .format(params.app_user,params.app_group,params.install_dir))              def  configure (self, env) :     	           import  params         env.set_params(params)         Directory(params.app_conf_dir,             mode=0755 ,             owner=params.app_user,             group=params.app_group,             create_parents = True          )                           File(params.app_conf_dir + '/application.properties' ,             content=Template('application.properties.j2' ),             owner=params.app_user,             group=params.app_group,             mode=0755          )                  Directory(params.install_dir,             mode=0755 ,             owner=params.app_user,             group=params.app_group,             create_parents = True          )                  def  start (self ,env) :         import  params         env.set_params(params)         self.configure(env)                  no_op_test = format("ls {app_pid_path} >/dev/null 2>&1 && ps `cat {app_pid_path}` | grep `cat {app_pid_path}` >/dev/null 2>&1" )                           Execute("sh {0}/startup.sh" .format(params.app_bin_dir),               user=params.app_user,               not_if=no_op_test         )              def  stop (self ,env) :         import  params         env.set_params(params)         self.configure(env)                  no_op_test = ""                            Execute("sh {0}/shutdown.sh" .format(params.app_bin_dir),               user=params.app_user         )              def  status (self, env) :         import  status_params         env.set_params(status_params)                  check_process_status(params.app_pid_path)               if  __name__ == "__main__" :    Master().execute() 
其中脚本中用到的 params.app_bin_dir 和 status_params.app_pid_path 参数,都是来自同目录下定义的参数配置文件 params.py。status_params 就定义了一个pid 文件的路径,就不贴了。
参数定义:params.py 
也可以和 主脚本写在一起,不过参数多了还是分开写方便
params.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 from  resource_management.core.logger import  Loggerfrom  resource_management.libraries.functions import  conf_selectfrom  resource_management.libraries.functions import  stack_selectfrom  resource_management import  *from  resource_management.libraries.functions.get_not_managed_resources import  get_not_managed_resourcesfrom  resource_management.libraries.functions.expect import  expectfrom  ambari_commons.ambari_metrics_helper import  select_metric_collector_hosts_from_hostnamesfrom  ambari_commons import  OSCheckfrom  resource_management.libraries.resources.hdfs_resource import  HdfsResourceimport  osconfig = Script.get_config() exec_tmp_dir = Script.get_tmp_dir() install_dir = '/data/micro-services/8082-data-api-jar'  app_conf_dir = install_dir + '/conf'  app_bin_dir = install_dir + '/bin'  app_user = config['configurations' ]['ark-data-api-env' ]['ark-dataapi-user' ] app_group = config['configurations' ]['ark-data-api-env' ]['ark-dataapi-group' ] app_pid = 'lryhis.pid'  app_pid_path = app_bin_dir + "/"  + app_pid app_server_port = config['configurations' ]['ark-data-api-server' ]['dataapi.app.server.port' ] app_log_file = install_dir + '/logs/application.log'  presto_host = config['configurations' ]['ark-data-api-server' ]['dataapi.presto.host' ] presto_query_timeout = config['configurations' ]['ark-data-api-server' ]['dataapi.presto.query.timeout' ] <!-- 省略部分 --> db_host=config['configurations' ]['ark-data-api-server' ]['dataapi.db.host' ] db_admin_user=config['configurations' ]['ark-data-api-server' ]['dataapi.db.admin.user' ] db_admin_password=config['configurations' ]['ark-data-api-server' ]['dataapi.db.admin.password' ] db_pool_size=config['configurations' ]['ark-data-api-server' ]['dataapi.db.pool.size' ] 
其中config = Script.get_config() 会在当前目录找相关的configurations 配置文件
1 config['configurations']['ark-data-api-server']['dataapi.app.server.port'] 
代表 在configurations 目录下 ark-data-api-server文件中的 dataapi.app.server.port 配置
ambari参数配置文件:ark-data-api-server.xml  参考:
ark-data-api-server.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration  supports_final ="true" >     <property >          <name > dataapi.app.server.port</name >          <value > 8082</value >          <final > true</final >          <description > 服务启动端口</description >      </property >         	     <property >          <name > dataapi.db.admin.user</name >          <value > xxxx</value >          <final > false</final >          <description > </description >      </property >           <property >          <name > dataapi.db.admin.password</name >          <value > xxxx</value >          <property-type > PASSWORD</property-type >          <final > false</final >          <description > </description >      </property >      <property >          <name > dataapi.db.pool.size</name >          <value > 20</value >          <final > false</final >          <description > </description >      </property >  	 </configuration > 
其中 supports_final="true" 和 final=true 表示 该参数不可修改
配置文件模版:application.properties.jr 
这个模版文件通过特殊占位符,在定义configure 方法中替换占位符,生成application.properties到指定目录。 而这个配置文件并不是全部配置项,而是一个覆盖的。前面说了启动时 指定了配置文件 为 classpath:/,$conf_file,这个配置文件会覆盖classpath 目录下的配置
package/scripts/ark_app_api.py 1 2 3 4 5 6 7         File(params.app_conf_dir + '/application.properties' ,             content=Template('application.properties.j2' ),             owner=params.app_user,             group=params.app_group,             mode=0755          ) 
最后上一个模版文件:
application.properties.j2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 server.port ={{app_server_port}} jdbc.presto.driverClassName  = com.facebook.presto.jdbc.PrestoDriver jdbc.presto.url =jdbc:presto://{{presto_host}}/hive/default jdbc.presto.query.timeout ={{presto_query_timeout}} dbHost ={{db_host}} dbUser ={{db_admin_user}} dbPassword ={{db_admin_password}} jdbc.mysql.driverClassName  =com.mysql.jdbc.Driver jdbc.mysql.url =jdbc:mysql://${dbHost}:3306/database_xxx?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8 jdbc.mysql.username =${dbUser} jdbc.mysql.password =${dbPassword} jdbc.mysql.maxActive  = {{db_pool_size}} spring.redis.host ={{redis_host}} spring.redis.port ={{redis_port}} 
从这里可以看出来,特殊占位符为双大括号, 占位符里面的值为 params.py 文件内定义的。例如 app_server_port 
开发插件后的部署 定义 stacks 
与参考文章不一样的是,我的服务插件直接塞到了HDP目录下。所以stacks里只填写了服务信息在哪里,其他都是在common-services 里面定义好了。
common-services 和 stacks 处于同一目录
服务的定义配置文件都放在:
1 /var/lib/ambari-server/resources/common-services/ARK_DATA_API/0.0.1 
stacks定义文件放在:
1 /var/lib/ambari-server/resources/stacks/HDP/2.0.6/services/ARK_DATA_API/metainfo.xml 
metainfo.xml: 
告诉ambari该插件相关信息映射到 common-services/ARK_DATA_API/0.0.1下的metainfo.xml 
metainfo.xml 1 2 3 4 5 6 7 8 9 <metainfo >   <schemaVersion > 2.0</schemaVersion >    <services >      <service >        <name > ARK_DATA_API</name >        <extends > common-services/ARK_DATA_API/0.0.1</extends >      </service >    </services >  </metainfo > 
部署 
将相关文件复制到ambari 的common-services目录和stacks目录 ,ambari-server restart 即可
参考: https://my.oschina.net/naqin/blog/389720