blob: 00b80b4b5acf4bf4b708b13bed0f01926ee632f8 [file] [log] [blame]
#!/usr/bin/env python
'''
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.
'''
__all__ = ["Script"]
import os
import sys
import json
import logging
import shutil
from resource_management.core.environment import Environment
from resource_management.core.exceptions import Fail, ClientComponentHasNoStatus, ComponentIsNotRunning
from resource_management.core.resources.packaging import Package
from resource_management.core.resources import Tarball
from resource_management.core.resources import Directory
from resource_management.libraries.script.config_dictionary import ConfigDictionary
from resource_management.libraries.script.repo_installer import RepoInstaller
from resource_management.core.logger import Logger
USAGE = """Usage: {0} <COMMAND> <JSON_CONFIG> <BASEDIR> <STROUTPUT> <LOGGING_LEVEL>
<COMMAND> command type (INSTALL/CONFIGURE/START/STOP/SERVICE_CHECK...)
<JSON_CONFIG> path to command json file. Ex: /var/lib/ambari-agent/data/command-2.json
<BASEDIR> path to service metadata dir. Ex: /var/lib/ambari-agent/cache/stacks/HDP/2.0.6/services/HDFS
<STROUTPUT> path to file with structured command output (file will be created). Ex:/tmp/my.txt
<LOGGING_LEVEL> log level for stdout. Ex:DEBUG,INFO
"""
class Script(object):
"""
Executes a command for custom service. stdout and stderr are written to
tmpoutfile and to tmperrfile respectively.
Script instances share configuration as a class parameter and therefore
different Script instances can not be used from different threads at
the same time within a single python process
Accepted command line arguments mapping:
1 command type (START/STOP/...)
2 path to command json file
3 path to service metadata dir (Directory "package" inside service directory)
4 path to file with structured command output (file will be created)
"""
structuredOut = {}
def put_structured_out(self, sout):
Script.structuredOut.update(sout)
try:
with open(self.stroutfile, 'w') as fp:
json.dump(Script.structuredOut, fp)
except IOError:
Script.structuredOut.update({"errMsg" : "Unable to write to " + self.stroutfile})
def execute(self):
"""
Sets up logging;
Parses command parameters and executes method relevant to command type
"""
# set up logging (two separate loggers for stderr and stdout with different loglevels)
logger = logging.getLogger('resource_management')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(message)s')
chout = logging.StreamHandler(sys.stdout)
chout.setLevel(logging.INFO)
chout.setFormatter(formatter)
cherr = logging.StreamHandler(sys.stderr)
cherr.setLevel(logging.ERROR)
cherr.setFormatter(formatter)
logger.addHandler(cherr)
logger.addHandler(chout)
# parse arguments
if len(sys.argv) < 6:
logger.error("Script expects at least 5 arguments")
print USAGE.format(os.path.basename(sys.argv[0])) # print to stdout
sys.exit(1)
command_name = str.lower(sys.argv[1])
command_data_file = sys.argv[2]
basedir = sys.argv[3]
self.stroutfile = sys.argv[4]
logging_level = sys.argv[5]
logging_level_str = logging._levelNames[logging_level]
chout.setLevel(logging_level_str)
logger.setLevel(logging_level_str)
try:
with open(command_data_file, "r") as f:
pass
Script.config = ConfigDictionary(json.load(f))
except IOError:
logger.exception("Can not read json file with command parameters: ")
sys.exit(1)
# Run class method depending on a command type
try:
method = self.choose_method_to_execute(command_name)
with Environment(basedir) as env:
method(env)
except ClientComponentHasNoStatus or ComponentIsNotRunning:
# Support of component status checks.
# Non-zero exit code is interpreted as an INSTALLED status of a component
sys.exit(1)
except Fail:
logger.exception("Error while executing command '{0}':".format(command_name))
sys.exit(1)
def choose_method_to_execute(self, command_name):
"""
Returns a callable object that should be executed for a given command.
"""
self_methods = dir(self)
if not command_name in self_methods:
raise Fail("Script '{0}' has no method '{1}'".format(sys.argv[0], command_name))
method = getattr(self, command_name)
return method
@staticmethod
def get_config():
"""
HACK. Uses static field to store configuration. This is a workaround for
"circular dependency" issue when importing params.py file and passing to
it a configuration instance.
"""
return Script.config
def install(self, env):
"""
Default implementation of install command is to install all packages
from a list, received from the server.
Feel free to override install() method with your implementation. It
usually makes sense to call install_packages() manually in this case
"""
self.install_packages(env)
def install_packages(self, env):
"""
List of packages that are required by service is received from the server
as a command parameter. The method installs all packages
from this list
"""
config = self.get_config()
repo_installed = False
try:
package_list_str = config['hostLevelParams']['package_list']
if isinstance(package_list_str,basestring) and len(package_list_str) > 0:
package_list = json.loads(package_list_str)
for package in package_list:
name = package['name']
type = package['type']
if type.lower() == "tarball":
if name.startswith(os.path.sep):
tarball = name
else:
basedir = env.config.basedir
tarball = os.path.join(basedir, name)
install_location = config['configurations']['global']['app_install_dir']
Directory(install_location, action = "delete")
Directory(install_location)
Tarball(tarball, location=install_location)
elif type.lower() == "folder":
if name.startswith(os.path.sep):
src = name
else:
basedir = env.config.basedir
src = os.path.join(basedir, name)
dest = config['configurations']['global']['app_install_dir']
Directory(dest, action = "delete")
Logger.info("Copying from " + src + " to " + dest)
shutil.copytree(src, dest)
else:
if not repo_installed:
RepoInstaller.install_repos(config)
repo_installed = True
Package(name)
except KeyError, e:
Logger.info("Error installing packages. " + repr(e))
pass # No reason to worry
#RepoInstaller.remove_repos(config)
def fail_with_error(self, message):
"""
Prints error message and exits with non-zero exit code
"""
print("Error: " + message)
sys.stderr.write("Error: " + message)
sys.exit(1)
def start(self, env):
"""
To be overridden by subclasses
"""
self.fail_with_error('start method isn\'t implemented')
def stop(self, env):
"""
To be overridden by subclasses
"""
self.fail_with_error('stop method isn\'t implemented')
def restart(self, env):
"""
Default implementation of restart command is to call stop and start methods
Feel free to override restart() method with your implementation.
For client components we call install
"""
config = self.get_config()
componentCategory = None
try :
componentCategory = config['roleParams']['component_category']
except KeyError:
pass
if componentCategory and componentCategory.strip().lower() == 'CLIENT'.lower():
self.install(env)
else:
self.stop(env)
self.start(env)
def configure(self, env):
"""
To be overridden by subclasses
"""
self.fail_with_error('configure method isn\'t implemented')