script.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. #!/usr/bin/env python
  2. '''
  3. Licensed to the Apache Software Foundation (ASF) under one
  4. or more contributor license agreements. See the NOTICE file
  5. distributed with this work for additional information
  6. regarding copyright ownership. The ASF licenses this file
  7. to you under the Apache License, Version 2.0 (the
  8. "License"); you may not use this file except in compliance
  9. with the License. You may obtain a copy of the License at
  10. http://www.apache.org/licenses/LICENSE-2.0
  11. Unless required by applicable law or agreed to in writing, software
  12. distributed under the License is distributed on an "AS IS" BASIS,
  13. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. See the License for the specific language governing permissions and
  15. limitations under the License.
  16. '''
  17. import tempfile
  18. __all__ = ["Script"]
  19. import os
  20. import sys
  21. import json
  22. import logging
  23. import platform
  24. from ambari_commons.os_check import OSCheck
  25. from resource_management.libraries.resources import XmlConfig
  26. from resource_management.libraries.resources import PropertiesFile
  27. from resource_management.core.resources import File, Directory
  28. from resource_management.core.source import InlineTemplate
  29. from resource_management.core.environment import Environment
  30. from resource_management.core.logger import Logger
  31. from resource_management.core.exceptions import Fail, ClientComponentHasNoStatus, ComponentIsNotRunning
  32. from resource_management.core.resources.packaging import Package
  33. from resource_management.libraries.functions.version_select_util import get_component_version
  34. from resource_management.libraries.functions.version import compare_versions
  35. from resource_management.libraries.functions.version import format_hdp_stack_version
  36. from resource_management.libraries.script.config_dictionary import ConfigDictionary, UnknownConfiguration
  37. from resource_management.core.resources.system import Execute
  38. if OSCheck.is_windows_family():
  39. from resource_management.libraries.functions.install_hdp_msi import install_windows_msi
  40. from resource_management.libraries.functions.reload_windows_env import reload_windows_env
  41. from resource_management.libraries.functions.zip_archive import archive_dir
  42. from resource_management.libraries.resources import Msi
  43. else:
  44. from resource_management.libraries.functions.tar_archive import archive_dir
  45. USAGE = """Usage: {0} <COMMAND> <JSON_CONFIG> <BASEDIR> <STROUTPUT> <LOGGING_LEVEL> <TMP_DIR>
  46. <COMMAND> command type (INSTALL/CONFIGURE/START/STOP/SERVICE_CHECK...)
  47. <JSON_CONFIG> path to command json file. Ex: /var/lib/ambari-agent/data/command-2.json
  48. <BASEDIR> path to service metadata dir. Ex: /var/lib/ambari-agent/cache/common-services/HDFS/2.1.0.2.0/package
  49. <STROUTPUT> path to file with structured command output (file will be created). Ex:/tmp/my.txt
  50. <LOGGING_LEVEL> log level for stdout. Ex:DEBUG,INFO
  51. <TMP_DIR> temporary directory for executable scripts. Ex: /var/lib/ambari-agent/data/tmp
  52. """
  53. _PASSWORD_MAP = {"/configurations/cluster-env/hadoop.user.name":"/configurations/cluster-env/hadoop.user.password"}
  54. def get_path_from_configuration(name, configuration):
  55. subdicts = filter(None, name.split('/'))
  56. for x in subdicts:
  57. if x in configuration:
  58. configuration = configuration[x]
  59. else:
  60. return None
  61. return configuration
  62. class Script(object):
  63. """
  64. Executes a command for custom service. stdout and stderr are written to
  65. tmpoutfile and to tmperrfile respectively.
  66. Script instances share configuration as a class parameter and therefore
  67. different Script instances can not be used from different threads at
  68. the same time within a single python process
  69. Accepted command line arguments mapping:
  70. 1 command type (START/STOP/...)
  71. 2 path to command json file
  72. 3 path to service metadata dir (Directory "package" inside service directory)
  73. 4 path to file with structured command output (file will be created)
  74. """
  75. structuredOut = {}
  76. command_data_file = ""
  77. basedir = ""
  78. stroutfile = ""
  79. logging_level = ""
  80. # Class variable
  81. tmp_dir = ""
  82. def get_stack_to_component(self):
  83. """
  84. To be overridden by subclasses.
  85. Returns a dictionary where the key is a stack name, and the value is the component name used in selecting the version.
  86. """
  87. return {}
  88. def load_structured_out(self):
  89. Script.structuredOut = {}
  90. if os.path.exists(self.stroutfile):
  91. with open(self.stroutfile, 'r') as fp:
  92. Script.structuredOut = json.load(fp)
  93. # version is only set in a specific way and should not be carried
  94. if "version" in Script.structuredOut:
  95. del Script.structuredOut["version"]
  96. # reset security issues and errors found on previous runs
  97. if "securityIssuesFound" in Script.structuredOut:
  98. del Script.structuredOut["securityIssuesFound"]
  99. if "securityStateErrorInfo" in Script.structuredOut:
  100. del Script.structuredOut["securityStateErrorInfo"]
  101. def put_structured_out(self, sout):
  102. curr_content = Script.structuredOut.copy()
  103. Script.structuredOut.update(sout)
  104. try:
  105. with open(self.stroutfile, 'w') as fp:
  106. json.dump(Script.structuredOut, fp)
  107. except IOError, err:
  108. Script.structuredOut.update({"errMsg" : "Unable to write to " + self.stroutfile})
  109. def save_component_version_to_structured_out(self):
  110. """
  111. :param stack_name: One of HDP, HDPWIN, PHD, BIGTOP.
  112. :return: Append the version number to the structured out.
  113. """
  114. from resource_management.libraries.functions.default import default
  115. stack_name = default("/hostLevelParams/stack_name", None)
  116. stack_to_component = self.get_stack_to_component()
  117. if stack_to_component and stack_name:
  118. component_name = stack_to_component[stack_name] if stack_name in stack_to_component else None
  119. component_version = get_component_version(stack_name, component_name)
  120. if component_version:
  121. self.put_structured_out({"version": component_version})
  122. def should_expose_component_version(self, command_name):
  123. """
  124. Analyzes config and given command to determine if stack version should be written
  125. to structured out. Currently only HDP stack versions >= 2.2 are supported.
  126. :param command_name: command name
  127. :return: True or False
  128. """
  129. from resource_management.libraries.functions.default import default
  130. stack_version_unformatted = str(default("/hostLevelParams/stack_version", ""))
  131. hdp_stack_version = format_hdp_stack_version(stack_version_unformatted)
  132. if hdp_stack_version != "" and compare_versions(hdp_stack_version, '2.2') >= 0:
  133. if command_name.lower() == "status":
  134. request_version = default("/commandParams/request_version", None)
  135. if request_version is not None:
  136. return True
  137. else:
  138. # Populate version only on base commands
  139. return command_name.lower() == "start" or command_name.lower() == "install" or command_name.lower() == "restart"
  140. return False
  141. def execute(self):
  142. """
  143. Sets up logging;
  144. Parses command parameters and executes method relevant to command type
  145. """
  146. logger, chout, cherr = Logger.initialize_logger()
  147. # parse arguments
  148. if len(sys.argv) < 7:
  149. logger.error("Script expects at least 6 arguments")
  150. print USAGE.format(os.path.basename(sys.argv[0])) # print to stdout
  151. sys.exit(1)
  152. command_name = str.lower(sys.argv[1])
  153. self.command_data_file = sys.argv[2]
  154. self.basedir = sys.argv[3]
  155. self.stroutfile = sys.argv[4]
  156. self.load_structured_out()
  157. self.logging_level = sys.argv[5]
  158. Script.tmp_dir = sys.argv[6]
  159. logging_level_str = logging._levelNames[self.logging_level]
  160. chout.setLevel(logging_level_str)
  161. logger.setLevel(logging_level_str)
  162. # on windows we need to reload some of env variables manually because there is no default paths for configs(like
  163. # /etc/something/conf on linux. When this env vars created by one of the Script execution, they can not be updated
  164. # in agent, so other Script executions will not be able to access to new env variables
  165. if OSCheck.is_windows_family():
  166. reload_windows_env()
  167. try:
  168. with open(self.command_data_file) as f:
  169. pass
  170. Script.config = ConfigDictionary(json.load(f))
  171. # load passwords here(used on windows to impersonate different users)
  172. Script.passwords = {}
  173. for k, v in _PASSWORD_MAP.iteritems():
  174. if get_path_from_configuration(k, Script.config) and get_path_from_configuration(v, Script.config):
  175. Script.passwords[get_path_from_configuration(k, Script.config)] = get_path_from_configuration(v, Script.config)
  176. except IOError:
  177. logger.exception("Can not read json file with command parameters: ")
  178. sys.exit(1)
  179. # Run class method depending on a command type
  180. try:
  181. method = self.choose_method_to_execute(command_name)
  182. with Environment(self.basedir, tmp_dir=Script.tmp_dir) as env:
  183. env.config.download_path = Script.tmp_dir
  184. method(env)
  185. if command_name == "install":
  186. self.set_version()
  187. except ClientComponentHasNoStatus or ComponentIsNotRunning:
  188. # Support of component status checks.
  189. # Non-zero exit code is interpreted as an INSTALLED status of a component
  190. sys.exit(1)
  191. except Fail:
  192. logger.exception("Error while executing command '{0}':".format(command_name))
  193. sys.exit(1)
  194. finally:
  195. if self.should_expose_component_version(command_name):
  196. self.save_component_version_to_structured_out()
  197. def choose_method_to_execute(self, command_name):
  198. """
  199. Returns a callable object that should be executed for a given command.
  200. """
  201. self_methods = dir(self)
  202. if not command_name in self_methods:
  203. raise Fail("Script '{0}' has no method '{1}'".format(sys.argv[0], command_name))
  204. method = getattr(self, command_name)
  205. return method
  206. @staticmethod
  207. def get_config():
  208. """
  209. HACK. Uses static field to store configuration. This is a workaround for
  210. "circular dependency" issue when importing params.py file and passing to
  211. it a configuration instance.
  212. """
  213. return Script.config
  214. @staticmethod
  215. def get_password(user):
  216. return Script.passwords[user]
  217. @staticmethod
  218. def get_tmp_dir():
  219. """
  220. HACK. Uses static field to avoid "circular dependency" issue when
  221. importing params.py.
  222. """
  223. return Script.tmp_dir
  224. def install(self, env):
  225. """
  226. Default implementation of install command is to install all packages
  227. from a list, received from the server.
  228. Feel free to override install() method with your implementation. It
  229. usually makes sense to call install_packages() manually in this case
  230. """
  231. self.install_packages(env)
  232. def install_packages(self, env, exclude_packages=[]):
  233. """
  234. List of packages that are required< by service is received from the server
  235. as a command parameter. The method installs all packages
  236. from this list
  237. """
  238. config = self.get_config()
  239. if 'host_sys_prepped' in config['hostLevelParams']:
  240. # do not install anything on sys-prepped host
  241. if config['hostLevelParams']['host_sys_prepped'] == True:
  242. Logger.info("Node has all packages pre-installed. Skipping.")
  243. return
  244. pass
  245. try:
  246. package_list_str = config['hostLevelParams']['package_list']
  247. if isinstance(package_list_str, basestring) and len(package_list_str) > 0:
  248. package_list = json.loads(package_list_str)
  249. for package in package_list:
  250. if not package['name'] in exclude_packages:
  251. name = package['name']
  252. if OSCheck.is_windows_family():
  253. if name[-4:] == ".msi":
  254. #TODO all msis must be located in resource folder of server, change it to repo later
  255. Msi(name, http_source=os.path.join(config['hostLevelParams']['jdk_location']))
  256. else:
  257. Package(name)
  258. except KeyError:
  259. pass # No reason to worry
  260. if OSCheck.is_windows_family():
  261. #TODO hacky install of windows msi, remove it or move to old(2.1) stack definition when component based install will be implemented
  262. install_windows_msi(os.path.join(config['hostLevelParams']['jdk_location'], "hdp.msi"),
  263. config["hostLevelParams"]["agentCacheDir"], "hdp.msi", self.get_password("hadoop"),
  264. str(config['hostLevelParams']['stack_version']))
  265. reload_windows_env()
  266. pass
  267. @staticmethod
  268. def fail_with_error(message):
  269. """
  270. Prints error message and exits with non-zero exit code
  271. """
  272. print("Error: " + message)
  273. sys.stderr.write("Error: " + message)
  274. sys.exit(1)
  275. def start(self, env, rolling_restart=False):
  276. """
  277. To be overridden by subclasses
  278. """
  279. self.fail_with_error('start method isn\'t implemented')
  280. def stop(self, env, rolling_restart=False):
  281. """
  282. To be overridden by subclasses
  283. """
  284. self.fail_with_error('stop method isn\'t implemented')
  285. def pre_rolling_restart(self, env):
  286. """
  287. To be overridden by subclasses
  288. """
  289. pass
  290. def restart(self, env):
  291. """
  292. Default implementation of restart command is to call stop and start methods
  293. Feel free to override restart() method with your implementation.
  294. For client components we call install
  295. """
  296. config = self.get_config()
  297. componentCategory = None
  298. try:
  299. componentCategory = config['roleParams']['component_category']
  300. except KeyError:
  301. pass
  302. restart_type = ""
  303. if config is not None:
  304. command_params = config["commandParams"] if "commandParams" in config else None
  305. if command_params is not None:
  306. restart_type = command_params["restart_type"] if "restart_type" in command_params else ""
  307. if restart_type:
  308. restart_type = restart_type.encode('ascii', 'ignore')
  309. rolling_restart = restart_type.lower().startswith("rolling")
  310. if componentCategory and componentCategory.strip().lower() == 'CLIENT'.lower():
  311. if rolling_restart:
  312. self.pre_rolling_restart(env)
  313. self.install(env)
  314. else:
  315. # To remain backward compatible with older stacks, only pass rolling_restart if True.
  316. if rolling_restart:
  317. self.stop(env, rolling_restart=rolling_restart)
  318. else:
  319. self.stop(env)
  320. if rolling_restart:
  321. self.pre_rolling_restart(env)
  322. # To remain backward compatible with older stacks, only pass rolling_restart if True.
  323. if rolling_restart:
  324. self.start(env, rolling_restart=rolling_restart)
  325. else:
  326. self.start(env)
  327. if rolling_restart:
  328. self.post_rolling_restart(env)
  329. if self.should_expose_component_version("restart"):
  330. self.save_component_version_to_structured_out()
  331. def post_rolling_restart(self, env):
  332. """
  333. To be overridden by subclasses
  334. """
  335. pass
  336. def configure(self, env, rolling_restart=False):
  337. """
  338. To be overridden by subclasses
  339. """
  340. self.fail_with_error('configure method isn\'t implemented')
  341. def security_status(self, env):
  342. """
  343. To be overridden by subclasses to provide the current security state of the component.
  344. Implementations are required to set the "securityState" property of the structured out data set
  345. to one of the following values:
  346. UNSECURED - If the component is not configured for any security protocol such as
  347. Kerberos
  348. SECURED_KERBEROS - If the component is configured for Kerberos
  349. UNKNOWN - If the security state cannot be determined
  350. ERROR - If the component is supposed to be secured, but there are issues with the
  351. configuration. For example, if the component is configured for Kerberos
  352. but the configured principal and keytab file fail to kinit
  353. """
  354. self.put_structured_out({"securityState": "UNKNOWN"})
  355. def generate_configs_get_template_file_content(self, filename, dicts):
  356. config = self.get_config()
  357. content = ''
  358. for dict in dicts.split(','):
  359. if dict.strip() in config['configurations']:
  360. try:
  361. content += config['configurations'][dict.strip()]['content']
  362. except Fail:
  363. # 'content' section not available in the component client configuration
  364. pass
  365. return content
  366. def generate_configs_get_xml_file_content(self, filename, dict):
  367. config = self.get_config()
  368. return {'configurations':config['configurations'][dict],
  369. 'configuration_attributes':config['configuration_attributes'][dict]}
  370. def generate_configs_get_xml_file_dict(self, filename, dict):
  371. config = self.get_config()
  372. return config['configurations'][dict]
  373. def generate_configs(self, env):
  374. """
  375. Generates config files and stores them as an archive in tmp_dir
  376. based on xml_configs_list and env_configs_list from commandParams
  377. """
  378. import params
  379. env.set_params(params)
  380. config = self.get_config()
  381. xml_configs_list = config['commandParams']['xml_configs_list']
  382. env_configs_list = config['commandParams']['env_configs_list']
  383. properties_configs_list = config['commandParams']['properties_configs_list']
  384. Directory(self.get_tmp_dir(), recursive=True)
  385. conf_tmp_dir = tempfile.mkdtemp(dir=self.get_tmp_dir())
  386. output_filename = os.path.join(self.get_tmp_dir(), config['commandParams']['output_file'])
  387. try:
  388. for file_dict in xml_configs_list:
  389. for filename, dict in file_dict.iteritems():
  390. XmlConfig(filename,
  391. conf_dir=conf_tmp_dir,
  392. **self.generate_configs_get_xml_file_content(filename, dict)
  393. )
  394. for file_dict in env_configs_list:
  395. for filename,dicts in file_dict.iteritems():
  396. File(os.path.join(conf_tmp_dir, filename),
  397. content=InlineTemplate(self.generate_configs_get_template_file_content(filename, dicts)))
  398. for file_dict in properties_configs_list:
  399. for filename, dict in file_dict.iteritems():
  400. PropertiesFile(os.path.join(conf_tmp_dir, filename),
  401. properties=self.generate_configs_get_xml_file_dict(filename, dict)
  402. )
  403. archive_dir(output_filename, conf_tmp_dir)
  404. finally:
  405. Directory(conf_tmp_dir, action="delete")
  406. def set_version(self):
  407. from resource_management.libraries.functions.default import default
  408. stack_name = default("/hostLevelParams/stack_name", None)
  409. version = default("/commandParams/version", None)
  410. stack_version_unformatted = str(default("/hostLevelParams/stack_version", ""))
  411. hdp_stack_version = format_hdp_stack_version(stack_version_unformatted)
  412. stack_to_component = self.get_stack_to_component()
  413. if stack_to_component:
  414. component_name = stack_to_component[stack_name] if stack_name in stack_to_component else None
  415. if component_name and stack_name and version and \
  416. compare_versions(format_hdp_stack_version(hdp_stack_version), '2.2.0.0') >= 0:
  417. Execute(('/usr/bin/hdp-select', 'set', component_name, version),
  418. sudo = True)