Python

Upgrading to Python 2.7 on CentOS 6.5

Hey Folks,

The systems running Tuxlabs are currently running CentOS 6.5 to emulate a production RHEL like setup for an Openstack Cloud. Running an operating system this old has it’s drawbacks such as dependencies. I was recently installing a well know Python framework and ran into compatibility issues. The framework required Python 2.7 and CentOS 6.5 comes with 2.6. The below is a step by step procedure for how to upgrade to Python 2.7 on CentOS 6.5 if  you ever should need it. However, as a reminder run a newer OS when possible and for god sakes if you don’t need Redhat support, run Ubuntu.

Step one, we verify we are indeed running Python 2.6

[tuxninja@diamond ~]$ python --version
Python 2.6.6
[tuxninja@diamond ~]$

Ok then, let’s upgrade Python to 2.7. First let’s update all of our system applications, just in case for version dependencies and it’s good for security etc.

[tuxninja@diamond ~]$ yum -y update

Next, we have to install Develop Tools, it is a required dependency to install Python.

[tuxninja@diamond ~]$ yum groupinstall "Development tools"

Additionally, we will need these…

[tuxninja@diamond ~]$ sudo yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel

Now, let’s install Python 2.7

[tuxninja@diamond ~]$ cd /opt
[tuxninja@diamond opt]$ sudo wget --no-check-certificate https://www.python.org/ftp/python/2.7.6/Python-2.7.6.tar.xz
[tuxninja@diamond opt]$ sudo tar xf Python-2.7.6.tar.xz 
[tuxninja@diamond opt]$ cd Python-2.7.6
[tuxninja@diamond Python-2.7.6]$ sudo ./configure --prefix=/usr/local
[tuxninja@diamond Python-2.7.6]$ sudo make && sudo make altinstall

It is important to use ‘altinstall’ otherwise you will end up with two different versions of Python on your filesystem, both named ‘python’.

You can verify the install like so

[tuxninja@diamond Python-2.7.6]$ ls -la /usr/local/bin/python2.7*
-rwxr-xr-x 1 root root 6214493 Apr 30 15:14 /usr/local/bin/python2.7
-rwxr-xr-x 1 root root    1674 Apr 30 15:14 /usr/local/bin/python2.7-config
[tuxninja@diamond Python-2.7.6]$ /usr/local/bin/python2.7 --version
Python 2.7.6
[tuxninja@diamond Python-2.7.6]$

That’s it ! Enjoy.

Upgrading to Python 2.7 on CentOS 6.5 Read More »

Runner: Multi-threaded SSH with Sudo support using Python & Paramiko

Example of Runner

$ runner -r web1 -c "whoami" -s
RUNNER [INFO]: MATCHING HOSTNAMES WITH 'web1'
RUNNER: 1 HOSTS HAVE BEEN SELECTED
RUNNER [INFO]: LOGFILE SET - logs/runner.log.2015-01-17.03:10:00
RUNNER [INFO]: USER SET - tuxninja
RUNNER [INFO]: SSH CONNECT TIMEOUT is: 5 seconds
RUNNER [INFO]: THREADS SET - 20
RUNNER [INFO]: SUDO IS ON
RUNNER [INPUT]: Please Enter Site Pass: 
web1.tuxlabs.com: 
web1.tuxlabs.com: root
web1.tuxlabs.com: [tuxninja@web1 ~]$ 

RUNNER [RESULT]: Successfully logged into 1/1 hosts and ran your commands in 0:00:08 second(s)
RUNNER [RESULT]: There were 0 login failures.

Why Runner ?

I have been working as a Systems & Network Administrator since 1999. In that time I have repeatedly had the need for rapidly executing commands across thousands of servers. There are many applications out there that solve this problem in various ways…to name a few…pdsh, Ansible, Salt, Chef, Puppet (mcollective),  even Cfengine and more. Some require agents running on the machines, some use SSH, but require keys…or learning curves. Alternatively, you can write your own code to solve this problem, which is what I did mostly for fun. I don’t recommend re-inventing the wheel if you need this for your job, just use what is already out there, or download runner and hack it to your hearts content for your purposes.

Fabric vs. Paramiko

Because I use Python for most of my work code these days, I decided to write my multi-threaded SSH command runner in Python this way I can use Runner for parallel SSH transport & easily bolt on my other Python scripts for additional functionality. Python has fantastic support for SSH via two libraries Fabric & Paramiko. Fabric is built on top of Paramiko. Fabric provides a simpler interface than Paramiko does for doing just about anything you can think of. Create a fabfile run it, and wolla instant results from commands ran via SSH. Fabric is really great for running & re-running a set of commands to automate an install or reporting for example. All that being said I still chose to use Paramiko over Fabric for three reasons.

  1. I don’t like abstraction. Fabric hides the ugly-ness of Paramiko, which I prefer to understand better.
  2. Writing this using Paramiko lent itself better to a command line utility used for adhoc commands than Fabric did.
  3. I wasn’t sure if Fabric’s abstraction would limit me later based on needing custom functionality. So for Runner I chose Paramiko, but to be clear, 9 times out of 10 I think I would choose Fabric.

Bastions

A bastion or jump box is a machine that is used as the gatekeeper of access to the rest of the machines in your network. In secure environments where your Corp network is separate from your Production network, you will have to SSH into a bastion, which usually has some form of 2-factor authentication (at least it should !) and then from there you may SSH into other hosts. A bastion can throw a real wrench in trying to manage thousands of machines in seconds, because you would have to authenticate to the bastion 1000 times ! The way around this, is by setting up your SSH config to proxy commands.

ProxyCommand & Sconnect

Sconnect (or connect.c) is a binary that is most commonly used as the proxy command for SSH. You can download / read more about sconnect here : https://bitbucket.org/gotoh/connect/wiki/Home and it will also tell you how to setup your SSH config. Using a ProxyCommand with Runner is required, you can however use any ProxyCommand you would like. Really quickly here is what you basically need to do.

  1. Download / Compile connect.c
  2. Copy it to /usr/local/bin/sconnect and set executable permissions
  3. In your SSH Config (.ssh/config) add…
    1. Host <ssh-config-profile-name>
      User tuxninja
      ForwardAgent yes
      HostName <bastion_name>
      DynamicForward 8081 (any uncommon port is fine)
    2. Host *.tuxlabs.com
      User tuxninja
      ProxyCommand /usr/local/bin/sconnect -4 -w 4 -S localhost:8081 %h.tuxlabs.com %p

That is basically it. Then you should start a screen session so you can background the SSH session, since you will leave this open for other SSH sessions to proxy through so you don’t have to go through 2-factor authentication more than once. So something like…

screen -S sshsession
ssh <ssh-config-profile-name>

After you authenticate, detach yourself from the screen using CTRL A then D. Now you can ssh to anything @ domain name in my case tuxlab.com and it will forward through the bastion. At this point you still have to authenticate using a username / password, which is fine. Runner deals with this.

Hosts

Runner requires a hosts file to run. By default it is configured to look in hosts/hosts-all for a list of all hosts. I use a script called ‘update-runner-hosts.pl’, which is included in my github to gather hosts from a URL and update the required hosts file. Once you have populated hosts/hosts-all with the FQDN for your hosts, you are ready to use Runner.

Note: You can use ‘-f’ to provide a custom location for your hosts file.

Great Flags / Features

So some of the really great features of Runner are threading (-t), sudo (-s), list only mode (-l) and the regular expression (-r). -r is for pattern matching your hosts lists, which is incredibly handy and absolutely required in an environment with hundreds to thousands of hosts and you only want to select hosts with -r ‘web’ in them.

(-1) one host per pool mode is a great feature, however it is dependent on understanding your environments hostname pattern so you will have to modify the regular expression in the code to make sure it works for you. It is currently setup to identify hostnames in pools when the naming convention is something like apache1234.tuxlabs.com.

Ok I could go on and on about runner, but it’s better to just share the code at this point and let you go! Note the statically defined proxy_command in the code, you may need to change this if you didn’t use sconnect or the same port.

Note: by default runner uses the user you are logged in as to SSH, you can prompt input for a different user with ‘-u’.

All code and accessories are available for download on github : https://github.com/jasonriedel/tuxlabs/tree/master/runner

Email tuxninja@tuxlabs.com with any question ! Happy SSH’ing admins!

Note: In various versions of this code I had a ‘-h’ allowing you to pass a CSV list of hosts, somehow I let that drop out of this version, sorry ! Feel free to re-add it !

The Runner Code

#!/usr/bin/env python
#Author: Jason Riedel

import paramiko
import getpass
import Queue
import threading
import argparse
import os.path
import time
import logging
import re
import datetime

## SETUP AVAILABLE ARGUMENTS ##
parser = argparse.ArgumentParser()
parser.add_argument('-f', action="store", dest="file_path", required=False, help="Specify your own path to a hosts file")
parser.add_argument('-l', action="store_true", dest="list_only", required=False, help="List all known hosts")
parser.add_argument('-q', action="store_true", dest="quiet_mode", required=False, help="Quiet mode: turns off RUNNER INFO messages.")
parser.add_argument('-qq', action="store_true", dest="super_quiet_mode", required=False, help="Super Quiet mode: turns off ALL RUNNER messages except [INPUT].")
parser.add_argument('-r', action="store", dest="host_match", required=False, help="Select Hosts matching supplied pattern")
parser.add_argument('-c', action="store", dest="command_string", required=False, help="Command to run")
parser.add_argument('-s', action="store_true", dest="sudo", required=False, help="Run command inside root shell using sudo") 
parser.add_argument('-t', action="store", dest="connect_timeout", required=False, help="ssh timeout to hosts in seconds")
parser.add_argument('-T', action="store", dest="threads", required=False, help="# of threads to run (don't get crazy)")
parser.add_argument('-u', action="store", dest="site_user", required=False, help="Specify a username (by default I use who you are logged in as)")
parser.add_argument('-1', action="store_true", dest="host_per_pool", required=False, help="One host per pool")
args = parser.parse_args()

##GLOBAL##
logging.getLogger('paramiko.transport').addHandler(logging.NullHandler())

stime = time.time()

## SET TIMEOUT ##
connect_timeout = 5
if args.connect_timeout:
    connect_timeout = args.connect_timeout

## SET THREADS / WORKERS ##
workers = 20
if args.threads:
    workers = int(args.threads)

## SET USER / PASS ##
site_user = getpass.getuser()
site_passwd = ''
if args.site_user:
    site_user = args.site_user

failed_logins = []
successful_logins = []

tstamp = datetime.datetime.now().strftime("%Y-%m-%d.%H:%M:%S")
logfile_dir = 'logs'
if not os.path.exists(logfile_dir):
    os.makedirs(logfile_dir)
logfile_path = '%s/runner.log.%s' % (logfile_dir, tstamp)
logfile = open(logfile_path, 'w')

## END GLOBAL ##

def ssh_to_host(hosts, site_passwd):
    for i in range(workers):
        t = threading.Thread(target=worker, args=(site_user, site_passwd))
        t.daemon = True
        t.start()

    for hostname in hosts:
        hostname = hostname.rstrip()
        q.put(hostname)

    q.join()

def worker(site_user, site_passwd):
    while True:
        hostname = q.get()
        node_shell(hostname, site_user, site_passwd)
        q.task_done()


def node_shell(hostname, site_user, site_passwd):
    ssh = paramiko.SSHClient()
    proxy_command = "sconnect -4 -w 4 -S localhost:8081 %s %s" % (hostname,'22')
    proxy_sock = paramiko.ProxyCommand(proxy_command)
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        ssh.connect(hostname, username=site_user, password=site_passwd, timeout=connect_timeout, sock=proxy_sock)
        transport = ssh.get_transport()
        transport.set_keepalive(1)

        cmd = args.command_string
	if args.sudo: 
		try: 
			## have to use invoke shell for sudo due to ssh config on machines requirng a TTY
			channel = ssh.invoke_shell() 
			sudocmd = 'sudo ' + cmd

			channel.send(sudocmd + '\n') 

			buff = ''
			while not '[sudo] password' in buff: 
				resp = channel.recv(9999)
				buff += resp

			channel.send(site_passwd + '\n') 

			buff = ''
			while not buff.endswith('$ '):
				resp = channel.recv(9999)
				buff += resp

			for line in buff.split('\n'):
				log_and_print("%s: %s" % (hostname, line))

		except Exception as e:
			log_and_print("ERROR: Sudo failed: %s" % (e))  
  
	else: 
        	(stdin, stdout, stderr) = ssh.exec_command(cmd)

		## stdout 
        	for line in stdout.readlines():
            		line = line.rstrip()
            		log_and_print("%s: %s" % (hostname, line))
		## stderr
        	for line in stderr.readlines():
            		line = line.rstrip()
            		log_and_print("%s: %s" % (hostname, line))

        successful_logins.append(hostname)
        ssh.close()

    except Exception as e:
        log_and_print("%s: failed to login : %s" % (hostname, e))
        failed_logins.append(hostname)
        ssh.close()

def log_and_print(message):
    if args.super_quiet_mode or args.list_only:
        if "RUNNER [INPUT]" in message or "RUNNER [ERROR]" in message or "RUNNER" not in message:
            print message
            logfile.write(message + '\n')
    elif args.quiet_mode or args.list_only:
        if "RUNNER [INFO]" not in message:
            print message
            logfile.write(message + '\n')
    else:
        print message
        if not args.list_only:
            logfile.write(message + '\n')

def get_hosts(file_path):
    if os.path.exists(file_path):
        hosts = open(file_path)
        selected_hosts = []
        if not args.host_match:
            selected_hosts = list(hosts)
            log_and_print("RUNNER [INFO]: SELECTING ALL HOSTS")
        else:
            host_match = args.host_match
            for host in hosts:
                if re.search(host_match, host):
                    selected_hosts.append(host)
            log_and_print("RUNNER [INFO]: MATCHING HOSTNAMES WITH '%s'" % (host_match))
    else:
        log_and_print("RUNNER [ERROR]: %s does not exist ! Try running ./update-runner-hosts" % (file_path))
        exit()

    ## Select one host per pool
    if args.host_per_pool:
        seen = {}
        host_per_pool = []
        for host in selected_hosts:
	    # Here strip values that make hostnames unique like #'s
	    # That way the dict matches after 1 host per pool has been seen 
            nhost = re.sub("\d+?\.", ".", host) #Removing #'s in a hostname like host1234.tuxlabs.com
            if not nhost in seen:
                seen[nhost] = 1
                host_per_pool.append(host)
        selected_hosts = host_per_pool

    log_and_print("RUNNER: %s HOSTS HAVE BEEN SELECTED" % (len(selected_hosts)))
    return selected_hosts

if __name__ == "__main__":
    file_path = 'hosts/hosts-all' ## update-hosts-all creates the DIR 

    if args.file_path:
        file_path = args.file_path
        if '~' in file_path:
            print "RUNNER [ERROR]: -f does not support '~'"
            exit()

    if args.list_only or args.command_string:
        selected_hosts = get_hosts(file_path)
        if args.list_only:
            for host in selected_hosts:
                host = host.rstrip()
                log_and_print(host)
            log_and_print("\nThere were %s hosts listed." % (len(selected_hosts)))
            exit()

        else:
            log_and_print("RUNNER [INFO]: LOGFILE SET - %s" % (logfile_path))
            log_and_print("RUNNER [INFO]: USER SET - %s" % (site_user))
            log_and_print("RUNNER [INFO]: SSH CONNECT TIMEOUT is: %s seconds" % (connect_timeout))
            log_and_print("RUNNER [INFO]: THREADS SET - %s" % (workers))
	    if args.sudo:
		log_and_print("RUNNER [INFO]: SUDO IS ON") 

            site_passwd = getpass.getpass("RUNNER [INPUT]: Please Enter Site Pass: ")

            q = Queue.Queue()

            ssh_to_host(selected_hosts,site_passwd)

            etime=time.time()
            run_time = int(etime-stime)

            timestamp = str(datetime.timedelta(seconds=run_time))
            log_and_print("\nRUNNER [RESULT]: Successfully logged into %s/%s hosts and ran your commands in %s second(s)" % (len(successful_logins), len(selected_hosts), timestamp))
            log_and_print("RUNNER [RESULT]: There were %s login failures.\n" % (len(failed_logins)))
            if len(failed_logins) > 0:
                for failed_host in failed_logins:
                    log_and_print("RUNNER [RESULT]: Failed to login to: %s" % (failed_host))
    else:
        parser.print_help()
        output = "\nRUNNER [INFO]: Either -l (list hosts only) or -s (Run cmd string) is required.\n"
        log_and_print(output)

Runner: Multi-threaded SSH with Sudo support using Python & Paramiko Read More »

Python & Fun with Checkio.org

My Brief Coding History

13 years ago, I fell in love with Perl. I liked Perl a lot better than my previous flings with HTML, Shell (Bash), PHP, ASP, Javascript, Sed, Awk and a few others, mainly because Perl was always willing to do what I wanted, when I wanted, making my life as a Sys Admin a lot easier and never asked much in return just to be loved. I loved Perl for years and years and in a way I will never stop loving Perl, after all Perl was my first love. But too often our first loves die hard no matter how hard we try to will their existence forward…

“I never thought I could love a language more than Perl, until I met Python”  (TuxNinja, 2012)

Enter Python

Old loves die hard.. I haven’t used Perl for about ~18 months, whatever the exact amount of time is I am not sure, but it matches the exact time at which I started learning Python.  I decided to start learning Python because it’s popularity seemed to be growing exponentially (and still does), once I learned basic syntax by reading and watching a couple of videos, I was up and running and it wasn’t long before I was solving real business critical problems with it.

[stextbox id=”grey” caption=”Best Way To Learn A New Language”]I have learned a lot of languages. By far the best way to learn a new language is to solve a real problem (preferably at your job) with it. The reason being is that inevitably when you solve the problem you will need to maintain the code (i.e. add features and fix bugs). This ‘recall’ you experience every time you re-visit and re-factor your code will burn into your brain valuable learning’s better than any amount of writing and/or reading alone. Suffices to say that troubleshooting is the best way to learn.[/stextbox]

Two Reasons

There were two major reasons once I started learning Python I decided it was my new #1.

  1. Python is the most readable language I have ever seen. It reads like pseudo code or plain english. If that isn’t important to you, go try to read someones competition winning obfuscated code ./facepalm. For the 13 years I coded Perl every time I took over a Perl script from someone else I re-wrote it. That is because Perl allows for coders to follow many different styles such as a C or Shell style of coding. It’s flexibility is it’s greatest enemy in my opinion, there are 1000 ways to do something in Perl. In Python there are 10’s of ways, but there is usually only 1 pythonic way. I prefer a language that has a clear best way to do something and that encourages readability. Of course there are still some assholes out there who prefer lambda() and map() over list comprehension and to them I say… read a book (err website)
  2. Community support. I forever loved Perl. Two of the reasons was the incredible support I found on www.perlmonks.com and because of CPAN and the plethra of modules that existed. Well Python wins again. It’s seems most former monks now spend their time correcting peoples Python on StackOverflow.com and PyPi > CPAN, sorry but it’s true !

All that being said, I encourage you to learn Python, Ruby, and Node.js… these 3 seem to be the front runners of that the next generation is learning and therefore will continue to be abound now and in the future. But since this is a Python article and the language I know best, learn Python first !

CheckIO

Finally, the purpose of this article! About 3 months into learning Python a good friend of mine who is a programmer for a living told me about CheckIO.org, which is a website, that provides a fun & free way to learn Python in the form of a game that reminds me of Wii’s Super Mario. Now when I initially signed up ~15 months ago I never actually ended up playing the game. Until a couple of days ago when I got an email from them asking how I liked CheckIO. Well that was all the reminder I needed, looking to continue in life long learning and specifically my Python knowledge I started playing the game. It’s been 48 hours and I am pretty hooked ! The benefits to learning using CheckIO are unmatched by any other learning mechanisms I have used to date here’s why.

How It Works

CheckIO starts you at your ‘home’ and presents you with challenges that it gives you points and badges for completing. Once you complete enough you move on to the next challenge and eventually unlock other adventures and your next set of challenges. Yea, yea ok that’s cool so what…well when you solve your challenge, you have the option of publishing your code and reviewing / voting on the best solution of all time for that problem. This is an amazing thing. It combines not only exercising to solve a problem yourself, but ultimately showing you the most clean, efficient and pythonic way (caveat: not always but mostly the social voting keeps this true). This is an incredible about of knowledge in one place, and the challenges you are solving are universally applicable to some of the most common problems with data structures, sorting and counting that you will run into. Admittedly, I am a level 4 currently and only about 60% of the way through and have already unlocked other adventures, but I was having so much fun and learning so much that I decided I’m going to complete 100% of an adventure before moving on.

CheckIO Teaches You To Be An Efficient Programmer

Feel free to read that again. Yes it does. My whole programming life I have been focused on function…function, function, function. Making my code functional was always #1 because after all you are solving a problem and making your code functional has to be the right first step anyway, you can’t work on making an application efficient if you haven’t figured out a way to solve the problem programmaticly. So the only time I went back and re-wrote an program to make it more efficient was when it was heavily used so incredibly inefficient that it demanded to be FIXED!

Let There Be Light

What CheckIO does is encourages you to compete with other programmers solving the same challenges in the game format, keeping it fun. So finally 6 challenges in, I started to get it… instead of just solving the challenge, why not try to solve the challenge in the most efficient & cleanest way possible and then publish my code for voting with the community. I did just that and the difference in my code was a difference of 47 lines.  The example I am showing below is a basic one, but the story is what’s crucial. If you solve the CheckIO problems and challenge yourself to write the best possible solution each time and then compare against your peers you will ultimately be a far better programmer then most people. Not to mention you will have a nice repository for re-usable code for common problems you will encounter as a programmer.

Ok enough…it’s show and tell time.

The Problem

Full Problem Description: http://www.checkio.org/mission/roman-numerals/
Short Version: For this task, you should return a roman numeral using the specified integer value ranging from 1 to 3999.

Input: An integer ranging from 1 to 3999.
Output: A string in the form of a Roman numeral.
Example:

checkio(6) == 'VI'
checkio(76) == 'LXXVI'

Functional Solution

import random

numerals = { 1 : 'I', 4 : 'IV', 5 : 'V', 9 : 'IX', 10 : 'X', 40 : 'XL', 50 : 'L', 90 : 'XC', 100 : 'C', 400 : 'CD', 500 : 'D', 900 : 'CM', 1000 : 'M' }
number = random.randrange(4000)
string = ''

if (number / 1000):
    thousands = number / 1000 # thousands
    string += numerals[1000]*thousands
    number = number - (thousands*1000)
if (number / 900):
    ninehundreds = number / 900 # 900's
    string += numerals[900]*ninehundreds
    number = number - (ninehundreds*900)
if (number / 500):
    fivehundreds = number / 500 # 500's
    string += numerals[500]*fivehundreds
    number = number - (fivehundreds*500)
if (number / 400):
    fourhundreds = number / 400 # 400's
    string += numerals[400]*fourhundreds
    number = number - (fourhundreds*400)
if (number / 100):
    hundreds = number / 100 # 100's
    string += numerals[100]*hundreds
    number = number - (hundreds*100)
if (number / 90):
    nineties = number / 90 # 90's
    string += numerals[90]*nineties
    number = number - (nineties*90)
if (number / 50):
    fifties = number / 50 # 50's
    string += numerals[50]*fifties
    number = number - (fifties*50)
if (number / 40):
    forties = number / 40 # 40's
    string += numerals[40]*forties
    number = number - (forties*40)
if (number / 10):
    tens =  number / 10 # 10's
    string += numerals[10]*tens
    number = number - (tens*10)
if (number / 9):
    nines =  number / 9 # 9's
    string += numerals[9]*nines
    number = number - (nines*9)
if (number / 5):
    fives = number / 5 # 5's
    string += numerals[5]*fives
    number = number - (fives*5)
if (number / 4):
    fours = number / 4 # 4's
    string += numerals[4]*fours
    number = number - (fours*4)
if (number / 1):
    ones = number / 1 # 1's
    string += numerals[1]*ones
    number = number - (ones*1)

print string

My Think Twice Solution

It’s amazing how quickly you can improve code once you understand how it needs to function. This improvement took me a total of 2 minutes to finish.

import random

numerals = { 1 : 'I', 4 : 'IV', 5 : 'V', 9 : 'IX', 10 : 'X', 40 : 'XL', 50 : 'L', 90 : 'XC', 100 : 'C', 400 : 'CD', 500 : 'D', 900 : 'CM', 1000 : 'M' }
number = random.randrange(4000)
string = ''

for roman in sorted(numerals, reverse=True):
    if (number / roman):
        multiplier = number / roman
        string += numerals[roman]*multiplier
        number = number - (multiplier*roman)

print string

Published Solution

The above solutions were what I used to test and run before submitting, when you actually submit it has to be as a CheckIO function. The real code submitted can be found below here.

 

 

 

Python & Fun with Checkio.org Read More »