├── tests ├── __init__.py ├── etc │ ├── iptables.rules │ ├── keepalived.conf-bad │ ├── ldirectord.conf-bad │ ├── ldirectord.conf │ ├── keepalived.conf │ └── ldirectord.conf-1 ├── maintenance │ └── 192.0.2.201:80 ├── cache │ └── .gitignore ├── scripts │ ├── iptables │ ├── ipvsadm │ ├── ipvsadm2 │ └── ipvsadm3 ├── test_keepalived.py ├── test_keepalived_disable.py ├── test_keepalived_enable.py ├── test_lvs.py ├── test_keepalived2.py ├── test_keepalived_show_real.py ├── test_firewall.py ├── test_ldirectord.py └── test_shell.py ├── debian ├── compat ├── python-lvsm.manpages ├── rules ├── control ├── changelog └── copyright ├── lvsm ├── modules │ ├── __init__.py │ ├── parse_test.py │ ├── parseactions.py │ ├── ldirectordprompts.py │ ├── keepalivedprompts.py │ ├── ldparser.py │ ├── ldirectord.py │ ├── kaparser.py │ └── keepalived.py ├── snimpy_dummy │ ├── __init__.py │ ├── snmp.py │ ├── mib.py │ └── manager.py ├── __init__.py ├── lvs.py ├── __main__.py ├── utils.py ├── sourcecontrol.py ├── termcolor.py ├── firewall.py ├── genericdirector.py └── shell.py ├── MANIFEST.in ├── requirements.txt ├── .travis.yml ├── .gitignore ├── setup.py ├── LICENSE.rst ├── README.rst ├── doc ├── lvsm.1.rst └── lvsm.1 ├── samples └── lvsm.conf.sample └── CHANGES.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /lvsm/modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lvsm/snimpy_dummy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/python-lvsm.manpages: -------------------------------------------------------------------------------- 1 | doc/lvsm.1 2 | -------------------------------------------------------------------------------- /tests/etc/iptables.rules: -------------------------------------------------------------------------------- 1 | # iptables 2 | -------------------------------------------------------------------------------- /tests/etc/keepalived.conf-bad: -------------------------------------------------------------------------------- 1 | foobar 2 | -------------------------------------------------------------------------------- /tests/etc/ldirectord.conf-bad: -------------------------------------------------------------------------------- 1 | foobar 2 | -------------------------------------------------------------------------------- /tests/maintenance/192.0.2.201:80: -------------------------------------------------------------------------------- 1 | Disabled for testing 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include tests * 2 | include *.rst 3 | include *.txt 4 | -------------------------------------------------------------------------------- /tests/etc/ldirectord.conf: -------------------------------------------------------------------------------- 1 | # director config 2 | maintenancedir = tests/maintenance 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # these requirements are for testing only 2 | # nose 3 | pyparsing==1.5.7 4 | -------------------------------------------------------------------------------- /tests/cache/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /lvsm/snimpy_dummy/snmp.py: -------------------------------------------------------------------------------- 1 | """Dummy file used to import snmp expcetions""" 2 | 3 | class SNMPException(Exception): 4 | pass -------------------------------------------------------------------------------- /lvsm/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | LVSM - A shell to manage LVS and iptables. 3 | """ 4 | 5 | __author__ = 'Khosrow E.' 6 | __version__ = '0.5.2' 7 | __license__ = 'MIT' 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | # - "python setup.py install" 6 | - "pip install -r requirements.txt" 7 | # command to run tests 8 | script: "python setup.py test" 9 | -------------------------------------------------------------------------------- /lvsm/snimpy_dummy/mib.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy module to simulate the mib module in snimpy 3 | """ 4 | 5 | def load(mib): 6 | """dummy load""" 7 | pass 8 | 9 | class SMIException(Exception): 10 | """SMI related exception. Any exception thrown in this module is 11 | inherited from this one.""" 12 | pass -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ --with python2 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | # Mac OS X files 30 | .DS_Store 31 | 32 | # project files 33 | *.sublime-* 34 | 35 | # config files used for testing 36 | lvsm.conf 37 | 38 | # KDE files 39 | .directory 40 | -------------------------------------------------------------------------------- /lvsm/modules/parse_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../'))) 4 | from lvsm.modules import ldparser, kaparser 5 | 6 | if len(sys.argv) != 2: 7 | print "Need config file name" 8 | sys.exit(0) 9 | 10 | c = sys.argv[1] 11 | 12 | f = open(c) 13 | conf = "".join(f.readlines()) 14 | 15 | #t = ldparser.tokenize_config(conf) 16 | t = kaparser.tokenize_config(conf) 17 | 18 | if t: 19 | print "%s parsed OK!" % c 20 | print t.dump() 21 | 22 | else: 23 | print "%s didn't parse OK!" % c 24 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: lvsm 2 | Section: python 3 | Priority: extra 4 | Maintainer: Khosrow Ebrahimpour 5 | Build-Depends: debhelper (>= 9.0.0), python, python-setuptools 6 | Standards-Version: 3.9.6 7 | X-Python-Version: >= 2.7 8 | Homepage: https://github.com/khosrow/lvsm 9 | 10 | Package: python-lvsm 11 | Architecture: any 12 | Depends: ${shlibs:Depends}, ${misc:Depends}, ${python:Depends}, python-pkg-resources, iptables, ipvsadm, snimpy, python-pyparsing 13 | Suggests: keepalived | ldirectord 14 | Description: a shell to manage LVS and iptables config on a Linux server 15 | lvsm provides a command shell to manage a Linux Virtual Server (LVS) 16 | as a unified system and aims to simplify the management of such systems. 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # from distutils.core import setup 2 | from setuptools import setup, find_packages 3 | import lvsm 4 | 5 | setup( 6 | name='lvsm', 7 | version=lvsm.__version__, 8 | author=lvsm.__author__, 9 | author_email='khosrow@khosrow.ca', 10 | packages=find_packages(exclude='tests'), 11 | url='https://github.com/khosrow/lvsm', 12 | license='LICENSE.rst', 13 | description=lvsm.__doc__.strip(), 14 | long_description="\n\n".join([open('README.rst').read(), 15 | open('CHANGES.rst').read()]), 16 | entry_points={ 17 | 'console_scripts': [ 18 | 'lvsm = lvsm.__main__:main', 19 | 'kaparser = lvsm.modules.kaparser:main', 20 | ], 21 | }, 22 | test_suite="tests", 23 | data_files=[('share/doc/lvsm/samples', ['samples/lvsm.conf.sample'])] 24 | ) 25 | -------------------------------------------------------------------------------- /tests/scripts/iptables: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $1 = "-t" ] && [ $2 = "nat" ] 4 | then 5 | echo "Chain PREROUTING (policy ACCEPT) 6 | target prot opt source destination 7 | 8 | Chain INPUT (policy ACCEPT) 9 | target prot opt source destination 10 | 11 | Chain OUTPUT (policy ACCEPT) 12 | target prot opt source destination 13 | 14 | Chain POSTROUTING (policy ACCEPT) 15 | target prot opt source destination" 16 | else 17 | echo "Chain INPUT (policy ACCEPT) 18 | target prot opt source destination 19 | ACCEPT tcp -- anywhere 192.0.2.2 tcp dpt:80 20 | 21 | Chain FORWARD (policy ACCEPT) 22 | target prot opt source destination 23 | 24 | Chain OUTPUT (policy ACCEPT) 25 | target prot opt source destination" 26 | fi 27 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | lvsm (0.5.2) unstable; urgency=low 2 | 3 | * Builds successfully on LP.net 4 | * Lots of minor bug fixes 5 | 6 | -- Khosrow Ebrahimpour Thu, 10 Mar 2016 21:10:45 +0500 7 | 8 | lvsm (0.5.1-1) precise; urgency=low 9 | 10 | * Fixes for displaying FWMark 11 | * Syntax parsing now optional 12 | * Numerous bug fixes for Keepalived enable/disable 13 | 14 | -- Khosrow Ebrahimpour Wed, 16 Dec 2015 20:36:43 +0000 15 | 16 | lvsm (0.5.0-1cmc-1) unstable; urgency=low 17 | 18 | * Updated package to create /var/cache/lvsm 19 | 20 | -- Khosrow Ebrahimpour Tue, 11 Feb 2014 15:08:58 +0000 21 | 22 | lvsm (0.5.0-cmc) unstable; urgency=low 23 | 24 | * Initial release for use at CMC 25 | 26 | -- Khosrow Ebrahimpour Wed, 29 Jan 2014 15:55:47 +0000 27 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | *MIT License* 2 | 3 | Copyright (c) 2012 Khosrow Ebrahimpour 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /lvsm/lvs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Director specific functionality 3 | """ 4 | # from genericdirector import GenericDirector 5 | # from modules.ldirectord import Ldirectord 6 | # from modules.keepalived import Keepalived 7 | 8 | class Director(object): 9 | """ 10 | Factory class that returns a director object based on the name provided 11 | """ 12 | # directors = {'generic': GenericDirector, 13 | # 'ldirectord': Ldirectord, 14 | # 'keepalived': Keepalived} 15 | 16 | def __new__(self, name, ipvsadm, configfile='', restart_cmd='', nodes='', args=dict()): 17 | if name == 'keepalived': 18 | from modules.keepalived import Keepalived 19 | return Keepalived(ipvsadm, configfile, restart_cmd, nodes, args) 20 | elif name == 'ldirectord': 21 | from modules.ldirectord import Ldirectord 22 | return Ldirectord(ipvsadm, configfile, restart_cmd, nodes, args) 23 | else: 24 | from genericdirector import GenericDirector 25 | return GenericDirector(ipvsadm, configfile, restart_cmd, nodes, args) 26 | -------------------------------------------------------------------------------- /tests/etc/keepalived.conf: -------------------------------------------------------------------------------- 1 | ! Configuration File for keepalived 2 | 3 | global_defs { 4 | notification_email { 5 | user@example.org 6 | } 7 | notification_email_from Alexandre.Cassen@firewall.loc 8 | smtp_server 192.168.200.1 9 | smtp_connect_timeout 30 10 | router_id LVS_DEVEL 11 | } 12 | 13 | vrrp_instance VI_1 { 14 | interface eth0 15 | virtual_router_id 50 16 | nopreempt 17 | priority 100 18 | advert_int 1 19 | virtual_ipaddress { 20 | 192.168.200.11 21 | 192.168.200.12 22 | 192.168.200.13 23 | } 24 | } 25 | 26 | virtual_server 10.10.10.2 1358 { 27 | delay_loop 6 28 | lb_algo rr 29 | lb_kind NAT 30 | persistence_timeout 50 31 | protocol TCP 32 | 33 | sorry_server 192.168.200.200 1358 34 | 35 | real_server 192.168.200.2 1358 { 36 | weight 1 37 | HTTP_GET { 38 | url { 39 | path /testurl3/test.jsp 40 | digest 640205b7b0fc66c1ea91c463fac6334d 41 | } 42 | connect_timeout 3 43 | nb_get_retry 3 44 | delay_before_retry 3 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | LVS Manger - a shell to manage LVS and iptables 3 | ************ 4 | .. image:: https://secure.travis-ci.org/khosrow/lvsm.png 5 | :target: https://travis-ci.org/#!/khosrow/lvsm 6 | 7 | *lvsm* provides a command shell to manage a `Linux Virtual Server`_ (LVS) 8 | as a unified system and aims to simplify the management of such systems. 9 | The program assumes a Linux server running IPVS with `iptables`_ 10 | rules as firewall. 11 | 12 | The program can be run as a shell by simply running ``lvsm`` or invoked as a command by passing all the arguments 13 | in the command line like ``lvsm configure show director`` 14 | 15 | 16 | ===================== 17 | Further Documentation 18 | ===================== 19 | 20 | See the project `wiki`_ for detailed documentation on the configuration file and the commands to use. 21 | 22 | 23 | ======= 24 | License 25 | ======= 26 | This software is released under the `MIT license`_. 27 | 28 | .. _Linux Virtual Server: http://www.linuxvirtualserver.org/ 29 | .. _iptables: http://www.netfilter.org/projects/iptables 30 | .. _MIT license: https://github.com/khosrow/lvsm/blob/master/LICENSE.rst 31 | .. _wiki: https://github.com/khosrow/lvsm/wiki 32 | -------------------------------------------------------------------------------- /doc/lvsm.1.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | lvsm 3 | ==== 4 | 5 | ---------------------------- 6 | Linux Virtual Server Manager 7 | ---------------------------- 8 | 9 | :Author: Khosrow E. 10 | :Manual section: 1 11 | :Date: October 18, 2016 12 | :Version: 0.5.3 13 | 14 | Synopsis 15 | ======== 16 | 17 | **lvsm** [-h] [-c ][commands] 18 | 19 | Description 20 | =========== 21 | 22 | Lvsm provides a command shell to manage a Linux Virtual Server (LVS) as a unified system and aims to simplify the management of such systems. The program assumes a Linux server running IPVS with iptables rules as firewall. 23 | 24 | This program can be run as a shell by simply running lvsm or invoked as a command by passing all the arguments in the command line like: 'lvsm configure show director' 25 | 26 | Use 'lvsm help ' for information on a specific command. 27 | 28 | Options 29 | ======= 30 | 31 | -h, --help Show this help message and exit. 32 | -d, --debug Enable debug messages during runtime 33 | -c conffile, --config=conffile 34 | Specify which configuration file to use. The default is **/etc/lvsm.conf** 35 | -m, --monochrome Disable color display 36 | -n, --numeric Enable numeric host names, and avoid using DNS 37 | -v, --version Display lvsm version 38 | 39 | License 40 | ======= 41 | 42 | This software is released under the MIT license. 43 | -------------------------------------------------------------------------------- /tests/test_keepalived.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | 10 | from lvsm.modules import keepalived 11 | 12 | 13 | class Keepalived(unittest.TestCase): 14 | """Tests for the functionality of the keepalived module""" 15 | def setUp(self): 16 | args = {'keepalived-mib': 'KEEPALIVED-MIB', 17 | 'snmp_community': 'private', 18 | 'snmp_host': 'localhost', 19 | 'snmp_user': '', 20 | 'snmp_password': '', 21 | 'cache_dir': path + '/cache' 22 | } 23 | self.director = keepalived.Keepalived(path + '/scripts/ipvsadm2', 24 | path + '/etc/keepalived.conf', 25 | restart_cmd='', 26 | nodes='', 27 | args=args) 28 | 29 | def test_parseconfig1(self): 30 | # Testing parser on a valid config file 31 | configfile = path + '/etc/keepalived.conf' 32 | self.assertTrue(self.director.parse_config(configfile)) 33 | 34 | def test_parseconfig2(self): 35 | # Testing parser on an invalid config file 36 | configfile = path + '/etc/keepalived.conf-bad' 37 | self.assertFalse(self.director.parse_config(configfile)) 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://dep.debian.net/deps/dep5 2 | Upstream-Name: lvsm 3 | Source: 4 | 5 | Files: * 6 | Copyright: 2012 Khosrow Ebrahimpour 7 | License: MIT License 8 | 9 | Files: debian/* 10 | Copyright: 2014 Khosrow Ebrahimpour 11 | License: MIT License 12 | 13 | License: MIT License 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining 16 | a copy of this software and associated documentation files (the 17 | "Software"), to deal in the Software without restriction, including 18 | without limitation the rights to use, copy, modify, merge, publish, 19 | distribute, sublicense, and/or sell copies of the Software, and to 20 | permit persons to whom the Software is furnished to do so, subject to 21 | the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included 24 | in all copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 27 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 28 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 29 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 30 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 31 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 32 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | # Please also look if there are files or directories which have a 35 | # different copyright/license attached and list them here. 36 | -------------------------------------------------------------------------------- /tests/test_keepalived_disable.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | 10 | from lvsm.modules import keepalived 11 | 12 | 13 | class Keepalived(unittest.TestCase): 14 | """Tests for the functionality of the keepalived module""" 15 | def setUp(self): 16 | args = {'keepalived-mib': 'KEEPALIVED-MIB', 17 | 'snmp_community': 'private', 18 | 'snmp_host': 'localhost', 19 | 'snmp_user': '', 20 | 'snmp_password': '', 21 | 'cache_dir': path + '/cache' 22 | } 23 | self.director = keepalived.Keepalived(path + '/scripts/ipvsadm2', 24 | path + '/etc/keepalived.conf', 25 | restart_cmd='', 26 | nodes='', 27 | args=args) 28 | 29 | def tearDown(self): 30 | filepath1 = self.director.cache_dir + '/realServerWeight.2.1' 31 | filepath2 = self.director.cache_dir + '/realServerReason.2.1' 32 | os.unlink(filepath1) 33 | os.unlink(filepath2) 34 | 35 | def test_disablehost(self): 36 | output = StringIO.StringIO() 37 | sys.stdout = output 38 | self.assertTrue(self.director.disable('udp', '192.0.2.202')) 39 | 40 | def test_disablehostport(self): 41 | output = StringIO.StringIO() 42 | sys.stdout = output 43 | self.assertTrue(self.director.disable('udp', '192.0.2.202', 'domain')) 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /samples/lvsm.conf.sample: -------------------------------------------------------------------------------- 1 | # Configuration for lvsm shell 2 | # 3 | # Default values are listed below. All other configuration items are optional. 4 | 5 | ### General Section ### 6 | # 7 | # Path to ipvsadm binary 8 | #ipvsadm = ipvsadm 9 | 10 | # Path to iptables binary 11 | #iptables = iptables 12 | 13 | # Terminal paging program to use 14 | #pager = /bin/more 15 | 16 | # Cache directory used by lvsm 17 | #cache_dir = /var/cache/lvsm 18 | 19 | ### Clustering Section ### 20 | # 21 | # List nodes in the cluster, comma separated 22 | #nodes = 23 | 24 | ### Director Section ### 25 | # 26 | # Type of director to use. 27 | # Values are 'ldirectord', 'keepalived' or it can be omitted. 28 | #director = 29 | 30 | # Path to the binary of the director program. 31 | #director_bin = 32 | 33 | # Path the configuration file for the director 34 | #director_config = 35 | 36 | # Command used to reload director configuration 37 | #director_cmd = 38 | 39 | ### Firewall (iptables) Section ### 40 | # 41 | # Path to firewall configuration 42 | #firewall_config = 43 | 44 | # Command to reload firewall configuration 45 | #firewall_cmd = 46 | 47 | ### Version Control Section ### 48 | # 49 | # SCM used to manage the configuration files. 50 | # Values are 'svn', 'git' or it can be omitted. 51 | #version_control = 52 | 53 | # Remote used to perform the git pull operation 54 | #git_remote = 55 | 56 | # Local location where the git repository is 57 | #git_repo = 58 | 59 | ### SNMP Section ### 60 | # SNMP is used with keepalived to disable/enable servers 61 | # 62 | # Path to the KEEPALIVED-MIB file 63 | #keepalived-mib = KEEPALIVED-MIB 64 | #keepalived-mib = 65 | 66 | # Community name with r/w permissions 67 | #snmp_community = 68 | 69 | # Host name with r/w permissions 70 | #snmp_host = 71 | 72 | # Username if auth is required 73 | #snmp_user = 74 | 75 | # Passowrd if auth is required 76 | #snmp_password = 77 | -------------------------------------------------------------------------------- /tests/test_keepalived_enable.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | 10 | from lvsm.modules import keepalived 11 | 12 | 13 | class Keepalived(unittest.TestCase): 14 | """Tests for the functionality of the keepalived module""" 15 | def setUp(self): 16 | args = {'keepalived-mib': 'KEEPALIVED-MIB', 17 | 'snmp_community': 'private', 18 | 'snmp_host': 'localhost', 19 | 'snmp_user': '', 20 | 'snmp_password': '', 21 | 'cache_dir': path + '/cache' 22 | } 23 | self.director = keepalived.Keepalived(path + '/scripts/ipvsadm2', 24 | path + '/etc/keepalived.conf', 25 | restart_cmd='', 26 | nodes='', 27 | args=args) 28 | 29 | # create the file before we continue 30 | filepath1 = self.director.cache_dir + '/realServerWeight.2.2' 31 | filepath2 = self.director.cache_dir + '/realServerReason.2.2' 32 | f = open(filepath1, 'w') 33 | f.write('1') 34 | f.close() 35 | 36 | f = open(filepath2, 'w') 37 | f.write('test') 38 | f.close() 39 | 40 | def test_enablehost(self): 41 | output = StringIO.StringIO() 42 | sys.stdout = output 43 | 44 | self.assertTrue(self.director.enable('udp', '192.0.2.203')) 45 | 46 | def test_enablehostport(self): 47 | output = StringIO.StringIO() 48 | sys.stdout = output 49 | 50 | self.assertTrue(self.director.enable('udp', '192.0.2.203', 'domain')) 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /tests/test_lvs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | 10 | from lvsm import lvs 11 | 12 | 13 | class GenericDirector(unittest.TestCase): 14 | """Testing the generic director funcationality""" 15 | def setUp(self): 16 | # for now only testing ldirectord 17 | self.director = lvs.Director('generic', path + '/scripts/ipvsadm') 18 | self.maxDiff = None 19 | 20 | def test_convertfilename(self): 21 | filename = 'localhost:http' 22 | expected_result = '127.0.0.1:80' 23 | self.assertEqual(self.director.convert_filename(filename), 24 | expected_result) 25 | 26 | def test_show(self): 27 | expected_result = ["", 28 | "Layer 4 Load balancing", 29 | "======================", 30 | "TCP 192.0.2.2:80 rr ", 31 | " -> 192.0.2.200:80 Masq 1 0 0 ", 32 | "", 33 | "UDP 192.0.2.2:53 rr ", 34 | " -> 192.0.2.202:53 Masq 1 0 0 ", 35 | " -> 192.0.2.203:53 Masq 1 0 0 ", 36 | "", 37 | "FWM 1 rr ", 38 | " -> 192.0.2.204:0 Masq 1 0 0 ", 39 | "", 40 | ""] 41 | result = self.director.show(True, False) 42 | self.assertEqual(result, expected_result) 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /tests/test_keepalived2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | 10 | from lvsm.modules import keepalived 11 | 12 | 13 | class Keepalived(unittest.TestCase): 14 | """Tests for the functionality of the keepalived module""" 15 | def setUp(self): 16 | args = {'keepalived-mib': 'KEEPALIVED-MIB', 17 | 'snmp_community': 'private', 18 | 'snmp_host': 'localhost', 19 | 'snmp_user': '', 20 | 'snmp_password': '', 21 | 'cache_dir': path + '/cache' 22 | } 23 | self.director = keepalived.Keepalived(path + '/scripts/ipvsadm3', 24 | path + '/etc/keepalived.conf', 25 | restart_cmd='', 26 | nodes='', 27 | args=args) 28 | 29 | def test_show(self): 30 | self.maxDiff = None 31 | # Testing show on non-standard ports 32 | expected_result = ['', 33 | 'Layer 4 Load balancing', 34 | '======================', 35 | 'TCP 192.0.2.2:8888 rr ', 36 | ' -> 192.0.2.200:8888 Masq 1 0 0 ', 37 | ' -> 192.0.2.201:8888 Masq 1 0 0 ', 38 | '', 39 | 'UDP 192.0.2.2:domain rr ', 40 | ' -> 192.0.2.202:domain Masq 1 0 0 ', 41 | ' -> 192.0.2.203:domain Masq 1 0 0 ', 42 | '', 43 | ''] 44 | self.assertEqual(self.director.show(numeric=False, color=False), expected_result) 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /doc/lvsm.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructeredText. 2 | . 3 | .TH LVSM 1 "October 18, 2016" "0.5.3" "" 4 | .SH NAME 5 | lvsm \- Linux Virtual Server Manager 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBlvsm\fP [\-h] [\-c ][commands] 36 | .SH DESCRIPTION 37 | .sp 38 | Lvsm provides a command shell to manage a Linux Virtual Server (LVS) as a unified system and aims to simplify the management of such systems. The program assumes a Linux server running IPVS with iptables rules as firewall. 39 | .sp 40 | This program can be run as a shell by simply running lvsm or invoked as a command by passing all the arguments in the command line like: \(aqlvsm configure show director\(aq 41 | .sp 42 | Use \(aqlvsm help \(aq for information on a specific command. 43 | .SH OPTIONS 44 | .INDENT 0.0 45 | .INDENT 3.5 46 | .INDENT 0.0 47 | .TP 48 | .B \-h, \-\-help 49 | Show this help message and exit. 50 | .TP 51 | .B \-d, \-\-debug 52 | Enable debug messages during runtime 53 | .TP 54 | .BI \-c \ conffile, \ \-\-config\fB= conffile 55 | Specify which configuration file to use. The default is \fB/etc/lvsm.conf\fP 56 | .TP 57 | .B \-m, \-\-monochrome 58 | Disable color display 59 | .TP 60 | .B \-n, \-\-numeric 61 | Enable numeric host names, and avoid using DNS 62 | .TP 63 | .B \-v, \-\-version 64 | Display lvsm version 65 | .UNINDENT 66 | .UNINDENT 67 | .UNINDENT 68 | .SH LICENSE 69 | .sp 70 | This software is released under the MIT license. 71 | .SH AUTHOR 72 | Khosrow E. 73 | .\" Generated by docutils manpage writer. 74 | .\" 75 | . 76 | -------------------------------------------------------------------------------- /tests/test_keepalived_show_real.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | 10 | from lvsm.modules import keepalived 11 | 12 | 13 | class Keepalived(unittest.TestCase): 14 | """Tests for the functionality of the keepalived module""" 15 | def setUp(self): 16 | args = {'keepalived-mib': 'KEEPALIVED-MIB', 17 | 'snmp_community': 'private', 18 | 'snmp_host': 'localhost', 19 | 'snmp_user': '', 20 | 'snmp_password': '', 21 | 'cache_dir': path + '/cache' 22 | } 23 | self.director = keepalived.Keepalived(path + '/scripts/ipvsadm2', 24 | path + '/etc/keepalived.conf', 25 | restart_cmd='', 26 | nodes='', 27 | args=args) 28 | def teaDown(self): 29 | filepath1 = self.director.cache_dir + '/realServerWeight.1.1' 30 | filepath2 = self.director.cache_dir + '/realServerReason.1.1' 31 | try: 32 | os.unlink(filepath1) 33 | os.unlink(filepath2) 34 | except OSError: 35 | pass 36 | 37 | def test_show_real_disabled1(self): 38 | # Test show_real_disabled when there's no output 39 | expected_result = [] 40 | result = self.director.show_real_disabled('','',numeric=True) 41 | self.assertEqual(result, expected_result) 42 | 43 | def test_show_real_disabled2(self): 44 | # test when there's at least one host disabled 45 | output = StringIO.StringIO() 46 | sys.stdout = output 47 | 48 | # create a dummy file to trigger the host to be disabled 49 | filepath1 = self.director.cache_dir + '/realServerWeight.1.1' 50 | filepath2 = self.director.cache_dir + '/realServerReason.1.1' 51 | try: 52 | # create the file before we continue 53 | f = open(filepath1, 'w') 54 | f.write('1') 55 | f.close() 56 | 57 | f = open(filepath2, 'w') 58 | f.write('test') 59 | f.close() 60 | 61 | expected_result = ['192.0.2.200:80\t\tReason: test'] 62 | result = self.director.show_real_disabled('','',numeric=True) 63 | self.assertEqual(result, expected_result) 64 | except IOError: 65 | pass 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /tests/scripts/ipvsadm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import getopt 5 | import os 6 | 7 | header=['IP Virtual Server version 1.2.1 (size=4096)', 8 | 'Prot LocalAddress:Port Scheduler Flags', 9 | ' -> RemoteAddress:Port Forward Weight ActiveConn InActConn'] 10 | 11 | numeric_tcp=['TCP 192.0.2.2:80 rr', 12 | ' -> 192.0.2.200:80 Masq 1 0 0', 13 | ' -> 192.0.2.201:80 Masq 1 0 0'] 14 | 15 | numeric_udp=['UDP 192.0.2.2:53 rr', 16 | ' -> 192.0.2.202:53 Masq 1 0 0', 17 | ' -> 192.0.2.203:53 Masq 1 0 0'] 18 | 19 | numeric_fwm=['FWM 1 rr', 20 | ' -> 192.0.2.204:0 Masq 1 0 0'] 21 | 22 | def main(): 23 | try: 24 | opts, args = getopt.getopt(sys.argv[1:], "Lnt:u:f:", 25 | ["list"]) 26 | except getopt.error, msg: 27 | print msg 28 | sys.exit(1) 29 | 30 | listing = True 31 | tcp = False 32 | udp = False 33 | fwm = False 34 | numeric = False 35 | 36 | for opt, arg in opts: 37 | if opt in ("-L", "--list"): 38 | listing = True 39 | elif opt == "-t": 40 | if arg == "192.0.2.2:80": 41 | tcp = True 42 | elif opt == "-u": 43 | if arg == "192.0.2.2:53": 44 | udp = True 45 | elif opt == "-f": 46 | if ags == "1": 47 | fwm = True 48 | elif opt == "-n": 49 | numeric = True 50 | 51 | # This section is for ldirectord 52 | # check if a file for a real server is in the maintenance dir 53 | # If yes, disable that RIP 54 | path = os.path.abspath(os.path.dirname(__file__)) 55 | maintenance = path + "/../maintenance" 56 | filenames = os.listdir(maintenance) 57 | 58 | for index, item in enumerate(numeric_tcp): 59 | for filename in filenames: 60 | if filename in item: 61 | del numeric_tcp[index] 62 | 63 | for index, item in enumerate(numeric_udp): 64 | for filename in filenames: 65 | if filename in item: 66 | del numeric_udp[index] 67 | 68 | # Now display the result 69 | if tcp: 70 | if numeric: 71 | full_list = numeric_tcp 72 | elif udp: 73 | if numeric: 74 | full_list = numeric_udp 75 | elif fwm: 76 | if numeric: 77 | full_list = numeric_fwm 78 | else: 79 | if numeric: 80 | full_list = numeric_tcp + numeric_udp + numeric_fwm 81 | 82 | for item in header: 83 | print item 84 | for item in full_list: 85 | print item 86 | 87 | if __name__ == '__main__': 88 | main() 89 | -------------------------------------------------------------------------------- /tests/test_firewall.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | from lvsm import firewall 10 | 11 | 12 | class FirewallTestCase(unittest.TestCase): 13 | """Testing firewall module""" 14 | def setUp(self): 15 | self.firewall = firewall.Firewall(path + '/scripts/iptables') 16 | 17 | def test_show(self): 18 | output = StringIO.StringIO() 19 | sys.stdout = output 20 | expected_result = """ 21 | IP Packet filter rules 22 | ====================== 23 | Chain INPUT (policy ACCEPT) 24 | target prot opt source destination 25 | ACCEPT tcp -- anywhere 192.0.2.2 tcp dpt:80 26 | 27 | Chain FORWARD (policy ACCEPT) 28 | target prot opt source destination 29 | 30 | Chain OUTPUT (policy ACCEPT) 31 | target prot opt source destination""" 32 | lines = self.firewall.show(numeric=False, color=False) 33 | result = '' 34 | for line in lines: 35 | result = result + line + '\n' 36 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 37 | 38 | def test_showvirtual(self): 39 | output = StringIO.StringIO() 40 | sys.stdout = output 41 | expected_result = """ 42 | IP Packet filter rules 43 | ====================== 44 | ACCEPT tcp -- anywhere\ 45 | 192.0.2.2 tcp dpt:80""" 46 | lines = self.firewall.show_virtual('192.0.2.2', 'http', 'tcp', 47 | numeric=False, color=False) 48 | result = '' 49 | for line in lines: 50 | result = result + line + '\n' 51 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 52 | 53 | def test_shownat(self): 54 | output = StringIO.StringIO() 55 | sys.stdout = output 56 | expected_result = """ 57 | NAT rules 58 | ========= 59 | Chain PREROUTING (policy ACCEPT) 60 | target prot opt source destination 61 | 62 | Chain INPUT (policy ACCEPT) 63 | target prot opt source destination 64 | 65 | Chain OUTPUT (policy ACCEPT) 66 | target prot opt source destination 67 | 68 | Chain POSTROUTING (policy ACCEPT) 69 | target prot opt source destination""" 70 | 71 | lines = self.firewall.show_nat(numeric=False) 72 | 73 | result = '' 74 | for line in lines: 75 | result = result + line + '\n' 76 | 77 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 78 | 79 | 80 | if __name__ == "__main__": 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======== 3 | 4 | 0.5.1 (2014-11-18) 5 | ------------------ 6 | 7 | * Finalizing release 8 | * Added some features 9 | 10 | 0.5.1 (2014-02-23) 11 | ------------------ 12 | 13 | * Lots of bug fixes 14 | * Added ability to handle templating of configs 15 | 16 | 0.5.0 (2014-01-29) 17 | ------------------ 18 | 19 | * Complete redo of the syntax 20 | * Added support for keepalived 21 | * Added support for git (limited) 22 | 23 | 0.4.1 (2013-06-21) 24 | ------------------ 25 | 26 | * Added use of logging module 27 | * Fixed bug where non-standard port number would crash the app 28 | 29 | 0.4.0 (2013-04-11) 30 | ------------------ 31 | 32 | * Added ldirectord config parsing 33 | * Fixed logic errors in "config edit" 34 | * Fixed bug with genericdirector 35 | * Updated test cases 36 | 37 | 0.3.5 (2013-04-04) 38 | ------------------ 39 | 40 | * Fixed regression bug with director.show 41 | * Fixed logic error in director.enable 42 | * PEP8-ified 43 | 44 | 0.3.4 (2013-02-17) 45 | ------------------ 46 | 47 | * Fixed minor issue with enable 48 | * Fixed minor issue with firewall.show_virtual 49 | * added config item to define what scm to use 50 | * director.show now displays disabled hosts 51 | 52 | 0.3.3 (2013-02-05) 53 | ------------------ 54 | 55 | * Fixed pagination 56 | * Added colour support to ipvsadm and iptables output 57 | * enable/disable commands now ensure the RIP is enabled/disabled 58 | * enable/disable now work across a cluster (if defined) 59 | * General bug fixes 60 | 61 | 0.3.2 (2013-01-13) 62 | ------------------ 63 | 64 | * Fixed bug when quitting and no files are modified 65 | * Added pagination support for long output 66 | * Added new "status show nat" command 67 | * refactored Director classes to use a factory pattern 68 | * Added stub Keepalived class 69 | 70 | 71 | 0.3.1 (2013-01-09) 72 | ------------------ 73 | 74 | * Added color term support 75 | * Added verification of modified configs 76 | * refactored CommandPrompt and Director classes 77 | * Fixed typos 78 | 79 | 80 | 0.3.0 (2012-11-06) 81 | ------------------ 82 | 83 | * Added ability to restart firewall and director 84 | * Added ``--version`` flag 85 | * Fixed bug with unhelpful ipvsadm error message 86 | * Fixed bug in firewall module's ``show()`` 87 | 88 | 89 | 0.2.1 (2012-10-22) 90 | ------------------ 91 | 92 | * Bug fixes in Director class 93 | 94 | 0.2.0 (2012-10-21) 95 | ------------------ 96 | 97 | * Bug fixes 98 | * Disabling real servers now prompts user for a comment 99 | * ``show virtual`` now displays firewall info for VIP 100 | 101 | 0.1.1 (2012-10-17) 102 | ------------------ 103 | 104 | * Bug fixes 105 | 106 | 0.1.0 (2012-10-16) 107 | ------------------ 108 | 109 | * Initial relase 110 | -------------------------------------------------------------------------------- /lvsm/snimpy_dummy/manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy module that simulates snimpy functionality on Keepalived 3 | for unit testing 4 | """ 5 | # The following vip/rip are hardcoded into the dummy module 6 | # TCP 82.94.164.162:80 7 | # -> 216.34.181.45:80 8 | # -> 173.194.43.3:80 9 | # UDP 82.94.164.162:53 10 | # -> 208.67.222.222:53 11 | # -> 208.67.220.220:53 12 | 13 | # TCP 192.0.2.2:80 14 | # -> 192.0.2.200:80 15 | # -> 192.0.2.201:80 16 | # UDP 192.0.2.2:53 17 | # -> 192.0.2.202:53 18 | # -> 192.0.2.203:53 19 | 20 | import socket 21 | 22 | 23 | class TupleArray: 24 | """A 2-D dictionary object""" 25 | def __init__(self): 26 | self.data = dict() 27 | 28 | def __setitem__(self, tup, value): 29 | i, j = tup 30 | try: 31 | self.data[int(i)][j] = value 32 | except KeyError: 33 | self.data[int(i)] = dict() 34 | self.data[int(i)][j] = value 35 | 36 | 37 | def __getitem__(self, tup): 38 | i, j = tup 39 | return self.data[int(i)][j] 40 | 41 | 42 | class Manager(object): 43 | def __init__(self, 44 | host='localhost', 45 | community='public', 46 | version=2, 47 | cache=False, 48 | none=False, 49 | timeout=None, 50 | retries=None, 51 | secname=None, 52 | authprotocol=None, 53 | authpassword=None, 54 | privprotocol=None, 55 | privpassword=None): 56 | """dummy init method""" 57 | self.virtualServerAddress = {'1': socket.inet_aton('192.0.2.2'), 58 | '2': socket.inet_aton('192.0.2.2')} 59 | self.virtualServerRealServersTotal = {'1': 2, '2': 2} 60 | self.virtualServerPort = {'1': 80, '2': 53} 61 | 62 | self.virtualServerProtocol = {'1': 'tcp', '2': 'udp'} 63 | 64 | self.realServerAddress = TupleArray() 65 | self.realServerAddress[1,1] = socket.inet_aton('192.0.2.200') 66 | self.realServerAddress[1,2] = socket.inet_aton('192.0.2.201') 67 | self.realServerAddress[2,1] = socket.inet_aton('192.0.2.202') 68 | self.realServerAddress[2,2] = socket.inet_aton('192.0.2.203') 69 | 70 | self.realServerPort = TupleArray() 71 | self.realServerPort[1, 1] = 80 72 | self.realServerPort[1, 2] = 80 73 | self.realServerPort[2, 1] = 53 74 | self.realServerPort[2, 2] = 53 75 | 76 | self.realServerWeight = TupleArray() 77 | self.realServerWeight[1, 1] = 1 78 | self.realServerWeight[1, 2] = 1 79 | self.realServerWeight[2, 1] = 1 80 | # Hardcoding zero to avoid warnings when running test_disablehost methods 81 | self.realServerWeight[2, 2] = 0 82 | 83 | 84 | def load(mibname): 85 | """Dummy load method""" 86 | pass 87 | -------------------------------------------------------------------------------- /tests/test_ldirectord.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../lvsm'))) 8 | 9 | 10 | from lvsm.modules import ldirectord 11 | 12 | 13 | class Ldirectord(unittest.TestCase): 14 | """Tests functionality of the ldirectord module""" 15 | def setUp(self): 16 | # for now only testing ldirectord 17 | self.director = ldirectord.Ldirectord(path + '/scripts/ipvsadm', 18 | path + '/etc/ldirectord.conf') 19 | 20 | def test_disablehost(self): 21 | output = StringIO.StringIO() 22 | sys.stdout = output 23 | filepath = self.director.maintenance_dir + '/208.67.222.222' 24 | self.assertTrue(self.director.disable('resolver1.opendns.com')) 25 | # now clean up the file 26 | try: 27 | os.unlink(filepath) 28 | except OSError as e: 29 | pass 30 | 31 | def test_disablehostport(self): 32 | output = StringIO.StringIO() 33 | sys.stdout = output 34 | filepath = self.director.maintenance_dir + '/208.67.222.222:53' 35 | self.assertTrue(self.director.disable('resolver1.opendns.com', 'domain')) 36 | # now clean up the file 37 | try: 38 | os.unlink(filepath) 39 | except OSError as e: 40 | pass 41 | 42 | def test_enablehost(self): 43 | output = StringIO.StringIO() 44 | sys.stdout = output 45 | filepath = self.director.maintenance_dir + '/208.67.222.222' 46 | try: 47 | # create the file before we continue 48 | f = open(filepath, 'w') 49 | f.close() 50 | self.assertTrue(self.director.enable('resolver1.opendns.com')) 51 | except IOError as e: 52 | pass 53 | 54 | def test_enablehostport(self): 55 | output = StringIO.StringIO() 56 | sys.stdout = output 57 | filepath = self.director.maintenance_dir + '/208.67.222.222:53' 58 | try: 59 | # create the file before we continue 60 | f = open(filepath, 'w') 61 | f.close() 62 | self.assertTrue(self.director.enable('resolver1.opendns.com', 'domain')) 63 | except IOError as e: 64 | pass 65 | 66 | def test_enablehostname(self): 67 | output = StringIO.StringIO() 68 | sys.stdout = output 69 | filepath = self.director.maintenance_dir + '/slashdot.org' 70 | try: 71 | # create the file before we continue 72 | f = open(filepath, 'w') 73 | f.close() 74 | self.assertTrue(self.director.enable('slashdot.org', '')) 75 | except IOError as e: 76 | pass 77 | 78 | def test_parseconfig1(self): 79 | # Test parser on a valid config file 80 | configfile = path + '/etc/ldirectord.conf-1' 81 | self.assertTrue(self.director.parse_config(configfile)) 82 | 83 | def test_parseconfig2(self): 84 | # Test parser on an invalid config file 85 | self.assertFalse(False) 86 | 87 | 88 | if __name__ == "__main__": 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /lvsm/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Khosrow Ebrahimpour - Sep 2012 3 | 4 | """ 5 | lvsm - LVS Manager 6 | LVS Manager is a shell that eases the management of a linux virtual server. 7 | 8 | Using it without arguments will enter an interactive shell. Supplying one or 9 | more command-line arguments will run lvsm for a "single-shot" use. 10 | 11 | Usage: lvsm [-h] [-c ][commands] 12 | 13 | Options: 14 | -h, --help Show this help message and exit 15 | -c , -config= 16 | Specify which configuration file to use 17 | The default is /etc/lvsm/lvsm.conf 18 | -d, --debug Enable debug messages during runtime 19 | -m, --monochrome Disable color display 20 | -n, --numeric Enable numeric host names, and avoid using DNS 21 | -v, --version Display lvsm version 22 | """ 23 | 24 | import getopt 25 | import sys 26 | import __init__ as appinfo 27 | import logging 28 | import os 29 | 30 | from lvsm import utils 31 | from lvsm import shell 32 | 33 | logging.basicConfig(format='[%(levelname)s]: %(message)s') 34 | logger = logging.getLogger('lvsm') 35 | 36 | def usage(code, msg=''): 37 | if code: 38 | fd = sys.stderr 39 | else: 40 | fd = sys.stdout 41 | print >> fd, __doc__ 42 | 43 | config = utils.parse_config(None) 44 | lvsshell = shell.LivePrompt(config) 45 | lvsshell.onecmd(' '.join(["help"])) 46 | print >> fd, "Use 'lvsm help ' for information on a specific command." 47 | 48 | if msg: 49 | print >> fd, msg 50 | sys.exit(code) 51 | 52 | 53 | def main(): 54 | CONFFILE = "/etc/lvsm/lvsm.conf" 55 | 56 | try: 57 | opts, args = getopt.getopt(sys.argv[1:], "hvc:dmn", 58 | ["help", "version", "config=", "debug", "monochrome", "numeric"]) 59 | except getopt.error, msg: 60 | usage(2, msg) 61 | 62 | color = True 63 | numeric = False 64 | for opt, arg in opts: 65 | if opt in ("-h", "--help"): 66 | usage(0) 67 | elif opt in ("-v", "--version"): 68 | print "lvsm " + appinfo.__version__ 69 | sys.exit(0) 70 | elif opt in ("-c", "--config"): 71 | CONFFILE = arg 72 | elif opt in ("-d", "--debug"): 73 | logger.setLevel(logging.DEBUG) 74 | elif opt in ("-m", "--monochrome"): 75 | color = False 76 | elif opt in ("-n", "--numeric"): 77 | numeric = True 78 | 79 | # open config file and read it 80 | # config = utils.parse_config(CONFFILE) 81 | config = utils.parse_config(os.path.expanduser(os.path.expandvars(CONFFILE))) 82 | logger.debug('Parsed config file') 83 | logger.debug(str(config)) 84 | 85 | try: 86 | lvsshell = shell.LivePrompt(config) 87 | 88 | if not color: 89 | lvsshell.do_set("color off") 90 | if numeric: 91 | lvsshell.do_set("numeric on") 92 | 93 | if args: 94 | lvsshell.onecmd(' '.join(args[:])) 95 | else: 96 | lvsshell.cmdloop() 97 | except KeyboardInterrupt: 98 | print "\nleaving abruptly!" 99 | sys.exit(1) 100 | 101 | if __name__ == "__main__": 102 | main() 103 | -------------------------------------------------------------------------------- /tests/scripts/ipvsadm2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import getopt 5 | import os 6 | 7 | header=['IP Virtual Server version 1.2.1 (size=4096)', 8 | 'Prot LocalAddress:Port Scheduler Flags', 9 | ' -> RemoteAddress:Port Forward Weight ActiveConn InActConn'] 10 | 11 | alpha_tcp=['TCP 192.0.2.2:http rr', 12 | ' -> 192.0.2.200:http Masq 1 0 0', 13 | ' -> 192.0.2.201:http Masq 1 0 0'] 14 | 15 | alpha_udp=['UDP 192.0.2.2:domain rr', 16 | ' -> 192.0.2.202:domain Masq 1 0 0', 17 | ' -> 192.0.2.203:domain Masq 1 0 0'] 18 | 19 | numeric_tcp=['TCP 192.0.2.2:80 rr', 20 | ' -> 192.0.2.200:80 Masq 1 0 0', 21 | ' -> 192.0.2.201:80 Masq 1 0 0'] 22 | 23 | numeric_udp=['UDP 192.0.2.2:53 rr', 24 | ' -> 192.0.2.202:53 Masq 1 0 0', 25 | ' -> 192.0.2.203:53 Masq 1 0 0'] 26 | 27 | def main(): 28 | try: 29 | opts, args = getopt.getopt(sys.argv[1:], "Lnt:u:f:", 30 | ["list"]) 31 | except getopt.error, msg: 32 | print msg 33 | sys.exit(1) 34 | 35 | listing = True 36 | tcp = False 37 | udp = False 38 | fwm = False 39 | numeric = False 40 | 41 | for opt, arg in opts: 42 | if opt in ("-L", "--list"): 43 | listing = True 44 | elif opt == "-t": 45 | if (arg == "192.0.2.2:80" or arg == "192.0.2.2:http"): 46 | tcp = True 47 | elif opt == "-u": 48 | if (arg == "192.0.2.2:53" or arg == "192.0.2.2:domain"): 49 | udp = True 50 | elif opt == "-f": 51 | fwm = True 52 | elif opt == "-n": 53 | numeric = True 54 | 55 | path = os.path.abspath(os.path.dirname(__file__)) 56 | maintenance = path + "/../cache" 57 | filenames = os.listdir(maintenance) 58 | 59 | for filename in filenames: 60 | parts = filename.split('.') 61 | if parts[0] == "realServerWeight": 62 | i = int(parts[1]) 63 | j = int(parts[2]) 64 | num = j 65 | 66 | if i == 1: 67 | old = numeric_tcp[num] 68 | new = old.replace("Masq 1", "Masq 0") 69 | numeric_tcp[num] = new 70 | 71 | old = alpha_tcp[num] 72 | new = old.replace("Masq 1", "Masq 0") 73 | alpha_tcp[num] = new 74 | 75 | elif i == 2: 76 | old = numeric_udp[num] 77 | new = old.replace("Masq 1", "Masq 0") 78 | numeric_udp[num] = new 79 | 80 | old = alpha_udp[num] 81 | new = old.replace("Masq 1", "Masq 0") 82 | alpha_udp[num] = new 83 | 84 | if tcp: 85 | if numeric: 86 | full_list = numeric_tcp 87 | else: 88 | full_list = alpha_tcp 89 | elif udp: 90 | if numeric: 91 | full_list = numeric_udp 92 | else: 93 | full_list = alpha_udp 94 | else: 95 | if numeric: 96 | full_list = numeric_tcp + numeric_udp 97 | else: 98 | full_list = alpha_tcp + alpha_udp 99 | 100 | for item in header: 101 | print item 102 | for item in full_list: 103 | print item 104 | 105 | if __name__ == '__main__': 106 | main() 107 | -------------------------------------------------------------------------------- /tests/scripts/ipvsadm3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import getopt 5 | import os 6 | 7 | header=['IP Virtual Server version 1.2.1 (size=4096)', 8 | 'Prot LocalAddress:Port Scheduler Flags', 9 | ' -> RemoteAddress:Port Forward Weight ActiveConn InActConn'] 10 | 11 | alpha_tcp=['TCP 192.0.2.2:8888 rr', 12 | ' -> 192.0.2.200:8888 Masq 1 0 0', 13 | ' -> 192.0.2.201:8888 Masq 1 0 0'] 14 | 15 | alpha_udp=['UDP 192.0.2.2:domain rr', 16 | ' -> 192.0.2.202:domain Masq 1 0 0', 17 | ' -> 192.0.2.203:domain Masq 1 0 0'] 18 | 19 | numeric_tcp=['TCP 192.0.2.2:8888 rr', 20 | ' -> 192.0.2.200:8888 Masq 1 0 0', 21 | ' -> 192.0.2.201:8888 Masq 1 0 0'] 22 | 23 | numeric_udp=['UDP 192.0.2.2:53 rr', 24 | ' -> 192.0.2.202:53 Masq 1 0 0', 25 | ' -> 192.0.2.203:53 Masq 1 0 0'] 26 | 27 | def main(): 28 | try: 29 | opts, args = getopt.getopt(sys.argv[1:], "Lnt:u:f:", 30 | ["list"]) 31 | except getopt.error, msg: 32 | print msg 33 | sys.exit(1) 34 | 35 | listing = True 36 | tcp = False 37 | udp = False 38 | fwm = False 39 | numeric = False 40 | 41 | for opt, arg in opts: 42 | if opt in ("-L", "--list"): 43 | listing = True 44 | elif opt == "-t": 45 | if (arg == "192.0.2.2:80" or arg == "192.0.2.2:8888"): 46 | tcp = True 47 | elif opt == "-u": 48 | if (arg == "192.0.2.2:53" or arg == "192.0.2.2:domain"): 49 | udp = True 50 | elif opt == "-f": 51 | fwm = True 52 | elif opt == "-n": 53 | numeric = True 54 | 55 | path = os.path.abspath(os.path.dirname(__file__)) 56 | maintenance = path + "/../cache" 57 | filenames = os.listdir(maintenance) 58 | 59 | for filename in filenames: 60 | parts = filename.split('.') 61 | if parts[0] == "realServerWeight": 62 | i = int(parts[1]) 63 | j = int(parts[2]) 64 | num = j 65 | 66 | if i == 1: 67 | old = numeric_tcp[num] 68 | new = old.replace("Masq 1", "Masq 0") 69 | numeric_tcp[num] = new 70 | 71 | old = alpha_tcp[num] 72 | new = old.replace("Masq 1", "Masq 0") 73 | alpha_tcp[num] = new 74 | 75 | elif i == 2: 76 | old = numeric_udp[num] 77 | new = old.replace("Masq 1", "Masq 0") 78 | numeric_udp[num] = new 79 | 80 | old = alpha_udp[num] 81 | new = old.replace("Masq 1", "Masq 0") 82 | alpha_udp[num] = new 83 | 84 | if tcp: 85 | if numeric: 86 | full_list = numeric_tcp 87 | else: 88 | full_list = alpha_tcp 89 | elif udp: 90 | if numeric: 91 | full_list = numeric_udp 92 | else: 93 | full_list = alpha_udp 94 | else: 95 | if numeric: 96 | full_list = numeric_tcp + numeric_udp 97 | else: 98 | full_list = alpha_tcp + alpha_udp 99 | 100 | for item in header: 101 | print item 102 | for item in full_list: 103 | print item 104 | 105 | if __name__ == '__main__': 106 | main() 107 | -------------------------------------------------------------------------------- /lvsm/modules/parseactions.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from pyparsing import * 3 | 4 | 5 | def validate_ip4(s, loc, tokens): 6 | """Helper function to validate IPv4 addresses""" 7 | try: 8 | socket.inet_pton(socket.AF_INET, tokens[0]) 9 | except socket.error: 10 | errmsg = "invalid IPv4 address." 11 | raise ParseFatalException(s, loc, errmsg) 12 | 13 | return tokens 14 | 15 | def validate_ip6(s, loc, tokens): 16 | """Helper function to validate IPv6 adddresses""" 17 | try: 18 | socket.ineet_pton(socket.AF_INET6, tokens[0]) 19 | except socket.error: 20 | errmsg = "invalid IPv6 address." 21 | raise ParseFatalException(s, loc, errmsg) 22 | 23 | return tokens 24 | 25 | def validate_port(s, loc, tokens): 26 | """Helper function that verifies we have a valid port number""" 27 | # port = tokens[1] 28 | port = tokens[0] 29 | if int(port) < 65535 and int(port) > 0: 30 | return tokens 31 | else: 32 | errmsg = "Invalid port number!" 33 | raise ParseFatalException(s, loc, errmsg) 34 | 35 | def validate_scheduler(s, loc, tokens): 36 | schedulers = ['rr', 'wrr', 'lc', 'wlc', 'lblc', 'lblcr', 'dh', 'sh', 'sed', 'nq'] 37 | 38 | if tokens[0][1] in schedulers: 39 | return tokens 40 | else: 41 | errmsg = "Invalid scheduler type!" 42 | raise ParseFatalException(s, loc, errmsg) 43 | 44 | def validate_checktype(s, loc, tokens): 45 | checktypes = ["connect", "negotiate", "ping", "off", "on", "external", "external-perl"] 46 | if ((tokens[0][1] in checktypes) or (tokens[0][1].isdigit() and int(tokens[0][1]) > 0)): 47 | return tokens 48 | else: 49 | errmsg = "Invalid checktype!" 50 | raise ParseFatalException(s, loc, errmsg) 51 | 52 | def validate_int(s, loc, tokens): 53 | """Validate that the token is an integer""" 54 | try: 55 | int(tokens[0]) 56 | except ValueError: 57 | errmsg = "Value must be an integer!" 58 | raise ParseFatalException(s, loc, errmsg) 59 | 60 | def validate_protocol(s, loc, tokens): 61 | protocols = ['fwm', 'udp', 'tcp'] 62 | if tokens[0][1] in protocols: 63 | return tokens 64 | else: 65 | errmsg = "Invalid protocol!" 66 | raise ParseFatalException(s, loc, errmsg) 67 | 68 | def validate_service(s, loc, tokens): 69 | services = ["dns", "ftp", "http", "https", "http_proxy", "imap", "imaps" 70 | ,"ldap", "nntp", "mysql", "none", "oracle", "pop" , "pops" 71 | , "radius", "pgsql" , "sip" , "smtp", "submission", "simpletcp"] 72 | if tokens[0][1] in services: 73 | return tokens 74 | else: 75 | errmsg = "Invalid service type!" 76 | raise ParseFatalException(s, loc, errmsg) 77 | 78 | def validate_yesno(s, loc, tokens): 79 | # if tokens[0] == "yes" or tokens[0] == "no": 80 | if tokens[0] in ['yes', 'no']: 81 | return tokens 82 | else: 83 | errmsg = "Value must be 'yes' or 'no'" 84 | raise ParseFatalException(s, loc, errmsg) 85 | 86 | def validate_httpmethod(s, loc, tokens): 87 | if tokens[0][1] in ['GET', 'HEAD']: 88 | return tokens 89 | else: 90 | errmsg = "Value must be 'GET' or 'HEAD'" 91 | raise ParseFatalException(s, loc, errmsg) 92 | 93 | def validate_lbmethod(s, loc, tokens): 94 | """Validate the load balancing method used for real servers""" 95 | methods = ['gate', 'masq', 'ipip'] 96 | if tokens[0] in methods: 97 | return tokens 98 | else: 99 | errmsg = "Loadbalancing method must be one of %s " % ', '.join(methods) 100 | raise ParseFatalException(s, loc, errmsg) -------------------------------------------------------------------------------- /lvsm/utils.py: -------------------------------------------------------------------------------- 1 | """Common utility functions used by lvsm""" 2 | import socket 3 | import subprocess 4 | import sys 5 | import logging 6 | 7 | logger = logging.getLogger('lvsm') 8 | 9 | def parse_config(filename): 10 | #open config file and read it 11 | if filename: 12 | lines = print_file(filename) 13 | else: 14 | lines = list() 15 | # list of valid config keys and their default values 16 | config_items = {'ipvsadm': 'ipvsadm', 17 | 'iptables': 'iptables', 18 | 'pager': '/bin/more', 19 | 'cache_dir': '/var/cache/lvsm', 20 | 'template_lang': '', 21 | 'director_config': '', 22 | 'parse_director_config': 'yes', 23 | 'director': 'generic', 24 | 'director_cmd': '', 25 | 'director_bin': '', 26 | 'firewall_config': '', 27 | 'firewall_cmd': '', 28 | 'nodes': '', 29 | 'version_control': '', 30 | 'git_remote': '', 31 | 'git_branch': 'master', 32 | 'keepalived-mib': 'KEEPALIVED-MIB', 33 | 'snmp_community': '', 34 | 'snmp_host': '', 35 | 'snmp_user': '', 36 | 'snmp_password': '' 37 | } 38 | linenum = 0 39 | for line in lines: 40 | linenum += 1 41 | conf, sep, comment = line.rstrip().partition('#') 42 | if conf: 43 | k, sep, v = conf.rstrip().partition('=') 44 | key = k.lstrip().rstrip() 45 | value = v.lstrip().rstrip() 46 | if config_items.get(key) is None: 47 | logger.error("configuration file line %d: invalid variable '%s'" % (linenum, key)) 48 | sys.exit(1) 49 | else: 50 | config_items[key] = value 51 | # if the item is a config file, verify that the file exists 52 | if key in ['director_config', 'firewall_config']: 53 | try: 54 | file = open(value) 55 | file.close() 56 | except IOError as e: 57 | logger.error("in lvsm configuration file line %d" % linenum) 58 | logger.error(e) 59 | sys.exit(1) 60 | return config_items 61 | 62 | 63 | def print_file(filename): 64 | """opens a file and returns its contents as list""" 65 | lines = list() 66 | try: 67 | f = open(filename) 68 | #lines = f.readlines() 69 | for line in f: 70 | lines.append(line.rstrip('\n')) 71 | f.close() 72 | except IOError as e: 73 | logger.error(e) 74 | return lines 75 | 76 | 77 | def getportnum(port): 78 | """ 79 | Accepts a port name or number and returns the port number as an int. 80 | Returns -1 in case of invalid port name. 81 | """ 82 | try: 83 | portnum = int(port) 84 | if portnum < 0 or portnum > 65535: 85 | logger.error("invalid port number: %s" % port) 86 | portnum = -1 87 | except: 88 | try: 89 | p = socket.getservbyname(port) 90 | portnum = int(p) 91 | except socket.error, e: 92 | logger.error("%s: %s" % (e, port)) 93 | portnum = -1 94 | return portnum 95 | 96 | 97 | def gethostbyname_ex(host): 98 | """Accepts a hostname and return it's IPv4 address(es) as a list""" 99 | try: 100 | (hostname, aliaslist, ipaddrlist) = socket.gethostbyname_ex(host) 101 | except socket.gaierror as e: 102 | logger.error("%s: %s" % (e.strerror, host)) 103 | return list() 104 | else: 105 | return ipaddrlist 106 | 107 | 108 | def pager(pager,lines): 109 | """print lines to screen and mimic behaviour of MORE command""" 110 | text = "\n".join(lines) 111 | if pager.upper() == 'NONE': 112 | print text 113 | else: 114 | try: 115 | p = subprocess.Popen(pager.split(), stdin=subprocess.PIPE) 116 | except OSError as e: 117 | logger.error("Problem with pager: %s" % pager) 118 | logger.error(e) 119 | else: 120 | stdout, stderr = p.communicate(input=text) 121 | 122 | 123 | def check_output(args, cwd=None, silent=False): 124 | """Wrapper for subprocess.check_output""" 125 | if not silent: 126 | logger.info("Running command: %s " % " ".join(args)) 127 | try: 128 | try: 129 | output = subprocess.check_output(args, cwd=cwd) 130 | return output 131 | # python 2.6 compatibility code 132 | except AttributeError: 133 | output, stderr = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd).communicate() 134 | return output 135 | except OSError as e: 136 | logger.error(e) 137 | -------------------------------------------------------------------------------- /tests/etc/ldirectord.conf-1: -------------------------------------------------------------------------------- 1 | # 2 | # Ldirectord will periodically connect to each real server 3 | # and request a known URL. If the data returned by the server 4 | # does not contain the the expected response then the 5 | # test fails and the real server will be taken out of the available 6 | # pool. The real server will be added back into the pool once the 7 | # test succeeds. If all real servers are removed from the pool then 8 | # localhost is added to the pool as a fallback measure. 9 | # 10 | # Based on the sample ldirectord.cf provided with ldirectord 11 | # 12 | # Prepared: February 2005 13 | # 14 | 15 | # Global Directives 16 | checktimeout=10 17 | checkinterval=2 18 | #fallback=127.0.0.1:80 19 | autoreload=no 20 | #logfile="/var/log/ldirectord.log" 21 | logfile="local0" 22 | quiescent=yes 23 | 24 | # Virtual Server for HTTP 25 | virtual=192.168.6.240:80 26 | fallback=127.0.0.1:80 27 | real=192.168.7.4:80 masq 28 | real=192.168.7.5:80 masq 29 | service=http 30 | request="index.html" 31 | receive="Test Page" 32 | scheduler=rr 33 | #persistent=600 34 | protocol=tcp 35 | checktype=negotiate 36 | 37 | # Virtual Service for HTTPS 38 | virtual=192.168.6.240:443 39 | fallback=127.0.0.1:443 40 | real=192.168.7.4:443 masq 41 | real=192.168.7.5:443 masq 42 | service=https 43 | request="index.html" 44 | receive="Test Page" 45 | scheduler=rr 46 | #persistent=600 47 | protocol=tcp 48 | checktype=negotiate 49 | 50 | # Virtual Service for FTP 51 | # Note that peresistancy needs to be turned on for FTP when 52 | # used with LVS-TUN (ipip) or LVS-DR (gate), but not with LVS-NAT (masq). 53 | virtual=192.168.6.240:21 54 | fallback=127.0.0.1:21 55 | real=192.168.7.4:21 masq 56 | real=192.168.7.5:21 masq 57 | service=ftp 58 | request="welcome.msg" 59 | receive="Welcome" 60 | login="anonymous" 61 | passwd="anon@anon.anon" 62 | scheduler=rr 63 | #persistent=600 64 | protocol=tcp 65 | checktype=negotiate 66 | 67 | ## Virtual Service for IMAP 68 | #virtual=192.168.6.240:143 69 | # fallback=127.0.0.1:143 70 | # real=192.168.7.4:143 masq 71 | # real=192.168.7.5:143 masq 72 | # service=imap 73 | # #login="test" 74 | # #passwd="test" 75 | # scheduler=rr 76 | # #persistent=600 77 | # protocol=tcp 78 | # checktype=negotiate 79 | # 80 | ## Virtual Service for POP 81 | #virtual=192.168.6.240:110 82 | # fallback=127.0.0.1:110 83 | # real=192.168.7.4:110 masq 84 | # real=192.168.7.5:110 masq 85 | # service=pop 86 | # #login="test" 87 | # #passwd="test" 88 | # scheduler=rr 89 | # #persistent=600 90 | # protocol=tcp 91 | # 92 | ## Virtual Service for SMTP 93 | #virtual=192.168.6.240:25 94 | # fallback=127.0.0.1:25 95 | # real=192.168.7.4:25 masq 96 | # real=192.168.7.5:25 masq 97 | # service=smtp 98 | # scheduler=rr 99 | # #persistent=600 100 | # protocol=tcp 101 | # 102 | ## Virtual Service for LDAP 103 | #virtual=192.168.6.240:389 104 | # fallback=127.0.0.1:389 105 | # real=192.168.7.4:389 masq 106 | # real=192.168.7.5:389 masq 107 | # service=ldap 108 | # scheduler=rr 109 | # #persistent=600 110 | # protocol=tcp 111 | # 112 | 113 | #Sample configuration for an nntp virtual service. 114 | #Fallback setting overides global 115 | #virtual=192.168.6.240:119 116 | # real=192.168.7.4:119 masq 117 | # real=192.168.7.5:119 masq 118 | # fallback=127.0.0.1:119 119 | # service=nntp 120 | # scheduler=wlc 121 | # #persistent=600 122 | # #netmask=255.255.255.255 123 | # protocol=tcp 124 | 125 | #Sample configuration for a UDP DNS virtual service. 126 | #Fallback setting overides global 127 | #virtual=192.168.6.240:53 128 | # real=192.168.7.4:53 masq 129 | # real=192.168.7.5:53 masq 130 | # fallback=127.0.0.1:53 131 | # request="some.domain.com.au" 132 | # recieve="127.0.0.1" 133 | # service=dns 134 | # scheduler=wlc 135 | # #persistent=600 136 | # #netmask=255.255.255.255 137 | # protocol=udp 138 | 139 | #Sample configuration for a MySQL virtual service. 140 | #virtual = 192.168.6.240:3306 141 | # real=192.168.7.4:3306 masq 142 | # real=192.168.7.5:3306 masq 143 | # fallback=127.0.0.1:3306 masq 144 | # checktype = negotiate 145 | # login = "readuser" 146 | # passwd = "genericpassword" 147 | # database = "portal" 148 | # request = "SELECT * FROM link" 149 | # scheduler = wrr 150 | 151 | #Sample configuration for an unsuported protocol 152 | #The real servers will just be brought up without checking for availability 153 | #Fallback setting overides global 154 | #virtual=192.168.6.240:23 155 | # real=192.168.7.4:23 masq 156 | # real=192.168.7.5:23 masq 157 | # fallback=127.0.0.1:21 158 | # service=none 159 | # scheduler=wlc 160 | # request="welcome.msg" 161 | # receive="test" 162 | # persistent=600 163 | # #netmask=255.255.255.255 164 | # protocol=tcp 165 | 166 | -------------------------------------------------------------------------------- /lvsm/modules/ldirectordprompts.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from lvsm import shell 4 | 5 | logger = logging.getLogger('lvsm') 6 | 7 | class VirtualPrompt(shell.VirtualPrompt): 8 | """ 9 | Display information about virtual IP with ldirector specific fucntions. 10 | """ 11 | def __init__(self, config, stdin=sys.stdin, stdout=sys.stdout): 12 | shell.VirtualPrompt.__init__(self, config) 13 | 14 | # def do_disable(self, line): 15 | # """ 16 | # \rDisable real server across VIPs. 17 | # \rsyntax: disable 18 | # """ 19 | 20 | # syntax = "syntax: disable " 21 | 22 | # commands = line.split() 23 | # if len(commands) > 2 or len(commands) == 0: 24 | # print syntax 25 | # elif len(commands) <= 2: 26 | # host = commands[0] 27 | # if len(commands) == 1: 28 | # port = '' 29 | # elif len(commands) == 2: 30 | # port = commands[1] 31 | # else: 32 | # print syntax 33 | # return 34 | # # ask for an optional reason for disabling 35 | # reason = raw_input("Reason for disabling [default = None]: ") 36 | # if not self.director.disable(host, port, reason): 37 | # logger.error("Could not disable %s" % host) 38 | # else: 39 | # print syntax 40 | 41 | # def do_enable(self, line): 42 | # """ 43 | # \rEnable real server across VIPs. 44 | # \rsyntax: enable 45 | # """ 46 | 47 | # syntax = "syntax: enable " 48 | 49 | # commands = line.split() 50 | # if len(commands) > 2 or len(commands) == 0: 51 | # print syntax 52 | # elif len(commands) <= 2: 53 | # host = commands[0] 54 | # if len(commands) == 1: 55 | # port = '' 56 | # elif len(commands) == 2: 57 | # port = commands[0] 58 | # else: 59 | # print syntax 60 | # return 61 | # if not self.director.enable(host, port): 62 | # logger.error("Could not enable %s" % host) 63 | # else: 64 | # print syntax 65 | 66 | class RealPrompt(shell.RealPrompt): 67 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 68 | shell.RealPrompt.__init__(self, config) 69 | 70 | def do_disable(self, line): 71 | """ 72 | \rDisable real server across VIPs. 73 | \rsyntax: disable 74 | """ 75 | 76 | syntax = "*** Syntax: disable " 77 | 78 | commands = line.split() 79 | if len(commands) > 2 or len(commands) == 0: 80 | print syntax 81 | elif len(commands) <= 2: 82 | host = commands[0] 83 | if len(commands) == 1: 84 | port = '' 85 | elif len(commands) == 2: 86 | port = commands[1] 87 | else: 88 | print syntax 89 | return 90 | # ask for an optional reason for disabling 91 | reason = raw_input("Reason for disabling [default = None]: ") 92 | if not self.director.disable(host, port, reason=reason): 93 | logger.error("Could not disable %s" % host) 94 | else: 95 | print syntax 96 | 97 | def do_enable(self, line): 98 | """ 99 | \rEnable real server across VIPs. 100 | \rsyntax: enable 101 | """ 102 | 103 | syntax = "*** Syntax: enable " 104 | 105 | commands = line.split() 106 | if len(commands) > 2 or len(commands) == 0: 107 | print syntax 108 | elif len(commands) <= 2: 109 | host = commands[0] 110 | if len(commands) == 1: 111 | port = '' 112 | elif len(commands) == 2: 113 | port = commands[1] 114 | else: 115 | print syntax 116 | return 117 | if not self.director.enable(host, port): 118 | logger.error("Could not enable %s" % host) 119 | else: 120 | print syntax 121 | 122 | def complete_disable(self, text, line, begidx, endidx): 123 | """Tab completion for disable command""" 124 | tokens = line.split() 125 | reals = self.director.get_real(protocol='') 126 | 127 | if not text: 128 | if len(tokens) == 1: 129 | completions = reals[:] 130 | elif len(tokens) == 2: 131 | completions = [r for r in reals if r.startswith(text)] 132 | else: 133 | completions = list() 134 | 135 | return completions 136 | 137 | def complete_enable(self, text, line, begidx, endidx): 138 | """Tab completion for enable command""" 139 | tokens = line.split() 140 | reals = self.director.get_real(protocol='') 141 | 142 | if not text: 143 | if len(tokens) == 1: 144 | completions = reals[:] 145 | elif len(tokens) == 2: 146 | completions = [r for r in reals if r.startswith(text)] 147 | else: 148 | completions = list() 149 | 150 | return completions -------------------------------------------------------------------------------- /lvsm/sourcecontrol.py: -------------------------------------------------------------------------------- 1 | """ 2 | Source control classes used for managing the configuration 3 | """ 4 | import subprocess 5 | import logging 6 | import getpass 7 | import utils 8 | 9 | logger = logging.getLogger('lvsm') 10 | 11 | 12 | class Subversion(object): 13 | def __init__(self, args): 14 | super(Subversion, self).__init__() 15 | 16 | def commit(self, filename): 17 | # ask for username and passwd so user isn't bugged on each server 18 | self.username = raw_input("Enter SVN username: ") 19 | self.password = getpass.getpass("Enter SVN password: ") 20 | 21 | # prepare the svn commit command 22 | cmd = ['svn', 'commit'] 23 | cmd.append('--username') 24 | cmd.append(self.username) 25 | cmd.append('--password') 26 | cmd.append(self.password) 27 | cmd.append(filename) 28 | 29 | # call the svn command 30 | try: 31 | logger.info("Running command: %s" % " ".join(cmd)) 32 | ret = subprocess.call(cmd) 33 | if ret: 34 | logger.error("svn returned an error!") 35 | except IOError as e: 36 | logger.error(e) 37 | 38 | def modified(self, filename): 39 | """Check the status of the file and if modified return True""" 40 | # prepare the svn command 41 | cmd = ['svn', 'status', filename] 42 | 43 | # call the svn command 44 | try: 45 | logger.info("Running the command: %s" % " ".join(cmd)) 46 | ret = utils.check_output(cmd) 47 | if ret and ret.startswith('M'): 48 | return True 49 | except IOError as e: 50 | logger.error(e) 51 | except subprocess.CalledProcessError as e: 52 | logger.error(e.output) 53 | return False 54 | 55 | def update(self, filename, node): 56 | # prepare the svn command 57 | cmd = ['svn', 'update', 58 | '--username', self.username, 59 | '--password', self.password, 60 | filename] 61 | 62 | # call the svn command 63 | try: 64 | logger.info("Running the command: %s" % " ".join(cmd)) 65 | ret = subprocess.call(cmd) 66 | if ret: 67 | logger.error("svn return an error!") 68 | except IOError as e: 69 | logger.error(e) 70 | 71 | 72 | class Git(object): 73 | """ 74 | Git class handles storing the configuration in a git repository 75 | The assumption is that the repositories are already setup. 76 | """ 77 | def __init__(self, args): 78 | super(Git, self).__init__() 79 | 80 | try: 81 | self.remote = args['git_remote'] 82 | except KeyError: 83 | logger.error("git remote not defined!") 84 | import sys 85 | sys.exit(1) 86 | 87 | self.branch = args['git_branch'] 88 | 89 | def commit(self, filename): 90 | try: 91 | cmd = ['dirname', filename] 92 | wd = utils.check_output(cmd, silent=True).rstrip('\n') 93 | 94 | cmd = ['basename', filename] 95 | name = utils.check_output(cmd, silent=True).rstrip('\n') 96 | 97 | args = ['git', 'commit', name] 98 | logger.info("Running command: %s" % " ".join(args)) 99 | subprocess.call(args, cwd=wd) 100 | 101 | except (OSError, subprocess.CalledProcessError) as e: 102 | logger.error(e) 103 | 104 | def modified(self, filename): 105 | """Verifies that a file was modified. Returns True if it was""" 106 | try: 107 | cmd = ['dirname', filename] 108 | wd = utils.check_output(cmd, silent=True).rstrip('\n') 109 | 110 | cmd = ['basename', filename] 111 | name = utils.check_output(cmd, silent=True).rstrip('\n') 112 | 113 | args = ['git', 'status', '-s', name] 114 | stdout = utils.check_output(args, cwd=wd) 115 | 116 | except (OSError, subprocess.CalledProcessError) as e: 117 | logger.error(e) 118 | return False 119 | 120 | output = stdout.strip(' \n') 121 | if output and output.startswith('M'): 122 | logger.debug('%s was modified' % filename) 123 | return True 124 | else: 125 | return False 126 | 127 | def update(self, filename, node): 128 | """ 129 | Check the status of the file and if modified return True. 130 | Assumption is that a 'remote' named 'lvsm' is created and 131 | points to the matching git repo on each opposite node. 132 | ex. 133 | remote.lvsm.url=user@node1:/etc/lvsm/ 134 | """ 135 | 136 | try: 137 | cmd = ['dirname', filename] 138 | logger.debug('Updating %s' % filename) 139 | wd = utils.check_output(cmd, silent=True).rstrip('\n') 140 | logger.debug('working directory used by git: %s' % wd) 141 | 142 | # remote = 'lvsm' 143 | args = ['ssh', node, 'cd', wd, ';', 'git', 'pull', self.remote, self.branch] 144 | logger.info('Running command: %s' % " ".join(args)) 145 | subprocess.call(args) 146 | 147 | except (OSError, subprocess.CalledProcessError) as e: 148 | logger.error(e) 149 | return False 150 | 151 | class SourceControl(object): 152 | """ 153 | Factory class for the scm objects. Only this class should be instantiated. 154 | """ 155 | scm = {'subversion': Subversion, 'git': Git} 156 | 157 | def __new__(self, name, args=dict()): 158 | return SourceControl.scm[name](args) 159 | -------------------------------------------------------------------------------- /lvsm/termcolor.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright (c) 2008-2011 Volvox Development Team 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | # Author: Konstantin Lepa 23 | # 24 | # Patched by Khosrow Ebrahimpour 25 | 26 | """ANSII Color formatting for output in terminal.""" 27 | 28 | from __future__ import print_function 29 | import os 30 | 31 | 32 | __ALL__ = ['colored', 'cprint'] 33 | 34 | VERSION = (1, 1, 0) 35 | 36 | ATTRIBUTES = dict(list(zip(['bold', 37 | 'dark', 38 | '', 39 | 'underline', 40 | 'blink', 41 | '', 42 | 'reverse', 43 | 'concealed' 44 | ], 45 | list(range(1, 9))))) 46 | del ATTRIBUTES[''] 47 | 48 | 49 | HIGHLIGHTS = dict(list(zip(['on_grey', 50 | 'on_red', 51 | 'on_green', 52 | 'on_yellow', 53 | 'on_blue', 54 | 'on_magenta', 55 | 'on_cyan', 56 | 'on_white' 57 | ], 58 | list(range(40, 48))))) 59 | 60 | 61 | COLORS = dict(list(zip(['grey', 62 | 'red', 63 | 'green', 64 | 'yellow', 65 | 'blue', 66 | 'magenta', 67 | 'cyan', 68 | 'white', 69 | ], 70 | list(range(30, 38))))) 71 | 72 | 73 | RESET = '\033[0m' 74 | 75 | 76 | def colored(text, color=None, on_color=None, attrs=None): 77 | """Colorize text. 78 | 79 | Available text colors: 80 | red, green, yellow, blue, magenta, cyan, white. 81 | 82 | Available text highlights: 83 | on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white. 84 | 85 | Available attributes: 86 | bold, dark, underline, blink, reverse, concealed. 87 | 88 | Example: 89 | colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink']) 90 | colored('Hello, World!', 'green') 91 | """ 92 | if os.getenv('ANSI_COLORS_DISABLED') is None: 93 | fmt_str = '\033[%dm%s' 94 | if color is not None: 95 | text = fmt_str % (COLORS[color], text) 96 | 97 | if on_color is not None: 98 | text = fmt_str % (HIGHLIGHTS[on_color], text) 99 | 100 | if attrs is not None: 101 | for attr in attrs: 102 | text = fmt_str % (ATTRIBUTES[attr], text) 103 | 104 | if color is not None or on_color is not None or attrs is not None: 105 | text += RESET 106 | return text 107 | 108 | 109 | def cprint(text, color=None, on_color=None, attrs=None, **kwargs): 110 | """Print colorize text. 111 | 112 | It accepts arguments of print function. 113 | """ 114 | 115 | print((colored(text, color, on_color, attrs)), **kwargs) 116 | 117 | 118 | if __name__ == '__main__': 119 | print('Current terminal type: %s' % os.getenv('TERM')) 120 | print('Test basic colors:') 121 | cprint('Grey color', 'grey') 122 | cprint('Red color', 'red') 123 | cprint('Green color', 'green') 124 | cprint('Yellow color', 'yellow') 125 | cprint('Blue color', 'blue') 126 | cprint('Magenta color', 'magenta') 127 | cprint('Cyan color', 'cyan') 128 | cprint('White color', 'white') 129 | print(('-' * 78)) 130 | 131 | print('Test highlights:') 132 | cprint('On grey color', on_color='on_grey') 133 | cprint('On red color', on_color='on_red') 134 | cprint('On green color', on_color='on_green') 135 | cprint('On yellow color', on_color='on_yellow') 136 | cprint('On blue color', on_color='on_blue') 137 | cprint('On magenta color', on_color='on_magenta') 138 | cprint('On cyan color', on_color='on_cyan') 139 | cprint('On white color', color='grey', on_color='on_white') 140 | print('-' * 78) 141 | 142 | print('Test attributes:') 143 | cprint('Bold grey color', 'grey', attrs=['bold']) 144 | cprint('Dark red color', 'red', attrs=['dark']) 145 | cprint('Underline green color', 'green', attrs=['underline']) 146 | cprint('Blink yellow color', 'yellow', attrs=['blink']) 147 | cprint('Reversed blue color', 'blue', attrs=['reverse']) 148 | cprint('Concealed Magenta color', 'magenta', attrs=['concealed']) 149 | cprint('Bold underline reverse cyan color', 'cyan', 150 | attrs=['bold', 'underline', 'reverse']) 151 | cprint('Dark blink concealed white color', 'white', 152 | attrs=['dark', 'blink', 'concealed']) 153 | print(('-' * 78)) 154 | 155 | print('Test mixing:') 156 | cprint('Underline red on grey color', 'red', 'on_grey', 157 | ['underline']) 158 | cprint('Reversed green on red color', 'green', 'on_red', ['reverse']) 159 | -------------------------------------------------------------------------------- /lvsm/firewall.py: -------------------------------------------------------------------------------- 1 | """Firewall funcationality""" 2 | import subprocess 3 | import socket 4 | import utils 5 | import termcolor 6 | import logging 7 | 8 | logger = logging.getLogger('lvsm') 9 | 10 | 11 | class Firewall(): 12 | def __init__(self, iptables): 13 | self.iptables = iptables 14 | 15 | def show(self, numeric, color): 16 | args = [self.iptables, "-L", "-v"] 17 | if numeric: 18 | args.append("-n") 19 | try: 20 | try: 21 | logger.info("Running: %s" % " ".join(args)) 22 | output = subprocess.check_output(args) 23 | # python 2.6 compatibility code 24 | except AttributeError as e: 25 | output, stderr = subprocess.Popen(args, stdout=subprocess.PIPE).communicate() 26 | except OSError as e: 27 | logger.error("problem with iptables - %s : %s" % (e.strerror , args[0])) 28 | return list() 29 | 30 | result = ['', 'IP Packet filter rules'] 31 | result += ['======================'] 32 | if color: 33 | for line in output.split('\n'): 34 | if 'Chain' not in line and 'ACCEPT' in line: 35 | result.append(termcolor.colored(line, 'green')) 36 | elif 'Chain' not in line and ('REJECT' in line or 'DROP' in line): 37 | result.append(termcolor.colored(line, 'red')) 38 | else: 39 | result.append(line) 40 | else: 41 | result += output.split('\n') 42 | 43 | return result 44 | 45 | def show_nat(self, numeric): 46 | args = [self.iptables, "-t", "nat", "-L", "-v"] 47 | if numeric: 48 | args.append("-n") 49 | try: 50 | try: 51 | logger.info("Running: %s" % " ".join(args)) 52 | output = subprocess.check_output(args) 53 | # python 2.6 compatibility code 54 | except AttributeError as e: 55 | output, stderr = subprocess.Popen(args, stdout=subprocess.PIPE).communicate() 56 | except OSError as e: 57 | logger.error("Problem with iptables - %s : %s " % (e.strerror, args[0])) 58 | return list() 59 | result = ['', 'NAT rules', '========='] 60 | result += output.split('\n') 61 | return result 62 | 63 | def show_mangle(self, numeric, color, fwm=None): 64 | """Show the iptables mangle table""" 65 | args = [self.iptables, '-t', 'mangle', '-L', '-v'] 66 | if numeric: 67 | args.append('-n') 68 | 69 | try: 70 | try: 71 | logger.info("Running: %s" % " ".join(args)) 72 | output = subprocess.check_output(args) 73 | # python 2.6 compat code 74 | except AttributeError as e: 75 | output, stderr = subprocess.Popen(args, stdout=subprocess.PIPE).communicate() 76 | except OSError as e: 77 | logger.error("Problem with iptables - %s : %s" % (e.strerror, args[0])) 78 | return list() 79 | 80 | result = list() 81 | if output: 82 | lines = output.split('\n') 83 | 84 | # Find which column contains the destination 85 | #header = lines[1].split() 86 | #try: 87 | # index = header.index('destination') 88 | #except ValueError as e: 89 | # index = -1 90 | 91 | for line in lines: 92 | tokens = line.split() 93 | if fwm and hex(fwm) in tokens: 94 | result.append(line) 95 | else: 96 | result.append(line) 97 | 98 | result.insert(0, '') 99 | result.insert(1, 'Mangle rules') 100 | result.insert(2, '============') 101 | return result 102 | 103 | def show_virtual(self, host, port, protocol, numeric, color): 104 | result = list() 105 | args = [self.iptables, '-L', 'INPUT'] 106 | if port: 107 | portnum = utils.getportnum(port) 108 | try: 109 | portname = socket.getservbyport(int(portnum)) 110 | except socket.error: 111 | portname = portnum 112 | except OverflowError as e: 113 | logger.error("%s" % e) 114 | return list() 115 | 116 | if numeric: 117 | args.append('-n') 118 | hostnames = utils.gethostbyname_ex(host) 119 | else: 120 | # Turn this into a list so it behaves like the above case 121 | # And we only perform a list membership check 122 | hostnames = [socket.getfqdn(host)] 123 | 124 | # Nested try/except needed to catch exceptions in the "Except" 125 | try: 126 | try: 127 | logger.info("Running: %s" % " ".join(args)) 128 | output = subprocess.check_output(args) 129 | # python 2.6 compatibility code 130 | except AttributeError as e: 131 | output, stderr = subprocess.Popen(args, stdout=subprocess.PIPE).communicate() 132 | except OSError as e: 133 | logger.error("Problem with iptables - %s : %s" % (e.strerror, args[0])) 134 | return list() 135 | if output: 136 | lines = output.split('\n') 137 | for line in lines: 138 | # break the iptables output into tokens 139 | # assumptions: 140 | # 2nd item is the protocol - tokens[1] 141 | # 5th item is the hostname - tokens[4] 142 | # 7th item is the portname - tokens[6] 143 | tokens = line.split() 144 | 145 | if len(tokens) >= 7: 146 | if ((tokens[1] == protocol or tokens[2] == "all") and 147 | tokens[4] in hostnames and 148 | ( not port or (tokens[6] == "dpt:" + str(portname) or tokens[6] == "dpt:" + str(portnum))) 149 | ): 150 | if color: 151 | if line.startswith('ACCEPT'): 152 | result.append(termcolor.colored(line, 'green')) 153 | elif (line.startswith('REJECT') or 154 | line.startswith('DROP')): 155 | result.append(termcolor.colored(line, 'red')) 156 | else: 157 | result.append(line) 158 | else: 159 | result.append(line) 160 | # If we have any output, let's also display some headers 161 | if result: 162 | result.insert(0, '') 163 | result.insert(1, 'IP Packet filter rules') 164 | result.insert(2, '======================') 165 | 166 | return result 167 | -------------------------------------------------------------------------------- /lvsm/modules/keepalivedprompts.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from lvsm import shell 4 | 5 | logger = logging.getLogger('lvsm') 6 | 7 | class VirtualPrompt(shell.VirtualPrompt): 8 | """ 9 | Display information about virtual IP with keepalived specific functions. 10 | """ 11 | def __init__(self, config, stdin=sys.stdin, stdout=sys.stdout): 12 | shell.VirtualPrompt.__init__(self, config) 13 | 14 | # def do_disable(self, line): 15 | # """ 16 | # \rDisable real server belonging to a VIP. 17 | # \rsyntax: disable tcp|udp|fwm real 18 | # """ 19 | 20 | # syntax = "syntax: disable tcp|udp|fwm real " 21 | 22 | # commands = line.split() 23 | # if len(commands) == 6: 24 | # module = commands[0] 25 | # vip = commands[1] 26 | # vport = commands[2] 27 | # rip = commands[4] 28 | # rport = commands[5] 29 | # print "Disabling real: %s:%s" % (rip, rport) 30 | # else: 31 | # print "\n%s\n" % syntax 32 | 33 | # def do_enable(self, line): 34 | # """ 35 | # \renable real server belonging to a VIP. 36 | # \rsyntax: enable tcp|udp|fwm real 37 | # """ 38 | 39 | # syntax = "syntax: enable tcp|udp|fwm real " 40 | 41 | # commands = line.split() 42 | # if len(commands) == 6: 43 | # module = commands[0] 44 | # vip = commands[1] 45 | # vport = commands[2] 46 | # rip = commands[4] 47 | # rport = commands[5] 48 | # print "enabling real: %s:%s" % (rip, rport) 49 | # else: 50 | # print "\n%s\n" % syntax 51 | 52 | class RealPrompt(shell.RealPrompt): 53 | """ 54 | Display information about real servers with keepalived specific functions. 55 | """ 56 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 57 | shell.RealPrompt.__init__(self, config) 58 | 59 | def do_disable(self, line): 60 | """ 61 | \rDisable real server across VIPs. 62 | \rsyntax: disable tcp|udp|fwm [] [ ] 63 | """ 64 | 65 | syntax = "*** Syntax: disable tcp|udp|fwm [] [ ]" 66 | 67 | # Some default values to be used 68 | port = '' 69 | vip = '' 70 | vport = '' 71 | commands = line.split() 72 | if len(commands) < 2 or len(commands) > 5: 73 | print syntax 74 | elif len(commands) >= 2: 75 | protocol = commands[0] 76 | # Verify protocol is valid 77 | if protocol not in self.protocols: 78 | print syntax 79 | return 80 | 81 | host = commands[1] 82 | if len(commands) == 3: 83 | port = commands[2] 84 | if len(commands) == 4: 85 | vip = commands[3] 86 | if len(commands) == 5: 87 | vport = commands[4] 88 | 89 | # ask for an optional reason for disabling 90 | reason = raw_input("Reason for disabling [default = None]: ") 91 | # if not self.director.disable(host, port, reason=reason): 92 | if not self.director.disable(protocol, 93 | host, port, 94 | vip, vport, 95 | reason=reason): 96 | logger.error("Could not disable %s" % host) 97 | else: 98 | print syntax 99 | 100 | def do_enable(self, line): 101 | """ 102 | \rEnable real server across VIPs. 103 | \rsyntax: enable tcp|udp|fwm [] [ ] 104 | """ 105 | 106 | syntax = "*** Syntax: enable tcp|udp|fwm [] [ ]" 107 | 108 | # Some default values to be used 109 | port = '' 110 | vip = '' 111 | vport = '' 112 | 113 | commands = line.split() 114 | if len(commands) < 2 or len(commands) > 5: 115 | print syntax 116 | elif len(commands) >= 2: 117 | protocol = commands[0] 118 | # Verify protocol is valid 119 | if protocol not in self.protocols: 120 | print syntax 121 | return 122 | 123 | host = commands[1] 124 | if len(commands) == 3: 125 | port = commands[2] 126 | elif len(commands) == 4: 127 | vip = commands[3] 128 | if len(commands) == 5: 129 | vport = commands[4] 130 | 131 | if not self.director.enable(protocol, 132 | host, port, 133 | vip, vport): 134 | logger.error("Could not enable %s" % host) 135 | else: 136 | print syntax 137 | 138 | def complete_disable(self, text, line, begidx, endidx): 139 | """Tab completion for the disable command""" 140 | tokens = line.split() 141 | if len(line) < 12: 142 | completions = [p for p in self.protocols if p.startswith(text)] 143 | elif len(tokens) == 2: 144 | prot = tokens[1] 145 | reals = self.director.get_real(prot) 146 | if not text: 147 | completions = reals[:] 148 | elif len(tokens) == 3 and text: 149 | prot = tokens[1] 150 | reals = self.director.get_real(prot) 151 | completions = [p for p in reals if p.startswith(text)] 152 | elif len(tokens) == 4 and not text: 153 | prot = tokens[1] 154 | virtuals = self.director.get_virtual(prot) 155 | completions = virtuals[:] 156 | elif len(tokens) == 5 and text: 157 | virtuals = self.director.get_virtual(prot) 158 | completions = [p for p in virtuals if p.startswith(text)] 159 | else: 160 | completions = list() 161 | 162 | return completions 163 | 164 | def complete_enable(self, text, line, begidx, endidx): 165 | """Tab completion for the disable command""" 166 | tokens = line.split() 167 | if len(line) < 11: 168 | completions = [p for p in self.protocols if p.startswith(text)] 169 | elif len(tokens) == 2 and not text: 170 | prot = tokens[1] 171 | reals = self.director.get_real(prot) 172 | completions = reals[:] 173 | elif len(tokens) == 3 and text: 174 | prot = tokens[1] 175 | reals = self.director.get_real(prot) 176 | completions = [p for p in reals if p.startswith(text)] 177 | elif len(tokens) == 4 and not text: 178 | prot = tokens[1] 179 | virtuals = self.director.get_virtual(prot) 180 | completions = virtuals[:] 181 | elif len(tokens) == 5 and text: 182 | prot = tokens[1] 183 | virtuals = self.director.get_virtual(prot) 184 | completions = [p for p in virtuals if p.startswith(text)] 185 | else: 186 | completions = list() 187 | 188 | return completions -------------------------------------------------------------------------------- /lvsm/modules/ldparser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse a ldirector configuration file. The config specs are defined in ldirectord.cf(5) 3 | """ 4 | import logging 5 | from lvsm.modules import parseactions 6 | from pyparsing import * 7 | 8 | logging.basicConfig(format='[%(levelname)s]: %(message)s') 9 | logger = logging.getLogger('ldparser') 10 | 11 | def tokenize_config(configfile): 12 | """Tokenize the config file. This method will do the bulk of the 13 | parsing. Additional verifications can be made in parse_config""" 14 | # Needed to parse the tabbed ldirector config 15 | indentStack = [1] 16 | 17 | # Define statics 18 | EQUAL = Literal("=").suppress() 19 | COLON = Literal(":").suppress() 20 | # INDENT = White(" ").suppress() 21 | # INDENT = Regex("^ {4,}").suppress() 22 | 23 | # Define parsing rules for single lines 24 | hostname = Word(alphanums + '._+-') 25 | ip4_address = Combine(Word(nums) - ('.' + Word(nums)) * 3) 26 | ip4_address.setParseAction(parseactions.validate_ip4) 27 | ip6_address = Word(alphanums + ':') 28 | ip6_address.setParseAction(parseactions.validate_ip6) 29 | 30 | port = Word(alphanums) 31 | port.setParseAction(parseactions.validate_port) 32 | 33 | lbmethod = Word(alphas) 34 | lbmethod.setParseAction(parseactions.validate_lbmethod) 35 | 36 | ip4_addrport = (ip4_address | hostname) + COLON + port 37 | ip6_addrport = (ip6_address | hostname) + COLON + port 38 | 39 | yesno = Word(printables) 40 | yesno.setParseAction(parseactions.validate_yesno) 41 | 42 | integer = Word(printables) 43 | integer.setParseAction(parseactions.validate_int) 44 | # integer.setParseAction(lambda t:int(t[0])) 45 | 46 | send_receive = dblQuotedString + Literal(",") + dblQuotedString 47 | 48 | real4 = Group(Literal("real") + EQUAL + ip4_addrport + lbmethod + Optional(Word(nums)) + Optional(send_receive)) 49 | # real4 = Group(Literal("real") + EQUAL + ip4_addrport + lbmethod + Optional(Word(nums)) + Optional(Word(dblQuotedString) + Word(dblQuotedString))) 50 | real6 = Group(Literal("real6") + EQUAL + ip6_addrport + lbmethod + Optional(Word(nums)) + Optional(send_receive)) 51 | 52 | virtual4 = Group(Literal("virtual") + EQUAL + ip4_addrport) 53 | virtual6 = Group(Literal("virtual6") + EQUAL + ip6_addrport) 54 | comment = Literal("#") + Optional(restOfLine) 55 | 56 | # Optional keywords 57 | optionals = Forward() 58 | autoreload = Group(Literal("autoreload") + EQUAL + yesno) 59 | callback = Group(Literal("callback") + EQUAL + dblQuotedString) 60 | checkcommand = Group(Literal("checkcommand") + EQUAL + (dblQuotedString | Word(printables))) 61 | # checkinterval = Group(Literal("checkinterval") + EQUAL + Word(alphanums)) 62 | checkinterval = Group(Literal("checkinterval") + EQUAL + integer) 63 | checktimeout = Group(Literal("checktimeout") + EQUAL + integer) 64 | checktype = Group(Literal("checktype") + EQUAL + Word(alphanums)) 65 | checkport = Group(Literal("checkport") + EQUAL + Word(alphanums)) 66 | cleanstop = Group(Literal("cleanstop") + EQUAL + yesno) 67 | database = Group(Literal("database") + EQUAL + dblQuotedString) 68 | emailalert = Group(Literal("emailalert") + EQUAL + Word(printables)) 69 | emailalertfreq = Group(Literal("emailalertfreq") + EQUAL + integer) 70 | emailalertfrom = Group(Literal("emailalertfrom") + EQUAL + Word(printables)) 71 | emailalertstatus = Group(Literal("emailalertstatus") + EQUAL + Word(printables)) 72 | execute = Group(Literal("execute") + EQUAL + Word(printables)) 73 | failurecount = Group(Literal("failurecount") + EQUAL + integer) 74 | # fallback = Group(Literal("fallback") + EQUAL + ip4_addrport + Optional(lbmethod, default='')) 75 | fallback = Group(Literal("fallback") + EQUAL + ip4_addrport) 76 | fallback6 = Group(Literal("fallback6") + EQUAL + ip6_addrport) 77 | fallbackcommand = Group(Literal("fallbackcommand") + EQUAL + (dblQuotedString | Word(printables))) 78 | fork = Group(Literal("fork") + EQUAL + yesno) 79 | httpmethod = Group(Literal("httpmethod") + EQUAL + Word(alphanums)) 80 | load = Group(Literal("load") + EQUAL + dblQuotedString) 81 | logfile = Group(Literal("logfile") + EQUAL + Word(printables)) 82 | login = Group(Literal("login") + EQUAL + dblQuotedString) 83 | maintenancedir = Group(Literal("maintenancedir") + EQUAL + Word(printables)) 84 | monitorfile = Group(Literal("monitorfile") + EQUAL + (dblQuotedString | Word(printables))) 85 | negotiatetimeout = Group(Literal("negotiatetimeout") + EQUAL + integer) 86 | netmask = Group(Literal("netmask") + EQUAL + ip4_address) 87 | passwd = Group(Literal("passwd") + EQUAL + dblQuotedString) 88 | persistent = Group(Literal("persistent") + EQUAL + integer) 89 | protocol = Group(Literal("protocol") + EQUAL + Word(alphas)) 90 | quiescent = Group(Literal("quiescent") + EQUAL + yesno) 91 | readdquiescent = Group(Literal("readdquiescent") + EQUAL + yesno) 92 | receive = Group(Literal("receive") + EQUAL + dblQuotedString) 93 | request = Group(Literal("request") + EQUAL + dblQuotedString) 94 | scheduler = Group(Literal("scheduler") + EQUAL + Word(alphas)) 95 | secret = Group(Literal("secret") + EQUAL + dblQuotedString) 96 | service = Group(Literal("service") + EQUAL + Word(alphas)) 97 | supervised = Group(Literal("supervised") + EQUAL + yesno) 98 | smtp = Group(Literal("smtp") + EQUAL + (ip4_address | hostname)) 99 | virtualhost = Group(Literal("virtualhost") + EQUAL + '"' + hostname + '"' ) 100 | 101 | # Validate all the matched elements 102 | checkport.setParseAction(parseactions.validate_port) 103 | checktype.setParseAction(parseactions.validate_checktype) 104 | httpmethod.setParseAction(parseactions.validate_httpmethod) 105 | protocol.setParseAction(parseactions.validate_protocol) 106 | scheduler.setParseAction(parseactions.validate_scheduler) 107 | service.setParseAction(parseactions.validate_service) 108 | 109 | # TODO: implement fwm parsing 110 | # TODO: validate protocol with respect to the virtual directive 111 | 112 | optionals << ( checkcommand | checkinterval | checktimeout | checktype | checkport | cleanstop 113 | | database | emailalert | emailalertfreq | emailalertstatus | failurecount | fallback | fallback6 114 | | fallbackcommand | httpmethod | load | login | monitorfile | negotiatetimeout | netmask 115 | | passwd | persistent | protocol | quiescent | receive | request | scheduler | secret 116 | | service | smtp | virtualhost) 117 | # optionals = ( checkcommand | checkinterval | checktimeout | checktype | checkport | cleanstop 118 | # | database | emailalert | emailalertfreq | emailalertstatus | failurecount | fallback 119 | # | fallbackcommand | httpmethod | load | login | monitorfile | negotiatetimeout | netmask 120 | # | passwd | persistent | protocol | quiescent | receive | request | scheduler | secret 121 | # | service | smtp | virtualhost) 122 | 123 | glb_optionals = ( checktimeout | negotiatetimeout | checkinterval | failurecount | fallback | fallback6 124 | | fallbackcommand | autoreload | callback | logfile | execute | fork | supervised 125 | | quiescent | readdquiescent | emailalert | emailalertfreq | emailalertstatus 126 | | emailalertfrom | cleanstop | smtp | maintenancedir ) 127 | 128 | ## Define block of config 129 | # both of the next two styles works 130 | # virtual4_keywords = indentedBlock(OneOrMore(real4 & ZeroOrMore(optionals)), indentStack, True) 131 | # virtual4_block = virtual4 + virtual4_keywords 132 | virtual4_keywords = OneOrMore(real4) & ZeroOrMore(optionals) 133 | virtual4_block = Group(virtual4 + indentedBlock(virtual4_keywords, indentStack, True)) 134 | virtual4_block.ignore(comment) 135 | 136 | virtual6_keywords = OneOrMore(real6) & ZeroOrMore(optionals) 137 | virtual6_block = Group(virtual6 + indentedBlock(virtual6_keywords, indentStack, True)) 138 | virtual6_block.ignore(comment) 139 | 140 | allconfig = OneOrMore(virtual4_block | virtual6_block) & ZeroOrMore(glb_optionals) 141 | allconfig.ignore(comment) 142 | 143 | try: 144 | tokens = allconfig.parseString(configfile) 145 | except ParseException as pe: 146 | logger.error("Exception parsing config file") 147 | logger.error(pe) 148 | except ParseFatalException as pe: 149 | logger.error("While parsing config file") 150 | logger.error(pe) 151 | else: 152 | return tokens 153 | finally: 154 | pass 155 | -------------------------------------------------------------------------------- /lvsm/modules/ldirectord.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import socket 4 | import subprocess 5 | import sys 6 | import time 7 | import logging 8 | from pyparsing import * 9 | from lvsm import utils, genericdirector 10 | from lvsm.modules import ldparser 11 | 12 | logger = logging.getLogger('lvsm') 13 | 14 | 15 | class Ldirectord(genericdirector.GenericDirector): 16 | """Handles ldirector-specific functionality like enable/disable actions. 17 | """ 18 | def __init__(self, ipvsadm, configfile='', restart_cmd='', nodes='', args=dict()): 19 | super(Ldirectord, self).__init__(ipvsadm, configfile, restart_cmd, nodes, args) 20 | try: 21 | f = open(self.configfile) 22 | except OSError as e: 23 | logger.error(e) 24 | 25 | self.maintenance_dir = "" 26 | 27 | for line in f: 28 | if line.find("maintenancedir") > -1: 29 | s, sep, path = line.partition('=') 30 | self.maintenance_dir = os.path.abspath(os.path.expanduser(os.path.expandvars(path.strip()))) 31 | 32 | def disable(self, host, port='', reason=''): 33 | # Prepare a canned error message 34 | error_msg = "Problem disabling on remote node" 35 | if self.maintenance_dir: 36 | hostips = utils.gethostbyname_ex(host) 37 | if not hostips: 38 | return False 39 | 40 | # In disable cmd, we ignore if the host has more than one IP 41 | hostip = hostips[0] 42 | 43 | if port: 44 | # check that it's a valid port 45 | portnum = utils.getportnum(port) 46 | if portnum == -1: 47 | return False 48 | hostport = hostip + ":" + str(portnum) 49 | else: 50 | hostport = hostip 51 | # go through maint_dir to see if host is already disabled 52 | filenames = os.listdir(self.maintenance_dir) 53 | for filename in filenames: 54 | f = self.convert_filename(filename) 55 | if hostport == f or hostip == f: 56 | logger.warning("host is already disabled!") 57 | return True 58 | try: 59 | f = open(self.maintenance_dir + "/" + hostport, 'w') 60 | f.write(reason) 61 | except IOError as e: 62 | logger.error(e) 63 | return False 64 | 65 | # Copy the state file to other nodes 66 | fullpath = self.maintenance_dir + "/" + hostport 67 | self.filesync_nodes('copy', fullpath) 68 | 69 | # Confirm that it's removed from ldirector 70 | i = 0 71 | print "Disabling server ", 72 | while i < 10: 73 | sys.stdout.write(".") 74 | sys.stdout.flush() 75 | time.sleep(1) 76 | i = i + 1 77 | found = False 78 | 79 | output = self.show_running(numeric=True, color=False) 80 | for line in output: 81 | if hostport in line: 82 | found = True 83 | break 84 | if not found: 85 | print " OK" 86 | break 87 | if found: 88 | print " Failed" 89 | # note: even if the real is still showing up, we've created 90 | # the file, so we should still return true 91 | return True 92 | else: 93 | logger.error("maintenance_dir not defined in config.") 94 | return False 95 | 96 | def enable(self, host, port=''): 97 | """enable a previously disabled server""" 98 | # Prepare a canned error message. 99 | error_msg = "Problem enabling on remote node" 100 | 101 | hostips = utils.gethostbyname_ex(host) 102 | if not hostips: 103 | return False 104 | 105 | # In enable cmd, we ignore if the host has more than one IP 106 | hostip = hostips[0] 107 | 108 | # If port was provided the file will be of form xx.xx.xx.xx:nn 109 | if port: 110 | # Check that the port is valid. 111 | portnum = utils.getportnum(port) 112 | if portnum == -1: 113 | return False 114 | hostport = hostip + ":" + str(portnum) 115 | # If no port was provided the file be of form xx.xx.xx.xx 116 | else: 117 | hostport = hostip 118 | if self.maintenance_dir: 119 | filenames = os.listdir(self.maintenance_dir) 120 | # Go through all files in maintenance_dir 121 | # and find a matching filename, and remove it. 122 | for filename in filenames: 123 | f = self.convert_filename(filename) 124 | # If user asks to enable xx.xx.xx.xx and xx.xx.xx.xx:nn 125 | # is disabled, we enable all ports and remove all the files. 126 | if (hostport == f or (not port and hostip in f)): 127 | try: 128 | os.unlink(self.maintenance_dir + "/" + filename) 129 | except OSError as e: 130 | logger.error(e) 131 | return False 132 | 133 | # Remove the same file from other nodes in the cluster. 134 | fullpath = self.maintenance_dir + "/" + filename 135 | self.filesync_nodes('remove', fullpath) 136 | 137 | # Wait 4.5 seconds before checking output of ipvsadm. 138 | # This is an arbitrary number and could possibly be changed 139 | i = 0 140 | print "Enabling server ", 141 | while i < 10: 142 | sys.stdout.write(".") 143 | sys.stdout.flush() 144 | time.sleep(1) 145 | i = i + 1 146 | 147 | # Verify that the host is active in ldirectord. 148 | output = self.show_running(numeric=True, color=False) 149 | for line in output: 150 | if hostport in line: 151 | print " OK" 152 | return True 153 | # If we get to this point, means the host is not active. 154 | print " Failed" 155 | # Note: even if the real isn't showing up, we have removed 156 | # the file, so we should still return true. 157 | return True 158 | # If we make it out here means the real wasn't in the file list. 159 | logger.error("Server not found in maintenance_dir!") 160 | return False 161 | else: 162 | logger.error("maintenance_dir not defined!") 163 | return False 164 | 165 | def show_real_disabled(self, host, port, numeric): 166 | """show status of disabled real server across multiple VIPs""" 167 | # note that host='' and port='' returns all disabled server 168 | hostip = '' 169 | hostport = '' 170 | if host: 171 | hostips = utils.gethostbyname_ex(host) 172 | if not hostips: 173 | return 174 | 175 | # Here we only use the first IP if a host has more than one 176 | hostip = hostips[0] 177 | 178 | if port: 179 | portnum = utils.getportnum(port) 180 | if portnum == -1: 181 | return 182 | hostport = hostip + ":" + str(portnum) 183 | 184 | output = list() 185 | 186 | if not self.maintenance_dir: 187 | logger.error("maintenance_dir not defined!") 188 | return output 189 | 190 | # make sure to catch errors like related to maint_dir 191 | try: 192 | filenames = os.listdir(self.maintenance_dir) 193 | except (IOError, OSError) as e: 194 | logger.error("Config item maintenance_dir") 195 | logger.error(e) 196 | return output 197 | 198 | for filename in filenames: 199 | try: 200 | f = open(self.maintenance_dir + "/" + filename) 201 | reason = 'Reason: ' + f.readline() 202 | f.close() 203 | except IOError as e: 204 | reason = '' 205 | logger.error(e) 206 | if (not host or 207 | self.convert_filename(filename) == hostip or 208 | self.convert_filename(filename) == hostport): 209 | 210 | # decide if we have to convert to hostname or not 211 | if numeric: 212 | output.append(filename + "\t\t" + reason) 213 | else: 214 | rip = filename.split(":")[0] 215 | try: 216 | (ripname, al, ipl) = socket.gethostbyaddr(rip) 217 | except socket.herror, e: 218 | logger.error(e) 219 | return False 220 | if len(filename.split(":")) == 2: 221 | ripport = filename.split(":")[1] 222 | try: 223 | ripportname = socket.getservbyport(int(ripport)) 224 | except socket.error as e: 225 | ripportname = ripport 226 | ripname = ripname + ':' + ripportname 227 | else: 228 | pass 229 | output.append(ripname + "\t\t" + reason) 230 | return output 231 | 232 | def parse_config(self, configfile): 233 | """Read the config file and validate configuration syntax""" 234 | try: 235 | f = open(configfile) 236 | except IOError as e: 237 | logger.error(e) 238 | return False 239 | 240 | conf = "".join(f.readlines()) 241 | tokens = ldparser.tokenize_config(conf) 242 | 243 | if tokens: 244 | return True 245 | else: 246 | return False 247 | -------------------------------------------------------------------------------- /tests/test_shell.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | import StringIO 5 | 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | from lvsm import shell 8 | 9 | 10 | class Configure(unittest.TestCase): 11 | """Verify correct functionality in configure""" 12 | config = {'ipvsadm': path + '/scripts/ipvsadm', 13 | 'iptables': path + '/scripts/iptables', 14 | 'pager': 'none', 15 | 'cache_dir': path + '/cache', 16 | 'director_config': path + '/etc/ldirectord.conf', 17 | 'firewall_config': path + '/etc/iptables.rules', 18 | 'director': 'ldirectord', 19 | 'director_cmd': '', 20 | 'director_bin': '', 21 | 'firewall_cmd': '', 22 | 'nodes':'', 23 | 'version_control': '', 24 | 'keepalived-mib': 'KEEPALIVED-MIB', 25 | 'snmp_community': '', 26 | 'snmp_host': '', 27 | 'snmp_user': '', 28 | 'snmp_password': '' 29 | } 30 | shell = shell.ConfigurePrompt(config) 31 | 32 | def test_showdirector1(self): 33 | output = StringIO.StringIO() 34 | sys.stdout = output 35 | expected_result = "# director config\nmaintenancedir = tests/maintenance\n" 36 | self.shell.onecmd(' show director') 37 | result = output.getvalue() 38 | self.assertEqual(result, expected_result) 39 | 40 | def test_showdirector2(self): 41 | # verify error checking 42 | self.assertTrue(True) 43 | 44 | def test_showfirewall1(self): 45 | output = StringIO.StringIO() 46 | sys.stdout = output 47 | expected_result = "# iptables\n" 48 | self.shell.onecmd(' show firewall') 49 | result = output.getvalue() 50 | self.assertEqual(result, expected_result) 51 | 52 | def test_showfirewall2(self): 53 | # verify error checking 54 | self.assertTrue(True) 55 | 56 | 57 | class Virtual(unittest.TestCase): 58 | config = {'ipvsadm': path + '/scripts/ipvsadm', 59 | 'iptables': path + '/scripts/iptables', 60 | 'pager': 'none', 61 | 'cache_dir': path + '/cache', 62 | 'director_config': path + '/etc/ldirectord.conf', 63 | 'firewall_config': path + '/etc/iptables.rules', 64 | 'director': 'ldirectord', 65 | 'director_cmd': '', 66 | 'director_bin': '', 67 | 'firewall_cmd': '', 68 | 'nodes':'', 69 | 'version_control': '', 70 | 'keepalived-mib': 'KEEPALIVED-MIB', 71 | 'snmp_community': '', 72 | 'snmp_host': '', 73 | 'snmp_user': '', 74 | 'snmp_password': '' 75 | } 76 | shell = shell.VirtualPrompt(config) 77 | shell.settings['color'] = False 78 | maintenance_dir = path + '/maintenance' 79 | 80 | def test_status1(self): 81 | self.shell.settings['numeric'] = True 82 | output = StringIO.StringIO() 83 | sys.stdout = output 84 | expected_result = """ 85 | Layer 4 Load balancing 86 | ====================== 87 | TCP 192.0.2.2:80 rr 88 | -> 192.0.2.200:80 Masq 1 0 0 89 | 90 | UDP 192.0.2.2:53 rr 91 | -> 192.0.2.202:53 Masq 1 0 0 92 | -> 192.0.2.203:53 Masq 1 0 0 93 | 94 | FWM 1 rr 95 | -> 192.0.2.204:0 Masq 1 0 0 96 | 97 | 98 | Disabled real servers: 99 | ---------------------- 100 | 192.0.2.201:80\t\tReason: Disabled for testing""" 101 | self.shell.onecmd(' status') 102 | result = output.getvalue() 103 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 104 | 105 | def test_status2(self): 106 | # Veirfy error checking 107 | self.assertTrue(True) 108 | 109 | def test_showvirtualtcp1(self): 110 | self.shell.settings['numeric'] = True 111 | output = StringIO.StringIO() 112 | sys.stdout = output 113 | expected_result = """ 114 | Layer 4 Load balancing 115 | ====================== 116 | TCP 192.0.2.2:80 rr 117 | -> 192.0.2.200:80 Masq 1 0 0 118 | 119 | 120 | IP Packet filter rules 121 | ====================== 122 | ACCEPT tcp -- anywhere 192.0.2.2 tcp dpt:80""" 123 | self.shell.onecmd(' show tcp 192.0.2.2 http') 124 | result = output.getvalue() 125 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 126 | 127 | def test_showvirtualtcp2(self): 128 | # TODO: Verify error checking 129 | self.assertTrue(True) 130 | 131 | def test_showvirtualudp(self): 132 | self.shell.settings['numeric'] = False 133 | output = StringIO.StringIO() 134 | sys.stdout = output 135 | expected_result = """ 136 | Layer 4 Load balancing 137 | ====================== 138 | UDP 192.0.2.2:domain rr 139 | -> 192.0.2.202:domain Masq 1 0 0 140 | -> 192.0.2.203:domain Masq 1 0 0""" 141 | self.shell.onecmd(' show udp 192.0.2.2 53') 142 | result = output.getvalue() 143 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 144 | 145 | def test_showvirtualfwm(self): 146 | self.shell.settings['numeric'] = True 147 | output = StringIO.StringIO() 148 | sys.stdout = output 149 | expected_result = """ 150 | Layer 4 Load balancing 151 | ====================== 152 | FWM 1 rr 153 | -> 192.0.2.204:0 Masq 1 0 0""" 154 | self.shell.onecmd(' show fwm 1') 155 | result = output.getvalue() 156 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 157 | 158 | # def test_showrealactive(self): 159 | # self.shell.settings['numeric'] = False 160 | # output = StringIO.StringIO() 161 | # sys.stdout = output 162 | # expected_result = """ 163 | # Active servers: 164 | # --------------- 165 | # TCP dinsdale.python.org:http 166 | # -> slashdot.org:http""" 167 | # self.shell.onecmd(' show real slashdot.org 80') 168 | # result = output.getvalue() 169 | # self.assertEqual(result.rstrip(), expected_result.rstrip()) 170 | 171 | # def test_showrealdisabled(self): 172 | # output = StringIO.StringIO() 173 | # sys.stdout = output 174 | # expected_result = """ 175 | # Disabled servers: 176 | # ----------------- 177 | # lga15s34-in-f3.1e100.net:http\t\tReason: Disabled for testing""" 178 | # self.shell.onecmd(' show real 173.194.43.3 80') 179 | # result = output.getvalue() 180 | # self.assertEqual(result.rstrip(), expected_result.rstrip()) 181 | 182 | # def test_disablereal(self): 183 | # filepath = self.maintenance_dir + '/208.67.222.222' 184 | # # this is the disabling message we'll store in the file 185 | # sys.stdin = StringIO.StringIO('disabled by test case') 186 | # self.shell.onecmd('disable 208.67.222.222') 187 | # self.assertTrue(os.path.exists(filepath)) 188 | # # now clean up the file 189 | # try: 190 | # os.unlink(filepath) 191 | # except OSError as e: 192 | # pass 193 | 194 | # def test_enablereal(self): 195 | # filepath = self.maintenance_dir + '/208.67.222.222:53' 196 | # try: 197 | # # create the file before we continue 198 | # f = open(filepath, 'w') 199 | # f.close() 200 | # self.shell.onecmd('enable 208.67.222.222 domain') 201 | # self.assertTrue(not os.path.exists(filepath)) 202 | # except IOError as e: 203 | # pass 204 | 205 | class Real(unittest.TestCase): 206 | config = {'ipvsadm': path + '/scripts/ipvsadm', 207 | 'iptables': path + '/scripts/iptables', 208 | 'pager': 'none', 209 | 'cache_dir': path + '/cache', 210 | 'director_config': path + '/etc/ldirectord.conf', 211 | 'firewall_config': path + '/etc/iptables.rules', 212 | 'director': 'ldirectord', 213 | 'director_cmd': '', 214 | 'director_bin': '', 215 | 'firewall_cmd': '', 216 | 'nodes':'', 217 | 'version_control': '', 218 | 'keepalived-mib': 'KEEPALIVED-MIB', 219 | 'snmp_community': '', 220 | 'snmp_host': '', 221 | 'snmp_user': '', 222 | 'snmp_password': '' 223 | } 224 | shell = shell.RealPrompt(config) 225 | shell.settings['color'] = False 226 | maintenance_dir = path + '/maintenance' 227 | 228 | def test_showrealactive(self): 229 | # Test 'show real' without port 230 | self.shell.settings['numeric'] = True 231 | output = StringIO.StringIO() 232 | sys.stdout = output 233 | expected_result = """ 234 | Layer 4 Load balancing 235 | ====================== 236 | 237 | Active servers: 238 | --------------- 239 | UDP 192.0.2.2:53 rr 240 | -> 192.0.2.202:53 Masq 1 0 0 241 | """ 242 | self.shell.onecmd(' show 192.0.2.202') 243 | result = output.getvalue() 244 | self.assertEqual(result.rstrip(), expected_result.rstrip()) 245 | 246 | # def test_showrealdisabled(self): 247 | # # Test 'show real' on a disabled host 248 | # self.shell.settings['numeric'] = True 249 | # output = StringIO.StringIO() 250 | # sys.stdout = output 251 | # expected_result = """ 252 | # Layer 4 Load balancing 253 | # ====================== 254 | 255 | # Disabled servers: 256 | # ----------------- 257 | # 173.194.43.3:80 Reason: Disabled for testing 258 | # """ 259 | # self.shell.onecmd(' show 173.194.43.3') 260 | # result = output.getvalue() 261 | # # self.assertEqual(result.rstrip(), expected_result.rstrip()) 262 | # self.assertTrue(True) 263 | -------------------------------------------------------------------------------- /lvsm/modules/kaparser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse a keepalived configuration file. The config specs are defined in keepalived.conf(5) 3 | """ 4 | from pyparsing import * 5 | import logging 6 | import parseactions 7 | 8 | logging.basicConfig(format='[%(levelname)s]: %(message)s') 9 | logger = logging.getLogger('keepalived') 10 | 11 | def tokenize_config(configfile): 12 | 13 | LBRACE, RBRACE = map(Suppress, "{}") 14 | 15 | # generic value types 16 | email_addr = Regex(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}") 17 | integer = Word(nums) 18 | string = Word(printables) 19 | 20 | ip4_address = Regex(r"\d{1,3}(\.\d{1,3}){3}") 21 | ip6_address = Regex(r"[0-9a-fA-F:]+") 22 | ip_address = ip4_address | ip6_address 23 | 24 | ip4_class = Regex(r"\d{1,3}(\.\d{1,3}){3}\/\d{1,3}") 25 | ip6_class = Regex(r"[0-9a-fA-F:]+\/\d{1,3}") 26 | ip_class = ip4_class | ip6_class 27 | ip_classaddr = ip_address | ip_class 28 | 29 | ip4_range = Regex(r"\d{1,3}(\.\d{1,3}){3}-\d{1,3}") 30 | # ip6_range = Regex(r"[0-9a-fA-F:]+\/\d{1,3}-\d{1,3}") 31 | # ip_range = ip6_range | ip4_range 32 | 33 | scope = oneOf("site link host nowhere global") 34 | 35 | # global params 36 | notification_emails = Dict(Group("notification_email" + 37 | LBRACE + 38 | OneOrMore(email_addr) + 39 | RBRACE)) 40 | notification_email_from = Dict(Group("notification_email_from" + email_addr)) 41 | smtp_server = Dict(Group("smtp_server" + ip_address)) 42 | smtp_connect_timeout = Dict(Group("smtp_connect_timeout" + integer)) 43 | router_id = Dict(Group("router_id" + string)) 44 | 45 | global_params = (notification_emails | notification_email_from | 46 | smtp_server | smtp_connect_timeout | router_id) 47 | 48 | # Parameters to "ip addr add" command 49 | ipaddr_cmd = (ip_classaddr + Optional("dev" + Word(alphanums)) + 50 | Optional("scope" + scope) + Optional("label" + string)) 51 | 52 | # Params for ip routes 53 | ip_route = ("src" + ip_address + Optional("to") + ip_classaddr + 54 | oneOf("via gw") + ip_address + "dev" + Word(alphanums) + 55 | "scope" + scope + "table" + Word(alphanums)) 56 | black_hole = ("black_hole" + ip_classaddr) 57 | 58 | route_params = (ip_route | black_hole) 59 | 60 | string_or_quoted = (printables | quotedString) 61 | 62 | 63 | # vrrp_script params 64 | vrrp_scr_name = ("script" + quotedString) 65 | vrrp_scr_interval = ("interval" + integer) 66 | vrrp_scr_weight = ("weight" + integer) 67 | vrrp_scr_fall = ("fall" + integer) 68 | vrrp_scr_rise = ("rise" + integer) 69 | 70 | vrrp_scr_params = (vrrp_scr_name | vrrp_scr_interval | vrrp_scr_weight | 71 | vrrp_scr_fall | vrrp_scr_rise) 72 | 73 | # vrrp_sync_params 74 | sync_group = ("group" + LBRACE + OneOrMore(Word(printables)) + RBRACE) 75 | notify_master = ("notify_master" + string_or_quoted) 76 | notify_backup = ("notify_backup" + string_or_quoted) 77 | notify_fault = ("notify_fault" + string_or_quoted) 78 | notify = ("notify" + string_or_quoted) 79 | smtp_alert = ("smtp_alert") 80 | 81 | vrrp_sync_params = (sync_group | notify_fault | notify_backup | 82 | notify_master | notify | smtp_alert) 83 | 84 | # vrrp_instance_params 85 | use_vmac = ("use_vmac") 86 | native_ipv6 = ("native_ipv6") 87 | state = Dict(Group("state" + oneOf("MASTER BACKUP"))) 88 | interface = Dict(Group("interface" + Word(alphanums))) 89 | track_interface = Dict(Group("track_interface" + LBRACE + OneOrMore(Word(alphanums)) + RBRACE)) 90 | track_script = Dict(Group("track_script" + LBRACE + OneOrMore(Word(alphanums)) + RBRACE)) 91 | dont_track_primary = ("dont_track_primary") 92 | mcast_src_ip = Dict(Group("mcast_src_ip" + ip_address)) 93 | unicast_peer = Dict(Group("unicast_peer" + LBRACE + OneOrMore(ip_address) + RBRACE)) 94 | lvs_sync_daemon = Dict(Group("lvs_sync_daemon_interface" + Word(alphanums))) 95 | garp_master_delay = Dict(Group("garp_master_delay" + integer)) 96 | virtual_router_id = Dict(Group("virtual_router_id" + integer)) 97 | priority = Dict(Group("priority" + integer)) 98 | advert_int = Dict(Group("advert_int" + integer)) 99 | authentication = Dict(Group("authentication" + 100 | LBRACE + 101 | "auth_type" + oneOf("PASS AH") + 102 | "auth_pass" + string + 103 | RBRACE)) 104 | nopreempt = ("nopreempt") 105 | preempt_delay = Dict(Group("preempt_delay" + integer)) 106 | debug = ("debug") 107 | virtual_ipaddress = Dict(Group("virtual_ipaddress" + 108 | LBRACE + 109 | OneOrMore(ipaddr_cmd) + 110 | RBRACE)) 111 | virtual_ipaddress_excluded = Dict(Group("virtual_ipaddress_excluded" + 112 | LBRACE + 113 | OneOrMore(ipaddr_cmd) + 114 | RBRACE)) 115 | virtual_routes = Dict(Group("virtual_routes" + 116 | LBRACE + 117 | OneOrMore(ip_route) + 118 | RBRACE)) 119 | 120 | vrrp_instance_params = (state | interface | 121 | track_interface | track_script | 122 | use_vmac | native_ipv6 | dont_track_primary | 123 | nopreempt | debug | preempt_delay | 124 | mcast_src_ip | unicast_peer | lvs_sync_daemon | 125 | garp_master_delay | virtual_router_id | priority | 126 | virtual_ipaddress | virtual_ipaddress_excluded | 127 | virtual_routes | advert_int | authentication | 128 | notify_master | notify_backup | notify_fault | 129 | notify | smtp_alert) 130 | 131 | # virtual_server_group params 132 | vip_vport = (ip_address + integer) 133 | vip_range_vport = (ip4_range + integer) 134 | fwmark = ("fwmark" + integer) 135 | 136 | vserver_group_params = (vip_vport | vip_range_vport | fwmark) 137 | 138 | # virtual_server params 139 | vserver_group = ("group" + string) 140 | vserver_vip_vport = (ip_address + integer) 141 | vserver_fwm = ("fwmark" + integer) 142 | vserver_id = (vserver_group | vserver_vip_vport | vserver_fwm) 143 | 144 | delay_loop = ("delay_loop" + integer) 145 | lb_algo = ("lb_algo" + oneOf("rr wrr lc wlc lblc sh dh")) 146 | lb_kind = ("lb_kind" + oneOf("NAT DR TUN")) 147 | persistence_timeout = ("persistence_timeout" + integer) 148 | persistence_granularity = ("persistence_granularity" + string) # TODO: this should be a netmask 149 | protocol = ("protocol" + oneOf("TCP UDP FWM")) 150 | ha_suspend = ("ha_suspend") 151 | virtual_host = ("virtual_host" + string) 152 | alpha = ("alpha") 153 | omega = ("omega") 154 | quorom = ("quorom" + integer) 155 | hysteresis = ("hysteresis" + integer) 156 | quorom_up = ("quorom_up" + (string | quotedString)) 157 | quorom_down = ("quorom_down" + (string | quotedString)) 158 | sorry_server = ("sorry_server" + ip_address + integer) 159 | 160 | vserver_params = (delay_loop | persistence_timeout | ha_suspend | 161 | persistence_granularity | virtual_host | alpha | 162 | omega | quorom | hysteresis | quorom_up | quorom_down | 163 | sorry_server) 164 | 165 | # real_server_params section 166 | weight = ("weight" + integer) 167 | inhibit_on_failure = ("inhibit_on_failure") 168 | notify_up = ("notify_up" + (string | quotedString)) 169 | notify_down = ("notify_down" + (string | quotedString)) 170 | 171 | # http_check_params 172 | path = ("path" + string) 173 | digest = ("digest" + Word(hexnums)) 174 | status_code = ("status_code" + integer) 175 | 176 | url_params = (path | status_code | digest) 177 | 178 | url_check = Group("url" + 179 | LBRACE + 180 | OneOrMore(url_params) + 181 | RBRACE) 182 | 183 | connect_port = ("connect_port" + integer) 184 | bindto = ("bindto" + ip_address) 185 | connect_timeout = ("connect_timeout" + integer) 186 | nb_get_retry = ("nb_get_retry" + integer) 187 | delay_before_retry = ("delay_before_retry" + integer) 188 | 189 | http_check_params = (connect_port | connect_timeout | bindto | 190 | nb_get_retry | delay_before_retry) 191 | 192 | http_check = Group(oneOf("HTTP_GET SSL_GET") + 193 | LBRACE + 194 | OneOrMore(url_check) & 195 | ZeroOrMore(http_check_params) & 196 | RBRACE) 197 | 198 | tcp_check = Group("TCP_CHECK" + 199 | LBRACE + 200 | OneOrMore(connect_port | connect_timeout | bindto) + 201 | RBRACE) 202 | 203 | # smtp_check params 204 | connect_ip = ("connect_ip" + ip_address) 205 | smtp_host = Group("host" + 206 | LBRACE + 207 | connect_ip + 208 | connect_port + 209 | bindto + 210 | RBRACE) 211 | retry = Dict(Group("retry" + integer)) 212 | helo_name = Dict(Group("helo_name" + string_or_quoted)) 213 | smtp_check_params = (connect_timeout | retry | delay_before_retry | helo_name ) 214 | 215 | smtp_check = Dict(Group("SMTP_CHECK" + 216 | LBRACE + 217 | Optional(smtp_host) + 218 | ZeroOrMore(smtp_check_params) + 219 | RBRACE)) 220 | 221 | # misc_check params 222 | misc_path = Dict(Group("misc_path" + string_or_quoted)) 223 | misc_timeout = Dict(Group("misc_timeout" + integer)) 224 | misc_dynamic = ("misc_dynamic") 225 | 226 | misc_check = Dict(Group("MISC_CHECK" + 227 | LBRACE + 228 | misc_path + 229 | ZeroOrMore(misc_timeout | misc_dynamic) + 230 | RBRACE)) 231 | 232 | check_type = (http_check | tcp_check | smtp_check | misc_check) 233 | 234 | real_server_params = (weight | inhibit_on_failure | notify_up | notify_down | check_type) 235 | # Real server block 236 | real_server = Dict(Group("real_server" + ip_address + integer + 237 | LBRACE + 238 | OneOrMore(real_server_params) + 239 | RBRACE 240 | )) 241 | 242 | 243 | # Major blocks in the keepalived config 244 | 245 | global_defs = Dict(Group("global_defs" + 246 | LBRACE + 247 | OneOrMore(global_params) + 248 | RBRACE)) 249 | static_ipaddress = Dict(Group("static_ipaddress" + 250 | LBRACE + 251 | OneOrMore(ipaddr_cmd) + 252 | RBRACE)) 253 | static_routes = Dict(Group("static_routes" + 254 | LBRACE + 255 | OneOrMore(route_params) + 256 | RBRACE)) 257 | vrrp_script = Dict(Group("vrrp_script" + 258 | LBRACE + 259 | OneOrMore(vrrp_scr_params) + 260 | RBRACE)) 261 | 262 | vrrp_sync_group = Dict(Group("vrrp_sync_group" + string + 263 | LBRACE + 264 | OneOrMore(vrrp_sync_params) + 265 | RBRACE)) 266 | 267 | vrrp_instance = Dict(Group("vrrp_instance" + string + 268 | LBRACE + 269 | OneOrMore(vrrp_instance_params) + 270 | RBRACE)) 271 | 272 | virtual_server_group = Dict(Group("virtual_server_group" + string + 273 | LBRACE + 274 | OneOrMore(vserver_group_params) + 275 | RBRACE)) 276 | 277 | virtual_server = Dict(Group("virtual_server" + vserver_id + 278 | LBRACE & 279 | # Each([lb_algo, lb_kind, protocol, ZeroOrMore(vserver_params), OneOrMore(real_server) ]) + 280 | lb_algo & 281 | lb_kind & 282 | protocol & 283 | ZeroOrMore(vserver_params) & 284 | OneOrMore(real_server) & 285 | RBRACE 286 | )) 287 | 288 | comment = oneOf("# !") + restOfLine 289 | 290 | allconfig = (global_defs & 291 | ZeroOrMore(static_ipaddress | static_routes | vrrp_instance | 292 | vrrp_script | vrrp_sync_group | 293 | virtual_server_group | virtual_server) 294 | ) 295 | allconfig.ignore(comment) 296 | 297 | try: 298 | tokens = allconfig.parseString(configfile) 299 | except ParseException as e: 300 | logger.error("Exception") 301 | logger.error(e) 302 | except ParseFatalException as e: 303 | logger.error("FatalException") 304 | logger.error(e) 305 | else: 306 | return tokens 307 | 308 | def main(): 309 | import sys 310 | import argparse 311 | 312 | parser = argparse.ArgumentParser(description=__doc__, 313 | usage="%(prog)s [options] filename") 314 | 315 | parser.add_argument("-q", "--quiet", 316 | help="Quiet mode. Return 0 on success, 1 on failure.", 317 | action="store_true") 318 | parser.add_argument("-v", "--verbose", 319 | help="Verbose mode. Print all tokens on success.", 320 | action="store_true") 321 | parser.add_argument("file", type=argparse.FileType('r')) 322 | 323 | try: 324 | args = parser.parse_args() 325 | except IOError as e: 326 | print e 327 | sys.exit(2) 328 | 329 | try: 330 | conf = "".join(args.file.readlines()) 331 | except IOError as e: 332 | print "%s" % e 333 | sys.exit(1) 334 | 335 | t = tokenize_config(conf) 336 | 337 | if t: 338 | if args.verbose: 339 | print t.dump() 340 | print "---------" 341 | if not args.quiet: 342 | print "%s parsed OK!" % args.file.name 343 | sys.exit(0) 344 | else: 345 | if not args.quiet: 346 | print "%s didn't parse OK!" % args.file.name 347 | sys.exit(1) 348 | 349 | if __name__ == "__main__": 350 | main() 351 | -------------------------------------------------------------------------------- /lvsm/genericdirector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Director specific funcationality 3 | """ 4 | import socket 5 | import subprocess 6 | import utils 7 | import termcolor 8 | import logging 9 | 10 | logger = logging.getLogger('lvsm') 11 | 12 | class Server(): 13 | def __init__(self, ip, port): 14 | self.ip = ip 15 | self.port = port 16 | 17 | 18 | class Virtual(Server): 19 | def __init__(self, proto, ip, port, sched, persistence=None): 20 | Server.__init__(self, ip, port) 21 | self.proto = proto 22 | self.realServers = list() 23 | self.sched = sched 24 | self.persistence = persistence 25 | 26 | def __str__(self, numeric=True, color=False, real=None, port=None): 27 | """provide an easy way to print this object""" 28 | proto = self.proto.upper().ljust(4) 29 | host = self.ip 30 | service = self.port 31 | 32 | logger.debug("Virtual:__str__ proto=%s,host=%s,service=%s" % (proto, host, service)) 33 | 34 | if self.proto.upper() == 'FWM': 35 | pass 36 | elif not numeric: 37 | try: 38 | try: 39 | host, aliaslist, addrlist = socket.gethostbyaddr(self.ip) 40 | except socket.herror: 41 | pass 42 | service = socket.getservbyport(int(self.port)) 43 | except socket.error: 44 | pass 45 | if self.proto.upper() == 'FWM': 46 | ipport = host.ljust(40) 47 | else: 48 | ipport = (host + ":" + service).ljust(40) 49 | 50 | sched = self.sched.ljust(7) 51 | 52 | if self.persistence: 53 | line = "%s %s %s persistence %s" % (proto, ipport, sched, self.persistence) 54 | else: 55 | line = "%s %s %s" % (proto, ipport, sched) 56 | 57 | if color: 58 | line = termcolor.colored(line, attrs=['bold']) 59 | 60 | output = [line] 61 | for r in self.realServers: 62 | if real: 63 | if r.ip == real: 64 | if port: 65 | if int(r.port) == port: 66 | output = [line] 67 | output.append(r.__str__(numeric,color)) 68 | else: 69 | output = [line] 70 | output.append(r.__str__(numeric,color)) 71 | 72 | else: 73 | output.append(r.__str__(numeric, color)) 74 | 75 | # If a real server is provided, don't return empty VIPs 76 | if real: 77 | if len(output) == 1: 78 | return '' 79 | 80 | # Add space between each VIP in the final output 81 | if output: 82 | output.append('') 83 | 84 | return '\n'.join(output) 85 | 86 | # # line = "IP: %s Port: %s Protocol: %s Scheduler: %s" % (host, service, self.proto, self.sched) 87 | # header = "Protocol IP:port Scheduler Flags" 88 | # hr = "-------- --------------------------------------- --------- --------" 89 | # proto = self.proto.upper().ljust(8) 90 | # ipport = (host + ":" + service).ljust(39) 91 | # line = "%s %s %s" % (proto, ipport, self.sched) 92 | # if color: 93 | # line = termcolor.colored(line, attrs=['bold']) 94 | 95 | # output = [header, hr, line] 96 | # header = "Label IP:port Method Weight ActiveConn InactiveConn" 97 | # hr = "-------- --------------------------------------- ------- ------ ---------- ------------" 98 | # output.append('') 99 | # output.append(header) 100 | # output.append(hr) 101 | # for r in self.realServers: 102 | # output.append(r.__str__(numeric,color)) 103 | # output.append('') 104 | # output.append('') 105 | # return '\n'.join(output) 106 | 107 | 108 | class Real(Server): 109 | def __init__(self, ip, port, weight, method, active, inactive): 110 | Server.__init__(self, ip,port) 111 | self.weight = weight 112 | self.method = method 113 | self.active = active 114 | self.inactive = inactive 115 | 116 | def __str__(self, numeric=False, color=False): 117 | host = self.ip 118 | service = self.port 119 | if not numeric: 120 | try: 121 | host, aliaslist, addrlist = socket.gethostbyaddr(self.ip) 122 | except socket.herror: 123 | pass 124 | try: 125 | service = socket.getservbyport(int(self.port)) 126 | except socket.error: 127 | pass 128 | ipport = (host + ":" + service).ljust(40) 129 | method = self.method.ljust(7) 130 | weight = self.weight.ljust(6) 131 | active = self.active.ljust(10) 132 | inactive = self.inactive.ljust(10) 133 | line = " -> %s %s %s %s %s" % (ipport, method, weight, active, inactive) 134 | # line = " %s %s %s %s %s" % (ipport, method, weight, active, inactive) 135 | # if real server weight is zero, we highlight it as red 136 | if color and self.weight == '0': 137 | line = termcolor.colored(line, 'red') 138 | return line 139 | 140 | 141 | class GenericDirector(object): 142 | """ 143 | Generic class that knows about ipvsadm. If director isn't defined, this 144 | is the fallback. Should be inherited by classes implementing specific 145 | director funcationality. 146 | """ 147 | def __init__(self, ipvsadm, configfile='', restart_cmd='', nodes='', args=dict()): 148 | self.ipvsadm = ipvsadm 149 | self.configfile = configfile 150 | self.restart_cmd = restart_cmd 151 | if nodes != '': 152 | self.nodes = nodes.replace(' ', '').split(',') 153 | else: 154 | self.nodes = None 155 | self.hostname = socket.gethostname() 156 | # this variable will hold the full IPVS table 157 | self.virtuals = list() 158 | 159 | def build_ipvs(self): 160 | """Build a model fo the running ipvsadm table internally""" 161 | args = [self.ipvsadm, '-L', '-n'] 162 | 163 | try: 164 | output = utils.check_output(args) 165 | except OSError as e: 166 | logger.error("Problem with ipvsadm - %s" % e.strerror) 167 | return False 168 | except subprocess.CalledProcessErrror as e: 169 | logger.error("Problem with ipvsadm - %s" % e.output) 170 | return False 171 | 172 | # Clear out the old virtual table 173 | self.virtuals = list() 174 | # Break up the output and generate VIP and RIPs from it 175 | # Assumption is that the first 3 lines of the ipvsadm output 176 | # are just informational so we skip them 177 | for line in output.split('\n')[3:]: 178 | if (line.startswith('TCP') or 179 | line.startswith('UDP') or 180 | line.startswith('FWM')): 181 | 182 | # break the virtual line into tokens. There should only be 3 183 | tokens = line.split() 184 | # first one is the protocol 185 | proto = tokens[0] 186 | if line.startswith('FWM'): 187 | # there's no port number in fwm mode 188 | ip = tokens[1] 189 | port = '' 190 | else: 191 | # second token will be ip:port 192 | ip, sep, port = tokens[1].rpartition(':') 193 | # 3rd is the scheduler 194 | sched = tokens[2] 195 | # [optional] 5th is the persistence timeout 196 | if len(tokens) == 5: 197 | persistence = tokens[4] 198 | else: 199 | persistence = None 200 | 201 | v = Virtual(proto, ip, port, sched, persistence) 202 | self.virtuals.append(v) 203 | # If the line doesn't begin with the above values, it is realserver 204 | else: 205 | # The reals are always added to the last vip 206 | if len(self.virtuals) > 0: 207 | tokens = line.split() 208 | if len(tokens) == 6: 209 | ip, sep, port = tokens[1].rpartition(':') 210 | method = tokens[2] 211 | weight = tokens[3] 212 | active = tokens[4] 213 | inactive = tokens[5] 214 | v = self.virtuals[-1] 215 | r = Real(ip, port, weight, method, active, inactive) 216 | v.realServers.append(r) 217 | 218 | def disable(self, host, port='', reason=''): 219 | """ 220 | Disable a previously Enabled server. 221 | To be implemented by inheriting classes 222 | """ 223 | logger.error("Disable not implemented for 'generic' director") 224 | return False 225 | 226 | def enable(self, host, port=''): 227 | """ 228 | Enable a previously disabled server. 229 | To be implemented by inheriting classes 230 | """ 231 | logger.error("enable not implemented for 'generic' director") 232 | return False 233 | 234 | def show(self, numeric, color): 235 | # Call ipvsadm and do the color highlighting. 236 | result = ["", "Layer 4 Load balancing"] 237 | result += ["======================"] 238 | result += self.show_running(numeric, color) 239 | 240 | # Show a list of disabled real servers. 241 | disabled = self.show_real_disabled('', '', numeric) 242 | if disabled: 243 | header = ["", "Disabled real servers:", "----------------------"] 244 | disabled = header + disabled 245 | else: 246 | disabled = list() 247 | 248 | return result + disabled + [''] 249 | 250 | def show_running(self, numeric, color): 251 | """ 252 | Show the running status of IPVS. Basically runs "ipvsadm". 253 | """ 254 | # Create the IPVS table in memory 255 | self.build_ipvs() 256 | result = list() 257 | for v in self.virtuals: 258 | result += v.__str__(numeric, color).split('\n') 259 | 260 | return result 261 | 262 | def show_virtual(self, host, port, proto, numeric, color): 263 | """Show status of virtual server. 264 | """ 265 | 266 | logger.debug("GenericDirector:show_virtual Parameters: Host %s Port %s Proto %s" % (host, port, proto)) 267 | 268 | # if the protocol is FWM, don't convert the "host" IP 269 | if proto.upper() == "FWM": 270 | hostips = ['1'] 271 | else: 272 | # make sure we have a valid host 273 | hostips = utils.gethostbyname_ex(host) 274 | if not hostips: 275 | logger.debug("GenericDirector:show_virtual invalid host IP") 276 | return list() 277 | 278 | # make sure the port is valid 279 | if port: 280 | portnum = utils.getportnum(port) 281 | if portnum == -1: 282 | logger.debug("GenericDirector:show_virtual invalid port number") 283 | return list() 284 | 285 | # Update the ipvs table 286 | self.build_ipvs() 287 | 288 | result = ["", "Layer 4 Load balancing"] 289 | result += ["======================"] 290 | for v in self.virtuals: 291 | if v.proto == proto.upper() and v.ip in hostips: 292 | if not port or v.port == str(portnum): 293 | result += v.__str__(numeric, color).split('\n') 294 | 295 | return result 296 | 297 | def show_real(self, host, port, numeric, color): 298 | """Show status of a real server across multiple VIPs. 299 | Will consider both active and disabled servers. 300 | """ 301 | header = ["", "Layer 4 Load balancing" , "======================"] 302 | output = header 303 | 304 | active = self.show_real_active(host, port, numeric, color) 305 | if active: 306 | active = ["", "Active servers:", "---------------"] + active 307 | output = output + active 308 | 309 | disabled = self.show_real_disabled(host, port, numeric) 310 | if disabled: 311 | disabled = ["", "Disabled servers:", "-----------------"] + disabled 312 | output = output + disabled 313 | 314 | # return header + active + disabled + ["\n"] 315 | return output 316 | 317 | def show_real_active(self, host, port, numeric, color): 318 | """Show status of an active real server across multiple VIPs. 319 | """ 320 | # make sure we have a valid host 321 | hostips = utils.gethostbyname_ex(host) 322 | 323 | if not hostips: 324 | return list() 325 | 326 | # If more than one ip is returned for a host. Use the first one 327 | hostip = hostips[0] 328 | # If port is defined verify that it's a valid number 329 | if port: 330 | portnum = utils.getportnum(port) 331 | if portnum == -1: 332 | return list() 333 | else: 334 | portnum = None 335 | 336 | # Update the ipvs table 337 | self.build_ipvs() 338 | 339 | result = list() 340 | 341 | for v in self.virtuals: 342 | # for real in v.realServers: 343 | # if real.ip == hostip: 344 | # logger.debug("real port type: %s" % type(real.port)) 345 | # logger.debug("port num type: %s" % type(portnum)) 346 | # if not port or real.port == portnum: 347 | # result += v.__str__(numeric, color, real.ip, port).split('\n') 348 | # result += v.__str__(numeric, color, hostip, portnum).split('\n') 349 | r = v.__str__(numeric, color, hostip, portnum) 350 | if r: 351 | result += r.split('\n') 352 | return result 353 | 354 | def show_real_disabled(self, host, port, numeric): 355 | """ 356 | Show status of disabled real server across multiple VIPs. 357 | To be implemented by inheriting classes. 358 | Return value must be a list 359 | """ 360 | return list() 361 | 362 | def convert_filename(self, filename): 363 | """ 364 | Convert a filename of format host[:port] to IP[:port] 365 | Assumption is that for hosts with more than one IP, 366 | the first IP in the list is used. 367 | """ 368 | values = filename.split(':') 369 | portnum = -1 370 | if not values: 371 | return '' 372 | hostips = utils.gethostbyname_ex(values[0]) 373 | if len(values) == 2: 374 | portnum = utils.getportnum(values[1]) 375 | if portnum > -1: 376 | return hostips[0] + ':' + str(portnum) 377 | else: 378 | return hostips[0] 379 | 380 | def check_real(self, host, port): 381 | """Check a host/port to see if it's in the realserver list.""" 382 | # useful with show_real command 383 | pass 384 | 385 | def restart(self): 386 | """Restart the director.""" 387 | if self.restart_cmd: 388 | print "restaring director" 389 | try: 390 | result = subprocess.call(self.restart_cmd, shell=True) 391 | except OSError as e: 392 | print "[ERROR] problem restaring director - " + e.strerror 393 | else: 394 | print "[ERROR] 'director_cmd' not defined in config!" 395 | 396 | def parse_config(self, configfile): 397 | """Parse config file, and syntax check. 398 | Returns True on success, False on failure. 399 | To be implemented by inheriting classes. 400 | """ 401 | return True 402 | 403 | def get_virtual(self, protocol): 404 | """return a list of the virtual servers by protocol. 405 | Used for autocomplete mode in the shell. 406 | """ 407 | args = [self.ipvsadm, '-L'] 408 | result = list() 409 | try: 410 | output = utils.check_output(args, silent=True) 411 | except OSError as e: 412 | logger.error(" %s" % e.strerror) 413 | return result 414 | lines = output.splitlines() 415 | for line in lines: 416 | if line.startswith(protocol.upper()): 417 | r, sep, temp = line.partition(':') 418 | result.append(r[5:]) 419 | 420 | return result 421 | 422 | def get_real(self, protocol): 423 | """return a list of all real servers. 424 | Used for autocomplete mode in the shell.""" 425 | 426 | args = [self.ipvsadm, '-L'] 427 | result = list() 428 | prot = '' 429 | try: 430 | output = utils.check_output(args, silent=True) 431 | except OSError as e: 432 | logger.error(" %s" % e.strerror) 433 | return result 434 | 435 | lines = output.splitlines() 436 | 437 | for line in lines[3:]: 438 | if line[0:3] in ['TCP', 'UDP', 'FWM']: 439 | prot = line[0:3] 440 | elif (line.startswith(" ->") 441 | and (not protocol or protocol.upper() == prot)): 442 | r, sep , temp = line.partition(':') 443 | real = r[5:] 444 | if real not in result: 445 | result.append(real) 446 | return result 447 | 448 | def filesync_nodes(self, op, filename): 449 | """ 450 | Sync a file between nodes in the cluste. 451 | op has to be one of 'remove' or 'copy'. 452 | filename is the name of the file to be copied/removed 453 | The method return True/False 454 | """ 455 | if self.nodes is not None: 456 | for node in self.nodes: 457 | if node != self.hostname: 458 | 459 | # Assumption is we only need to remotely remove a file 460 | # Or copy a file to a remote location 461 | if op == 'remove': 462 | args = ['ssh', node, 'rm', filename] 463 | elif op == 'copy': 464 | remote = node + ":" + filename 465 | args = ['scp', filename, remote] 466 | else: 467 | logger.error('Unknown operation \'%s\' in filesync method!' % op) 468 | return False 469 | 470 | logger.debug('Running command : %s' % (' '.join(args))) 471 | try: 472 | utils.check_output(args) 473 | except OSError as e: 474 | logger.error("Unable to sync state file to %s" % node) 475 | logger.error(e) 476 | return False 477 | except subprocess.CalledProcessError as e: 478 | logger.error("Unable to sync state file to %s" % node) 479 | logger.error(e) 480 | return False 481 | return True 482 | -------------------------------------------------------------------------------- /lvsm/modules/keepalived.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import socket 4 | from lvsm import genericdirector, utils 5 | from lvsm.modules import kaparser 6 | 7 | logger = logging.getLogger('lvsm') 8 | 9 | # needed for testing the code on non-Linux platforms 10 | try: 11 | from snimpy import manager 12 | from snimpy import mib 13 | from snimpy import snmp 14 | except ImportError: 15 | logger.warn("Python module 'snimpy' not found, loading a dummy module.") 16 | logger.warn("'enable' and 'disable' commands will not be availble.""") 17 | from lvsm.snimpy_dummy import manager 18 | from lvsm.snimpy_dummy import mib 19 | from lvsm.snimpy_dummy import snmp 20 | 21 | class Keepalived(genericdirector.GenericDirector): 22 | """ 23 | Implements Keepalived specific functions. Stub for now. 24 | """ 25 | def __init__(self, ipvsadm, configfile='', restart_cmd='', nodes='', args=dict()): 26 | super(Keepalived, self).__init__(ipvsadm, 27 | configfile, 28 | restart_cmd, 29 | nodes, 30 | args) 31 | # Now handle args 32 | self.mib = args['keepalived-mib'] 33 | self.snmp_community = args['snmp_community'] 34 | self.snmp_host = args['snmp_host'] 35 | if args['snmp_user']: 36 | self.snmp_user = args['snmp_user'] 37 | else: 38 | self.snmp_user = None 39 | 40 | if args['snmp_password']: 41 | self.snmp_password = args['snmp_password'] 42 | else: 43 | self.snmp_password = None 44 | 45 | self.cache_dir = args['cache_dir'] 46 | 47 | def rmfile(self, filepath): 48 | """ 49 | A safe way to remove files in keepalived 50 | """ 51 | try: 52 | logger.debug("Keeplived.rmfile(): removing file %s" % filepath) 53 | os.unlink(filepath) 54 | except OSError as e: 55 | logger.error(e) 56 | logger.error('Please make sure %s is writable!' % self.cache_dir) 57 | logger.error('%s needs to be manually deleted to avoid future problems.' % filepath) 58 | 59 | def disable(self, protocol, host, port='', vhost='', vport='', reason=''): 60 | """ 61 | Disable a real server in keepalived. This command rellies on snimpy 62 | and will set the weight of the real server to 0. 63 | The reason is not used in this case. 64 | """ 65 | found = False 66 | 67 | hostips = utils.gethostbyname_ex(host) 68 | if not hostips: 69 | logger.error('Real server %s is not valid!' % host) 70 | return False 71 | 72 | # Here we only use the first IP if the host has more than one 73 | hostip = hostips[0] 74 | if port: 75 | # check that it's a valid port 76 | portnum = utils.getportnum(port) 77 | if portnum == -1: 78 | logger.error('Port %s is not valid!' % port) 79 | return False 80 | 81 | if vhost: 82 | vipnums = utils.gethostbyname_ex(vhost) 83 | if not vipnums: 84 | logger.error('Virtual host %s not valid!' % vhost) 85 | return False 86 | 87 | # only take the first ip address if host has more than one 88 | vipnum = vipnums[0] 89 | 90 | if vport: 91 | vportnum = utils.getportnum(vport) 92 | if vportnum == -1: 93 | logger.error('Virtual port %s is not valid!' % vport) 94 | return False 95 | 96 | try: 97 | manager.load(self.mib) 98 | m = manager.Manager(self.snmp_host, self.snmp_community) 99 | # Not compatible with earlier 100 | # versions of snimpy 101 | #secname=self.snmp_user, 102 | #authpassword=self.snmp_password) 103 | except (snmp.SNMPException, mib.SMIException) as e: 104 | logger.error(e) 105 | logger.error("Unable to perfrom action!") 106 | return False 107 | 108 | # iterate through the virtual servers 109 | # and disable the matching real server 110 | try: 111 | for i in m.virtualServerAddress: 112 | hexip = m.virtualServerAddress[i] 113 | vip = socket.inet_ntoa(hexip) 114 | logger.debug("Keepalived.disable(): Checking VIP: %s" % vip) 115 | logger.debug("Keepalived.disable(): Protocol: %s" % str(m.virtualServerProtocol[i])) 116 | if m.virtualServerProtocol[i] == protocol: 117 | if not vhost or vipnum == vip: 118 | vp = m.virtualServerPort[i] 119 | if not vport or vportnum == vp: 120 | # iterate over the realservers in 121 | # the specific virtual 122 | j = m.virtualServerRealServersTotal[i] 123 | idx = 1 124 | while idx <= j: 125 | hexip = m.realServerAddress[i,idx] 126 | rip = socket.inet_ntoa(hexip) 127 | rp = m.realServerPort[i,idx] 128 | if hostip == rip: 129 | if not port or (port and portnum == rp): 130 | logger.debug('Keepalived.disable(): Disabling %s:%s on VIP %s:%s' % (rip, rp, vip, vp)) 131 | # 'found' is used to keep track of 132 | # matching real servers to disable 133 | found = True 134 | 135 | # Record the original weight 136 | # before disabling it 137 | # It'll be used when enabling 138 | weight = m.realServerWeight[i,idx] 139 | logger.debug('Keepalived.disable(): Current weight: %s' % weight) 140 | 141 | if weight == 0: 142 | logger.warning("Real server %s:%s is already disabled on VIP %s:%s" % (rip, rp, vip, vp)) 143 | idx += 1 144 | continue 145 | 146 | filename = "realServerWeight.%s.%s" % (i, idx) 147 | fullpath = '%s/%s' % (self.cache_dir, filename) 148 | rfilename = "realServerReason.%s.%s" % (i, idx) 149 | rfullpath = '%s/%s' % (self.cache_dir, rfilename) 150 | try: 151 | # Create a file with the original weight 152 | logger.info('Creating file: %s' % fullpath) 153 | f = open(fullpath, 'w') 154 | f.write(str(weight)) 155 | f.close() 156 | 157 | # Create a file with the disable reason 158 | logger.info('Creating file: %s' % rfullpath) 159 | f = open(rfullpath, 'w') 160 | f.write(str(reason)) 161 | f.close() 162 | except IOError as e: 163 | logger.error(e) 164 | logger.error('Please make sure %s is writable before proceeding!' % self.cache_dir) 165 | return False 166 | 167 | # Copy the file to the other nodes 168 | # In case of a switch lvsm will have 169 | # the weight info on all nodes 170 | self.filesync_nodes('copy', fullpath) 171 | 172 | # set the weight to zero 173 | community = "private" 174 | cmd_example = "snmpset -v2c -c %s localhost KEEPALIVED-MIB::%s = 0" % (community, filename) 175 | logger.info("Running equivalent command to: %s" % cmd_example) 176 | m.realServerWeight[i,idx] = 0 177 | print "Disabled %s:%s on VIP %s:%s (%s). Weight set to 0." % (rip, rp, vip, vp, protocol) 178 | idx += 1 179 | except snmp.SNMPException as e: 180 | logger.error(e) 181 | logger.error("Unable to complete the command successfully! Please verify manually.") 182 | return False 183 | 184 | if not found: 185 | logger.error('No matching real servers were found!') 186 | return False 187 | else: 188 | return True 189 | 190 | def enable(self, protocol, rhost, rport='',vhost='', vport=''): 191 | """ 192 | Enable a real server in keepalived. This command rellies on snimpy 193 | and will set the weight of the real server back to its original weight. 194 | Assumption: original weight is stored in self.cache_dir/realServerWeight.x.y 195 | The reason is not used in this case. 196 | """ 197 | 198 | hostips = utils.gethostbyname_ex(rhost) 199 | if not hostips: 200 | logger.error('Real server %s is not valid!' % rhost) 201 | return False 202 | 203 | # Here we only use the first IP if the host has more than one 204 | hostip = hostips[0] 205 | 206 | if rport: 207 | # check that it's a valid port 208 | portnum = utils.getportnum(rport) 209 | if portnum == -1: 210 | logger.error('Port %s is not valid!' % rport) 211 | return False 212 | 213 | if vhost: 214 | vipnum = utils.gethostname(vhost) 215 | if not vipnum: 216 | logger.error('Virtual host %s not valid!' % vhost) 217 | return False 218 | 219 | if vport: 220 | vportnum = utils.getportnum(vport) 221 | if vportnum == -1: 222 | logger.error('Virtual port %s is not valid!' % vport) 223 | return False 224 | 225 | try: 226 | manager.load(self.mib) 227 | m = manager.Manager(self.snmp_host, self.snmp_community) 228 | # Not compatible with earlier 229 | # versions of snimpy 230 | # secname=self.snmp_user, 231 | # authpassword=self.snmp_password) 232 | except (snmp.SNMPException, mib.SMIException) as e: 233 | logger.error(e) 234 | logger.error("Unable to perfrom action!") 235 | return False 236 | 237 | # iterate through the virtual servers 238 | # and enable the matching real server 239 | # if the weight is zero. 240 | # Note: if file is not found in the cache_dir (i.e. /var/cache/lvsm) 241 | # we set the weight 1 (keepalived default) 242 | try: 243 | for i in m.virtualServerAddress: 244 | hexip = m.virtualServerAddress[i] 245 | vip = socket.inet_ntoa(hexip) 246 | logger.debug("Keepalived.enable(): Checking VIP: %s" % vip) 247 | logger.debug("Keepalived.enable(): Protocol: %s" % str(m.virtualServerProtocol[i])) 248 | if m.virtualServerProtocol[i] == protocol: 249 | if not vhost or vipnum == vip: 250 | vp = m.virtualServerPort[i] 251 | if not vport or vportnum == vp: 252 | # iterate over the realservers in 253 | # the specific virtual host 254 | j = m.virtualServerRealServersTotal[i] 255 | idx = 1 256 | while idx <= j: 257 | hexip = m.realServerAddress[i,idx] 258 | rip = socket.inet_ntoa(hexip) 259 | logger.debug("Keepalived.enable(): RIP: %s" % rip) 260 | rp = m.realServerPort[i,idx] 261 | if hostip == rip: 262 | if not rport or (rport and portnum == rp): 263 | # Record the original weight somewhere before disabling it 264 | # Will be used when enabling the server 265 | weight = m.realServerWeight[i,idx] 266 | logger.debug('Keepalived.enable(): Current weight: %s' % weight) 267 | if weight > 0: 268 | msg = "Real server %s:%s on VIP %s:%s is already enabled with a weight of %s" % (rip, rp, vip, vp, weight) 269 | logger.warning(msg) 270 | idx += 1 271 | continue 272 | 273 | filename = "realServerWeight.%s.%s" % (i, idx) 274 | fullpath = '%s/%s' % (self.cache_dir, filename) 275 | 276 | logger.debug('Keepalived.enable(): Enabling %s:%s on VIP %s:%s' % (rip, rp, vip, vp)) 277 | try: 278 | logger.debug('Keepalived.enable(): Reading server weight from file: %s' % fullpath) 279 | f = open(fullpath, 'r') 280 | str_weight = f.readline().rstrip() 281 | f.close() 282 | # make sure the weight is a valid int 283 | orig_weight = int(str_weight) 284 | except IOError as e: 285 | logger.warning("%s. Using 1 as default weight!" % e) 286 | logger.warning("To ensure the correct wieght is set, please restart Keepalived.") 287 | orig_weight = 1 288 | 289 | # set the weight to zero 290 | community = "private" 291 | cmd_example = "snmpset -v2c -c %s localhost KEEPALIVED-MIB::%s = %s" % (community, filename, orig_weight) 292 | logger.info("Running equivalent command to: %s" % cmd_example) 293 | m.realServerWeight[i,idx] = orig_weight 294 | print "Enabled %s:%s on VIP %s:%s (%s). Weight set to %s." % (rip, rp, vip, vp, protocol, orig_weight) 295 | 296 | # Now remove the placeholder file locally 297 | try: 298 | logger.debug("Keeplived.enable(): removing placehodler file") 299 | os.unlink(fullpath) 300 | except OSError as e: 301 | logger.error(e) 302 | logger.error('Please make sure %s is writable!' % self.cache_dir) 303 | logger.error('%s needs to be manually deleted to avoid future problems.' % fullpath) 304 | 305 | # Try removing the reason file 306 | rfilename = "realServerReason.%s.%s" % (i, idx) 307 | rfullpath = '%s/%s' % (self.cache_dir, rfilename) 308 | try: 309 | logger.debug("Keeplived.enable(): removing reason file") 310 | os.unlink(rfullpath) 311 | except OSError as e: 312 | logger.error(e) 313 | logger.error('Please make sure %s is writable!' % self.cache_dir) 314 | logger.error('%s needs to be manually deleted to avoid future problems.' % rfullpath) 315 | 316 | 317 | # remove the placeholder file in other nodes 318 | self.filesync_nodes('remove', fullpath) 319 | 320 | idx += 1 321 | except snmp.SNMPException as e: 322 | logger.error(e) 323 | logger.error("Unable to complete the command successfully! Please verify manually.") 324 | return False 325 | 326 | return True 327 | 328 | def show_real_disabled(self, host, port, numeric): 329 | """show status of disabled real server across multiple VIPs""" 330 | 331 | logger.debug("Keepalived.show_real_disabled(): host:%s" % host) 332 | logger.debug("Keepalived.show_real_disabled(): port:%s" % port) 333 | output = list() 334 | 335 | # update the ipvs table 336 | self.build_ipvs() 337 | for i, v in enumerate(self.virtuals): 338 | for j, r in enumerate(v.realServers): 339 | if r.weight == "0": 340 | if not host or utils.gethostbyname_ex(host)[0] == r.ip: 341 | if not port or utils.getportnum(port) == r.port: 342 | if numeric: 343 | output.append("%s:%s" % (r.ip, r.port)) 344 | else: 345 | try: 346 | host, aliaslist, ipaddrlist = socket.gethostbyaddr(r.ip) 347 | except socket.herror as e: 348 | host = r.ip 349 | try: 350 | portname = socket.getservbyport(int(r.port)) 351 | except socket.error as e: 352 | portname = r.port 353 | output.append("%s:%s" % (host, portname)) 354 | 355 | try: 356 | filename = "%s/realServerReason.%d.%d" % (self.cache_dir, i+1, j+1) 357 | f = open(filename) 358 | reason = 'Reason: ' + f.readline() 359 | logger.debug("Keepalived.show_real_disabled(): %s" % reason) 360 | f.close() 361 | except IOError as e: 362 | logger.error(e) 363 | reason = '' 364 | 365 | output[-1] = output[-1] + "\t\t" + reason 366 | return output 367 | 368 | def parse_config(self, configfile): 369 | """Read the config file and validate configuration syntax""" 370 | try: 371 | f = open(configfile) 372 | except IOError as e: 373 | logger.error(e) 374 | return False 375 | 376 | conf = "".join(f.readlines()) 377 | tokens = kaparser.tokenize_config(conf) 378 | 379 | if tokens: 380 | return True 381 | else: 382 | return False 383 | -------------------------------------------------------------------------------- /lvsm/shell.py: -------------------------------------------------------------------------------- 1 | """Generic lvsm CommandPrompt""" 2 | 3 | import cmd 4 | import logging 5 | import shutil 6 | import socket 7 | import subprocess 8 | import sys 9 | import tempfile 10 | 11 | import utils 12 | import termcolor 13 | import firewall 14 | import lvs 15 | 16 | logger = logging.getLogger('lvsm') 17 | 18 | 19 | class CommandPrompt(cmd.Cmd): 20 | """ 21 | Generic Class for all command prompts used in lvsm. All prompts should 22 | inherit from CommandPrompt and not from cmd.Cmd directly. 23 | """ 24 | settings = {'numeric': False, 25 | 'color': True, 26 | 'commands': False} 27 | variables = ['numeric', 'color', 'commands'] 28 | doc_header = "Commands (type help ):" 29 | 30 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 31 | # super(CommandPrompt, self).__init__() 32 | cmd.Cmd.__init__(self) 33 | self.config = config 34 | 35 | # Build args dict to pass to director object 36 | args = {'keepalived-mib': self.config['keepalived-mib'], 37 | 'snmp_community': self.config['snmp_community'], 38 | 'snmp_host': self.config['snmp_host'], 39 | 'snmp_user': self.config['snmp_user'], 40 | 'snmp_password': self.config['snmp_password'], 41 | 'cache_dir': self.config['cache_dir'] 42 | } 43 | 44 | self.director = lvs.Director(self.config['director'], 45 | self.config['ipvsadm'], 46 | self.config['director_config'], 47 | self.config['director_cmd'], 48 | self.config['nodes'], 49 | args) 50 | 51 | self.rawprompt = rawprompt 52 | # disable color if the terminal doesn't support it 53 | if not sys.stdout.isatty(): 54 | self.settings['color'] = False 55 | 56 | if self.settings['color']: 57 | c = "red" 58 | a = ["bold"] 59 | else: 60 | c = None 61 | a = None 62 | self.prompt = termcolor.colored(self.rawprompt, color=c, 63 | attrs=a) 64 | if logger.getEffectiveLevel() < 30: 65 | self.settings['commands'] = True 66 | 67 | def emptyline(self): 68 | """Override the default emptyline and return a blank line.""" 69 | pass 70 | 71 | def postcmd(self, stop, line): 72 | """Hook method executed just after a command dispatch is finished.""" 73 | # check to see if the prompt should be colorized 74 | if self.settings['color']: 75 | self.prompt = termcolor.colored(self.rawprompt, 76 | color="red", 77 | attrs=["bold"]) 78 | else: 79 | self.prompt = self.rawprompt 80 | return stop 81 | 82 | def print_topics(self, header, cmds, cmdlen, maxcol): 83 | if cmds: 84 | self.stdout.write("%s\n"%str(header)) 85 | if self.ruler: 86 | self.stdout.write("%s\n"%str(self.ruler * len(header))) 87 | for cmd in cmds: 88 | self.stdout.write(" %s\n" % cmd) 89 | self.stdout.write("\n") 90 | 91 | def do_exit(self, line): 92 | """Exit from lvsm shell.""" 93 | modified = list() 94 | 95 | if self.config['version_control'] in ['git', 'svn']: 96 | 97 | import sourcecontrol 98 | args = { 'git_remote': self.config['git_remote'], 99 | 'git_branch': self.config['git_branch'] } 100 | 101 | scm = sourcecontrol.SourceControl(self.config['version_control'], args) 102 | 103 | # check to see if the files have changed 104 | if (self.config['director_config'] and 105 | scm.modified(self.config['director_config'])): 106 | modified.append(self.config['director_config']) 107 | 108 | if (self.config['firewall_config'] and 109 | scm.modified(self.config['firewall_config'])): 110 | modified.append(self.config['firewall_config']) 111 | 112 | # If any files are modified ask user if they want to quit 113 | if modified: 114 | print "The following config file(s) were not committed:" 115 | for filename in modified: 116 | print filename 117 | print 118 | while True: 119 | answer = raw_input("Do you want to quit? (y/n) ") 120 | if answer.lower() == "y": 121 | print "goodbye." 122 | sys.exit(0) 123 | elif answer.lower() == "n": 124 | break 125 | 126 | if not modified: 127 | print "goodbye." 128 | sys.exit(0) 129 | 130 | def do_quit(self, line): 131 | """Exit from lvsm shell.""" 132 | self.do_exit(line) 133 | 134 | def do_end(self, line): 135 | """Return to previous context.""" 136 | return True 137 | 138 | def do_set(self, line): 139 | """Set or display different variables.""" 140 | if not line: 141 | print 142 | print "Shell Settings" 143 | print "==============" 144 | for key, value in self.settings.items(): 145 | print str(key) + " : " + str(value) 146 | print 147 | else: 148 | tokens = line.split() 149 | if len(tokens) == 2: 150 | if tokens[0] == "numeric": 151 | if tokens[1] == "on": 152 | self.settings['numeric'] = True 153 | elif tokens[1] == "off": 154 | self.settings['numeric'] = False 155 | else: 156 | print "*** Syntax: set numeric on|off" 157 | elif tokens[0] == "color": 158 | if tokens[1] == "on": 159 | self.settings['color'] = True 160 | self.prompt = termcolor.colored(self.rawprompt, 161 | color="red", 162 | attrs=["bold"]) 163 | elif tokens[1] == "off": 164 | self.settings['color'] = False 165 | self.prompt = self.rawprompt 166 | else: 167 | print "*** Syntax: set color on|off" 168 | elif tokens[0] == "commands": 169 | if tokens[1] == "on": 170 | self.settings['commands'] = True 171 | # logging.INFO = 20 172 | if logger.getEffectiveLevel() > 20: 173 | logger.setLevel(logging.INFO) 174 | elif tokens[1] == "off": 175 | # logging.INFO = 20 176 | # logging.DEBUG = 10 177 | if logger.getEffectiveLevel() >= 20: 178 | logger.setLevel(logging.WARNING) 179 | self.settings['commands'] = False 180 | else: 181 | logger.error("Running in DEBUG mode, cannot disable commands display.") 182 | else: 183 | print "*** Syntax: set numeric on|off" 184 | else: 185 | self.help_set() 186 | else: 187 | self.help_set() 188 | 189 | def help_help(self): 190 | print 191 | print "show help" 192 | 193 | def help_set(self): 194 | print "Set or display different variables." 195 | print "" 196 | print "syntax: set [ ]" 197 | print "" 198 | print " can be one of:" 199 | print "\tcolor on|off Toggle color display ON/OFF" 200 | print "\tcommands on|off Toggle running commands display ON/OFF" 201 | print "\tnumeric on|off Toggle numeric ipvsadm display ON/OFF" 202 | print "" 203 | 204 | def complete_set(self, text, line, begidx, endidx): 205 | """Tab completion for the set command.""" 206 | if len(line) < 12: 207 | if not text: 208 | completions = self.variables[:] 209 | else: 210 | completions = [m for m in self.variables if m.startswith(text)] 211 | else: 212 | completions = [] 213 | return completions 214 | 215 | 216 | class LivePrompt(CommandPrompt): 217 | """ 218 | Class for the live command prompt. This is the main landing point 219 | and is called from __main__.py 220 | """ 221 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 222 | # super(CommandPrompt, self).__init__() 223 | CommandPrompt.__init__(self, config, rawprompt="lvsm(live)# ") 224 | self.modules = ['director', 'firewall', 'nat', 'virtual', 'real'] 225 | self.protocols = ['tcp', 'udp', 'fwm'] 226 | self.firewall = firewall.Firewall(self.config['iptables']) 227 | 228 | def do_configure(self, line): 229 | """Enter configuration level.""" 230 | commands = line.split() 231 | # configshell = prompts.configure.ConfigurePrompt(self.config) 232 | configshell = ConfigurePrompt(self.config) 233 | if not line: 234 | configshell.cmdloop() 235 | else: 236 | configshell.onecmd(' '.join(commands[0:])) 237 | 238 | def do_virtual(self, line): 239 | """ 240 | \rVirtual IP level. 241 | \rLevel providing information on virtual IPs 242 | """ 243 | commands = line.split() 244 | 245 | # Check for the director before instantiating the right class 246 | if self.config['director'] == 'ldirectord': 247 | from lvsm.modules import ldirectordprompts 248 | virtualshell = ldirectordprompts.VirtualPrompt(self.config) 249 | elif self.config['director'] == 'keepalived': 250 | from lvsm.modules import keepalivedprompts 251 | virtualshell = keepalivedprompts.VirtualPrompt(self.config) 252 | else: 253 | virtualshell = VirtualPrompt(self.config) 254 | 255 | if not line: 256 | virtualshell.cmdloop() 257 | else: 258 | virtualshell.onecmd(' '.join(commands[0:])) 259 | 260 | def do_real(self, line): 261 | """ 262 | \rReal server level. 263 | \rProvides information on real servers. 264 | """ 265 | commands = line.split() 266 | 267 | # Check for the director before instantiating the right class 268 | if self.config['director'] == 'ldirectord': 269 | from lvsm.modules import ldirectordprompts 270 | realshell = ldirectordprompts.RealPrompt(self.config) 271 | elif self.config['director'] == 'keepalived': 272 | from lvsm.modules import keepalivedprompts 273 | realshell = keepalivedprompts.RealPrompt(self.config) 274 | else: 275 | realshell = RealPrompt(self.config) 276 | 277 | if not line: 278 | realshell.cmdloop() 279 | else: 280 | realshell.onecmd(' '.join(commands[0:])) 281 | 282 | def do_firewall(self, line): 283 | """ 284 | \rFirewall level. 285 | \riptables information is available at this level. 286 | """ 287 | commands = line.split() 288 | 289 | fwshell = FirewallPrompt(self.config) 290 | if not line: 291 | fwshell.cmdloop() 292 | else: 293 | fwshell.onecmd(' '.join(commands[0:])) 294 | 295 | def do_restart(self, line): 296 | """Restart the director or firewall module.""" 297 | if line == "director": 298 | if self.config['director_cmd']: 299 | print "restaring director" 300 | try: 301 | subprocess.call(self.config['director_cmd'], shell=True) 302 | except OSError as e: 303 | logger.error("problem while restaring director - %s" % e.strerror) 304 | else: 305 | logger.error("'director_cmd' not defined in lvsm configuration!") 306 | elif line == "firewall": 307 | if self.config['firewall_cmd']: 308 | print "restarting firewall" 309 | try: 310 | subprocess.call(self.config['firewall_cmd'], shell=True) 311 | except OSError as e: 312 | logger.error("problem restaring firewall - %s" % e.strerror) 313 | else: 314 | logger.error("'firewall_cmd' not defined in lvsm configuration!") 315 | else: 316 | print "syntax: restart firewall|director" 317 | 318 | def do_version(self, line): 319 | """ 320 | \rDisplay version information about modules 321 | """ 322 | args = [self.config['ipvsadm'], '--version'] 323 | ipvsadm = utils.check_output(args) 324 | header = ["", "Linux Virtual Server", 325 | "===================="] 326 | 327 | print '\n'.join(header) 328 | print ipvsadm 329 | print 330 | 331 | header = ["Director", 332 | "========"] 333 | print '\n'.join(header) 334 | 335 | if not self.config['director_bin'] : 336 | director = 'director binary not defined. Unable to get version!' 337 | else: 338 | args = [self.config['director_bin'], '--version'] 339 | director = utils.check_output(args) 340 | 341 | print director 342 | print 343 | 344 | args = [self.config['iptables'], '--version'] 345 | iptables = utils.check_output(args) 346 | header = ["Packet Filtering", 347 | "================"] 348 | 349 | print '\n'.join(header) 350 | print iptables 351 | print 352 | 353 | def help_configure(self): 354 | print "" 355 | print "The configuration level." 356 | print "Items related to configuration of IPVS and iptables are available here." 357 | print "" 358 | 359 | def help_restart(self): 360 | print "Restart the given module." 361 | print "" 362 | print "Module must be one of director or firewall." 363 | print "" 364 | print "syntax: restart director|firewall" 365 | 366 | def complete_restart(self, text, line, begix, endidx): 367 | """Tab completion for restart command.""" 368 | if len(line) < 17: 369 | if not text: 370 | completions = self.modules[:] 371 | else: 372 | completions = [m for m in self.modules if m.startswith(text)] 373 | else: 374 | completions = [] 375 | return completions 376 | 377 | 378 | class ConfigurePrompt(CommandPrompt): 379 | """ 380 | Configure prompt class. Handles commands for manipulating configuration 381 | items in the various plugins. 382 | """ 383 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 384 | CommandPrompt.__init__(self, config, rawprompt="lvsm(configure)# ") 385 | # List of moduels used in autocomplete function 386 | self.modules = ['director', 'firewall'] 387 | 388 | def svn_sync(self, filename, username, password): 389 | """Commit changed configs to svn and do update on remote node.""" 390 | # commit config locally 391 | args = ['svn', 392 | 'commit', 393 | '--username', 394 | username, 395 | '--password', 396 | password, 397 | filename] 398 | svn_cmd = ('svn commit --username ' + username + 399 | ' --password ' + password + ' ' + filename) 400 | logger.info('Running command : %s' % svn_cmd) 401 | try: 402 | result = subprocess.call(svn_cmd, shell=True) 403 | except OSError as e: 404 | logger.error("Problem with configuration sync - %s" % e.strerror) 405 | 406 | # update config on all nodes 407 | n = self.config['nodes'] 408 | if n != '': 409 | nodes = n.replace(' ', '').split(',') 410 | else: 411 | nodes = None 412 | 413 | try: 414 | hostname = utils.check_output(['hostname', '-s']) 415 | except (OSError, subprocess.CalledProcessError): 416 | hostname = '' 417 | if nodes is not None: 418 | svn_cmd = ('svn update --username ' + username + 419 | ' --password ' + password + ' ' + filename) 420 | for node in nodes: 421 | if node != hostname: 422 | args = 'ssh ' + node + ' ' + svn_cmd 423 | logger.info('Running command : %s' % (' '.join(args))) 424 | try: 425 | subprocess.call(args, shell=True) 426 | except OSError as e: 427 | logger.error("Problem with configuration sync - %s" % e.strerror) 428 | 429 | def complete_show(self, text, line, begidx, endidx): 430 | """Tab completion for the show command.""" 431 | if len(line) < 14: 432 | if not text: 433 | completions = self.modules[:] 434 | else: 435 | completions = [m for m in self.modules if m.startswith(text)] 436 | else: 437 | completions = [] 438 | return completions 439 | 440 | def help_show(self): 441 | "" 442 | print "Show configuration for an item. The configuration files are defined in lvsm.conf" 443 | print "" 444 | print " can be one of the following" 445 | print "\tdirector the IPVS director config file" 446 | print "\tfirewall the iptables firewall config file" 447 | print "" 448 | 449 | def do_show(self, line): 450 | """Show director or firewall configuration.""" 451 | if line == "director" or line == "firewall": 452 | configkey = line + "_config" 453 | if not self.config[configkey]: 454 | logger.error("'%s' not defined in configuration file!" % configkey) 455 | else: 456 | lines = utils.print_file(self.config[configkey]) 457 | utils.pager(self.config['pager'], lines) 458 | else: 459 | print "\nsyntax: show \n" 460 | 461 | def complete_edit(self, text, line, begidx, endidx): 462 | """Tab completion for the show command""" 463 | if len(line) < 14: 464 | if not text: 465 | completions = self.modules[:] 466 | else: 467 | completions = [m for m in self.modules if m.startswith(text)] 468 | else: 469 | completions = [] 470 | return completions 471 | 472 | def help_edit(self): 473 | print "" 474 | print "Edit the configuration of an item. The configuration files are defined in lvsm.conf" 475 | print "syntax: edit " 476 | print "" 477 | print " can be one of the follwoing" 478 | print "\tdirector the IPVS director config file" 479 | print "\tfirewall the iptables firewall config file" 480 | print "" 481 | 482 | def do_edit(self, line): 483 | """Edit the configuration of an item.""" 484 | if line == "director": 485 | key = line + "_config" 486 | configfile = self.config[key] 487 | if not configfile: 488 | logger.error("'%s' not defined in config file!" % key) 489 | else: 490 | # make a temp copy of the config 491 | try: 492 | temp = tempfile.NamedTemporaryFile(prefix='keepalived.conf.') 493 | shutil.copyfile(configfile, temp.name) 494 | except IOError as e: 495 | logger.error(e.strerror) 496 | return 497 | 498 | while True: 499 | args = "vi " + temp.name 500 | logger.info('Running command : %s' % args) 501 | result = subprocess.call(args, shell=True) 502 | if result != 0: 503 | logger.error("Something happened during the edit of %s" % self.config[key]) 504 | 505 | try: 506 | template = self.config['template_lang'] 507 | except KeyError: 508 | template = None 509 | 510 | # If parsing is disabled, skip the large if/else block 511 | if self.config['parse_director_config'].lower() == 'no': 512 | logger.warn('Director parsing disabled.') 513 | logger.warn('To enable it, please activate the \'parse_director_config\' option in lvsm.conf') 514 | shutil.copy(temp.name, configfile) 515 | temp.close() 516 | break 517 | 518 | # Parse the config file and verify the changes 519 | # If successful, copy changes back to original file 520 | 521 | # If a template language is defined, run it against the config 522 | # before parsing the configuration. 523 | if template: 524 | try: 525 | output = tempfile.NamedTemporaryFile() 526 | logger.info('Running command: %s ' % ' '.join(args)) 527 | args = [template, temp.name] 528 | p = subprocess.Popen(args, stdout=output, stderr=subprocess.PIPE) 529 | out, err = p.communicate() 530 | ret = p.wait() 531 | except OSError as e: 532 | logger.error(e) 533 | logger.error("Please fix the above error before editting the config file.") 534 | break 535 | except IOError as e: 536 | logger.error(e) 537 | break 538 | 539 | if ret: 540 | logger.error(err) 541 | break 542 | elif self.director.parse_config(output.name): 543 | shutil.copyfile(temp.name, configfile) 544 | temp.close() 545 | break 546 | else: 547 | answer = raw_input("Found a syntax error in your config file, edit again? (y/n) ") 548 | if answer.lower() == 'y': 549 | pass 550 | elif answer.lower() == 'n': 551 | logger.warn("Changes were not saved due to syntax errors.") 552 | break 553 | 554 | elif self.director.parse_config(temp.name): 555 | shutil.copyfile(temp.name, configfile) 556 | temp.close() 557 | break 558 | else: 559 | answer = raw_input("Found a syntax error in your config file, edit again? (y/n) ") 560 | if answer.lower() == 'y': 561 | pass 562 | elif answer.lower() == 'n': 563 | logger.warn("Changes were not saved due to syntax errors.") 564 | break 565 | 566 | elif line == "firewall": 567 | key = line + "_config" 568 | configfile = self.config[key] 569 | if not configfile: 570 | logger.error("'%s' not defined in config file!" % key) 571 | else: 572 | args = "vi " + configfile 573 | logger.info(str(args)) 574 | result = subprocess.call(args, shell=True) 575 | if result != 0: 576 | logger.error("Something happened during the edit of %s" % self.config[key]) 577 | else: 578 | print "syntax: edit " 579 | 580 | def help_sync(self): 581 | print "Sync all configuration files across the cluster." 582 | print "" 583 | print "syntax: sync" 584 | 585 | def do_sync(self, line): 586 | """Sync all configuration files across the cluster.""" 587 | if line: 588 | print "*** Syntax: sync" 589 | else: 590 | if self.config['version_control'] in ['git', 'svn']: 591 | 592 | import sourcecontrol 593 | args = { 'git_remote': self.config['git_remote'], 594 | 'git_branch': self.config['git_branch'] } 595 | 596 | scm = sourcecontrol.SourceControl(self.config['version_control'], args) 597 | 598 | # Create a list of nodes to run the update command on 599 | if self.config['nodes'] != '': 600 | nodes = self.config['nodes'].replace(' ', '').split(',') 601 | else: 602 | nodes = None 603 | hostname = socket.gethostname() 604 | 605 | # simple variable, to show users that no mods were made 606 | modified = False 607 | 608 | # check to see if the files have changed 609 | if (self.config['director_config'] and 610 | scm.modified(self.config['director_config'])): 611 | scm.commit(self.config['director_config']) 612 | modified = True 613 | for node in nodes: 614 | if node != hostname: 615 | scm.update(self.config['director_config'], node) 616 | 617 | if (self.config['firewall_config'] and 618 | scm.modified(self.config['firewall_config'])): 619 | scm.commit(self.config['firewall_config']) 620 | modified = True 621 | for node in nodes: 622 | if node != hostname: 623 | scm.update(self.config['director_config'], node) 624 | 625 | if not modified: 626 | print "Configurations not modified. No sync necessary." 627 | 628 | else: 629 | logger.error("'version_control' not defined correctly in lvsm.conf") 630 | 631 | 632 | class VirtualPrompt(CommandPrompt): 633 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 634 | # Change the word delimiters so that - or . don't cause a new match 635 | try: 636 | import readline 637 | readline.set_completer_delims(' ') 638 | except ImportError: 639 | pass 640 | # super(CommandPrompt, self).__init__() 641 | CommandPrompt.__init__(self, config, rawprompt="lvsm(live)(virtual)# ") 642 | self.modules = ['director', 'firewall', 'nat', 'virtual', 'real'] 643 | self.protocols = ['tcp', 'udp', 'fwm'] 644 | self.firewall = firewall.Firewall(self.config['iptables']) 645 | 646 | def do_status(self,line): 647 | """ 648 | \rDisplay status of all virtual servers 649 | """ 650 | syntax = "*** Syntax: status" 651 | numeric = self.settings['numeric'] 652 | color = self.settings['color'] 653 | 654 | if not line: 655 | d = self.director.show(numeric, color) 656 | d.append('') 657 | utils.pager(self.config['pager'], d) 658 | else: 659 | print syntax 660 | 661 | def do_show(self, line): 662 | """ 663 | \rShow status of a virtual server 664 | \rSyntax: show tcp|udp|fwm 665 | """ 666 | syntax = "*** Syntax: show tcp|udp|fwm " 667 | commands = line.split() 668 | numeric = self.settings['numeric'] 669 | color = self.settings['color'] 670 | 671 | if len(commands) == 3 or len(commands) == 2: 672 | protocol = commands[0] 673 | vip = commands[1] 674 | if len(commands) == 3: 675 | port = commands[2] 676 | else: 677 | port = None 678 | if protocol in self.protocols: 679 | d = self.director.show_virtual(vip, port, protocol, numeric, color) 680 | f = self.firewall.show_virtual(vip, port, protocol, numeric, color) 681 | utils.pager(self.config['pager'], d + f) 682 | else: 683 | print syntax 684 | else: 685 | print syntax 686 | 687 | def complete_show(self, text, line, begidx, endidx): 688 | """Tab completion for the show command""" 689 | if len(line) < 8: 690 | completions = [p for p in self.protocols if p.startswith(text)] 691 | elif len(line.split()) == 2: 692 | prot = line.split()[1] 693 | virtuals = self.director.get_virtual(prot) 694 | if not text: 695 | completions = virtuals[:] 696 | elif len(line.split()) == 3 and text: 697 | prot = line.split()[1] 698 | virtuals = self.director.get_virtual(prot) 699 | completions = [p for p in virtuals if p.startswith(text)] 700 | 701 | return completions 702 | 703 | class RealPrompt(CommandPrompt): 704 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 705 | # Change the word delimiters so that - or . don't cause a new match 706 | try: 707 | import readline 708 | readline.set_completer_delims(' ') 709 | except ImportError: 710 | pass 711 | # super(CommandPrompt, self).__init__() 712 | CommandPrompt.__init__(self, config, rawprompt="lvsm(live)(real)# ") 713 | 714 | self.modules = ['director', 'firewall', 'nat', 'virtual', 'real'] 715 | self.protocols = ['tcp', 'udp', 'fwm'] 716 | self.firewall = firewall.Firewall(self.config['iptables']) 717 | 718 | def do_show(self, line): 719 | """ 720 | \rShow information about a specific real server. 721 | \rsyntax: show [] 722 | """ 723 | syntax = "*** Syntax: show []" 724 | commands = line.split() 725 | numeric = self.settings['numeric'] 726 | color = self.settings['color'] 727 | if len(commands) == 2: 728 | host = commands[0] 729 | port = commands[1] 730 | utils.pager(self.config['pager'], self.director.show_real(host, port, numeric, color)) 731 | elif len(commands) == 1: 732 | host = commands[0] 733 | port = None 734 | utils.pager(self.config['pager'], self.director.show_real(host, port, numeric, color)) 735 | else: 736 | print syntax 737 | 738 | def complete_show(self, text, line, begidx, endidx): 739 | """Tab completion for show command""" 740 | tokens = line.split() 741 | reals = self.director.get_real(protocol='') 742 | 743 | if len(tokens) == 1 and not text: 744 | completions = reals[:] 745 | elif len(tokens) == 2 and text: 746 | completions = [r for r in reals if r.startswith(text)] 747 | else: 748 | completions = list() 749 | 750 | return completions 751 | 752 | # def do_disable(self, line): 753 | # """ 754 | # \rDisable real server across VIPs. 755 | # \rsyntax: disable 756 | # """ 757 | 758 | # syntax = "*** Syntax: disable " 759 | 760 | # commands = line.split() 761 | # if len(commands) > 2 or len(commands) == 0: 762 | # print syntax 763 | # elif len(commands) <= 2: 764 | # host = commands[0] 765 | # if len(commands) == 1: 766 | # port = '' 767 | # elif len(commands) == 2: 768 | # port = commands[1] 769 | # else: 770 | # print syntax 771 | # return 772 | # # ask for an optional reason for disabling 773 | # reason = raw_input("Reason for disabling [default = None]: ") 774 | # if not self.director.disable(host, port, reason=reason): 775 | # logger.error("Could not disable %s" % host) 776 | # else: 777 | # print syntax 778 | 779 | # def do_enable(self, line): 780 | # """ 781 | # \rEnable real server across VIPs. 782 | # \rsyntax: enable 783 | # """ 784 | 785 | # syntax = "*** Syntax: enable " 786 | 787 | # commands = line.split() 788 | # if len(commands) > 2 or len(commands) == 0: 789 | # print syntax 790 | # elif len(commands) <= 2: 791 | # host = commands[0] 792 | # if len(commands) == 1: 793 | # port = '' 794 | # elif len(commands) == 2: 795 | # port = commands[1] 796 | # else: 797 | # print syntax 798 | # return 799 | # if not self.director.enable(host, port): 800 | # logger.error("Could not enable %s" % host) 801 | # else: 802 | # print syntax 803 | 804 | # def complete_disable(self, text, line, begidx, endidx): 805 | # """Tab completion for disable command.""" 806 | # servers = ['real', 'virtual'] 807 | # if (line.startswith("disable real") or 808 | # line.startswith("disable virtual")): 809 | # completions = [] 810 | # elif not text: 811 | # completions = servers[:] 812 | # else: 813 | # completions = [s for s in servers if s.startswith(text)] 814 | # return completions 815 | 816 | # def complete_enable(self, text, line, begidx, endidx): 817 | # """Tab completion for enable command.""" 818 | # if (line.startswith("enable real") or 819 | # line.startswith("enable virtual")): 820 | # completions = [] 821 | # elif not text: 822 | # completions = servers[:] 823 | # else: 824 | # completions = [s for s in servers if s.startswith(text)] 825 | # return completions 826 | 827 | class FirewallPrompt(CommandPrompt): 828 | """Class handling shell prompt for firewall (iptables) related actions""" 829 | def __init__(self, config, rawprompt='', stdin=sys.stdin, stdout=sys.stdout): 830 | # super(CommandPrompt, self).__init__() 831 | CommandPrompt.__init__(self, config, rawprompt="lvsm(live)(firewall)# ") 832 | self.firewall = firewall.Firewall(self.config['iptables']) 833 | 834 | def do_status(self, line): 835 | """ 836 | \rDisplay status of all packet filtering rules 837 | """ 838 | mangle = self.firewall.show_mangle(self.settings['numeric'], self.settings['color']) 839 | ports = self.firewall.show(self.settings['numeric'], self.settings['color']) 840 | nat = self.firewall.show_nat(self.settings['numeric']) 841 | 842 | utils.pager(self.config['pager'], mangle + ports + nat + ['']) 843 | 844 | def do_show(self, line): 845 | """ 846 | \rShow the running status specific packet filter tables. 847 | \rSyntax: show 848 | \r
can be one of the following 849 | nat the NAT table. 850 | fwm|mangle the mangle table. 851 | filters the input filters table. 852 | """ 853 | if line == "nat": 854 | output = self.firewall.show_nat(self.settings['numeric']) 855 | elif line == "filters": 856 | output = self.firewall.show(self.settings['numeric'], self.settings['color']) 857 | elif line == "mangle" or line == "fwm": 858 | output = self.firewall.show_mangle(self.settings['numeric'], self.settings['color']) 859 | else: 860 | print "*** Syntax: show nat|fwm|mangle|filters" 861 | return 862 | utils.pager(self.config['pager'], output + ['']) 863 | 864 | def complete_show(self, text, line, begidx, endidx): 865 | """Command completion for the show command""" 866 | args = ['nat', 'filters'] 867 | if not text: 868 | completions = args[:] 869 | else: 870 | completions = [s for s in args if s.startswith(text)] 871 | return completions 872 | --------------------------------------------------------------------------------