Skip to content
Closed
78 changes: 0 additions & 78 deletions airflow/contrib/sensors/hdfs_sensor.py

This file was deleted.

170 changes: 101 additions & 69 deletions airflow/hooks/hdfs_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,85 +17,117 @@
# specific language governing permissions and limitations
# under the License.

from six import PY2
import hdfs3
from hdfs3.utils import MyNone

from airflow import configuration
from airflow.exceptions import AirflowException
from airflow.hooks.base_hook import BaseHook


snakebite_imported = False
if PY2:
from snakebite.client import Client, HAClient, Namenode, AutoConfigClient
snakebite_imported = True
class HdfsHook(BaseHook):
"""Hook for interacting with HDFS using the hdfs3 library.

By default hdfs3 loads its configuration from `core-site.xml` and
`hdfs-site.xml` if these files can be found in any of the typical
locations. The hook loads `host` and `port` parameters from the
hdfs connection (if given) and extra configuration parameters can be
supplied in the connections extra JSON as follows:

class HDFSHookException(AirflowException):
pass
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part could also maybe go to the "UPDATING" doc.

"pars": {
"dfs.domain.socket.path": "/var/lib/hadoop-hdfs/dn_socket"
},
"ha": {
"host": "nameservice1",
"conf": {
"dfs.nameservices": "ns1",
"dfs.ha.namenodes.ns1": "nn1,nn2",
"dfs.namenode.rpc-address.ns1.nn1": "host1:8020",
"dfs.namenode.rpc-address.ns1.nn2": "host2:8020",
"dfs.namenode.http-address.ns1.nn1": "host1:50070",
"dfs.namenode.http-address.ns1.nn2": "host2:50070"
}
},
"autoconf": True,
"token": "...",
"ticket_cache": "..."
}

Here `pars` can be used to supply configuration options with the same key
names as typically contained in the XML config files, which will take
precedence over any parameters loaded from files. The `ha` configuration
section can be used to supply options for using hdfs3 in high-availability
mode (see the hdfs3 documentation for more details). The `autoconf` option
controls whether hdfs3 uses the available hadoop config files for
its configuration, whilst the `token` and `ticket_cache` options are
used for configuring kerberos.

class HDFSHook(BaseHook):
"""
Interact with HDFS. This class is a wrapper around the snakebite library.

:param hdfs_conn_id: Connection id to fetch connection info
:type hdfs_conn_id: str
:param proxy_user: effective user for HDFS operations
:type proxy_user: str
:param autoconfig: use snakebite's automatically configured client
:type autoconfig: bool
Security modes can also be configured by defining appropriate value for
the `hadoop.security.authentication` key in `pars`. Kerberos is used
automatically if Airflow has been configured to use kerberos.

:param str hdfs_conn_id: Connection ID to fetch parameters from.
:param bool autoconf: Whether to use autoconfig to discover
configuration options from the hdfs XML configuration files.
"""
def __init__(self, hdfs_conn_id='hdfs_default', proxy_user=None,
autoconfig=False):
if not snakebite_imported:
raise ImportError(
'This HDFSHook implementation requires snakebite, but '
'snakebite is not compatible with Python 3 '
'(as of August 2015). Please use Python 2 if you require '
'this hook -- or help by submitting a PR!')

def __init__(self, hdfs_conn_id=None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a few deprecation utility classes/functions to help keep the re-written classes as backwards compatible as possible.

This is not the case here: __init__(self, hdfs_conn_id='hdfs_default', proxy_user=None, autoconfig=False) -> __init__(self, hdfs_conn_id=None)

super(HdfsHook, self).__init__(None)

self.hdfs_conn_id = hdfs_conn_id
self.proxy_user = proxy_user
self.autoconfig = autoconfig
self._conn = None

def get_conn(self):
"""
Returns a snakebite HDFSClient object.
"""
# When using HAClient, proxy_user must be the same, so is ok to always
# take the first.
effective_user = self.proxy_user
autoconfig = self.autoconfig
use_sasl = configuration.conf.get('core', 'security') == 'kerberos'

try:
connections = self.get_connections(self.hdfs_conn_id)

if not effective_user:
effective_user = connections[0].login
if not autoconfig:
autoconfig = connections[0].extra_dejson.get('autoconfig',
False)
hdfs_namenode_principal = connections[0].extra_dejson.get(
'hdfs_namenode_principal')
except AirflowException:
if not autoconfig:
raise

if autoconfig:
# will read config info from $HADOOP_HOME conf files
client = AutoConfigClient(effective_user=effective_user,
use_sasl=use_sasl)
elif len(connections) == 1:
client = Client(connections[0].host, connections[0].port,
effective_user=effective_user, use_sasl=use_sasl,
hdfs_namenode_principal=hdfs_namenode_principal)
elif len(connections) > 1:
nn = [Namenode(conn.host, conn.port) for conn in connections]
client = HAClient(nn, effective_user=effective_user,

@gglanzani gglanzani Jun 30, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the high availability case handled in the new version?

hdfs3 will read from the configuration files the HA settings (see docs), but if we're getting the configuration from the connection, we need to at least specify how to do so in the extra section.

I was thinking that we could specify a ha key in the extra section, so that extra looks like

{
  'ha': {
    host = "nameservice1"
    conf = {
        "dfs.nameservices": "nameservice1",
        "dfs.ha.namenodes.nameservice1": "namenode113,namenode188",
        "dfs.namenode.rpc-address.nameservice1.namenode113": "hostname_of_server1:8020",
        "dfs.namenode.rpc-address.nameservice1.namenode188": "hostname_of_server2:8020",
        "dfs.namenode.http-address.nameservice1.namenode188": "hostname_of_server1:50070",
        "dfs.namenode.http-address.nameservice1.namenode188": "hostname_of_server2:50070"
    }
} # number of braces might be wrong here :)

and then in the code we could do

ha = params.extra_dejson.get('ha', {})
if ha:
    pars.update(ha.get('conf'))

...
self._conn = hdfs3.HDFileSystem(
    host=ha.get('host') or params.host or MyNone,
...
)

What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way other services support HA via connections is to have multiple rows in the Connections table with different host names but the same conn_id.

(Sorry, quick note, will try to expand on this later)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also known as the poor mans load balancing. This will not really work since there is no fallback, but it will pick a random connection, and then you need to pray that one is up.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True.

Could we add a new method to Connection base/hook to get all connections with the given ID, and use that here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that would work.

Apart from the HA stuff, I think we should also drop hdfs3 and go for PyArrow: https://arrow.apache.org/docs/python/filesystems.html

use_sasl=use_sasl,
hdfs_namenode_principal=hdfs_namenode_principal)
else:
raise HDFSHookException("conn_id doesn't exist in the repository "
"and autoconfig is not specified")

return client
if self._conn is None:
hdfs_params = {}

# Configure kerberos security if used by Airflow.
if configuration.conf.get("core", "security") == "kerberos":
hdfs_params["hadoop.security.authentication"] = "kerberos"

if self.hdfs_conn_id is None:
self._conn = hdfs3.HDFileSystem(pars=hdfs_params, autoconf=True)
else:
conn_params = self.get_connection(self.hdfs_conn_id)
conn_extra_params = conn_params.extra_dejson

# Extract hadoop parameters from extra.
hdfs_params.update(conn_extra_params.get("pars", {}))

# Extract high-availability config if given.
ha_params = conn_extra_params.get("ha", {})
hdfs_params.update(ha_params.get("conf", {}))

# Collect extra parameters to pass to kwargs. Note that we
# avoid passing empty parameters, as hdfs3 seems to do
# funky things with defining its own None variable.
extra_kws = {
"user": conn_params.login or None,
"ticket_cache": conn_extra_params.get("ticket_cache"),
"token": conn_extra_params.get("token"),
}
extra_kws = {k: v for k, v in extra_kws.items() if v}

# Build connection.
self._conn = hdfs3.HDFileSystem(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading through hdfs3 docs, it seems that parameters such as user, ticket_cache, and token might also be useful for accessing a kerberized cluster (see here).

These parameters, however, might need to be dag specific (i.e. a dag impersonates user_a, another user_b). So we might put them in the hook's __init__. What do you think?

@jrderuiter jrderuiter Jul 23, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, as I would like to avoid adding too many too specific arguments. We could add a single argument hdfs3_kwargs, which contains kwargs that are passed directly to hdfs3.HDFileSystem.

However, it might be better to keep these arguments in the connection, as this provides a more uniform interface for the hooks down the line.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if we provide them in the connection, it means we need a connection per different ticket_cache/user.

@Fokko Is this something usual in Airflow? Providing this in the dag code is more flexible.

@bolkedebruin How are you handling this at ING?

host=ha_params.get("host") or conn_params.host or MyNone,
port=conn_params.port or MyNone,
pars=hdfs_params,
autoconf=conn_extra_params.get("autoconf", True),
**extra_kws
)

return self._conn

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.close()

def close(self):
"""Closes the HDFSHook and any underlying connections."""

if self._conn is not None:
self._conn.disconnect()
self._conn = None
Loading