Skip to content

Python Click

Python Click is a powerful and user-friendly library for creating command-line interfaces (CLIs) in Python applications. It simplifies the process of building CLIs by providing decorators and utilities that make defining commands, options, and arguments straightforward and intuitive.

Key Features:

  • Declarative Syntax: Click uses a declarative syntax to define commands, arguments, and options.
  • Command Groups: Click allows you to group related commands together, creating a structured and organized CLI application.
  • Input and Output Handling: Click provides utilities for reading input from users and printing output to the console.
  • Options and Arguments: Click supports both command-line options (flags) and arguments (values) that your commands can accept.
  • Automatic Type Conversion: Click automatically converts input values to the specified data types.
  • Help Messages: Click generates help messages for your commands and options, allowing users to learn about your CLI's functionality without extra documentation.

Warning

This requires the following dependencies and has been tested at these specified versions:

napalm==4.1.0
click==8.1.6
poetry==1.5.1

Creating Basic Command-Line Commands

In Click, you can define a basic command using the @click.command() decorator. This decorator is used to create a function that represents a command-line command.

In our example, we're using the @click.option()'s for

  • --platform
  • --ip
  • --username
  • --password

Create a file called network_cli.py and add the following:

import click
from napalm import get_network_driver

@click.command()
@click.option('--ip', prompt='Enter device IP address', help='Device IP address')
@click.option('--platform', prompt='Enter device platform (e.g., ios, eos)', help='Network device type')
@click.option('--username', prompt='Enter your username', help='Your username')
@click.option('--password', prompt='Enter your password', help='Your password', hide_input=True)
def get_device_facts(platform, ip, username, password):
    """Fetch network device information using Python Napalm."""
    driver = get_network_driver(platform)
    with driver(hostname=ip, username=username, password=password) as device_conn:
        facts = device_conn.get_facts()
        click.echo(facts)


if __name__ == "__main__":
    get_device_facts()

Running the Command

ntc@jlw:/tmp$ python3 network_cli.py --ip csr1 --platform ios --username ntc
Enter your password:
{'uptime': 39960.0, 'vendor': 'Cisco', 'os_version': 'Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.1.1, RELEASE SOFTWARE (fc3)', 'serial_number': '9AL82RHCRDK', 'model': 'CSR1000V', 'hostname': 'csr1', 'fqdn': 'csr1.ntc.com', 'interface_list': ['GigabitEthernet1', 'GigabitEthernet2', 'GigabitEthernet3', 'GigabitEthernet4']}
ntc@jlw:/tmp$

Creating Basic Command-Line Commands - LAB

Add to the script the ability to:

  • Add debugging to the script
  • Provide the ability to use the get_interfaces method, but default to get_facts

Arguments

Click also provides arguments, which are subtly different than options. They require values and will be used similar to positional parameters.

Let's make some minor adjustments to our script:

import click
from napalm import get_network_driver

@click.command()
@click.argument('ip')
@click.argument('platform')
@click.option('--username', prompt='Enter your username', help='Your username')
@click.option('--password', prompt='Enter your password', help='Your password', hide_input=True)
def get_device_facts(platform, ip, username, password):
    """Fetch network device information using Python Napalm."""
    driver = get_network_driver(platform)
    with driver(hostname=ip, username=username, password=password) as device_conn:
        facts = device_conn.get_facts()
        click.echo(facts)


if __name__ == "__main__":
    get_device_facts()

Arguments have several other features to be aware of:

  • File based on arguments, in which you pass the path to the file, which will be used in the next section.
  • Environment based variables, in which the variables can be read from an environment variable.
  • nargs parameter, which controls the order in which data is returned as.

Creating Basic Command-Line Commands - LAB

Add to the script the ability to:

  • Convert your IP & Platform options into arguments

Organizing Commands with Groups

In Click, you can organize related commands using command groups. A command group is created using the @click.group() decorator. This allows you to group related commands under a common command-line entry point. Let's organize commands for interacting with network devices using Napalm into two groups: get and set.

import click
from napalm import get_network_driver

# Create the main command group
@click.group()
def cli():
    """Interact with network devices using Napalm."""
    pass

# Add commands to the "get" group
@cli.command()
@click.argument('ip')
@click.argument('platform')
@click.option('--username', prompt='Enter your username', help='Your username')
@click.option('--password', prompt='Enter your password', help='Your password', hide_input=True)
def get(platform, ip, username, password):
    """Get network device facts."""
    driver = get_network_driver(platform)
    with driver(hostname=ip, username=username, password=password) as device_conn:
        facts = device_conn.get_facts()
        click.echo(facts)

# Add commands to the "set" group
@cli.command()
@click.argument('ip')
@click.argument('platform')
@click.argument('config_file', type=click.File('r'))
@click.option('--username', prompt='Enter your username', help='Your username')
@click.option('--password', prompt='Enter your password', help='Your password', hide_input=True)
@click.option('--commit', '-c', is_flag=True, help="Commit the configuration.")
def set(platform, ip, config_file, username, password, commit):
    """Apply configuration from a file."""
    driver = get_network_driver(platform)
    with driver(hostname=ip, username=username, password=password) as device_conn:
        config = config_file.read()
        device_conn.load_merge_candidate(config=config)
        if commit:
            device_conn.commit_config()
        else:
            device_conn.discard_config()


if __name__ == "__main__":
    cli()

Warning

In production, we should not overide the builtin set instead we should namespace the function, e.g. set_group and add the decorator @click.group(name='set')

Let's explore what the output looks like now.

ntc@lab:/tmp$ python3 network_cli.py --help
Usage: network_cli.py [OPTIONS] COMMAND [ARGS]...

  Interact with network devices using Napalm.

Options:
  --help  Show this message and exit.

Commands:
  get  Get network device facts.
  set  Apply configuration from a file.
ntc@lab:/tmp$ python3 network_cli.py get --help
Usage: network_cli.py get [OPTIONS] IP PLATFORM

  Get network device facts.

Options:
  --username TEXT  Your username
  --password TEXT  Your password
  --help           Show this message and exit.
ntc@lab:/tmp$ python3 network_cli.py set --help
Usage: network_cli.py set [OPTIONS] IP PLATFORM CONFIG_FILE

  Apply configuration from a file.

Options:
  --username TEXT  Your username
  --password TEXT  Your password
  --username TEXT  Your username
  -c, --commit     Commit the configuration.
  --help           Show this message and exit.

To run a group command or a subcommand, use the corresponding command-line syntax. For example:

  • To get device facts: python network_cli.py get
  • To apply a configuration: python network_cli set

As you can see, you now have nested cli commands each with their own arguments and options.

Creating Basic Command-Line Commands - LAB

Add to the script the ability to:

  • Add different groups for getting facts vs getting interfaces
  • Create an option on interfaces to only show interfaces that are up

Console Scripts

While under the hood, this is actually leveraging setuptools' Python entry point integration, it is helpful to understand this pattern to create global cli commands that can be ran from anywhere.

Let's create our pyproject.toml file next to our network_cli.py file.

[tool.poetry]
name = "network_cli"
version = "1.0.0"
description = "A cli tool to manage your network"
authors = ["NTC <info@networktocode.com>"]

[tool.poetry.dependencies]
python = "^3.8"
napalm = "*"
click = "*"


[tool.poetry.scripts]
network_cli = 'network_cli:cli'

The key syntax to understand is:

  • Must be placed in [tool.poetry.scripts]
  • The key name is what the command line will be called
  • The value is in format of `f"{dotted_file_path}:{function_name}"``

To use this, simply go through your normal poetry commands.

  • poetry install
  • poetry shell

Now we can be in any directory and have access.

(network-cli-py3.10) ntc@bash:~$ network_cli --help
Usage: network_cli [OPTIONS] COMMAND [ARGS]...

  Interact with network devices using Napalm.

Options:
  --help  Show this message and exit.

Commands:
  get  Get network device facts.
  set  Apply configuration from a file.
(network-cli-py3.10) ntc@bash:~$

Console Scripts - LAB

  • Create the console script as shown in the demo
  • Make it accessible even if not in the environment
    • Hint: This is a poetry process

Advanced Features & Best Practices

  • Setting custom exit codes with click.Abort().
  • Installing and using third-party Click plugins.
  • Extending an existing click app.
  • Organizing commands and options logically and often with different files and folders.