b144c7ee11
We add the codes about YARN RM HA implementation. If YARN_STANDBYRM role is enabled in cluster, we will do some validation, and call CM API enable_rm_ha to enable RM HA in the cluster. Partial-implements: blueprint cdh-ha-support Change-Id: I5562a310ef5b6fffa3439b28db2ceb212cc6286f
523 lines
20 KiB
Python
523 lines
20 KiB
Python
# Copyright (c) 2014 Intel Corporation.
|
|
#
|
|
# Licensed 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.
|
|
#
|
|
# The contents of this file are mainly copied from cm_api sources,
|
|
# released by Cloudera. Codes not used by Sahara CDH plugin are removed.
|
|
# You can find the original codes at
|
|
#
|
|
# https://github.com/cloudera/cm_api/tree/master/python/src/cm_api
|
|
#
|
|
# To satisfy the pep8 and python3 tests, we did some changes to the codes.
|
|
# We also change some importings to use Sahara inherited classes.
|
|
|
|
from oslo_serialization import jsonutils as json
|
|
import six
|
|
|
|
from sahara.plugins.cdh.client import role_config_groups
|
|
from sahara.plugins.cdh.client import roles
|
|
from sahara.plugins.cdh.client import types
|
|
|
|
SERVICES_PATH = "/clusters/%s/services"
|
|
SERVICE_PATH = "/clusters/%s/services/%s"
|
|
ROLETYPES_CFG_KEY = 'roleTypeConfigs'
|
|
|
|
|
|
def create_service(resource_root, name, service_type,
|
|
cluster_name="default"):
|
|
"""Create a service
|
|
|
|
:param resource_root: The root Resource object.
|
|
:param name: Service name
|
|
:param service_type: Service type
|
|
:param cluster_name: Cluster name
|
|
:return: An ApiService object
|
|
"""
|
|
apiservice = ApiService(resource_root, name, service_type)
|
|
return types.call(resource_root.post, SERVICES_PATH % (cluster_name,),
|
|
ApiService, True, data=[apiservice])[0]
|
|
|
|
|
|
def get_service(resource_root, name, cluster_name="default"):
|
|
"""Lookup a service by name
|
|
|
|
:param resource_root: The root Resource object.
|
|
:param name: Service name
|
|
:param cluster_name: Cluster name
|
|
:return: An ApiService object
|
|
"""
|
|
return _get_service(resource_root, "%s/%s"
|
|
% (SERVICES_PATH % (cluster_name,), name))
|
|
|
|
|
|
def _get_service(resource_root, path):
|
|
return types.call(resource_root.get, path, ApiService)
|
|
|
|
|
|
def get_all_services(resource_root, cluster_name="default", view=None):
|
|
"""Get all services
|
|
|
|
:param resource_root: The root Resource object.
|
|
:param cluster_name: Cluster name
|
|
:return: A list of ApiService objects.
|
|
"""
|
|
return types.call(resource_root.get, SERVICES_PATH % (cluster_name,),
|
|
ApiService, True,
|
|
params=(dict(view=view) if view else None))
|
|
|
|
|
|
def delete_service(resource_root, name, cluster_name="default"):
|
|
"""Delete a service by name
|
|
|
|
:param resource_root: The root Resource object.
|
|
:param name: Service name
|
|
:param cluster_name: Cluster name
|
|
:return: The deleted ApiService object
|
|
"""
|
|
return types.call(resource_root.delete,
|
|
"%s/%s" % (SERVICES_PATH % (cluster_name,), name),
|
|
ApiService)
|
|
|
|
|
|
class ApiService(types.BaseApiResource):
|
|
_ATTRIBUTES = {
|
|
'name': None,
|
|
'type': None,
|
|
'displayName': None,
|
|
'serviceState': types.ROAttr(),
|
|
'healthSummary': types.ROAttr(),
|
|
'healthChecks': types.ROAttr(),
|
|
'clusterRef': types.ROAttr(types.ApiClusterRef),
|
|
'configStale': types.ROAttr(),
|
|
'configStalenessStatus': types.ROAttr(),
|
|
'clientConfigStalenessStatus': types.ROAttr(),
|
|
'serviceUrl': types.ROAttr(),
|
|
'maintenanceMode': types.ROAttr(),
|
|
'maintenanceOwners': types.ROAttr(),
|
|
}
|
|
|
|
def __init__(self, resource_root, name=None, type=None):
|
|
types.BaseApiObject.init(self, resource_root, locals())
|
|
|
|
def __str__(self):
|
|
return ("<ApiService>: %s (cluster: %s)"
|
|
% (self.name, self._get_cluster_name()))
|
|
|
|
def _get_cluster_name(self):
|
|
if hasattr(self, 'clusterRef') and self.clusterRef:
|
|
return self.clusterRef.clusterName
|
|
return None
|
|
|
|
def _path(self):
|
|
"""Return the API path for this service
|
|
|
|
This method assumes that lack of a cluster reference means that the
|
|
object refers to the Cloudera Management Services instance.
|
|
"""
|
|
if self._get_cluster_name():
|
|
return SERVICE_PATH % (self._get_cluster_name(), self.name)
|
|
else:
|
|
return '/cm/service'
|
|
|
|
def _role_cmd(self, cmd, roles, api_version=1):
|
|
return self._post("roleCommands/" + cmd, types.ApiBulkCommandList,
|
|
data=roles, api_version=api_version)
|
|
|
|
def _parse_svc_config(self, json_dic, view=None):
|
|
"""Parse a json-decoded ApiServiceConfig dictionary into a 2-tuple
|
|
|
|
:param json_dic: The json dictionary with the config data.
|
|
:param view: View to materialize.
|
|
:return: 2-tuple (service config dictionary, role type configurations)
|
|
"""
|
|
svc_config = types.json_to_config(json_dic, view == 'full')
|
|
rt_configs = {}
|
|
if ROLETYPES_CFG_KEY in json_dic:
|
|
for rt_config in json_dic[ROLETYPES_CFG_KEY]:
|
|
rt_configs[rt_config['roleType']] = types.json_to_config(
|
|
rt_config, view == 'full')
|
|
|
|
return (svc_config, rt_configs)
|
|
|
|
def create_yarn_job_history_dir(self):
|
|
"""Create the Yarn job history directory
|
|
|
|
:return: Reference to submitted command.
|
|
:since: API v6
|
|
"""
|
|
return self._cmd('yarnCreateJobHistoryDirCommand', api_version=6)
|
|
|
|
def get_config(self, view=None):
|
|
"""Retrieve the service's configuration
|
|
|
|
Retrieves both the service configuration and role type configuration
|
|
for each of the service's supported role types. The role type
|
|
configurations are returned as a dictionary, whose keys are the
|
|
role type name, and values are the respective configuration
|
|
dictionaries.
|
|
|
|
The 'summary' view contains strings as the dictionary values. The full
|
|
view contains types.ApiConfig instances as the values.
|
|
|
|
:param view: View to materialize ('full' or 'summary')
|
|
:return: 2-tuple (service config dictionary, role type configurations)
|
|
"""
|
|
path = self._path() + '/config'
|
|
resp = self._get_resource_root().get(
|
|
path, params=(dict(view=view) if view else None))
|
|
return self._parse_svc_config(resp, view)
|
|
|
|
def update_config(self, svc_config, **rt_configs):
|
|
"""Update the service's configuration
|
|
|
|
:param svc_config: Dictionary with service configuration to update.
|
|
:param rt_configs: Dict of role type configurations to update.
|
|
:return: 2-tuple (service config dictionary, role type configurations)
|
|
"""
|
|
path = self._path() + '/config'
|
|
|
|
if svc_config:
|
|
data = types.config_to_api_list(svc_config)
|
|
else:
|
|
data = {}
|
|
if rt_configs:
|
|
rt_list = []
|
|
for rt, cfg in six.iteritems(rt_configs):
|
|
rt_data = types.config_to_api_list(cfg)
|
|
rt_data['roleType'] = rt
|
|
rt_list.append(rt_data)
|
|
data[ROLETYPES_CFG_KEY] = rt_list
|
|
|
|
resp = self._get_resource_root().put(path, data=json.dumps(data))
|
|
return self._parse_svc_config(resp)
|
|
|
|
def create_role(self, role_name, role_type, host_id):
|
|
"""Create a role
|
|
|
|
:param role_name: Role name
|
|
:param role_type: Role type
|
|
:param host_id: ID of the host to assign the role to
|
|
:return: An ApiRole object
|
|
"""
|
|
return roles.create_role(self._get_resource_root(), self.name,
|
|
role_type, role_name, host_id,
|
|
self._get_cluster_name())
|
|
|
|
def delete_role(self, name):
|
|
"""Delete a role by name
|
|
|
|
:param name: Role name
|
|
:return: The deleted ApiRole object
|
|
"""
|
|
return roles.delete_role(self._get_resource_root(), self.name, name,
|
|
self._get_cluster_name())
|
|
|
|
def get_roles_by_type(self, role_type, view=None):
|
|
"""Get all roles of a certain type in a service
|
|
|
|
:param role_type: Role type
|
|
:param view: View to materialize ('full' or 'summary')
|
|
:return: A list of ApiRole objects.
|
|
"""
|
|
return roles.get_roles_by_type(self._get_resource_root(), self.name,
|
|
role_type, self._get_cluster_name(),
|
|
view)
|
|
|
|
def get_all_role_config_groups(self):
|
|
"""Get a list of role configuration groups in the service
|
|
|
|
:return: A list of ApiRoleConfigGroup objects.
|
|
:since: API v3
|
|
"""
|
|
return role_config_groups.get_all_role_config_groups(
|
|
self._get_resource_root(), self.name, self._get_cluster_name())
|
|
|
|
def start(self):
|
|
"""Start a service
|
|
|
|
:return: Reference to the submitted command.
|
|
"""
|
|
return self._cmd('start')
|
|
|
|
def stop(self):
|
|
"""Stop a service
|
|
|
|
:return: Reference to the submitted command.
|
|
"""
|
|
return self._cmd('stop')
|
|
|
|
def restart(self):
|
|
"""Restart a service
|
|
|
|
:return: Reference to the submitted command.
|
|
"""
|
|
return self._cmd('restart')
|
|
|
|
def start_roles(self, *role_names):
|
|
"""Start a list of roles
|
|
|
|
:param role_names: names of the roles to start.
|
|
:return: List of submitted commands.
|
|
"""
|
|
return self._role_cmd('start', role_names)
|
|
|
|
def create_hbase_root(self):
|
|
"""Create the root directory of an HBase service
|
|
|
|
:return: Reference to the submitted command.
|
|
"""
|
|
return self._cmd('hbaseCreateRoot')
|
|
|
|
def create_hdfs_tmp(self):
|
|
"""Create /tmp directory in HDFS
|
|
|
|
Create the /tmp directory in HDFS with appropriate ownership and
|
|
permissions.
|
|
|
|
:return: Reference to the submitted command
|
|
:since: API v2
|
|
"""
|
|
return self._cmd('hdfsCreateTmpDir')
|
|
|
|
def refresh(self, *role_names):
|
|
"""Execute the "refresh" command on a set of roles
|
|
|
|
:param role_names: Names of the roles to refresh.
|
|
:return: Reference to the submitted command.
|
|
"""
|
|
return self._role_cmd('refresh', role_names)
|
|
|
|
def decommission(self, *role_names):
|
|
"""Decommission roles in a service
|
|
|
|
:param role_names: Names of the roles to decommission.
|
|
:return: Reference to the submitted command.
|
|
"""
|
|
return self._cmd('decommission', data=role_names)
|
|
|
|
def deploy_client_config(self, *role_names):
|
|
"""Deploys client configuration to the hosts where roles are running
|
|
|
|
:param role_names: Names of the roles to decommission.
|
|
:return: Reference to the submitted command.
|
|
"""
|
|
return self._cmd('deployClientConfig', data=role_names)
|
|
|
|
def format_hdfs(self, *namenodes):
|
|
"""Format NameNode instances of an HDFS service
|
|
|
|
:param namenodes: Name of NameNode instances to format.
|
|
:return: List of submitted commands.
|
|
"""
|
|
return self._role_cmd('hdfsFormat', namenodes)
|
|
|
|
def install_oozie_sharelib(self):
|
|
"""Installs the Oozie ShareLib
|
|
|
|
Oozie must be stopped before running this command.
|
|
|
|
:return: Reference to the submitted command.
|
|
:since: API v3
|
|
"""
|
|
return self._cmd('installOozieShareLib', api_version=3)
|
|
|
|
def create_oozie_db(self):
|
|
"""Creates the Oozie Database Schema in the configured database
|
|
|
|
:return: Reference to the submitted command.
|
|
:since: API v2
|
|
"""
|
|
return self._cmd('createOozieDb', api_version=2)
|
|
|
|
def upgrade_oozie_db(self):
|
|
"""Upgrade Oozie Database schema as part of a major version upgrade
|
|
|
|
:return: Reference to the submitted command.
|
|
:since: API v6
|
|
"""
|
|
return self._cmd('oozieUpgradeDb', api_version=6)
|
|
|
|
def create_hive_metastore_tables(self):
|
|
"""Creates the Hive metastore tables in the configured database
|
|
|
|
Will do nothing if tables already exist. Will not perform an upgrade.
|
|
|
|
:return: Reference to the submitted command.
|
|
:since: API v3
|
|
"""
|
|
return self._cmd('hiveCreateMetastoreDatabaseTables', api_version=3)
|
|
|
|
def create_hive_warehouse(self):
|
|
"""Creates the Hive warehouse directory in HDFS
|
|
|
|
:return: Reference to the submitted command.
|
|
:since: API v3
|
|
"""
|
|
return self._cmd('hiveCreateHiveWarehouse')
|
|
|
|
def create_hive_userdir(self):
|
|
"""Creates the Hive user directory in HDFS
|
|
|
|
:return: Reference to the submitted command.
|
|
:since: API v4
|
|
"""
|
|
return self._cmd('hiveCreateHiveUserDir')
|
|
|
|
def enable_nn_ha(self, active_name, standby_host_id, nameservice, jns,
|
|
standby_name_dir_list=None, qj_name=None,
|
|
standby_name=None, active_fc_name=None,
|
|
standby_fc_name=None, zk_service_name=None,
|
|
force_init_znode=True,
|
|
clear_existing_standby_name_dirs=True,
|
|
clear_existing_jn_edits_dir=True):
|
|
"""Enable High Availability (HA) with Auto-Failover for HDFS NameNode
|
|
|
|
@param active_name: Name of Active NameNode.
|
|
@param standby_host_id: ID of host where Standby NameNode will be
|
|
created.
|
|
@param nameservice: Nameservice to be used while enabling HA.
|
|
Optional if Active NameNode already has this
|
|
config set.
|
|
@param jns: List of Journal Nodes to be created during the command.
|
|
Each element of the list must be a dict containing the
|
|
following items:
|
|
- jns['jnHostId']: ID of the host where the new JournalNode
|
|
will be created.
|
|
- jns['jnName']: Name of the JournalNode role (optional)
|
|
- jns['jnEditsDir']: Edits dir of the JournalNode. Can be
|
|
omitted if the config is already set
|
|
at RCG level.
|
|
@param standby_name_dir_list: List of directories for the new Standby
|
|
NameNode. If not provided then it will
|
|
use same dirs as Active NameNode.
|
|
@param qj_name: Name of the journal located on each JournalNodes'
|
|
filesystem. This can be optionally provided if the
|
|
config hasn't been already set for the Active NameNode.
|
|
If this isn't provided and Active NameNode doesn't
|
|
also have the config, then nameservice is used by
|
|
default.
|
|
@param standby_name: Name of the Standby NameNode role to be created
|
|
(Optional).
|
|
@param active_fc_name: Name of the Active Failover Controller role to
|
|
be created (Optional).
|
|
@param standby_fc_name: Name of the Standby Failover Controller role to
|
|
be created (Optional).
|
|
@param zk_service_name: Name of the ZooKeeper service to use for auto-
|
|
failover. If HDFS service already depends on a
|
|
ZooKeeper service then that ZooKeeper service
|
|
will be used for auto-failover and in that case
|
|
this parameter can either be omitted or should
|
|
be the same ZooKeeper service.
|
|
@param force_init_znode: Indicates if the ZNode should be force
|
|
initialized if it is already present. Useful
|
|
while re-enabling High Availability. (Default:
|
|
TRUE)
|
|
@param clear_existing_standby_name_dirs: Indicates if the existing name
|
|
directories for Standby
|
|
NameNode should be cleared
|
|
during the workflow.
|
|
Useful while re-enabling High
|
|
Availability. (Default: TRUE)
|
|
@param clear_existing_jn_edits_dir: Indicates if the existing edits
|
|
directories for the JournalNodes
|
|
for the specified nameservice
|
|
should be cleared during the
|
|
workflow. Useful while re-enabling
|
|
High Availability. (Default: TRUE)
|
|
@return: Reference to the submitted command.
|
|
@since: API v6
|
|
"""
|
|
args = dict(
|
|
activeNnName=active_name,
|
|
standbyNnName=standby_name,
|
|
standbyNnHostId=standby_host_id,
|
|
standbyNameDirList=standby_name_dir_list,
|
|
nameservice=nameservice,
|
|
qjName=qj_name,
|
|
activeFcName=active_fc_name,
|
|
standbyFcName=standby_fc_name,
|
|
zkServiceName=zk_service_name,
|
|
forceInitZNode=force_init_znode,
|
|
clearExistingStandbyNameDirs=clear_existing_standby_name_dirs,
|
|
clearExistingJnEditsDir=clear_existing_jn_edits_dir,
|
|
jns=jns
|
|
)
|
|
return self._cmd('hdfsEnableNnHa', data=args, api_version=6)
|
|
|
|
def enable_rm_ha(self, new_rm_host_id, zk_service_name=None):
|
|
"""Enable high availability for a YARN ResourceManager.
|
|
|
|
@param new_rm_host_id: id of the host where the second ResourceManager
|
|
will be added.
|
|
@param zk_service_name: Name of the ZooKeeper service to use for auto-
|
|
failover. If YARN service depends on a
|
|
ZooKeeper service then that ZooKeeper service
|
|
will be used for auto-failover and in that case
|
|
this parameter can be omitted.
|
|
@return: Reference to the submitted command.
|
|
@since: API v6
|
|
"""
|
|
args = dict(
|
|
newRmHostId=new_rm_host_id,
|
|
zkServiceName=zk_service_name
|
|
)
|
|
return self._cmd('enableRmHa', data=args)
|
|
|
|
|
|
class ApiServiceSetupInfo(ApiService):
|
|
_ATTRIBUTES = {
|
|
'name': None,
|
|
'type': None,
|
|
'config': types.Attr(types.ApiConfig),
|
|
'roles': types.Attr(roles.ApiRole),
|
|
}
|
|
|
|
def __init__(self, name=None, type=None,
|
|
config=None, roles=None):
|
|
# The BaseApiObject expects a resource_root, which we don't care about
|
|
resource_root = None
|
|
# Unfortunately, the json key is called "type". So our input arg
|
|
# needs to be called "type" as well, despite it being a python keyword.
|
|
types.BaseApiObject.init(self, None, locals())
|
|
|
|
def set_config(self, config):
|
|
"""Set the service configuration
|
|
|
|
:param config: A dictionary of config key/value
|
|
"""
|
|
if self.config is None:
|
|
self.config = {}
|
|
self.config.update(types.config_to_api_list(config))
|
|
|
|
def add_role_info(self, role_name, role_type, host_id, config=None):
|
|
"""Add a role info
|
|
|
|
The role will be created along with the service setup.
|
|
|
|
:param role_name: Role name
|
|
:param role_type: Role type
|
|
:param host_id: The host where the role should run
|
|
:param config: (Optional) A dictionary of role config values
|
|
"""
|
|
if self.roles is None:
|
|
self.roles = []
|
|
api_config_list = (config is not None
|
|
and types.config_to_api_list(config)
|
|
or None)
|
|
self.roles.append({
|
|
'name': role_name,
|
|
'type': role_type,
|
|
'hostRef': {'hostId': host_id},
|
|
'config': api_config_list})
|