script.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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 re
  20. import os
  21. import sys
  22. import logging
  23. import platform
  24. import tarfile
  25. from ambari_commons import OSCheck, OSConst
  26. from ambari_commons.os_family_impl import OsFamilyFuncImpl, OsFamilyImpl
  27. from resource_management.libraries.resources import XmlConfig
  28. from resource_management.libraries.resources import PropertiesFile
  29. from resource_management.core.resources import File, Directory
  30. from resource_management.core.source import InlineTemplate
  31. from resource_management.core.environment import Environment
  32. from resource_management.core.logger import Logger
  33. from resource_management.core.exceptions import Fail, ClientComponentHasNoStatus, ComponentIsNotRunning
  34. from resource_management.core.resources.packaging import Package
  35. from resource_management.libraries.functions.version_select_util import get_component_version
  36. from resource_management.libraries.functions.version import compare_versions
  37. from resource_management.libraries.functions.version import format_hdp_stack_version
  38. from resource_management.libraries.script.config_dictionary import ConfigDictionary, UnknownConfiguration
  39. from resource_management.core.resources.system import Execute
  40. from contextlib import closing
  41. import ambari_simplejson as json # simplejson is much faster comparing to Python 2.6 json module and has the same functions set.
  42. if OSCheck.is_windows_family():
  43. from resource_management.libraries.functions.install_hdp_msi import install_windows_msi
  44. from resource_management.libraries.functions.reload_windows_env import reload_windows_env
  45. from resource_management.libraries.functions.zip_archive import archive_dir
  46. from resource_management.libraries.resources import Msi
  47. else:
  48. from resource_management.libraries.functions.tar_archive import archive_dir
  49. USAGE = """Usage: {0} <COMMAND> <JSON_CONFIG> <BASEDIR> <STROUTPUT> <LOGGING_LEVEL> <TMP_DIR>
  50. <COMMAND> command type (INSTALL/CONFIGURE/START/STOP/SERVICE_CHECK...)
  51. <JSON_CONFIG> path to command json file. Ex: /var/lib/ambari-agent/data/command-2.json
  52. <BASEDIR> path to service metadata dir. Ex: /var/lib/ambari-agent/cache/common-services/HDFS/2.1.0.2.0/package
  53. <STROUTPUT> path to file with structured command output (file will be created). Ex:/tmp/my.txt
  54. <LOGGING_LEVEL> log level for stdout. Ex:DEBUG,INFO
  55. <TMP_DIR> temporary directory for executable scripts. Ex: /var/lib/ambari-agent/tmp
  56. """
  57. _PASSWORD_MAP = {"/configurations/cluster-env/hadoop.user.name":"/configurations/cluster-env/hadoop.user.password"}
  58. def get_path_from_configuration(name, configuration):
  59. subdicts = filter(None, name.split('/'))
  60. for x in subdicts:
  61. if x in configuration:
  62. configuration = configuration[x]
  63. else:
  64. return None
  65. return configuration
  66. class Script(object):
  67. """
  68. Executes a command for custom service. stdout and stderr are written to
  69. tmpoutfile and to tmperrfile respectively.
  70. Script instances share configuration as a class parameter and therefore
  71. different Script instances can not be used from different threads at
  72. the same time within a single python process
  73. Accepted command line arguments mapping:
  74. 1 command type (START/STOP/...)
  75. 2 path to command json file
  76. 3 path to service metadata dir (Directory "package" inside service directory)
  77. 4 path to file with structured command output (file will be created)
  78. """
  79. structuredOut = {}
  80. command_data_file = ""
  81. basedir = ""
  82. stroutfile = ""
  83. logging_level = ""
  84. logger = None
  85. # Class variable
  86. tmp_dir = ""
  87. def get_stack_to_component(self):
  88. """
  89. To be overridden by subclasses.
  90. Returns a dictionary where the key is a stack name, and the value is the component name used in selecting the version.
  91. """
  92. return {}
  93. def load_structured_out(self):
  94. Script.structuredOut = {}
  95. if os.path.exists(self.stroutfile):
  96. if os.path.getsize(self.stroutfile) > 0:
  97. with open(self.stroutfile, 'r') as fp:
  98. try:
  99. Script.structuredOut = json.load(fp)
  100. except Exception:
  101. errMsg = 'Unable to read structured output from ' + self.stroutfile
  102. self.logger.warn(errMsg)
  103. pass
  104. # version is only set in a specific way and should not be carried
  105. if "version" in Script.structuredOut:
  106. del Script.structuredOut["version"]
  107. # reset security issues and errors found on previous runs
  108. if "securityIssuesFound" in Script.structuredOut:
  109. del Script.structuredOut["securityIssuesFound"]
  110. if "securityStateErrorInfo" in Script.structuredOut:
  111. del Script.structuredOut["securityStateErrorInfo"]
  112. def put_structured_out(self, sout):
  113. curr_content = Script.structuredOut.copy()
  114. Script.structuredOut.update(sout)
  115. try:
  116. with open(self.stroutfile, 'w') as fp:
  117. json.dump(Script.structuredOut, fp)
  118. except IOError, err:
  119. Script.structuredOut.update({"errMsg" : "Unable to write to " + self.stroutfile})
  120. def save_component_version_to_structured_out(self):
  121. """
  122. :param stack_name: One of HDP, HDPWIN, PHD, BIGTOP.
  123. :return: Append the version number to the structured out.
  124. """
  125. from resource_management.libraries.functions.default import default
  126. stack_name = default("/hostLevelParams/stack_name", None)
  127. stack_to_component = self.get_stack_to_component()
  128. if stack_to_component and stack_name:
  129. component_name = stack_to_component[stack_name] if stack_name in stack_to_component else None
  130. component_version = get_component_version(stack_name, component_name)
  131. if component_version:
  132. self.put_structured_out({"version": component_version})
  133. def should_expose_component_version(self, command_name):
  134. """
  135. Analyzes config and given command to determine if stack version should be written
  136. to structured out. Currently only HDP stack versions >= 2.2 are supported.
  137. :param command_name: command name
  138. :return: True or False
  139. """
  140. from resource_management.libraries.functions.default import default
  141. stack_version_unformatted = str(default("/hostLevelParams/stack_version", ""))
  142. hdp_stack_version = format_hdp_stack_version(stack_version_unformatted)
  143. if hdp_stack_version != "" and compare_versions(hdp_stack_version, '2.2') >= 0:
  144. if command_name.lower() == "status":
  145. request_version = default("/commandParams/request_version", None)
  146. if request_version is not None:
  147. return True
  148. else:
  149. # Populate version only on base commands
  150. return command_name.lower() == "start" or command_name.lower() == "install" or command_name.lower() == "restart"
  151. return False
  152. def execute(self):
  153. """
  154. Sets up logging;
  155. Parses command parameters and executes method relevant to command type
  156. """
  157. logger, chout, cherr = Logger.initialize_logger(__name__)
  158. # parse arguments
  159. if len(sys.argv) < 7:
  160. logger.error("Script expects at least 6 arguments")
  161. print USAGE.format(os.path.basename(sys.argv[0])) # print to stdout
  162. sys.exit(1)
  163. self.logger = logger
  164. self.command_name = str.lower(sys.argv[1])
  165. self.command_data_file = sys.argv[2]
  166. self.basedir = sys.argv[3]
  167. self.stroutfile = sys.argv[4]
  168. self.load_structured_out()
  169. self.logging_level = sys.argv[5]
  170. Script.tmp_dir = sys.argv[6]
  171. logging_level_str = logging._levelNames[self.logging_level]
  172. chout.setLevel(logging_level_str)
  173. logger.setLevel(logging_level_str)
  174. # on windows we need to reload some of env variables manually because there is no default paths for configs(like
  175. # /etc/something/conf on linux. When this env vars created by one of the Script execution, they can not be updated
  176. # in agent, so other Script executions will not be able to access to new env variables
  177. if OSCheck.is_windows_family():
  178. reload_windows_env()
  179. try:
  180. with open(self.command_data_file) as f:
  181. pass
  182. Script.config = ConfigDictionary(json.load(f))
  183. # load passwords here(used on windows to impersonate different users)
  184. Script.passwords = {}
  185. for k, v in _PASSWORD_MAP.iteritems():
  186. if get_path_from_configuration(k, Script.config) and get_path_from_configuration(v, Script.config):
  187. Script.passwords[get_path_from_configuration(k, Script.config)] = get_path_from_configuration(v, Script.config)
  188. except IOError:
  189. logger.exception("Can not read json file with command parameters: ")
  190. sys.exit(1)
  191. # Run class method depending on a command type
  192. try:
  193. method = self.choose_method_to_execute(self.command_name)
  194. with Environment(self.basedir, tmp_dir=Script.tmp_dir) as env:
  195. env.config.download_path = Script.tmp_dir
  196. method(env)
  197. finally:
  198. if self.should_expose_component_version(self.command_name):
  199. self.save_component_version_to_structured_out()
  200. def choose_method_to_execute(self, command_name):
  201. """
  202. Returns a callable object that should be executed for a given command.
  203. """
  204. self_methods = dir(self)
  205. if not command_name in self_methods:
  206. raise Fail("Script '{0}' has no method '{1}'".format(sys.argv[0], command_name))
  207. method = getattr(self, command_name)
  208. return method
  209. @staticmethod
  210. def get_config():
  211. """
  212. HACK. Uses static field to store configuration. This is a workaround for
  213. "circular dependency" issue when importing params.py file and passing to
  214. it a configuration instance.
  215. """
  216. return Script.config
  217. @staticmethod
  218. def get_password(user):
  219. return Script.passwords[user]
  220. @staticmethod
  221. def get_tmp_dir():
  222. """
  223. HACK. Uses static field to avoid "circular dependency" issue when
  224. importing params.py.
  225. """
  226. return Script.tmp_dir
  227. @staticmethod
  228. def get_component_from_role(role_directory_map, default_role):
  229. """
  230. Gets the /usr/hdp/current/<component> component given an Ambari role,
  231. such as DATANODE or HBASE_MASTER.
  232. :return: the component name, such as hbase-master
  233. """
  234. from resource_management.libraries.functions.default import default
  235. command_role = default("/role", default_role)
  236. if command_role in role_directory_map:
  237. return role_directory_map[command_role]
  238. else:
  239. return role_directory_map[default_role]
  240. @staticmethod
  241. def get_stack_name():
  242. """
  243. Gets the name of the stack from hostLevelParams/stack_name.
  244. :return: a stack name or None
  245. """
  246. from resource_management.libraries.functions.default import default
  247. return default("/hostLevelParams/stack_name", None)
  248. @staticmethod
  249. def get_hdp_stack_version():
  250. """
  251. Gets the normalized version of the HDP stack in the form #.#.#.# if it is
  252. present on the configurations sent.
  253. :return: a normalized HDP stack version or None
  254. """
  255. stack_name = Script.get_stack_name()
  256. if stack_name is None or stack_name.upper() not in ["HDP", "HDPWIN"]:
  257. return None
  258. config = Script.get_config()
  259. if 'hostLevelParams' not in config or 'stack_version' not in config['hostLevelParams']:
  260. return None
  261. stack_version_unformatted = str(config['hostLevelParams']['stack_version'])
  262. if stack_version_unformatted is None or stack_version_unformatted == '':
  263. return None
  264. return format_hdp_stack_version(stack_version_unformatted)
  265. @staticmethod
  266. def is_hdp_stack_greater_or_equal(compare_to_version):
  267. """
  268. Gets whether the hostLevelParams/stack_version, after being normalized,
  269. is greater than or equal to the specified stack version
  270. :param compare_to_version: the version to compare to
  271. :return: True if the command's stack is greater than the specified version
  272. """
  273. return Script.is_hdp_stack_greater_or_equal_to(Script.get_hdp_stack_version(), compare_to_version)
  274. @staticmethod
  275. def is_hdp_stack_greater_or_equal_to(formatted_hdp_stack_version, compare_to_version):
  276. """
  277. Gets whether the provided formatted_hdp_stack_version (normalized)
  278. is greater than or equal to the specified stack version
  279. :param formatted_hdp_stack_version: the version of stack to compare
  280. :param compare_to_version: the version of stack to compare to
  281. :return: True if the command's stack is greater than the specified version
  282. """
  283. if formatted_hdp_stack_version is None or formatted_hdp_stack_version == "":
  284. return False
  285. return compare_versions(formatted_hdp_stack_version, compare_to_version) >= 0
  286. @staticmethod
  287. def is_hdp_stack_less_than(compare_to_version):
  288. """
  289. Gets whether the hostLevelParams/stack_version, after being normalized,
  290. is less than the specified stack version
  291. :param compare_to_version: the version to compare to
  292. :return: True if the command's stack is less than the specified version
  293. """
  294. hdp_stack_version = Script.get_hdp_stack_version()
  295. if hdp_stack_version is None:
  296. return False
  297. return compare_versions(hdp_stack_version, compare_to_version) < 0
  298. def install(self, env):
  299. """
  300. Default implementation of install command is to install all packages
  301. from a list, received from the server.
  302. Feel free to override install() method with your implementation. It
  303. usually makes sense to call install_packages() manually in this case
  304. """
  305. self.install_packages(env)
  306. def install_packages(self, env, exclude_packages=[]):
  307. """
  308. List of packages that are required< by service is received from the server
  309. as a command parameter. The method installs all packages
  310. from this list
  311. exclude_packages - list of regexes (possibly raw strings as well), the
  312. packages which match the regex won't be installed.
  313. NOTE: regexes don't have Python syntax, but simple package regexes which support only * and .* and ?
  314. """
  315. config = self.get_config()
  316. if 'host_sys_prepped' in config['hostLevelParams']:
  317. # do not install anything on sys-prepped host
  318. if config['hostLevelParams']['host_sys_prepped'] == True:
  319. Logger.info("Node has all packages pre-installed. Skipping.")
  320. return
  321. pass
  322. try:
  323. package_list_str = config['hostLevelParams']['package_list']
  324. if isinstance(package_list_str, basestring) and len(package_list_str) > 0:
  325. package_list = json.loads(package_list_str)
  326. for package in package_list:
  327. if not Script.matches_any_regexp(package['name'], exclude_packages):
  328. name = package['name']
  329. # HACK: On Windows, only install ambari-metrics packages using Choco Package Installer
  330. # TODO: Update this once choco packages for hadoop are created. This is because, service metainfo.xml support
  331. # <osFamily>any<osFamily> which would cause installation failure on Windows.
  332. if OSCheck.is_windows_family():
  333. if "ambari-metrics" in name:
  334. Package(name)
  335. else:
  336. Package(name)
  337. except KeyError:
  338. pass # No reason to worry
  339. if OSCheck.is_windows_family():
  340. #TODO hacky install of windows msi, remove it or move to old(2.1) stack definition when component based install will be implemented
  341. hadoop_user = config["configurations"]["cluster-env"]["hadoop.user.name"]
  342. install_windows_msi(config['hostLevelParams']['jdk_location'],
  343. config["hostLevelParams"]["agentCacheDir"], ["hdp-2.3.0.0.winpkg.msi", "hdp-2.3.0.0.cab", "hdp-2.3.0.0-01.cab"],
  344. hadoop_user, self.get_password(hadoop_user),
  345. str(config['hostLevelParams']['stack_version']))
  346. reload_windows_env()
  347. @staticmethod
  348. def matches_any_regexp(string, regexp_list):
  349. for regex in regexp_list:
  350. # we cannot use here Python regex, since * will create some troubles matching plaintext names.
  351. package_regex = '^' + re.escape(regex).replace('\\.\\*','.*').replace("\\?", ".").replace("\\*", ".*") + '$'
  352. if re.match(package_regex, string):
  353. return True
  354. return False
  355. @staticmethod
  356. def fail_with_error(message):
  357. """
  358. Prints error message and exits with non-zero exit code
  359. """
  360. print("Error: " + message)
  361. sys.stderr.write("Error: " + message)
  362. sys.exit(1)
  363. def start(self, env, rolling_restart=False):
  364. """
  365. To be overridden by subclasses
  366. """
  367. self.fail_with_error("start method isn't implemented")
  368. def stop(self, env, rolling_restart=False):
  369. """
  370. To be overridden by subclasses
  371. """
  372. self.fail_with_error("stop method isn't implemented")
  373. def pre_rolling_restart(self, env):
  374. """
  375. To be overridden by subclasses
  376. """
  377. pass
  378. def restart(self, env):
  379. """
  380. Default implementation of restart command is to call stop and start methods
  381. Feel free to override restart() method with your implementation.
  382. For client components we call install
  383. """
  384. config = self.get_config()
  385. componentCategory = None
  386. try:
  387. componentCategory = config['roleParams']['component_category']
  388. except KeyError:
  389. pass
  390. restart_type = ""
  391. if config is not None:
  392. command_params = config["commandParams"] if "commandParams" in config else None
  393. if command_params is not None:
  394. restart_type = command_params["restart_type"] if "restart_type" in command_params else ""
  395. if restart_type:
  396. restart_type = restart_type.encode('ascii', 'ignore')
  397. rolling_restart = restart_type.lower().startswith("rolling")
  398. if componentCategory and componentCategory.strip().lower() == 'CLIENT'.lower():
  399. if rolling_restart:
  400. self.pre_rolling_restart(env)
  401. self.install(env)
  402. else:
  403. # To remain backward compatible with older stacks, only pass rolling_restart if True.
  404. if rolling_restart:
  405. self.stop(env, rolling_restart=rolling_restart)
  406. else:
  407. self.stop(env)
  408. if rolling_restart:
  409. self.pre_rolling_restart(env)
  410. # To remain backward compatible with older stacks, only pass rolling_restart if True.
  411. if rolling_restart:
  412. self.start(env, rolling_restart=rolling_restart)
  413. else:
  414. self.start(env)
  415. if rolling_restart:
  416. self.post_rolling_restart(env)
  417. if self.should_expose_component_version("restart"):
  418. self.save_component_version_to_structured_out()
  419. def post_rolling_restart(self, env):
  420. """
  421. To be overridden by subclasses
  422. """
  423. pass
  424. def configure(self, env, rolling_restart=False):
  425. """
  426. To be overridden by subclasses
  427. """
  428. self.fail_with_error('configure method isn\'t implemented')
  429. def security_status(self, env):
  430. """
  431. To be overridden by subclasses to provide the current security state of the component.
  432. Implementations are required to set the "securityState" property of the structured out data set
  433. to one of the following values:
  434. UNSECURED - If the component is not configured for any security protocol such as
  435. Kerberos
  436. SECURED_KERBEROS - If the component is configured for Kerberos
  437. UNKNOWN - If the security state cannot be determined
  438. ERROR - If the component is supposed to be secured, but there are issues with the
  439. configuration. For example, if the component is configured for Kerberos
  440. but the configured principal and keytab file fail to kinit
  441. """
  442. self.put_structured_out({"securityState": "UNKNOWN"})
  443. def generate_configs_get_template_file_content(self, filename, dicts):
  444. config = self.get_config()
  445. content = ''
  446. for dict in dicts.split(','):
  447. if dict.strip() in config['configurations']:
  448. try:
  449. content += config['configurations'][dict.strip()]['content']
  450. except Fail:
  451. # 'content' section not available in the component client configuration
  452. pass
  453. return content
  454. def generate_configs_get_xml_file_content(self, filename, dict):
  455. config = self.get_config()
  456. return {'configurations':config['configurations'][dict],
  457. 'configuration_attributes':config['configuration_attributes'][dict]}
  458. def generate_configs_get_xml_file_dict(self, filename, dict):
  459. config = self.get_config()
  460. return config['configurations'][dict]
  461. def generate_configs(self, env):
  462. """
  463. Generates config files and stores them as an archive in tmp_dir
  464. based on xml_configs_list and env_configs_list from commandParams
  465. """
  466. import params
  467. env.set_params(params)
  468. config = self.get_config()
  469. xml_configs_list = config['commandParams']['xml_configs_list']
  470. env_configs_list = config['commandParams']['env_configs_list']
  471. properties_configs_list = config['commandParams']['properties_configs_list']
  472. Directory(self.get_tmp_dir(), recursive=True)
  473. conf_tmp_dir = tempfile.mkdtemp(dir=self.get_tmp_dir())
  474. output_filename = os.path.join(self.get_tmp_dir(), config['commandParams']['output_file'])
  475. try:
  476. for file_dict in xml_configs_list:
  477. for filename, dict in file_dict.iteritems():
  478. XmlConfig(filename,
  479. conf_dir=conf_tmp_dir,
  480. mode=0644,
  481. **self.generate_configs_get_xml_file_content(filename, dict)
  482. )
  483. for file_dict in env_configs_list:
  484. for filename,dicts in file_dict.iteritems():
  485. File(os.path.join(conf_tmp_dir, filename),
  486. mode=0644,
  487. content=InlineTemplate(self.generate_configs_get_template_file_content(filename, dicts)))
  488. for file_dict in properties_configs_list:
  489. for filename, dict in file_dict.iteritems():
  490. PropertiesFile(os.path.join(conf_tmp_dir, filename),
  491. mode=0644,
  492. properties=self.generate_configs_get_xml_file_dict(filename, dict)
  493. )
  494. with closing(tarfile.open(output_filename, "w:gz")) as tar:
  495. try:
  496. tar.add(conf_tmp_dir, arcname=os.path.basename("."))
  497. finally:
  498. tar.close()
  499. finally:
  500. Directory(conf_tmp_dir, action="delete")