herokuping

Send ICMP ping from python on Heroku


I have a Django app running on Heroku. I want to ping an external website to see if it responds from my app. I have found multiple solutions locally, but none work on Heroku:

I installed iputils-ping via the https://buildpack-registry.s3.amazonaws.com/buildpacks/heroku-community/apt.tgz buildpack, and confirmed I see it installed in the deploy logs:

remote: -----> Installing iputils-ping_3%3a20190709-3ubuntu1_amd64.deb
remote:  new Debian package, version 2.0.
remote:  size 40000 bytes: control archive=1204 bytes.
remote:      648 bytes,    18 lines      control              
remote:      334 bytes,     5 lines      md5sums              
remote:      693 bytes,    32 lines   *  postinst             #!/bin/sh
remote:  Package: iputils-ping
remote:  Source: iputils
remote:  Version: 3:20190709-3ubuntu1
remote:  Architecture: amd64
remote:  Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
remote:  Installed-Size: 106

I have an Aptfile inside my root folder:

In [4]: os.listdir()
Out[4]: 
['Aptfile',...]

and in that is one line:

iputils-ping.

I then try to run: subprocess.run(["ping", "-c", "2", url], stdout=subprocess.PIPE,stderr=subprocess.PIPE, text=True) but I get an error:

<ipython-input-5-f67d5e6c83d7> in <module>
----> 1 subprocess.run(["ping", "-c", "2", url], stdout=subprocess.PIPE,stderr=subprocess.PIPE, text=True)

~/.heroku/python/lib/python3.9/subprocess.py in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    499         kwargs['stderr'] = PIPE
    500 
--> 501     with Popen(*popenargs, **kwargs) as process:
    502         try:
    503             stdout, stderr = process.communicate(input, timeout=timeout)

~/.heroku/python/lib/python3.9/subprocess.py in __init__(self, args, bufsize, executable, stdin, stdout, stderr, preexec_fn, close_fds, shell, cwd, env, universal_newlines, startupinfo, creationflags, restore_signals, start_new_session, pass_fds, user, group, extra_groups, encoding, errors, text, umask)
    945                             encoding=encoding, errors=errors)
    946 
--> 947             self._execute_child(args, executable, preexec_fn, close_fds,
    948                                 pass_fds, cwd, env,
    949                                 startupinfo, creationflags, shell,

~/.heroku/python/lib/python3.9/subprocess.py in _execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite, restore_signals, gid, gids, uid, umask, start_new_session)
   1817                     if errno_num != 0:
   1818                         err_msg = os.strerror(errno_num)
-> 1819                     raise child_exception_type(errno_num, err_msg, err_filename)
   1820                 raise child_exception_type(err_msg)
   1821 

FileNotFoundError: [Errno 2] No such file or directory: 'ping'

Solution

  • This is more complex than it might appear on the surface. Unfortunately, it looks like it may not be possible.

    Install the ping command

    My first suggestion is to install the iputils-ping package using the Apt buildpack and call that from your Python code. This is a good approach for installing Ubuntu packages on your dyno, and it usually works.

    Command not found

    Except in this case, ping won't be on the PATH, so simply running ping won't work.

    This is because of how the Apt buildpack operates. Instead of simply apt-get installing everything, it pulls the .deb files down and extracts them to a special location: $BUILD_DIR/.apt/. It then adds $HOME/.apt/usr/bin to the PATH environment variable.

    For most binaries, this will work fine. Except the iputlis-ping package (and the alternative inetutils-ping package) put the ping binary in /bin/, not /usr/bin/.

    On a regular Ubuntu machine, /bin is simply a symlink to /usr/bin so either path will work, but the Apt buildpack doesn't have a link like that. ping is only available as $HOME/.apt/bin/ping, and the buildpack does not add $HOME/.apt/bin to the PATH.

    So, you should be able to run it via that full path (/app/.apt/bin/ping). That's not ideal since it won't work locally, so modifying PATH may be the best solution.

    I think that should be achievable by adding a file like .profile.d/000-ping.sh to your repository¹:

    export PATH=/app/.apt/bin:$PATH
    

    ...except root

    But even after jumping through the hoops of installing the package and finding the binary, it doesn't look like it will work. Dynos appear to have their networking sufficiently locked down so as to prevent outgoing pings.

    /app/.apt/bin/ping: socket: Operation not permitted
    

    Given that, it's unlikely that you'll be able to send a ping no matter what approach you try.


    ¹A better solution would be to update the buildpack to include a symlink from .apt/bin to .apt/usr/bin, mimicking a real system. I may submit a pull request if I can verify that this works.