Merge "Configure pagination"
diff --git a/.gitignore b/.gitignore
index 1bfce6e..aa8e42a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
+.kitchen
 tests/build/
 *.swp
 *.pyc
-.ropeproject
\ No newline at end of file
+.ropeproject
diff --git a/Makefile b/Makefile
index fc83783..1043fbe 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,38 @@
 DESTDIR=/
 SALTENVDIR=/usr/share/salt-formulas/env
 RECLASSDIR=/usr/share/salt-formulas/reclass
-FORMULANAME=$(shell grep name: metadata.yml|head -1|cut -d : -f 2|grep -Eo '[a-z0-9\-]*')
+FORMULANAME=$(shell grep name: metadata.yml|head -1|cut -d : -f 2|grep -Eo '[a-z0-9\-\_]*')
+VERSION=$(shell grep version: metadata.yml|head -1|cut -d : -f 2|grep -Eo '[a-z0-9\.\-\_]*')
+VERSION_MAJOR := $(shell echo $(VERSION)|cut -d . -f 1-2)
+VERSION_MINOR := $(shell echo $(VERSION)|cut -d . -f 3)
+
+NEW_MAJOR_VERSION ?= $(shell date +%Y.%m|sed 's,\.0,\.,g')
+NEW_MINOR_VERSION ?= $(shell /bin/bash -c 'echo $$[ $(VERSION_MINOR) + 1 ]')
+
+MAKE_PID := $(shell echo $$PPID)
+JOB_FLAG := $(filter -j%, $(subst -j ,-j,$(shell ps T | grep "^\s*$(MAKE_PID).*$(MAKE)")))
+
+ifneq ($(subst -j,,$(JOB_FLAG)),)
+JOBS := $(subst -j,,$(JOB_FLAG))
+else
+JOBS := 1
+endif
+
+KITCHEN_LOCAL_YAML?=.kitchen.yml
+KITCHEN_OPTS?="--concurrency=$(JOBS)"
+KITCHEN_OPTS_CREATE?=""
+KITCHEN_OPTS_CONVERGE?=""
+KITCHEN_OPTS_VERIFY?=""
+KITCHEN_OPTS_TEST?=""
 
 all:
 	@echo "make install - Install into DESTDIR"
 	@echo "make test    - Run tests"
+	@echo "make kitchen - Run Kitchen CI tests (create, converge, verify)"
 	@echo "make clean   - Cleanup after tests run"
+	@echo "make release-major  - Generate new major release"
+	@echo "make release-minor  - Generate new minor release"
+	@echo "make changelog      - Show changes since last release"
 
 install:
 	# Formula
@@ -14,6 +40,7 @@
 	cp -a $(FORMULANAME) $(DESTDIR)/$(SALTENVDIR)/
 	[ ! -d _modules ] || cp -a _modules $(DESTDIR)/$(SALTENVDIR)/
 	[ ! -d _states ] || cp -a _states $(DESTDIR)/$(SALTENVDIR)/ || true
+	[ ! -d _grains ] || cp -a _grains $(DESTDIR)/$(SALTENVDIR)/ || true
 	# Metadata
 	[ -d $(DESTDIR)/$(RECLASSDIR)/service/$(FORMULANAME) ] || mkdir -p $(DESTDIR)/$(RECLASSDIR)/service/$(FORMULANAME)
 	cp -a metadata/service/* $(DESTDIR)/$(RECLASSDIR)/service/$(FORMULANAME)
@@ -21,6 +48,71 @@
 test:
 	[ ! -d tests ] || (cd tests; ./run_tests.sh)
 
+release-major: check-changes
+	@echo "Current version is $(VERSION), new version is $(NEW_MAJOR_VERSION)"
+	@[ $(VERSION_MAJOR) != $(NEW_MAJOR_VERSION) ] || (echo "Major version $(NEW_MAJOR_VERSION) already released, nothing to do. Do you want release-minor?" && exit 1)
+	echo "$(NEW_MAJOR_VERSION)" > VERSION
+	sed -i 's,version: .*,version: "$(NEW_MAJOR_VERSION)",g' metadata.yml
+	[ ! -f debian/changelog ] || dch -v $(NEW_MAJOR_VERSION) -m --force-distribution -D `dpkg-parsechangelog -S Distribution` "New version"
+	make genchangelog-$(NEW_MAJOR_VERSION)
+	(git add -u; git commit -m "Version $(NEW_MAJOR_VERSION)")
+	git tag -s -m $(NEW_MAJOR_VERSION) $(NEW_MAJOR_VERSION)
+
+release-minor: check-changes
+	@echo "Current version is $(VERSION), new version is $(VERSION_MAJOR).$(NEW_MINOR_VERSION)"
+	echo "$(VERSION_MAJOR).$(NEW_MINOR_VERSION)" > VERSION
+	sed -i 's,version: .*,version: "$(VERSION_MAJOR).$(NEW_MINOR_VERSION)",g' metadata.yml
+	[ ! -f debian/changelog ] || dch -v $(VERSION_MAJOR).$(NEW_MINOR_VERSION) -m --force-distribution -D `dpkg-parsechangelog -S Distribution` "New version"
+	make genchangelog-$(VERSION_MAJOR).$(NEW_MINOR_VERSION)
+	(git add -u; git commit -m "Version $(VERSION_MAJOR).$(NEW_MINOR_VERSION)")
+	git tag -s -m $(NEW_MAJOR_VERSION) $(VERSION_MAJOR).$(NEW_MINOR_VERSION)
+
+check-changes:
+	@git log --pretty=oneline --decorate $(VERSION)..HEAD | grep -Eqc '.*' || (echo "No new changes since version $(VERSION)"; exit 1)
+
+changelog:
+	git log --pretty=short --invert-grep --grep="Merge pull request" --decorate $(VERSION)..HEAD
+
+genchangelog: genchangelog-$(VERSION_MAJOR).$(NEW_MINOR_VERSION)
+
+genchangelog-%:
+	$(eval NEW_VERSION := $(patsubst genchangelog-%,%,$@))
+	(echo "=========\nChangelog\n=========\n"; \
+	(echo $(NEW_VERSION);git tag) | sort -r | grep -E '^[0-9\.]+' | while read i; do \
+	    cur=$$i; \
+	    test $$i = $(NEW_VERSION) && i=HEAD; \
+	    prev=`(echo $(NEW_VERSION);git tag)|sort|grep -E '^[0-9\.]+'|grep -B1 "$$cur\$$"|head -1`; \
+	    echo "Version $$cur\n=============================\n"; \
+	    git log --pretty=short --invert-grep --grep="Merge pull request" --decorate $$prev..$$i; \
+	    echo; \
+	done) > CHANGELOG.rst
+
+kitchen-check:
+	@[ -e $(KITCHEN_LOCAL_YAML) ] || (echo "Kitchen tests not available, there's no $(KITCHEN_LOCAL_YAML)." && exit 1)
+
+kitchen: kitchen-check kitchen-create kitchen-converge kitchen-verify kitchen-list
+
+kitchen-create: kitchen-check
+	kitchen create ${KITCHEN_OPTS} ${KITCHEN_OPTS_CREATE}
+	[ "$(shell echo $(KITCHEN_LOCAL_YAML)|grep -Eo docker)" = "docker" ] || sleep 120
+
+kitchen-converge: kitchen-check
+	kitchen converge ${KITCHEN_OPTS} ${KITCHEN_OPTS_CONVERGE} &&\
+	kitchen converge ${KITCHEN_OPTS} ${KITCHEN_OPTS_CONVERGE}
+
+kitchen-verify: kitchen-check
+	[ ! -d tests/integration ] || kitchen verify -t tests/integration ${KITCHEN_OPTS} ${KITCHEN_OPTS_VERIFY}
+	[ -d tests/integration ]   || kitchen verify ${KITCHEN_OPTS} ${KITCHEN_OPTS_VERIFY}
+
+kitchen-test: kitchen-check
+	[ ! -d tests/integration ] || kitchen test -t tests/integration ${KITCHEN_OPTS} ${KITCHEN_OPTS_TEST}
+	[ -d tests/integration ]   || kitchen test ${KITCHEN_OPTS} ${KITCHEN_OPTS_TEST}
+
+kitchen-list: kitchen-check
+	kitchen list
+
 clean:
+	[ ! -x "$(shell which kitchen)" ] || kitchen destroy
+	[ ! -d .kitchen ] || rm -rf .kitchen
 	[ ! -d tests/build ] || rm -rf tests/build
 	[ ! -d build ] || rm -rf build
diff --git a/README.rst b/README.rst
index 4d4d1ca..bcce38e 100644
--- a/README.rst
+++ b/README.rst
@@ -630,3 +630,36 @@
 Developers should also join the discussion on the IRC list, at:
 
     https://wiki.openstack.org/wiki/Meetings/openstack-salt
+
+Documentation and Bugs
+======================
+
+To learn how to install and update salt-formulas, consult the documentation
+available online at:
+
+    http://salt-formulas.readthedocs.io/
+
+In the unfortunate event that bugs are discovered, they should be reported to
+the appropriate issue tracker. Use Github issue tracker for specific salt
+formula:
+
+    https://github.com/salt-formulas/salt-formula-neutron/issues
+
+For feature requests, bug reports or blueprints affecting entire ecosystem,
+use Launchpad salt-formulas project:
+
+    https://launchpad.net/salt-formulas
+
+You can also join salt-formulas-users team and subscribe to mailing list:
+
+    https://launchpad.net/~salt-formulas-users
+
+Developers wishing to work on the salt-formulas projects should always base
+their work on master branch and submit pull request against specific formula.
+
+    https://github.com/salt-formulas/salt-formula-neutron
+
+Any questions or feedback is always welcome so feel free to join our IRC
+channel:
+
+    #salt-formulas @ irc.freenode.net
diff --git a/neutron/_modules/neutronng.py b/neutron/_modules/neutronng.py
new file mode 100644
index 0000000..ba7060d
--- /dev/null
+++ b/neutron/_modules/neutronng.py
@@ -0,0 +1,419 @@
+# -*- coding: utf-8 -*-
+
+import logging
+from functools import wraps
+LOG = logging.getLogger(__name__)
+# Import third party libs
+HAS_NEUTRON = False
+try:
+    from neutronclient.v2_0 import client
+    HAS_NEUTRON = True
+except ImportError:
+    pass
+
+__opts__ = {}
+
+
+def __virtual__():
+    '''
+    Only load this module if neutron
+    is installed on this minion.
+    '''
+    if HAS_NEUTRON:
+        return 'neutronng'
+    return False
+
+
+def _autheticate(func_name):
+    '''
+    Authenticate requests with the salt keystone module and format return data
+    '''
+    @wraps(func_name)
+    def decorator_method(*args, **kwargs):
+        '''
+        Authenticate request and format return data
+        '''
+        connection_args = {'profile': kwargs.get('profile', None)}
+        nkwargs = {}
+        for kwarg in kwargs:
+            if 'connection_' in kwarg:
+                connection_args.update({kwarg: kwargs[kwarg]})
+            elif '__' not in kwarg:
+                nkwargs.update({kwarg: kwargs[kwarg]})
+        kstone = __salt__['keystone.auth'](**connection_args)
+        token = kstone.auth_token
+        endpoint = kstone.service_catalog.url_for(
+            service_type='network',
+            endpoint_type='publicURL')
+        neutron_interface = client.Client(
+            endpoint_url=endpoint, token=token)
+        return_data = func_name(neutron_interface, *args, **nkwargs)
+        if isinstance(return_data, list):
+            # format list as a dict for rendering
+            return {data.get('name', None) or data['id']: data
+                    for data in return_data}
+        return return_data
+    return decorator_method
+
+
+@_autheticate
+def list_floatingips(neutron_interface, **kwargs):
+    '''
+    list all floatingips
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.list_floatingips
+    '''
+    return neutron_interface.list_floatingips(**kwargs)['floatingips']
+
+
+@_autheticate
+def list_security_groups(neutron_interface, **kwargs):
+    '''
+    list all security_groups
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.list_security_groups
+    '''
+    return neutron_interface.list_security_groups(**kwargs)['security_groups']
+
+
+@_autheticate
+def list_subnets(neutron_interface, **kwargs):
+    '''
+    list all subnets
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.list_subnets
+    '''
+    return neutron_interface.list_subnets(**kwargs)['subnets']
+
+
+@_autheticate
+def list_networks(neutron_interface, **kwargs):
+    '''
+    list all networks
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.list_networks
+    '''
+    return neutron_interface.list_networks(**kwargs)['networks']
+
+
+@_autheticate
+def list_ports(neutron_interface, **kwargs):
+    '''
+    list all ports
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.list_ports
+    '''
+    return neutron_interface.list_ports(**kwargs)['ports']
+
+
+@_autheticate
+def list_routers(neutron_interface, **kwargs):
+    '''
+    list all routers
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.list_routers
+    '''
+    return neutron_interface.list_routers(**kwargs)['routers']
+
+@_autheticate
+def update_floatingip(neutron_interface, fip, port_id=None):
+    '''
+    update floating IP. Should be used to associate and disassociate
+    floating IP with instance
+    CLI Example:
+    .. code-block:: bash
+        to associate with an instance's port
+        salt '*' neutron.update_floatingip openstack-floatingip-id port-id
+        to disassociate from an instance's port
+        salt '*' neutron.update_floatingip openstack-floatingip-id
+    '''
+    neutron_interface.update_floatingip(fip, {"floatingip":
+                                              {"port_id": port_id}})
+
+
+@_autheticate
+def update_subnet(neutron_interface, subnet_id, **subnet_params):
+    '''
+    update given subnet
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.update_subnet openstack-subnet-id name='new_name'
+    '''
+    neutron_interface.update_subnet(subnet_id, {'subnet': subnet_params})
+
+
+@_autheticate
+def update_network(neutron_interface, network_id, **net_params):
+    '''
+    Update give network
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.update_network openstack-net-id admin_state_up=false
+    '''
+    network_params = {}
+    for param in net_params:
+        if 'provider_' in param or 'router_' in param:
+            network_params[param.replace('_', ':', 1)] = net_params[param]
+        else:
+            network_params[param] = net_params[param]
+    LOG.info('ATTRIBUTES ' + str(network_params))
+    neutron_interface.update_network(network_id, {'network': network_params})
+
+
+@_autheticate
+def update_router(neutron_interface, router_id, **router_params):
+    '''
+    update given router
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.update_router openstack-router-id name='new_name'
+            external_gateway='openstack-network-id' administrative_state=true
+    '''
+    neutron_interface.update_router(router_id, {'router': router_params})
+
+
+@_autheticate
+def router_gateway_set(neutron_interface, router_id, external_gateway):
+    '''
+    Set external gateway for a router
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.update_router openstack-router-id openstack-network-id
+    '''
+    neutron_interface.update_router(
+        router_id, {'router': {'external_gateway_info':
+                               {'network_id': external_gateway}}})
+
+
+@_autheticate
+def router_gateway_clear(neutron_interface, router_id):
+    '''
+    Clear external gateway for a router
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.update_router openstack-router-id
+    '''
+    neutron_interface.update_router(
+        router_id, {'router': {'external_gateway_info': None}})
+
+
+@_autheticate
+def create_router(neutron_interface, **router_params):
+    '''
+    Create OpenStack Neutron router
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.create_router name=R1
+    '''
+    response = neutron_interface.create_router({'router': router_params})
+    if 'router' in response and 'id' in response['router']:
+        return response['router']['id']
+
+
+@_autheticate
+def router_add_interface(neutron_interface, router_id, subnet_id):
+    '''
+    Attach router to a subnet
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.router_add_interface openstack-router-id subnet-id
+    '''
+    neutron_interface.add_interface_router(router_id, {'subnet_id': subnet_id})
+
+
+@_autheticate
+def router_rem_interface(neutron_interface, router_id, subnet_id):
+    '''
+    Dettach router from a subnet
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.router_rem_interface openstack-router-id subnet-id
+    '''
+    neutron_interface.remove_interface_router(
+        router_id, {'subnet_id': subnet_id})
+
+
+@_autheticate
+def create_security_group(neutron_interface, **sg_params):
+    '''
+    Create a new security group
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.create_security_group name='new_rule'
+            description='test rule'
+    '''
+    response = neutron_interface.create_security_group(
+        {'security_group': sg_params})
+    if 'security_group' in response and 'id' in response['security_group']:
+        return response['security_group']['id']
+
+
+@_autheticate
+def create_security_group_rule(neutron_interface, **rule_params):
+    '''
+    Create a rule entry for a security group
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.create_security_group_rule
+    '''
+    neutron_interface.create_security_group_rule(
+        {'security_group_rule': rule_params})
+
+
+@_autheticate
+def create_floatingip(neutron_interface, **floatingip_params):
+    '''
+    Create a new floating IP
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.create_floatingip floating_network_id=ext-net-id
+    '''
+    response = neutron_interface.create_floatingip(
+        {'floatingip': floatingip_params})
+    if 'floatingip' in response and 'id' in response['floatingip']:
+        return response['floatingip']['id']
+
+
+@_autheticate
+def create_subnet(neutron_interface, **subnet_params):
+    '''
+    Create a new subnet in OpenStack
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.create_subnet name='subnet name'
+            network_id='openstack-network-id' cidr='192.168.10.0/24' \\
+            gateway_ip='192.168.10.1' ip_version='4' enable_dhcp=false \\
+            start_ip='192.168.10.10' end_ip='192.168.10.20'
+    '''
+    if 'start_ip' in subnet_params:
+        subnet_params.update(
+            {'allocation_pools': [{'start': subnet_params.pop('start_ip'),
+                                   'end': subnet_params.pop('end_ip', None)}]})
+    response = neutron_interface.create_subnet({'subnet': subnet_params})
+    if 'subnet' in response and 'id' in response['subnet']:
+        return response['subnet']['id']
+
+
+@_autheticate
+def create_network(neutron_interface, **net_params):
+    '''
+    Create a new network segment in OpenStack
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.create_network name=External
+            provider_network_type=flat provider_physical_network=ext
+    '''
+    network_params = {}
+    for param in net_params:
+        if 'provider_' in param or 'router_' in param:
+            network_params[param.replace('_', ':', 1)] = net_params[param]
+        else:
+            network_params[param] = net_params[param]
+    response = neutron_interface.create_network({'network': network_params})
+    if 'network' in response and 'id' in response['network']:
+        return response['network']['id']
+
+
+@_autheticate
+def create_port(neutron_interface, **port_params):
+    '''
+    Create a new port in OpenStack
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.create_port network_id='openstack-network-id'
+    '''
+    response = neutron_interface.create_port({'port': port_params})
+    if 'port' in response and 'id' in response['port']:
+        return response['port']['id']
+
+
+@_autheticate
+def update_port(neutron_interface, port_id, **port_params):
+    '''
+    Create a new port in OpenStack
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.update_port name='new_port_name'
+    '''
+    neutron_interface.update_port(port_id, {'port': port_params})
+
+
+@_autheticate
+def delete_floatingip(neutron_interface, floating_ip_id):
+    '''
+    delete a floating IP
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.delete_floatingip openstack-floating-ip-id
+    '''
+    neutron_interface.delete_floatingip(floating_ip_id)
+
+
+@_autheticate
+def delete_security_group(neutron_interface, sg_id):
+    '''
+    delete a security group
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.delete_security_group openstack-security-group-id
+    '''
+    neutron_interface.delete_security_group(sg_id)
+
+
+@_autheticate
+def delete_security_group_rule(neutron_interface, rule):
+    '''
+    delete a security group rule. pass all rule params that match the rule
+    to be deleted
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.delete_security_group_rule direction='ingress'
+            ethertype='ipv4' security_group_id='openstack-security-group-id'
+            port_range_min=100 port_range_max=4096 protocol='tcp'
+            remote_group_id='default'
+    '''
+    sg_rules = neutron_interface.list_security_group_rules(
+        security_group_id=rule['security_group_id'])
+    for sg_rule in sg_rules['security_group_rules']:
+        sgr_id = sg_rule.pop('id')
+        if sg_rule == rule:
+            neutron_interface.delete_security_group_rule(sgr_id)
+
+
+@_autheticate
+def delete_subnet(neutron_interface, subnet_id):
+    '''
+    delete given subnet
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.delete_subnet openstack-subnet-id
+    '''
+    neutron_interface.delete_subnet(subnet_id)
+
+
+@_autheticate
+def delete_network(neutron_interface, network_id):
+    '''
+    delete given network
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.delete_network openstack-network-id
+    '''
+    neutron_interface.delete_network(network_id)
+
+
+@_autheticate
+def delete_router(neutron_interface, router_id):
+    '''
+    delete given router
+    CLI Example:
+    .. code-block:: bash
+        salt '*' neutron.delete_router openstack-router-id
+    '''
+    neutron_interface.delete_router(router_id)
\ No newline at end of file
diff --git a/neutron/_states/neutronng.py b/neutron/_states/neutronng.py
new file mode 100644
index 0000000..3a7483b
--- /dev/null
+++ b/neutron/_states/neutronng.py
@@ -0,0 +1,528 @@
+# -*- coding: utf-8 -*-
+'''
+Management of Neutron resources
+===============================
+:depends:   - neutronclient Python module
+:configuration: See :py:mod:`salt.modules.neutron` for setup instructions.
+.. code-block:: yaml
+    neutron network present:
+      neutron.network_present:
+        - name: Netone
+        - provider_physical_network: PHysnet1
+        - provider_network_type: vlan
+'''
+import logging
+from functools import wraps
+LOG = logging.getLogger(__name__)
+
+
+def __virtual__():
+    '''
+    Only load if neutron module is present in __salt__
+    '''
+    return 'neutronng' if 'neutron.list_networks' in __salt__ else False
+
+
+def _test_call(method):
+    (resource, functionality) = method.func_name.split('_')
+    if functionality == 'present':
+        functionality = 'updated'
+    else:
+        functionality = 'removed'
+
+    @wraps(method)
+    def check_for_testing(name, *args, **kwargs):
+        if __opts__.get('test', None):
+            return _no_change(name, resource, test=functionality)
+        return method(name, *args, **kwargs)
+    return check_for_testing
+
+
+def _neutron_module_call(method, *args, **kwargs):
+    return __salt__['neutronng.{0}'.format(method)](*args, **kwargs)
+
+
+def _auth(profile=None):
+    '''
+    Set up neutron credentials
+    '''
+    if profile:
+        credentials = __salt__['config.option'](profile)
+        user = credentials['keystone.user']
+        password = credentials['keystone.password']
+        tenant = credentials['keystone.tenant']
+        auth_url = credentials['keystone.auth_url']
+
+    kwargs = {
+        'connection_user': user,
+        'connection_password': password,
+        'connection_tenant': tenant,
+        'connection_auth_url': auth_url
+    }
+
+    return kwargs
+
+@_test_call
+def network_present(name=None,
+                    provider_network_type=None,
+                    provider_physical_network=None,
+                    router_external=None,
+                    admin_state_up=None,
+                    shared=None,
+                    provider_segmentation_id=None,
+                    profile=None):
+    '''
+    Ensure that the neutron network is present with the specified properties.
+    name
+        The name of the network to manage
+    '''
+    #for p in connection_args: print p
+    connection_args = _auth(profile)
+    print connection_args
+    for p in connection_args: print p
+    print("neco")
+    print provider_network_type
+    print name
+    print provider_physical_network
+    print router_external
+    print admin_state_up
+    print shared
+    print provider_segmentation_id
+    existing_network = _neutron_module_call(
+        'list_networks', name=name, **connection_args)
+    network_arguments = _get_non_null_args(
+        name=name,
+        provider_network_type=provider_network_type,
+        provider_physical_network=provider_physical_network,
+        router_external=router_external,
+        admin_state_up=admin_state_up,
+        shared=shared,
+        provider_segmentation_id=provider_segmentation_id)
+    #for p in network_arguments: print p
+    if not existing_network:
+        network_arguments.update(connection_args)
+        _neutron_module_call('create_network', **network_arguments)
+        existing_network = _neutron_module_call(
+            'list_networks', name=name, **connection_args)
+        if existing_network:
+            return _created(name, 'network', existing_network[name])
+        return _update_failed(name, 'network')
+    # map internal representation to display format
+    #for p in connection_args: print p
+    LOG.info('CONNECTION STRINGS' + str(connection_args))
+    LOG.info('existing ' + str(existing_network))
+    LOG.info('new ' + str(network_arguments))
+    existing_network = dict((key.replace(':', '_', 1), value)
+                            for key, value in
+                            existing_network[name].iteritems())
+    # generate differential
+    diff = dict((key, value) for key, value in network_arguments.iteritems()
+                if existing_network.get(key, None) != value)
+    if diff:
+        # update the changes
+        network_arguments = diff.copy()
+        network_arguments.update(connection_args)
+        try:
+            LOG.debug('updating network {0} with changes {1}'.format(
+                name, str(diff)))
+            _neutron_module_call('update_network',
+                                 existing_network['id'],
+                                 **network_arguments)
+            changes_dict = _created(name, 'network', diff)
+            changes_dict['comment'] = '{1} {0} updated'.format(name, 'network')
+            return changes_dict
+        except:
+            LOG.exception('Could not update network {0}'.format(name))
+            return _update_failed(name, 'network')
+    return _no_change(name, 'network')
+
+
+@_test_call
+def network_absent(name, profile=None):
+    connection_args = _auth(profile)
+    existing_network = _neutron_module_call(
+        'list_networks', name=name, **connection_args)
+    if existing_network:
+        _neutron_module_call(
+            'delete_network', existing_network[name]['id'], **connection_args)
+        if _neutron_module_call('list_networks', name=name, **connection_args):
+            return _delete_failed(name, 'network')
+        return _deleted(name, 'network', existing_network[name])
+    return _absent(name, 'network')
+
+
+@_test_call
+def subnet_present(name=None,
+                   network=None,
+                   cidr=None,
+                   ip_version=4,
+                   enable_dhcp=True,
+                   allocation_pools=None,
+                   gateway_ip=None,
+                   dns_nameservers=None,
+                   host_routes=None,
+                   profile=None):
+    '''
+    Ensure that the neutron subnet is present with the specified properties.
+    name
+        The name of the subnet to manage
+    '''
+    connection_args = _auth(profile)
+
+    existing_subnet = _neutron_module_call(
+        'list_subnets', name=name, **connection_args)
+    subnet_arguments = _get_non_null_args(
+        name=name,
+        network=network,
+        cidr=cidr,
+        ip_version=ip_version,
+        enable_dhcp=enable_dhcp,
+        allocation_pools=allocation_pools,
+        gateway_ip=gateway_ip,
+        dns_nameservers=dns_nameservers,
+        host_routes=host_routes)
+    # replace network with network_id
+    if 'network' in subnet_arguments:
+        network = subnet_arguments.pop('network', None)
+        existing_network = _neutron_module_call(
+            'list_networks', name=network, **connection_args)
+        if existing_network:
+            subnet_arguments['network_id'] = existing_network[network]['id']
+    if not existing_subnet:
+        subnet_arguments.update(connection_args)
+        _neutron_module_call('create_subnet', **subnet_arguments)
+        existing_subnet = _neutron_module_call(
+            'list_subnets', name=name, **connection_args)
+        if existing_subnet:
+            return _created(name, 'subnet', existing_subnet[name])
+        return _update_failed(name, 'subnet')
+    # change from internal representation
+    existing_subnet = existing_subnet[name]
+    # create differential
+    LOG.error('existing ' + str(existing_subnet))
+    LOG.error('new ' + str(subnet_arguments))
+    diff = dict((key, value) for key, value in subnet_arguments.iteritems()
+                if existing_subnet.get(key, None) != value)
+    if diff:
+        # update the changes
+        subnet_arguments = diff.copy()
+        subnet_arguments.update(connection_args)
+        try:
+            LOG.debug('updating subnet {0} with changes {1}'.format(
+                name, str(diff)))
+            _neutron_module_call('update_subnet',
+                                 existing_subnet['id'],
+                                 **subnet_arguments)
+            changes_dict = _created(name, 'subnet', diff)
+            changes_dict['comment'] = '{1} {0} updated'.format(name, 'subnet')
+            return changes_dict
+        except:
+            LOG.exception('Could not update subnet {0}'.format(name))
+            return _update_failed(name, 'subnet')
+    return _no_change(name, 'subnet')
+
+
+@_test_call
+def subnet_absent(name, profile=None):
+    connection_args = _auth(profile)
+    existing_subnet = _neutron_module_call(
+        'list_subnets', name=name, **connection_args)
+    if existing_subnet:
+        _neutron_module_call(
+            'delete_subnet', existing_subnet[name]['id'], **connection_args)
+        if _neutron_module_call('list_subnets', name=name, **connection_args):
+            return _delete_failed(name, 'subnet')
+        return _deleted(name, 'subnet', existing_subnet[name])
+    return _absent(name, 'subnet')
+    return _absent(name, 'network')
+
+
+@_test_call
+def router_present(name=None,
+                   gateway_network=None,
+                   interfaces=None,
+                   admin_state_up=None,
+                   profile=None):
+    '''
+    Ensure that the neutron router is present with the specified properties.
+    name
+        The name of the subnet to manage
+    gateway_network
+        The network that would be the router's default gateway
+    interfaces
+        list of subnets the router attaches to
+    '''
+    connection_args = _auth(profile)
+    existing_router = _neutron_module_call(
+        'list_routers', name=name, **connection_args)
+    if not existing_router:
+        _neutron_module_call('create_router', name=name, **connection_args)
+        created_router = _neutron_module_call(
+            'list_routers', name=name, **connection_args)
+        if created_router:
+            router_id = created_router[name]['id']
+            network = _neutron_module_call(
+                'list_networks', name=gateway_network, **connection_args)
+            gateway_network_id = network[gateway_network]['id']
+            _neutron_module_call('router_gateway_set',
+                                 router_id=router_id,
+                                 external_gateway=gateway_network_id,
+                                 **connection_args)
+            for interface in interfaces:
+                subnet = _neutron_module_call(
+                    'list_subnets', name=interface, **connection_args)
+                subnet_id = subnet[interface]['id']
+                _neutron_module_call('router_add_interface',
+                                     router_id=router_id,
+                                     subnet_id=subnet_id,
+                                     **connection_args)
+            return _created(name,
+                            'router',
+                            _neutron_module_call('list_routers',
+                                                 name=name,
+                                                 **connection_args))
+        return _create_failed(name, 'router')
+
+    router_id = existing_router[name]['id']
+    existing_router = existing_router[name]
+    diff = {}
+    if admin_state_up and existing_router['admin_state_up'] != admin_state_up:
+        diff.update({'admin_state_up': admin_state_up})
+    if gateway_network:
+        network = _neutron_module_call(
+            'list_networks', name=gateway_network, **connection_args)
+        gateway_network_id = network[gateway_network]['id']
+        if not existing_router['external_gateway_info'] and not existing_router['external_gateway_info'] == None:
+            if existing_router['external_gateway_info']['network_id'] != gateway_network_id:
+                diff.update({'external_gateway_info': {'network_id': gateway_network_id}})
+        elif not 'network_id' in existing_router['external_gateway_info'] or existing_router['external_gateway_info']['network_id'] != gateway_network_id:
+            diff.update({'external_gateway_info': {'network_id': gateway_network_id}})
+    if diff:
+        # update the changes
+        router_args = diff.copy()
+        router_args.update(connection_args)
+        try:
+            _neutron_module_call('update_router', existing_router['id'], **router_args)
+            changes_dict = _created(name, 'router', diff)
+            changes_dict['comment'] = 'Router {0} updated'.format(name)
+            return changes_dict
+        except:
+            LOG.exception('Router {0} could not be updated'.format(name))
+            return _update_failed(name, 'router')
+    return _no_change(name, 'router')
+
+def security_group_present(name=None,
+                           description=None,
+                           rules=[],
+                           profile=None):
+    '''
+    Ensure that the security group is present with the specified properties.
+    name
+        The name of the security group
+    description
+        The description of the security group
+    rules
+        list of rules to be added to the given security group
+    '''
+    #for p in rules: print p
+    # If the user is an admin, he's able to see the security groups from
+    # other tenants. In this case, we'll use the tenant id to get an existing
+    # security group.
+    connection_args = _auth(profile)
+    tenant_name = connection_args['connection_tenant']
+    try:
+        tenant_id = __salt__['keystone.tenant_get'](
+            name=tenant_name, **connection_args)[tenant_name]['id']
+    except:
+        tenant_id = None
+        LOG.debug('Cannot get the tenant id. User {0} is not an admin.'.format(
+            connection_args['connection_user']))
+    if tenant_id:
+        security_group = _neutron_module_call(
+            'list_security_groups', name=name, tenant_id=tenant_id,
+            **connection_args)
+    else:
+        security_group = _neutron_module_call(
+            'list_security_groups', name=name, **connection_args)
+
+    if not security_group:
+        # Create the security group as it doesn't exist already.
+        security_group_id = _neutron_module_call('create_security_group',
+                                                 name=name,
+                                                 description=description,
+                                                 **connection_args)
+    else:
+        security_group_id = security_group[name]['id']
+
+    # Set the missing rules attributes (in case the user didn't specify them
+    # in pillar) to some default values.
+    rules_attributes_defaults = {
+        'direction': 'ingress',
+        'ethertype': 'IPv4',
+        'protocol': 'TCP',
+        'port_range_min': None,
+        'port_range_max': None,
+        'remote_ip_prefix': None
+    }
+    for rule in rules:
+        for attribute in rules_attributes_defaults.keys():
+            if not rule.has_key(attribute):
+                rule[attribute] = rules_attributes_defaults[attribute]
+
+    for rule in rules:
+        for attribute in rules_attributes_defaults.keys():
+            print rule
+            print rule[attribute]
+
+    # Remove all the duplicates rules given by the user in pillar.
+    unique_rules = []
+    for rule in rules:
+        if rule not in unique_rules:
+            unique_rules.append(rule)
+
+    #for p in unique_rules: print p
+
+    # Get the existing security group rules.
+    existing_rules = _neutron_module_call(
+        'list_security_groups',
+        id=security_group_id,
+        **connection_args)[name]['security_group_rules']
+
+    #for p in existing_rules: print p
+
+    new_rules = {}
+    for rule in unique_rules:
+        rule_found = False
+        for existing_rule in existing_rules:
+            attributes_match = True
+            # Compare the attributes of the existing security group rule with
+            # the attributes of the rule that we want to add.
+            for attribute in rules_attributes_defaults.keys():
+                existing_attribute = '' if not existing_rule[attribute] \
+                                        else str(existing_rule[attribute]).lower()
+                attribute = '' if not rule[attribute] \
+                               else str(rule[attribute]).lower()
+                if existing_attribute != attribute:
+                    attributes_match = False
+                    break
+            if attributes_match:
+                rule_found = True
+                break
+        if rule_found:
+            # Skip adding the rule as it already exists.
+            continue
+        rule_index = len(new_rules) + 1
+        new_rules.update({'Rule {0}'.format(rule_index): rule})
+        print security_group_id
+        print rule['direction']
+        print rule['ethertype']
+        print rule['protocol']
+        print rule['port_range_min']
+        print rule['port_range_max']
+        print rule['remote_ip_prefix']
+        _neutron_module_call('create_security_group_rule',
+                             security_group_id=security_group_id,
+                             direction=rule['direction'],
+                             ethertype=rule['ethertype'],
+                             protocol=rule['protocol'],
+                             port_range_min=rule['port_range_min'],
+                             port_range_max=rule['port_range_max'],
+                             remote_ip_prefix=rule['remote_ip_prefix'],
+                             **connection_args)
+
+       # print security_group_id
+       # print direction
+       # print ethertype
+       # print protocol
+       # print port_range_min
+       # print port_range_max
+       # print remote_ip_prefix
+
+    if not security_group:
+        # The security group didn't exist. It was created and specified
+        # rules were added to it.
+        security_group = _neutron_module_call('list_security_groups',
+                                              id=security_group_id,
+                                              **connection_args)[name]
+        return _created(name, 'security_group', security_group)
+    if len(new_rules) == 0:
+        # Security group already exists and specified rules are already
+        # present.
+        return _no_change(name, 'security_group')
+    # Security group already exists, but the specified rules were added to it.
+    return _updated(name, 'security_group', {'New Rules': new_rules})
+
+def _created(name, resource, resource_definition):
+    changes_dict = {'name': name,
+                    'changes': resource_definition,
+                    'result': True,
+                    'comment': '{0} {1} created'.format(resource, name)}
+    return changes_dict
+
+def _updated(name, resource, resource_definition):
+    changes_dict = {'name': name,
+                    'changes': resource_definition,
+                    'result': True,
+                    'comment': '{0} {1} updated'.format(resource, name)}
+    return changes_dict
+
+def _no_change(name, resource, test=False):
+    changes_dict = {'name': name,
+                    'changes': {},
+                    'result': True}
+    if test:
+        changes_dict['comment'] = \
+            '{0} {1} will be {2}'.format(resource, name, test)
+    else:
+        changes_dict['comment'] = \
+            '{0} {1} is in correct state'.format(resource, name)
+    return changes_dict
+
+
+def _deleted(name, resource, resource_definition):
+    changes_dict = {'name': name,
+                    'changes': {},
+                    'comment': '{0} {1} removed'.format(resource, name),
+                    'result': True}
+    return changes_dict
+
+
+def _absent(name, resource):
+    changes_dict = {'name': name,
+                    'changes': {},
+                    'comment': '{0} {1} not present'.format(resource, name),
+                    'result': True}
+    return changes_dict
+
+
+def _delete_failed(name, resource):
+    changes_dict = {'name': name,
+                    'changes': {},
+                    'comment': '{0} {1} failed to delete'.format(resource,
+                                                                 name),
+                    'result': False}
+    return changes_dict
+
+def _create_failed(name, resource):
+    changes_dict = {'name': name,
+                    'changes': {},
+                    'comment': '{0} {1} failed to create'.format(resource,
+                                                                 name),
+                    'result': False}
+    return changes_dict
+
+def _update_failed(name, resource):
+    changes_dict = {'name': name,
+                    'changes': {},
+                    'comment': '{0} {1} failed to update'.format(resource,
+                                                                 name),
+                    'result': False}
+    return changes_dict
+
+
+def _get_non_null_args(**kwargs):
+    '''
+    Return those kwargs which are not null
+    '''
+    return dict((key, value,) for key, value in kwargs.iteritems()
+                if value is not None)
\ No newline at end of file
diff --git a/neutron/client.sls b/neutron/client.sls
new file mode 100644
index 0000000..e78c35b
--- /dev/null
+++ b/neutron/client.sls
@@ -0,0 +1,91 @@
+{%- from "neutron/map.jinja" import client with context %}
+{%- if client.enabled %}
+
+neutron_client_packages:
+  pkg.installed:
+  - names: {{ client.pkgs }}
+
+
+{%- for identity_name, identity in client.server.iteritems() %}
+
+{%- for network_name, network in identity.network.iteritems() %}
+
+neutron_openstack_network_{{ network_name }}:
+  neutronng.network_present:
+    - name: {{ network_name }}
+    - profile: {{ identity_name }}
+
+    {%- if network.provider_network_type is defined %}
+    - provider_network_type: {{ network.provider_network_type }}
+    {%- endif %}
+    {%- if network.provider_physical_network is defined %}
+    - provider_physical_network: {{ network.provider_physical_network }}
+    {%- endif %}
+    {%- if network.router_external is defined %}
+    - router_external: {{ network.router_external }}
+    {%- endif %}
+    {%- if network.admin_state_up is defined %}
+    - admin_state_up: {{ network.admin_state_up }}
+    {%- endif %}
+    {%- if network.shared is defined %}
+    - shared: {{ network.shared }}
+    {%- endif %}
+    {%- if network.provider_segmentation_id is defined %}
+    - provider_segmentation_id: {{ network.provider_segmentation_id }}
+    {%- endif %}
+
+{%- for subnet_name, subnet in network.subnet.iteritems() %}
+neutron_openstack_subnet_{{ subnet_name }}:
+  neutronng.subnet_present:
+    - name: {{ subnet_name }}
+    - network: {{ network_name }}
+    - profile: {{ identity_name }}
+
+    {%- if subnet.cidr is defined %}
+    - cidr: {{ subnet.cidr  }}
+    {%- endif %}
+    {%- if subnet.ip_version is defined %}
+    - ip_version: {{ subnet.ip_version }}
+    {%- endif %}
+    {%- if subnet.enable_dhcp is defined %}
+    - enable_dhcp: {{ subnet.enable_dhcp }}
+    {%- endif %}
+    {%- if subnet.allocation_pools is defined %}
+    - allocation_pools: {{ subnet.allocation_pools }}
+    {%- endif %}
+    {%- if subnet.gateway_ip is defined %}
+    - gateway_ip: {{ subnet.gateway_ip }}
+    {%- endif %}
+    {%- if subnet.dns_nameservers is defined %}
+    - dns_nameservers: {{ subnet.dns_nameservers }}
+    {%- endif %}
+    {%- if subnet.host_routes is defined %}
+    - host_routes: {{ subnet.host_routes }}
+    {%- endif %}
+    - require:
+      - neutronng: neutron_openstack_network_{{ network_name }}
+
+{%- endfor %}
+{%- endfor %}
+
+{%- for router_name, router in identity.router.iteritems() %}
+neutron_openstack_router_{{ router_name }}:
+  neutronng.router_present:
+    - name: {{ router_name }}
+    - interfaces: {{ router.interfaces }}
+    - gateway_network: {{ router.gateway_network }}
+    - profile: {{ identity_name }}
+{%- endfor %}
+
+{%- for security_group_name, security_group in identity.security_group.iteritems() %}
+openstack_security_group_{{ security_group_name }}:
+  neutronng.security_group_present:
+    - name: {{ security_group_name }}
+    - description: {{ security_group.description }}
+    - rules: {{ security_group.rules }}
+    - profile: {{ identity_name }}
+{%- endfor %}
+
+{%- endfor %}
+
+{%- endif %}
diff --git a/neutron/init.sls b/neutron/init.sls
index 8e39f6f..c12f434 100644
--- a/neutron/init.sls
+++ b/neutron/init.sls
@@ -8,4 +8,7 @@
 {% endif %}
 {% if pillar.neutron.compute is defined %}
 - neutron.compute
+{% endif %}
+{% if pillar.neutron.client is defined %}
+- neutron.client
 {% endif %}
\ No newline at end of file
diff --git a/neutron/map.jinja b/neutron/map.jinja
index 1af2a68..ede8d16 100644
--- a/neutron/map.jinja
+++ b/neutron/map.jinja
@@ -48,6 +48,15 @@
     },
 }, merge=pillar.neutron.get('server', {})) %}
 
+{% set client = salt['grains.filter_by']({
+    'Debian': {
+        'pkgs': ['python-neutronclient']
+    },
+    'RedHat': {
+        'pkgs': ['python-neutronclient']
+    },
+}, merge=pillar.neutron.get('client', {})) %}
+
 {%- if pillar.neutron.server is defined %}
 
 {%- set tmp_server = pillar.neutron.server %}