Skip to content

Commit

Permalink
initial import.
Browse files Browse the repository at this point in the history
  • Loading branch information
sorend committed Jan 21, 2017
0 parents commit d5ac68f
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.pyc
__pycache__
venv
.cache
*.egg-info
.eggs
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sshconfig
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
language: python
python:
- "2.7"
install:
- python setup.py install
script:
- "py.test ."
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2017 Søren A. Davidsen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

sshconfig
===========

Sshconfig is a library for reading and modifying your ssh/config file in a non-intrusive way, meaning
your file should look more or less the same after modifications. Idea is to keep it simple,
so you can modify it for your needs.

Read more about ssh config files here: [Create SSH config file on Linux](https://www.cyberciti.biz/faq/create-ssh-config-file-on-linux-unix/)


Installation and usage
---------------------------

Install through pip is the most easy way. You can install from the Git source directly:

pip install -e https://github.com/sorend/sshconfig.git

Below is some example use:

from __future__ import print_function
from sshconfig import read_ssh_config, empty_ssh_config
from os.path import expanduser

c = read_ssh_config(expanduser("~/.ssh/config"))
print("hosts", c.hosts())

# assuming you have a host "svu"
print("svu host", c.host("svu")) # print the settings
c.update("svu", Hostname="ssh.svu.local", Port=1234)
print("svu host now", c.host("svu"))

c.add("newsvu", Hostname="ssh-new.svu.local", Port=22, User="stud1234")
print("newsvu", c.host("newsvu"))

c.rename("newsvu", "svu-new")
print("svu-new", c.host("svu-new"))

c.write(expanduser("~/.ssh/newconfig")) # write to new file

# creating a new config file.
c2 = empty_ssh_config()
c2.add("svu", Hostname="ssh.svu.local", User="teachmca", Port=22)
c2.write("newconfig")


About
-----

sshconfig is created at the Department of Computer Science at Sri Venkateswara University, Tirupati, INDIA by a student as part of his projects.
20 changes: 20 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from setuptools import setup

MY_VERSION = '0.0.1'

setup(
name='sshconfig',
version=MY_VERSION,
description='Lightweight SSH config library',
author='Søren Atmakuri Davidsen',
author_email='[email protected]',
url='https://github.com/sorend/sshconfig',
download_url='https://github.com/sorend/sshconfig/tarball/%s' % (MY_VERSION,),
license='MIT',
keywords=['ssh', 'config'],
install_requires=[
],
setup_requires=['pytest-runner'],
tests_require=['pytest'],
)
156 changes: 156 additions & 0 deletions sshconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

import re

def read_ssh_config(path):
"""
Read ssh config file and return parsed SshConfig
"""
with open(path, "r") as f:
lines = f.read().splitlines()
return SshConfig(lines)

def empty_ssh_config():
"""
Creates a new empty ssh configuration.
"""
return SshConfig([])

def _key_value(line):
no_comment = line.split("#")[0]
return [ x.strip() for x in re.split(r"\s+", no_comment.strip(), 1) ]

class SshConfig(object):
"""
Class for manipulating SSH configuration.
"""
def __init__(self, lines):
self.lines_ = []
self.hosts_ = set()
self.parse(lines)

def parse(self, lines):
cur_entry = None
for line in lines:
kv = _key_value(line)
if len(kv) > 1:
k, v = kv
if k == "Host":
cur_entry = v
self.hosts_.add(v)
self.lines_.append(dict(line=line, host=cur_entry, key=k, value=v))
else:
self.lines_.append(dict(line=line, host=None))

def hosts(self):
"""
Return the hosts found in the configuration.
Returns
-------
Tuple of Host entries (including "*" if found)
"""
return tuple(self.hosts_)

def host(self, host):
"""
Return the configuration of a specific host as a dictionary.
Parameters
----------
host : the host to return values for.
Returns
-------
dict of key value pairs, excluding "Host"
"""
if host in self.hosts_:
return { k: v for k, v in [ (x["key"], x["value"]) for x in self.lines_
if x["host"] == host and x["key"] != "Host" ]}
else:
return {}

def update(self, host, **kwargs):
"""
Update configuration for an existing host. Note, you can update the "Host" value, but
it will still be referred to by the old "Host" value.
Parameters
----------
host : the Host to modify.
**kwargs : The new configuration parameters
"""
if host not in self.hosts_:
raise ValueError("Host %s: not found." % host)

if "Host" in kwargs:
raise ValueError("Cannot modify Host value with update, use rename.")

def update_line(k, v):
return "\t%s\t%s" % (k, v)

for key, value in kwargs.items():
found = False
for line in self.lines_:
if line["host"] == host and line["key"] == key:
line["value"] = value
line["line"] = update_line(key, value)
found = True

if not found:
max_idx = max([ idx for idx, line in enumerate(self.lines_) if line["host"] == host ])
self.lines_.insert(max_idx + 1, dict(line=update_line(key, value),
host=host, key=key, value=value))

def rename(self, old_host, new_host):
"""
Renames a host configuration.
Parameters
----------
old_host : the host to rename.
new_host : the new host value
"""
if new_host in self.hosts_:
raise ValueError("Host %s: already exists." % new_host)
for line in self.lines_: # update lines
if line["host"] == old_host:
line["host"] = new_host
if line["key"] == "Host":
line["value"] = new_host
line["line"] = "Host\t%s" % new_host
self.hosts_.remove(old_host) # update host cache
self.hosts_.add(new_host)

def add(self, host, **kwargs):
"""
Add another host to the SSH configuration.
Parameters
----------
host: The Host entry to add.
**kwargs: The parameters for the host (without "Host" parameter itself)
"""
if host in self.hosts_:
raise ValueError("Host %s: exists (use update)." % host)
self.hosts_.add(host)
self.lines_.append(dict(line="", host=None))
self.lines_.append(dict(line="Host\t%s" % host, host=host, key="Host", value=host))
for k, v in kwargs.items():
self.lines_.append(dict(line="\t%s\t%s" % (k, str(v)), host=host, key=k, value=v))

def config(self):
"""
Return the configuration as a string.
"""
return "\n".join([ x["line"] for x in self.lines_ ])

def write(self, path):
"""
Writes ssh config file
Parameters
----------
path : The file to write to
"""
with open(path, "w") as f:
f.write(self.config())
13 changes: 13 additions & 0 deletions tests/test_config
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

# comment
Host *
User something

# comment 2
Host svu
Hostname www.svuniversity.ac.in
Port 22
ProxyCommand nc -w 300 -x localhost:9050 %h %p

# another comment
# bla bla
103 changes: 103 additions & 0 deletions tests/test_sshconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from __future__ import print_function
import sshconfig
import pytest
import os

test_config = os.path.join(os.path.dirname(__file__), "test_config")

def test_parsing():
c = sshconfig.read_ssh_config(test_config)
assert len(c.hosts()) == 2
assert c.host("*")["User"] == "something"
assert c.host("svu")["ProxyCommand"] == "nc -w 300 -x localhost:9050 %h %p"

s1 = c.config().splitlines()
s2 = open(test_config).readlines()
assert len(s1) == len(s2)

def test_update():
c = sshconfig.read_ssh_config(test_config)

c.update("svu", Compression="no", Port=2222)
assert "\tCompression\tno" in c.config()
assert "\tPort\t2222" in c.config()

def test_update_host_failed():
c = sshconfig.read_ssh_config(test_config)

with pytest.raises(ValueError):
c.update("svu", Host="svu-new")

def test_rename():

c = sshconfig.read_ssh_config(test_config)

assert c.host("svu")["Hostname"] == "www.svuniversity.ac.in"

c.rename("svu", "svu-new")

assert "Host\tsvu-new" in c.config()
assert "Host\tsvu\n" not in c.config()
assert "svu" not in c.hosts()
assert "svu-new" in c.hosts()

c.update("svu-new", Port=123) # has to be success
assert c.host("svu-new")["Port"] == 123
assert c.host("svu-new")["Hostname"] == "www.svuniversity.ac.in" # still same

with pytest.raises(ValueError): # we can't refer to the renamed host
c.update("svu", Port=123)

def test_update_fail():
c = sshconfig.read_ssh_config(test_config)

with pytest.raises(ValueError):
c.update("notfound", Port=1234)

def test_add():

c = sshconfig.read_ssh_config(test_config)

c.add("venkateswara", Hostname="venkateswara.onion", User="other", Port=22,
ProxyCommand="nc -w 300 -x localhost:9050 %h %p")

assert "venkateswara" in c.hosts()
assert c.host("venkateswara")["ProxyCommand"] == "nc -w 300 -x localhost:9050 %h %p"

assert "Host\tvenkateswara" in c.config()

with pytest.raises(ValueError):
c.add("svu")

with pytest.raises(ValueError):
c.add("venkateswara")

def test_save():
import tempfile

tc = os.path.join(tempfile.gettempdir(), "temp_ssh_config-4123")
try:
c = sshconfig.read_ssh_config(test_config)

c.update("svu", Hostname="ssh.svuniversity.ac.in", User="mca")
c.write(tc)

c2 = sshconfig.read_ssh_config(tc)
assert c2.host("svu")["Hostname"] == "ssh.svuniversity.ac.in"
assert c2.host("svu")["User"] == "mca"

finally:
os.remove(tc)

def test_empty():
import tempfile
tc = os.path.join(tempfile.gettempdir(), "temp_ssh_config-123")
try:
c = sshconfig.empty_ssh_config()
c.add("svu33", Hostname="ssh33.svu.local", User="mca", Port=22)
c.write(tc)
c2 = sshconfig.read_ssh_config(tc)
assert 1 == len(c2.hosts())
assert c2.host("svu33")["Hostname"] == "ssh33.svu.local"
finally:
os.remove(tc)

0 comments on commit d5ac68f

Please sign in to comment.