patg.net

home

Managing Comware 5.2-based switches with Ansible

16 Oct 2014

Since I last posted, I have been busy trying to learn both about Comware-based switches as well as how to create an Ansible module that can manage such a switch - the HP V1910 24G in particular. I hadn't used this switch before and had to do a bit of research as to how it works, as well as refreshing some networking basics particular to switches.

About the HP V1910 24G Switch

This switch is an advanced, smart managed, fixed-configuration Gigabit switch. Its target customer would be a small business. It's a 24-port switch that can be managed using a web UI or command line interface. It is Comware 5.2-based and for me to have all the features I needed with Comware, I had to run a special command to have the switch go into what is called developer mode -- something that the Ansible modules I wrote do automatically for the user. The developer mode provides

I was hoping originally that there would be a REST API, which many switch vendors provide and some of the switch modules in Ansible that currently exist take advantage of either in writing a module or creating a connection plugin. With the 1910, I was unable to find one, so in order to write an Ansible module, I had to interact with the switch using SSH, specifically through the Paramiko library.

At first, one would think "ah, SSH, just run Ansible against it". However, Ansible by default assumes a POSIX-based system (UNIX host) and even simply running a simple ansible ping against the device will result in Ansible attempting to create a temporary directory and run several posix commands. Since the switch is not POSIX-based, this obviously fails.

About the implementation

First of all, it should be noted that a lot of work was proof-of-concept, trying to discover how feasible this would be. My honest opinion is that it is feasible, once you understand how to interact with the switch and work with Paramiko and switch output.

What I ended up writing was an Ansible module that sends the appropriate Comware commands to do specific tasks. This was difficult in that I had to deal with responses from the switch that don't behave the way a typical UNIX host would. For instance, in some actions one performs on the switch will result in a reply that expects a yes or no ('y' or 'n') that are read by the switch using getchar() (and without a carriage return), hence I ended up using Paramiko channel object methods recv() and send() as opposed to exec_command() (I will digress on the specifics of this in another post). Also difficult but surmountable was how to read and parse the output from the switch and know that I had read everything I needed to. At first, I would run some commands and my program would wait forever -- because it was still trying to recv() !

Lastly, there was a huge amount of work in parsing the output from the summary, running configuration and other commands into a usable "facts" dictionary that is used all througout each of the modules and by Ansible itself.

I was inspired by some great snippets in the paramiko_expect library. I would have used this library, but wanted to keep my library requirements at a minimum; plus I only needed a subset of features.

Also changed is the Ansible module arrangement. The Ansible progect has split out modules into core and extras git repositories, as well as renaming them with ".py" extensions (a good thing!). This added one requirement for me that my modules which have common methods needed a library. Originally, one would put module libraries in [module-utils][module-utils] directory, but with this change, it made more sense for me to create my own python library, comware52, a PyPI module that I would eventually like to make less specific to Ansible and more specific to writing Python code to talk to Comware-based switches.

Using the modules

It's very easy to use. You of course need a switch, and currently my code is in a pull request at my branch on github. Also, you will need to install my comware52 Python module.

There are 4 Ansible comware52 modules:


Using the "comware52" module

A Sample playbook: (there are several samples at comware52_playbooks using the base module, which simply gathers facts if you run ansible-playbook in -vvv displays a dictionary with the switch facts.)

First, there is an example inventory file with these playbooks:

[switch1]
localhost

[switch1:vars]
switch_host=192.168.x.x
switch_user=admin
switch_password=redacted
switch_key_file=~/.ssh/id_dsa

Secondly, a simple task using the "comware52" module:

- hosts: switch1
  tasks:
  - name: gather facts from switch
    comware_5_2:
      gather_facts=true
      host={{ switch_host }}
      username={{ switch_user }}
      password={{ switch_password }}

If ansible is run with the -vvvv` option (NOTE: the output has been formatted for readability):

ok: [localhost] => {"ansible_facts": {
    "current_config":
        {"domain default": "enable system", "ftp server": "enable",
         "interfaces": {"GigabitEthernet1/0/1": {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/10":
                           {"stp edged-port": "enable",
                            "vlan":
                                {"tagged": {},
                                 "untagged": {"access": ["44"]}}},
                        "GigabitEthernet1/0/11":
                          {"port link-type": "trunk",
                           "stp edged-port": "enable",
                           "vlan": {"tagged": {"trunk": ["1", "22"]}, "untagged": {}}},
                        "GigabitEthernet1/0/12":
                          {"port link-type": "trunk",
                           "stp edged-port": "enable",
                           "vlan": {"tagged": {"trunk": ["1", "22"]}, "untagged": {}}},
                        "GigabitEthernet1/0/13":
                          {"port link-type": "hybrid",
                           "stp edged-port": "enable",
                           "vlan": {"tagged": {}, "untagged": {"hybrid": ["1", "22"]}}},
                        "GigabitEthernet1/0/14":
                          {"port link-type": "hybrid",
                           "stp edged-port": "enable",
                           "vlan": {"tagged": {}, "untagged": {"hybrid": ["1", "22"]}}},
                        "GigabitEthernet1/0/15":
                          {"port link-type": "hybrid",
                           "stp edged-port": "enable",
                           "vlan":
                            {"tagged": {},
                             "untagged": {"hybrid": ["1", "55", "66", "77"]}}},
                        "GigabitEthernet1/0/16":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/17":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/18":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/19":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/2":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/20":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/21":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/22":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/23":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/24":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/25":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/26":
                          {"port link-type": "trunk",
                           "stp edged-port": "enable",
                           "vlan":
                             {"tagged": {"trunk": ["1", "44"]}, "untagged": {}}},
                        "GigabitEthernet1/0/27":
                          {"port link-type": "hybrid",
                           "stp edged-port": "enable",
                           "vlan":
                             {"tagged": {}, "untagged": {"hybrid": ["1"]}}},
                        "GigabitEthernet1/0/28":
                          {"stp edged-port": "enable",
                           "vlan": {"tagged": {}, "untagged": {"access": ["44"]}}},
                        "GigabitEthernet1/0/3":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/4":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/5":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/6":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/7":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/8":
                          {"stp edged-port": "enable"},
                        "GigabitEthernet1/0/9":
                          {"stp edged-port": "enable",
                           "vlan":
                             {"tagged": {}, "untagged": {"access": ["44"]}}}, "NULL0": {},
                        "Vlan-interface1": {}},
         "ip ttl-expires": "enable",
         "local-user":
           { "admin":
               {"authorization-attribute": "level 2",
                "password": "cipher $c$3$ieXiyTSM+tfFD+y5eUiQVq2EamKJl27j7R0=",
                "service-type": ["ssh", "telnet", "terminal", "web"]},
             "admin2":
               {"authorization-attribute": "level 2",
                "password": "cipher $c$3$ieXiyTSM+tfFD+y5eUiQVq2EamKJl27j7R0=",
                "service-type": ["ssh", "telnet", "terminal", "web"]},
             "admin3":
               {"authorization-attribute": "level 2",
                "password": "cipher $c$3$ieXiyTSM+tfFD+y5eUiQVq2EamKJl27j7R0=",
                "service-type": ["ssh", "telnet", "terminal", "web"]}},
         "password-recovery": "enable",
         "sysname": "HP",
         "user-group": "system",
         "vlans":
           {"1": {}, "22": {"name": "VLAN_22"},
            "44": {"name": "VLAN_44"},
            "55": {},
            "66": {},
            "77": {}}},
  "summary": {
    "Current_boot_app_is": "flash:/V1910-CMW520-R1513P62.BIN",
    "Default_gateway": "192.168.1.1",
    "IP_Method": "DHCP",
    "IP_address": "192.168.1.14",
    "Next_backup_boot_app_is": "NULL",
    "Next_main_boot_app_is": "flash:/v1910-cmw520-r1513p62.bin",
    "Select_menu_option": "Summary",
    "Subnet_mask": "255.255.255.0",
    "bootrom_version": "Bootrom Version is 163",
    "cpld_version": "CPLD Version is 002",
    "hardware_version": "Hardware Version is REV.B",
    "memory_dram": "128M    bytes DRAM",
    "memory_flash": "128M    bytes Nand Flash Memory",
    "memory_register": "Config Register points to Nand Flash",
    "model": "",
    "software_copyright": "Copyright (c) 2010-2013 Hewlett-Packard Development Company, L.P.",
    "software_version": "Comware Software, Version 5.20, Release 1513P62",
    "subslot_0": "[SubSlot 0] 24GE+4SFP Hardware Version is REV.B",
    "uptime": "HP V1910-24G Switch uptime is 2 weeks, 0 day, 22 hours, 2 minutes"},
  "vlans":
    {"1":
      {"Description": "VLAN 0001",
       "IP_Address": "192.168.1.14",
       "Name": "VLAN 0001",
       "Route_Interface": "configured",
       "Subnet_Mask": "255.255.255.0",
       "Tagged_Ports": "none",
       "Untagged_Ports":
         [ "GigabitEthernet1/0/1",
           "GigabitEthernet1/0/2",
           "GigabitEthernet1/0/3",
           "GigabitEthernet1/0/4",
           "GigabitEthernet1/0/5",
           "GigabitEthernet1/0/6",
           "GigabitEthernet1/0/7",
           "GigabitEthernet1/0/8",
           "GigabitEthernet1/0/11",
           "GigabitEthernet1/0/12",
           "GigabitEthernet1/0/13",
           "GigabitEthernet1/0/14",
           "GigabitEthernet1/0/15",
           "GigabitEthernet1/0/16",
           "GigabitEthernet1/0/17",
           "GigabitEthernet1/0/18",
           "GigabitEthernet1/0/19",
           "GigabitEthernet1/0/20",
           "GigabitEthernet1/0/21",
           "GigabitEthernet1/0/22",
           "GigabitEthernet1/0/23",
           "GigabitEthernet1/0/24",
           "GigabitEthernet1/0/25",
           "GigabitEthernet1/0/26",
           "GigabitEthernet1/0/27"],
           "VLAN_Type": "static"},
       "22":
         {"Description": "VLAN 0022",
          "Name": "VLAN_22",
          "Route_Interface": "not configured",
          "Tagged_Ports":
            ["GigabitEthernet1/0/11", "GigabitEthernet1/0/12"],
          "Untagged_Ports":
            ["GigabitEthernet1/0/13", "GigabitEthernet1/0/14"],
          "VLAN_ID": "22",
          "VLAN_Type": "static"},
        "44":
          {"Description": "VLAN 0044",
           "Name": "VLAN_44",
           "Route_Interface": "not configured",
           "Tagged_Ports":
             ["GigabitEthernet1/0/26", "GigabitEthernet1/0/27"],
           "Untagged_Ports":
             ["GigabitEthernet1/0/9", "GigabitEthernet1/0/10", "GigabitEthernet1/0/28"],
           "VLAN_ID": "44",
           "VLAN_Type": "static"},
         "55":
           {"Description": "VLAN 0055",
            "Name": "vlan 55",
            "Route_Interface": "not configured",
            "Tagged_Ports": "none",
            "Untagged_Ports": ["GigabitEthernet1/0/15"],
            "VLAN_ID": "55",
            "VLAN_Type": "static"},
          "66":
            {"Description": "VLAN 0066",
             "Name": "vlan 66",
             "Route_Interface": "not configured",
             "Tagged_Ports": "none",
             "Untagged_Ports": ["GigabitEthernet1/0/15"],
             "VLAN_ID": "66",
             "VLAN_Type": "static"},
          "77":
            {"Description": "VLAN 0077",
             "Name": "vlan 77",
             "Route_Interface": "not configured",
             "Tagged_Ports": "none",
             "Untagged_Ports": ["GigabitEthernet1/0/15"],
             "VLAN_ID": "77",
             "VLAN_Type": "static"}
           }
        },
"changed": false, "failed": false, "msg": ""}

ok: [localhost] => {"ansible_facts": < SNIP - the same as above ! />

PLAY RECAP ********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0


Using an ssh key

It should also be noted that one can use an ssh key to connect to the switch. The setup of ssh key authentication is involved and currently is assumed to be in place should you use it. Its my intention to also create an ansible module to do this setup as well!

To use a key, I could have simply used the private_key_file option in the playbook, specifying my private key on the host I am running this locally from. This idea I obtained from the Paramiko connection plugin for Ansible:

    comware_5_2:
      gather_facts=true
      host={{ switch_host }}
      username={{ switch_user }}
      private_key_file=~/.ssh/id_dsa


Using the "comware52_vlan" Ansible module

Since the previous example included so much output, this example will only include the actual playbook and non-verbose output.

The example below is quite self-evident. It creates a vlan, arbitrarily named "VLAN_44", modifies it, and then deletes it.

The creation initially sets two ports g1/0/9 and g1/0/10 to be untagged and of type "access". The modification adds another untagged port, g1/0/28 and one tagged port, g1/0/27, set to link-type "hybrid". Note: in order for me to add reliable modification functionality, the easiest thing to do was to completely delete and recreate the VLAN. This may be changed in the future. Finally, VLAN44 is deleted, simply by providing a vlanid value of 44.

- hosts: switch1
  tasks:
  - name: create VLAN_44
    comware_5_2_vlan:
      host={{ switch_host }}
      username={{ switch_user }}
      password={{ switch_password }}
      state=present
      vlan_id=44
      vlan_name=VLAN_44
      untagged_port_type=access
      untagged_ports=GigabitEthernet1/0/9,GigabitEthernet1/0/10

  - name: modify VLAN_44
    comware_5_2_vlan:
      host={{ switch_host }}
      username={{ switch_user }}
      password={{ switch_password }}
      state=present
      vlan_id=44
      vlan_name=VLAN_44
      untagged_port_type=access
      untagged_ports=GigabitEthernet1/0/9,GigabitEthernet1/0/10,GigabitEthernet1/0/28
      tagged_port_type=hybrid
      tagged_ports=GigabitEthernet1/0/27

     - name: delete VLAN_44
    comware_5_2_vlan:
      host={{ switch_host }}
      username={{ switch_user }}
      password={{ switch_password }}
      state=absent
      vlan_id=44
      vlan_name=VLAN_44

The running of this playbook:

~/code/comware_5_2_playbooks$ ansible-playbook -i inventory switch_vlan_sing
le.yml

PLAY [switch1] ****************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [create VLAN_44] ********************************************************
changed: [localhost]

TASK: [modify VLAN_44] ********************************************************
changed: [localhost]

TASK: [delete VLAN_44] ********************************************************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=5    changed=3    unreachable=0    failed=0


Using the "comware52_port" module

After implementing the "comware52vlan" Ansible module, I realized it would useful and simple to add a Ansible module that lets you modify ports specifically, hence the "comware52port" Ansible module was created.

The simple example below simply sets up the port g1/0/15 to have a link-type of "hybrid" for VLANs 55, 66, and 77:

(python-dev)patg@ubuntu:~/code/comware_5_2_playbooks$ cat port.yml
# file: port.yml
- hosts: switch1
  tasks:
  - name: Set port g1/0/15
    comware_5_2_port:
      host={{ switch_host }}
      username={{ switch_user }}
      password={{ switch_password }}
      name=GigabitEthernet1/0/15
      vlans=55,66,77
      link_type=hybrid

Running it results in:

~/code/comware_5_2_playbooks$ ansible-playbook -i inventory port.yml

PLAY [switch1] ****************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [Set port g1/0/15] ******************************************************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0


Using the "comware52_user" Ansible module

Lastly, if one needs to manage users on a Comware switch, the "comware52_user" Ansible module makes this possible.

The example below creates a user called "jimbob", with a password, authorization level of 2, and gives access to this user through web, ssh, and terminal.

- hosts: switch1
  tasks:
  - name: create jimbob user
    comware_5_2_user:
      host={{ switch_host }}
      username={{ switch_user }}
      password={{ switch_password }}
      state=present
      user_name=jimbob
      user_pass=seekrit
      auth_level="level 2"
      services=web,ssh,terminal

Running the playbook:

~/code/comware_5_2_playbooks$ ansible-playbook -i inventory user_create.yml

PLAY [switch1] ****************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [create jimbob user] ****************************************************
changed: [localhost]

PLAY RECAP ********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0

If it is needed to delete the user, only changing "state" to "absent" is required.


Summary

If you should find yourself with Comware 5.2-based switches that you want to manage using Ansible, do feel free to give my code a spin.

Also, I'm very open to input on this. I approached this having never worked a lot with Comware, so any tips and suggestions you may have, or even criticisms, I am very open to!

comments powered by Disqus