Using an SSH keyfile with Fabric - python

How do you configure fabric to connect to remote hosts using SSH keyfiles (for example, Amazon EC2 instances)?

Finding a simple fabfile with a working example of SSH keyfile usage isn't easy for some reason. I wrote a blog post about it (with a matching gist).
Basically, the usage goes something like this:
from fabric.api import *
env.hosts = ['host.name.com']
env.user = 'user'
env.key_filename = '/path/to/keyfile.pem'
def local_uname():
local('uname -a')
def remote_uname():
run('uname -a')
The important part is setting the env.key_filename environment variable, so that the Paramiko configuration can look for it when connecting.

Also worth mentioning here that you can use the command line args for this:
fab command -i /path/to/key.pem [-H [user#]host[:port]]

Another cool feature available as of Fabric 1.4 - Fabric now supports SSH configs.
If you already have all the SSH connection parameters in your ~/.ssh/config file, Fabric will natively support it, all you need to do is add:
env.use_ssh_config = True
at the beginning of your fabfile.

For fabric2 in fabfile use the following:
from fabric import task, Connection
#task
def staging(ctx):
ctx.name = 'staging'
ctx.user = 'ubuntu'
ctx.host = '192.1.1.1'
ctx.connect_kwargs.key_filename = os.environ['ENV_VAR_POINTS_TO_PRIVATE_KEY_PATH']
#task
def do_something_remote(ctx):
with Connection(ctx.host, ctx.user, connect_kwargs=ctx.connect_kwargs) as conn:
conn.sudo('supervisorctl status')
and run it with:
fab staging do_something_remote
UPDATE:
For multiple hosts (one host will do also) you can use this:
from fabric2 import task, SerialGroup
#task
def staging(ctx):
conns = SerialGroup(
'user#10.0.0.1',
'user#10.0.0.2',
connect_kwargs=
{
'key_filename': os.environ['PRIVATE_KEY_TO_HOST']
})
ctx.CONNS = conns
ctx.APP_SERVICE_NAME = 'google'
#task
def stop(ctx):
for conn in ctx.CONNS:
conn.sudo('supervisorctl stop ' + ctx.APP_SERVICE_NAME)
and run it with fab or fab2:
fab staging stop

For me, the following didn't work:
env.user=["ubuntu"]
env.key_filename=['keyfile.pem']
env.hosts=["xxx-xx-xxx-xxx.ap-southeast-1.compute.amazonaws.com"]
or
fab command -i /path/to/key.pem [-H [user#]host[:port]]
However, the following did:
env.key_filename=['keyfile.pem']
env.hosts=["ubuntu#xxx-xx-xxx-xxx-southeast-1.compute.amazonaws.com"]
or
env.key_filename=['keyfileq.pem']
env.host_string="ubuntu#xxx-xx-xxx-xxx.ap-southeast-1.compute.amazonaws.com"

I had to do this today, my .py file was as simple as possible, like the one posted in the answer of #YuvalAdam but still I kept getting prompted for a password...
Looking at the paramiko (the library used by fabric for ssh) log, I found the line:
Incompatible ssh peer (no acceptable kex algorithm)
I updated paramiko with:
sudo pip install paramiko --upgrade
And now it's working.

None of these answers worked for me on py3.7, fabric2.5.0 and paramiko 2.7.1.
However, using the PKey attribute in the documentation does work: http://docs.fabfile.org/en/2.5/concepts/authentication.html#private-key-objects
from paramiko import RSAKey
ctx.connect_kwargs.pkey = RSAKey.from_private_key_file('path_to_your_aws_key')
with Connection(ctx.host, user, connect_kwargs=ctx.connect_kwargs) as conn:
//etc....

As stated above, Fabric will support .ssh/config file settings after a fashion, but using a pem file for ec2 seems to be problematic. IOW a properly setup .ssh/config file will work from the command line via 'ssh servername' and fail to work with 'fab sometask' when env.host=['servername'].
This was overcome by specifying the env.key_filename='keyfile' in my fabfile.py and duplicating the IdentityFile entry already in my .ssh/config.
This could be either Fabric or paramiko, which in my case was Fabric 1.5.3 and Paramiko 1.9.0.

Related

How do I run a local command with fabric 2?

I want to use Fabric and run a command on local, without having to establish any additional connections.
How do I do this in fabric 2? ... documentation seems to miss to give any example.
The design decision to drop the local command in Fabric 2 makes this more difficult, but I was able to simulate it by using Context from Invoke instead of Connection:
from fabric import Connection
from invoke.context import Context
#task
def hostname(c):
c.run('hostname')
#task
def test(c):
conn = Connection('user#host')
hostname(conn)
local_ctx = Context(c.config) # can be passed into #task;
# Connection is a subclass of Context
hostname(local_ctx)
After several different attemps and spending lots of time I found this elegant solution for starting a server (pty=True) and run local commands.
fabfile.py
from fabric import task
#task
def env_test(c):
c.run("env", replace_env=False)
#task
def go(c):
c.run("manage.py runserver", replace_env=False, pty=True)
Please be aware again, these two commands are only meant for local development tasks!
Further Reading: Fabric2 Connections, Upgrading from 1.x
run, sudo, and local are done the same:
from fabric import Connection
cn = Connection('scott#104.131.61.12') # presumes ssh keys were exchanged
cn.run('ls -al') # assuming ssh to linux server - as scott
cn.sudo('whoami') # as root
cn.local('echo ---------- now from local')
cn.local('dir /w') # assuming client is windows
I am adding #TheRealChx101's comment as an answer because I ran into troubles with Connection.local.
Not all environment variables got into the pty, so some of my scripts did not work properly.
With the import from invoke run as local stanza (Invoke's local instead of Fabric's), everything worked fine.
# -*- coding: utf-8 -*-
from fabric import task
from invoke import run as local
#task(default=True)
def testwxmsg(c):
local("pytest --reuse-db --no-migrations tests/weixin/test_release_accrual.py")
This is similar to the answer by #phoibos, but I wanted to show that #task is not needed.
import sys
from fabric import Connection
from invoke.context import Context
target_host=sys.argv[1]
if target_host == 'localhost':
ctx = Context()
else:
ctx = Connection(target_host)
ctx.run('hostname', echo=False, hide=None)
if isinstance(ctx, Connection):
ctx.close()
Local:
> python demo.py localhost
MyComputerName
Remote:
> python demo.py demo.example.com
demo.example.com

Retrieve current host from within an executed task

I'm using fabric to execute some remote commands on several hosts by setting:
env.hosts = [host1, host2, ...]
There are several tasks I want to perform on some of the hosts, and some I don't.
is there's a way I can retrieve the current hostname the task is executing on?
Any help would be great.
Thanks,
Meny
Why not use different host roles?
from fabric.api import env, roles, run
env.roledefs['webservers'] = ['www1', 'www2', 'www3']
#roles('webservers')
def my_task():
run('ls -l')
Additionally, you can get the current executing host from the env dictionary:
def my_task():
print 'Currently executing on {0}'.format(env.host)
eclaird answer definitely helped me use a better practice when executing tasks on several hosts with different roles.
Though, while playing with it, it seems that within the task, env.host will give the name of the current hostname.
for example:
#parallel(pool_size=len(env.hosts))
def upload_to_s3(to):
awscreds = 'some credentials...'
cmd = '%s aws s3 sync /mnt/backup %s/%s/' % (awscreds, to, env.host)
run(cmd)

Python Fabric decorator

I've some fabric tasks in my fabfile and I need to initialize, the env variable before their execution. I'm trying to use a decorator, it works but fabric always says "no host found Please specify (single)" however if I print the content of my variable "env" all seems good.
Also I call my tasks from another python script.
from fabric.api import *
from instances import find_instances
def init_env(func):
def wrapper(*args, **kwargs):
keysfolder = 'keys/'
env.user = 'admin'
env.key_filename = '%skey_%s_prod.pem'%(keysfolder, args[0])
env.hosts = find_instances(args[1])
return func(args[0], args[1])
return wrapper
#init_env
def restart_apache2(region, groupe):
print(env.hosts)
run('/etc/init.d/apache2 restart')
return True
My script which call the fabfile:
from fabfile import init_env, restart_apache2
restart_apache2('eu-west-1', 'apache2')
Output of print in restart apache2:
[u'10.10.0.1', u'10.10.0.2']
Any idea why my task restart_apache2 doesn't use the env variable?
Thanks
EDIT:
Which is interesting it's if in my script which calls the fabfile, I use settings from fabric.api and set a host ip, it works. This show that my decorator has well initialized the env variable because the key and user are send to fabric. It's only the env.hosts that's not read by fabric...
EDIT2:
I can reach my goal with using settings from fabric.api, like that:
#init_env
def restart_apache2(region, groupe):
for i in env.hosts:
with settings(host_string = '%s#%s' % (env.user, i)):
run('/etc/init.d/apache2 restart')
return True
Bonus question, has there a solution to use directly the env.hosts without settings?
I'm guessing here a little, but I'm assuming you've got into trouble because you're trying to solve two problems at once.
The first issue relates to the issue of multiple hosts. Fabric includes the concepts of roles, which are just groups of machines that you can issue commands to in one go. The information in the find_instances function could be used to populate this data.
from fabric import *
from something import find_instances
env.roledefs = {
'eu-west-1' : find_instances('eu-west-1'),
'eu-west-2' : find_instances('eu-west-2'),
}
#task
def restart_apache2():
run('/etc/init.d/apache2 restart')
The second issue is that you have different keys for different groups of servers. One way to resolve this problem is to use an SSH config file to prevent you from having to mix the details of the keys / users accounts with your fabric code. You can either add an entry per instance into your ~/.ssh/config, or you can use local SSH config (env.use_ssh_config and env.ssh_config_path)
Host instance00
User admin
IdentityFile keys/key_instance00_prod.pem
Host instance01
User admin
IdentityFile keys/key_instance01_prod.pem
# ...
On the command line, you should then be able to issue the commands like:
fab restart_apache2 -R eu-west-1
Or, you can still do single hosts:
fab restart_apache2 -H apache2
In your script, these two are equivalent to the execute function:
from fabric.api import execute
from fabfile import restart_apache2
execute(restart_apache2, roles = ['eu-west-1'])
execute(restart_apache2, hosts = ['apache2'])

Fabric Sudo No Password Solution

This question is about best practices. I'm running a deployment script with Fabric. My deployment user 'deploy' needs sudo to restart services. So I am using the sudo function from fabric to run these commands in my script. This works fine but prompts for password during script execution. I DON'T want to type a password during deployments. What's the best practice here. The only solution I can think of is changing the sudo permissions to not require password for the commands my deployment user runs. This doesn't seem right to me.
The ideal solution is to create a user on your server that is used only for deployment (eg, deploy). Then, set env.user=deploy in your fabfile. Then on your servers, you can give the user the necessary permission on a command-by-command basis in a sudoers file:
IMPORTANT: Always use sudo visudo to modify a sudoers file
Cmnd_Alias RELOAD_SITE = /bin/bash -l -c supervisorctl*, /usr/bin/supervisorctl*
deploy ALL = NOPASSWD: RELOAD_SITE
You can add as many Cmnd_Alias directives as is needed by the deploy user, then grant NOPASSWD access for each of those commands. See man sudoers for more details.
I like to keep my deploy-specific sudoers config in /etc/sudoers.d/deploy and include that file from /etc/sudoers by adding: includedir /etc/suoders.d at the end.
You can use:
fabric.api import env
# [...]
env.password = 'yourpassword'
The best way to do this is with subtasks. You can prompt for a password in the fabfile and never expose any passwords, nor make reckless configuration changes to sudo on the remote system(s).
import getpass
from fabric.api import env, parallel, run, task
from fabric.decorators import roles
from fabric.tasks import execute
env.roledefs = {'my_role': ['host1', 'host2']}
#task
# #parallel -- uncomment if you need parallel execution, it'll work!
#roles('my_role')
def deploy(*args, **kwargs):
print 'deploy args:', args, kwargs
print 'password:', env.password
run('echo hello')
#task
def prompt(task_name, *args, **kwargs):
env.password = getpass.getpass('sudo password: ')
execute(task_name, *args, role='my_role', **kwargs)
Note that you can even combine this with parallel execution and the prompt task still only runs once, while the deploy task runs for each host in the role, in parallel.
Finally, an example of how you would invoke it:
$ fab prompt:deploy,some_arg,another_arg,key=value
Seems like sudo may not be that bad of an option after all. You can specify which commands a user can run and the arguments the command may take (man sudoers). If the problem is just having to type the password, an option would involve using the pexpect module to login automatically, maybe with a password that you could store encrypted:
import pexpect, sys
pwd = getEncryptedPassword()
cmd = "yourcommand"
sCmd = pexpect.spawn('sudo {0}'.format(cmd))
sCmd.logfile_read = sys.stdout
sCmd.expect('Password:')
sCmd.sendline(pwd)
sCmd.expect(pexpect.EOF)
Use the keyring module to store and access passwords securely.
Here's how I do it with Fabric 2:
from fabric import task
import keyring
#task
def restart_apache(connection):
# set the password with keyring.set_password('some-host', 'some-user', 'passwd')
connection.config.sudo.password = keyring.get_password(connection.host, 'some-user')
connection.sudo('service apache2 restart')
You could also use GPG or any other command-line password tool. For example:
connection.config.sudo.password = connection.local('gpg --quiet -d /path/to/secret.gpg', hide=True).strip()
The secret.gpg file can be generated with echo "mypassword" | gpg -e > secret.gpg. The hide argument avoids echoing the password to the console.
To retain support for --prompt-for-sudo-password, add a conditional:
if not connection.config.sudo.password:
connection.config.sudo.password = keyring.get_password(connection.host, 'some-user')
You can also use passwords for multiple machines:
from fabric import env
env.hosts = ['user1#host1:port1', 'user2#host2.port2']
env.passwords = {'user1#host1:port1': 'password1', 'user2#host2.port2': 'password2'}
See this answer: https://stackoverflow.com/a/5568219/552671
As Bartek also suggests, enable password-less sudo for the deployment 'user' in the sudoers file.
Something like:
run('echo "{0} ALL=(ALL) ALL" >> /etc/sudoers'.format(env.user))

How to force the fabric connect to remote host before run() executed?

I am use fabric to write my rsync wrapper, the variable env.host_string will be set by execute() to run task. To get env.host_string, I run('pwd') first, and run rsync.
Is It possible to make sure user set env.hosts before some checkpoint arrived, such as the condition src == env.host_string ?
from fabric.api import run, env, task, abort
from string import Template
#task
def sync_to_same_dir(src, path):
env.host_string
cmd_template = Template("""rsync --dry-run -e ssh -avr $user#$src_host:$path/ $path/""")
if path.endswith("/"):
path = path[:-1]
run("pwd") # it's a work around
if src == env.host_string:
abort("cannot rysnc to the same host")
cmd = cmd_template.substitute(path=path, user=env.user, src_host=src)
run(cmd)
I find the answer from fabric's source code. There is a simple idea: how run check host as needed ?
#needs_host
def put(local_path=None, remote_path=None, use_sudo=False,
mirror_local_mode=False, mode=None):
"""
Upload one or more files to a remote host.
"""
I trace the needs_host it will prompt to ask hosts, when the user don't assign any hosts:
No hosts found. Please specify (single) host string for connection:
We can rewrite the task as:
from fabric.network import needs_host
#task
#needs_host
def the_task_needs_host(): pass
What are you trying to do? A task knows what host it's using w/o having any other fabric calls:
fab -f host-test.py foo
[98.98.98.98] Executing task 'foo'
98.98.98.98
98.98.98.98
Done.
And here is the script eg:
#!/user/bin/env python
from fabric.api import *
env.user = 'mgoose'
#task
#hosts("98.98.98.98")
def foo():
print(env.host)
print(env.host_string)
So you don't have to do anything special to know what host your task is on.

Categories