Browse Source

Create an underlying trunk.

Joe Clarke 4 years ago
commit
e8ae0315d4
2 changed files with 996 additions and 0 deletions
  1. 622 0
      meraki_api/__init__.py
  2. 374 0
      setup_meraki_nets.py

+ 622 - 0
meraki_api/__init__.py

@@ -0,0 +1,622 @@
+#
+# Copyright (c) 2018  Joe Clarke <jclarke@cisco.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import requests
+import logging
+import json
+
+
+class Meraki(object):
+    MERAKI_API = 'https://api.meraki.com/api/v0'
+
+    _headers = {
+        'X-Cisco-Meraki-API-Key': None,
+        'Content-type': 'application/json'
+    }
+
+    _logit = False
+    _key = None
+
+    _name = None
+    _id = None
+
+    _class_name = 'Meraki'
+
+    _name_prop = 'name'
+    _id_prop = 'id'
+
+    _api_url_endpoint = ''
+
+    def __init__(self, **kwargs):
+        if 'logit' in kwargs:
+            self._logit = kwargs['logit']
+        if 'key' in kwargs:
+            self._key = kwargs['key']
+            self._headers['X-Cisco-Meraki-API-Key'] = kwargs['key']
+        if 'name' in kwargs:
+            self._name = kwargs['name']
+        if 'id' in kwargs:
+            self._id = str(kwargs['id'])
+
+        self.__dict = {}
+        self._initialized = False
+
+    def set_key(self, key):
+        self._key = key
+        self._headers['X-Cisco-Meraki-API-Key'] = key
+
+    def set_name(self, name):
+        self._name = name
+
+    def _set_id(self, id):
+        self._id = id
+
+    def __check_headers(self):
+        if self._headers['X-Cisco-Meraki-API-Key'] is None:
+            msg = 'Meraki API key is not set!'
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return False
+
+        return True
+
+    def realize(self):
+        return self._check_obj()
+
+    @staticmethod
+    def _get_json_errors(response):
+        res = ''
+        try:
+            jobj = response.json()
+            if 'errors' in jobj:
+                res = ', '.join(jobj['errors'])
+        except:
+            pass
+
+        return res
+
+    def _check_obj(self):
+        if self._initialized:
+            return True
+
+        if self._id is None:
+            msg = '{} id is not set!'.format(self._class_name)
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return False
+
+        return self.__get_current_obj()
+
+    def __get_current_obj(self):
+        if not self.__check_headers():
+            return False
+
+        url = self.MERAKI_API + self._api_url_endpoint + '/' + self._id
+        try:
+            response = requests.request('GET', url, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error getting {} for {}: {} ({})'.format(
+                self._class_name.lower(), self._id, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return False
+
+        jobj = response.json()
+        self.__populate_obj(jobj)
+        # Object may not have a name yet.
+        if self._name_prop in jobj:
+            self._name = jobj[self._name_prop]
+            
+        self._initialized = True
+        return True
+
+    def __populate_obj(self, jobj):
+        for k, v in jobj.items():
+            self.__dict[k] = v
+
+    def get(self, field):
+        if not self._check_obj():
+            raise Exception('Failed to initialize object!')
+
+        if field in self.__dict:
+            return self.__dict[field]
+        else:
+            raise Exception('Field {} does not exist'.format(field))
+
+    def set(self, field, value):
+        if not self._check_obj():
+            raise Exception('Failed to initialize object!')
+
+        self.__dict[field] = value
+
+    def get_organizations(self):
+        if not self.__check_headers():
+            return None
+
+        url = self.MERAKI_API + '/organizations'
+        try:
+            response = requests.request('GET', url, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error getting list of organizations: {} ({})'.format(
+                e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return None
+
+        res = []
+        for org in response.json():
+            org_obj = Organization(
+                key=self._key, name=org[Organization._name_prop], id=org[Organization._id_prop])
+            res.append(org_obj)
+
+        return res
+
+
+class Organization(Meraki):
+    def __init__(self, **kwargs):
+        super(Organization, self).__init__(**kwargs)
+
+        self._class_name = 'Organization'
+        self._api_url_endpoint = '/organizations'
+        self.ORG_API = self.MERAKI_API + self._api_url_endpoint
+
+    def get_inventory(self):
+        if not self._check_obj():
+            return None
+
+        url = self.ORG_API + '/' + self._id + '/inventory'
+
+        try:
+            response = requests.request('GET', url, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error getting inventory for organization {}: {} ({})'.format(
+                self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return None
+
+        return response.json()
+
+    def get_networks(self):
+        if not self._check_obj():
+            return None
+
+        url = self.ORG_API + '/' + self._id + '/networks'
+        try:
+            response = requests.request('GET', url, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error getting list of networks for {}: {} ({})'.format(
+                self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return None
+
+        res = []
+        for n in response.json():
+            net = Network(
+                key=self._key, name=n[Network._name_prop], id=n[Network._id_prop
+                                                                ])
+            res.append(net)
+
+        return res
+
+    def create_network(self, name, **kwargs):
+        if not self._check_obj():
+            return None
+
+        payload = {
+            'name': name
+        }
+
+        for key in ['type', 'tags', 'timezone', 'copy_from_network_id']:
+            if key in kwargs:
+                if key == 'timezone':
+                    payload['timeZone'] = kwargs[key]
+                elif key == 'copy_from_network_id':
+                    payload['copyFromNetworkId'] = kwargs[key]
+                else:
+                    payload[key] = kwargs[key]
+
+        if 'type' not in kwargs:
+            payload['type'] = 'wireless switch appliance'
+
+        url = self.ORG_API + '/' + self._id + '/networks'
+        try:
+            response = requests.request(
+                'POST', url, json=payload, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error creating new network {} in {}: {} ({})'.format(
+                name, self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return None
+
+        jobj = response.json()
+        net_obj = Network(key=self._key, id=jobj['id'], name=jobj['name'])
+
+        return net_obj
+
+
+class Network(Meraki):
+    def __init__(self, **kwargs):
+        super(Network, self).__init__(**kwargs)
+
+        self._class_name = 'Network'
+        self._api_url_endpoint = '/networks'
+        self.NET_API = self.MERAKI_API + self._api_url_endpoint
+
+    def get_devices(self):
+        if not self._check_obj():
+            return None
+
+        url = self.NET_API + '/' + self._id + '/devices'
+        try:
+            response = requests.request('GET', url, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error getting device list for network {}: {} ({})'.format(
+                self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return None
+
+        res = []
+        for d in response.json():
+            dev = Device(
+                key=self._key, name=d[Device._name_prop], id=d[Device._id_prop])
+            res.append(dev)
+
+        return res
+
+    def claim_device(self, dev):
+        if not self._check_obj():
+            return None
+
+        url = self.NET_API + '/' + self._id + '/devices/claim'
+        payload = {
+            'serial': dev._id
+        }
+        try:
+            response = requests.request(
+                'POST', url, json=payload, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error claiming device {} for network {}: {} ({})'.format(
+                dev._id, self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return None
+
+        new_dev = Device(key=self._key, id=dev._id, net=self)
+        return new_dev
+
+    def create_vlan(self, name, id, subnet, appliance_ip):
+        if not self._check_obj():
+            return None
+
+        payload = {
+            'id': id,
+            'name': name,
+            'subnet': subnet,
+            'applianceIp': appliance_ip
+        }
+
+        url = self.NET_API + '/' + self._id + '/vlans'
+        try:
+            response = requests.request(
+                'POST', url, json=payload, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error adding VLAN {} (ID: {}) to network {}: {} ({})'.format(
+                name, id, self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return None
+
+        jobj = response.json()
+        vlan_obj = Vlan(
+            key=self._key, id=jobj['id'], name=jobj['name'], net=self)
+
+        return vlan_obj
+
+
+class SSID(Meraki):
+    _id_prop = 'number'
+
+    def __init__(self, **kwargs):
+        super(SSID, self).__init__(**kwargs)
+        if not 'net' in kwargs:
+            raise TypeError('Missing mandatory net argument!')
+
+        self._net = kwargs['net']
+
+        self._class_name = 'SSID'
+        self._api_url_endpoint = '/networks/' + \
+            self._net.get(Network._id_prop) + '/ssids'
+        self.SSID_API = self.MERAKI_API + self._api_url_endpoint
+
+    def update_ssid(self, **kwargs):
+        if not self._check_obj():
+            return False
+
+        payload = {}
+        for key in ['name', 'enabled', 'auth_mode', 'encryption_mode', 'psk', 'ip_assignment_mode']:
+            if key in kwargs:
+                if key == 'auth_mode':
+                    payload['authMode'] = kwargs[key]
+                elif key == 'encryption_mode':
+                    payload['encryptionMode'] = kwargs[key]
+                elif key == 'ip_assignment_mode':
+                    payload['ipAssignmentMode'] = kwargs[key]
+                else:
+                    payload[key] = kwargs[key]
+
+        if len(payload) == 0:
+            return False
+
+        if 'ip_assignment_mode' not in kwargs:
+            payload['ipAssignmentMode'] = 'Bridge mode'
+
+        url = self.SSID_API + '/' + self._id
+        try:
+            response = requests.request(
+                'PUT', url, json=payload, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error updating SSID properties for {}: {} ({})'.format(
+                self._id, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+                return False
+
+        jobj = response.json()
+        for k, v in jobj.items():
+            self.set(k, v)
+
+        return True
+
+
+class Vlan(Meraki):
+    def __init__(self, **kwargs):
+        super(Vlan, self).__init__(**kwargs)
+        if not 'net' in kwargs:
+            raise TypeError('Missing mandatory net argument!')
+
+        self._net = kwargs['net']
+
+        self._class_name = 'Vlan'
+        self._api_url_endpoint = '/networks/' + \
+            self._net.get(Network._id_prop) + '/vlans'
+        self.VLAN_API = self.MERAKI_API + self._api_url_endpoint
+
+    def update_vlan(self, **kwargs):
+        if not self._check_obj():
+            return False
+
+        payload = {}
+        for key in ['name', 'subnet', 'appliance_ip', 'fixed_ip_assignments', 'reserved_ip_ranges', 'dns_nameservers']:
+            if key in kwargs:
+                if key == 'appliance_ip':
+                    payload['applianceIp'] = kwargs[key]
+                elif key == 'fixed_ip_assignments':
+                    payload['fixedIpAssignments'] = kwargs[key]
+                elif key == 'reserved_ip_ranges':
+                    payload['reservedIpRanges'] = kwargs[key]
+                elif key == 'dns_nameservers':
+                    payload['dnsNameservers'] = kwargs[key]
+                else:
+                    payload[key] = kwargs[key]
+
+        if len(payload) == 0:
+            return False
+
+        url = self.VLAN_API + '/' + self._id
+        try:
+            response = requests.request(
+                'PUT', url, json=payload, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error updating VLAN properties for {}: {} ({})'.format(
+                self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+                return False
+
+        jobj = response.json()
+        for k, v in jobj.items():
+            self.set(k, v)
+
+        return True
+
+
+class Device(Meraki):
+
+    _id_prop = 'serial'
+
+    def __init__(self, **kwargs):
+        super(Device, self).__init__(**kwargs)
+        if not 'net' in kwargs:
+            raise TypeError('Missing mandatory net argument!')
+
+        self._net = kwargs['net']
+
+        self._class_name = 'Device'
+        self._api_url_endpoint = '/networks/' + \
+            self._net.get(Network._id_prop) + '/devices'
+        self.DEV_API = self.MERAKI_API + self._api_url_endpoint
+
+    def remove_device(self):
+        if not self._check_obj():
+            return False
+
+        url = self.DEV_API + '/' + self._id + '/remove'
+        try:
+            response = requests.request('POST', url, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Failed to remove device {} for network {}: {} ({})'.format(
+                self._name, self._net.get(Network._name_prop), e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+            return False
+
+        return True
+
+    def update_device(self, **kwargs):
+        if not self._check_obj():
+            return False
+
+        payload = {}
+        for key in ['name', 'tags', 'lat', 'lng', 'address', 'move_map_marker']:
+            if key in kwargs:
+                if key == 'move_map_marker':
+                    payload['moveMapMarker'] = kwargs[key]
+                else:
+                    payload[key] = kwargs[key]
+
+        if len(payload) == 0:
+            return False
+
+        url = self.DEV_API + '/' + self._id
+        try:
+            response = requests.request(
+                'PUT', url, json=payload, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error updating device properties for {}: {} ({})'.format(
+                self._name, e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+                return False
+
+        jobj = response.json()
+        for k, v in jobj.items():
+            if k == 'moveMapMarker':
+                continue
+            self.set(k, v)
+
+        return True
+
+
+class SwitchPort(Meraki):
+    _id_prop = 'number'
+
+    def __init__(self, **kwargs):
+        super(SwitchPort, self).__init__(**kwargs)
+        if not 'dev' in kwargs:
+            raise TypeError('Missing mandatory dev argument!')
+
+        self._dev = kwargs['dev']
+
+        self._class_name = 'SwitchPort'
+        self._api_url_endpoint = '/devices/' + \
+            self._net.get(Network._id_prop) + '/switchPorts'
+        self.SWP_API = self.MERAKI_API + self._api_url_endpoint
+
+    def update_switchport(self, **kwargs):
+        if not self._check_obj():
+            return False
+
+        payload = {}
+        for key in ['name', 'tags', 'enabled', 'type', 'vlan', 'voice_vlan', 'allowed_vlans', 'poe_enabled']:
+            if key in kwargs:
+                if key == 'voice_vlan':
+                    payload['voiceVlan'] = kwargs[key]
+                elif key == 'allowed_vlans':
+                    payload['allowedVlans'] = kwargs[key]
+                elif key == 'poe_enabled':
+                    payload['poeEnabled'] = kwargs[key]
+                else:
+                    payload[key] = kwargs[key]
+
+        if len(payload) == 0:
+            return False
+
+        url = self.SWP_API + '/' + self._id
+        try:
+            response = requests.request(
+                'PUT', url, json=payload, headers=self._headers)
+            response.raise_for_status()
+        except Exception as e:
+            msg = 'Error updating switchport properties for {} on device {}: {} ({})'.format(
+                self._id, self._dev.get(Device._name_prop), e, Meraki._get_json_errors(response))
+            if self._logit:
+                logging.error(msg)
+            else:
+                print(msg)
+
+                return False
+
+        jobj = response.json()
+        for k, v in jobj.items():
+            self.set(k, v)
+
+        return True

+ 374 - 0
setup_meraki_nets.py

@@ -0,0 +1,374 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2018  Joe Clarke <jclarke@cisco.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+
+import meraki_api
+import yaml
+import argparse
+import sys
+import os
+from colorama import Fore, Back, Style
+import colorama
+
+BANNER = '[{}] **********************************************************'
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        prog=sys.argv[0], description='Add devices to network')
+    parser.add_argument('--config', '-c', metavar='<CONFIG FILE>',
+                        help='Path to the organization configuration file', required=True)
+    parser.add_argument(
+        '--networks', '-n', metavar='<NETWORK>[,<NETWORK>[,...]]', help='Comma-separated list of networks to process')
+    args = parser.parse_args()
+
+    colorama.init()
+
+    if not os.path.isfile(args.config):
+        print('Config file {} does not exist or is not a file!'.format(args.config))
+        sys.exit(1)
+
+    print(BANNER.format('Loading config file'))
+    with open(args.config, 'r') as c:
+        config = yaml.load(c)
+    print('{}ok{}\n'.format(Fore.GREEN, Style.RESET_ALL))
+
+    for key in ['api_key', 'organization', 'networks']:
+        if not key in config:
+            print('Invalid config: {} is missing!'.format(key))
+            sys.exit(1)
+
+    meraki = meraki_api.Meraki(key=config['api_key'])
+    orgs = meraki.get_organizations()
+    org = None
+    for o in orgs:
+        if o.get('name') == config['organization']:
+            org = o
+            break
+
+    if org is None:
+        print('Failed to find organization {} in this profile!'.format(
+            config['organization']))
+        sys.exit(1)
+
+    nets = org.get_networks()
+    inv = org.get_inventory()
+
+    errors = 0
+
+    configure_nets = None
+    if args.networks is not None:
+        configure_nets = args.networks.split(',')
+
+    for net in config['networks']:
+        nerrors = 0
+        net_obj = None
+        nname = net.keys()[0]
+        print(BANNER.format('Configuring network {}'.format(nname)))
+        if configure_nets is not None and nname not in configure_nets:
+            print('{}skipping (not in specified network list){}'.format(
+                Fore.BLUE, Style.RESET_ALL))
+            continue
+
+        validn = True
+        for key in ['address', 'timezone']:
+            if key not in net[nname]:
+                print('{}Invalid network config for {}: {} is missing!{}'.format(
+                    Fore.RED, nname, key, Style.RESET_ALL))
+                errors += 1
+                validn = False
+                break
+
+        if not validn:
+            continue
+
+        for n in nets:
+            if n.get('name') == nname:
+                net_obj = n
+                break
+
+        if net_obj is None:
+            nargs = {
+                'timezone': net[nname]['timezone']
+            }
+            if 'copy_from_network' in net[nname]:
+                for n in nets:
+                    if n.get('name') == net[nname]['copy_from_network']:
+                        nargs['copy_from_network_id'] = n.get('id')
+                        break
+            net_obj = org.create_network(
+                nname, **nargs)
+
+        if net_obj is None:
+            print('{}Error creating new network {}!{}'.format(
+                Fore.RED, nname, Style.RESET_ALL))
+            errors += 1
+            continue
+
+        if 'devices' in net[nname]:
+            for dev in net[nname]['devices']:
+                serial = dev.keys()[0]
+                if 'name' not in dev[serial]:
+                    print('{}Invalid device {}: name is missing!{}'.format(
+                        Fore.RED, serial, Style.RESET_ALL))
+                    nerrors += 1
+                    continue
+
+                inv_dev = filter(
+                    lambda device: device['serial'] == serial, inv)
+                if len(inv_dev) == 1:
+                    dev_obj = None
+                    if inv_dev[0]['networkId'] is not None and inv_dev[0]['networkId'] != net_obj.get('id'):
+                        try:
+                            inv_net_obj = meraki_api.Network(
+                                key=config['api_key'], id=inv_dev[0]['networkId'])
+                            dev_obj = meraki_api.Device(
+                                key=config['api_key'], id=inv_dev[0]['serial'], net=inv_net_obj)
+
+                            res = dev_obj.remove_device()
+                            if not res:
+                                print('{}Error removing {} from network {}!{}'.format(
+                                    Fore.RED, inv_dev[0]['serial'], inv_dev[0]['networkId'], Style.RESET_ALL))
+                                nerrors += 1
+                                continue
+                            print('{}update: removed {} from network {}{}'.format(
+                                Fore.YELLOW, inv_dev[0]['serial'], inv_dev[0]['networkId'], Style.RESET_ALL))
+                            res = net_obj.claim_device(dev_obj)
+                            if not res:
+                                print('{}Error claiming {}!{}'.format(
+                                    Fore.RED, inv_dev[0]['serial'], Style.RESET_ALL))
+                                nerrors += 1
+                                continue
+                            print('{}update: claimed {}{}'.format(
+                                Fore.YELLOW, inv_dev[0]['serial'], Style.RESET_ALL))
+                            dev_obj = res
+                        except Exception as e:
+                            print('{}Error updating device network membership for {}: {}{}'.format(
+                                Fore.RED, inv_dev[0]['serial'], e, Style.RESET_ALL))
+                            nerrors += 1
+                            continue
+                    elif inv_dev[0]['networkId'] is None:
+                        try:
+                            dev_obj = meraki_api.Device(
+                                key=config['api_key'], id=inv_dev[0]['serial'], net=net_obj)
+                            res = net_obj.claim_device(dev_obj)
+                            if not res:
+                                print('{}Error claiming device {}{}'.format(
+                                    Fore.RED, inv_dev[0]['serial'], Style.RESET_ALL))
+                                nerrors += 1
+                                continue
+                            print('{}update: claimed {}{}'.format(
+                                Fore.YELLOW, inv_dev[0]['serial'], Style.RESET_ALL))
+                            dev_obj = res
+                        except Exception as e:
+                            print('{}Error claiming device {}: {}{}'.format(
+                                Fore.RED, inv_dev[0]['serial'], e, Style.RESET_ALL))
+                            nerrors += 1
+                            continue
+
+                    else:
+                        dev_obj = meraki_api.Device(
+                            key=config['api_key'], id=inv_dev[0]['serial'], net=net_obj)
+                        print('{}ok: {} is in network{}'.format(
+                            Fore.GREEN, inv_dev[0]['serial'], Style.RESET_ALL))
+
+                    dev_location = net[nname]['address']
+                    dev_name = dev[serial]['name']
+                    if 'location' in dev[serial]:
+                        dev_location += '\n' + dev[serial]['location']
+                    dev_obj.update_device(
+                        name=dev_name, address=dev_location, move_map_marker=True)
+                    print('{}update: updated {} name and location{}'.format(
+                        Fore.YELLOW, inv_dev[0]['serial'], Style.RESET_ALL))
+
+                else:
+                    print('{}Error finding {} in inventory!{}'.format(
+                        Fore.RED, serial, Style.RESET_ALL))
+                    nerrors += 1
+
+        if 'vlans' in net[nname]:
+            # Ugh.  There is no API to enable VLANs yet.  So it's best to
+            # make this a manual step.  We could interact over the web, but
+            # then we'd need to ask for a real user's credentials.
+            #
+            # If we copied from an existing network, then we assume that
+            # network has VLANs enabled.  If not, this will fail.
+            #
+            if 'copy_from_network' not in net[nname]:
+                print('\n')
+                raw_input(
+                    '!!! Enable VLANs for network "{}" manually in the dashboard (under Security appliance > Addressing & VLANs), then hit enter to proceed !!!'.format(nname))
+                print('')
+            for vlan in net[nname]['vlans']:
+                vname = vlan.keys()[0]
+                done_msg = ''
+                if int(vlan[vname]['id']) != 1:
+                    vlan_obj = net_obj.create_vlan(
+                        vname, vlan[vname]['id'], vlan[vname]['subnet'], vlan[vname]['appliance_ip'])
+                    done_msg = '{}update: created VLAN {} (id={}, subnet={}, appliance_ip={}){}'.format(
+                        Fore.YELLOW, vname, vlan[vname]['id'], vlan[vname]['subnet'], vlan[vname]['appliance_ip'], Style.RESET_ALL)
+                else:
+                    vlan_obj = meraki_api.Vlan(
+                        key=config['api_key'], id=1, net=net_obj)
+                    done_msg = '{}ok: VLAN with ID {} exists{}'.format(
+                        Fore.GREEN, vlan[vname]['id'], Style.RESET_ALL)
+                if vlan_obj is None:
+                    print('{}Error creating VLAN {} (id={}, subnet={}, appliance_ip={})!{}'.format(
+                        Fore.RED, vlan[vname]['id'], vlan[vname]['subnet'], vlan[vname]['appliance_ip'], Style.RESET_ALL))
+                    nerrors += 1
+                    continue
+                print(done_msg)
+                vargs = {}
+                for key in ['reserved_ip_ranges', 'fixed_ip_assignments', 'dns_nameservers']:
+                    if key in vlan[vname]:
+                        vargs[key] = vlan[vname][key]
+                res = vlan_obj.update_vlan(**vargs)
+                vargs_str = ', '.join(['{}={}'.format(k, v)
+                                       for k, v in vargs.iteritems()])
+                if not res:
+                    print('{}Error updating VLAN {} ({})!{}'.format(
+                        Fore.RED, vname, vargs_str, Style.RESET_ALL))
+                    nerrors += 1
+                else:
+                    print('{}update: Update VLAN {} ({}){}'.format(
+                        Fore.YELLOW, vname, vargs_str, Style.RESET_ALL))
+
+        if 'ssids' in net[nname]:
+            if len(net[nname]['ssids']) > 15:
+                print('{}Only fifteen SSIDs are allowed per network!{}'.format(
+                    Fore.RED, Style.RESET_ALL))
+                nerrors += 1
+            else:
+                si = 0
+                for ssid in net[nname]['ssids']:
+                    sname = ssid.keys()[0]
+                    ssid_obj = meraki_api.SSID(
+                        key=config['api_key'], id=si, name=sname, net=net_obj)
+                    sargs = {}
+                    for key in ['name', 'enabled', 'auth_mode', 'encryption_mode', 'psk', 'ip_assignment_mode']:
+                        if key in ssid[sname]:
+                            sargs[key] = ssid[sname][key]
+                    res = ssid_obj.update_ssid(**sargs)
+                    sargs_str = ', '.join(
+                        ['{}={}'.format(k, v) for k, v in sargs.iteritems()])
+                    if not res:
+                        print('{}Error updating SSID {} ({})!{}'.format(
+                            Fore.RED, sname, sargs_str, Style.RESET_ALL))
+                        nerrors += 1
+                    else:
+                        print('{}update: Update SSID {} ({}){}'.format(
+                            Fore.YELLOW, sname, sargs_str, Style.RESET_ALL))
+                    si += 1
+
+        if 'switches' in net[nname]:
+            for switch in net[nname]['switches']:
+                serial = switch.keys()[0]
+                dev_obj = meraki_api.Device(
+                    key=config['api_key'], id=serial, net=net_obj)
+                if not dev_obj.realize():
+                    print('{}Device {} is not in network {}{}'.format(
+                        Fore.RED, serial, net_obj.get('name')), Style.RESET_ALL)
+                    nerrors += 1
+                    continue
+
+                for switchport in switch[serial]:
+                    port_range = switchport.keys()[0]
+                    ports = []
+                    if isinstance(port_range, (int, long)):
+                        port_obj = meraki_api.SwitchPort(
+                            key=config['api_key'], id=port_range, dev=dev_obj)
+                        ports.append(port_obj)
+                    else:
+                        prs = port_range.split(',')
+                        for pr in prs:
+                            pr = pr.strip()
+                            if isinstance(pr, (int, long)):
+                                port_obj = meraki_api.SwitchPort(
+                                    key=config['api_key'], id=pr, dev=dev_obj)
+                                ports.append(pr)
+                            else:
+                                if '-' not in pr:
+                                    print('{}Port range {} is invalid.{}'.format(
+                                        Fore.RED, pr, Style.RESET_ALL))
+                                    nerrors += 1
+                                    continue
+
+                                (start, end) = pr.split('-')
+                                start = start.strip()
+                                end = end.strip()
+                                if not isinstance(start, (int, long)) or not isinstance(end, (int, long)):
+                                    print('{}Error with port range {} and {} must be integers{}'.format(
+                                        Fore.RED, pr, start, end, Style.RESET_ALL))
+                                    nerrors += 1
+                                    continue
+
+                                if start >= end:
+                                    print(
+                                        '{}Error with port range {}; start must be less than end{}'.format(Fore.RED, pr, Style.RESET_ALL))
+                                    nerrors += 1
+                                    continue
+
+                                pi = start
+                                while pi <= end:
+                                    port_obj = meraki_api.SwitchPort(
+                                        key=config['api_key'], id=pi, dev=dev_obj)
+                                    ports.append(port_obj)
+                                    pi += 1
+
+                    for port in ports:
+                        pargs = {}
+                        for key in ['name', 'tags', 'enabled', 'type', 'vlan', 'voice_vlan', 'allowed_vlans', 'poe_enabled']:
+                            if key in switchport[port_range]:
+                                pargs[key] = switchport[port_range][key]
+                        res = port.update_switchport(**pargs)
+                        pargs_str = ', '.join(
+                            ['{}={}'.format(k, v) for k, v in pargs.iteritems()])
+                        if not res:
+                            print('{}Error updating switchport range {} ({}){}'.format(
+                                Fore.RED, port_range, pargs_str, Style.RESET_ALL))
+                            nerrors += 1
+                        else:
+                            print('{}update: Update switchport range {} ({}){}'.format(
+                                Fore.YELLOW, port_range, pargs_str, Style.RESET_ALL))
+
+        if nerrors == 0:
+            print('{}ok: network {} has been setup successfully!{}\n'.format(
+                Fore.GREEN, nname, Style.RESET_ALL))
+        else:
+            print(
+                '{}Error fully configuring network {}.  See the errors above for more details.{}\n'.format(Fore.RED, nname, Style.RESET_ALL))
+            errors += nerrors
+
+    if errors == 0:
+        print('{}ok: all networks have been setup successfully!{}\n'.format(
+            Fore.GREEN, Style.RESET_ALL))
+    else:
+        print('{}There were errors setting up some of the networks.  See the output above for more details.{}\n'.format(
+            Fore.RED, Style.RESET_ALL))
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()