Merge "Fix Malformed Table in Integration Tests doc"
authorSteven Pisarski <s.pisarski@cablelabs.com>
Fri, 19 Jan 2018 14:38:30 +0000 (14:38 +0000)
committerGerrit Code Review <gerrit@opnfv.org>
Fri, 19 Jan 2018 14:38:30 +0000 (14:38 +0000)
122 files changed:
ci/run_tests.sh
docs/how-to-use/APITests.rst
docs/how-to-use/InstallSnaps.rst
docs/how-to-use/IntegrationTests.rst
docs/how-to-use/LibraryUsage.rst
docs/how-to-use/UnitTests.rst
docs/how-to-use/VirtEnvDeploy.rst
examples/demo.py
examples/inst-w-volume/deploy-env.yaml [new file with mode: 0644]
examples/inst-w-volume/deploy-vm-with-volume.yaml [new file with mode: 0644]
examples/launch.py
requirements.txt
setup.py
snaps/config/__init__.py [moved from snaps/provisioning/ansible_pb/ubuntu-network-setup/playbooks/configure_host.yml with 58% similarity]
snaps/config/cluster_template.py [new file with mode: 0644]
snaps/config/flavor.py [new file with mode: 0644]
snaps/config/image.py [new file with mode: 0644]
snaps/config/keypair.py [new file with mode: 0644]
snaps/config/network.py [new file with mode: 0644]
snaps/config/project.py [new file with mode: 0644]
snaps/config/qos.py [new file with mode: 0644]
snaps/config/router.py [new file with mode: 0644]
snaps/config/security_group.py [new file with mode: 0644]
snaps/config/stack.py [new file with mode: 0644]
snaps/config/tests/__init__.py [new file with mode: 0644]
snaps/config/tests/cluster_template_tests.py [new file with mode: 0644]
snaps/config/tests/flavor_tests.py [new file with mode: 0644]
snaps/config/tests/image_tests.py [new file with mode: 0644]
snaps/config/tests/keypair_tests.py [new file with mode: 0644]
snaps/config/tests/network_tests.py [new file with mode: 0644]
snaps/config/tests/project_tests.py [new file with mode: 0644]
snaps/config/tests/qos_tests.py [new file with mode: 0644]
snaps/config/tests/router_tests.py [new file with mode: 0644]
snaps/config/tests/security_group_tests.py [new file with mode: 0644]
snaps/config/tests/stack_tests.py [new file with mode: 0644]
snaps/config/tests/user_tests.py [new file with mode: 0644]
snaps/config/tests/vm_inst_tests.py [new file with mode: 0644]
snaps/config/tests/volume_tests.py [new file with mode: 0644]
snaps/config/tests/volume_type_tests.py [new file with mode: 0644]
snaps/config/user.py [new file with mode: 0644]
snaps/config/vm_inst.py [new file with mode: 0644]
snaps/config/volume.py [new file with mode: 0644]
snaps/config/volume_type.py [new file with mode: 0644]
snaps/domain/cluster_template.py [new file with mode: 0644]
snaps/domain/flavor.py
snaps/domain/network.py
snaps/domain/stack.py
snaps/domain/test/cluster_template_tests.py [new file with mode: 0644]
snaps/domain/test/network_tests.py
snaps/domain/test/stack_tests.py
snaps/domain/test/vm_inst_tests.py
snaps/domain/test/volume_tests.py
snaps/domain/vm_inst.py
snaps/domain/volume.py
snaps/openstack/cluster_template.py [new file with mode: 0644]
snaps/openstack/create_flavor.py
snaps/openstack/create_image.py
snaps/openstack/create_instance.py
snaps/openstack/create_keypairs.py
snaps/openstack/create_network.py
snaps/openstack/create_project.py
snaps/openstack/create_qos.py
snaps/openstack/create_router.py
snaps/openstack/create_security_group.py
snaps/openstack/create_stack.py
snaps/openstack/create_user.py
snaps/openstack/create_volume.py [new file with mode: 0644]
snaps/openstack/create_volume_type.py
snaps/openstack/openstack_creator.py
snaps/openstack/os_credentials.py
snaps/openstack/tests/cluster_template_tests.py [new file with mode: 0644]
snaps/openstack/tests/conf/os_credentials_tests.py
snaps/openstack/tests/conf/os_env.yaml.template
snaps/openstack/tests/create_flavor_tests.py
snaps/openstack/tests/create_image_tests.py
snaps/openstack/tests/create_instance_tests.py
snaps/openstack/tests/create_keypairs_tests.py
snaps/openstack/tests/create_network_tests.py
snaps/openstack/tests/create_project_tests.py
snaps/openstack/tests/create_qos_tests.py
snaps/openstack/tests/create_router_tests.py
snaps/openstack/tests/create_security_group_tests.py
snaps/openstack/tests/create_stack_tests.py
snaps/openstack/tests/create_user_tests.py
snaps/openstack/tests/create_volume_tests.py [new file with mode: 0644]
snaps/openstack/tests/create_volume_type_tests.py
snaps/openstack/tests/heat/agent-group.yaml [new file with mode: 0644]
snaps/openstack/tests/heat/agent.yaml [new file with mode: 0644]
snaps/openstack/tests/heat/flavor_heat_template.yaml [moved from snaps/provisioning/ansible_pb/centos-network-setup/playbooks/configure_host.yml with 58% similarity]
snaps/openstack/tests/heat/floating_ip_heat_template.yaml
snaps/openstack/tests/heat/keypair_heat_template.yaml [new file with mode: 0644]
snaps/openstack/tests/heat/router_heat_template.yaml [new file with mode: 0644]
snaps/openstack/tests/heat/security_group_heat_template.yaml [new file with mode: 0644]
snaps/openstack/tests/heat/volume_heat_template.yaml [new file with mode: 0644]
snaps/openstack/tests/openstack_tests.py
snaps/openstack/tests/os_source_file_test.py
snaps/openstack/utils/cinder_utils.py
snaps/openstack/utils/deploy_utils.py
snaps/openstack/utils/heat_utils.py
snaps/openstack/utils/keystone_utils.py
snaps/openstack/utils/launch_utils.py [new file with mode: 0644]
snaps/openstack/utils/magnum_utils.py [new file with mode: 0644]
snaps/openstack/utils/neutron_utils.py
snaps/openstack/utils/nova_utils.py
snaps/openstack/utils/settings_utils.py
snaps/openstack/utils/tests/cinder_utils_tests.py
snaps/openstack/utils/tests/heat_utils_tests.py
snaps/openstack/utils/tests/keystone_utils_tests.py
snaps/openstack/utils/tests/magnum_utils_tests.py [new file with mode: 0644]
snaps/openstack/utils/tests/neutron_utils_tests.py
snaps/openstack/utils/tests/nova_utils_tests.py
snaps/openstack/utils/tests/settings_utils_tests.py
snaps/playbook_runner.py
snaps/provisioning/ansible_pb/__init__.py [deleted file]
snaps/provisioning/ansible_pb/centos-network-setup/__init__.py [deleted file]
snaps/provisioning/ansible_pb/centos-network-setup/playbooks/__init__.py [deleted file]
snaps/provisioning/ansible_pb/centos-network-setup/templates/ifcfg-interface [deleted file]
snaps/provisioning/ansible_pb/ubuntu-network-setup/__init__.py [deleted file]
snaps/provisioning/ansible_pb/ubuntu-network-setup/playbooks/__init__.py [deleted file]
snaps/provisioning/ansible_pb/ubuntu-network-setup/templates/ethN.cfg [deleted file]
snaps/provisioning/tests/ansible_utils_tests.py
snaps/test_suite_builder.py

index 14f1a11..464ba63 100755 (executable)
@@ -6,7 +6,7 @@ set -e
 sudo pip install virtualenv
 virtualenv ./vpy
 source ./vpy/bin/activate
-pip install -e ../
+pip install -chttps://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt?h=stable/pike -e ../
 
 # $1 is the IP to the pod's build server
 # $2 is the IP to the pod's control server
index 4a8035a..50cd437 100644 (file)
@@ -34,6 +34,12 @@ nova_utils_tests.py - NovaSmokeTests
 Ensures that a Nova client can be obtained as well as the proper
 exceptions thrown with the wrong credentials.
 
+cinder_utils_tests.py - CinderSmokeTests
+----------------------------------------
+
+Ensures that a Cinder client can be obtained as well as the proper
+exceptions thrown with the wrong credentials.
+
 heat_utils_tests.py - HeatSmokeTests
 ------------------------------------
 
@@ -157,22 +163,63 @@ neutron_utils_tests.py - NeutronUtilsSubnetTests
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | Test Name                             | Neutron API   | Description                                               |
 +=======================================+===============+===========================================================+
-| test_create_subnet                    | 2             | Ensures neutron_utils.create_subnet() can properly create |
+| test_create_subnet                    | 2             | Ensures neutron_utils.create_network() can properly create|
 |                                       |               | an OpenStack subnet object                                |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_subnet_null_name          | 2             | Ensures neutron_utils.create_subnet() raises an exception |
+| test_create_subnet_null_name          | 2             | Ensures neutron_utils.create_network() raises an exception|
 |                                       |               | when the subnet name is None                              |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_subnet_empty_name         | 2             | Ensures neutron_utils.create_subnet() raises an exception |
+| test_create_subnet_empty_name         | 2             | Ensures neutron_utils.create_network() raises an exception|
 |                                       |               | when the subnet name is an empty string                   |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_subnet_null_cidr          | 2             | Ensures neutron_utils.create_subnet() raises an exception |
+| test_create_subnet_null_cidr          | 2             | Ensures neutron_utils.create_network() raises an exception|
 |                                       |               | when the subnet CIDR is None                              |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_subnet_empty_cidr         | 2             | Ensures neutron_utils.create_subnet() raises an exception |
+| test_create_subnet_empty_cidr         | 2             | Ensures neutron_utils.create_network() raises an exception|
 |                                       |               | when the subnet CIDR is an empty string                   |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+neutron_utils_tests.py - NeutronUtilsIPv6Tests
+----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Neutron API   | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_network_slaac             | 2             | Ensures neutron_utils.create_network() can properly create|
+|                                       |               | an OpenStack network with an IPv6 subnet when DHCP is True|
+|                                       |               | and modes are 'slaac'                                     |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_stateful          | 2             | Ensures neutron_utils.create_network() can properly create|
+|                                       |               | an OpenStack network with an IPv6 subnet when DHCP is True|
+|                                       |               | and modes are 'stateful'                                  |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_stateless         | 2             | Ensures neutron_utils.create_network() can properly create|
+|                                       |               | an OpenStack network with an IPv6 subnet when DHCP is True|
+|                                       |               | and modes are 'stateless'                                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_no_dhcp_slaac     | 2             | Ensures neutron_utils.create_network() raises a BadRequest|
+|                                       |               | exception when deploying the network with an IPv6 subnet  |
+|                                       |               | when DHCP is False and modes are 'slaac'                  |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_invalid_start_ip  | 2             | Ensures neutron_utils.create_network() sets the start IP  |
+|                                       |               | address to the minimum value when the start configuration |
+|                                       |               | parameter is some garbage value                           |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_invalid_end_ip    | 2             | Ensures neutron_utils.create_network() sets the end IP    |
+|                                       |               | address to the maximum value when the end configuration   |
+|                                       |               | parameter is some garbage value                           |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_with_bad_cidr     | 2             | Ensures neutron_utils.create_network() raises a BadRequest|
+|                                       |               | exception when the IPv6 CIDR is incorrect                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_invalid_gateway_ip| 2             | Ensures neutron_utils.create_network() raises a BadRequest|
+|                                       |               | exception when the IPv6 gateway IP does not match the CIDR|
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_with_bad_dns      | 2             | Ensures neutron_utils.create_network() raises a BadRequest|
+|                                       |               | exception when the IPv6 DNS IP address is not a valid IPv6|
+|                                       |               | address                                                   |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 neutron_utils_tests.py - NeutronUtilsRouterTests
 ------------------------------------------------
 
@@ -186,12 +233,6 @@ neutron_utils_tests.py - NeutronUtilsRouterTests
 | face                                  |               | an OpenStack router object with an interface to the       |
 |                                       |               | external network                                          |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_router_empty_name         | 2             | Ensures neutron_utils.create_router() raises an exception |
-|                                       |               | when the name is an empty string                          |
-+---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_router_null_name          | 2             | Ensures neutron_utils.create_router() raises an exception |
-|                                       |               | when the name is None                                     |
-+---------------------------------------+---------------+-----------------------------------------------------------+
 | test_add_interface_router             | 2             | Ensures neutron_utils.add_interface_router() properly adds|
 |                                       |               | an interface to another subnet                            |
 +---------------------------------------+---------------+-----------------------------------------------------------+
@@ -201,6 +242,9 @@ neutron_utils_tests.py - NeutronUtilsRouterTests
 | test_add_interface_router_null_subnet | 2             | Ensures neutron_utils.add_interface_router() raises an    |
 |                                       |               | exception when the subnet object is None                  |
 +---------------------------------------+---------------+-----------------------------------------------------------+
+| test_add_interface_router_missing_sub | 2             | Ensures neutron_utils.add_interface_router() raises an    |
+| net                                   |               | exception when the subnet object had been deleted         |
++---------------------------------------+---------------+-----------------------------------------------------------+
 | test_create_port                      | 2             | Ensures neutron_utils.create_port() can properly create an|
 |                                       |               | OpenStack port object                                     |
 +---------------------------------------+---------------+-----------------------------------------------------------+
@@ -256,6 +300,95 @@ neutron_utils_tests.py - NeutronUtilsFloatingIpTests
 | test_floating_ips                     | 2             | Ensures that a floating IP can be created                 |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+cinder_utils_tests.py - CinderUtilsQoSTests
+-------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |  Cinder API   | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_qos_both                  | 2 & 3         | Ensures that a QoS Spec can be created with a Consumer    |
+|                                       |               | value of 'both'                                           |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_qos_front                 | 2 & 3         | Ensures that a QoS Spec can be created with a Consumer    |
+|                                       |               | value of 'front-end'                                      |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_qos_back                  | 2 & 3         | Ensures that a QoS Spec can be created with a Consumer    |
+|                                       |               | value of 'back-end'                                       |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_qos                | 2 & 3         | Ensures that a QoS Spec can be created and deleted        |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+cinder_utils_tests.py - CinderUtilsSimpleVolumeTypeTests
+--------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |  Cinder API   | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_simple_volume_type        | 2 & 3         | Tests the creation of a simple volume type with the       |
+|                                       |               | function cinder_utils#create_volume_type()                |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_volume_type        | 2 & 3         | Tests the creation of a simple volume type with the       |
+|                                       |               | function cinder_utils#create_volume_type() then deletes   |
+|                                       |               | with the function cinder_utils#delete_volume_type()       |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+cinder_utils_tests.py - CinderUtilsAddEncryptionTests
+-----------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |  Cinder API   | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_simple_encryption         | 2 & 3         | Tests the creation of a simple volume type encryption     |
+|                                       |               | with the function cinder_utils#create_volume_encryption() |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_encryption         | 2 & 3         | Tests the creation of a simple volume type encryption     |
+|                                       |               | with the function cinder_utils#create_volume_encryption() |
+|                                       |               | then deletes with the function                            |
+|                                       |               | cinder_utils#delete_volume_type_encryption()              |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_with_all_attrs            | 2 & 3         | Tests the creation of a simple volume type encryption     |
+|                                       |               | with the function cinder_utils#create_volume_encryption() |
+|                                       |               | where all configuration attributes have been set          |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_bad_key_size              | 2 & 3         | Tests to ensure that the function                         |
+|                                       |               | cinder_utils#create_volume_encryption() raises a          |
+|                                       |               | BadRequest exception when the key_size attribute is -1    |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+cinder_utils_tests.py - CinderUtilsVolumeTypeCompleteTests
+----------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |  Cinder API   | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_with_encryption           | 2 & 3         | Tests the creation of a volume type with encryption       |
+|                                       |               | with the function cinder_utils#create_volume_type()       |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_with_qos                  | 2 & 3         | Tests the creation of a volume type with a QoS Spec       |
+|                                       |               | with the function cinder_utils#create_volume_type()       |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_with_invalid_qos          | 2 & 3         | Tests the creation of a volume type with an invalid QoS   |
+|                                       |               | Spec with the function cinder_utils#create_volume_type()  |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_with_qos_and_encryption   | 2 & 3         | Tests the creation of a volume type with a QoS Spec and   |
+|                                       |               | encryption with the function                              |
+|                                       |               | cinder_utils#create_volume_type()                         |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+cinder_utils_tests.py - CinderUtilsVolumeTests
+----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |  Cinder API   | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_simple_volume             | 2 & 3         | Tests the creation of a simple volume with the function   |
+|                                       |               | cinder_utils#create_volume()                              |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_volume             | 2 & 3         | Tests the creation of a volume with the function          |
+|                                       |               | cinder_utils#create_volume() then deletion with the       |
+|                                       |               | function cinder_utils#delete_volume()                     |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 nova_utils_tests.py - NovaUtilsKeypairTests
 -------------------------------------------
 
@@ -295,6 +428,16 @@ nova_utils_tests.py - NovaUtilsInstanceTests
 |                                       |               | nova_utils.create_server()                                |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+nova_utils_tests.py - NovaUtilsInstanceVolumeTests
+--------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Nova API      | Description                                               |
++=======================================+===============+===========================================================+
+| test_add_remove_volume                | 2             | Ensures that a VM instance can properly attach and detach |
+|                                       |               | a volume using the nova interface                         |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 create_flavor_tests.py - CreateFlavorTests
 ------------------------------------------
 
@@ -324,9 +467,9 @@ heat_utils_tests.py - HeatUtilsCreateSimpleStackTests
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | Test Name                             | Heat API      | Description                                               |
 +=======================================+===============+===========================================================+
-| test_create_stack                     | 1             | Tests the heat_utils.create_stack() with a test template  |
+| test_create_stack                     | 1-3           | Tests the heat_utils.create_stack() with a test template  |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_stack_x2                  | 1             | Tests the heat_utils.create_stack() with a test template  |
+| test_create_stack_x2                  | 1-3           | Tests the heat_utils.create_stack() with a test template  |
 |                                       |               | and attempts to deploy a second time w/o actually         |
 |                                       |               | deploying any objects                                     |
 +---------------------------------------+---------------+-----------------------------------------------------------+
@@ -337,21 +480,120 @@ heat_utils_tests.py - HeatUtilsCreateComplexStackTests
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | Test Name                             | Heat API      | Description                                               |
 +=======================================+===============+===========================================================+
-| test_get_settings_from_stack          | 1             | Tests the heat_utils functions that are responsible for   |
+| test_get_settings_from_stack          | 1-3           | Tests the heat_utils functions that are responsible for   |
 |                                       |               | reverse engineering settings objects of the types deployed|
 |                                       |               | by Heat                                                   |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+heat_utils_tests.py - HeatUtilsRouterTests
+------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Heat API      | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_router_with_stack         | 1-3           | Tests ability of the function                             |
+|                                       |               | heat_utils.get_stack_routers() to return the correct      |
+|                                       |               | OpenStackRouter instance                                  |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+heat_utils_tests.py - HeatUtilsVolumeTests
+------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Heat API      | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_vol_with_stack            | 1-3           | Tests ability of the function                             |
+|                                       |               | heat_utils.create_stack() to return the correct           |
+|                                       |               | Volume domain objects deployed with Heat                  |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_vol_types_with_stack      | 1-3           | Tests ability of the function                             |
+|                                       |               | heat_utils.get_stack_volumes_types() to return the correct|
+|                                       |               | VolumeType domain objects deployed with Heat              |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+heat_utils_tests.py - HeatUtilsKeypairTests
+-------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Heat API      | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_keypair_with_stack        | 1-3           | Tests ability of the function                             |
+|                                       |               | heat_utils.get_stack_keypairs() to return the correct     |
+|                                       |               | Keypair domain objects deployed with Heat                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+heat_utils_tests.py - HeatUtilsSecurityGroupTests
+-------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Heat API      | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_security_group_with_stack | 1-3           | Tests ability of the function                             |
+|                                       |               | heat_utils.get_stack_security_groups() to return the      |
+|                                       |               | correct SecurityGroup domain objects deployed with Heat   |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+heat_utils_tests.py - HeatUtilsFlavorTests
+------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Heat API      | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_flavor_with_stack         | 1-3           | Tests ability of the function                             |
+|                                       |               | heat_utils.get_stack_flavors() to return the correct      |
+|                                       |               | Flavor domain objects deployed with Heat                  |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+magnum_utils_tests.py - MagnumUtilsTests
+----------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Magnum API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_cluster_template_simple   | 1             | Tests ability of the function                             |
+|                                       |               | magnum_utils.create_cluster_template() to create a simple |
+|                                       |               | cluster template OpenStack object with minimal config     |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_all      | 1             | Tests ability of the function                             |
+|                                       |               | magnum_utils.create_cluster_template() to create a        |
+|                                       |               | cluster template OpenStack object with maximum config     |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad_image| 1             | Ensures the function                                      |
+|                                       |               | magnum_utils.create_cluster_template() will raise a       |
+|                                       |               | BadRequest exception when the image does not exist        |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad_ext  | 1             | Ensures the function                                      |
+| _net                                  |               | magnum_utils.create_cluster_template() will raise a       |
+|                                       |               | BadRequest exception when the external network does not   |
+|                                       |               | exist                                                     |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad      | 1             | Ensures the function                                      |
+| _flavor                               |               | magnum_utils.create_cluster_template() will raise a       |
+|                                       |               | BadRequest exception when the flavor does not exist       |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad      | 1             | Ensures the function                                      |
+| _master_flavor                        |               | magnum_utils.create_cluster_template() will raise a       |
+|                                       |               | BadRequest exception when the master flavor does not exist|
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad      | 1             | Ensures the function                                      |
+| _network_driver                       |               | magnum_utils.create_cluster_template() will raise a       |
+|                                       |               | BadRequest exception when the network driver is invalid   |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad      | 1             | Ensures the function                                      |
+| _volume_driver                        |               | magnum_utils.create_cluster_template() will raise a       |
+|                                       |               | BadRequest exception when the volume driver is invalid    |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 settings_utils_tests.py - SettingsUtilsNetworkingTests
 ------------------------------------------------------
 
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | Test Name                             | API           | Description                                               |
 +=======================================+===============+===========================================================+
-| test_derive_net_settings_no_subnet    | Neutron 2     | Tests to ensure that derived NetworkSettings from an      |
+| test_derive_net_settings_no_subnet    | Neutron 2     | Tests to ensure that derived NetworkConfig from an        |
 |                                       |               | OpenStack network are correct without a subnet            |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_derive_net_settings_two_subnets  | Neutron 2     | Tests to ensure that derived NetworkSettings from an      |
+| test_derive_net_settings_two_subnets  | Neutron 2     | Tests to ensure that derived NetworkConfig from an        |
 |                                       |               | OpenStack network are correct with two subnets            |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
@@ -361,9 +603,9 @@ settings_utils_tests.py - SettingsUtilsVmInstTests
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | Test Name                             | API           | Description                                               |
 +=======================================+===============+===========================================================+
-| test_derive_vm_inst_settings          | Neutron 2     | Tests to ensure that derived VmInstanceSettings from an   |
+| test_derive_vm_inst_config            | Neutron 2     | Tests to ensure that derived VmInstanceSettings from an   |
 |                                       |               | OpenStack VM instance is correct                          |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_derive_image_settings            | Neutron 2     | Tests to ensure that derived ImageSettings from an        |
+| test_derive_image_settings            | Neutron 2     | Tests to ensure that derived ImageConfig from an        |
 |                                       |               | OpenStack VM instance is correct                          |
 +---------------------------------------+---------------+-----------------------------------------------------------+
index dc53ef0..f5e9cf8 100644 (file)
@@ -69,4 +69,12 @@ The "pip" command below needs to be executed as root, if you are not using a vir
    sudo pip install -e <path to repo>/snaps/
    (note: on CentOS 7 and Ubuntu 14.04 you may have to try the previous command several times)
 
+SNAPS is now hosted on the Python Package Manager (PyPI).
+
+::
+
+   pip install snaps
+
+This will install the stable Euphrates version.
+
 The install should now be complete and you can start using the SNAPS-OO libraries.
index f082f9b..59ec8a9 100644 (file)
@@ -80,7 +80,7 @@ create_image_tests.py - CreateImageNegativeTests
 | Test Name                             | Glance API    | Description                                               |
 +=======================================+===============+===========================================================+
 | test_bad_image_name                   | 1 & 2         | Ensures OpenStackImage.create() results in an Exception   |
-|                                       |               | being raised when the ImageSettings.name attribute has    |
+|                                       |               | being raised when the ImageConfig.name attribute has    |
 |                                       |               | not been set                                              |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | test_bad_image_url                    | 1 & 2         | Ensures OpenStackImage.create() results in an Exception   |
@@ -185,6 +185,18 @@ create_network_tests.py - CreateNetworkSuccessTests
 |                                       |               | 'admin' project ID                                        |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+create_network_tests.py - CreateNetworkIPv6Tests
+------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | Neutron API   | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_network_one_ipv6_subnet   | 2             | Ensures that a network can be created with an IPv6 subnet |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_ipv4_ipv6_subnet  | 2             | Ensures that a network can be created with an IPv4 and    |
+|                                       |               | IPv6 subnet                                               |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 create_router_tests.py - CreateRouterSuccessTests
 -------------------------------------------------
 
@@ -229,38 +241,247 @@ create_router_tests.py - CreateRouterNegativeTests
 |                                        |               | create a router to an external network that does not exist|
 +----------------------------------------+---------------+-----------------------------------------------------------+
 
+create_qos_tests.py - CreateQoSTests
+------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |  Cinder API   | Description                                               |
++========================================+===============+===========================================================+
+| test_create_qos                        | 2 & 3         | Tests the creation of a QoS Spec with the class           |
+|                                        |               | OpenStackQoS                                              |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_qos                 | 2 & 3         | Tests the creation of a QoS Spec with the class           |
+|                                        |               | OpenStackQoS, its deletion with cinder_utils.py the       |
+|                                        |               | the attempts to use the clean() method to ensure an       |
+|                                        |               | exception is not called                                   |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_same_qos                   | 2 & 3         | Tests the creation of a QoS Spec with the class           |
+|                                        |               | OpenStackQoS then instantiates another OpenStackQoS       |
+|                                        |               | object with the same configuration to ensure the second   |
+|                                        |               | instance returns the ID of the original                   |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
+create_volume_type_tests.py - CreateSimpleVolumeTypeSuccessTests
+----------------------------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |  Cinder API   | Description                                               |
++========================================+===============+===========================================================+
+| test_create_volume_type                | 2 & 3         | Tests the creation of a Volume Type with the class        |
+|                                        |               | OpenStackVolumeType                                       |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_volume_type         | 2 & 3         | Tests the creation of a Volume Type with the class        |
+|                                        |               | OpenStackVolumeType, its deletion with cinder_utils.py,   |
+|                                        |               | then attempts to use the clean() method to ensure an      |
+|                                        |               | exception is not raised                                   |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_same_volume_type           | 2 & 3         | Tests the creation of a Volume Type with the class        |
+|                                        |               | OpenStackVolumeType then instantiates another             |
+|                                        |               | OpenStackVolumeType object with the same configuration to |
+|                                        |               | ensure the second instance returns the ID of the original |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
+create_volume_type_tests.py - CreateSimpleVolumeTypeComplexTests
+----------------------------------------------------------------
+
++-----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                               |  Cinder API   | Description                                               |
++=========================================+===============+===========================================================+
+| test_volume_type_with_qos               | 2 & 3         | Tests the creation of a Volume Type with the class        |
+|                                         |               | OpenStackVolumeType with a QoSSpec                        |
++-----------------------------------------+---------------+-----------------------------------------------------------+
+| test_volume_type_with_encryption        | 2 & 3         | Tests the creation of a Volume Type with the class        |
+|                                         |               | OpenStackVolumeType with encryption                       |
++-----------------------------------------+---------------+-----------------------------------------------------------+
+| test_volume_type_with_qos_and_encryption| 2 & 3         | Tests the creation of a Volume Type with the class        |
+|                                         |               | OpenStackVolumeType with encryption and QoS Spec          |
++-----------------------------------------+---------------+-----------------------------------------------------------+
+
+create_volume_tests.py - CreateSimpleVolumeSuccessTests
+-------------------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |  Cinder API   | Description                                               |
++========================================+===============+===========================================================+
+| test_create_volume_simple              | 2 & 3         | Tests the creation of a Volume Type with the class        |
+|                                        |               | OpenStackVolume                                           |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_volume              | 2 & 3         | Tests the creation of a Volume with the class             |
+|                                        |               | OpenStackVolume, its deletion with cinder_utils.py, then  |
+|                                        |               | attempts to use the clean() method to ensure an           |
+|                                        |               | exception is not raised                                   |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_same_volume                | 2 & 3         | Tests the creation of a Volume with the class             |
+|                                        |               | OpenStackVolume then instantiates another                 |
+|                                        |               | OpenStackVolume object with the same configuration to     |
+|                                        |               | ensure the second instance returns the ID of the original |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
+create_volume_tests.py - CreateSimpleVolumeFailureTests
+-------------------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |  Cinder API   | Description                                               |
++========================================+===============+===========================================================+
+| test_create_volume_bad_size            | 2 & 3         | Tests to ensure that attempting to create a volume with a |
+|                                        |               | size of -1 raises a BadRequest exception                  |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_volume_bad_type            | 2 & 3         | Tests to ensure that attempting to create a volume with a |
+|                                        |               | type that does not exist raises a NotFound exception      |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_volume_bad_image           | 2 & 3         | Tests to ensure that attempting to create a volume with an|
+|                                        |               | image that does not exist raises a BadRequest exception   |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_volume_bad_zone            | 2 & 3         | Tests to ensure that attempting to create a volume with an|
+|                                        |               | invalid availability zone raises a BadRequest exception   |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
+create_volume_tests.py - CreateVolumeWithTypeTests
+--------------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |  Cinder API   | Description                                               |
++========================================+===============+===========================================================+
+| test_bad_volume_type                   | 2 & 3         | Tests to ensure the creation of a Volume with the         |
+|                                        |               | OpenStackVolume#create() method raises a NotFound         |
+|                                        |               | exception when attempting to apply a VolumeType that does |
+|                                        |               | not exist                                                 |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_valid_volume_type                 | 2 & 3         | Tests to ensure the creation of a Volume with the         |
+|                                        |               | OpenStackVolume#create() method properly creates the      |
+|                                        |               | volume when associating with a valid VolumeType           |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
+create_volume_tests.py - CreateVolumeWithImageTests
+---------------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |  Cinder API   | Description                                               |
++========================================+===============+===========================================================+
+| test_bad_image_name                    | 2 & 3         | Tests to ensure the creation of a Volume with the         |
+|                                        |               | OpenStackVolume#create() method raises a BadRequest       |
+|                                        |               | exception when attempting to apply an image that does not |
+|                                        |               | exist                                                     |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_valid_volume_image                | 2 & 3         | Tests to ensure the creation of a Volume with the         |
+|                                        |               | OpenStackVolume#create() method properly creates the      |
+|                                        |               | volume when associating with a valid image                |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
 create_stack_tests.py - CreateStackSuccessTests
 -----------------------------------------------
 
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| Test Name                             | Neutron API   | Description                                               |
+| Test Name                             |   Heat API    | Description                                               |
 +=======================================+===============+===========================================================+
-| test_create_stack_template_file       | 2             | Ensures that a Heat stack can be created with a file-based|
+| test_create_stack_template_file       | 1-3           | Ensures that a Heat stack can be created with a file-based|
 |                                       |               | Heat template file                                        |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_stack_template_dict       | 2             | Ensures that a Heat stack can be created with a dictionary|
+| test_create_stack_template_dict       | 1-3           | Ensures that a Heat stack can be created with a dictionary|
 |                                       |               | Heat template                                             |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_delete_stack              | 2             | Ensures that a Heat stack can be created and deleted      |
+| test_create_delete_stack              | 1-3           | Ensures that a Heat stack can be created and deleted      |
 |                                       |               | while having clean() called 2x without an exception       |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_create_same_stack                | 2             | Ensures that a Heat stack with the same name cannot be    |
+| test_create_same_stack                | 1-3           | Ensures that a Heat stack with the same name cannot be    |
 |                                       |               | created 2x                                                |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_retrieve_network_creators        | 2             | Ensures that an OpenStackHeatStack instance can return an |
+| test_retrieve_network_creators        | 1-3           | Ensures that an OpenStackHeatStack instance can return an |
 |                                       |               | OpenStackNetwork instance configured as deployed          |
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| test_retrieve_vm_inst_creators        | 2             | Ensures that an OpenStackHeatStack instance can return an |
+| test_retrieve_vm_inst_creators        | 1-3           | Ensures that an OpenStackHeatStack instance can return an |
 |                                       |               | OpenStackVmInstance instance configured as deployed       |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+create_stack_tests.py - CreateStackVolumeTests
+----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_retrieve_volume_creator          | 1-3           | Ensures that an OpenStackHeatStack instance can return a  |
+|                                       |               | OpenStackVolume instance that it was responsible for      |
+|                                       |               | deploying                                                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_retrieve_volume_type_creator     | 1-3           | Ensures that an OpenStackHeatStack instance can return a  |
+|                                       |               | OpenStackVolumeType instance that it was responsible for  |
+|                                       |               | deploying                                                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_stack_tests.py - CreateStackFloatingIpTests
+--------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_connect_via_ssh_heat_vm          | 1             | Ensures that an OpenStackHeatStack instance can create a  |
+|                                       |               | VM with a floating IP that can be accessed via            |
+|                                       |               | OpenStackVmInstance                                       |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_stack_tests.py - CreateStackNestedResourceTests
+------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_nested                           | 1             | Ensures that an OpenStackHeatStack with an external       |
+|                                       |               | resource file with VMs with floating IPs can be accessed  |
+|                                       |               | in the class OpenStackVmInstance and return the associated|
+|                                       |               | initialized OpenStackVmInstance objects                   |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_stack_tests.py - CreateStackRouterTests
+----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_retrieve_router_creator          | 1             | Ensures that an OpenStackHeatStack instance can return a  |
+|                                       |               | OpenStackRouter instance that it was responsible for      |
+|                                       |               | deploying                                                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_stack_tests.py - CreateStackFlavorTests
+----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_retrieve_flavor_creator          | 1-3           | Ensures that an OpenStackHeatStack instance can return a  |
+|                                       |               | OpenStackFlavor instance that it was responsible for      |
+|                                       |               | deploying                                                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_stack_tests.py - CreateStackKeypairTests
+-----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_retrieve_keypair_creator         | 1-3           | Ensures that an OpenStackHeatStack instance can return a  |
+|                                       |               | OpenStackKeypair instance that it was responsible for     |
+|                                       |               | deploying                                                 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_stack_tests.py - CreateStackSecurityGroupTests
+-----------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             |   Heat API    | Description                                               |
++=======================================+===============+===========================================================+
+| test_retrieve_security_group_creator  | 1-3           | Ensures that an OpenStackHeatStack instance can return a  |
+|                                       |               | OpenStackSecurityGroup instance that it was responsible   |
+|                                       |               | for deploying                                             |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 create_stack_tests.py - CreateComplexStackTests
 -----------------------------------------------
 
 +---------------------------------------+---------------+-----------------------------------------------------------+
-| Test Name                             | Neutron API   | Description                                               |
+| Test Name                             |   Heat API    | Description                                               |
 +=======================================+===============+===========================================================+
-| test_connect_via_ssh_heat_vm          | 2             | Ensures that two OpenStackHeatStack instances can return  |
+| test_connect_via_ssh_heat_vm          | 1-3           | Ensures that two OpenStackHeatStack instances can return  |
 |                                       |               | OpenStackVmInstance instances one configured with a       |
 |                                       |               | floating IP and keypair and can be access via SSH         |
 +---------------------------------------+---------------+-----------------------------------------------------------+
@@ -269,15 +490,25 @@ create_stack_tests.py - CreateStackNegativeTests
 ------------------------------------------------
 
 +----------------------------------------+---------------+-----------------------------------------------------------+
-| Test Name                              | Neutron API   | Description                                               |
+| Test Name                              |   Heat API    | Description                                               |
 +========================================+===============+===========================================================+
-| test_missing_dependencies              | 2             | Ensures that a Heat template fails to deploy when expected|
+| test_missing_dependencies              | 1-3           | Ensures that a Heat template fails to deploy when expected|
 |                                        |               | dependencies are missing                                  |
 +----------------------------------------+---------------+-----------------------------------------------------------+
-| test_bad_stack_file                    | 2             | Ensures that a Heat template fails to deploy when the Heat|
+| test_bad_stack_file                    | 1-3           | Ensures that a Heat template fails to deploy when the Heat|
 |                                        |               | template file does not exist                              |
 +----------------------------------------+---------------+-----------------------------------------------------------+
 
+create_stack_tests.py - CreateStackFailureTests
+-----------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |   Heat API    | Description                                               |
++========================================+===============+===========================================================+
+| test_stack_failure                     | 1-3           | Ensures that a Heat template fails to deploy when expected|
+|                                        |               | dependencies are missing                                  |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
 create_instance_tests.py - CreateInstanceSimpleTests
 ----------------------------------------------------
 
@@ -323,6 +554,15 @@ create_instance_tests.py - CreateInstanceSingleNetworkTests
 | test_ssh_client_fip_after_active      | Nova 2        | Ensures that an instance can be reached over SSH when the |
 |                                       | Neutron 2     | floating IP is assigned after to the VM becoming ACTIVE   |
 +---------------------------------------+---------------+-----------------------------------------------------------+
+| test_ssh_client_fip_after_init        | Nova 2        | Ensures that an instance can have a floating IP assigned  |
+|                                       | Neutron 2     | added after initialization                                |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_ssh_client_fip_reverse_engineer  | Nova 2        | Ensures that an instance can be reverse engineered and    |
+|                                       | Neutron 2     | allows for a floating IP to be added after initialization |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_ssh_client_fip_after_reboot      | Nova 2        | Ensures that an instance can be reached over SSH after    |
+|                                       | Neutron 2     | a reboot call has been issued                             |
++---------------------------------------+---------------+-----------------------------------------------------------+
 | test_ssh_client_fip_second_creator    | Nova 2        | Ensures that an instance can be reached over SSH via a    |
 |                                       | Neutron 2     | second identical creator object                           |
 +---------------------------------------+---------------+-----------------------------------------------------------+
@@ -378,14 +618,18 @@ create_instance_tests.py - CreateInstanceFromThreePartImage
 |                                                     | Neutron 2     | delete it when using a 3-part image                       |
 +-----------------------------------------------------+---------------+-----------------------------------------------------------+
 
-create_instance_tests.py - CreateInstancePubPrivNetTests
---------------------------------------------------------
+create_instance_tests.py - CreateInstanceIPv6NetworkTests (Staging)
+-------------------------------------------------------------------
 
 +---------------------------------------+---------------+-----------------------------------------------------------+
 | Test Name                             | API Versions  | Description                                               |
 +=======================================+===============+===========================================================+
-| test_dual_ports_dhcp                  | Nova 2        | Ensures that a VM with two ports/NICs can have its second |
-|                                       | Neutron 2     | NIC configured via SSH/Ansible after startup              |
+| test_v4fip_v6overlay                  | Nova 2        | Expects a BadRequest exception to be raised when          |
+|                                       | Neutron 2     | attempting to add an IPv4 floating IP to a VM with an IPv6|
+|                                       |               | port                                                      |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_fip_v4and6_overlay               | Nova 2        | Connects to a VM via a floating IP joined to a port that  |
+|                                       | Neutron 2     | has been confiured with both IPv4 and IPv6 addresses      |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
 create_instance_tests.py - InstanceSecurityGroupTests
@@ -410,6 +654,19 @@ create_instance_tests.py - InstanceSecurityGroupTests
 |                                       | Neutron 2     | that has already been added to the instance               |
 +---------------------------------------+---------------+-----------------------------------------------------------+
 
+create_instance_tests.py - CreateInstanceVolumeTests
+----------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                             | API Versions  | Description                                               |
++=======================================+===============+===========================================================+
+| test_create_instance_with_one_volume  | Nova 2        | Ensures that a VM instance can have one volume attached   |
+|                                       | Cinder 2 & 3  | to it                                                     |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_instance_with_two_volumes | Nova 2        | Ensures that a VM instance can have two volumes attached  |
+|                                       | Cinder 2 & 3  | to it                                                     |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
 ansible_utils_tests.py - AnsibleProvisioningTests
 -------------------------------------------------
 
@@ -423,3 +680,35 @@ ansible_utils_tests.py - AnsibleProvisioningTests
 |                                       | Neutron 2     | apply a Ansible playbook containing Jinga2 substitution   |
 |                                       |               | values                                                    |
 +---------------------------------------+---------------+-----------------------------------------------------------+
+
+cluster_template_tests.py - CreateClusterTemplateTests
+------------------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name                              |  Magnum API   | Description                                               |
++========================================+===============+===========================================================+
+| test_create_cluster_template           | 1             | Tests the creation of a Cluster template with the class   |
+|                                        |               | OpenStackClusterTemplate                                  |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_cluster_template    | 1             | Tests the creation and deletiong of a Cluster template    |
+|                                        |               | with the class OpenStackClusterTemplate                   |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_same_cluster_template      | 1             | Tests the creation of a Cluster template 2x using the same|
+|                                        |               | config object to ensure it was only created once          |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad_flavor| 1             | Tests to ensure OpenStackClusterTemplate#create() will    |
+|                                        |               | raise an exception when the flavor is invalid             |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad_master| 1             | Tests to ensure OpenStackClusterTemplate#create() will    |
+| _flavor                                |               | raise an exception when the master flavor is invalid      |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad_image | 1             | Tests to ensure OpenStackClusterTemplate#create() will    |
+|                                        |               | raise an exception when the image is invalid              |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad       | 1             | Tests to ensure OpenStackClusterTemplate#create() will    |
+| _network_driver                        |               | raise an exception when the network driver is invalid     |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_cluster_template_bad       | 1             | Tests to ensure OpenStackClusterTemplate#create() will    |
+| _volume_driver                         |               | raise an exception when the volume driver is invalid      |
++----------------------------------------+---------------+-----------------------------------------------------------+
+
index 16cf446..aa7bf91 100644 (file)
@@ -30,15 +30,16 @@ attributes are listed below:
 -  auth\_url
 -  project\_name (aka. tenant\_name)
 -  identity\_api\_version (for obtaining Keystone authorization token.
-   Versions 2.0 & v3 only validated.)
--  image\_api\_version (Glance version 1 & 2 only validated)
+   default = 2, Versions 2.0 & v3 only validated.)
+-  image\_api\_version (default = 2, Glance version 1 & 2 only validated)
 -  network\_api\_version (Neutron version 2 currently only validated)
 -  compute\_api\_version (Nova version 2 currently only validated)
 -  heat\_api\_version (Heat version 1 currently only validated)
+-  volume\_api\_version (default = 2, Heat versions 2 & 3 currently only validated)
 -  user\_domain\_id (default='default')
--  user\_domain\_name (default='default')
+-  user\_domain\_name (default='Default')
 -  project\_domain\_id (default='default')
--  project\_domain\_name (default='default')
+-  project\_domain\_name (default='Default')
 -  interface (default='admin', used to specify the endpoint type for keystone: public, admin, internal)
 -  cacert (default=False, expected values T|F to denote server certificate verification, else value contains the path to an HTTPS certificate)
 -  region_name (The region name default=None)
@@ -78,7 +79,7 @@ Create User
 -----------
 -  User - snaps.openstack.create\_user.OpenStackUser
 
-   -  snaps.openstack.create\_user.UserSettings
+   -  snaps.openstack.user.UserConfig
 
       -  name - the username (required)
       -  password - the user's password (required)
@@ -88,11 +89,14 @@ Create User
       -  email - the user's email address (optional)
       -  enabled - flag to determine whether or not the user should be
          enabled (default=True)
+      -  roles - dict where key is the role's name and value is the name
+         the project to associate with the role (optional)
 
 .. code:: python
 
-    from snaps.openstack.create_user import UserSettings, OpenStackUser
-    user_settings = UserSettings(name='username', password='password')
+    from snaps.config.user import UserConfig
+    from snaps.openstack.create_user import OpenStackUser
+    user_settings = UserConfig(name='username', password='password')
     user_creator = OpenStackUser(os_creds, user_settings)
     user_creator.create()
 
@@ -109,19 +113,20 @@ Create Project
 --------------
 -  Project - snaps.openstack.create\_project.OpenStackProject
 
-   -  snaps.openstack.create\_project.ProjectSettings
+   -  snaps.openstack.project.ProjectConfig
 
       -  name - the project name (required)
       -  domain - the project's domain (default='default')
       -  description - the project's description (optional)
-      -  enables - flag to determine whether or not the project should
+      -  enabled - flag to determine whether or not the project should
          be enabled (default=True)
 
 
 .. code:: python
 
-    from snaps.openstack.create_project import ProjectSettings, OpenStackProject
-    project_settings = ProjectSettings(name='username', password='password')
+    from snaps.openstack.project import ProjectConfig
+    from snaps.openstack.create_project import OpenStackProject
+    project_settings = ProjectConfig(name='username', password='password')
     project_creator = OpenStackProject(os_creds, project_settings)
     project_creator.create()
 
@@ -135,7 +140,7 @@ Create Flavor
 -------------
 -  Flavor - snaps.openstack.create\_flavor.OpenStackFlavor
 
-   -  snaps.openstack.create\_flavor.FlavorSettings
+   -  snaps.config.flavor.FlavorConfig
 
       -  name - the flavor name (required)
       -  flavor\_id - the flavor's string ID (default='auto')
@@ -148,11 +153,13 @@ Create Flavor
          if backend supports QoS extension (default=1.0)
       -  is\_public - flag that denotes whether or not other projects
          can access image (default=True)
+      -  metadata - freeform dict() for special metadata (optional)
 
 .. code:: python
 
-    from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
-    flavor_settings = FlavorSettings(name='flavor-name', ram=4, disk=10, vcpus=2)
+    from snaps.config.flavor import FlavorConfig
+    from snaps.openstack.create_flavor import OpenStackFlavor
+    flavor_settings = FlavorConfig(name='flavor-name', ram=4, disk=10, vcpus=2)
     flavor_creator = OpenStackFlavor(os_creds, flavor_settings)
     flavor_creator.create()
 
@@ -166,27 +173,33 @@ Create Image
 ------------
 -  Image - snaps.openstack.create\_image.OpenStackImage
 
-   -  snaps.openstack.create\_image.ImageSettings
+   -  snaps.config.image.ImageConfig
 
       -  name - the image name (required)
       -  image\_user - the default image user generally used by
          OpenStackVMInstance class for obtaining an SSH connection
          (required)
-      -  img\_format - the image's format (i.e. qcow2) (required)
+      -  img\_format or format - the image's format (i.e. qcow2) (required)
       -  url - the download URL to obtain the image file (this or
          image\_file must be configured, not both)
       -  image\_file - the location of the file to be sourced from the
          local filesystem (this or url must be configured, not both)
+      -  extra\_properties - dict() object containing extra parameters to
+         pass when loading the image (i.e. ids of kernel and initramfs images)
       -  nic\_config\_pb\_loc - the location of the ansible playbook
          that can configure additional NICs. Floating IPs are required
-         to perform this operation. (optional)
+         to perform this operation. (optional and deprecated)
+      -  kernel\_image\_settings - the image settings for a kernel image (optional)
+      -  ramdisk\_image\_settings - the image settings for a ramdisk image (optional)
+      -  public - image will be created with public visibility when True (default = False)
 
 
 .. code:: python
 
-    from snaps.openstack.create_image import ImageSettings, OpenStackImage
-    image_settings = ImageSettings(name='image-name', image_user='ubuntu', img_format='qcow2',
-                                   url='http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img')
+    from snaps.openstack.create_image import OpenStackImage
+    from snaps.config.image import ImageConfig
+    image_settings = ImageConfig(name='image-name', image_user='ubuntu', img_format='qcow2',
+                                 url='http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img')
     image_creator = OpenStackImage(os_creds, image_settings)
     image_creator.create()
 
@@ -200,7 +213,7 @@ Create Keypair
 --------------
 -  Keypair - snaps.openstack.create\_keypair.OpenStackKeypair
 
-   -  snaps.openstack.create\_keypair.KeypairSettings
+   -  snaps.openstack.keypair.KeypairConfig
 
       -  name - the keypair name (required)
       -  public\_filepath - the file location to where the public key is
@@ -209,11 +222,16 @@ Create Keypair
          file is to be written or currently resides (optional but highly
          recommended to leverage or the private key will be lost
          forever)
+      -  key\_size - The number of bytes for the key size when it needs to
+         be generated (value must be >=512, default = 1024)
+      -  delete\_on\_clean - when True, the key files will be deleted when
+         OpenStackKeypair#clean() is called (default = False)
 
 .. code:: python
 
-    from snaps.openstack.create_keypairs import KeypairSettings, OpenStackKeypair
-    keypair_settings = KeypairSettings(name='kepair-name', private_filepath='/tmp/priv-kp')
+    from snaps.openstack.keypair.KeypairConfig
+    from snaps.openstack.create_keypairs import OpenStackKeypair
+    keypair_settings = KeypairConfig(name='kepair-name', private_filepath='/tmp/priv-kp')
     keypair_creator = OpenStackKeypair(os_creds, keypair_settings)
     keypair_creator.create()
 
@@ -228,7 +246,7 @@ Create Network
 
 -  Network - snaps.openstack.create\_network.OpenStackNetwork
 
-   -  snaps.openstack.create\_network.NetworkSettings
+   -  snaps.config_network.NetworkConfig
 
       -  name - the name of the network (required)
       -  admin\_state\_up - flag denoting the administrative status of
@@ -242,8 +260,10 @@ Create Network
       -  network\_type - the type of network (i.e. vlan\|vxlan\|flat)
       -  physical\_network - the name of the physical network (required
          when network\_type is 'flat')
+      -  segmentation\_id - the id of the segmentation (required
+         when network\_type is 'vlan')
       -  subnet\_settings (list of optional
-         snaps.openstack.create\_network.SubnetSettings objects)
+         snaps.config.network.SubnetConfig objects)
 
          -  cidr - the subnet's CIDR (required)
          -  ip\_version - 4 or 6 (default=4)
@@ -267,10 +287,11 @@ Create Network
 
 .. code:: python
 
-    from snaps.openstack.create_network import NetworkSettings, SubnetSettings, OpenStackNetwork
+    from snaps.config.network import NetworkConfig, SubnetConfig
+    from snaps.openstack.create_network import OpenStackNetwork
 
-    subnet_settings = SubnetSettings(name='subnet-name', cidr='10.0.0.0/24')
-    network_settings = NetworkSettings(name='network-name', subnet_settings=[subnet_settings])
+    subnet_settings = SubnetConfig(name='subnet-name', cidr='10.0.0.0/24')
+    network_settings = NetworkConfig(name='network-name', subnet_settings=[subnet_settings])
 
     network_creator = OpenStackNetwork(os_creds, network_settings)
     network_creator.create()
@@ -320,10 +341,12 @@ Create Security Group
 
 .. code:: python
 
+    from snaps.config.network import SubnetConfig
+    from snaps.config.rule import RuleConfig
     from snaps.openstack.create_security_group import SecurityGroupSettings, SecurityGroupRuleSettings, Direction, OpenStackSecurityGroup
 
-    rule_settings = SubnetSettings(name='subnet-name', cidr='10.0.0.0/24')
-    network_settings = NetworkSettings(name='network-name', subnet_settings=[subnet_settings])
+    rule_settings = RuleConfig(name='subnet-name', cidr='10.0.0.0/24')
+    network_settings = SubnetConfig(name='network-name', subnet_settings=[subnet_settings])
 
     sec_grp_name = 'sec-grp-name'
     rule_settings = SecurityGroupRuleSettings(name=sec_grp_name, direction=Direction.ingress)
@@ -343,7 +366,7 @@ Create Router
 
 -  Router - snaps.openstack.create\_router.OpenStackRouter
 
-   -  snaps.openstack.create\_router.RouterSettings
+   -  snaps.openstack.router.RouterConfig
 
       -  name - the router name (required)
       -  project\_name - the name of the project (optional - can only be
@@ -356,30 +379,41 @@ Create Router
       -  internal\_subnets - list of subnet names to which this router
          will connect (optional)
       -  port\_settings (list of optional
-         snaps.openstack.create\_router.PortSettings objects) - creates
+         snaps.config.network.PortConfig objects) - creates
          custom ports to internal subnets (similar to internal\_subnets
          with more control)
 
-         -  name
-         -  network\_name
-         -  admin\_state\_up
+         -  name - the port's display name
+         -  network\_name - the name of the network on which to create the port
+         -  admin\_state\_up - A boolean value denoting the administrative
+            status of the port (default = True)
          -  project\_name - the name of the project (optional - can only
             be set by admin users)
-         -  mac\_address
-         -  ip\_addrs
-         -  fixed\_ips
-         -  security\_groups
-         -  allowed\_address\_pairs
-         -  opt\_value
-         -  opt\_name
-         -  device\_owner
-         -  device\_id
+         -  mac\_address - the port's MAC address to set (optional and
+            recommended not to set this configuration value)
+         -  ip\_addrs - list of dict() objects containing two keys 'subnet_name'
+            and 'ip' where the value of the 'ip' entry is the expected IP
+            address assigned. This value gets mapped to the fixed\_ips
+            attribute (optional)
+         -  fixed\_ips - dict() where the key is the subnet ID and value is the
+            associated IP address to assign to the port (optional)
+         -  security\_groups - list of security group IDs (not tested)
+         -  allowed\_address\_pairs - A dictionary containing a set of zero or
+            more allowed address pairs. An address pair contains an IP address
+            and MAC address (optional)
+         -  opt\_value - the extra DHCP option value (optional)
+         -  opt\_name - the extra DHCP option name (optional)
+         -  device\_owner - The ID of the entity that uses this port.
+            For example, a DHCP agent (optional)
+         -  device\_id - The ID of the device that uses this port.
+            For example, a virtual server (optional)
 
 .. code:: python
 
-    from snaps.openstack.create_router import RouterSettings, OpenStackRouter
+    from snaps.config.router import RouterConfig
+    from snaps.openstack.create_router import OpenStackRouter
 
-    router_settings = RouterSettings(name='router-name', external_gateway='external')
+    router_settings = RouterConfig(name='router-name', external_gateway='external')
     router_creator = OpenStackRouter(os_creds, router_settings)
     router_creator.create()
 
@@ -389,6 +423,122 @@ Create Router
     # Cleanup
     router_creator.clean()
 
+Create QoS Spec
+---------------
+
+-  Volume Type - snaps.openstack.create\_qos.OpenStackQoS
+
+   -  snaps.openstack.qos.QoSConfig
+
+      -  name - the volume type's name (required)
+      -  consumer - the qos's consumer type of the enum type Consumer (required)
+      -  specs - freeform dict() to be added as 'specs' (optional)
+
+.. code:: python
+
+    from snaps.openstack.qos import QoSConfig
+    from snaps.openstack.create_qos import OpenStackQoS
+
+    qos_settings = QoSConfig(name='stack-name', consumer=Consumer.front-end)
+    qos_creator = OpenStackQoS(os_creds, vol_type_settings)
+    qos_creator.create()
+
+    # Perform logic
+    ...
+
+    # Cleanup
+    qos_creator.clean()
+
+Create Volume Type
+------------------
+
+-  Volume Type - snaps.openstack.create\_volume\_type.OpenStackVolumeType
+
+   -  snaps.config.volume\_type.VolumeTypeConfig
+
+      -  name - the volume type's name (required)
+      -  description - the volume type's description (optional)
+      -  encryption - instance or config for VolumeTypeEncryptionConfig (optional)
+      -  qos\_spec\_name - name of the QoS Spec to associate (optional)
+      -  public - instance or config for VolumeTypeEncryptionConfig (optional)
+
+.. code:: python
+
+    from snaps.config.volume_type import VolumeTypeConfig
+    from snaps.openstack.create_volume_type import OpenStackVolumeType
+
+    vol_type_settings = VolumeTypeConfig(name='stack-name')
+    vol_type_creator = OpenStackHeatStack(os_creds, vol_type_settings)
+    vol_type_creator.create()
+
+    # Perform logic
+    ...
+
+    # Cleanup
+    vol_type_creator.clean()
+
+Create Volume
+-------------
+
+-  Volume - snaps.openstack.create\_volume.OpenStackVolume
+
+   -  snaps.config.volume.VolumeConfig
+
+      -  name - the volume type's name (required)
+      -  description - the volume type's description (optional)
+      -  size - size of volume in GB (default = 1)
+      -  image_name - when a glance image is used for the image source (optional)
+      -  type\_name - the associated volume's type name (optional)
+      -  availability\_zone - the name of the compute server on which to
+         deploy the volume (optional)
+      -  multi_attach - when true, volume can be attached to more than one
+         server (default = False)
+
+.. code:: python
+
+    from snaps.config.volume import VolumeConfig
+    from snaps.openstack.create\_volume import OpenStackVolume
+
+    vol_settings = VolumeConfig(name='stack-name')
+    vol_creator = OpenStackVolume(os_creds, vol_settings)
+    vol_creator.create()
+
+    # Perform logic
+    ...
+
+    # Cleanup
+    vol_type_creator.clean()
+
+Create Heat Stack
+-----------------
+
+-  Heat Stack - snaps.openstack.create\_stack.OpenStackHeatStack
+
+   -  snaps.config.stack.StackConfig
+
+      -  name - the stack's name (required)
+      -  template - the heat template in dict() format (required when
+         template_path is None)
+      -  template\_path - the location of the heat template file (required
+         when template is None)
+      -  env\_values - dict() of strings for substitution of template
+         default values (optional)
+
+.. code:: python
+
+    from snaps.config.stack import StackConfig
+    from snaps.openstack.create_stack import OpenStackHeatStack
+
+    stack_settings = StackConfig(name='stack-name', template_path='/tmp/template.yaml')
+    stack_creator = OpenStackHeatStack(os_creds, stack_settings)
+    stack_creator.create()
+
+    # Perform logic
+    ...
+
+    # Cleanup
+    stack_creator.clean()
+
 Create VM Instance
 ------------------
 
@@ -399,7 +549,7 @@ Create VM Instance
       -  name - the name of the VM (required)
       -  flavor - the name of the flavor (required)
       -  port\_settings - list of
-         snaps.openstack.create\_network.PortSettings objects where each
+         snaps.config.network.PortConfig objects where each
          denote a NIC (see above in create router section for details)
          API does not require, but newer NFVIs now require VMs have at
          least one network
@@ -434,17 +584,17 @@ Create VM Instance
       -  userdata - the cloud-init script to execute after VM has been
          started
 
-   -  image\_settings - see snaps.openstack.create\_image.ImageSettings
+   -  image\_settings - see snaps.config.image.ImageConfig
       above (required)
    -  keypair\_settings - see
-      snaps.openstack.create\_keypairs.KeypairSettings above (optional)
+      snaps.openstack.keypair.KeypairConfig above (optional)
 
 .. code:: python
 
     from snaps.openstack.create_instance import VmInstanceSettings, FloatingIpSettings, OpenStackVmInstance
-    from snaps.openstack.create_network import PortSettings
+    from snaps.config.network import PortConfig
 
-    port_settings = PortSettings(name='port-name', network_name=network_settings.name)
+    port_settings = PortConfig(name='port-name', network_name=network_settings.name)
     floating_ip_settings = FloatingIpSettings(name='fip1', port_name=port_settings.name, router_name=router_settings.name)
     instance_settings = VmInstanceSettings(name='vm-name', flavor='flavor_settings.name', port_settings=[port_settings],
                                            floating_ip_settings=[floating_ip_settings])
@@ -503,7 +653,12 @@ an example of this pattern as this is the only API where SNAPS is
 supporting more than one version)
 
 -  snaps.openstack.utils.keystone\_utils - for calls to the Keystone
-   APIs
+   APIs (support for versions 2 & 3)
 -  snaps.openstack.utils.glance\_utils - for calls to the Glance APIs
+   (support for versions 1 & 2)
 -  snaps.openstack.utils.neutron\_utils - for calls to the Neutron APIs
--  snaps.openstack.utils.nova\_utils - for calls to the Nova APIs
+   (version 2)
+-  snaps.openstack.utils.nova\_utils - for calls to the Nova APIs (version 2)
+-  snaps.openstack.utils.heat\_utils - for calls to the Heat APIs (version 1)
+-  snaps.openstack.utils.cinder\_utils - for calls to the Cinder APIs
+   (support for versions 2 & 3)
index 5fb04db..5bd4f08 100644 (file)
@@ -36,11 +36,17 @@ OSCredsUnitTests
 Ensures that all required members are included when constructing a
 OSCreds object
 
+SecurityGroupRuleConfigUnitTests
+--------------------------------
+
+Ensures that all required members are included when constructing a
+SecurityGroupRuleConfig object
+
 SecurityGroupRuleSettingsUnitTests
 ----------------------------------
 
 Ensures that all required members are included when constructing a
-SecurityGroupRuleSettings object
+deprecated SecurityGroupRuleSettings object
 
 SecurityGroupRuleDomainObjectTests
 ----------------------------------
@@ -48,11 +54,17 @@ SecurityGroupRuleDomainObjectTests
 Ensures that all required members are included when constructing a
 SecurityGroupRule domain object
 
+SecurityGroupConfigUnitTests
+----------------------------
+
+Ensures that all required members are included when constructing a
+SecuirtyGroupConfig object
+
 SecurityGroupSettingsUnitTests
 ------------------------------
 
 Ensures that all required members are included when constructing a
-SecuirtyGroupSettings object
+deprecated SecuirtyGroupSettings object
 
 SecurityGroupDomainObjectTests
 ------------------------------
@@ -60,11 +72,17 @@ SecurityGroupDomainObjectTests
 Ensures that all required members are included when constructing a
 SecurityGroup domain object
 
+ImageConfigUnitTests
+--------------------
+
+Ensures that all required members are included when constructing a
+ImageConfig object
+
 ImageSettingsUnitTests
 ----------------------
 
 Ensures that all required members are included when constructing a
-ImageSettings object
+ImageSettings object (deprecated see ImageConfigUnitTests)
 
 ImageDomainObjectTests
 ----------------------
@@ -72,11 +90,17 @@ ImageDomainObjectTests
 Ensures that all required members are included when constructing a
 Image domain object
 
+FlavorConfigUnitTests
+---------------------
+
+Ensures that all required members are included when constructing a
+FlavorConfig object
+
 FlavorSettingsUnitTests
 -----------------------
 
 Ensures that all required members are included when constructing a
-FlavorSettings object
+deprecated FlavorSettings object
 
 FlavorDomainObjectTests
 -----------------------
@@ -84,11 +108,17 @@ FlavorDomainObjectTests
 Ensures that all required members are included when constructing a
 Flavor domain object
 
+KeypairConfigUnitTests
+----------------------
+
+Ensures that all required members are included when constructing a
+KeypairConfig object
+
 KeypairSettingsUnitTests
 ------------------------
 
 Ensures that all required members are included when constructing a
-KeypairSettings object
+deprecated KeypairSettings object
 
 KeypairDomainObjectTests
 ------------------------
@@ -96,11 +126,17 @@ KeypairDomainObjectTests
 Ensures that all required members are included when constructing a
 Keypair domain object
 
+UserConfigUnitTests
+-------------------
+
+Ensures that all required members are included when constructing a
+UserConfig object
+
 UserSettingsUnitTests
 ---------------------
 
 Ensures that all required members are included when constructing a
-UserSettings object
+deprecated UserSettings object
 
 UserDomainObjectTests
 ---------------------
@@ -108,11 +144,17 @@ UserDomainObjectTests
 Ensures that all required members are included when constructing a
 User domain object
 
+ProjectConfigUnitTests
+----------------------
+
+Ensures that all required members are included when constructing a
+ProjectConfig object
+
 ProjectSettingsUnitTests
 ------------------------
 
 Ensures that all required members are included when constructing a
-ProjectSettings object
+deprecated ProjectSettings object
 
 ProjectDomainObjectTests
 ------------------------
@@ -144,11 +186,17 @@ RoleDomainObjectTests
 Ensures that all required members are included when constructing a
 Role domain object
 
+NetworkConfigUnitTests
+----------------------
+
+Ensures that all required members are included when constructing a
+NetworkConfig object
+
 NetworkSettingsUnitTests
 ------------------------
 
 Ensures that all required members are included when constructing a
-NetworkSettings object
+deprecated NetworkSettings object
 
 NetworkObjectTests
 ------------------
@@ -156,11 +204,17 @@ NetworkObjectTests
 Ensures that all required members are included when constructing a
 Network domain object
 
+SubnetConfigUnitTests
+---------------------
+
+Ensures that all required members are included when constructing a
+SubnetConfig object
+
 SubnetSettingsUnitTests
 -----------------------
 
 Ensures that all required members are included when constructing a
-SubnetSettings object
+deprecated SubnetSettings object
 
 SubnetObjectTests
 -----------------
@@ -168,11 +222,17 @@ SubnetObjectTests
 Ensures that all required members are included when constructing a
 Subnet domain object
 
+PortConfigUnitTests
+-------------------
+
+Ensures that all required members are included when constructing a
+PortConfig object
+
 PortSettingsUnitTests
 ---------------------
 
 Ensures that all required members are included when constructing a
-PortSettings object
+deprecated PortSettings object
 
 PortDomainObjectTests
 ---------------------
@@ -180,11 +240,17 @@ PortDomainObjectTests
 Ensures that all required members are included when constructing a
 Port domain object
 
+RouterConfigUnitTests
+---------------------
+
+Ensures that all required members are included when constructing a
+RouterConfig object
+
 RouterSettingsUnitTests
 -----------------------
 
 Ensures that all required members are included when constructing a
-RouterSettings object
+deprecated RouterSettings object
 
 RouterDomainObjectTests
 -----------------------
@@ -198,11 +264,17 @@ InterfaceRouterDomainObjectTests
 Ensures that all required members are included when constructing a
 InterfaceRouter domain object
 
+StackConfigUnitTests
+--------------------
+
+Ensures that all required members are included when constructing a
+StackConfig object
+
 StackSettingsUnitTests
 ----------------------
 
 Ensures that all required members are included when constructing a
-StackSettings object
+deprecated StackSettings object
 
 StackDomainObjectTests
 ----------------------
@@ -222,11 +294,83 @@ OutputDomainObjectTests
 Ensures that all required members are included when constructing a
 Output domain object (for Heat)
 
+VolumeConfigUnitTests
+---------------------
+
+Ensures that all required members are included when constructing a
+VolumeConfig object
+
+VolumeSettingsUnitTests
+-----------------------
+
+Ensures that all required members are included when constructing a
+deprecated VolumeSettings object
+
+VolumeDomainObjectTests
+-----------------------
+
+Ensures that all required members are included when constructing a
+Volume domain object (for Cinder)
+
+VolumeTypeConfigUnitTests
+-------------------------
+
+Ensures that all required members are included when constructing a
+VolumeTypeConfig object
+
+VolumeTypeSettingsUnitTests
+---------------------------
+
+Ensures that all required members are included when constructing a
+deprecated VolumeTypeSettings object
+
+VolumeTypeDomainObjectTests
+---------------------------
+
+Ensures that all required members are included when constructing a
+VolumeType domain object (for Cinder)
+
+VolumeTypeEncryptionObjectTests
+-------------------------------
+
+Ensures that all required members are included when constructing a
+VolumeTypeEncryption domain object (for Cinder)
+
+QoSConfigUnitTests
+------------------
+
+Ensures that all required members are included when constructing a
+QoSConfig object
+
+QoSSettingsUnitTests
+--------------------
+
+Ensures that all required members are included when constructing a
+deprecated QoSSettings object
+
+QoSSpecDomainObjectTests
+------------------------
+
+Ensures that all required members are included when constructing a
+QoSSpec domain object (for Cinder)
+
+VolumeDomainObjectTests
+-----------------------
+
+Ensures that all required members are included when constructing a
+Volume domain object (for Cinder)
+
+FloatingIpConfigUnitTests
+-------------------------
+
+Ensures that all required members are included when constructing a
+FloatingIpConfig object
+
 FloatingIpSettingsUnitTests
 ---------------------------
 
 Ensures that all required members are included when constructing a
-FloatingIpSettings object
+depecated FloatingIpSettings object
 
 FloatingIpDomainObjectTests
 ---------------------------
@@ -234,14 +378,46 @@ FloatingIpDomainObjectTests
 Ensures that all required members are included when constructing a
 FloatingIp domain object
 
+VmInstanceConfigUnitTests
+-------------------------
+
+Ensures that all required members are included when constructing a
+VmInstanceConfig object
+
 VmInstanceSettingsUnitTests
 ---------------------------
 
 Ensures that all required members are included when constructing a
-VmInstanceSettings object
+deprecated VmInstanceSettings object
 
 VmInstDomainObjectTests
 -----------------------
 
 Ensures that all required members are included when constructing a
 VmInst domain object
+
+ClusterTemplateConfigUnitTests
+------------------------------
+
+Ensures that all required members are included when constructing a
+ClusterTemplateConfig object
+
+ClusterTemplateUnitTests
+------------------------
+
+Ensures that all required members are included when constructing a
+ClusterTemplate object
+
+SettingsUtilsUnitTests
+----------------------
+
+Ensures that the settings_utils.py#create_volume_config() function properly
+maps a snaps.domain.Volume object correctly to a
+snaps.config.volume.VolumeConfig object as well as a
+snaps.domain.VolumeType object to a
+snaps.config.volume.VolumeConfig object
+
+
+Ensures that the settings_utils.py#create_flavor_config() function properly
+maps a snaps.domain.Flavor object correctly to a
+snaps.config.flavor.FlavorConfig object
\ No newline at end of file
index dd95202..6c99992 100644 (file)
@@ -3,9 +3,10 @@ Try an example
 
 Use launcher.py to deploy and clean up example environments.  These examples are described in YAML files.
 
-#. Add your OpenStack connection information to the deploy-complex-network.yaml.
+#. Add your OpenStack connection information.
 
-    Edit <path to repo>/examples/complex-network/deploy-complex-network.yaml
+    Edit <path to repo>/examples/inst-w-volume/deploy-env.yaml with your OpenStack
+    credentials and authorization URL
 
    -  openstack: the top level tag that denotes configuration for the OpenStack components
 
@@ -16,8 +17,7 @@ Use launcher.py to deploy and clean up example environments.  These examples are
    -  auth\_url: - the URL to the OpenStack APIs (required)
    -  project\_name: - the name of the OpenStack project for the user
       (required)
-   -  http\_proxy: - the {{ host }}:{{ port }} of the proxy server the
-      HTTPPhotoman01(optional)
+   -  http\_proxy: - the {{ host }}:{{ port }} of the proxy server (optional)
 
 #. Go to the examples directory.
 
@@ -29,13 +29,13 @@ Use launcher.py to deploy and clean up example environments.  These examples are
 
     ::
 
-      python launch.py -t ./complex-network/deploy-complex-network.yaml -d
+      python launch.py -t ./inst-w-volume/deploy-vm-with-volume.yaml -e ./inst-w-volume/deploy-env.yaml -d
 
 #. Clean the deployment.
 
     ::
 
-      python launch.py -t ./complex-network/deploy-complex-network.yaml -c
+      python launch.py -t ./complex-network/deploy-complex-network.yaml -e ./inst-w-volume/deploy-env.yaml -c
 
 #. Customize the deployment by changing the yaml file.
 
@@ -46,183 +46,374 @@ Use launcher.py to deploy and clean up example environments.  These examples are
 -  openstack: the top level tag that denotes configuration for the
    OpenStack components
 
-   -  connection: - contains the credentials and endpoints required to
-      connect with OpenStack
-   -  username: - the project's user (required)
-   -  password: - the tentant's user password (required)
-   -  auth\_url: - the URL to the OpenStack APIs (required)
-   -  project\_name: - the name of the OpenStack project for the user
-      (required)
-   -  http\_proxy: - the {{ host }}:{{ port }} of the proxy server the
-      HTTPPhotoman01(optional)
-   -  images: - describes each image
-   -  image:
-
-      -  name: The unique image name. If the name already exists for
-         your project, a new one will not be created (required)
-      -  format: The format type of the image i.e. qcow2 (required)
-      -  download\_url: The HTTP download location of the image file
-         (required)
-      -  nic\_config\_pb\_loc: The file location relative to the CWD
-         (python directory) to the Ansible Playbook used to configure
-         VMs with more than one port. VMs get their first NIC configured
-         for free while subsequent ones are not. This value/script will
-         only be leveraged when necessary. Centos has been supported
-         with
-         "provisioning/ansible/centos-network-setup/configure\_host.yml".
+   -  connections: the different connections/credentials to be used by the
+      launcher application
+
+       -  connection: the credentials and endpoints required to connect to an
+          OpenStack project/tenant
+
+          -  name: the name of the credentials for use when creating objects (required)
+          -  username: the project's user (required)
+          -  password: the tentant's user password (required)
+          -  auth\_url: the URL to the OpenStack APIs (required)
+          -  project\_name: the name of the OpenStack project for the user
+             (required)
+          -  identity\_api\_version: the Keystone client version to use (default = 2)
+          -  image\_api\_version: the Glance client version to use (default = 2)
+          -  network\_api\_version: the Neutron client version to use (default = 2)
+          -  compute\_api\_version: the Nova client version to use (default = 2)
+          -  heat\_api\_version: the Heat client version to use (default = 1)
+          -  volume\_api\_version: the Cinder client version to use (default = 2)
+          -  user\_domain\_id: the user domain ID to use (default = 'default')
+          -  user\_domain\_name: the user domain name to use (default = 'Default')
+          -  project\_domain\_id: the project domain ID to use (default = 'default')
+          -  project\_domain\_name: the project domain name to use (default = 'Default')
+          -  interface: Used to specify the endpoint type for keystone (default = 'public')
+          -  cacert: True for https or the certification file location (default = False)
+          -  region\_name: the region (default = None)
+          -  proxy\_settings: for accessing APIs hidden behind an HTTP proxy
+
+              - host: hostname or IP of HTTP proxy host (required)
+              - port: port number of the HTTP proxy server (required)
+              - http\_host: hostname or IP of HTTPS proxy host (default = host)
+              - port: port number of the HTTPS proxy server (default = port)
+              - ssh\_proxy\_cmd: the OpenSSH command used to access the SSH port
+                of a VM (optional)
+
+   -  projects: the projects/tenants to create
+
+       -  project: a project/tenant to create (admin user credentials required)
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  name: the project's name (required)
+          -  domain or domain_name: the project's domain name (default = 'Default')
+          -  description: the description (optional)
+          -  users: a list of users to associate to the project (optional)
+          -  enabled: when True the project will be enabled on creation (default = True)
+
+   -  users: the users to create
+
+       -  user: a user to create (admin user credentials required)
+
+          -  os\_creds\_name: the connection name (required)
+          -  name: the username (required)
+          -  password: the user's password (required)
+          -  project\_name: the user's primary project name (optional)
+          -  domain\_name: the user's domain name (default = 'Default')
+          -  email: the user's email address (optional)
+          -  roles: dict where key is the role's name and value is the name
+             of the project to associate with the role (optional)
+
+   -  flavors: the flavors to create
+
+       -  flavor: a flavor to create (admin user credentials required)
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  name: the name (required)
+          -  flavor\_id: the string ID (default 'auto')
+          -  ram: the required RAM in MB (required)
+          -  disk: the size of the root disk in GB (required)
+          -  vcpus: the number of virtual CPUs (required)
+          -  ephemeral: the size of the ephemeral disk in GB (default 0)
+          -  swap: the size of the dedicated swap disk in GB (default 0)
+          -  rxtx\_factor: the receive/transmit factor to be set on ports if
+             backend supports QoS extension (default 1.0)
+          -  is\_public: denotes whether or not the flavor is public (default = True)
+          -  metadata: freeform dict() for special metadata (optional)
+
+   -  qos_specs: the QoS Specs to create
+
+       -  qos_spec: a QoS Spec to create (admin user credentials required)
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  name: the name (required)
+          -  consumer: enumerations: 'front-end', 'back-end', 'both' (required)
+          -  specs: dict of custom values (optional)
+
+   -  volume_types: the Volume Type to create
+
+       -  volume_type: a Volume Type to create (admin user credentials required)
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  name: the name (required)
+          -  description: the description (optional)
+          -  qos_spec_name: the name of the associate QoS Spec (optional)
+          -  public: visibility (default - False)
+          -  encryption: the encryption settings (optional)
+
+             -  name: the name (required)
+             -  provider_class: the provider class (required i.e. LuksEncryptor)
+             -  control_location: enumerations: 'front-end', 'back-end' (required)
+             -  cipher: the encryption algorithm/mode to use (optional)
+             -  key_size: the size of the encryption key, in bits (optional)
+
+   -  volumes: the Volume to create
+
+       -  volume: a Volume to create
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  os\_user: the connection from a new user defined in template
+             (required or use "os\_creds\_name" above
+
+              - name: the user's name (required)
+              - project\_name: the project name to use
+
+          -  name: the name (required)
+          -  description: the description (optional)
+          -  size: the volume size in GB (default = 1)
+          -  image_name: the image name to leverage (optional)
+          -  type_name: the volume type name to associate (optional)
+          -  availability_zone: the zone name on which to deploy (optional)
+          -  multi_attach: when true, volume can be attached to more than one VM
+             (default = False)
+
+   -  images: describes each image to create
+
+       -  image:
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  os\_user: the connection from a new user defined in template
+             (required or use "os\_creds\_name" above
+
+              - name: the user's name (required)
+              - project\_name: the project name to use
+
+          -  name: The unique image name. If the name already exists for
+             your project, a new one will not be created (required)
+          -  image\_user: the image's default sudo user (required)
+          -  format or img\_format: the image format type (required i.e. qcow2)
+          -  url or download\_url: The HTTP download location of the image file
+             (required when "image_file" below has not been configured)
+          -  image\_file: the image file location (required when "url" has not
+             been configured)
+          -  kernel\_image\_settings: the settings for a kernel image (optional)
+          -  ramdisk\_image\_settings: the settings for a kernel image (optional)
+          -  public: publically visibile when True (default = True)
 
    -  networks:
-   -  network:
-
-      -  name: The name of the network to be created. If one already
-         exists, a new one will not be created (required)
-      -  admin\_state\_up: T\|F (default True)
-      -  shared: (optional)
-      -  project\_name: Name of the project who owns the network. Note:
-         only administrative users can specify projects other than their
-         own (optional)
-      -  external: T\|F whether or not network is external (default
-         False)
-      -  network\_type: The type of network to create. (optional)
-      -  subnets:
-      -  subnet:
-
-         -  name: The name of the network to be created. If one already
-            exists, a new one will not be created. Note: although
-            OpenStack allows for multiple subnets to be applied to any
-            given network, we have not included support as our current
-            use cases does not utilize this functionality (required)
-         -  cidr: The subnet mask value (required)
-         -  dns\_nameservers: A list of IP values used for DNS
-            resolution (default: 8.8.8.8)
-         -  ip\_version: 4\|6 (default: 4)
-         -  project\_name: Name of the project who owns the network.
-            Note: only administrative users can specify projects other
-            than their own (optional)
-         -  start: The start address for allocation\_pools (optional)
-         -  end: The ending address for allocation\_pools (optional)
-         -  gateway\_ip: The IP address to the gateway (optional)
-         -  enable\_dhcp: T\|F (optional)
-         -  dns\_nameservers: List of DNS server IPs
-         -  host\_routes: A list of host route dictionaries (optional)
-            i.e.:
-            ``yaml    "host_routes":[    {    "destination":"0.0.0.0/0",    "nexthop":"123.456.78.9"    },    {    "destination":"192.168.0.0/24",    "nexthop":"192.168.0.1"    }    ]``
-         -  destination: The destination for a static route (optional)
-         -  nexthop: The next hop for the destination (optional)
-         -  ipv6\_ra\_mode: Valid values: "dhcpv6-stateful",
-            "dhcpv6-stateless", and "slaac" (optional)
-         -  ipv6\_address\_mode: Valid values: "dhcpv6-stateful",
-            "dhcpv6-stateless", and "slaac" (optional)
+       -  network:
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  os\_user: the connection from a new user defined in template
+             (required or use "os\_creds\_name" above
+
+              - name: the user's name (required)
+              - project\_name: the project name to use
+
+          -  name: The name of the network to be created. If one already
+             exists, a new one will not be created (required)
+          -  admin\_state\_up: T\|F (default True)
+          -  shared: (optional)
+          -  project\_name: Name of the project who owns the network. Note:
+             only administrative users can specify projects other than their
+             own (optional)
+          -  external: T\|F whether or not network is external (default False)
+          -  network\_type: The type of network to create (optional)
+          -  physical\_network: the name of the physical network
+             (required when network_type is 'flat')
+          -  segmentation\_id: the id of the segmentation
+             (required when network_type is 'vlan')
+          -  subnets:
+              -  subnet:
+
+                 -  name: The name of the network to be created. If one already
+                    exists, a new one will not be created. Note: although
+                    OpenStack allows for multiple subnets to be applied to any
+                    given network, we have not included support as our current
+                    use cases does not utilize this functionality (required)
+                 -  cidr: The subnet mask value (required)
+                 -  dns\_nameservers: A list of IP values used for DNS
+                    resolution (default: 8.8.8.8)
+                 -  ip\_version: 4\|6 (default: 4)
+                 -  project\_name: Name of the project who owns the network.
+                    Note: only administrative users can specify projects other
+                    than their own (optional)
+                 -  start: The start address for allocation\_pools (optional)
+                 -  end: The ending address for allocation\_pools (optional)
+                 -  gateway\_ip: The IP address to the gateway (optional)
+                 -  enable\_dhcp: T\|F (optional)
+                 -  dns\_nameservers: List of DNS server IPs (default = ['8.8.8.8']
+                 -  host\_routes: A list of host route dictionaries (optional)
+                    i.e.:
+                    ``yaml    "host_routes":[    {    "destination":"0.0.0.0/0",    "nexthop":"123.456.78.9"    },    {    "destination":"192.168.0.0/24",    "nexthop":"192.168.0.1"    }    ]``
+                 -  destination: The destination for a static route (optional)
+                 -  nexthop: The next hop for the destination (optional)
+                 -  ipv6\_ra\_mode: Valid values: "dhcpv6-stateful",
+                    "dhcpv6-stateless", and "slaac" (optional)
+                 -  ipv6\_address\_mode: Valid values: "dhcpv6-stateful",
+                    "dhcpv6-stateless", and "slaac" (optional)
+
+   -  security_groups:
+
+      -  security_group:
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  os\_user: the connection from a new user defined in template
+             (required or use "os\_creds\_name" above
+
+              - name: the user's name (required)
+              - project\_name: the project name to use
+
+          -  name: The name of the security group to be created (required)
+          -  description: The security group's description (optional)
+          -  project\_name: Name of the project who owns the security group (optional)
+          -  rule\_settings: List of rules to place onto security group (optional)
+
+              -  description: the rule's description (optional)
+              -  protocol: rule's protcol ('icmp' or 'tcp' or 'udp' or 'null')
+              -  ethertype: rule's ethertype ('4' or '6')
+              -  port\_range\_min: The minimum port number in the range that is
+                 matched by the security group rule. When the protocol is 'tcp'
+                 or 'udp', this value must be <= 'port_range_max' (optional)
+              -  port\_range\_max: The maximum port number in the range that is
+                 matched by the security group rule. When the protocol is 'tcp'
+                 or 'udp', this value must be <= 'port_range_max' (optional)
+              -  remote\_ip\_prefix: The remote IP prefix to associate with this
+                 metering rule packet (optional)
 
    -  routers:
 
       -  router:
-      -  name: The name of the router to be created. If one already
-         exists, a new one will not be created (required)
-      -  project\_name: Name of the project who owns the network. Note:
-         only administrative users can specify projects other than their
-         own (optional)
-      -  internal\_subnets: A list of subnet names on which the router
-         will be placed (optional)
-      -  external\_gateway: A dictionary containing the external gateway
-         parameters: "network\_id", "enable\_snat",
-         "external\_fixed\_ips" (optional)
-      -  interfaces: A list of port interfaces to create to other
-         subnets (optional)
-
-         -  port (Leverages the same class/structure as port objects on
-            VM instances. See port definition below for a
-            full accounting of the port attributes. The ones listed
-            below are generally used for routers)
-
-            -  name: The name given to the new port (must be unique for
-               project) (required)
-            -  network\_name: The name of the new port's network
-               (required)
-            -  ip\_addrs: A list of k/v pairs (optional)
-            -  subnet\_name: the name of a subnet that is on the port's
-               network
-            -  ip: An IP address of the associated subnet to assign to
-               the new port (optional but generally required for router
-               interfaces)
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  os\_user: the connection from a new user defined in template
+             (required or use "os\_creds\_name" above
+
+              - name: the user's name (required)
+              - project\_name: the project name to use
+
+          -  name: The name of the router to be created (required)
+          -  project\_name: Name of the project who owns the network (optional)
+          -  external\_gateway: Name of the external network to which to route
+             (optional)
+          -  admin\_state\_up: T\|F (default True)
+          -  external\_fixed\_ids: Dictionary containing the IP address
+             parameters (optional)
+          -  internal\_subnets: List of subnet names to which to connect this
+             router (optional)
+
+             -  port_settings (Leverages the same class/structure as port objects on
+                VM instances. See port definition below for a
+                full accounting of the port attributes. The ones listed
+                below are generally used for routers)
+
+                -  name: The name given to the new port (required and must be
+                   unique for project)
+                -  network\_name: The name of the network on which to create
+                   the port (optional)
+                -  admin\_state\_up: T\|F (default True)
+                -  project\_name: Name of the project who owns the network (optional)
+                -  mac\_address: The port's MAC address (optional)
+                -  ip\_addrs: A list of k/v pairs (optional)
+                -  security\_groups: a list of names of the the security groups
+                   to apply to the port
+                -  opt\_value: The extra DHCP option value (optional)
+                -  opt\_name: The extra DHCP option name (optional)
 
    -  keypairs:
 
       -  keypair:
-      -  name: The name of the keypair to be created. If one already
-         exists, a new one will not be created but simply loaded from
-         its configured file location (required)
-      -  public\_filepath: The path to where the generated public key
-         will be stored if it does not exist (optional but really
-         required for provisioning purposes)
-      -  private\_filepath: The path to where the generated private key
-         will be stored if it does not exist (optional but really
-         required for provisioning purposes)
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  os\_user: the connection from a new user defined in template
+             (required or use "os\_creds\_name" above
+
+              - name: the user's name (required)
+              - project\_name: the project name to use
+
+          -  name: The name of the keypair to be created. If one already
+             exists, a new one will not be created but simply loaded from
+             its configured file location (required)
+          -  public\_filepath: The path to where the generated public key
+             will be stored if it does not exist (optional but really
+             required for provisioning purposes)
+          -  private\_filepath: The path to where the generated private key
+             will be stored if it does not exist (optional but really
+             required for provisioning purposes)
 
    -  instances:
 
       -  instance:
-      -  name: The unique instance name for project. (required)
-      -  flavor: Must be one of the preconfigured flavors (required)
-      -  imageName: The name of the image to be used for deployment
-         (required)
-      -  keypair\_name: The name of the keypair to attach to instance
-         (optional but required for NIC configuration and Ansible
-         provisioning)
-      -  sudo\_user: The name of a sudo\_user that is attached to the
-         keypair (optional but required for NIC configuration and
-         Ansible provisioning)
-      -  vm\_boot\_timeout: The number of seconds to block waiting for
-         an instance to deploy and boot (default 900)
-      -  vm\_delete\_timeout: The number of seconds to block waiting for
-         an instance to be deleted (default 300)
-      -  ssh\_connect\_timeout: The number of seconds to block waiting
-         for an instance to achieve an SSH connection (default 120)
-      -  ports: A list of port configurations (should contain at least
-         one)
-      -  port: Denotes the configuration of a NIC
-
-         -  name: The unique port name for project (required)
-         -  network\_name: The name of the network to which the port is
-            attached (required)
-         -  ip\_addrs: Static IP addresses to be added to the port by
-            subnet (optional)
-         -  subnet\_name: The name of the subnet
-         -  ip: The assigned IP address (when null, OpenStack will
-            assign an IP to the port)
-         -  admin\_state\_up: T\|F (default True)
-         -  project\_name: The name of the project who owns the network.
-            Only administrative users can specify a the project ID other
-            than their own (optional)
-         -  mac\_address: The desired MAC for the port (optional)
-         -  fixed\_ips: A dictionary that allows one to specify only a
-            subnet ID, OpenStack Networking allocates an available IP
-            from that subnet to the port. If you specify both a subnet
-            ID and an IP address, OpenStack Networking tries to allocate
-            the specified address to the port. (optional)
-         -  seurity\_groups: A list of security group IDs (optional)
-         -  allowed\_address\_pairs: A dictionary containing a set of
-            zero or more allowed address pairs. An address pair contains
-            an IP address and MAC address. (optional)
-         -  opt\_value: The extra DHCP option value (optional)
-         -  opt\_name: The extra DHCP option name (optional)
-         -  device\_owner: The ID of the entity that uses this port. For
-            example, a DHCP agent (optional)
-         -  device\_id: The ID of the device that uses this port. For
-            example, a virtual server (optional)
-
-   -  floating\_ips: list of floating\_ip configurations (optional)
-
-      -  floating\_ip:
-      -  name: Must be unique for VM instance (required)
-      -  port\_name: The name of the port requiring access to the
-         external network (required)
-      -  subnet\_name: The name of the subnet contains the IP address on
-         the port on which to create the floating IP (optional)
-      -  router\_name: The name of the router connected to an external
-         network used to attach the floating IP (required)
-      -  provisioning: (True\|False) Denotes whether or not this IP can
-         be used for Ansible provisioning (default True)
+
+          -  os\_creds\_name: the connection name (default = 'default'
+             required or use "os\_user" below instead)
+          -  os\_user: the connection from a new user defined in template
+             (required or use "os\_creds\_name" above
+
+              - name: the user's name (required)
+              - project\_name: the project name to use
+
+          -  name: The unique instance name for project. (required)
+          -  flavor: Must be one of the preconfigured flavors (required)
+          -  imageName: The name of the image to be used for deployment
+             (required)
+          -  keypair\_name: The name of the keypair to attach to instance
+             (optional but required for NIC configuration and Ansible
+             provisioning)
+          -  sudo\_user: The name of a sudo\_user that is attached to the
+             keypair (optional but required for NIC configuration and
+             Ansible provisioning)
+          -  vm\_boot\_timeout: The number of seconds to block waiting for
+             an instance to deploy and boot (default 900)
+          -  vm\_delete\_timeout: The number of seconds to block waiting for
+             an instance to be deleted (default 300)
+          -  ssh\_connect\_timeout: The number of seconds to block waiting
+             for an instance to achieve an SSH connection (default 120)
+          -  ports: A list of port configurations (should contain at least
+             one)
+          -  port: Denotes the configuration of a NIC
+
+             -  name: The unique port name for project (required)
+             -  network\_name: The name of the network to which the port is
+                attached (required)
+             -  ip\_addrs: Static IP addresses to be added to the port by
+                subnet (optional)
+             -  subnet\_name: The name of the subnet
+             -  ip: The assigned IP address (when null, OpenStack will
+                assign an IP to the port)
+             -  admin\_state\_up: T\|F (default True)
+             -  project\_name: The name of the project who owns the network.
+                Only administrative users can specify a the project ID other
+                than their own (optional)
+             -  mac\_address: The desired MAC for the port (optional)
+             -  fixed\_ips: A dictionary that allows one to specify only a
+                subnet ID, OpenStack Networking allocates an available IP
+                from that subnet to the port. If you specify both a subnet
+                ID and an IP address, OpenStack Networking tries to allocate
+                the specified address to the port. (optional)
+             -  seurity\_groups: A list of security group IDs (optional)
+             -  allowed\_address\_pairs: A dictionary containing a set of
+                zero or more allowed address pairs. An address pair contains
+                an IP address and MAC address. (optional)
+             -  opt\_value: The extra DHCP option value (optional)
+             -  opt\_name: The extra DHCP option name (optional)
+             -  device\_owner: The ID of the entity that uses this port. For
+                example, a DHCP agent (optional)
+             -  device\_id: The ID of the device that uses this port. For
+                example, a virtual server (optional)
+
+       -  floating\_ips: list of floating\_ip configurations (optional)
+
+          -  floating\_ip:
+          -  name: Must be unique for VM instance (required)
+          -  port\_name: The name of the port requiring access to the
+             external network (required)
+          -  subnet\_name: The name of the subnet contains the IP address on
+             the port on which to create the floating IP (optional)
+          -  router\_name: The name of the router connected to an external
+             network used to attach the floating IP (required)
+          -  provisioning: (True\|False) Denotes whether or not this IP can
+             be used for Ansible provisioning (default True)
 
 -  ansible: Each set of attributes below are contained in a list
 
index 108bdc0..87c095b 100644 (file)
@@ -1,22 +1,29 @@
 import logging
+
+from snaps.config.vm_inst import VmInstanceConfig
+
 logging.basicConfig(level=logging.INFO)
 
 # Credentials
 from snaps.openstack.os_credentials import OSCreds, ProxySettings
 
 
-proxy_settings = ProxySettings(host='10.197.123.27', port='3128',
-                               ssh_proxy_cmd='/usr/local/bin/corkscrew 10.197.123.27 3128 %h %p')
+proxy_settings = ProxySettings(
+    host='10.197.123.27', port='3128',
+    ssh_proxy_cmd='/usr/local/bin/corkscrew 10.197.123.27 3128 %h %p')
 
-os_creds = OSCreds(username='admin', password='cable123', auth_url='http://192.168.67.10:5000/v2.0/',
-                   project_name='admin', proxy_settings=proxy_settings)
+os_creds = OSCreds(
+    username='admin', password='cable123',
+    auth_url='http://192.168.67.10:5000/v2.0/', project_name='admin',
+    proxy_settings=proxy_settings)
 
 
 # Images
-from snaps.openstack.create_image import ImageSettings, OpenStackImage
+from snaps.openstack.create_image import OpenStackImage
+from snaps.config.image import ImageConfig
 
-image_settings = ImageSettings(name='cirros-test', image_user='cirros', img_format='qcow2',
-                               url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img')
+image_settings = ImageConfig(name='cirros-test', image_user='cirros', img_format='qcow2',
+                             url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img')
 
 image = OpenStackImage(os_creds, image_settings)
 image.create()
@@ -24,27 +31,33 @@ image.create()
 
 
 # Network
-from snaps.openstack.create_network import NetworkSettings, SubnetSettings, OpenStackNetwork
+from snaps.config.network import NetworkConfig, SubnetConfig
+from snaps.openstack.create_network import OpenStackNetwork
 
-subnet_settings = SubnetSettings(name='test-subnet', cidr='10.0.0.1/24')
-network_settings = NetworkSettings(name='test-net', subnet_settings=[subnet_settings])
+subnet_settings = SubnetConfig(name='test-subnet', cidr='10.0.0.1/24')
+network_settings = NetworkConfig(
+    name='test-net', subnet_settings=[subnet_settings])
 network = OpenStackNetwork(os_creds, network_settings)
 network.create()
 
 
 # Flavors
-from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
+from snaps.config.flavor import FlavorConfig
+from snaps.openstack.create_flavor import OpenStackFlavor
 
-flavor_settings = FlavorSettings(name='test-flavor', ram=256, disk=10, vcpus=2)
+flavor_settings = FlavorConfig(name='test-flavor', ram=256, disk=10, vcpus=2)
 flavor = OpenStackFlavor(os_creds, flavor_settings)
 flavor.create()
 
 # Instances
-from snaps.openstack.create_network import PortSettings
-from snaps.openstack.create_instance import VmInstanceSettings, OpenStackVmInstance
-
-port_settings = PortSettings(name='test-port', network_name=network_settings.name)
-instance_settings = VmInstanceSettings(name='test-inst', flavor=flavor_settings.name, port_settings=[port_settings])
+from snaps.config.network import PortConfig
+from snaps.openstack.create_instance import OpenStackVmInstance
+
+port_settings = PortConfig(
+    name='test-port', network_name=network_settings.name)
+instance_settings = VmInstanceConfig(
+    name='test-inst', flavor=flavor_settings.name,
+    port_settings=[port_settings])
 
 vm_inst = OpenStackVmInstance(os_creds, instance_settings, image_settings)
 vm_inst.create(block=True)
diff --git a/examples/inst-w-volume/deploy-env.yaml b/examples/inst-w-volume/deploy-env.yaml
new file mode 100644 (file)
index 0000000..7baf8a2
--- /dev/null
@@ -0,0 +1,44 @@
+---
+admin_user: admin
+admin_pass: ChangeMe
+admin_proj: admin
+auth_url: http://10.197.103.31:5000/
+proxy_host:
+proxy_port:
+id_api_version: 3
+
+ext_net: public1
+
+username: test_user
+pass: test_user
+proj: example-volume
+
+flavor_name: example.m1.small
+flavor_ram: 2048
+flavor_disk: 10
+flavor_cpus: 1
+
+qos_name: example_qos
+vol_type_name: example-vol-type
+vol_type_encryption_name: example_vol-type-encryption
+volume_name: example-volume
+
+image_name: example-image
+image_format: qcow2
+image_url: http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img
+image_file:
+image_user: cirros
+
+net_name: example-net
+subnet_name: example-subnet
+cidr: 10.0.8.0/24
+router_name: example-router
+
+kp_name: example-kp
+kp_pub_path: ~/tmp/example-kp.pub
+kp_priv_path: ~/tmp/example-kp
+
+sg_name: example-sg
+
+port_name: example-port
+inst_name: example-inst
diff --git a/examples/inst-w-volume/deploy-vm-with-volume.yaml b/examples/inst-w-volume/deploy-vm-with-volume.yaml
new file mode 100644 (file)
index 0000000..30dbcc0
--- /dev/null
@@ -0,0 +1,141 @@
+# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+---
+openstack:
+  connections:
+    # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host.
+    - connection:
+        name: admin-creds
+        username: {{ admin_user }}
+        project_name: {{ admin_proj }}
+        password: {{ admin_pass }}
+        auth_url: {{ auth_url }}
+        identity_api_version: {{ id_api_version }}
+  projects:
+    - project:
+        os_creds_name: admin-creds
+        name: {{ proj }}
+        description: Project for Orchestrators
+        users:
+          - {{ username }}
+          - {{ admin_user }}
+  users:
+    - user:
+        os_creds_name: admin-creds
+        name: {{ username }}
+        password: {{ pass }}
+        project_name: {{ proj }}
+        roles: {admin: {{ proj }}}
+  flavors:
+    - flavor:
+        os_creds_name: admin-creds
+        name: {{ flavor_name }}
+        ram: {{ flavor_ram }}
+        disk: {{ flavor_disk }}
+        vcpus: {{ flavor_cpus }}
+  qos_specs:
+    - qos_spec:
+        os_creds_name: admin-creds
+        name: {{ qos_name }}
+        consumer: both
+  volume_types:
+    - volume_type:
+        os_creds_name: admin-creds
+        name: {{ vol_type_name }}
+        encryption:
+          name: {{ vol_type_encryption_name }}
+          provider_class: LuksEncryptor
+          control_location: front-end
+  volumes:
+    - volume:
+        os_user:
+          name: {{ username }}
+          project_name: {{ proj }}
+        name: {{ volume_name }}
+        size: 10
+  images:
+    - image:
+        os_creds_name: admin-creds
+        name: {{ image_name }}
+        format: {{ image_format }}
+        image_user: {{ image_user }}
+        download_url: {{ image_url }}
+        image_file: {{ image_file }}
+        public: True
+  networks:
+    - network:
+        os_user:
+          name: {{ username }}
+          project_name: {{ proj }}
+        name: {{ net_name }}
+        project_name: {{ proj }}
+        subnets:
+          - subnet:
+              name: {{ subnet_name }}
+              project_name: {{ proj }}
+              cidr: {{ cidr }}
+              dns_nameservers: [8.8.8.8]
+  routers:
+    - router:
+        os_user:
+          name: {{ username }}
+          project_name: {{ proj }}
+        name: {{ router_name }}
+        external_gateway: {{ ext_net }}
+        internal_subnets:
+          - {{ subnet_name }}
+  keypairs:
+    - keypair:
+        os_user:
+          name: {{ username }}
+          project_name: {{ proj }}
+        name: {{ kp_name }}
+        public_filepath: {{ kp_pub_path }}
+        private_filepath: {{ kp_priv_path }}
+        delete_on_clean: True
+  security_groups:
+    - security_group:
+        os_user:
+          name: {{ username }}
+          project_name: {{ proj }}
+        name: {{ sg_name }}
+        rules:
+          - direction: ingress
+            protocol: icmp
+          - direction: ingress
+            protocol: tcp
+            port_range_min: 22
+            port_range_max: 22
+  instances:
+    - instance:
+        os_user:
+          name: {{ username }}
+          project_name: {{ proj }}
+        name: {{ inst_name }}
+        flavor: {{ flavor_name }}
+        imageName: {{ image_name }}
+        keypair_name: {{ kp_name }}
+        security_group_names: [{{ sg_name }}]
+        volume_names:
+          - {{ volume_name }}
+        ports:
+          - port:
+              name: {{ port_name_prfx }}-1a
+              network_name: {{ net_name }}
+        floating_ips:
+          - floating_ip:
+              name: fip1
+              port_name: {{ port_name }}
+              router_name: {{ router_name }}
index 76353a2..04bc5d6 100644 (file)
 # This script is responsible for deploying virtual environments
 import argparse
 import logging
-import re
 
-import time
 from jinja2 import Environment, FileSystemLoader
 import os
 import yaml
 
 from snaps import file_utils
-from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
-from snaps.openstack.create_image import ImageSettings, OpenStackImage
-from snaps.openstack.create_instance import VmInstanceSettings
-from snaps.openstack.create_keypairs import KeypairSettings, OpenStackKeypair
-from snaps.openstack.create_network import (
-    PortSettings, NetworkSettings, OpenStackNetwork)
-from snaps.openstack.create_project import OpenStackProject, ProjectSettings
-from snaps.openstack.create_router import RouterSettings, OpenStackRouter
-from snaps.openstack.create_security_group import (
-    OpenStackSecurityGroup, SecurityGroupSettings)
-from snaps.openstack.create_user import OpenStackUser, UserSettings
-from snaps.openstack.os_credentials import OSCreds, ProxySettings
-from snaps.openstack.utils import deploy_utils
-from snaps.provisioning import ansible_utils
+from snaps.openstack.utils import launch_utils
 
 __author__ = 'spisarski'
 
 logger = logging.getLogger('snaps_launcher')
 
 ARG_NOT_SET = "argument not set"
-DEFAULT_CREDS_KEY = 'admin'
-
-
-def __get_creds_dict(os_conn_config):
-    """
-    Returns a dict of OSCreds where the key is the creds name.
-    For backwards compatibility, credentials not contained in a list (only
-    one) will be returned with the key of None
-    :param os_conn_config: the credential configuration
-    :return: a dict of OSCreds objects
-    """
-    if 'connection' in os_conn_config:
-        return {DEFAULT_CREDS_KEY: __get_os_credentials(os_conn_config)}
-    elif 'connections' in os_conn_config:
-        out = dict()
-        for os_conn_dict in os_conn_config['connections']:
-            config = os_conn_dict.get('connection')
-            if not config:
-                raise Exception('Invalid connection format')
-
-            name = config.get('name')
-            if not name:
-                raise Exception('Connection config requires a name field')
-
-            out[name] = __get_os_credentials(os_conn_dict)
-        return out
-
-
-def __get_creds(os_creds_dict, os_user_dict, inst_config):
-    """
-    Returns the appropriate credentials
-    :param os_creds_dict: a dictionary of OSCreds objects where the name is the
-                          key
-    :param os_user_dict: a dictionary of OpenStackUser objects where the name
-                         is the key
-    :param inst_config:
-    :return: an OSCreds instance or None
-    """
-    os_creds = os_creds_dict.get(DEFAULT_CREDS_KEY)
-    if 'os_user' in inst_config:
-        os_user_conf = inst_config['os_user']
-        if 'name' in os_user_conf:
-            user_creator = os_user_dict.get(os_user_conf['name'])
-            if user_creator:
-                return user_creator.get_os_creds(
-                    project_name=os_user_conf.get('project_name'))
-    elif 'os_creds_name' in inst_config:
-        if 'os_creds_name' in inst_config:
-            os_creds = os_creds_dict[inst_config['os_creds_name']]
-    return os_creds
-
-
-def __get_os_credentials(os_conn_config):
-    """
-    Returns an object containing all of the information required to access
-    OpenStack APIs
-    :param os_conn_config: The configuration holding the credentials
-    :return: an OSCreds instance
-    """
-    config = os_conn_config.get('connection')
-    if not config:
-        raise Exception('Invalid connection configuration')
-
-    proxy_settings = None
-    http_proxy = config.get('http_proxy')
-    if http_proxy:
-        tokens = re.split(':', http_proxy)
-        ssh_proxy_cmd = config.get('ssh_proxy_cmd')
-        proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
-                                       ssh_proxy_cmd=ssh_proxy_cmd)
-    else:
-        if 'proxy_settings' in config:
-            host = config['proxy_settings'].get('host')
-            port = config['proxy_settings'].get('port')
-            if host and host != 'None' and port and port != 'None':
-                proxy_settings = ProxySettings(**config['proxy_settings'])
-
-    if proxy_settings:
-        config['proxy_settings'] = proxy_settings
-    else:
-        del config['proxy_settings']
-
-    return OSCreds(**config)
-
-
-def __parse_ports_config(config):
-    """
-    Parses the "ports" configuration
-    :param config: The dictionary to parse
-    :return: a list of PortConfig objects
-    """
-    out = list()
-    for port_config in config:
-        out.append(PortSettings(**port_config.get('port')))
-    return out
-
-
-def __create_instances(os_creds_dict, creator_class, config_class, config,
-                       config_key, cleanup=False, os_users_dict=None):
-    """
-    Returns a dictionary of SNAPS creator objects where the key is the name
-    :param os_creds_dict: Dictionary of OSCreds objects where the key is the
-                          name
-    :param config: The list of configurations for the same type
-    :param config_key: The list of configurations for the same type
-    :param cleanup: Denotes whether or not this is being called for cleanup
-    :return: dictionary
-    """
-    out = {}
-
-    if config:
-        try:
-            for config_dict in config:
-                inst_config = config_dict.get(config_key)
-                if inst_config:
-                    creator = creator_class(
-                        __get_creds(os_creds_dict, os_users_dict, inst_config),
-                        config_class(**inst_config))
-
-                    if cleanup:
-                        creator.initialize()
-                    else:
-                        creator.create()
-                    out[inst_config['name']] = creator
-            logger.info('Created configured %s', config_key)
-        except Exception as e:
-            logger.error('Unexpected error instantiating creator [%s] '
-                         'with exception %s', creator_class, e)
-
-    return out
-
-
-def __create_vm_instances(os_creds_dict, os_users_dict, instances_config,
-                          image_dict, keypairs_dict, cleanup=False):
-    """
-    Returns a dictionary of OpenStackVmInstance objects where the key is the
-    instance name
-    :param os_creds_dict: Dictionary of OSCreds objects where the key is the
-                          name
-    :param os_users_dict: Dictionary of OpenStackUser objects where the key is
-                          the username
-    :param instances_config: The list of VM instance configurations
-    :param image_dict: A dictionary of images that will probably be used to
-                       instantiate the VM instance
-    :param keypairs_dict: A dictionary of keypairs that will probably be used
-                          to instantiate the VM instance
-    :param cleanup: Denotes whether or not this is being called for cleanup
-    :return: dictionary
-    """
-    vm_dict = {}
-
-    if instances_config:
-        try:
-            for instance_config in instances_config:
-                conf = instance_config.get('instance')
-                if conf:
-                    if image_dict:
-                        image_creator = image_dict.get(conf.get('imageName'))
-                        if image_creator:
-                            instance_settings = VmInstanceSettings(
-                                **instance_config['instance'])
-                            kp_name = conf.get('keypair_name')
-                            vm_dict[conf[
-                                'name']] = deploy_utils.create_vm_instance(
-                                __get_creds(
-                                    os_creds_dict, os_users_dict, conf),
-                                instance_settings,
-                                image_creator.image_settings,
-                                keypair_creator=keypairs_dict[kp_name],
-                                init_only=cleanup)
-                        else:
-                            raise Exception('Image creator instance not found.'
-                                            ' Cannot instantiate')
-                    else:
-                        raise Exception('Image dictionary is None. Cannot '
-                                        'instantiate')
-                else:
-                    raise Exception('Instance configuration is None. Cannot '
-                                    'instantiate')
-            logger.info('Created configured instances')
-        except Exception as e:
-            logger.error('Unexpected error creating VM instances - %s', e)
-    return vm_dict
-
-
-def __apply_ansible_playbooks(ansible_configs, os_creds_dict, vm_dict,
-                              image_dict, flavor_dict, env_file):
-    """
-    Applies ansible playbooks to running VMs with floating IPs
-    :param ansible_configs: a list of Ansible configurations
-    :param os_creds_dict: Dictionary of OSCreds objects where the key is the
-                          name
-    :param vm_dict: the dictionary of newly instantiated VMs where the name is
-                    the key
-    :param image_dict: the dictionary of newly instantiated images where the
-                       name is the key
-    :param flavor_dict: the dictionary of newly instantiated flavors where the
-                        name is the key
-    :param env_file: the path of the environment for setting the CWD so
-                     playbook location is relative to the deployment file
-    :return: t/f - true if successful
-    """
-    logger.info("Applying Ansible Playbooks")
-    if ansible_configs:
-        # Ensure all hosts are accepting SSH session requests
-        for vm_inst in list(vm_dict.values()):
-            if not vm_inst.vm_ssh_active(block=True):
-                logger.warning(
-                    "Timeout waiting for instance to respond to SSH requests")
-                return False
-
-        # Set CWD so the deployment file's playbook location can leverage
-        # relative paths
-        orig_cwd = os.getcwd()
-        env_dir = os.path.dirname(env_file)
-        os.chdir(env_dir)
-
-        # Apply playbooks
-        for ansible_config in ansible_configs:
-            if 'pre_sleep_time' in ansible_config:
-                try:
-                    sleep_time = int(ansible_config['pre_sleep_time'])
-                    logger.info('Waiting %s seconds to apply playbooks',
-                                sleep_time)
-                    time.sleep(sleep_time)
-                except:
-                    pass
-
-            os_creds = os_creds_dict.get(None, 'admin')
-            __apply_ansible_playbook(ansible_config, os_creds, vm_dict,
-                                     image_dict, flavor_dict)
-
-        # Return to original directory
-        os.chdir(orig_cwd)
-
-    return True
-
-
-def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
-                             flavor_dict):
-    """
-    Applies an Ansible configuration setting
-    :param ansible_config: the configuration settings
-    :param os_creds: the OpenStack credentials object
-    :param vm_dict: the dictionary of newly instantiated VMs where the name is
-                    the key
-    :param image_dict: the dictionary of newly instantiated images where the
-                       name is the key
-    :param flavor_dict: the dictionary of newly instantiated flavors where the
-                        name is the key
-    """
-    if ansible_config:
-        (remote_user, floating_ips, private_key_filepath,
-         proxy_settings) = __get_connection_info(
-            ansible_config, vm_dict)
-        if floating_ips:
-            retval = ansible_utils.apply_playbook(
-                ansible_config['playbook_location'], floating_ips, remote_user,
-                private_key_filepath,
-                variables=__get_variables(ansible_config.get('variables'),
-                                          os_creds, vm_dict, image_dict,
-                                          flavor_dict),
-                proxy_setting=proxy_settings)
-            if retval != 0:
-                # Not a fatal type of event
-                logger.warning(
-                    'Unable to apply playbook found at location - %s',
-                    ansible_config.get('playbook_location'))
-
-
-def __get_connection_info(ansible_config, vm_dict):
-    """
-    Returns a tuple of data required for connecting to the running VMs
-    (remote_user, [floating_ips], private_key_filepath, proxy_settings)
-    :param ansible_config: the configuration settings
-    :param vm_dict: the dictionary of VMs where the VM name is the key
-    :return: tuple where the first element is the user and the second is a list
-             of floating IPs and the third is the
-    private key file location and the fourth is an instance of the
-    snaps.ProxySettings class
-    (note: in order to work, each of the hosts need to have the same sudo_user
-    and private key file location values)
-    """
-    if ansible_config.get('hosts'):
-        hosts = ansible_config['hosts']
-        if len(hosts) > 0:
-            floating_ips = list()
-            remote_user = None
-            pk_file = None
-            proxy_settings = None
-            for host in hosts:
-                vm = vm_dict.get(host)
-                if vm:
-                    fip = vm.get_floating_ip()
-                    if fip:
-                        remote_user = vm.get_image_user()
-
-                        if fip:
-                            floating_ips.append(fip.ip)
-                        else:
-                            raise Exception(
-                                'Could not find floating IP for VM - ' +
-                                vm.name)
-
-                        pk_file = vm.keypair_settings.private_filepath
-                        proxy_settings = vm.get_os_creds().proxy_settings
-                else:
-                    logger.error('Could not locate VM with name - ' + host)
-
-            return remote_user, floating_ips, pk_file, proxy_settings
-    return None
-
-
-def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict):
-    """
-    Returns a dictionary of substitution variables to be used for Ansible
-    templates
-    :param var_config: the variable configuration settings
-    :param os_creds: the OpenStack credentials object
-    :param vm_dict: the dictionary of newly instantiated VMs where the name is
-                    the key
-    :param image_dict: the dictionary of newly instantiated images where the
-                       name is the key
-    :param flavor_dict: the dictionary of newly instantiated flavors where the
-                        name is the key
-    :return: dictionary or None
-    """
-    if var_config and vm_dict and len(vm_dict) > 0:
-        variables = dict()
-        for key, value in var_config.items():
-            value = __get_variable_value(value, os_creds, vm_dict, image_dict,
-                                         flavor_dict)
-            if key and value:
-                variables[key] = value
-                logger.info(
-                    "Set Jinga2 variable with key [%s] the value [%s]",
-                    key, value)
-            else:
-                logger.warning('Key [%s] or Value [%s] must not be None',
-                               str(key), str(value))
-        return variables
-    return None
-
-
-def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
-                         flavor_dict):
-    """
-    Returns the associated variable value for use by Ansible for substitution
-    purposes
-    :param var_config_values: the configuration dictionary
-    :param os_creds: the OpenStack credentials object
-    :param vm_dict: the dictionary of newly instantiated VMs where the name is
-                    the key
-    :param image_dict: the dictionary of newly instantiated images where the
-                       name is the key
-    :param flavor_dict: the dictionary of newly instantiated flavors where the
-                        name is the key
-    :return:
-    """
-    if var_config_values['type'] == 'string':
-        return __get_string_variable_value(var_config_values)
-    if var_config_values['type'] == 'vm-attr':
-        return __get_vm_attr_variable_value(var_config_values, vm_dict)
-    if var_config_values['type'] == 'os_creds':
-        return __get_os_creds_variable_value(var_config_values, os_creds)
-    if var_config_values['type'] == 'port':
-        return __get_vm_port_variable_value(var_config_values, vm_dict)
-    if var_config_values['type'] == 'floating_ip':
-        return __get_vm_fip_variable_value(var_config_values, vm_dict)
-    if var_config_values['type'] == 'image':
-        return __get_image_variable_value(var_config_values, image_dict)
-    if var_config_values['type'] == 'flavor':
-        return __get_flavor_variable_value(var_config_values, flavor_dict)
-    return None
-
-
-def __get_string_variable_value(var_config_values):
-    """
-    Returns the associated string value
-    :param var_config_values: the configuration dictionary
-    :return: the value contained in the dictionary with the key 'value'
-    """
-    return var_config_values['value']
-
-
-def __get_vm_attr_variable_value(var_config_values, vm_dict):
-    """
-    Returns the associated value contained on a VM instance
-    :param var_config_values: the configuration dictionary
-    :param vm_dict: the dictionary containing all VMs where the key is the VM's
-                    name
-    :return: the value
-    """
-    vm = vm_dict.get(var_config_values['vm_name'])
-    if vm:
-        if var_config_values['value'] == 'floating_ip':
-            return vm.get_floating_ip().ip
-        if var_config_values['value'] == 'image_user':
-            return vm.get_image_user()
-
-
-def __get_os_creds_variable_value(var_config_values, os_creds):
-    """
-    Returns the associated OS credentials value
-    :param var_config_values: the configuration dictionary
-    :param os_creds: the credentials
-    :return: the value
-    """
-    logger.info("Retrieving OS Credentials")
-    if os_creds:
-        if var_config_values['value'] == 'username':
-            logger.info("Returning OS username")
-            return os_creds.username
-        elif var_config_values['value'] == 'password':
-            logger.info("Returning OS password")
-            return os_creds.password
-        elif var_config_values['value'] == 'auth_url':
-            logger.info("Returning OS auth_url")
-            return os_creds.auth_url
-        elif var_config_values['value'] == 'project_name':
-            logger.info("Returning OS project_name")
-            return os_creds.project_name
-
-    logger.info("Returning none")
-    return None
-
-
-def __get_vm_port_variable_value(var_config_values, vm_dict):
-    """
-    Returns the associated OS credentials value
-    :param var_config_values: the configuration dictionary
-    :param vm_dict: the dictionary containing all VMs where the key is the VM's
-                    name
-    :return: the value
-    """
-    port_name = var_config_values.get('port_name')
-    vm_name = var_config_values.get('vm_name')
-
-    if port_name and vm_name:
-        vm = vm_dict.get(vm_name)
-        if vm:
-            port_value_id = var_config_values.get('port_value')
-            if port_value_id:
-                if port_value_id == 'mac_address':
-                    return vm.get_port_mac(port_name)
-                if port_value_id == 'ip_address':
-                    return vm.get_port_ip(port_name)
-
-
-def __get_vm_fip_variable_value(var_config_values, vm_dict):
-    """
-    Returns the floating IP value if found
-    :param var_config_values: the configuration dictionary
-    :param vm_dict: the dictionary containing all VMs where the key is the VM's
-                    name
-    :return: the floating IP string value or None
-    """
-    fip_name = var_config_values.get('fip_name')
-    vm_name = var_config_values.get('vm_name')
-
-    if vm_name:
-        vm = vm_dict.get(vm_name)
-        if vm:
-            fip = vm.get_floating_ip(fip_name)
-            if fip:
-                return fip.ip
-
-
-def __get_image_variable_value(var_config_values, image_dict):
-    """
-    Returns the associated image value
-    :param var_config_values: the configuration dictionary
-    :param image_dict: the dictionary containing all images where the key is
-                       the name
-    :return: the value
-    """
-    logger.info("Retrieving image values")
-
-    if image_dict:
-        if var_config_values.get('image_name'):
-            image_creator = image_dict.get(var_config_values['image_name'])
-            if image_creator:
-                if var_config_values.get('value') and \
-                                var_config_values['value'] == 'id':
-                    return image_creator.get_image().id
-                if var_config_values.get('value') and \
-                        var_config_values['value'] == 'user':
-                    return image_creator.image_settings.image_user
-
-    logger.info("Returning none")
-    return None
-
-
-def __get_flavor_variable_value(var_config_values, flavor_dict):
-    """
-    Returns the associated flavor value
-    :param var_config_values: the configuration dictionary
-    :param flavor_dict: the dictionary containing all flavor creators where the
-                        key is the name
-    :return: the value or None
-    """
-    logger.info("Retrieving flavor values")
-
-    if flavor_dict:
-        if var_config_values.get('flavor_name'):
-            flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
-            if flavor_creator:
-                if var_config_values.get('value') and \
-                                var_config_values['value'] == 'id':
-                    return flavor_creator.get_flavor().id
 
 
 def main(arguments):
@@ -591,108 +66,11 @@ def main(arguments):
     config = yaml.load(output)
 
     if config:
-        os_config = config.get('openstack')
-
-        creators = list()
-        vm_dict = dict()
-        images_dict = dict()
-        flavors_dict = dict()
-        os_creds_dict = dict()
         clean = arguments.clean is not ARG_NOT_SET
-
-        if os_config:
-            os_creds_dict = __get_creds_dict(os_config)
-
-            try:
-                # Create projects
-                projects_dict = __create_instances(
-                    os_creds_dict, OpenStackProject, ProjectSettings,
-                    os_config.get('projects'), 'project', clean)
-                creators.append(projects_dict)
-
-                # Create users
-                users_dict = __create_instances(
-                    os_creds_dict, OpenStackUser, UserSettings,
-                    os_config.get('users'), 'user', clean)
-                creators.append(users_dict)
-
-                # Associate new users to projects
-                if not clean:
-                    for project_creator in projects_dict.values():
-                        users = project_creator.project_settings.users
-                        for user_name in users:
-                            user_creator = users_dict.get(user_name)
-                            if user_creator:
-                                project_creator.assoc_user(
-                                    user_creator.get_user())
-
-                # Create flavors
-                flavors_dict = __create_instances(
-                    os_creds_dict, OpenStackFlavor, FlavorSettings,
-                    os_config.get('flavors'), 'flavor', clean, users_dict)
-                creators.append(flavors_dict)
-
-                # Create images
-                images_dict = __create_instances(
-                    os_creds_dict, OpenStackImage, ImageSettings,
-                    os_config.get('images'), 'image', clean, users_dict)
-                creators.append(images_dict)
-
-                # Create networks
-                creators.append(__create_instances(
-                    os_creds_dict, OpenStackNetwork, NetworkSettings,
-                    os_config.get('networks'), 'network', clean, users_dict))
-
-                # Create routers
-                creators.append(__create_instances(
-                    os_creds_dict, OpenStackRouter, RouterSettings,
-                    os_config.get('routers'), 'router', clean, users_dict))
-
-                # Create keypairs
-                keypairs_dict = __create_instances(
-                    os_creds_dict, OpenStackKeypair, KeypairSettings,
-                    os_config.get('keypairs'), 'keypair', clean, users_dict)
-                creators.append(keypairs_dict)
-
-                # Create security groups
-                creators.append(__create_instances(
-                    os_creds_dict, OpenStackSecurityGroup,
-                    SecurityGroupSettings,
-                    os_config.get('security_groups'), 'security_group', clean,
-                    users_dict))
-
-                # Create instance
-                vm_dict = __create_vm_instances(
-                    os_creds_dict, users_dict, os_config.get('instances'),
-                    images_dict, keypairs_dict,
-                    arguments.clean is not ARG_NOT_SET)
-                creators.append(vm_dict)
-                logger.info(
-                    'Completed creating/retrieving all configured instances')
-            except Exception as e:
-                logger.error(
-                    'Unexpected error deploying environment. Rolling back due'
-                    ' to - ' + str(e))
-                raise
-
-        # Must enter either block
-        if arguments.clean is not ARG_NOT_SET:
-            # Cleanup Environment
-            __cleanup(creators, arguments.clean_image is not ARG_NOT_SET)
-        elif arguments.deploy is not ARG_NOT_SET:
-            logger.info('Configuring NICs where required')
-            for vm in vm_dict.values():
-                vm.config_nics()
-            logger.info('Completed NIC configuration')
-
-            # Provision VMs
-            ansible_config = config.get('ansible')
-            if ansible_config and vm_dict:
-                if not __apply_ansible_playbooks(ansible_config,
-                                                 os_creds_dict, vm_dict,
-                                                 images_dict, flavors_dict,
-                                                 arguments.tmplt_file):
-                    logger.error("Problem applying ansible playbooks")
+        clean_image = arguments.clean_image is not ARG_NOT_SET
+        deploy = arguments.deploy is not ARG_NOT_SET
+        launch_utils.launch_config(
+            config, arguments.tmplt_file, deploy, clean, clean_image)
     else:
         logger.error(
             'Unable to read configuration file - ' + arguments.tmplt_file)
@@ -701,17 +79,6 @@ def main(arguments):
     exit(0)
 
 
-def __cleanup(creators, clean_image=False):
-    for creator_dict in reversed(creators):
-        for key, creator in creator_dict.items():
-            if ((isinstance(creator, OpenStackImage) and clean_image)
-                    or not isinstance(creator, OpenStackImage)):
-                try:
-                    creator.clean()
-                except Exception as e:
-                    logger.warning('Error cleaning component - %s', e)
-
-
 if __name__ == '__main__':
     # To ensure any files referenced via a relative path will begin from the
     # directory in which this file resides
index b0b60c0..137159f 100644 (file)
@@ -1,10 +1,14 @@
-python-novaclient!=7.0.0,>=6.0.0 # Apache-2.0
-python-neutronclient>=5.1.0 # Apache-2.0
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+python-novaclient>=9.0.0 # Apache-2.0
+python-neutronclient>=6.3.0 # Apache-2.0
 python-keystoneclient>=3.8.0 # Apache-2.0
-python-glanceclient>=2.5.0 # Apache-2.0
+python-glanceclient>=2.8.0 # Apache-2.0
 python-heatclient>=1.6.1 # Apache-2.0
-python-cinderclient
-ansible>=2.1.0,<2.4
+python-cinderclient>=3.1.0 # Apache-2.0
+python-magnumclient>=2.0.0 # Apache-2.0
+ansible<2.4,>=2.1.0
 wrapt>=1.7.0 # BSD License
 scp
-cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0
+cryptography!=2.0,>=1.6 # BSD/Apache-2.0
index f0cf5e8..566d844 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -1,17 +1,29 @@
-#!/usr/bin/env python
-
-# Copyright (c) 2016 Cable Television Laboratories and others.
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
 #
-# All rights reserved. This program and the accompanying materials
-# are made available under the terms of the Apache License, Version 2.0
-# which accompanies this distribution, and is available at
-# http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 
-from setuptools import setup
+# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
+import setuptools
 
-__author__ = 'spisarski'
+# In python < 2.7.4, a lazy loading of package `pbr` will break
+# setuptools if some other modules registered functions in `atexit`.
+# solution from: http://bugs.python.org/issue15881#msg170215
+try:
+    import multiprocessing  # noqa
+except ImportError:
+    pass
 
-setup(
-    setup_requires=['pbr>=1.9', 'setuptools>=17.1'],
-    pbr=True,
-)
+setuptools.setup(
+    setup_requires=['pbr>=2.0.0'],
+    pbr=True)
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
 #                    and others.  All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
----
-- name: Configure NIC
-  hosts: all
-  become: yes
-  become_method: sudo
-  become_user: root
-
-  tasks:
-   - name: Setup /etc/network/interfaces.d/{{nic_name}}.cfg file
-     action: template owner=root group=root mode=644 src=../templates/ethN.cfg dest=/etc/network/interfaces.d/{{nic_name}}.cfg
-   - name : Restart Network
-     command: service networking restart
\ No newline at end of file
+__author__ = 'spisarski'
diff --git a/snaps/config/cluster_template.py b/snaps/config/cluster_template.py
new file mode 100644 (file)
index 0000000..0acf25a
--- /dev/null
@@ -0,0 +1,308 @@
+# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import enum
+from neutronclient.common.utils import str2bool
+
+
+class ServerType(enum.Enum):
+    """
+    The cluter server types supported
+    """
+    vm = 'vm'
+    baremetal = 'baremetal'
+
+
+class ContainerOrchestrationEngine(enum.Enum):
+    """
+    The types of supported COEs
+    """
+    kubernetes = 'kubernetes'
+    swarm = 'swarm'
+    mesos = 'mesos'
+
+
+class DockerStorageDriver(enum.Enum):
+    """
+    Drivers for managing storage for the images in the container's writable
+    layer
+    """
+    devicemapper = 'devicemapper'
+    overlay = 'overlay'
+
+
+class ClusterTemplateConfig(object):
+    """
+    Configuration settings for OpenStack cluster template creation
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the cluster template's name (required)
+        :param image: name or ID of the base image in Glance used to boot the
+                      cluster's servers. The image must have the attribute
+                      'os-distro' defined as appropriate for the cluster
+                      driver (required)
+        :param keypair: name or ID of the keypair to gain cluster machine
+                        access (required)
+        :param network_driver: The name of a network driver for providing the
+                               networks for the containers. Note that this is
+                               different and separate from the Neutron network
+                               for the bay/cluster. The operation and
+                               networking model are specific to the particular
+                               driver (optional)
+        :param external_net: name or IDof the external Neutron network to
+                             provide connectivity to the cluster (required)
+        :param floating_ip_enabled: Whether enable or not using the floating IP
+                                    of cloud provider. Some cloud providers
+                                    used floating IP, some used public IP,
+                                    thus Magnum provide this option for
+                                    specifying the choice of using floating IP
+                                    (default - True)
+        :param docker_volume_size: The size in GB for the local storage on each
+                                   server for the Docker daemon to cache the
+                                   images and host the containers. Cinder
+                                   volumes provide the storage. The default is
+                                   25 GB. For the devicemapper storage driver,
+                                   the minimum value is 3GB. For the overlay
+                                   storage driver, the minimum value is 1GB.
+                                   (default - 3)
+        :param server_type: ServerType enumeration (default - vm)
+        :param flavor: name or ID of the nova flavor for booting the node
+                       servers (default - m1.small)
+        :param master_flavor: name or ID of the nova flavor of the master node
+                              for this cluster (optional)
+        :param coe: ContainerOrchestrationEngine enum instance
+                    (default - kubernetes)
+        :param fixed_net: name of a Neutron network to provide connectivity
+                          to the internal network for the cluster
+                          (optional)
+        :param fixed_subnet: Fixed subnet that are using to allocate network
+                             address for nodes in bay/cluster (optional)
+        :param registry_enabled: Docker images by default are pulled from the
+                                 public Docker registry, but in some cases,
+                                 users may want to use a private registry.
+                                 This option provides an alternative registry
+                                 based on the Registry V2: Magnum will create a
+                                 local registry in the bay/cluster backed by
+                                 swift to host the images (default - True)
+        :param insecure_registry: The URL pointing to the user's own private
+                                  insecure docker registry to deploy and run
+                                  docker containers (optional)
+        :param docker_storage_driver: DockerStorageDriver enum instance to
+                                      manage storage for the images and
+                                      container's writable layer
+                                      (default - devicemapper)
+        :param dns_nameserver: The DNS nameserver for the servers and
+                               containers in the bay/cluster to use.
+                               This is configured in the private Neutron
+                               network for the bay/cluster.
+                               (default provided by Magnum - 8.8.8.8)
+        :param public: denotes whether or not the cluster template is public
+                       (default False)
+        :param tls_disabled: denotes whether or not TLS should be enabled
+                             (default False)
+        :param http_proxy: host:port for a proxy to use when direct HTTP
+                           access from the servers to sites on the external
+                           internet is blocked (optional)
+        :param https_proxy: host:port for a proxy to use when direct HTTPS
+                            access from the servers to sites on the external
+                            internet is blocked (optional)
+        :param no_proxy: comma separated list of IPs that should not be
+                         redirected through the proxy (optional)
+        :param volume_driver: The name of a volume driver for managing the
+                              persistent storage for the containers. The
+                              functionality supported are specific to the
+                              driver (optional)
+        :param master_lb_enabled: Since multiple masters may exist in a
+                                  bay/cluster, a Neutron load balancer is
+                                  created to provide the API endpoint for the
+                                  bay/cluster and to direct requests to the
+                                  masters. In some cases, such as when the
+                                  LBaaS service is not available, this option
+                                  can be set to false to create a bay/cluster
+                                  without the load balancer. In this case, one
+                                  of the masters will serve as the API endpoint
+                                  (default - True)
+        :param labels: Arbitrary labels in the form of a dict. The accepted
+                       keys and valid values are defined in the bay/cluster
+                       drivers. They are used as a way to pass additional
+                       parameters that are specific to a bay/cluster driver.
+                       (optional)
+        """
+        self.name = kwargs.get('name')
+        self.image = kwargs.get('image')
+        self.keypair = kwargs.get('keypair')
+        self.network_driver = kwargs.get('network_driver')
+        self.external_net = kwargs.get('external_net')
+        self.floating_ip_enabled = str2bool(
+            str(kwargs.get('floating_ip_enabled', True)))
+        self.docker_volume_size = int(kwargs.get('docker_volume_size', 3))
+        self.server_type = map_server_type(
+            kwargs.get('server_type', ServerType.vm))
+        self.flavor = kwargs.get('flavor')
+        self.master_flavor = kwargs.get('master_flavor')
+        self.coe = map_coe(
+            kwargs.get('coe', ContainerOrchestrationEngine.kubernetes))
+        self.fixed_net = kwargs.get('fixed_net')
+        self.fixed_subnet = kwargs.get('fixed_subnet')
+        self.registry_enabled = str2bool(
+            str(kwargs.get('registry_enabled', True)))
+        self.insecure_registry = kwargs.get('insecure_registry')
+        self.docker_storage_driver = map_docker_storage_driver(
+            kwargs.get('docker_storage_driver',
+                       DockerStorageDriver.devicemapper))
+        self.dns_nameserver = kwargs.get('dns_nameserver')
+        self.public = str2bool(str(kwargs.get('public', False)))
+        self.tls_disabled = str2bool(str(kwargs.get('tls_disabled', False)))
+        self.http_proxy = kwargs.get('http_proxy')
+        self.https_proxy = kwargs.get('https_proxy')
+        self.no_proxy = kwargs.get('no_proxy')
+        self.volume_driver = kwargs.get('volume_driver')
+        self.master_lb_enabled = str2bool(
+            str(kwargs.get('master_lb_enabled', True)))
+        self.labels = kwargs.get('labels')
+
+        if (not self.name or not self.image or not self.keypair
+                or not self.external_net):
+            raise ClusterTemplateConfigError(
+                'The attributes name, image, keypair, and '
+                'external_net are required for ClusterTemplateConfig')
+
+    def magnum_dict(self):
+        """
+        Returns a dictionary object representing this object.
+        This is meant to be sent into as kwargs into the Magnum client
+
+        :return: the dictionary object
+        """
+        out = dict()
+
+        if self.name:
+            out['name'] = self.name
+        if self.image:
+            out['image_id'] = self.image
+        if self.keypair:
+            out['keypair_id'] = self.keypair
+        if self.network_driver:
+            out['network_driver'] = self.network_driver
+        if self.external_net:
+            out['external_network_id'] = self.external_net
+        if self.floating_ip_enabled:
+            out['floating_ip_enabled'] = self.floating_ip_enabled
+        if self.docker_volume_size:
+            out['docker_volume_size'] = self.docker_volume_size
+        if self.server_type:
+            out['server_type'] = self.server_type.value
+        if self.flavor:
+            out['flavor_id'] = self.flavor
+        if self.master_flavor:
+            out['master_flavor_id'] = self.master_flavor
+        if self.coe:
+            out['coe'] = self.coe.value
+        if self.fixed_net:
+            out['fixed_network'] = self.fixed_net
+        if self.fixed_subnet:
+            out['fixed_subnet'] = self.fixed_subnet
+        if self.registry_enabled:
+            out['registry_enabled'] = self.registry_enabled
+        if self.insecure_registry:
+            out['insecure_registry'] = self.insecure_registry
+        if self.docker_storage_driver:
+            out['docker_storage_driver'] = self.docker_storage_driver.value
+        if self.dns_nameserver:
+            out['dns_nameserver'] = self.dns_nameserver
+        if self.public:
+            out['public'] = self.public
+        if self.tls_disabled:
+            out['tls_disabled'] = self.tls_disabled
+        if self.http_proxy:
+            out['http_proxy'] = self.http_proxy
+        if self.https_proxy:
+            out['https_proxy'] = self.https_proxy
+        if self.no_proxy:
+            out['no_proxy'] = self.no_proxy
+        if self.volume_driver:
+            out['volume_driver'] = self.volume_driver
+        if self.master_lb_enabled:
+            out['master_lb_enabled'] = self.master_lb_enabled
+        if self.labels:
+            out['labels'] = self.labels
+        return out
+
+
+class ClusterTemplateConfigError(Exception):
+    """
+    Exception to be thrown when a cluster template configuration is incorrect
+    """
+
+
+def map_server_type(server_type):
+    """
+    Takes a the server_type value maps it to the ServerType enum. When None
+    return None
+    :param server_type: the server_type value to map
+    :return: the ServerType enum object
+    :raise: ClusterTemplateConfigError if value is invalid
+    """
+    if not server_type:
+        return None
+    if isinstance(server_type, ServerType):
+        return server_type
+    elif isinstance(server_type, str):
+        for this_type in ServerType:
+            if this_type.value == server_type:
+                return this_type
+        raise ClusterTemplateConfigError(
+            'Invalid server type - ' + server_type)
+
+
+def map_coe(coe):
+    """
+    Takes a the coe value maps it to the ContainerOrchestrationEngine enum.
+    When None return None
+    :param coe: the COE value to map
+    :return: the ContainerOrchestrationEngine enum object
+    :raise: ClusterTemplateConfigError if value is invalid
+    """
+    if not coe:
+        return None
+    if isinstance(coe, ContainerOrchestrationEngine):
+        return coe
+    elif isinstance(coe, str):
+        for this_type in ContainerOrchestrationEngine:
+            if this_type.value == coe:
+                return this_type
+        raise ClusterTemplateConfigError('Invalid COE - ' + coe)
+
+
+def map_docker_storage_driver(driver):
+    """
+    Takes a the coe value maps it to the ContainerOrchestrationEngine enum.
+    When None return None
+    :param driver: the docker storage driver value to map
+    :return: the DockerStorageDriver enum object
+    :raise: ClusterTemplateConfigError if value is invalid
+    """
+    if not driver:
+        return None
+    if isinstance(driver, DockerStorageDriver):
+        return driver
+    elif isinstance(driver, str):
+        for this_type in DockerStorageDriver:
+            if this_type.value == driver:
+                return this_type
+        raise ClusterTemplateConfigError(
+            'Invalid DockerStorageDriver - ' + driver)
diff --git a/snaps/config/flavor.py b/snaps/config/flavor.py
new file mode 100644 (file)
index 0000000..b29fb7c
--- /dev/null
@@ -0,0 +1,107 @@
+# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class FlavorConfig(object):
+    """
+    Configuration settings for OpenStack flavor creation
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the flavor's name (required)
+        :param flavor_id: the string ID (default 'auto')
+        :param ram: the required RAM in MB (required)
+        :param disk: the size of the root disk in GB (required)
+        :param vcpus: the number of virtual CPUs (required)
+        :param ephemeral: the size of the ephemeral disk in GB (default 0)
+        :param swap: the size of the dedicated swap disk in GB (default 0)
+        :param rxtx_factor: the receive/transmit factor to be set on ports if
+                            backend supports QoS extension (default 1.0)
+        :param is_public: denotes whether or not the flavor is public
+                          (default True)
+        :param metadata: freeform dict() for special metadata
+        """
+        self.name = kwargs.get('name')
+
+        if kwargs.get('flavor_id'):
+            self.flavor_id = kwargs['flavor_id']
+        else:
+            self.flavor_id = 'auto'
+
+        self.ram = kwargs.get('ram')
+        self.disk = kwargs.get('disk')
+        self.vcpus = kwargs.get('vcpus')
+
+        if kwargs.get('ephemeral'):
+            self.ephemeral = kwargs['ephemeral']
+        else:
+            self.ephemeral = 0
+
+        if kwargs.get('swap'):
+            self.swap = kwargs['swap']
+        else:
+            self.swap = 0
+
+        if kwargs.get('rxtx_factor'):
+            self.rxtx_factor = kwargs['rxtx_factor']
+        else:
+            self.rxtx_factor = 1.0
+
+        if kwargs.get('is_public') is not None:
+            self.is_public = kwargs['is_public']
+        else:
+            self.is_public = True
+
+        if kwargs.get('metadata'):
+            self.metadata = kwargs['metadata']
+        else:
+            self.metadata = None
+
+        if not self.name or not self.ram or not self.disk or not self.vcpus:
+            raise FlavorConfigError(
+                'The attributes name, ram, disk, and vcpus are required for'
+                'FlavorConfig')
+
+        if not isinstance(self.ram, int):
+            raise FlavorConfigError('The ram attribute must be a integer')
+
+        if not isinstance(self.disk, int):
+            raise FlavorConfigError('The ram attribute must be a integer')
+
+        if not isinstance(self.vcpus, int):
+            raise FlavorConfigError('The vcpus attribute must be a integer')
+
+        if self.ephemeral and not isinstance(self.ephemeral, int):
+            raise FlavorConfigError(
+                'The ephemeral attribute must be an integer')
+
+        if self.swap and not isinstance(self.swap, int):
+            raise FlavorConfigError('The swap attribute must be an integer')
+
+        if self.rxtx_factor and not isinstance(self.rxtx_factor, (int, float)):
+            raise FlavorConfigError(
+                'The is_public attribute must be an integer or float')
+
+        if self.is_public and not isinstance(self.is_public, bool):
+            raise FlavorConfigError(
+                'The is_public attribute must be a boolean')
+
+
+class FlavorConfigError(Exception):
+    """
+    Exception to be thrown when a flavor configuration is incorrect
+    """
diff --git a/snaps/config/image.py b/snaps/config/image.py
new file mode 100644 (file)
index 0000000..fe1c913
--- /dev/null
@@ -0,0 +1,110 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class ImageConfig(object):
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the image's name (required)
+        :param image_user: the image's default sudo user (required)
+        :param format or img_format: the image format type (required)
+        :param url or download_url: the image download location (requires url
+                                    or img_file)
+        :param image_file: the image file location (requires url or img_file)
+        :param extra_properties: dict() object containing extra parameters to
+                                 pass when loading the image;
+                                 can be ids of kernel and initramfs images for
+                                 a 3-part image
+        :param nic_config_pb_loc: the file location to the Ansible Playbook
+                                  that can configure multiple NICs
+        :param kernel_image_settings: the settings for a kernel image
+        :param ramdisk_image_settings: the settings for a ramdisk image
+        :param exists: When True, an image with the given name must exist
+        :param public: When True, an image will be created with public
+                       visibility
+        """
+
+        self.name = kwargs.get('name')
+        self.image_user = kwargs.get('image_user')
+        self.format = kwargs.get('format')
+        if not self.format:
+            self.format = kwargs.get('img_format')
+
+        self.url = kwargs.get('url')
+        if not self.url:
+            self.url = kwargs.get('download_url')
+        if self.url == 'None':
+            self.url = None
+
+        self.image_file = kwargs.get('image_file')
+        if self.image_file == 'None':
+            self.image_file = None
+
+        self.extra_properties = kwargs.get('extra_properties')
+        self.nic_config_pb_loc = kwargs.get('nic_config_pb_loc')
+
+        kernel_image_settings = kwargs.get('kernel_image_settings')
+        if kernel_image_settings:
+            if isinstance(kernel_image_settings, dict):
+                self.kernel_image_settings = ImageConfig(
+                    **kernel_image_settings)
+            else:
+                self.kernel_image_settings = kernel_image_settings
+        else:
+            self.kernel_image_settings = None
+
+        ramdisk_image_settings = kwargs.get('ramdisk_image_settings')
+        if ramdisk_image_settings:
+            if isinstance(ramdisk_image_settings, dict):
+                self.ramdisk_image_settings = ImageConfig(
+                    **ramdisk_image_settings)
+            else:
+                self.ramdisk_image_settings = ramdisk_image_settings
+        else:
+            self.ramdisk_image_settings = None
+
+        if 'exists' in kwargs and kwargs['exists'] is True:
+            self.exists = True
+        else:
+            self.exists = False
+
+        if 'public' in kwargs and kwargs['public'] is True:
+            self.public = True
+        else:
+            self.public = False
+
+        if not self.name:
+            raise ImageConfigError("The attribute name is required")
+
+        if not (self.url or self.image_file) and not self.exists:
+            raise ImageConfigError(
+                'URL or image file must be set or image must already exist')
+
+        if not self.image_user:
+            raise ImageConfigError('Image user is required')
+
+        if not self.format and not self.exists:
+            raise ImageConfigError(
+                'Format is required when the image should not already exist')
+
+
+class ImageConfigError(Exception):
+    """
+    Exception to be thrown when an image settings are incorrect
+    """
+
+    def __init__(self, message):
+        Exception.__init__(self, message)
diff --git a/snaps/config/keypair.py b/snaps/config/keypair.py
new file mode 100644 (file)
index 0000000..2304c6e
--- /dev/null
@@ -0,0 +1,61 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from neutronclient.common.utils import str2bool
+
+
+class KeypairConfig(object):
+    """
+    Class representing a keypair configuration
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor - all parameters are optional
+        :param name: The keypair name.
+        :param public_filepath: The path to/from the filesystem where the
+                                public key file is or will be stored
+        :param private_filepath: The path where the generated private key file
+                                 will be stored
+        :param key_size: The number of bytes for the key size when it needs to
+                         be generated (Must be >=512 default 1024)
+        :param delete_on_clean: when True, the key files will be deleted when
+                                OpenStackKeypair#clean() is called
+        :return:
+        """
+
+        self.name = kwargs.get('name')
+        self.public_filepath = kwargs.get('public_filepath')
+        self.private_filepath = kwargs.get('private_filepath')
+        self.key_size = int(kwargs.get('key_size', 1024))
+
+        if kwargs.get('delete_on_clean') is not None:
+            if isinstance(kwargs.get('delete_on_clean'), bool):
+                self.delete_on_clean = kwargs.get('delete_on_clean')
+            else:
+                self.delete_on_clean = str2bool(kwargs.get('delete_on_clean'))
+        else:
+            self.delete_on_clean = None
+
+        if not self.name:
+            raise KeypairConfigError('Name is a required attribute')
+
+        if self.key_size < 512:
+            raise KeypairConfigError('key_size must be >=512')
+
+
+class KeypairConfigError(Exception):
+    """
+    Exception to be thrown when keypair settings are incorrect
+    """
diff --git a/snaps/config/network.py b/snaps/config/network.py
new file mode 100644 (file)
index 0000000..39a4254
--- /dev/null
@@ -0,0 +1,518 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import enum
+from neutronclient.common.utils import str2bool
+
+from snaps.openstack.utils import keystone_utils, neutron_utils
+
+
+class NetworkConfig(object):
+    """
+    Class representing a network configuration
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor - all parameters are optional
+        :param name: The network name.
+        :param admin_state_up: The administrative status of the network.
+                               True = up / False = down (default True)
+        :param shared: Boolean value indicating whether this network is shared
+                       across all projects/tenants. By default, only
+                       administrative users can change this value.
+        :param project_name: Admin-only. The name of the project that will own
+                             the network. This project can be different from
+                             the project that makes the create network request.
+                             However, only administrative users can specify a
+                             project ID other than their own. You cannot change
+                             this value through authorization policies.
+        :param external: when true, will setup an external network
+                         (default False).
+        :param network_type: the type of network (i.e. vlan|flat).
+        :param physical_network: the name of the physical network
+                                 (required when network_type is 'flat')
+        :param segmentation_id: the id of the segmentation
+                                 (this is required when network_type is 'vlan')
+        :param subnets or subnet_settings: List of SubnetConfig objects.
+        :return:
+        """
+
+        self.project_id = None
+
+        self.name = kwargs.get('name')
+        if kwargs.get('admin_state_up') is not None:
+            self.admin_state_up = str2bool(str(kwargs['admin_state_up']))
+        else:
+            self.admin_state_up = True
+
+        if kwargs.get('shared') is not None:
+            self.shared = str2bool(str(kwargs['shared']))
+        else:
+            self.shared = None
+
+        self.project_name = kwargs.get('project_name')
+
+        if kwargs.get('external') is not None:
+            self.external = str2bool(str(kwargs.get('external')))
+        else:
+            self.external = False
+
+        self.network_type = kwargs.get('network_type')
+        self.physical_network = kwargs.get('physical_network')
+        self.segmentation_id = kwargs.get('segmentation_id')
+
+        self.subnet_settings = list()
+        subnet_settings = kwargs.get('subnets')
+        if not subnet_settings:
+            subnet_settings = kwargs.get('subnet_settings', list())
+        if subnet_settings:
+            for subnet_config in subnet_settings:
+                if isinstance(subnet_config, SubnetConfig):
+                    self.subnet_settings.append(subnet_config)
+                else:
+                    self.subnet_settings.append(
+                        SubnetConfig(**subnet_config['subnet']))
+
+        if not self.name or len(self.name) < 1:
+            raise NetworkConfigError('Name required for networks')
+
+    def get_project_id(self, os_creds):
+        """
+        Returns the project ID for a given project_name or None
+        :param os_creds: the credentials required for keystone client retrieval
+        :return: the ID or None
+        """
+        if self.project_id:
+            return self.project_id
+        else:
+            if self.project_name:
+                keystone = keystone_utils.keystone_client(os_creds)
+                project = keystone_utils.get_project(
+                    keystone=keystone, project_name=self.project_name)
+                if project:
+                    return project.id
+
+        return None
+
+    def dict_for_neutron(self, os_creds):
+        """
+        Returns a dictionary object representing this object.
+        This is meant to be converted into JSON designed for use by the Neutron
+        API
+        TODO - expand automated testing to exercise all parameters
+
+        :param os_creds: the OpenStack credentials
+        :return: the dictionary object
+        """
+        out = dict()
+
+        if self.name:
+            out['name'] = self.name
+        if self.admin_state_up is not None:
+            out['admin_state_up'] = self.admin_state_up
+        if self.shared:
+            out['shared'] = self.shared
+        if self.project_name:
+            project_id = self.get_project_id(os_creds)
+            if project_id:
+                out['tenant_id'] = project_id
+            else:
+                raise NetworkConfigError(
+                    'Could not find project ID for project named - ' +
+                    self.project_name)
+        if self.network_type:
+            out['provider:network_type'] = self.network_type
+        if self.physical_network:
+            out['provider:physical_network'] = self.physical_network
+        if self.segmentation_id:
+            out['provider:segmentation_id'] = self.segmentation_id
+        if self.external:
+            out['router:external'] = self.external
+        return {'network': out}
+
+
+class NetworkConfigError(Exception):
+    """
+    Exception to be thrown when networks settings attributes are incorrect
+    """
+
+
+class IPv6Mode(enum.Enum):
+    """
+    A rule's direction
+    """
+    slaac = 'slaac'
+    stateful = 'dhcpv6-stateful'
+    stateless = 'dhcpv6-stateless'
+
+
+class SubnetConfig(object):
+    """
+    Class representing a subnet configuration
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor - all parameters are optional except cidr (subnet mask)
+        :param name: The subnet name (required)
+        :param cidr: The CIDR (required)
+        :param ip_version: The IP version, which is 4 or 6 (required)
+        :param project_name: The name of the project who owns the network.
+                             Only administrative users can specify a project ID
+                             other than their own. You cannot change this value
+                             through authorization policies (optional)
+        :param start: The start address for the allocation pools (optional)
+        :param end: The end address for the allocation pools (optional)
+        :param gateway_ip: The gateway IP address (optional)
+        :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
+                            disabled (optional)
+        :param dns_nameservers: A list of DNS name servers for the subnet.
+                                Specify each name server as an IP address
+                                and separate multiple entries with a space.
+                                For example [8.8.8.7 8.8.8.8]
+                                (default '8.8.8.8')
+        :param host_routes: A list of host route dictionaries for the subnet.
+                            For example:
+                                "host_routes":[
+                                    {
+                                        "destination":"0.0.0.0/0",
+                                        "nexthop":"123.456.78.9"
+                                    },
+                                    {
+                                        "destination":"192.168.0.0/24",
+                                        "nexthop":"192.168.0.1"
+                                    }
+                                ]
+        :param destination: The destination for static route (optional)
+        :param nexthop: The next hop for the destination (optional)
+        :param ipv6_ra_mode: an instance of the IPv6Mode enum
+                             (optional when enable_dhcp is True)
+        :param ipv6_address_mode: an instance of the IPv6Mode enum
+                                  (optional when enable_dhcp is True)
+        :raise: SubnetConfigError when config does not have or cidr values
+                are None
+        """
+        self.cidr = kwargs.get('cidr')
+        if kwargs.get('ip_version'):
+            self.ip_version = kwargs['ip_version']
+        else:
+            self.ip_version = 4
+
+        # Optional attributes that can be set after instantiation
+        self.name = kwargs.get('name')
+        self.project_name = kwargs.get('project_name')
+        self.start = kwargs.get('start')
+        self.end = kwargs.get('end')
+        self.gateway_ip = kwargs.get('gateway_ip')
+        self.enable_dhcp = kwargs.get('enable_dhcp')
+
+        if 'dns_nameservers' in kwargs:
+            self.dns_nameservers = kwargs.get('dns_nameservers')
+        else:
+            if self.ip_version == 4:
+                self.dns_nameservers = ['8.8.8.8']
+            else:
+                self.dns_nameservers = list()
+
+        self.host_routes = kwargs.get('host_routes')
+        self.destination = kwargs.get('destination')
+        self.nexthop = kwargs.get('nexthop')
+        self.ipv6_ra_mode = map_mode(kwargs.get('ipv6_ra_mode'))
+        self.ipv6_address_mode = map_mode(kwargs.get('ipv6_address_mode'))
+
+        if not self.name or not self.cidr:
+            raise SubnetConfigError('Name and cidr required for subnets')
+
+    def dict_for_neutron(self, os_creds, network=None):
+        """
+        Returns a dictionary object representing this object.
+        This is meant to be converted into JSON designed for use by the Neutron
+        API
+        :param os_creds: the OpenStack credentials
+        :param network: The network object on which the subnet will be created
+                        (optional)
+        :return: the dictionary object
+        """
+        out = {
+            'cidr': self.cidr,
+            'ip_version': self.ip_version,
+        }
+
+        if network:
+            out['network_id'] = network.id
+        if self.name:
+            out['name'] = self.name
+        if self.project_name:
+            keystone = keystone_utils.keystone_client(os_creds)
+            project = keystone_utils.get_project(
+                keystone=keystone, project_name=self.project_name)
+            project_id = None
+            if project:
+                project_id = project.id
+            if project_id:
+                out['tenant_id'] = project_id
+            else:
+                raise SubnetConfigError(
+                    'Could not find project ID for project named - ' +
+                    self.project_name)
+        if self.start and self.end:
+            out['allocation_pools'] = [{'start': self.start, 'end': self.end}]
+        if self.gateway_ip:
+            out['gateway_ip'] = self.gateway_ip
+        if self.enable_dhcp is not None:
+            out['enable_dhcp'] = self.enable_dhcp
+        if self.dns_nameservers and len(self.dns_nameservers) > 0:
+            out['dns_nameservers'] = self.dns_nameservers
+        if self.host_routes and len(self.host_routes) > 0:
+            out['host_routes'] = self.host_routes
+        if self.destination:
+            out['destination'] = self.destination
+        if self.nexthop:
+            out['nexthop'] = self.nexthop
+        if self.ipv6_ra_mode:
+            out['ipv6_ra_mode'] = self.ipv6_ra_mode.value
+        if self.ipv6_address_mode:
+            out['ipv6_address_mode'] = self.ipv6_address_mode.value
+        return out
+
+
+def map_mode(mode):
+    """
+    Takes a the direction value maps it to the Direction enum. When None return
+    None
+    :param mode: the mode value
+    :return: the IPv6Mode enum object
+    :raise: SubnetConfigError if value is invalid
+    """
+    if not mode:
+        return None
+    if isinstance(mode, IPv6Mode):
+        return mode
+    elif isinstance(mode, str):
+        mode_str = str(mode)
+        if mode_str == 'slaac':
+            return IPv6Mode.slaac
+        elif mode_str == 'dhcpv6-stateful':
+            return IPv6Mode.stateful
+        elif mode_str == 'stateful':
+            return IPv6Mode.stateful
+        elif mode_str == 'dhcpv6-stateless':
+            return IPv6Mode.stateless
+        elif mode_str == 'stateless':
+            return IPv6Mode.stateless
+        else:
+            raise SubnetConfigError('Invalid mode - ' + mode_str)
+    else:
+        return map_mode(mode.value)
+
+
+class SubnetConfigError(Exception):
+    """
+    Exception to be thrown when subnet settings attributes are incorrect
+    """
+
+
+class PortConfig(object):
+    """
+    Class representing a port configuration
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: A symbolic name for the port (optional).
+        :param network_name: The name of the network on which to create the
+                             port (required).
+        :param admin_state_up: A boolean value denoting the administrative
+                               status of the port (default = True)
+        :param project_name: The name of the project who owns the network.
+                             Only administrative users can specify a project ID
+                             other than their own. You cannot change this value
+                             through authorization policies (optional)
+        :param mac_address: The MAC address. If you specify an address that is
+                            not valid, a Bad Request (400) status code is
+                            returned. If you do not specify a MAC address,
+                            OpenStack Networking tries to allocate one. If a
+                            failure occurs, a Service Unavailable (503) status
+                            code is returned (optional)
+        :param ip_addrs: A list of dict objects where each contains two keys
+                         'subnet_name' and 'ip' values which will get mapped to
+                         self.fixed_ips. These values will be directly
+                         translated into the fixed_ips dict (optional)
+        :param security_groups: One or more security group IDs.
+        :param port_security_enabled: When True, security groups will be
+                                      applied to the port else not
+                                      (default - True)
+        :param allowed_address_pairs: A dictionary containing a set of zero or
+                                      more allowed address pairs. An address
+                                      pair contains an IP address and MAC
+                                      address (optional)
+        :param opt_value: The extra DHCP option value (optional)
+        :param opt_name: The extra DHCP option name (optional)
+        :param device_owner: The ID of the entity that uses this port.
+                             For example, a DHCP agent (optional)
+        :param device_id: The ID of the device that uses this port.
+                          For example, a virtual server (optional)
+        :param extra_dhcp_opts: k/v of options to use with your DHCP (optional)
+        :return:
+        """
+        if 'port' in kwargs:
+            kwargs = kwargs['port']
+
+        self.name = kwargs.get('name')
+        self.network_name = kwargs.get('network_name')
+
+        if kwargs.get('admin_state_up') is not None:
+            self.admin_state_up = str2bool(str(kwargs['admin_state_up']))
+        else:
+            self.admin_state_up = True
+
+        self.project_name = kwargs.get('project_name')
+        self.mac_address = kwargs.get('mac_address')
+        self.ip_addrs = kwargs.get('ip_addrs')
+        self.security_groups = kwargs.get('security_groups')
+
+        if kwargs.get('port_security_enabled') is not None:
+            self.port_security_enabled = str2bool(
+                str(kwargs['port_security_enabled']))
+        else:
+            self.port_security_enabled = None
+
+        self.allowed_address_pairs = kwargs.get('allowed_address_pairs')
+        self.opt_value = kwargs.get('opt_value')
+        self.opt_name = kwargs.get('opt_name')
+        self.device_owner = kwargs.get('device_owner')
+        self.device_id = kwargs.get('device_id')
+        self.extra_dhcp_opts = kwargs.get('extra_dhcp_opts')
+
+        if not self.network_name:
+            raise PortConfigError(
+                'The attribute network_name is required')
+
+    def __get_fixed_ips(self, neutron):
+        """
+        Sets the self.fixed_ips value
+        :param neutron: the Neutron client
+        :return: None
+        """
+
+        fixed_ips = list()
+        if self.ip_addrs:
+
+            for ip_addr_dict in self.ip_addrs:
+                subnet = neutron_utils.get_subnet(
+                    neutron, subnet_name=ip_addr_dict['subnet_name'])
+                if subnet and 'ip' in ip_addr_dict:
+                    fixed_ips.append({'ip_address': ip_addr_dict['ip'],
+                                      'subnet_id': subnet.id})
+                else:
+                    raise PortConfigError(
+                        'Invalid port configuration, subnet does not exist '
+                        'with name - ' + ip_addr_dict['subnet_name'])
+
+        return fixed_ips
+
+    def dict_for_neutron(self, neutron, os_creds):
+        """
+        Returns a dictionary object representing this object.
+        This is meant to be converted into JSON designed for use by the Neutron
+        API
+
+        TODO - expand automated testing to exercise all parameters
+        :param neutron: the Neutron client
+        :param os_creds: the OpenStack credentials
+        :return: the dictionary object
+        """
+
+        out = dict()
+
+        project_id = None
+        if self.project_name:
+            keystone = keystone_utils.keystone_client(os_creds)
+            project = keystone_utils.get_project(
+                keystone=keystone, project_name=self.project_name)
+            if project:
+                project_id = project.id
+
+        network = neutron_utils.get_network(
+            neutron, network_name=self.network_name, project_id=project_id)
+        if not network:
+            raise PortConfigError(
+                'Cannot locate network with name - ' + self.network_name)
+
+        out['network_id'] = network.id
+
+        if self.admin_state_up is not None:
+            out['admin_state_up'] = self.admin_state_up
+        if self.name:
+            out['name'] = self.name
+        if self.project_name:
+            if project_id:
+                out['tenant_id'] = project_id
+            else:
+                raise PortConfigError(
+                    'Could not find project ID for project named - ' +
+                    self.project_name)
+        if self.mac_address:
+            out['mac_address'] = self.mac_address
+
+        fixed_ips = self.__get_fixed_ips(neutron)
+        if fixed_ips and len(fixed_ips) > 0:
+            out['fixed_ips'] = fixed_ips
+
+        if self.security_groups:
+            sec_grp_ids = list()
+            for sec_grp_name in self.security_groups:
+                sec_grp = neutron_utils.get_security_group(
+                    neutron, sec_grp_name=sec_grp_name)
+                if sec_grp:
+                    sec_grp_ids.append(sec_grp.id)
+            out['security_groups'] = sec_grp_ids
+        if self.port_security_enabled is not None:
+            out['port_security_enabled'] = self.port_security_enabled
+        if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
+            out['allowed_address_pairs'] = self.allowed_address_pairs
+        if self.opt_value:
+            out['opt_value'] = self.opt_value
+        if self.opt_name:
+            out['opt_name'] = self.opt_name
+        if self.device_owner:
+            out['device_owner'] = self.device_owner
+        if self.device_id:
+            out['device_id'] = self.device_id
+        if self.extra_dhcp_opts:
+            out['extra_dhcp_opts'] = self.extra_dhcp_opts
+        return {'port': out}
+
+    def __eq__(self, other):
+        return (self.name == other.name and
+                self.network_name == other.network_name and
+                self.admin_state_up == other.admin_state_up and
+                self.project_name == other.project_name and
+                self.mac_address == other.mac_address and
+                self.ip_addrs == other.ip_addrs and
+                # self.fixed_ips == other.fixed_ips and
+                self.security_groups == other.security_groups and
+                self.allowed_address_pairs == other.allowed_address_pairs and
+                self.opt_value == other.opt_value and
+                self.opt_name == other.opt_name and
+                self.device_owner == other.device_owner and
+                self.device_id == other.device_id)
+
+
+class PortConfigError(Exception):
+    """
+    Exception to be thrown when port settings attributes are incorrect
+    """
diff --git a/snaps/config/project.py b/snaps/config/project.py
new file mode 100644 (file)
index 0000000..6790609
--- /dev/null
@@ -0,0 +1,57 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class ProjectConfig(object):
+    """
+    Class to hold the configuration settings required for creating OpenStack
+    project objects
+    """
+
+    def __init__(self, **kwargs):
+
+        """
+        Constructor
+        :param name: the project's name (required)
+        :param domain or domain_name: the project's domain name
+                                      (default = 'Default').
+                                      Field is used for v3 clients
+        :param description: the description (optional)
+        :param users: list of users to associate project to (optional)
+        :param enabled: denotes whether or not the project is enabled
+                        (default True)
+        """
+
+        self.name = kwargs.get('name')
+        self.domain_name = kwargs.get(
+            'domain', kwargs.get('domain', 'Default'))
+
+        self.description = kwargs.get('description')
+        if kwargs.get('enabled') is not None:
+            self.enabled = kwargs['enabled']
+        else:
+            self.enabled = True
+
+        self.users = kwargs.get('users', list())
+
+        if not self.name:
+            raise ProjectConfigError(
+                "The attribute name is required for ProjectConfig")
+
+
+class ProjectConfigError(Exception):
+    """
+    Exception to be thrown when project settings attributes are incorrect
+    """
diff --git a/snaps/config/qos.py b/snaps/config/qos.py
new file mode 100644 (file)
index 0000000..e507d2d
--- /dev/null
@@ -0,0 +1,92 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import enum
+
+
+class Consumer(enum.Enum):
+    """
+    QoS Specification consumer types
+    """
+    front_end = 'front-end'
+    back_end = 'back-end'
+    both = 'both'
+
+
+class QoSConfig(object):
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the qos's name (required)
+        :param consumer: the qos's consumer type of the enum type Consumer
+                         (required)
+        :param specs: dict of key/values
+        """
+
+        self.name = kwargs.get('name')
+
+        if kwargs.get('consumer'):
+            self.consumer = map_consumer(kwargs['consumer'])
+        else:
+            self.consumer = None
+
+        self.specs = kwargs.get('specs')
+        if not self.specs:
+            self.specs = dict()
+
+        if not self.name or not self.consumer:
+            raise QoSConfigError(
+                "The attributes name and consumer are required")
+
+
+def map_consumer(consumer):
+    """
+    Takes a the protocol value maps it to the Consumer enum. When None return
+    None
+    :param consumer: the value to map to the Enum
+    :return: the Protocol enum object
+    :raise: Exception if value is invalid
+    """
+    if not consumer:
+        return None
+    elif isinstance(consumer, Consumer):
+        return consumer
+    elif isinstance(consumer, str):
+        proto_str = str(consumer)
+        if proto_str == 'front-end':
+            return Consumer.front_end
+        elif proto_str == 'back-end':
+            return Consumer.back_end
+        elif proto_str == 'both':
+            return Consumer.both
+        else:
+            raise QoSConfigError('Invalid Consumer - ' + proto_str)
+    else:
+        if consumer.value == 'front-end':
+            return Consumer.front_end
+        elif consumer.value == 'back-end':
+            return Consumer.back_end
+        elif consumer.value == 'both':
+            return Consumer.both
+        else:
+            raise QoSConfigError('Invalid Consumer - ' + consumer.value)
+
+
+class QoSConfigError(Exception):
+    """
+    Exception to be thrown when an qos settings are incorrect
+    """
+
+    def __init__(self, message):
+        Exception.__init__(self, message)
diff --git a/snaps/config/router.py b/snaps/config/router.py
new file mode 100644 (file)
index 0000000..72164f2
--- /dev/null
@@ -0,0 +1,113 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from snaps.config.network import PortConfig
+from snaps.openstack.utils import neutron_utils, keystone_utils
+
+
+class RouterConfig(object):
+    """
+    Class representing a router configuration
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor - all parameters are optional
+        :param name: The router name.
+        :param project_name: The name of the project who owns the network. Only
+                             administrative users can specify a project ID
+                             other than their own. You cannot change this value
+                             through authorization policies.
+        :param external_gateway: Name of the external network to which to route
+        :param admin_state_up: The administrative status of the router.
+                               True = up / False = down (default True)
+        :param internal_subnets: List of subnet names to which to connect this
+                                 router for Floating IP purposes
+        :param port_settings: List of PortConfig objects
+        :return:
+        """
+        self.name = kwargs.get('name')
+        self.project_name = kwargs.get('project_name')
+        self.external_gateway = kwargs.get('external_gateway')
+
+        self.admin_state_up = kwargs.get('admin_state_up', True)
+        self.enable_snat = kwargs.get('enable_snat')
+        if kwargs.get('internal_subnets'):
+            self.internal_subnets = kwargs['internal_subnets']
+        else:
+            self.internal_subnets = list()
+
+        self.port_settings = list()
+        if kwargs.get('interfaces', kwargs.get('port_settings')):
+            interfaces = kwargs.get('interfaces', kwargs.get('port_settings'))
+            for interface in interfaces:
+                if isinstance(interface, PortConfig):
+                    self.port_settings.append(interface)
+                else:
+                    self.port_settings.append(
+                        PortConfig(**interface['port']))
+
+        if not self.name:
+            raise RouterConfigError('Name is required')
+
+    def dict_for_neutron(self, neutron, os_creds):
+        """
+        Returns a dictionary object representing this object.
+        This is meant to be converted into JSON designed for use by the Neutron
+        API
+
+        TODO - expand automated testing to exercise all parameters
+        :param neutron: The neutron client to retrieve external network
+                        information if necessary
+        :param os_creds: The OpenStack credentials
+        :return: the dictionary object
+        """
+        out = dict()
+        ext_gw = dict()
+
+        if self.name:
+            out['name'] = self.name
+        if self.project_name:
+            keystone = keystone_utils.keystone_client(os_creds)
+            project = keystone_utils.get_project(
+                keystone=keystone, project_name=self.project_name)
+            project_id = None
+            if project:
+                project_id = project.id
+            if project_id:
+                out['tenant_id'] = project_id
+            else:
+                raise RouterConfigError(
+                    'Could not find project ID for project named - ' +
+                    self.project_name)
+        if self.admin_state_up is not None:
+            out['admin_state_up'] = self.admin_state_up
+        if self.external_gateway:
+            ext_net = neutron_utils.get_network(
+                neutron, network_name=self.external_gateway)
+            if ext_net:
+                ext_gw['network_id'] = ext_net.id
+                out['external_gateway_info'] = ext_gw
+            else:
+                raise RouterConfigError(
+                    'Could not find the external network named - ' +
+                    self.external_gateway)
+
+        return {'router': out}
+
+
+class RouterConfigError(Exception):
+    """
+    Exception to be thrown when router settings attributes are incorrect
+    """
diff --git a/snaps/config/security_group.py b/snaps/config/security_group.py
new file mode 100644 (file)
index 0000000..32a1e95
--- /dev/null
@@ -0,0 +1,388 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import enum
+
+from snaps.openstack.utils import keystone_utils, neutron_utils
+
+
+class SecurityGroupConfig(object):
+    """
+    Class representing a keypair configuration
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: The security group's name (required)
+        :param description: The security group's description (optional)
+        :param project_name: The name of the project under which the security
+                             group will be created
+        :param rule_settings: a list of SecurityGroupRuleConfig objects
+        :return:
+        """
+        self.name = kwargs.get('name')
+        self.description = kwargs.get('description')
+        self.project_name = kwargs.get('project_name')
+        self.rule_settings = list()
+
+        rule_settings = kwargs.get('rules')
+        if not rule_settings:
+            rule_settings = kwargs.get('rule_settings')
+
+        if rule_settings:
+            for rule_setting in rule_settings:
+                if isinstance(rule_setting, SecurityGroupRuleConfig):
+                    self.rule_settings.append(rule_setting)
+                else:
+                    rule_setting['sec_grp_name'] = self.name
+                    self.rule_settings.append(SecurityGroupRuleConfig(
+                        **rule_setting))
+
+        if not self.name:
+            raise SecurityGroupConfigError('The attribute name is required')
+
+        for rule_setting in self.rule_settings:
+            if rule_setting.sec_grp_name is not self.name:
+                raise SecurityGroupConfigError(
+                    'Rule settings must correspond with the name of this '
+                    'security group')
+
+    def dict_for_neutron(self, keystone):
+        """
+        Returns a dictionary object representing this object.
+        This is meant to be converted into JSON designed for use by the Neutron
+        API
+
+        TODO - expand automated testing to exercise all parameters
+        :param keystone: the Keystone client
+        :return: the dictionary object
+        """
+        out = dict()
+
+        if self.name:
+            out['name'] = self.name
+        if self.description:
+            out['description'] = self.description
+        if self.project_name:
+            project = keystone_utils.get_project(
+                keystone=keystone, project_name=self.project_name)
+            project_id = None
+            if project:
+                project_id = project.id
+            if project_id:
+                out['tenant_id'] = project_id
+            else:
+                raise SecurityGroupConfigError(
+                    'Could not find project ID for project named - ' +
+                    self.project_name)
+
+        return {'security_group': out}
+
+
+class Direction(enum.Enum):
+    """
+    A rule's direction
+    """
+    ingress = 'ingress'
+    egress = 'egress'
+
+
+class Protocol(enum.Enum):
+    """
+    A rule's protocol
+    """
+    ah = 51
+    dccp = 33
+    egp = 8
+    esp = 50
+    gre = 47
+    icmp = 1
+    icmpv6 = 58
+    igmp = 2
+    ipv6_encap = 41
+    ipv6_frag = 44
+    ipv6_icmp = 58
+    ipv6_nonxt = 59
+    ipv6_opts = 60
+    ipv6_route = 43
+    ospf = 89
+    pgm = 113
+    rsvp = 46
+    sctp = 132
+    tcp = 6
+    udp = 17
+    udplite = 136
+    vrrp = 112
+    any = 'any'
+    null = 'null'
+
+
+class Ethertype(enum.Enum):
+    """
+    A rule's ethertype
+    """
+    IPv4 = 4
+    IPv6 = 6
+
+
+class SecurityGroupConfigError(Exception):
+    """
+    Exception to be thrown when security group settings attributes are
+    invalid
+    """
+
+
+class SecurityGroupRuleConfig(object):
+    """
+    Class representing a keypair configuration
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor - all parameters are optional
+        :param sec_grp_name: The security group's name on which to add the
+                             rule. (required)
+        :param description: The rule's description
+        :param direction: An enumeration of type
+                          create_security_group.RULE_DIRECTION (required)
+        :param remote_group_id: The group ID to associate with this rule
+                                (this should be changed to group name once
+                                snaps support Groups) (optional)
+        :param protocol: An enumeration of type
+                         create_security_group.RULE_PROTOCOL or a string value
+                         that will be mapped accordingly (optional)
+        :param ethertype: An enumeration of type
+                          create_security_group.RULE_ETHERTYPE (optional)
+        :param port_range_min: The minimum port number in the range that is
+                               matched by the security group rule. When the
+                               protocol is TCP or UDP, this value must be <=
+                               port_range_max.
+        :param port_range_max: The maximum port number in the range that is
+                               matched by the security group rule. When the
+                               protocol is TCP or UDP, this value must be <=
+                               port_range_max.
+        :param remote_ip_prefix: The remote IP prefix to associate with this
+                                 metering rule packet (optional)
+
+        TODO - Need to support the tenant...
+        """
+
+        self.description = kwargs.get('description')
+        self.sec_grp_name = kwargs.get('sec_grp_name')
+        self.remote_group_id = kwargs.get('remote_group_id')
+        self.direction = None
+        if kwargs.get('direction'):
+            self.direction = map_direction(kwargs['direction'])
+
+        self.protocol = None
+        if kwargs.get('protocol'):
+            self.protocol = map_protocol(kwargs['protocol'])
+        else:
+            self.protocol = Protocol.null
+
+        self.ethertype = None
+        if kwargs.get('ethertype'):
+            self.ethertype = map_ethertype(kwargs['ethertype'])
+
+        self.port_range_min = kwargs.get('port_range_min')
+        self.port_range_max = kwargs.get('port_range_max')
+        self.remote_ip_prefix = kwargs.get('remote_ip_prefix')
+
+        if not self.direction or not self.sec_grp_name:
+            raise SecurityGroupRuleConfigError(
+                'direction and sec_grp_name are required')
+
+    def dict_for_neutron(self, neutron):
+        """
+        Returns a dictionary object representing this object.
+        This is meant to be converted into JSON designed for use by the Neutron
+        API
+
+        :param neutron: the neutron client for performing lookups
+        :return: the dictionary object
+        """
+        out = dict()
+
+        if self.description:
+            out['description'] = self.description
+        if self.direction:
+            out['direction'] = self.direction.name
+        if self.port_range_min:
+            out['port_range_min'] = self.port_range_min
+        if self.port_range_max:
+            out['port_range_max'] = self.port_range_max
+        if self.ethertype:
+            out['ethertype'] = self.ethertype.name
+        if self.protocol and self.protocol.value != 'null':
+            out['protocol'] = self.protocol.value
+        if self.sec_grp_name:
+            sec_grp = neutron_utils.get_security_group(
+                neutron, sec_grp_name=self.sec_grp_name)
+            if sec_grp:
+                out['security_group_id'] = sec_grp.id
+            else:
+                raise SecurityGroupRuleConfigError(
+                    'Cannot locate security group with name - ' +
+                    self.sec_grp_name)
+        if self.remote_group_id:
+            out['remote_group_id'] = self.remote_group_id
+        if self.remote_ip_prefix:
+            out['remote_ip_prefix'] = self.remote_ip_prefix
+
+        return {'security_group_rule': out}
+
+    def rule_eq(self, rule):
+        """
+        Returns True if this setting created the rule
+        :param rule: the rule to evaluate
+        :return: T/F
+        """
+        if self.description is not None:
+            if rule.description is not None and rule.description != '':
+                return False
+        elif self.description != rule.description:
+            if rule.description != '':
+                return False
+
+        if self.direction.name != rule.direction:
+            return False
+
+        if self.ethertype and rule.ethertype:
+            if self.ethertype.name != rule.ethertype:
+                return False
+
+        if self.port_range_min and rule.port_range_min:
+            if self.port_range_min != rule.port_range_min:
+                return False
+
+        if self.port_range_max and rule.port_range_max:
+            if self.port_range_max != rule.port_range_max:
+                return False
+
+        if self.protocol and rule.protocol:
+            if self.protocol.name != rule.protocol:
+                return False
+
+        if self.remote_group_id and rule.remote_group_id:
+            if self.remote_group_id != rule.remote_group_id:
+                return False
+
+        if self.remote_ip_prefix and rule.remote_ip_prefix:
+            if self.remote_ip_prefix != rule.remote_ip_prefix:
+                return False
+
+        return True
+
+    def __eq__(self, other):
+        return (
+            self.description == other.description and
+            self.direction == other.direction and
+            self.port_range_min == other.port_range_min and
+            self.port_range_max == other.port_range_max and
+            self.ethertype == other.ethertype and
+            self.protocol == other.protocol and
+            self.sec_grp_name == other.sec_grp_name and
+            self.remote_group_id == other.remote_group_id and
+            self.remote_ip_prefix == other.remote_ip_prefix)
+
+    def __hash__(self):
+        return hash((self.sec_grp_name, self.description, self.direction,
+                     self.remote_group_id,
+                     self.protocol, self.ethertype, self.port_range_min,
+                     self.port_range_max, self.remote_ip_prefix))
+
+
+def map_direction(direction):
+    """
+    Takes a the direction value maps it to the Direction enum. When None return
+    None
+    :param direction: the direction value
+    :return: the Direction enum object
+    :raise: Exception if value is invalid
+    """
+    if not direction:
+        return None
+    if isinstance(direction, Direction):
+        return direction
+    elif (isinstance(direction, str) or isinstance(direction, unicode)
+            or isinstance(direction, unicode)):
+        dir_str = str(direction)
+        if dir_str == 'egress':
+            return Direction.egress
+        elif dir_str == 'ingress':
+            return Direction.ingress
+        else:
+            raise SecurityGroupRuleConfigError(
+                'Invalid Direction - ' + dir_str)
+    else:
+        return map_direction(direction.value)
+
+
+def map_protocol(protocol):
+    """
+    Takes a the protocol value maps it to the Protocol enum. When None return
+    None
+    :param protocol: the protocol value
+    :return: the Protocol enum object
+    :raise: Exception if value is invalid
+    """
+    if not protocol:
+        return None
+    elif isinstance(protocol, Protocol):
+        return protocol
+    elif (isinstance(protocol, str) or isinstance(protocol, unicode)
+            or isinstance(protocol, int)):
+        for proto_enum in Protocol:
+            if proto_enum.name == protocol or proto_enum.value == protocol:
+                if proto_enum == Protocol.any:
+                    return Protocol.null
+                return proto_enum
+        raise SecurityGroupRuleConfigError(
+            'Invalid Protocol - ' + protocol)
+    else:
+        return map_protocol(protocol.value)
+
+
+def map_ethertype(ethertype):
+    """
+    Takes a the ethertype value maps it to the Ethertype enum. When None return
+    None
+    :param ethertype: the ethertype value
+    :return: the Ethertype enum object
+    :raise: Exception if value is invalid
+    """
+    if not ethertype:
+        return None
+    elif isinstance(ethertype, Ethertype):
+        return ethertype
+    elif (isinstance(ethertype, str) or isinstance(ethertype, unicode)
+            or isinstance(ethertype, int)):
+        eth_str = str(ethertype)
+        if eth_str == 'IPv6' or eth_str == '6':
+            return Ethertype.IPv6
+        elif eth_str == 'IPv4' or eth_str == '4':
+            return Ethertype.IPv4
+        else:
+            raise SecurityGroupRuleConfigError(
+                'Invalid Ethertype - ' + eth_str)
+    else:
+        return map_ethertype(ethertype.value)
+
+
+class SecurityGroupRuleConfigError(Exception):
+    """
+    Exception to be thrown when security group rule settings attributes are
+    invalid
+    """
diff --git a/snaps/config/stack.py b/snaps/config/stack.py
new file mode 100644 (file)
index 0000000..4d5db29
--- /dev/null
@@ -0,0 +1,76 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+STACK_DELETE_TIMEOUT = 1200
+STACK_COMPLETE_TIMEOUT = 1200
+POLL_INTERVAL = 3
+STATUS_CREATE_FAILED = 'CREATE_FAILED'
+STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
+STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
+STATUS_DELETE_FAILED = 'DELETE_FAILED'
+
+
+class StackConfig(object):
+    """
+    Configuration for Heat stack
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the stack's name (required)
+        :param template: the heat template in dict() format (required if
+                         template_path attribute is None)
+        :param template_path: the location of the heat template file (required
+                              if template attribute is None)
+        :param resource_files: List of file paths to the resources referred to
+                               by the template
+        :param env_values: dict() of strings for substitution of template
+                           default values (optional)
+        """
+
+        self.name = kwargs.get('name')
+        self.template = kwargs.get('template')
+        self.template_path = kwargs.get('template_path')
+        self.resource_files = kwargs.get('resource_files')
+        self.env_values = kwargs.get('env_values')
+
+        if 'stack_create_timeout' in kwargs:
+            self.stack_create_timeout = kwargs['stack_create_timeout']
+        else:
+            self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
+
+        if not self.name:
+            raise StackConfigError('name is required')
+
+        if not self.template and not self.template_path:
+            raise StackConfigError('A Heat template is required')
+
+        if self.resource_files and not isinstance(self.resource_files, list):
+            raise StackConfigError(
+                'resource_files must be a list when not None')
+
+    def __eq__(self, other):
+        return (self.name == other.name and
+                self.template == other.template and
+                self.template_path == other.template_path and
+                self.env_values == other.env_values and
+                self.stack_create_timeout == other.stack_create_timeout)
+
+
+class StackConfigError(Exception):
+    """
+    Exception to be thrown when an stack configuration are incorrect
+    """
diff --git a/snaps/config/tests/__init__.py b/snaps/config/tests/__init__.py
new file mode 100644 (file)
index 0000000..271c742
--- /dev/null
@@ -0,0 +1,15 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+__author__ = 'spisarski'
diff --git a/snaps/config/tests/cluster_template_tests.py b/snaps/config/tests/cluster_template_tests.py
new file mode 100644 (file)
index 0000000..e06b783
--- /dev/null
@@ -0,0 +1,180 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.cluster_template import (
+    ClusterTemplateConfig, ClusterTemplateConfigError, ServerType,
+    DockerStorageDriver, ContainerOrchestrationEngine)
+
+
+class ClusterTemplateConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the ClusterTemplateConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(ClusterTemplateConfigError):
+            ClusterTemplateConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(ClusterTemplateConfigError):
+            ClusterTemplateConfig(config=dict())
+
+    def test_name_only(self):
+        with self.assertRaises(ClusterTemplateConfigError):
+            ClusterTemplateConfig(name='foo')
+
+    def test_minimal_named(self):
+        config = ClusterTemplateConfig(
+            name='foo', image='bar', keypair='keys', external_net='external')
+        self.assertIsNotNone(config)
+        self.assertEqual('foo', config.name)
+        self.assertEqual('bar', config.image)
+        self.assertEqual('keys', config.keypair)
+        self.assertIsNone(config.network_driver)
+        self.assertEqual('external', config.external_net)
+        self.assertTrue(config.floating_ip_enabled)
+        self.assertEqual(3, config.docker_volume_size)
+        self.assertEqual(ServerType.vm, config.server_type)
+        self.assertIsNone(config.flavor)
+        self.assertIsNone(config.master_flavor)
+        self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe)
+        self.assertIsNone(config.fixed_net)
+        self.assertIsNone(config.fixed_subnet)
+        self.assertTrue(config.registry_enabled)
+        self.assertIsNone(config.insecure_registry)
+        self.assertEqual(DockerStorageDriver.devicemapper,
+                         config.docker_storage_driver)
+        self.assertIsNone(config.dns_nameserver)
+        self.assertFalse(config.public)
+        self.assertFalse(config.tls_disabled)
+        self.assertIsNone(config.http_proxy)
+        self.assertIsNone(config.https_proxy)
+        self.assertIsNone(config.no_proxy)
+        self.assertIsNone(config.volume_driver)
+        self.assertTrue(config.master_lb_enabled)
+        self.assertIsNone(config.labels)
+
+    def test_minimal_config(self):
+        config = ClusterTemplateConfig(
+            **{'name': 'foo', 'image': 'bar', 'keypair': 'keys',
+               'external_net': 'external'})
+        self.assertIsNotNone(config)
+        self.assertEqual('foo', config.name)
+        self.assertEqual('bar', config.image)
+        self.assertEqual('keys', config.keypair)
+        self.assertIsNone(config.network_driver)
+        self.assertEqual('external', config.external_net)
+        self.assertTrue(config.floating_ip_enabled)
+        self.assertEqual(3, config.docker_volume_size)
+        self.assertEqual(ServerType.vm, config.server_type)
+        self.assertIsNone(config.flavor)
+        self.assertIsNone(config.master_flavor)
+        self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe)
+        self.assertIsNone(config.fixed_net)
+        self.assertIsNone(config.fixed_subnet)
+        self.assertTrue(config.registry_enabled)
+        self.assertIsNone(config.insecure_registry)
+        self.assertEqual(DockerStorageDriver.devicemapper,
+                         config.docker_storage_driver)
+        self.assertIsNone(config.dns_nameserver)
+        self.assertFalse(config.public)
+        self.assertFalse(config.tls_disabled)
+        self.assertIsNone(config.http_proxy)
+        self.assertIsNone(config.https_proxy)
+        self.assertIsNone(config.no_proxy)
+        self.assertIsNone(config.volume_driver)
+        self.assertTrue(config.master_lb_enabled)
+        self.assertIsNone(config.labels)
+
+    def test_all_named(self):
+        labels = {'foo': 'bar'}
+        config = ClusterTemplateConfig(
+            name='foo', image='bar', keypair='keys', network_driver='driver',
+            external_net='external', docker_volume_size=99,
+            server_type=ServerType.baremetal, flavor='testFlavor',
+            master_flavor='masterFlavor',
+            coe=ContainerOrchestrationEngine.kubernetes, fixed_net='fixedNet',
+            fixed_subnet='fixedSubnet', registry_enabled=False,
+            docker_storage_driver=DockerStorageDriver.overlay,
+            dns_nameserver='8.8.4.4', public=True, tls=False,
+            http_proxy='http://foo:8080', https_proxy='https://foo:443',
+            no_proxy='foo,bar', volume_driver='volDriver',
+            master_lb_enabled=False, labels=labels)
+        self.assertIsNotNone(config)
+        self.assertEqual('foo', config.name)
+        self.assertEqual('bar', config.image)
+        self.assertEqual('keys', config.keypair)
+        self.assertEqual('driver', config.network_driver)
+        self.assertEqual('external', config.external_net)
+        self.assertEqual(99, config.docker_volume_size)
+        self.assertEqual(ServerType.baremetal, config.server_type)
+        self.assertEqual('testFlavor', config.flavor)
+        self.assertEqual('masterFlavor', config.master_flavor)
+        self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe)
+        self.assertEqual('fixedNet', config.fixed_net)
+        self.assertEqual('fixedSubnet', config.fixed_subnet)
+        self.assertFalse(config.registry_enabled)
+        self.assertEqual(DockerStorageDriver.overlay,
+                         config.docker_storage_driver)
+        self.assertEqual('8.8.4.4', config.dns_nameserver)
+        self.assertTrue(config.public)
+        self.assertFalse(config.tls_disabled)
+        self.assertEqual('http://foo:8080', config.http_proxy)
+        self.assertEqual('https://foo:443', config.https_proxy)
+        self.assertEqual('foo,bar', config.no_proxy)
+        self.assertEqual('volDriver', config.volume_driver)
+        self.assertFalse(config.master_lb_enabled)
+        self.assertEqual(labels, config.labels)
+
+    def test_all_config(self):
+        labels = {'foo': 'bar'}
+        config = ClusterTemplateConfig(**{
+            'name': 'foo', 'image': 'bar', 'keypair': 'keys',
+            'network_driver': 'driver', 'external_net': 'external',
+            'docker_volume_size': '99', 'server_type': 'baremetal',
+            'flavor': 'testFlavor', 'master_flavor': 'masterFlavor',
+            'coe': 'kubernetes', 'fixed_net': 'fixedNet',
+            'fixed_subnet': 'fixedSubnet', 'registry_enabled': 'false',
+            'docker_storage_driver': 'overlay', 'dns_nameserver': '8.8.4.4',
+            'public': 'true', 'tls': 'false', 'http_proxy': 'http://foo:8080',
+            'https_proxy': 'https://foo:443', 'no_proxy': 'foo,bar',
+            'volume_driver': 'volDriver', 'master_lb_enabled': 'false',
+            'labels': labels})
+        self.assertIsNotNone(config)
+        self.assertEqual('foo', config.name)
+        self.assertEqual('bar', config.image)
+        self.assertEqual('keys', config.keypair)
+        self.assertEqual('driver', config.network_driver)
+        self.assertEqual('external', config.external_net)
+        self.assertEqual(99, config.docker_volume_size)
+        self.assertEqual(ServerType.baremetal, config.server_type)
+        self.assertEqual('testFlavor', config.flavor)
+        self.assertEqual('masterFlavor', config.master_flavor)
+        self.assertEqual(ContainerOrchestrationEngine.kubernetes, config.coe)
+        self.assertEqual('fixedNet', config.fixed_net)
+        self.assertEqual('fixedSubnet', config.fixed_subnet)
+        self.assertFalse(config.registry_enabled)
+        self.assertEqual(DockerStorageDriver.overlay,
+                         config.docker_storage_driver)
+        self.assertEqual('8.8.4.4', config.dns_nameserver)
+        self.assertTrue(config.public)
+        self.assertFalse(config.tls_disabled)
+        self.assertEqual('http://foo:8080', config.http_proxy)
+        self.assertEqual('https://foo:443', config.https_proxy)
+        self.assertEqual('foo,bar', config.no_proxy)
+        self.assertEqual('volDriver', config.volume_driver)
+        self.assertFalse(config.master_lb_enabled)
+        self.assertEqual(labels, config.labels)
diff --git a/snaps/config/tests/flavor_tests.py b/snaps/config/tests/flavor_tests.py
new file mode 100644 (file)
index 0000000..15cd99a
--- /dev/null
@@ -0,0 +1,254 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.flavor import FlavorConfig, FlavorConfigError
+
+
+class FlavorConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the FlavorConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(config=dict())
+
+    def test_name_only(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(config={'name': 'foo'})
+
+    def test_name_ram_only(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1)
+
+    def test_config_with_name_ram_only(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(config={'name': 'foo', 'ram': 1})
+
+    def test_name_ram_disk_only(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=1)
+
+    def test_config_with_name_ram_disk_only(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(config={'name': 'foo', 'ram': 1, 'disk': 1})
+
+    def test_ram_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram='bar', disk=2, vcpus=3, ephemeral=4,
+                         swap=5, rxtx_factor=6.0,
+                         is_public=False)
+
+    def test_config_ram_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 'bar', 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_ram_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1.5, disk=2, vcpus=3, ephemeral=4,
+                         swap=5, rxtx_factor=6.0, is_public=False)
+
+    def test_config_ram_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1.5, 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_disk_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk='bar', vcpus=3, ephemeral=4,
+                         swap=5, rxtx_factor=6.0,
+                         is_public=False)
+
+    def test_config_disk_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 'bar', 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_disk_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2.5, vcpus=3, ephemeral=4,
+                         swap=5, rxtx_factor=6.0, is_public=False)
+
+    def test_config_disk_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2.5, 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_vcpus_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2, vcpus='bar', ephemeral=4,
+                         swap=5, rxtx_factor=6.0,
+                         is_public=False)
+
+    def test_config_vcpus_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 'bar',
+                        'ephemeral': 4, 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_ephemeral_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2, vcpus=3, ephemeral='bar',
+                         swap=5, rxtx_factor=6.0,
+                         is_public=False)
+
+    def test_config_ephemeral_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 'bar', 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_ephemeral_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4.5,
+                         swap=5, rxtx_factor=6.0, is_public=False)
+
+    def test_config_ephemeral_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 4.5, 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_swap_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
+                         swap='bar', rxtx_factor=6.0,
+                         is_public=False)
+
+    def test_config_swap_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 'bar',
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_swap_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
+                         swap=5.5, rxtx_factor=6.0, is_public=False)
+
+    def test_config_swap_float(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 5.5,
+                        'rxtx_factor': 6.0, 'is_public': False})
+
+    def test_rxtx_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
+                         swap=5, rxtx_factor='bar', is_public=False)
+
+    def test_config_rxtx_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 5,
+                        'rxtx_factor': 'bar', 'is_public': False})
+
+    def test_is_pub_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
+                         swap=5, rxtx_factor=6.0, is_public='bar')
+
+    def test_config_is_pub_string(self):
+        with self.assertRaises(FlavorConfigError):
+            FlavorConfig(
+                config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
+                        'ephemeral': 4, 'swap': 5,
+                        'rxtx_factor': 6.0, 'is_public': 'bar'})
+
+    def test_name_ram_disk_vcpus_only(self):
+        settings = FlavorConfig(name='foo', ram=1, disk=2, vcpus=3)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('auto', settings.flavor_id)
+        self.assertEqual(1, settings.ram)
+        self.assertEqual(2, settings.disk)
+        self.assertEqual(3, settings.vcpus)
+        self.assertEqual(0, settings.ephemeral)
+        self.assertEqual(0, settings.swap)
+        self.assertEqual(1.0, settings.rxtx_factor)
+        self.assertEqual(True, settings.is_public)
+        self.assertEqual(None, settings.metadata)
+
+    def test_config_with_name_ram_disk_vcpus_only(self):
+        settings = FlavorConfig(
+            **{'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('auto', settings.flavor_id)
+        self.assertEqual(1, settings.ram)
+        self.assertEqual(2, settings.disk)
+        self.assertEqual(3, settings.vcpus)
+        self.assertEqual(0, settings.ephemeral)
+        self.assertEqual(0, settings.swap)
+        self.assertEqual(1.0, settings.rxtx_factor)
+        self.assertEqual(True, settings.is_public)
+        self.assertEqual(None, settings.metadata)
+
+    def test_all(self):
+        metadata = {'foo': 'bar'}
+        settings = FlavorConfig(
+            name='foo', flavor_id='bar', ram=1, disk=2, vcpus=3, ephemeral=4,
+            swap=5, rxtx_factor=6.0, is_public=False, metadata=metadata)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.flavor_id)
+        self.assertEqual(1, settings.ram)
+        self.assertEqual(2, settings.disk)
+        self.assertEqual(3, settings.vcpus)
+        self.assertEqual(4, settings.ephemeral)
+        self.assertEqual(5, settings.swap)
+        self.assertEqual(6.0, settings.rxtx_factor)
+        self.assertEqual(False, settings.is_public)
+        self.assertEqual(metadata, settings.metadata)
+
+    def test_config_all(self):
+        metadata = {'foo': 'bar'}
+        settings = FlavorConfig(
+            **{'name': 'foo', 'flavor_id': 'bar', 'ram': 1, 'disk': 2,
+               'vcpus': 3,
+               'ephemeral': 4, 'swap': 5, 'rxtx_factor': 6.0,
+               'is_public': False,
+               'metadata': metadata})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.flavor_id)
+        self.assertEqual(1, settings.ram)
+        self.assertEqual(2, settings.disk)
+        self.assertEqual(3, settings.vcpus)
+        self.assertEqual(4, settings.ephemeral)
+        self.assertEqual(5, settings.swap)
+        self.assertEqual(6.0, settings.rxtx_factor)
+        self.assertEqual(False, settings.is_public)
+        self.assertEqual(metadata, settings.metadata)
diff --git a/snaps/config/tests/image_tests.py b/snaps/config/tests/image_tests.py
new file mode 100644 (file)
index 0000000..8dcd2b4
--- /dev/null
@@ -0,0 +1,226 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.image import ImageConfigError, ImageConfig
+
+
+class ImageConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the ImageConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig(**{'name': 'foo'})
+
+    def test_name_user_only(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig(name='foo', image_user='bar')
+
+    def test_config_with_name_user_only(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig(**{'name': 'foo', 'image_user': 'bar'})
+
+    def test_name_user_format_only(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig(name='foo', image_user='bar', img_format='qcow2')
+
+    def test_config_with_name_user_format_only(self):
+        with self.assertRaises(ImageConfigError):
+            ImageConfig(
+                **{'name': 'foo', 'image_user': 'bar', 'format': 'qcow2'})
+
+    def test_name_user_format_url_only(self):
+        settings = ImageConfig(name='foo', image_user='bar',
+                               img_format='qcow2', url='http://foo.com')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertEqual('http://foo.com', settings.url)
+        self.assertIsNone(settings.image_file)
+        self.assertFalse(settings.exists)
+        self.assertFalse(settings.public)
+        self.assertIsNone(settings.nic_config_pb_loc)
+
+    def test_name_user_format_url_only_properties(self):
+        properties = {'hw_video_model': 'vga'}
+        settings = ImageConfig(name='foo', image_user='bar',
+                               img_format='qcow2', url='http://foo.com',
+                               extra_properties=properties)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertEqual('http://foo.com', settings.url)
+        self.assertEqual(properties, settings.extra_properties)
+        self.assertIsNone(settings.image_file)
+        self.assertFalse(settings.exists)
+        self.assertFalse(settings.public)
+        self.assertIsNone(settings.nic_config_pb_loc)
+
+    def test_config_with_name_user_format_url_only(self):
+        settings = ImageConfig(
+            **{'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+               'download_url': 'http://foo.com'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertEqual('http://foo.com', settings.url)
+        self.assertIsNone(settings.image_file)
+        self.assertFalse(settings.exists)
+        self.assertFalse(settings.public)
+        self.assertIsNone(settings.nic_config_pb_loc)
+
+    def test_name_user_format_file_only(self):
+        settings = ImageConfig(name='foo', image_user='bar',
+                               img_format='qcow2',
+                               image_file='/foo/bar.qcow')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertIsNone(settings.url)
+        self.assertEqual('/foo/bar.qcow', settings.image_file)
+        self.assertFalse(settings.exists)
+        self.assertFalse(settings.public)
+        self.assertIsNone(settings.nic_config_pb_loc)
+
+    def test_config_with_name_user_format_file_only(self):
+        settings = ImageConfig(
+            **{'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+               'image_file': '/foo/bar.qcow'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertIsNone(settings.url)
+        self.assertEqual('/foo/bar.qcow', settings.image_file)
+        self.assertFalse(settings.exists)
+        self.assertFalse(settings.public)
+        self.assertIsNone(settings.nic_config_pb_loc)
+
+    def test_all_url(self):
+        properties = {'hw_video_model': 'vga'}
+        kernel_settings = ImageConfig(name='kernel', url='http://kernel.com',
+                                      image_user='bar', img_format='qcow2')
+        ramdisk_settings = ImageConfig(name='ramdisk',
+                                       url='http://ramdisk.com',
+                                       image_user='bar', img_format='qcow2')
+        settings = ImageConfig(name='foo', image_user='bar',
+                               img_format='qcow2', url='http://foo.com',
+                               extra_properties=properties,
+                               nic_config_pb_loc='/foo/bar',
+                               kernel_image_settings=kernel_settings,
+                               ramdisk_image_settings=ramdisk_settings,
+                               exists=True, public=True)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertEqual('http://foo.com', settings.url)
+        self.assertEqual(properties, settings.extra_properties)
+        self.assertIsNone(settings.image_file)
+        self.assertEqual('/foo/bar', settings.nic_config_pb_loc)
+        self.assertEqual('kernel', settings.kernel_image_settings.name)
+        self.assertEqual('http://kernel.com',
+                         settings.kernel_image_settings.url)
+        self.assertEqual('bar', settings.kernel_image_settings.image_user)
+        self.assertEqual('qcow2', settings.kernel_image_settings.format)
+        self.assertEqual('ramdisk', settings.ramdisk_image_settings.name)
+        self.assertEqual('http://ramdisk.com',
+                         settings.ramdisk_image_settings.url)
+        self.assertEqual('bar', settings.ramdisk_image_settings.image_user)
+        self.assertEqual('qcow2', settings.ramdisk_image_settings.format)
+        self.assertTrue(settings.exists)
+        self.assertTrue(settings.public)
+
+    def test_config_all_url(self):
+        settings = ImageConfig(
+            **{'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+               'download_url': 'http://foo.com',
+               'extra_properties': '{\'hw_video_model\': \'vga\'}',
+               'nic_config_pb_loc': '/foo/bar',
+               'kernel_image_settings': {
+                   'name': 'kernel',
+                   'download_url': 'http://kernel.com',
+                   'image_user': 'bar',
+                   'format': 'qcow2'},
+               'ramdisk_image_settings': {
+                   'name': 'ramdisk',
+                   'download_url': 'http://ramdisk.com',
+                   'image_user': 'bar',
+                   'format': 'qcow2'},
+               'exists': True, 'public': True})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertEqual('http://foo.com', settings.url)
+        self.assertEqual('{\'hw_video_model\': \'vga\'}',
+                         settings.extra_properties)
+        self.assertIsNone(settings.image_file)
+        self.assertEqual('/foo/bar', settings.nic_config_pb_loc)
+        self.assertEqual('kernel', settings.kernel_image_settings.name)
+        self.assertEqual('http://kernel.com',
+                         settings.kernel_image_settings.url)
+        self.assertEqual('ramdisk', settings.ramdisk_image_settings.name)
+        self.assertEqual('http://ramdisk.com',
+                         settings.ramdisk_image_settings.url)
+        self.assertTrue(settings.exists)
+        self.assertTrue(settings.public)
+
+    def test_all_file(self):
+        properties = {'hw_video_model': 'vga'}
+        settings = ImageConfig(name='foo', image_user='bar',
+                               img_format='qcow2',
+                               image_file='/foo/bar.qcow',
+                               extra_properties=properties,
+                               nic_config_pb_loc='/foo/bar', exists=True,
+                               public=True)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertIsNone(settings.url)
+        self.assertEqual('/foo/bar.qcow', settings.image_file)
+        self.assertEqual(properties, settings.extra_properties)
+        self.assertEqual('/foo/bar', settings.nic_config_pb_loc)
+        self.assertTrue(settings.exists)
+        self.assertTrue(settings.public)
+
+    def test_config_all_file(self):
+        settings = ImageConfig(
+            **{'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+               'image_file': '/foo/bar.qcow',
+               'extra_properties': '{\'hw_video_model\' : \'vga\'}',
+               'nic_config_pb_loc': '/foo/bar', 'exists': True,
+               'public': True})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.image_user)
+        self.assertEqual('qcow2', settings.format)
+        self.assertIsNone(settings.url)
+        self.assertEqual('/foo/bar.qcow', settings.image_file)
+        self.assertEqual('{\'hw_video_model\' : \'vga\'}',
+                         settings.extra_properties)
+        self.assertEqual('/foo/bar', settings.nic_config_pb_loc)
+        self.assertTrue(settings.exists)
+        self.assertTrue(settings.public)
diff --git a/snaps/config/tests/keypair_tests.py b/snaps/config/tests/keypair_tests.py
new file mode 100644 (file)
index 0000000..6d0fec4
--- /dev/null
@@ -0,0 +1,179 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.keypair import KeypairConfigError, KeypairConfig
+
+
+class KeypairConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the KeypairConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(KeypairConfigError):
+            KeypairConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(KeypairConfigError):
+            KeypairConfig(**dict())
+
+    def test_small_key_size(self):
+        with self.assertRaises(KeypairConfigError):
+            KeypairConfig(name='foo', key_size=511)
+
+    def test_name_only(self):
+        settings = KeypairConfig(name='foo')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertIsNone(settings.public_filepath)
+        self.assertIsNone(settings.private_filepath)
+        self.assertIsNone(settings.delete_on_clean)
+
+    def test_config_with_name_only(self):
+        settings = KeypairConfig(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertIsNone(settings.public_filepath)
+        self.assertIsNone(settings.private_filepath)
+        self.assertIsNone(settings.delete_on_clean)
+
+    def test_name_pub_only(self):
+        settings = KeypairConfig(name='foo', public_filepath='/foo/bar.pub')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertIsNone(settings.private_filepath)
+        self.assertIsNone(settings.delete_on_clean)
+
+    def test_config_with_name_pub_only(self):
+        settings = KeypairConfig(
+            **{'name': 'foo', 'public_filepath': '/foo/bar.pub'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertIsNone(settings.private_filepath)
+        self.assertIsNone(settings.delete_on_clean)
+
+    def test_name_priv_only(self):
+        settings = KeypairConfig(name='foo', private_filepath='/foo/bar')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertIsNone(settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertIsNone(settings.delete_on_clean)
+
+    def test_config_with_name_priv_only(self):
+        settings = KeypairConfig(
+            **{'name': 'foo', 'private_filepath': '/foo/bar'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertIsNone(settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertIsNone(settings.delete_on_clean)
+
+    def test_all_delete_bool(self):
+        settings = KeypairConfig(
+            name='foo', public_filepath='/foo/bar.pub',
+            private_filepath='/foo/bar', delete_on_clean=True,
+            key_size=999)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(999, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertTrue(settings.delete_on_clean)
+
+    def test_all_delete_str_true_cap(self):
+        settings = KeypairConfig(
+            name='foo', public_filepath='/foo/bar.pub',
+            private_filepath='/foo/bar', delete_on_clean='True')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertTrue(settings.delete_on_clean)
+
+    def test_all_delete_str_true_lc(self):
+        settings = KeypairConfig(
+            name='foo', public_filepath='/foo/bar.pub',
+            private_filepath='/foo/bar', delete_on_clean='true')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertTrue(settings.delete_on_clean)
+
+    def test_all_delete_str_false_cap(self):
+        settings = KeypairConfig(
+            name='foo', public_filepath='/foo/bar.pub',
+            private_filepath='/foo/bar', delete_on_clean='False')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertFalse(settings.delete_on_clean)
+
+    def test_all_delete_str_false_lc(self):
+        settings = KeypairConfig(
+            name='foo', public_filepath='/foo/bar.pub',
+            private_filepath='/foo/bar', delete_on_clean='false',
+            key_size='999')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(999, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertFalse(settings.delete_on_clean)
+
+    def test_config_all_delete_false_bool(self):
+        settings = KeypairConfig(
+            **{'name': 'foo', 'public_filepath': '/foo/bar.pub',
+               'private_filepath': '/foo/bar', 'delete_on_clean': False,
+               'key_size': 999})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(999, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertFalse(settings.delete_on_clean)
+
+    def test_config_all_delete_false_str_cap(self):
+        settings = KeypairConfig(
+            **{'name': 'foo', 'public_filepath': '/foo/bar.pub',
+               'private_filepath': '/foo/bar', 'delete_on_clean': 'False'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertFalse(settings.delete_on_clean)
+
+    def test_config_all_delete_false_str_lc(self):
+        settings = KeypairConfig(
+            **{'name': 'foo', 'public_filepath': '/foo/bar.pub',
+               'private_filepath': '/foo/bar', 'delete_on_clean': 'false'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(1024, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertFalse(settings.delete_on_clean)
+
+    def test_config_all_delete_false_str_foo(self):
+        settings = KeypairConfig(
+            **{'name': 'foo', 'public_filepath': '/foo/bar.pub',
+               'private_filepath': '/foo/bar', 'delete_on_clean': 'foo',
+               'key_size': '999'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(999, settings.key_size)
+        self.assertEqual('/foo/bar.pub', settings.public_filepath)
+        self.assertEqual('/foo/bar', settings.private_filepath)
+        self.assertFalse(settings.delete_on_clean)
diff --git a/snaps/config/tests/network_tests.py b/snaps/config/tests/network_tests.py
new file mode 100644 (file)
index 0000000..43b69c7
--- /dev/null
@@ -0,0 +1,336 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.network import (
+    NetworkConfigError, NetworkConfig,  SubnetConfig, SubnetConfigError,
+    IPv6Mode, PortConfig, PortConfigError)
+
+
+class NetworkConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the NetworkConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(NetworkConfigError):
+            NetworkConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(NetworkConfigError):
+            NetworkConfig(**dict())
+
+    def test_name_only(self):
+        settings = NetworkConfig(name='foo')
+        self.assertEqual('foo', settings.name)
+        self.assertTrue(settings.admin_state_up)
+        self.assertIsNone(settings.shared)
+        self.assertIsNone(settings.project_name)
+        self.assertFalse(settings.external)
+        self.assertIsNone(settings.network_type)
+        self.assertIsNone(settings.segmentation_id)
+        self.assertEqual(0, len(settings.subnet_settings))
+
+    def test_config_with_name_only(self):
+        settings = NetworkConfig(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+        self.assertTrue(settings.admin_state_up)
+        self.assertIsNone(settings.shared)
+        self.assertIsNone(settings.project_name)
+        self.assertFalse(settings.external)
+        self.assertIsNone(settings.network_type)
+        self.assertIsNone(settings.segmentation_id)
+        self.assertEqual(0, len(settings.subnet_settings))
+
+    def test_all(self):
+        sub_settings = SubnetConfig(name='foo-subnet', cidr='10.0.0.0/24')
+        settings = NetworkConfig(
+            name='foo', admin_state_up=False, shared=True, project_name='bar',
+            external=True, network_type='vlan', physical_network='phy',
+            segmentation_id=2366, subnet_settings=[sub_settings])
+        self.assertEqual('foo', settings.name)
+        self.assertFalse(settings.admin_state_up)
+        self.assertTrue(settings.shared)
+        self.assertEqual('bar', settings.project_name)
+        self.assertTrue(settings.external)
+        self.assertEqual('vlan', settings.network_type)
+        self.assertEqual('phy', settings.physical_network)
+        self.assertEqual(2366, settings.segmentation_id)
+        self.assertEqual(1, len(settings.subnet_settings))
+        self.assertEqual('foo-subnet', settings.subnet_settings[0].name)
+
+    def test_config_all(self):
+        settings = NetworkConfig(
+            **{'name': 'foo', 'admin_state_up': False, 'shared': True,
+               'project_name': 'bar', 'external': True, 'network_type': 'vlan',
+               'physical_network': 'phy',
+               'segmentation_id': 2366,
+               'subnets':
+                   [{'subnet': {'name': 'foo-subnet',
+                                'cidr': '10.0.0.0/24'}}]})
+        self.assertEqual('foo', settings.name)
+        self.assertFalse(settings.admin_state_up)
+        self.assertTrue(settings.shared)
+        self.assertEqual('bar', settings.project_name)
+        self.assertTrue(settings.external)
+        self.assertEqual('vlan', settings.network_type)
+        self.assertEqual('phy', settings.physical_network)
+        self.assertEqual(2366, settings.segmentation_id)
+        self.assertEqual(1, len(settings.subnet_settings))
+        self.assertEqual('foo-subnet', settings.subnet_settings[0].name)
+
+
+class SubnetConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the SubnetConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(SubnetConfigError):
+            SubnetConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(SubnetConfigError):
+            SubnetConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(SubnetConfigError):
+            SubnetConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(SubnetConfigError):
+            SubnetConfig(**{'name': 'foo'})
+
+    def test_name_cidr_only(self):
+        settings = SubnetConfig(name='foo', cidr='10.0.0.0/24')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('10.0.0.0/24', settings.cidr)
+        self.assertEqual(4, settings.ip_version)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.start)
+        self.assertIsNone(settings.end)
+        self.assertIsNone(settings.enable_dhcp)
+        self.assertEqual(1, len(settings.dns_nameservers))
+        self.assertEqual('8.8.8.8', settings.dns_nameservers[0])
+        self.assertIsNone(settings.host_routes)
+        self.assertIsNone(settings.destination)
+        self.assertIsNone(settings.nexthop)
+        self.assertIsNone(settings.ipv6_ra_mode)
+        self.assertIsNone(settings.ipv6_address_mode)
+
+    def test_config_with_name_cidr_only(self):
+        settings = SubnetConfig(**{'name': 'foo', 'cidr': '10.0.0.0/24'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('10.0.0.0/24', settings.cidr)
+        self.assertEqual(4, settings.ip_version)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.start)
+        self.assertIsNone(settings.end)
+        self.assertIsNone(settings.gateway_ip)
+        self.assertIsNone(settings.enable_dhcp)
+        self.assertEqual(1, len(settings.dns_nameservers))
+        self.assertEqual('8.8.8.8', settings.dns_nameservers[0])
+        self.assertIsNone(settings.host_routes)
+        self.assertIsNone(settings.destination)
+        self.assertIsNone(settings.nexthop)
+        self.assertIsNone(settings.ipv6_ra_mode)
+        self.assertIsNone(settings.ipv6_address_mode)
+
+    def test_all_string_enums(self):
+        host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'}
+        settings = SubnetConfig(
+            name='foo', cidr='10.0.0.0/24', ip_version=6,
+            project_name='bar-project', start='10.0.0.2', end='10.0.0.101',
+            gateway_ip='10.0.0.1', enable_dhcp=False,
+            dns_nameservers=['8.8.8.8'], host_routes=[host_routes],
+            destination='dest', nexthop='hop', ipv6_ra_mode='dhcpv6-stateful',
+            ipv6_address_mode='slaac')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('10.0.0.0/24', settings.cidr)
+        self.assertEqual(6, settings.ip_version)
+        self.assertEqual('bar-project', settings.project_name)
+        self.assertEqual('10.0.0.2', settings.start)
+        self.assertEqual('10.0.0.101', settings.end)
+        self.assertEqual('10.0.0.1', settings.gateway_ip)
+        self.assertEqual(False, settings.enable_dhcp)
+        self.assertEqual(1, len(settings.dns_nameservers))
+        self.assertEqual('8.8.8.8', settings.dns_nameservers[0])
+        self.assertEqual(1, len(settings.host_routes))
+        self.assertEqual(host_routes, settings.host_routes[0])
+        self.assertEqual('dest', settings.destination)
+        self.assertEqual('hop', settings.nexthop)
+        self.assertEqual(IPv6Mode.stateful, settings.ipv6_ra_mode)
+        self.assertEqual(IPv6Mode.slaac, settings.ipv6_address_mode)
+
+    def test_all_type_enums(self):
+        host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'}
+        settings = SubnetConfig(
+            name='foo', cidr='10.0.0.0/24', ip_version=6,
+            project_name='bar-project', start='10.0.0.2', end='10.0.0.101',
+            gateway_ip='10.0.0.1', enable_dhcp=False,
+            dns_nameservers=['8.8.8.8'], host_routes=[host_routes],
+            destination='dest', nexthop='hop', ipv6_ra_mode=IPv6Mode.stateful,
+            ipv6_address_mode=IPv6Mode.slaac)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('10.0.0.0/24', settings.cidr)
+        self.assertEqual(6, settings.ip_version)
+        self.assertEqual('bar-project', settings.project_name)
+        self.assertEqual('10.0.0.2', settings.start)
+        self.assertEqual('10.0.0.101', settings.end)
+        self.assertEqual('10.0.0.1', settings.gateway_ip)
+        self.assertEqual(False, settings.enable_dhcp)
+        self.assertEqual(1, len(settings.dns_nameservers))
+        self.assertEqual('8.8.8.8', settings.dns_nameservers[0])
+        self.assertEqual(1, len(settings.host_routes))
+        self.assertEqual(host_routes, settings.host_routes[0])
+        self.assertEqual('dest', settings.destination)
+        self.assertEqual('hop', settings.nexthop)
+        self.assertEqual(IPv6Mode.stateful, settings.ipv6_ra_mode)
+        self.assertEqual(IPv6Mode.slaac, settings.ipv6_address_mode)
+
+    def test_config_all(self):
+        host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'}
+        settings = SubnetConfig(
+            **{'name': 'foo', 'cidr': '10.0.0.0/24', 'ip_version': 6,
+               'project_name': 'bar-project', 'start': '10.0.0.2',
+               'end': '10.0.0.101',
+               'gateway_ip': '10.0.0.1', 'enable_dhcp': False,
+               'dns_nameservers': ['8.8.8.8'], 'host_routes': [host_routes],
+               'destination': 'dest', 'nexthop': 'hop',
+               'ipv6_ra_mode': 'dhcpv6-stateless',
+               'ipv6_address_mode': 'slaac'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('10.0.0.0/24', settings.cidr)
+        self.assertEqual(6, settings.ip_version)
+        self.assertEqual('bar-project', settings.project_name)
+        self.assertEqual('10.0.0.2', settings.start)
+        self.assertEqual('10.0.0.101', settings.end)
+        self.assertEqual('10.0.0.1', settings.gateway_ip)
+        self.assertEqual(False, settings.enable_dhcp)
+        self.assertEqual(1, len(settings.dns_nameservers))
+        self.assertEqual('8.8.8.8', settings.dns_nameservers[0])
+        self.assertEqual(1, len(settings.host_routes))
+        self.assertEqual(host_routes, settings.host_routes[0])
+        self.assertEqual('dest', settings.destination)
+        self.assertEqual('hop', settings.nexthop)
+        self.assertEqual(IPv6Mode.stateless, settings.ipv6_ra_mode)
+        self.assertEqual(IPv6Mode.slaac, settings.ipv6_address_mode)
+
+
+class PortConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the PortConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(PortConfigError):
+            PortConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(PortConfigError):
+            PortConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(PortConfigError):
+            PortConfig(name='foo')
+
+    def test_config_name_only(self):
+        with self.assertRaises(PortConfigError):
+            PortConfig(**{'name': 'foo'})
+
+    def test_name_netname_only(self):
+        settings = PortConfig(name='foo', network_name='bar')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.network_name)
+        self.assertTrue(settings.admin_state_up)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.mac_address)
+        self.assertIsNone(settings.ip_addrs)
+        self.assertIsNone(settings.security_groups)
+        self.assertIsNone(settings.allowed_address_pairs)
+        self.assertIsNone(settings.opt_value)
+        self.assertIsNone(settings.opt_name)
+        self.assertIsNone(settings.device_owner)
+        self.assertIsNone(settings.device_id)
+
+    def test_config_with_name_netname_only(self):
+        settings = PortConfig(**{'name': 'foo', 'network_name': 'bar'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.network_name)
+        self.assertTrue(settings.admin_state_up)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.mac_address)
+        self.assertIsNone(settings.ip_addrs)
+        self.assertIsNone(settings.security_groups)
+        self.assertIsNone(settings.port_security_enabled)
+        self.assertIsNone(settings.allowed_address_pairs)
+        self.assertIsNone(settings.opt_value)
+        self.assertIsNone(settings.opt_name)
+        self.assertIsNone(settings.device_owner)
+        self.assertIsNone(settings.device_id)
+
+    def test_all(self):
+        ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}]
+        allowed_address_pairs = {'10.0.0.101', '1234.5678'}
+
+        settings = PortConfig(
+            name='foo', network_name='bar', admin_state_up=False,
+            project_name='foo-project', mac_address='1234', ip_addrs=ip_addrs,
+            security_groups=['foo_grp_id'], port_security_enabled=False,
+            allowed_address_pairs=allowed_address_pairs, opt_value='opt value',
+            opt_name='opt name', device_owner='owner',
+            device_id='device number')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.network_name)
+        self.assertFalse(settings.admin_state_up)
+        self.assertEqual('foo-project', settings.project_name)
+        self.assertEqual('1234', settings.mac_address)
+        self.assertEqual(ip_addrs, settings.ip_addrs)
+        self.assertEqual(1, len(settings.security_groups))
+        self.assertFalse(settings.port_security_enabled)
+        self.assertEqual('foo_grp_id', settings.security_groups[0])
+        self.assertFalse(settings.port_security_enabled)
+        self.assertEqual(allowed_address_pairs, settings.allowed_address_pairs)
+        self.assertEqual('opt value', settings.opt_value)
+        self.assertEqual('opt name', settings.opt_name)
+        self.assertEqual('owner', settings.device_owner)
+        self.assertEqual('device number', settings.device_id)
+
+    def test_config_all(self):
+        ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}]
+        allowed_address_pairs = {'10.0.0.101', '1234.5678'}
+
+        settings = PortConfig(
+            **{'name': 'foo', 'network_name': 'bar', 'admin_state_up': False,
+               'project_name': 'foo-project', 'mac_address': '1234',
+               'ip_addrs': ip_addrs, 'security_groups': ['foo_grp_id'],
+               'port_security_enabled': 'false',
+               'allowed_address_pairs': allowed_address_pairs,
+               'opt_value': 'opt value', 'opt_name': 'opt name',
+               'device_owner': 'owner', 'device_id': 'device number'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.network_name)
+        self.assertFalse(settings.admin_state_up)
+        self.assertEqual('foo-project', settings.project_name)
+        self.assertEqual('1234', settings.mac_address)
+        self.assertEqual(ip_addrs, settings.ip_addrs)
+        self.assertEqual(1, len(settings.security_groups))
+        self.assertFalse(settings.port_security_enabled)
+        self.assertEqual('foo_grp_id', settings.security_groups[0])
+        self.assertEqual(allowed_address_pairs, settings.allowed_address_pairs)
+        self.assertEqual('opt value', settings.opt_value)
+        self.assertEqual('opt name', settings.opt_name)
+        self.assertEqual('owner', settings.device_owner)
+        self.assertEqual('device number', settings.device_id)
diff --git a/snaps/config/tests/project_tests.py b/snaps/config/tests/project_tests.py
new file mode 100644 (file)
index 0000000..0470d83
--- /dev/null
@@ -0,0 +1,69 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.project import ProjectConfig, ProjectConfigError
+
+
+class ProjectConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the ProjectConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(ProjectConfigError):
+            ProjectConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(ProjectConfigError):
+            ProjectConfig(**dict())
+
+    def test_name_only(self):
+        settings = ProjectConfig(name='foo')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('Default', settings.domain_name)
+        self.assertIsNone(settings.description)
+        self.assertTrue(settings.enabled)
+        self.assertEqual(list(), settings.users)
+
+    def test_config_with_name_only(self):
+        settings = ProjectConfig(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('Default', settings.domain_name)
+        self.assertIsNone(settings.description)
+        self.assertTrue(settings.enabled)
+        self.assertEqual(list(), settings.users)
+
+    def test_all(self):
+        users = ['test1', 'test2']
+        settings = ProjectConfig(
+            name='foo', domain='bar', description='foobar', enabled=False,
+            users=users)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.domain_name)
+        self.assertEqual('foobar', settings.description)
+        self.assertFalse(settings.enabled)
+        self.assertEqual(users, settings.users)
+
+    def test_config_all(self):
+        users = ['test1', 'test2']
+        settings = ProjectConfig(
+            **{'name': 'foo', 'domain': 'bar', 'description': 'foobar',
+               'enabled': False, 'users': users})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.domain_name)
+        self.assertEqual('foobar', settings.description)
+        self.assertFalse(settings.enabled)
+        self.assertEqual(users, settings.users)
diff --git a/snaps/config/tests/qos_tests.py b/snaps/config/tests/qos_tests.py
new file mode 100644 (file)
index 0000000..7314c5b
--- /dev/null
@@ -0,0 +1,91 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.qos import QoSConfig, QoSConfigError, Consumer
+
+
+class QoSConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the QoSConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(QoSConfigError):
+            QoSConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(QoSConfigError):
+            QoSConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(QoSConfigError):
+            QoSConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(QoSConfigError):
+            QoSConfig(**{'name': 'foo'})
+
+    def test_invalid_consumer(self):
+        with self.assertRaises(QoSConfigError):
+            QoSConfig(name='foo', consumer='bar')
+
+    def test_config_with_invalid_consumer(self):
+        with self.assertRaises(QoSConfigError):
+            QoSConfig(**{'name': 'foo', 'consumer': 'bar'})
+
+    def test_name_consumer(self):
+        settings = QoSConfig(name='foo', consumer=Consumer.front_end)
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(Consumer.front_end, settings.consumer)
+        self.assertEqual(dict(), settings.specs)
+
+    def test_name_consumer_front_end_strings(self):
+        settings = QoSConfig(name='foo', consumer='front-end')
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(Consumer.front_end, settings.consumer)
+        self.assertEqual(dict(), settings.specs)
+
+    def test_name_consumer_back_end_strings(self):
+        settings = QoSConfig(name='foo', consumer='back-end')
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(Consumer.back_end, settings.consumer)
+        self.assertEqual(dict(), settings.specs)
+
+    def test_name_consumer_both_strings(self):
+        settings = QoSConfig(name='foo', consumer='both')
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(Consumer.both, settings.consumer)
+        self.assertEqual(dict(), settings.specs)
+
+    def test_all(self):
+        specs = {'spec1': 'val1', 'spec2': 'val2'}
+        settings = QoSConfig(name='foo', consumer=Consumer.both, specs=specs)
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(Consumer.both, settings.consumer)
+        self.assertEqual(specs, settings.specs)
+
+    def test_config_all(self):
+        settings = QoSConfig(
+            **{'name': 'foo', 'consumer': 'both', 'specs': {'spec1': 'val1'}})
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual(Consumer.both, settings.consumer)
+        self.assertEqual({'spec1': 'val1'}, settings.specs)
diff --git a/snaps/config/tests/router_tests.py b/snaps/config/tests/router_tests.py
new file mode 100644 (file)
index 0000000..2c8f91f
--- /dev/null
@@ -0,0 +1,98 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.network import PortConfig
+from snaps.config.router import RouterConfig, RouterConfigError
+
+
+class RouterConfigUnitTests(unittest.TestCase):
+    """
+    Class for testing the RouterConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(RouterConfigError):
+            RouterConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(RouterConfigError):
+            RouterConfig(**dict())
+
+    def test_name_only(self):
+        settings = RouterConfig(name='foo')
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.external_gateway)
+        self.assertTrue(settings.admin_state_up)
+        self.assertIsNone(settings.enable_snat)
+        self.assertIsNotNone(settings.internal_subnets)
+        self.assertTrue(isinstance(settings.internal_subnets, list))
+        self.assertEqual(0, len(settings.internal_subnets))
+        self.assertIsNotNone(settings.port_settings)
+        self.assertTrue(isinstance(settings.port_settings, list))
+        self.assertEqual(0, len(settings.port_settings))
+
+    def test_config_with_name_only(self):
+        settings = RouterConfig(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.external_gateway)
+        self.assertTrue(settings.admin_state_up)
+        self.assertIsNone(settings.enable_snat)
+        self.assertIsNotNone(settings.internal_subnets)
+        self.assertTrue(isinstance(settings.internal_subnets, list))
+        self.assertEqual(0, len(settings.internal_subnets))
+        self.assertIsNotNone(settings.port_settings)
+        self.assertTrue(isinstance(settings.port_settings, list))
+        self.assertEqual(0, len(settings.port_settings))
+
+    def test_all(self):
+        port_settings = PortConfig(name='foo', network_name='bar')
+        settings = RouterConfig(
+            name='foo', project_name='bar', external_gateway='foo_gateway',
+            admin_state_up=True, enable_snat=False,
+            internal_subnets=['10.0.0.1/24'], interfaces=[port_settings])
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.project_name)
+        self.assertEqual('foo_gateway', settings.external_gateway)
+        self.assertTrue(settings.admin_state_up)
+        self.assertFalse(settings.enable_snat)
+        self.assertIsNotNone(settings.internal_subnets)
+        self.assertTrue(isinstance(settings.internal_subnets, list))
+        self.assertEqual(1, len(settings.internal_subnets))
+        self.assertEqual(['10.0.0.1/24'], settings.internal_subnets)
+        self.assertEqual([port_settings], settings.port_settings)
+
+    def test_config_all(self):
+        settings = RouterConfig(
+            **{'name': 'foo', 'project_name': 'bar',
+               'external_gateway': 'foo_gateway', 'admin_state_up': True,
+               'enable_snat': False, 'internal_subnets': ['10.0.0.1/24'],
+               'interfaces':
+                   [{'port': {'name': 'foo-port',
+                              'network_name': 'bar-net'}}]})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.project_name)
+        self.assertEqual('foo_gateway', settings.external_gateway)
+        self.assertTrue(settings.admin_state_up)
+        self.assertFalse(settings.enable_snat)
+        self.assertIsNotNone(settings.internal_subnets)
+        self.assertTrue(isinstance(settings.internal_subnets, list))
+        self.assertEqual(1, len(settings.internal_subnets))
+        self.assertEqual(['10.0.0.1/24'], settings.internal_subnets)
+        self.assertEqual([PortConfig(**{'name': 'foo-port',
+                                        'network_name': 'bar-net'})],
+                         settings.port_settings)
diff --git a/snaps/config/tests/security_group_tests.py b/snaps/config/tests/security_group_tests.py
new file mode 100644 (file)
index 0000000..8834836
--- /dev/null
@@ -0,0 +1,187 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.security_group import (
+    Direction, SecurityGroupConfig,  SecurityGroupRuleConfig,
+    SecurityGroupConfigError, Protocol, Ethertype,
+    SecurityGroupRuleConfigError)
+
+
+class SecurityGroupRuleConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the SecurityGroupRuleConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(SecurityGroupRuleConfigError):
+            SecurityGroupRuleConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(SecurityGroupRuleConfigError):
+            SecurityGroupRuleConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(SecurityGroupRuleConfigError):
+            SecurityGroupRuleConfig(sec_grp_name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(SecurityGroupRuleConfigError):
+            SecurityGroupRuleConfig(**{'sec_grp_name': 'foo'})
+
+    def test_name_and_direction(self):
+        settings = SecurityGroupRuleConfig(sec_grp_name='foo',
+                                           direction=Direction.ingress)
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress, settings.direction)
+
+    def test_config_name_and_direction(self):
+        settings = SecurityGroupRuleConfig(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress, settings.direction)
+
+    def test_proto_ah_str(self):
+        settings = SecurityGroupRuleConfig(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 'ah'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress, settings.direction)
+        self.assertEqual(Protocol.ah, settings.protocol)
+
+    def test_proto_ah_value(self):
+        settings = SecurityGroupRuleConfig(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 51})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress, settings.direction)
+        self.assertEqual(Protocol.ah, settings.protocol)
+
+    def test_proto_any(self):
+        settings = SecurityGroupRuleConfig(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 'any'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress, settings.direction)
+        self.assertEqual(Protocol.null, settings.protocol)
+
+    def test_proto_null(self):
+        settings = SecurityGroupRuleConfig(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 'null'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress, settings.direction)
+        self.assertEqual(Protocol.null, settings.protocol)
+
+    def test_all(self):
+        settings = SecurityGroupRuleConfig(
+            sec_grp_name='foo', description='fubar',
+            direction=Direction.egress, remote_group_id='rgi',
+            protocol=Protocol.icmp, ethertype=Ethertype.IPv6, port_range_min=1,
+            port_range_max=2,
+            remote_ip_prefix='prfx')
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual('fubar', settings.description)
+        self.assertEqual(Direction.egress, settings.direction)
+        self.assertEqual('rgi', settings.remote_group_id)
+        self.assertEqual(Protocol.icmp, settings.protocol)
+        self.assertEqual(Ethertype.IPv6, settings.ethertype)
+        self.assertEqual(1, settings.port_range_min)
+        self.assertEqual(2, settings.port_range_max)
+        self.assertEqual('prfx', settings.remote_ip_prefix)
+
+    def test_config_all(self):
+        settings = SecurityGroupRuleConfig(
+            **{'sec_grp_name': 'foo',
+               'description': 'fubar',
+               'direction': 'egress',
+               'remote_group_id': 'rgi',
+               'protocol': 'tcp',
+               'ethertype': 'IPv6',
+               'port_range_min': 1,
+               'port_range_max': 2,
+               'remote_ip_prefix': 'prfx'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual('fubar', settings.description)
+        self.assertEqual(Direction.egress, settings.direction)
+        self.assertEqual('rgi', settings.remote_group_id)
+        self.assertEqual(Protocol.tcp, settings.protocol)
+        self.assertEqual(Ethertype.IPv6, settings.ethertype)
+        self.assertEqual(1, settings.port_range_min)
+        self.assertEqual(2, settings.port_range_max)
+        self.assertEqual('prfx', settings.remote_ip_prefix)
+
+
+class SecurityGroupConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the SecurityGroupConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(SecurityGroupConfigError):
+            SecurityGroupConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(SecurityGroupConfigError):
+            SecurityGroupConfig(**dict())
+
+    def test_name_only(self):
+        settings = SecurityGroupConfig(name='foo')
+        self.assertEqual('foo', settings.name)
+
+    def test_config_with_name_only(self):
+        settings = SecurityGroupConfig(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+
+    def test_invalid_rule(self):
+        rule_setting = SecurityGroupRuleConfig(
+            sec_grp_name='bar', direction=Direction.ingress,
+            description='test_rule_1')
+        with self.assertRaises(SecurityGroupConfigError):
+            SecurityGroupConfig(name='foo', rule_settings=[rule_setting])
+
+    def test_all(self):
+        rule_settings = list()
+        rule_settings.append(SecurityGroupRuleConfig(
+            sec_grp_name='bar', direction=Direction.egress,
+            description='test_rule_1'))
+        rule_settings.append(SecurityGroupRuleConfig(
+            sec_grp_name='bar', direction=Direction.ingress,
+            description='test_rule_2'))
+        settings = SecurityGroupConfig(
+            name='bar', description='fubar', project_name='foo',
+            rule_settings=rule_settings)
+
+        self.assertEqual('bar', settings.name)
+        self.assertEqual('fubar', settings.description)
+        self.assertEqual('foo', settings.project_name)
+        self.assertEqual(rule_settings[0], settings.rule_settings[0])
+        self.assertEqual(rule_settings[1], settings.rule_settings[1])
+
+    def test_config_all(self):
+        settings = SecurityGroupConfig(
+            **{'name': 'bar',
+               'description': 'fubar',
+               'project_name': 'foo',
+               'rules': [
+                   {'sec_grp_name': 'bar', 'direction': 'ingress'}]})
+
+        self.assertEqual('bar', settings.name)
+        self.assertEqual('fubar', settings.description)
+        self.assertEqual('foo', settings.project_name)
+        self.assertEqual(1, len(settings.rule_settings))
+        self.assertEqual('bar', settings.rule_settings[0].sec_grp_name)
+        self.assertEqual(Direction.ingress,
+                         settings.rule_settings[0].direction)
diff --git a/snaps/config/tests/stack_tests.py b/snaps/config/tests/stack_tests.py
new file mode 100644 (file)
index 0000000..773e9c2
--- /dev/null
@@ -0,0 +1,117 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+import snaps
+from snaps.config.stack import StackConfigError, StackConfig
+
+
+class StackConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the StackConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(StackConfigError):
+            StackConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(StackConfigError):
+            StackConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(StackConfigError):
+            StackConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(StackConfigError):
+            StackConfig(**{'name': 'foo'})
+
+    def test_resource_not_list(self):
+        with self.assertRaises(StackConfigError):
+            StackConfig(**{'name': 'foo', 'resource_files': 'bar'})
+
+    def test_config_minimum_template(self):
+        settings = StackConfig(**{'name': 'stack', 'template': 'foo'})
+        self.assertEqual('stack', settings.name)
+        self.assertEqual('foo', settings.template)
+        self.assertIsNone(settings.template_path)
+        self.assertIsNone(settings.resource_files)
+        self.assertIsNone(settings.env_values)
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
+                         settings.stack_create_timeout)
+
+    def test_config_minimum_template_path(self):
+        settings = StackConfig(**{'name': 'stack', 'template_path': 'foo'})
+        self.assertEqual('stack', settings.name)
+        self.assertIsNone(settings.template)
+        self.assertEqual('foo', settings.template_path)
+        self.assertIsNone(settings.resource_files)
+        self.assertIsNone(settings.env_values)
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
+                         settings.stack_create_timeout)
+
+    def test_minimum_template(self):
+        settings = StackConfig(name='stack', template='foo')
+        self.assertEqual('stack', settings.name)
+        self.assertEqual('foo', settings.template)
+        self.assertIsNone(settings.template_path)
+        self.assertIsNone(settings.resource_files)
+        self.assertIsNone(settings.env_values)
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
+                         settings.stack_create_timeout)
+
+    def test_minimum_template_path(self):
+        settings = StackConfig(name='stack', template_path='foo')
+        self.assertEqual('stack', settings.name)
+        self.assertEqual('foo', settings.template_path)
+        self.assertIsNone(settings.template)
+        self.assertIsNone(settings.resource_files)
+        self.assertIsNone(settings.env_values)
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
+                         settings.stack_create_timeout)
+
+    def test_resource(self):
+        settings = StackConfig(
+            name='stack', template_path='foo', resource_files=['foo', 'bar'])
+        self.assertEqual('stack', settings.name)
+        self.assertEqual('foo', settings.template_path)
+        self.assertIsNone(settings.template)
+        self.assertEqual(['foo', 'bar'], settings.resource_files)
+        self.assertIsNone(settings.env_values)
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
+                         settings.stack_create_timeout)
+
+    def test_all(self):
+        env_values = {'foo': 'bar'}
+        settings = StackConfig(
+            name='stack', template='bar', template_path='foo',
+            env_values=env_values, stack_create_timeout=999)
+        self.assertEqual('stack', settings.name)
+        self.assertEqual('bar', settings.template)
+        self.assertEqual('foo', settings.template_path)
+        self.assertEqual(env_values, settings.env_values)
+        self.assertEqual(999, settings.stack_create_timeout)
+
+    def test_config_all(self):
+        env_values = {'foo': 'bar'}
+        settings = StackConfig(
+            **{'name': 'stack', 'template': 'bar', 'template_path': 'foo',
+               'env_values': env_values, 'stack_create_timeout': 999})
+        self.assertEqual('stack', settings.name)
+        self.assertEqual('bar', settings.template)
+        self.assertEqual('foo', settings.template_path)
+        self.assertEqual(env_values, settings.env_values)
+        self.assertEqual(999, settings.stack_create_timeout)
diff --git a/snaps/config/tests/user_tests.py b/snaps/config/tests/user_tests.py
new file mode 100644 (file)
index 0000000..d3d8feb
--- /dev/null
@@ -0,0 +1,84 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.user import UserConfig
+
+
+class UserConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the UserConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(Exception):
+            UserConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(Exception):
+            UserConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(Exception):
+            UserConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(Exception):
+            UserConfig(**{'name': 'foo'})
+
+    def test_name_pass_enabled_str(self):
+        with self.assertRaises(Exception):
+            UserConfig(name='foo', password='bar', enabled='true')
+
+    def test_config_with_name_pass_enabled_str(self):
+        with self.assertRaises(Exception):
+            UserConfig(
+                **{'name': 'foo', 'password': 'bar', 'enabled': 'true'})
+
+    def test_name_pass_only(self):
+        settings = UserConfig(name='foo', password='bar')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.password)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.email)
+        self.assertTrue(settings.enabled)
+
+    def test_config_with_name_pass_only(self):
+        settings = UserConfig(**{'name': 'foo', 'password': 'bar'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.password)
+        self.assertIsNone(settings.project_name)
+        self.assertIsNone(settings.email)
+        self.assertTrue(settings.enabled)
+
+    def test_all(self):
+        settings = UserConfig(
+            name='foo', password='bar', project_name='proj-foo',
+            email='foo@bar.com', enabled=False)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.password)
+        self.assertEqual('proj-foo', settings.project_name)
+        self.assertEqual('foo@bar.com', settings.email)
+        self.assertFalse(settings.enabled)
+
+    def test_config_all(self):
+        settings = UserConfig(
+            **{'name': 'foo', 'password': 'bar', 'project_name': 'proj-foo',
+               'email': 'foo@bar.com', 'enabled': False})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.password)
+        self.assertEqual('proj-foo', settings.project_name)
+        self.assertEqual('foo@bar.com', settings.email)
+        self.assertFalse(settings.enabled)
diff --git a/snaps/config/tests/vm_inst_tests.py b/snaps/config/tests/vm_inst_tests.py
new file mode 100644 (file)
index 0000000..d7fb287
--- /dev/null
@@ -0,0 +1,246 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.network import PortConfig
+from snaps.config.vm_inst import (
+    FloatingIpConfig, VmInstanceConfig, FloatingIpConfigError,
+    VmInstanceConfigError)
+
+
+class VmInstanceConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the VmInstanceConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(VmInstanceConfigError):
+            VmInstanceConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(VmInstanceConfigError):
+            VmInstanceConfig(config=dict())
+
+    def test_name_only(self):
+        with self.assertRaises(VmInstanceConfigError):
+            VmInstanceConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(VmInstanceConfigError):
+            VmInstanceConfig(config={'name': 'foo'})
+
+    def test_name_flavor_only(self):
+        with self.assertRaises(VmInstanceConfigError):
+            VmInstanceConfig(name='foo', flavor='bar')
+
+    def test_config_with_name_flavor_only(self):
+        with self.assertRaises(VmInstanceConfigError):
+            VmInstanceConfig(config={'name': 'foo', 'flavor': 'bar'})
+
+    def test_name_flavor_port_only(self):
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
+        settings = VmInstanceConfig(name='foo', flavor='bar',
+                                    port_settings=[port_settings])
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.flavor)
+        self.assertEqual(1, len(settings.port_settings))
+        self.assertEqual('foo-port', settings.port_settings[0].name)
+        self.assertEqual('bar-net', settings.port_settings[0].network_name)
+        self.assertEqual(0, len(settings.security_group_names))
+        self.assertEqual(0, len(settings.floating_ip_settings))
+        self.assertIsNone(settings.sudo_user)
+        self.assertEqual(900, settings.vm_boot_timeout)
+        self.assertEqual(300, settings.vm_delete_timeout)
+        self.assertEqual(180, settings.ssh_connect_timeout)
+        self.assertEqual(300, settings.cloud_init_timeout)
+        self.assertIsNone(settings.availability_zone)
+        self.assertIsNone(settings.volume_names)
+
+    def test_config_with_name_flavor_port_only(self):
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
+        settings = VmInstanceConfig(
+            **{'name': 'foo', 'flavor': 'bar', 'ports': [port_settings]})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.flavor)
+        self.assertEqual(1, len(settings.port_settings))
+        self.assertEqual('foo-port', settings.port_settings[0].name)
+        self.assertEqual('bar-net', settings.port_settings[0].network_name)
+        self.assertEqual(0, len(settings.security_group_names))
+        self.assertEqual(0, len(settings.floating_ip_settings))
+        self.assertIsNone(settings.sudo_user)
+        self.assertEqual(900, settings.vm_boot_timeout)
+        self.assertEqual(300, settings.vm_delete_timeout)
+        self.assertEqual(180, settings.ssh_connect_timeout)
+        self.assertEqual(300, settings.cloud_init_timeout)
+        self.assertIsNone(settings.availability_zone)
+        self.assertIsNone(settings.volume_names)
+
+    def test_all(self):
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
+        fip_settings = FloatingIpConfig(name='foo-fip', port_name='bar-port',
+                                        router_name='foo-bar-router')
+
+        settings = VmInstanceConfig(
+            name='foo', flavor='bar', port_settings=[port_settings],
+            security_group_names=['sec_grp_1'],
+            floating_ip_settings=[fip_settings], sudo_user='joe',
+            vm_boot_timeout=999, vm_delete_timeout=333,
+            ssh_connect_timeout=111, cloud_init_timeout=998,
+            availability_zone='server name', volume_names=['vol1'])
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.flavor)
+        self.assertEqual(1, len(settings.port_settings))
+        self.assertEqual('foo-port', settings.port_settings[0].name)
+        self.assertEqual('bar-net', settings.port_settings[0].network_name)
+        self.assertEqual(1, len(settings.security_group_names))
+        self.assertEqual('sec_grp_1', settings.security_group_names[0])
+        self.assertEqual(1, len(settings.floating_ip_settings))
+        self.assertEqual('foo-fip', settings.floating_ip_settings[0].name)
+        self.assertEqual('bar-port',
+                         settings.floating_ip_settings[0].port_name)
+        self.assertEqual('foo-bar-router',
+                         settings.floating_ip_settings[0].router_name)
+        self.assertEqual('joe', settings.sudo_user)
+        self.assertEqual(999, settings.vm_boot_timeout)
+        self.assertEqual(333, settings.vm_delete_timeout)
+        self.assertEqual(111, settings.ssh_connect_timeout)
+        self.assertEqual(998, settings.cloud_init_timeout)
+        self.assertEqual('server name', settings.availability_zone)
+        self.assertEqual('vol1', settings.volume_names[0])
+
+    def test_config_all(self):
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
+        fip_settings = FloatingIpConfig(name='foo-fip', port_name='bar-port',
+                                        router_name='foo-bar-router')
+
+        settings = VmInstanceConfig(
+            **{'name': 'foo', 'flavor': 'bar', 'ports': [port_settings],
+               'security_group_names': ['sec_grp_1'],
+               'floating_ips': [fip_settings], 'sudo_user': 'joe',
+               'vm_boot_timeout': 999, 'vm_delete_timeout': 333,
+               'ssh_connect_timeout': 111, 'cloud_init_timeout': 998,
+               'availability_zone': 'server name', 'volume_names': ['vol2']})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('bar', settings.flavor)
+        self.assertEqual(1, len(settings.port_settings))
+        self.assertEqual('foo-port', settings.port_settings[0].name)
+        self.assertEqual('bar-net', settings.port_settings[0].network_name)
+        self.assertEqual(1, len(settings.security_group_names))
+        self.assertEqual(1, len(settings.floating_ip_settings))
+        self.assertEqual('foo-fip', settings.floating_ip_settings[0].name)
+        self.assertEqual('bar-port',
+                         settings.floating_ip_settings[0].port_name)
+        self.assertEqual('foo-bar-router',
+                         settings.floating_ip_settings[0].router_name)
+        self.assertEqual('joe', settings.sudo_user)
+        self.assertEqual(999, settings.vm_boot_timeout)
+        self.assertEqual(333, settings.vm_delete_timeout)
+        self.assertEqual(111, settings.ssh_connect_timeout)
+        self.assertEqual(998, settings.cloud_init_timeout)
+        self.assertEqual('server name', settings.availability_zone)
+        self.assertEqual('vol2', settings.volume_names[0])
+
+
+class FloatingIpConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the FloatingIpConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig(**dict())
+
+    def test_name_only(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig(name='foo')
+
+    def test_config_with_name_only(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig(**{'name': 'foo'})
+
+    def test_name_port_only(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig(name='foo', port_name='bar')
+
+    def test_config_with_name_port_only(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig(**{'name': 'foo', 'port_name': 'bar'})
+
+    def test_name_router_only(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig(name='foo', router_name='bar')
+
+    def test_config_with_name_router_only(self):
+        with self.assertRaises(FloatingIpConfigError):
+            FloatingIpConfig(**{'name': 'foo', 'router_name': 'bar'})
+
+    def test_name_port_router_name_only(self):
+        settings = FloatingIpConfig(name='foo', port_name='foo-port',
+                                    router_name='bar-router')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('foo-port', settings.port_name)
+        self.assertIsNone(settings.port_id)
+        self.assertEqual('bar-router', settings.router_name)
+        self.assertIsNone(settings.subnet_name)
+        self.assertTrue(settings.provisioning)
+
+    def test_name_port_router_id_only(self):
+        settings = FloatingIpConfig(name='foo', port_id='foo-port',
+                                    router_name='bar-router')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('foo-port', settings.port_id)
+        self.assertIsNone(settings.port_name)
+        self.assertEqual('bar-router', settings.router_name)
+        self.assertIsNone(settings.subnet_name)
+        self.assertTrue(settings.provisioning)
+
+    def test_config_with_name_port_router_only(self):
+        settings = FloatingIpConfig(
+            **{'name': 'foo', 'port_name': 'foo-port',
+               'router_name': 'bar-router'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('foo-port', settings.port_name)
+        self.assertIsNone(settings.port_id)
+        self.assertEqual('bar-router', settings.router_name)
+        self.assertIsNone(settings.subnet_name)
+        self.assertTrue(settings.provisioning)
+
+    def test_all(self):
+        settings = FloatingIpConfig(name='foo', port_name='foo-port',
+                                    router_name='bar-router',
+                                    subnet_name='bar-subnet',
+                                    provisioning=False)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('foo-port', settings.port_name)
+        self.assertIsNone(settings.port_id)
+        self.assertEqual('bar-router', settings.router_name)
+        self.assertEqual('bar-subnet', settings.subnet_name)
+        self.assertFalse(settings.provisioning)
+
+    def test_config_all(self):
+        settings = FloatingIpConfig(
+            **{'name': 'foo', 'port_name': 'foo-port',
+               'router_name': 'bar-router', 'subnet_name': 'bar-subnet',
+               'provisioning': False})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('foo-port', settings.port_name)
+        self.assertIsNone(settings.port_id)
+        self.assertEqual('bar-router', settings.router_name)
+        self.assertEqual('bar-subnet', settings.subnet_name)
+        self.assertFalse(settings.provisioning)
diff --git a/snaps/config/tests/volume_tests.py b/snaps/config/tests/volume_tests.py
new file mode 100644 (file)
index 0000000..b4b54bd
--- /dev/null
@@ -0,0 +1,91 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.volume import VolumeConfigError, VolumeConfig
+
+
+class VolumeConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the VolumeConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(VolumeConfigError):
+            VolumeConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(VolumeConfigError):
+            VolumeConfig(**dict())
+
+    def test_name_only(self):
+        settings = VolumeConfig(name='foo')
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.description)
+        self.assertEquals(1, settings.size)
+        self.assertIsNone(settings.image_name)
+        self.assertIsNone(settings.type_name)
+        self.assertIsNone(settings.availability_zone)
+        self.assertFalse(settings.multi_attach)
+
+    def test_config_with_name_only(self):
+        settings = VolumeConfig(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.description)
+        self.assertEquals(1, settings.size)
+        self.assertIsNone(settings.image_name)
+        self.assertIsNone(settings.type_name)
+        self.assertIsNone(settings.availability_zone)
+        self.assertFalse(settings.multi_attach)
+
+    def test_all_strings(self):
+        settings = VolumeConfig(
+            name='foo', description='desc', size='2', image_name='image',
+            type_name='type', availability_zone='zone1', multi_attach='true')
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual(2, settings.size)
+        self.assertEqual('image', settings.image_name)
+        self.assertEqual('type', settings.type_name)
+        self.assertEqual('zone1', settings.availability_zone)
+        self.assertTrue(settings.multi_attach)
+
+    def test_all_correct_type(self):
+        settings = VolumeConfig(
+            name='foo', description='desc', size=2, image_name='image',
+            type_name='bar', availability_zone='zone1', multi_attach=True)
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual(2, settings.size)
+        self.assertEqual('image', settings.image_name)
+        self.assertEqual('bar', settings.type_name)
+        self.assertEqual('zone1', settings.availability_zone)
+        self.assertTrue(settings.multi_attach)
+
+    def test_config_all(self):
+        settings = VolumeConfig(
+            **{'name': 'foo', 'description': 'desc', 'size': '2',
+               'image_name': 'foo', 'type_name': 'bar',
+               'availability_zone': 'zone1', 'multi_attach': 'true'})
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual(2, settings.size)
+        self.assertEqual('foo', settings.image_name)
+        self.assertEqual('bar', settings.type_name)
+        self.assertEqual('zone1', settings.availability_zone)
+        self.assertTrue(settings.multi_attach)
diff --git a/snaps/config/tests/volume_type_tests.py b/snaps/config/tests/volume_type_tests.py
new file mode 100644 (file)
index 0000000..7b4fb1b
--- /dev/null
@@ -0,0 +1,91 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.volume_type import (
+    VolumeTypeConfig,  VolumeTypeEncryptionConfig, ControlLocation,
+    VolumeTypeConfigError)
+
+
+class VolumeTypeConfigUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the VolumeTypeConfig class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(VolumeTypeConfigError):
+            VolumeTypeConfig()
+
+    def test_empty_config(self):
+        with self.assertRaises(VolumeTypeConfigError):
+            VolumeTypeConfig(**dict())
+
+    def test_name_only(self):
+        settings = VolumeTypeConfig(name='foo')
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.description)
+        self.assertIsNone(settings.qos_spec_name)
+        self.assertIsNone(settings.encryption)
+        self.assertFalse(settings.public)
+
+    def test_config_with_name_only(self):
+        settings = VolumeTypeConfig(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.description)
+        self.assertIsNone(settings.qos_spec_name)
+        self.assertIsNone(settings.encryption)
+        self.assertFalse(settings.public)
+
+    def test_all(self):
+        encryption_settings = VolumeTypeEncryptionConfig(
+            name='foo', provider_class='bar',
+            control_location=ControlLocation.back_end)
+        settings = VolumeTypeConfig(
+            name='foo', description='desc', encryption=encryption_settings,
+            qos_spec_name='spec_name', public=True)
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual('spec_name', settings.qos_spec_name)
+        self.assertEqual(encryption_settings, settings.encryption)
+        self.assertTrue(True, settings.public)
+
+    def test_all_string(self):
+        encryption_settings = {
+            'name': 'foo', 'provider_class': 'bar',
+            'control_location': 'back-end'}
+        settings = VolumeTypeConfig(
+            name='foo', description='desc', encryption=encryption_settings,
+            qos_spec_name='spec_name', public='true')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual('spec_name', settings.qos_spec_name)
+        self.assertEqual(VolumeTypeEncryptionConfig(**encryption_settings),
+                         settings.encryption)
+        self.assertTrue(settings.public)
+
+    def test_config_all(self):
+        encryption_settings = {
+            'name': 'foo', 'provider_class': 'bar',
+            'control_location': 'back-end'}
+        settings = VolumeTypeConfig(
+            **{'name': 'foo', 'description': 'desc',
+               'encryption': encryption_settings,
+               'qos_spec_name': 'spec_name', 'public': 'false'})
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual('spec_name', settings.qos_spec_name)
+        self.assertEqual(VolumeTypeEncryptionConfig(**encryption_settings),
+                         settings.encryption)
+        self.assertFalse(settings.public)
diff --git a/snaps/config/user.py b/snaps/config/user.py
new file mode 100644 (file)
index 0000000..fcc8fac
--- /dev/null
@@ -0,0 +1,59 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class UserConfig(object):
+    """
+    Class for holding user configurations
+    """
+    def __init__(self, **kwargs):
+
+        """
+        Constructor
+        :param name: the user's name (required)
+        :param password: the user's password (required)
+        :param project_name: the user's primary project name (optional)
+        :param domain_name: the user's domain name (default='Default'). For v3
+                            APIs
+        :param email: the user's email address (optional)
+        :param enabled: denotes whether or not the user is enabled
+                        (default True)
+        :param roles: dict where key is the role's name and value is the name
+                      of the project to associate with the role (optional)
+        """
+
+        self.name = kwargs.get('name')
+        self.password = kwargs.get('password')
+        self.project_name = kwargs.get('project_name')
+        self.email = kwargs.get('email')
+        self.domain_name = kwargs.get('domain_name', 'Default')
+        self.enabled = kwargs.get('enabled', True)
+        self.roles = kwargs.get('roles', dict())
+
+        if not self.name or not self.password:
+            raise UserConfigException(
+                'The attributes name and password are required for '
+                'UserConfig')
+
+        if not isinstance(self.enabled, bool):
+            raise UserConfigException(
+                'The attribute enabled must be of type boolean')
+
+
+class UserConfigException(Exception):
+    """
+    Raised when there is a problem with the values set in the UserConfig
+    class
+    """
diff --git a/snaps/config/vm_inst.py b/snaps/config/vm_inst.py
new file mode 100644 (file)
index 0000000..6a63e33
--- /dev/null
@@ -0,0 +1,166 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from snaps.config.network import PortConfig
+
+
+class VmInstanceConfig(object):
+    """
+    Class responsible for holding configuration setting for a VM Instance
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the name of the VM
+        :param flavor: the VM's flavor name
+        :param port_settings: the port configuration settings (required)
+        :param security_group_names: a set of names of the security groups to
+                                     add to the VM
+        :param floating_ip_settings: the floating IP configuration settings
+        :param sudo_user: the sudo user of the VM that will override the
+                          instance_settings.image_user when trying to
+                          connect to the VM
+        :param vm_boot_timeout: the amount of time a thread will wait
+                                for an instance to boot
+        :param vm_delete_timeout: the amount of time a thread will wait
+                                  for an instance to be deleted
+        :param ssh_connect_timeout: the amount of time a thread will wait
+                                    to obtain an SSH connection to a VM
+        :param cloud_init_timeout: the amount of time a thread will wait for
+                                   cloud-init to complete
+        :param availability_zone: the name of the compute server on which to
+                                  deploy the VM (optional)
+        :param volume_names: a list of the names of the volume to attach
+                             (optional)
+        :param userdata: the string contents of any optional cloud-init script
+                         to execute after the VM has been activated.
+                         This value may also contain a dict who's key value
+                         must contain the key 'cloud-init_file' which denotes
+                         the location of some file containing the cloud-init
+                         script
+        """
+        self.name = kwargs.get('name')
+        self.flavor = kwargs.get('flavor')
+        self.sudo_user = kwargs.get('sudo_user')
+        self.userdata = kwargs.get('userdata')
+
+        self.port_settings = list()
+        port_settings = kwargs.get('ports')
+        if not port_settings:
+            port_settings = kwargs.get('port_settings')
+        if port_settings:
+            for port_setting in port_settings:
+                if isinstance(port_setting, dict):
+                    self.port_settings.append(PortConfig(**port_setting))
+                elif isinstance(port_setting, PortConfig):
+                    self.port_settings.append(port_setting)
+
+        if kwargs.get('security_group_names'):
+            if isinstance(kwargs['security_group_names'], list):
+                self.security_group_names = kwargs['security_group_names']
+            elif isinstance(kwargs['security_group_names'], set):
+                self.security_group_names = kwargs['security_group_names']
+            elif isinstance(kwargs['security_group_names'], str):
+                self.security_group_names = [kwargs['security_group_names']]
+            else:
+                raise VmInstanceConfigError(
+                    'Invalid data type for security_group_names attribute')
+        else:
+            self.security_group_names = set()
+
+        self.floating_ip_settings = list()
+        floating_ip_settings = kwargs.get('floating_ips')
+        if not floating_ip_settings:
+            floating_ip_settings = kwargs.get('floating_ip_settings')
+        if floating_ip_settings:
+            for floating_ip_config in floating_ip_settings:
+                if isinstance(floating_ip_config, FloatingIpConfig):
+                    self.floating_ip_settings.append(floating_ip_config)
+                else:
+                    self.floating_ip_settings.append(FloatingIpConfig(
+                        **floating_ip_config['floating_ip']))
+
+        self.vm_boot_timeout = kwargs.get('vm_boot_timeout', 900)
+        self.vm_delete_timeout = kwargs.get('vm_delete_timeout', 300)
+        self.ssh_connect_timeout = kwargs.get('ssh_connect_timeout', 180)
+        self.cloud_init_timeout = kwargs.get('cloud_init_timeout', 300)
+        self.availability_zone = kwargs.get('availability_zone')
+        self.volume_names = kwargs.get('volume_names')
+
+        if self.volume_names and not isinstance(self.volume_names, list):
+            raise VmInstanceConfigError('volume_names must be a list')
+
+        if not self.name or not self.flavor:
+            raise VmInstanceConfigError(
+                'Instance configuration requires the attributes: name, flavor')
+
+        if len(self.port_settings) == 0:
+            raise VmInstanceConfigError(
+                'Instance configuration requires port settings (aka. NICS)')
+
+
+class FloatingIpConfig(object):
+    """
+    Class responsible for holding configuration settings for a floating IP
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the name of the floating IP
+        :param port_name: the name of the router to the external network
+        :param router_name: the name of the router to the external network
+        :param subnet_name: the name of the subnet on which to attach the
+                            floating IP
+        :param provisioning: when true, this floating IP can be used for
+                             provisioning
+
+        TODO - provisioning flag is a hack as I have only observed a single
+        Floating IPs that actually works on an instance. Multiple floating IPs
+        placed on different subnets from the same port are especially
+        troublesome as you cannot predict which one will actually connect.
+        For now, it is recommended not to setup multiple floating IPs on an
+        instance unless absolutely necessary.
+        """
+        self.name = kwargs.get('name')
+        self.port_name = kwargs.get('port_name')
+        self.port_id = kwargs.get('port_id')
+        self.router_name = kwargs.get('router_name')
+        self.subnet_name = kwargs.get('subnet_name')
+        if kwargs.get('provisioning') is not None:
+            self.provisioning = kwargs['provisioning']
+        else:
+            self.provisioning = True
+
+        # if not self.name or not self.port_name or not self.router_name:
+        if not self.name or not self.router_name:
+            raise FloatingIpConfigError(
+                'The attributes name, port_name and router_name are required')
+
+        if not self.port_name and not self.port_id:
+            raise FloatingIpConfigError(
+                'The attributes port_name or port_id are required')
+
+
+class VmInstanceConfigError(Exception):
+    """
+    Exception to be thrown when an VM instance settings are incorrect
+    """
+
+
+class FloatingIpConfigError(Exception):
+    """
+    Exception to be thrown when an VM instance settings are incorrect
+    """
diff --git a/snaps/config/volume.py b/snaps/config/volume.py
new file mode 100644 (file)
index 0000000..a31e8f5
--- /dev/null
@@ -0,0 +1,56 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from neutronclient.common.utils import str2bool
+
+
+class VolumeConfig(object):
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the volume's name (required)
+        :param description: the volume's name (optional)
+        :param size: the volume's size in GB (default 1)
+        :param image_name: when a glance image is used for the image source
+                           (optional)
+        :param type_name: the associated volume's type name (optional)
+        :param availability_zone: the name of the compute server on which to
+                                  deploy the volume (optional)
+        :param multi_attach: when true, volume can be attached to more than one
+                             server (default False)
+        """
+
+        self.name = kwargs.get('name')
+        self.description = kwargs.get('description')
+        self.size = int(kwargs.get('size', 1))
+        self.image_name = kwargs.get('image_name')
+        self.type_name = kwargs.get('type_name')
+        self.availability_zone = kwargs.get('availability_zone')
+
+        if kwargs.get('multi_attach'):
+            self.multi_attach = str2bool(str(kwargs.get('multi_attach')))
+        else:
+            self.multi_attach = False
+
+        if not self.name:
+            raise VolumeConfigError("The attribute name is required")
+
+
+class VolumeConfigError(Exception):
+    """
+    Exception to be thrown when an volume settings are incorrect
+    """
+
+    def __init__(self, message):
+        Exception.__init__(self, message)
diff --git a/snaps/config/volume_type.py b/snaps/config/volume_type.py
new file mode 100644 (file)
index 0000000..35ca1d4
--- /dev/null
@@ -0,0 +1,146 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import enum
+from neutronclient.common.utils import str2bool
+
+
+class VolumeTypeConfig(object):
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the volume's name (required)
+        :param description: the volume's name (optional)
+        :param encryption: VolumeTypeEncryptionConfig (optional)
+        :param qos_spec_name: name of the QoS Spec to associate (optional)
+        :param public: volume visibility where True denotes global
+                       (default - False)
+
+        TODO - Implement project_access parameter that will associate this
+        VolumeType to a list of project names
+        """
+
+        self.name = kwargs.get('name')
+        self.description = kwargs.get('description')
+        self.qos_spec_name = kwargs.get('qos_spec_name')
+
+        if 'encryption' in kwargs:
+            if isinstance(kwargs['encryption'], dict):
+                self.encryption = VolumeTypeEncryptionConfig(
+                    **kwargs['encryption'])
+            elif isinstance(kwargs['encryption'],
+                            VolumeTypeEncryptionConfig):
+                self.encryption = kwargs['encryption']
+        else:
+            self.encryption = None
+
+        if 'public' in kwargs:
+            if isinstance(kwargs['public'], str):
+                self.public = str2bool(kwargs['public'])
+            else:
+                self.public = kwargs['public']
+        else:
+            self.public = False
+
+        if not self.name:
+            raise VolumeTypeConfigError("The attribute name is required")
+
+    def __eq__(self, other):
+        return (self.name == other.name
+                and self.description == other.description
+                and self.qos_spec_name == other.qos_spec_name
+                and self.encryption == other.encryption
+                and self.public == other.public)
+
+
+class ControlLocation(enum.Enum):
+    """
+    QoS Specification consumer types
+    """
+    front_end = 'front-end'
+    back_end = 'back-end'
+
+
+class VolumeTypeEncryptionConfig(object):
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param name: the volume's name (required)
+        :param provider_class: the volume's provider class (e.g. LuksEncryptor)
+        :param control_location: the notional service where encryption is
+                                 performed (e.g., front-end=Nova). The default
+                                 value is 'front-end.'
+        :param cipher: the encryption algorithm/mode to use
+                       (e.g., aes-xts-plain64). If the field is left empty,
+                       the provider default will be used
+        :param key_size: the size of the encryption key, in bits
+                         (e.g., 128, 256). If the field is left empty, the
+                         provider default will be used
+        """
+
+        self.name = kwargs.get('name')
+        self.provider_class = kwargs.get('provider_class')
+        self.control_location = kwargs.get('control_location')
+        if kwargs.get('control_location'):
+            self.control_location = map_control_location(
+                kwargs['control_location'])
+        else:
+            self.control_location = None
+
+        self.cipher = kwargs.get('cipher')
+        self.key_size = kwargs.get('key_size')
+
+        if (not self.name or not self.provider_class
+                or not self.control_location):
+            raise VolumeTypeConfigError(
+                'The attributes name, provider_class, and control_location '
+                'are required')
+
+    def __eq__(self, other):
+        return (self.name == other.name
+                and self.provider_class == other.provider_class
+                and self.control_location == other.control_location
+                and self.cipher == other.cipher
+                and self.key_size == other.key_size)
+
+
+def map_control_location(control_location):
+    """
+    Takes a the protocol value maps it to the Consumer enum. When None return
+    None
+    :param control_location: the value to map to the Enum
+    :return: a ControlLocation enum object
+    :raise: Exception if control_location parameter is invalid
+    """
+    if not control_location:
+        return None
+    elif isinstance(control_location, ControlLocation):
+        return control_location
+    else:
+        proto_str = str(control_location)
+        if proto_str == 'front-end':
+            return ControlLocation.front_end
+        elif proto_str == 'back-end':
+            return ControlLocation.back_end
+        else:
+            raise VolumeTypeConfigError('Invalid Consumer - ' + proto_str)
+
+
+class VolumeTypeConfigError(Exception):
+    """
+    Exception to be thrown when an volume settings are incorrect
+    """
+
+    def __init__(self, message):
+        Exception.__init__(self, message)
diff --git a/snaps/domain/cluster_template.py b/snaps/domain/cluster_template.py
new file mode 100644 (file)
index 0000000..83892b2
--- /dev/null
@@ -0,0 +1,175 @@
+# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class ClusterTemplate(object):
+    """
+    Class for OpenStack cluster template domain object
+    """
+
+    def __init__(self, **kwargs):
+        """
+        Constructor
+        :param id: the cluster template's UUID
+        :param name: the cluster template's name
+        :param image: name or ID of the base image in Glance used to boot the
+                      cluster's servers. The image must have the attribute
+                      'os-distro' defined as appropriate for the cluster
+                      driver
+        :param keypair: name or ID of the keypair to gain cluster machine
+                        access
+        :param network_driver: The name of a network driver for providing the
+                               networks for the containers. Note that this is
+                               different and separate from the Neutron network
+                               for the bay/cluster. The operation and
+                               networking model are specific to the particular
+                               driver
+        :param external_net: name or IDof the external Neutron network to
+                             provide connectivity to the cluster
+        :param floating_ip_enabled: Whether enable or not using the floating IP
+                                    of cloud provider. Some cloud providers
+                                    used floating IP, some used public IP,
+                                    thus Magnum provide this option for
+                                    specifying the choice of using floating IP
+        :param docker_volume_size: The size in GB for the local storage on each
+                                   server for the Docker daemon to cache the
+                                   images and host the containers. Cinder
+                                   volumes provide the storage. The default is
+                                   25 GB. For the devicemapper storage driver,
+                                   the minimum value is 3GB. For the overlay
+                                   storage driver, the minimum value is 1GB.
+        :param server_type: server type string
+        :param flavor: name or ID of the nova flavor for booting the node
+                       servers
+        :param master_flavor: name or ID of the nova flavor of the master node
+                              for this cluster
+        :param coe: ContainerOrchestrationEngine enum instance
+        :param fixed_net: name of a Neutron network to provide connectivity
+                          to the internal network for the cluster
+        :param fixed_subnet: Fixed subnet that are using to allocate network
+                             address for nodes in bay/cluster
+        :param registry_enabled: Docker images by default are pulled from the
+                                 public Docker registry, but in some cases,
+                                 users may want to use a private registry.
+                                 This option provides an alternative registry
+                                 based on the Registry V2: Magnum will create a
+                                 local registry in the bay/cluster backed by
+                                 swift to host the images
+        :param insecure_registry: The URL pointing to the user's own private
+                                  insecure docker registry to deploy and run
+                                  docker containers
+        :param docker_storage_driver: DockerStorageDriver enum instance to
+                                      manage storage for the images and
+                                      container's writable layer
+        :param dns_nameserver: The DNS nameserver for the servers and
+                               containers in the bay/cluster to use.
+                               This is configured in the private Neutron
+                               network for the bay/cluster.
+        :param public: denotes whether or not the cluster type is public
+        :param tls_disabled: denotes whether or not TLS should be enabled
+        :param http_proxy: host:port for a proxy to use when direct HTTP
+                           access from the servers to sites on the external
+                           internet is blocked
+        :param https_proxy: host:port for a proxy to use when direct HTTPS
+                            access from the servers to sites on the external
+                            internet is blocked
+        :param no_proxy: comma separated list of IPs that should not be
+                         redirected through the proxy
+        :param volume_driver: The name of a volume driver for managing the
+                              persistent storage for the containers. The
+                              functionality supported are specific to the
+                              driver
+        :param master_lb_enabled: Since multiple masters may exist in a
+                                  bay/cluster, a Neutron load balancer is
+                                  created to provide the API endpoint for the
+                                  bay/cluster and to direct requests to the
+                                  masters. In some cases, such as when the
+                                  LBaaS service is not available, this option
+                                  can be set to false to create a bay/cluster
+                                  without the load balancer. In this case, one
+                                  of the masters will serve as the API endpoint
+        :param labels: Arbitrary labels in the form of a dict. The accepted
+                       keys and valid values are defined in the bay/cluster
+                       drivers. They are used as a way to pass additional
+                       parameters that are specific to a bay/cluster driver.
+        """
+        self.id = kwargs.get('id')
+        self.name = kwargs.get('name')
+        self.image = kwargs.get('image')
+        self.keypair = kwargs.get('keypair')
+        self.network_driver = kwargs.get('network_driver')
+        self.external_net = kwargs.get('external_net')
+        self.floating_ip_enabled = kwargs.get('floating_ip_enabled')
+        self.docker_volume_size = int(kwargs.get('docker_volume_size', 3))
+        self.server_type = kwargs.get('server_type')
+        self.flavor = kwargs.get('flavor')
+        self.master_flavor = kwargs.get('master_flavor')
+        self.coe = kwargs.get('coe')
+        self.fixed_net = kwargs.get('fixed_net')
+        self.fixed_subnet = kwargs.get('fixed_subnet')
+        self.registry_enabled = kwargs.get('registry_enabled')
+        self.insecure_registry = kwargs.get('insecure_registry')
+        self.docker_storage_driver = kwargs.get('docker_storage_driver')
+        self.dns_nameserver = kwargs.get('dns_nameserver')
+        self.public = kwargs.get('public', False)
+        self.tls_disabled = kwargs.get('tls_disabled')
+        self.http_proxy = kwargs.get('http_proxy')
+        self.https_proxy = kwargs.get('https_proxy')
+        self.no_proxy = kwargs.get('no_proxy')
+        self.volume_driver = kwargs.get('volume_driver')
+        self.master_lb_enabled = kwargs.get('master_lb_enabled', True)
+        self.labels = kwargs.get('labels')
+
+    def __eq__(self, other):
+        labels_eq = False
+        if (self.labels and isinstance(self.labels, dict)
+                and len(self.labels) == 0):
+            if (other.labels and isinstance(other.labels, dict)
+                    and len(other.labels) == 0):
+                labels_eq = True
+        elif not self.labels:
+            if (not other.labels or
+                    (isinstance(other.labels, dict)
+                     and len(other.labels) == 0)):
+                labels_eq = True
+        else:
+            labels_eq = self.labels == other.labels
+
+        return (self.name == other.name
+                and self.id == other.id
+                and self.image == other.image
+                and self.keypair == other.keypair
+                and self.network_driver == other.network_driver
+                and self.external_net == other.external_net
+                and self.floating_ip_enabled == other.floating_ip_enabled
+                and self.docker_volume_size == other.docker_volume_size
+                and self.server_type == other.server_type
+                and self.flavor == other.flavor
+                and self.master_flavor == other.master_flavor
+                and self.coe == other.coe
+                and self.fixed_net == other.fixed_net
+                and self.fixed_subnet == other.fixed_subnet
+                and self.registry_enabled == other.registry_enabled
+                and self.insecure_registry == other.insecure_registry
+                and self.docker_storage_driver == other.docker_storage_driver
+                and self.dns_nameserver == other.dns_nameserver
+                and self.public == other.public
+                and self.tls_disabled == other.tls_disabled
+                and self.http_proxy == other.http_proxy
+                and self.https_proxy == other.https_proxy
+                and self.no_proxy == other.no_proxy
+                and self.volume_driver == other.volume_driver
+                and self.master_lb_enabled == other.master_lb_enabled
+                and labels_eq)
index 035ca64..bf84cf4 100644 (file)
@@ -23,7 +23,7 @@ class Flavor:
         """
         Constructor
         :param name: the flavor's name
-        :param flavor_id: the flavor's id
+        :param flavor_id or id: the flavor's id
         :param ram: the flavor's RAM in MB
         :param disk: the flavor's disk size in GB
         :param vcpus: the flavor's number of virtual CPUs
@@ -33,11 +33,16 @@ class Flavor:
         :param is_public: denotes if flavor can be used by other projects
         """
         self.name = kwargs.get('name')
-        self.id = kwargs.get('id')
+        self.id = kwargs.get('flavor_id', kwargs.get('id'))
         self.ram = kwargs.get('ram')
         self.disk = kwargs.get('disk')
         self.vcpus = kwargs.get('vcpus')
         self.ephemeral = kwargs.get('ephemeral')
-        self.swap = kwargs.get('swap')
+
+        if kwargs.get('swap'):
+            self.swap = int(kwargs.get('swap'))
+        else:
+            self.swap = None
+
         self.rxtx_factor = kwargs.get('rxtx_factor')
         self.is_public = kwargs.get('is_public')
index 9cc1dd1..2d02966 100644 (file)
@@ -22,6 +22,13 @@ class Network:
     def __init__(self, **kwargs):
         """
         Constructor
+        :param name: the network's name
+        :param id: the network's ID
+        :param admin_state_up: T/F - network is up when True
+        :param shared: T/F - network can be shared amongst other project's
+        :param external: T/F - network is deemed to be external
+        :param type: vlan, vxlan, flat, etc.
+        :param subnets: list of Subnet objects
         """
         self.name = kwargs.get('name')
         self.id = kwargs.get('id')
@@ -29,12 +36,15 @@ class Network:
         self.shared = kwargs.get('shared')
         self.external = kwargs.get('router:external', kwargs.get('external'))
         self.type = kwargs.get('provider:network_type', kwargs.get('type'))
+        self.subnets = kwargs.get('subnets', list())
 
     def __eq__(self, other):
         return (self.name == other.name and self.id == other.id and
                 self.admin_state_up == other.admin_state_up and
                 self.shared == other.shared and
-                self.external == other.external and self.type == other.type)
+                self.external == other.external and
+                self.type == other.type and
+                self.subnets == other.subnets)
 
 
 class Subnet:
@@ -45,9 +55,23 @@ class Subnet:
     def __init__(self, **kwargs):
         """
         Constructor
+        :param name: the network's name
+        :param id: the subnet's ID
+        :param network_id: the network's ID
+        :param cidr: the CIDR
+        :param ip_version: the IP version
+        :param gateway_ip: the IP of the gateway
+        :param enable_dhcp: T/F if DHCP is enabled
+        :param dns_nameservers: list of DNS server IPs
+        :param host_routes: routes as returned in a dict by Neutron
+        :param ipv6_ra_mode: IPv6 RA Mode
+        :param ipv6_address_mode: IPv6 Address Mode
+        :param start: start IP address pool
+        :param end: end IP address pool
         """
         self.name = kwargs.get('name')
         self.id = kwargs.get('id')
+        self.network_id = kwargs.get('network_id')
         self.cidr = kwargs.get('cidr')
         self.ip_version = kwargs.get('ip_version')
         self.gateway_ip = kwargs.get('gateway_ip')
@@ -71,6 +95,7 @@ class Subnet:
     def __eq__(self, other):
         return (self.name == other.name and
                 self.id == other.id and
+                self.network_id == other.network_id and
                 self.cidr == other.cidr and
                 self.ip_version == other.ip_version and
                 self.gateway_ip == other.gateway_ip and
@@ -134,20 +159,46 @@ class Router:
         Constructor
         :param name: the router's name
         :param id: the router's id
+        :param status: the router's status
+        :param tenant_id: the router's project/tenant ID
+        :param admin_state_up: Router is up when True
+        :param external_gateway_info: dict() for populating external_network_id
+                                      and external_fixed_ips
+                   external_network_id: ID of the external network to route
+                                        in dict under key 'external_fixed_ips'
+                   external_fixed_ips: List IP addresses associated with the
+                                       external_network_id found in dict under
+                                       key 'network_id'
+        :param port_subnets: list of tuples where #1 is the Port domain object
+                             and #2 is a list of associated Subnet domain
+                             objects
         """
         self.name = kwargs.get('name')
         self.id = kwargs.get('id')
         self.status = kwargs.get('status')
         self.tenant_id = kwargs.get('tenant_id')
         self.admin_state_up = kwargs.get('admin_state_up')
-        self.external_gateway_info = kwargs.get('external_gateway_info')
+        self.port_subnets = kwargs.get('port_subnets')
+
+        if (kwargs.get('external_gateway_info') and
+                isinstance(kwargs.get('external_gateway_info'), dict) and
+                kwargs.get('external_gateway_info').get('external_fixed_ips')):
+            gateway_info = kwargs.get('external_gateway_info')
+
+            self.external_network_id = gateway_info.get('network_id')
+            self.external_fixed_ips = gateway_info.get('external_fixed_ips')
+        else:
+            self.external_fixed_ips = kwargs.get('external_fixed_ips', None)
+            self.external_network_id = kwargs.get('external_network_id', None)
 
     def __eq__(self, other):
         return (self.name == other.name and self.id == other.id and
                 self.status == other.status and
                 self.tenant_id == other.tenant_id and
                 self.admin_state_up == other.admin_state_up and
-                self.external_gateway_info == other.external_gateway_info)
+                self.external_network_id == other.external_network_id and
+                self.external_fixed_ips == other.external_fixed_ips and
+                self.port_subnets == other.port_subnets)
 
 
 class InterfaceRouter:
@@ -178,15 +229,29 @@ class SecurityGroup:
         Constructor
         :param name: the security group's name
         :param id: the security group's id
+        :param description: the security group's description
+        :param project_id: the security group's project_id
+        :param rules: list of SecurityGroupRule objects associated to this
         """
         self.name = kwargs.get('name')
         self.id = kwargs.get('id')
         self.description = kwargs.get('description')
         self.project_id = kwargs.get('project_id', kwargs.get('tenant_id'))
 
+        self.rules = list()
+        if kwargs.get('rules') and isinstance(kwargs.get('rules'), list):
+            for rule in kwargs.get('rules'):
+                if isinstance(rule, SecurityGroupRule):
+                    self.rules.append(rule)
+                else:
+                    self.rules.append(SecurityGroupRule(**rule))
+
     def __eq__(self, other):
-        return (self.name == other.name and self.id == other.id and
-                self.project_id == other.project_id)
+        return (self.name == other.name and
+                self.id == other.id and
+                self.description == other.description and
+                self.project_id == other.project_id and
+                self.rules == other.rules)
 
 
 class SecurityGroupRule:
@@ -198,7 +263,7 @@ class SecurityGroupRule:
         """
         Constructor
         :param id: the security group rule's id
-        :param sec_grp_id: the ID of the associated security group
+        :param security_group_id: the ID of the associated security group
         :param description: the security group rule's description
         :param direction: the security group rule's direction
         :param ethertype: the security group rule's ethertype
index 543c78b..080ab17 100644 (file)
@@ -37,14 +37,21 @@ class Resource:
     """
     SNAPS domain object for a resource created by a heat template
     """
-    def __init__(self, resource_type, resource_id):
+    def __init__(self, name, resource_type, resource_id, status,
+                 status_reason):
         """
         Constructor
-        :param resource_type: the type
+        :param name: the resource's name
+        :param resource_type: the resource's type
         :param resource_id: the ID attached to the resource of the given type
+        :param status: the resource's status code
+        :param status_reason: the resource's status code reason
         """
+        self.name = name
         self.type = resource_type
         self.id = resource_id
+        self.status = status
+        self.status_reason = status_reason
 
 
 class Output:
diff --git a/snaps/domain/test/cluster_template_tests.py b/snaps/domain/test/cluster_template_tests.py
new file mode 100644 (file)
index 0000000..76e5663
--- /dev/null
@@ -0,0 +1,109 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from snaps.config.cluster_template import (
+    ContainerOrchestrationEngine, ServerType, DockerStorageDriver)
+from snaps.domain.cluster_template import ClusterTemplate
+
+
+class ClusterTemplateUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the ClusterTypeConfig class
+    """
+    def test_all_named(self):
+        labels = {'foo': 'bar'}
+        config = ClusterTemplate(
+            id='tmplt-id', name='foo', image='bar', keypair='keys',
+            network_driver='driver', external_net='external',
+            docker_volume_size=99, server_type=ServerType.baremetal.value,
+            flavor='testFlavor', master_flavor='masterFlavor',
+            coe=ContainerOrchestrationEngine.kubernetes.value,
+            fixed_net='fixedNet', fixed_subnet='fixedSubnet',
+            registry_enabled=False,
+            docker_storage_driver=DockerStorageDriver.overlay.value,
+            dns_nameserver='8.8.4.4', public=True, tls=False,
+            http_proxy='http://foo:8080', https_proxy='https://foo:443',
+            no_proxy='foo,bar', volume_driver='volDriver',
+            master_lb_enabled=False, labels=labels)
+        self.assertIsNotNone(config)
+        self.assertEqual('tmplt-id', config.id)
+        self.assertEqual('foo', config.name)
+        self.assertEqual('bar', config.image)
+        self.assertEqual('keys', config.keypair)
+        self.assertEqual('driver', config.network_driver)
+        self.assertEqual('external', config.external_net)
+        self.assertEqual(99, config.docker_volume_size)
+        self.assertEqual(ServerType.baremetal.value, config.server_type)
+        self.assertEqual('testFlavor', config.flavor)
+        self.assertEqual('masterFlavor', config.master_flavor)
+        self.assertEqual(ContainerOrchestrationEngine.kubernetes.value,
+                         config.coe)
+        self.assertEqual('fixedNet', config.fixed_net)
+        self.assertEqual('fixedSubnet', config.fixed_subnet)
+        self.assertFalse(config.registry_enabled)
+        self.assertEqual(DockerStorageDriver.overlay.value,
+                         config.docker_storage_driver)
+        self.assertEqual('8.8.4.4', config.dns_nameserver)
+        self.assertTrue(config.public)
+        self.assertFalse(config.tls_disabled)
+        self.assertEqual('http://foo:8080', config.http_proxy)
+        self.assertEqual('https://foo:443', config.https_proxy)
+        self.assertEqual('foo,bar', config.no_proxy)
+        self.assertEqual('volDriver', config.volume_driver)
+        self.assertFalse(config.master_lb_enabled)
+        self.assertEqual(labels, config.labels)
+
+    def test_all_config(self):
+        labels = {'foo': 'bar'}
+        config = ClusterTemplate(**{
+            'id': 'tmplt-id', 'name': 'foo', 'image': 'bar', 'keypair': 'keys',
+            'network_driver': 'driver', 'external_net': 'external',
+            'docker_volume_size': '99', 'server_type': 'baremetal',
+            'flavor': 'testFlavor', 'master_flavor': 'masterFlavor',
+            'coe': 'kubernetes', 'fixed_net': 'fixedNet',
+            'fixed_subnet': 'fixedSubnet', 'registry_enabled': False,
+            'docker_storage_driver': 'overlay', 'dns_nameserver': '8.8.4.4',
+            'public': 'true', 'tls': 'false', 'http_proxy': 'http://foo:8080',
+            'https_proxy': 'https://foo:443', 'no_proxy': 'foo,bar',
+            'volume_driver': 'volDriver', 'master_lb_enabled': False,
+            'labels': labels})
+        self.assertIsNotNone(config)
+        self.assertEqual('tmplt-id', config.id)
+        self.assertEqual('foo', config.name)
+        self.assertEqual('bar', config.image)
+        self.assertEqual('keys', config.keypair)
+        self.assertEqual('driver', config.network_driver)
+        self.assertEqual('external', config.external_net)
+        self.assertEqual(99, config.docker_volume_size)
+        self.assertEqual(ServerType.baremetal.value, config.server_type)
+        self.assertEqual('testFlavor', config.flavor)
+        self.assertEqual('masterFlavor', config.master_flavor)
+        self.assertEqual(ContainerOrchestrationEngine.kubernetes.value,
+                         config.coe)
+        self.assertEqual('fixedNet', config.fixed_net)
+        self.assertEqual('fixedSubnet', config.fixed_subnet)
+        self.assertFalse(config.registry_enabled)
+        self.assertEqual(DockerStorageDriver.overlay.value,
+                         config.docker_storage_driver)
+        self.assertEqual('8.8.4.4', config.dns_nameserver)
+        self.assertTrue(config.public)
+        self.assertFalse(config.tls_disabled)
+        self.assertEqual('http://foo:8080', config.http_proxy)
+        self.assertEqual('https://foo:443', config.https_proxy)
+        self.assertEqual('foo,bar', config.no_proxy)
+        self.assertEqual('volDriver', config.volume_driver)
+        self.assertFalse(config.master_lb_enabled)
+        self.assertEqual(labels, config.labels)
index 24a60c9..3003326 100644 (file)
@@ -25,39 +25,48 @@ class NetworkObjectTests(unittest.TestCase):
     """
 
     def test_construction_kwargs_1(self):
+        subnet = Subnet(
+            **{'name': 'foo', 'id': 'bar', 'network_id': 'foo-bar'})
         network = Network(
             **{'name': 'foo', 'id': 'bar', 'provider:network_type': 'flat',
                'admin_state_up': False, 'shared': True,
-               'router:external': False})
+               'router:external': False, 'subnets': [subnet]})
         self.assertEqual('foo', network.name)
         self.assertEqual('bar', network.id)
         self.assertEqual('flat', network.type)
         self.assertFalse(network.admin_state_up)
         self.assertFalse(network.external)
         self.assertTrue(network.shared)
+        self.assertEqual([subnet], network.subnets)
 
     def test_construction_kwargs_2(self):
+        subnet = Subnet(
+            **{'name': 'foo', 'id': 'bar', 'network_id': 'foo-bar'})
         network = Network(
             **{'name': 'foo', 'id': 'bar', 'type': 'flat',
-               'admin_state_up': False, 'shared': True,
-               'external': False})
+               'admin_state_up': False, 'shared': True, 'external': False,
+               'subnets': [subnet]})
         self.assertEqual('foo', network.name)
         self.assertEqual('bar', network.id)
         self.assertEqual('flat', network.type)
         self.assertFalse(network.admin_state_up)
         self.assertFalse(network.external)
         self.assertTrue(network.shared)
+        self.assertEqual([subnet], network.subnets)
 
     def test_construction_named(self):
+        subnet = Subnet(
+            **{'name': 'foo', 'id': 'bar', 'network_id': 'foo-bar'})
         network = Network(
             name='foo', id='bar', type='flat', admin_state_up=False,
-            shared=True, external=False)
+            shared=True, external=False, subnets=[subnet])
         self.assertEqual('foo', network.name)
         self.assertEqual('bar', network.id)
         self.assertEqual('flat', network.type)
         self.assertFalse(network.admin_state_up)
         self.assertFalse(network.external)
         self.assertTrue(network.shared)
+        self.assertEqual([subnet], network.subnets)
 
 
 class SubnetObjectTests(unittest.TestCase):
@@ -213,7 +222,8 @@ class RouterDomainObjectTests(unittest.TestCase):
         self.assertEqual('hello', router.status)
         self.assertEqual('1234', router.tenant_id)
         self.assertEqual('yes', router.admin_state_up)
-        self.assertEqual('no', router.external_gateway_info)
+        self.assertIsNone(router.external_fixed_ips)
+        self.assertIsNone(router.external_network_id)
 
     def test_construction_named(self):
         router = Router(
@@ -224,7 +234,35 @@ class RouterDomainObjectTests(unittest.TestCase):
         self.assertEqual('hello', router.status)
         self.assertEqual('1234', router.tenant_id)
         self.assertEqual('yes', router.admin_state_up)
-        self.assertEqual('no', router.external_gateway_info)
+        self.assertIsNone(router.external_fixed_ips)
+        self.assertIsNone(router.external_network_id)
+
+    def test_ext_gateway_named(self):
+        router = Router(
+            external_fixed_ips=['456', '789'], external_network_id='123',
+            admin_state_up='yes', tenant_id='1234', status='hello', id='id',
+            name='name')
+        self.assertEqual('name', router.name)
+        self.assertEqual('id', router.id)
+        self.assertEqual('hello', router.status)
+        self.assertEqual('1234', router.tenant_id)
+        self.assertEqual('yes', router.admin_state_up)
+        self.assertEqual(['456', '789'], router.external_fixed_ips)
+        self.assertEqual('123', router.external_network_id)
+
+    def test_ext_net_ips_named(self):
+        ext_gateway = {'network_id': '123',
+                       'external_fixed_ips': ['456', '789']}
+        router = Router(
+            external_gateway_info=ext_gateway, admin_state_up='yes',
+            tenant_id='1234', status='hello', id='id', name='name')
+        self.assertEqual('name', router.name)
+        self.assertEqual('id', router.id)
+        self.assertEqual('hello', router.status)
+        self.assertEqual('1234', router.tenant_id)
+        self.assertEqual('yes', router.admin_state_up)
+        self.assertEqual(['456', '789'], router.external_fixed_ips)
+        self.assertEqual('123', router.external_network_id)
 
 
 class InterfaceRouterDomainObjectTests(unittest.TestCase):
@@ -254,12 +292,33 @@ class SecurityGroupDomainObjectTests(unittest.TestCase):
     def test_construction_proj_id_kwargs(self):
         sec_grp = SecurityGroup(
             **{'name': 'name', 'id': 'id', 'project_id': 'foo',
-               'description': 'test desc'})
+               'description': 'test desc',
+               'rules': [
+                    {'id': 'id', 'security_group_id': 'grp_id',
+                     'description': 'desc', 'direction': 'dir',
+                     'ethertype': 'eType', 'port_range_min': '10.0.0.100',
+                     'port_range_max': '10.0.0.200', 'protocol': 'proto',
+                     'remote_group_id': 'group_id',
+                     'remote_ip_prefix': 'ip_prefix'}
+               ]})
         self.assertEqual('name', sec_grp.name)
         self.assertEqual('id', sec_grp.id)
         self.assertEqual('test desc', sec_grp.description)
         self.assertEqual('foo', sec_grp.project_id)
 
+        self.assertEqual(1, len(sec_grp.rules))
+        rule = sec_grp.rules[0]
+        self.assertEqual('id', rule.id)
+        self.assertEqual('grp_id', rule.security_group_id)
+        self.assertEqual('desc', rule.description)
+        self.assertEqual('dir', rule.direction)
+        self.assertEqual('eType', rule.ethertype)
+        self.assertEqual('10.0.0.100', rule.port_range_min)
+        self.assertEqual('10.0.0.200', rule.port_range_max)
+        self.assertEqual('proto', rule.protocol)
+        self.assertEqual('group_id', rule.remote_group_id)
+        self.assertEqual('ip_prefix', rule.remote_ip_prefix)
+
     def test_construction_tenant_id_kwargs(self):
         sec_grp = SecurityGroup(
             **{'name': 'name', 'id': 'id',
@@ -268,15 +327,37 @@ class SecurityGroupDomainObjectTests(unittest.TestCase):
         self.assertEqual('id', sec_grp.id)
         self.assertEqual('foo', sec_grp.project_id)
         self.assertIsNone(sec_grp.description)
+        self.assertEqual(0, len(sec_grp.rules))
 
     def test_construction_named(self):
+        rules = [SecurityGroupRule(
+            **{'id': 'id', 'security_group_id': 'grp_id',
+               'description': 'desc', 'direction': 'dir',
+               'ethertype': 'eType', 'port_range_min': '10.0.0.100',
+               'port_range_max': '10.0.0.200', 'protocol': 'proto',
+               'remote_group_id': 'group_id',
+               'remote_ip_prefix': 'ip_prefix'}
+        )]
         sec_grp = SecurityGroup(description='test desc', tenant_id='foo',
-                                id='id', name='name')
+                                id='id', name='name', rules=rules)
         self.assertEqual('name', sec_grp.name)
         self.assertEqual('id', sec_grp.id)
         self.assertEqual('test desc', sec_grp.description)
         self.assertEqual('foo', sec_grp.project_id)
 
+        self.assertEqual(1, len(sec_grp.rules))
+        rule = sec_grp.rules[0]
+        self.assertEqual('id', rule.id)
+        self.assertEqual('grp_id', rule.security_group_id)
+        self.assertEqual('desc', rule.description)
+        self.assertEqual('dir', rule.direction)
+        self.assertEqual('eType', rule.ethertype)
+        self.assertEqual('10.0.0.100', rule.port_range_min)
+        self.assertEqual('10.0.0.200', rule.port_range_max)
+        self.assertEqual('proto', rule.protocol)
+        self.assertEqual('group_id', rule.remote_group_id)
+        self.assertEqual('ip_prefix', rule.remote_ip_prefix)
+
 
 class SecurityGroupRuleDomainObjectTests(unittest.TestCase):
     """
index f816ef8..21e31d2 100644 (file)
@@ -39,14 +39,22 @@ class ResourceDomainObjectTests(unittest.TestCase):
     """
 
     def test_construction_positional(self):
-        resource = Resource('foo', 'bar')
+        resource = Resource('res_name', 'foo', 'bar', 'status', 'reason')
+        self.assertEqual('res_name', resource.name)
         self.assertEqual('foo', resource.type)
         self.assertEqual('bar', resource.id)
+        self.assertEqual('status', resource.status)
+        self.assertEqual('reason', resource.status_reason)
 
     def test_construction_named(self):
-        resource = Resource(resource_id='bar', resource_type='foo')
+        resource = Resource(
+            status_reason=None, status=None, resource_id='bar',
+            resource_type='foo', name='res_name')
+        self.assertEqual('res_name', resource.name)
         self.assertEqual('foo', resource.type)
         self.assertEqual('bar', resource.id)
+        self.assertIsNone(resource.status)
+        self.assertIsNone(resource.status_reason)
 
 
 class OutputDomainObjectTests(unittest.TestCase):
index d293373..ad7a9ce 100644 (file)
@@ -23,26 +23,30 @@ class VmInstDomainObjectTests(unittest.TestCase):
     """
 
     def test_construction_positional(self):
-        vm_inst = VmInst('name', 'id', '456', '123', dict(), 'kp-name', list())
+        vm_inst = VmInst('name', 'id', '456', '123', list(), 'kp-name',
+                         ['foo', 'bar'], ['123', '456'])
         self.assertEqual('name', vm_inst.name)
         self.assertEqual('id', vm_inst.id)
         self.assertEqual('456', vm_inst.image_id)
         self.assertEqual('123', vm_inst.flavor_id)
-        self.assertEqual(dict(), vm_inst.networks)
+        self.assertEqual(list(), vm_inst.ports)
         self.assertEqual('kp-name', vm_inst.keypair_name)
-        self.assertEqual(list(), vm_inst.sec_grp_names)
+        self.assertEqual(['foo', 'bar'], vm_inst.sec_grp_names)
+        self.assertEqual(['123', '456'], vm_inst.volume_ids)
 
     def test_construction_named(self):
-        vm_inst = VmInst(sec_grp_names=list(), networks=dict(), inst_id='id',
-                         name='name', flavor_id='123', image_id='456',
-                         keypair_name='kp-name')
+        vm_inst = VmInst(
+            volume_ids=['123', '456'], sec_grp_names=['foo', 'bar'],
+            ports=list(), inst_id='id', name='name', flavor_id='123',
+            image_id='456', keypair_name='kp-name')
         self.assertEqual('name', vm_inst.name)
         self.assertEqual('id', vm_inst.id)
         self.assertEqual('456', vm_inst.image_id)
         self.assertEqual('123', vm_inst.flavor_id)
-        self.assertEqual(dict(), vm_inst.networks)
+        self.assertEqual(list(), vm_inst.ports)
         self.assertEqual('kp-name', vm_inst.keypair_name)
-        self.assertEqual(list(), vm_inst.sec_grp_names)
+        self.assertEqual(['foo', 'bar'], vm_inst.sec_grp_names)
+        self.assertEqual(['123', '456'], vm_inst.volume_ids)
 
 
 class FloatingIpDomainObjectTests(unittest.TestCase):
index ec5f7b7..6feadc9 100644 (file)
 # limitations under the License.
 
 import unittest
-from snaps.domain.volume import QoSSpec, VolumeType, VolumeTypeEncryption
+from snaps.domain.volume import (
+    QoSSpec, VolumeType, VolumeTypeEncryption, Volume)
+
+
+class VolumeDomainObjectTests(unittest.TestCase):
+    """
+    Tests the construction of the snaps.domain.volume.Volume class
+    """
+
+    def test_construction_positional(self):
+        volume = Volume('name1', 'id1', 'desc_val1', 2, 'type_val1',
+                        'avail_zone1', False, [{'attached_at': 'foo'}])
+        self.assertEqual('name1', volume.name)
+        self.assertEqual('id1', volume.id)
+        self.assertEqual('desc_val1', volume.description)
+        self.assertEqual(2, volume.size)
+        self.assertEqual('type_val1', volume.type)
+        self.assertEqual('avail_zone1', volume.availability_zone)
+        self.assertFalse(volume.multi_attach)
+        self.assertIsNotNone(volume.attachments)
+        self.assertTrue(isinstance(volume.attachments[0], dict))
+        self.assertEqual(1, len(volume.attachments))
+
+    def test_construction_named(self):
+        volume = Volume(attachments=[{'attached_at': 'foo'}],
+                        multi_attach=True, availability_zone='avail_zone2',
+                        vol_type='type_val2', size=3, description='desc_val2',
+                        volume_id='id2', name='name2')
+        self.assertEqual('name2', volume.name)
+        self.assertEqual('id2', volume.id)
+        self.assertEqual('desc_val2', volume.description)
+        self.assertEqual(3, volume.size)
+        self.assertEqual('type_val2', volume.type)
+        self.assertEqual('avail_zone2', volume.availability_zone)
+        self.assertTrue(volume.multi_attach)
+        self.assertIsNotNone(volume.attachments)
+        self.assertTrue(isinstance(volume.attachments[0], dict))
+        self.assertEqual(1, len(volume.attachments))
 
 
 class VolumeTypeDomainObjectTests(unittest.TestCase):
index ca38143..c49b03e 100644 (file)
@@ -19,35 +19,38 @@ class VmInst:
     SNAPS domain object for Images. Should contain attributes that
     are shared amongst cloud providers
     """
-    def __init__(self, name, inst_id, image_id, flavor_id, networks,
-                 keypair_name, sec_grp_names):
+    def __init__(self, name, inst_id, image_id, flavor_id, ports,
+                 keypair_name, sec_grp_names, volume_ids):
         """
         Constructor
         :param name: the image's name
         :param inst_id: the instance's id
         :param image_id: the instance's image id
         :param flavor_id: the ID used to spawn this instance
-        :param networks: dict of networks where the key is the network name and
-                         value is a list of associated IPs
+        :param ports: list of SNAPS-OO Port domain objects associated with this
+                      server instance
         :param keypair_name: the name of the associated keypair
         :param sec_grp_names: list of security group names
+        :param volume_ids: list of attached volume IDs
         """
         self.name = name
         self.id = inst_id
         self.image_id = image_id
         self.flavor_id = flavor_id
-        self.networks = networks
+        self.ports = ports
         self.keypair_name = keypair_name
         self.sec_grp_names = sec_grp_names
+        self.volume_ids = volume_ids
 
     def __eq__(self, other):
         return (self.name == other.name and
                 self.id == other.id and
                 self.image_id == other.image_id and
                 self.flavor_id == other.flavor_id and
-                self.networks == other.networks and
+                self.ports == other.ports and
                 self.keypair_name == other.keypair_name and
-                self.sec_grp_names == other.sec_grp_names)
+                self.sec_grp_names == other.sec_grp_names and
+                self.volume_ids == other.volume_ids)
 
 
 class FloatingIp:
index e82a60a..0042d71 100644 (file)
 # limitations under the License.
 
 
+class Volume:
+    """
+    SNAPS domain object for Volumes. Should contain attributes that
+    are shared amongst cloud providers
+    """
+    def __init__(self, name, volume_id, description, size, vol_type,
+                 availability_zone, multi_attach, attachments=list()):
+        """
+        Constructor
+        :param name: the volume's name
+        :param volume_id: the volume's id
+        :param description: the volume's description
+        :param size: the volume's size in GB
+        :param vol_type: the volume's type
+        :param availability_zone: the volume's availability zone
+        :param multi_attach: When true, volume can be attached to multiple VMs
+        :param attachments: List of dict objects containing the info on where
+                            this volume is attached
+        """
+        self.name = name
+        self.id = volume_id
+        self.description = description
+        self.size = size
+        self.type = vol_type
+        self.availability_zone = availability_zone
+        self.multi_attach = multi_attach
+        self.attachments = attachments
+
+    def __eq__(self, other):
+        return (self.name == other.name and self.id == other.id
+                and self.description == other.description
+                and self.size == other.size
+                and self.type == other.type
+                and self.availability_zone == other.availability_zone
+                and self.multi_attach == other.multi_attach)
+
+
 class VolumeType:
     """
     SNAPS domain object for Volume Types. Should contain attributes that
diff --git a/snaps/openstack/cluster_template.py b/snaps/openstack/cluster_template.py
new file mode 100644 (file)
index 0000000..c4ba76d
--- /dev/null
@@ -0,0 +1,94 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from magnumclient.common.apiclient.exceptions import NotFound
+
+from snaps.openstack.openstack_creator import OpenStackMagnumObject
+from snaps.openstack.utils import magnum_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('cluster_template')
+
+
+class OpenStackClusterTemplate(OpenStackMagnumObject):
+    """
+    Class responsible for managing an volume in OpenStack
+    """
+
+    def __init__(self, os_creds, cluster_template_config):
+        """
+        Constructor
+        :param os_creds: The OpenStack connection credentials
+        :param cluster_template_config: The volume type settings
+        :return:
+        """
+        super(self.__class__, self).__init__(os_creds)
+
+        self.cluster_template_config = cluster_template_config
+        self.__cluster_template = None
+
+    def initialize(self):
+        """
+        Loads the existing Volume
+        :return: The Volume domain object or None
+        """
+        super(self.__class__, self).initialize()
+
+        self.__cluster_template = magnum_utils.get_cluster_template(
+            self._magnum, template_config=self.cluster_template_config)
+
+        return self.__cluster_template
+
+    def create(self):
+        """
+        Creates the volume in OpenStack if it does not already exist and
+        returns the domain Volume object
+        :return: The Volume domain object or None
+        """
+        self.initialize()
+
+        if not self.__cluster_template:
+            self.__cluster_template = magnum_utils.create_cluster_template(
+                self._magnum, self.cluster_template_config)
+            logger.info(
+                'Created volume type with name - %s',
+                self.cluster_template_config.name)
+
+        return self.__cluster_template
+
+    def clean(self):
+        """
+        Cleanse environment of all artifacts
+        :return: void
+        """
+        if self.__cluster_template:
+            try:
+                magnum_utils.delete_cluster_template(
+                    self._magnum, self.__cluster_template.id)
+            except NotFound:
+                pass
+
+        self.__cluster_template = None
+
+    def get_cluster_template(self):
+        """
+        Returns the domain Volume object as it was populated when create() was
+        called
+        :return: the object
+        """
+        return self.__cluster_template
index 1479bb0..65b9059 100644 (file)
@@ -16,6 +16,7 @@ import logging
 
 from novaclient.exceptions import NotFound
 
+from snaps.config.flavor import FlavorConfig
 from snaps.openstack.openstack_creator import OpenStackComputeObject
 from snaps.openstack.utils import nova_utils
 
@@ -36,7 +37,7 @@ class OpenStackFlavor(OpenStackComputeObject):
         """
         Constructor
         :param os_creds: The OpenStack connection credentials
-        :param flavor_settings: The flavor settings
+        :param flavor_settings: a FlavorConfig instance
         :return:
         """
         super(self.__class__, self).__init__(os_creds)
@@ -70,8 +71,6 @@ class OpenStackFlavor(OpenStackComputeObject):
             if self.flavor_settings.metadata:
                 nova_utils.set_flavor_keys(self._nova, self.__flavor,
                                            self.flavor_settings.metadata)
-        else:
-            logger.info('Did not create flavor due to cleanup mode')
 
         return self.__flavor
 
@@ -96,97 +95,13 @@ class OpenStackFlavor(OpenStackComputeObject):
         return self.__flavor
 
 
-class FlavorSettings:
+class FlavorSettings(FlavorConfig):
     """
     Configuration settings for OpenStack flavor creation
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor
-        :param config: dict() object containing the configuration settings
-                       using the attribute names below as each member's the
-                       key and overrides any of the other parameters.
-        :param name: the flavor's name (required)
-        :param flavor_id: the string ID (default 'auto')
-        :param ram: the required RAM in MB (required)
-        :param disk: the size of the root disk in GB (required)
-        :param vcpus: the number of virtual CPUs (required)
-        :param ephemeral: the size of the ephemeral disk in GB (default 0)
-        :param swap: the size of the dedicated swap disk in GB (default 0)
-        :param rxtx_factor: the receive/transmit factor to be set on ports if
-                            backend supports QoS extension (default 1.0)
-        :param is_public: denotes whether or not the flavor is public
-                          (default True)
-        :param metadata: freeform dict() for special metadata
-        """
-        self.name = kwargs.get('name')
-
-        if kwargs.get('flavor_id'):
-            self.flavor_id = kwargs['flavor_id']
-        else:
-            self.flavor_id = 'auto'
-
-        self.ram = kwargs.get('ram')
-        self.disk = kwargs.get('disk')
-        self.vcpus = kwargs.get('vcpus')
-
-        if kwargs.get('ephemeral'):
-            self.ephemeral = kwargs['ephemeral']
-        else:
-            self.ephemeral = 0
-
-        if kwargs.get('swap'):
-            self.swap = kwargs['swap']
-        else:
-            self.swap = 0
-
-        if kwargs.get('rxtx_factor'):
-            self.rxtx_factor = kwargs['rxtx_factor']
-        else:
-            self.rxtx_factor = 1.0
-
-        if kwargs.get('is_public') is not None:
-            self.is_public = kwargs['is_public']
-        else:
-            self.is_public = True
-
-        if kwargs.get('metadata'):
-            self.metadata = kwargs['metadata']
-        else:
-            self.metadata = None
-
-        if not self.name or not self.ram or not self.disk or not self.vcpus:
-            raise FlavorSettingsError(
-                'The attributes name, ram, disk, and vcpus are required for'
-                'FlavorSettings')
-
-        if not isinstance(self.ram, int):
-            raise FlavorSettingsError('The ram attribute must be a integer')
-
-        if not isinstance(self.disk, int):
-            raise FlavorSettingsError('The ram attribute must be a integer')
-
-        if not isinstance(self.vcpus, int):
-            raise FlavorSettingsError('The vcpus attribute must be a integer')
-
-        if self.ephemeral and not isinstance(self.ephemeral, int):
-            raise FlavorSettingsError(
-                'The ephemeral attribute must be an integer')
-
-        if self.swap and not isinstance(self.swap, int):
-            raise FlavorSettingsError('The swap attribute must be an integer')
-
-        if self.rxtx_factor and not isinstance(self.rxtx_factor, (int, float)):
-            raise FlavorSettingsError(
-                'The is_public attribute must be an integer or float')
-
-        if self.is_public and not isinstance(self.is_public, bool):
-            raise FlavorSettingsError(
-                'The is_public attribute must be a boolean')
-
-
-class FlavorSettingsError(Exception):
-    """
-    Exception to be thrown when an flavor settings are incorrect
-    """
+        from warnings import warn
+        warn('Use snaps.config.flavor.FlavorConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
index 3024717..a5520e3 100644 (file)
@@ -19,6 +19,7 @@ import time
 
 from snaps.openstack.openstack_creator import OpenStackCloudObject
 from snaps.openstack.utils import glance_utils
+from snaps.config import image
 
 __author__ = 'spisarski'
 
@@ -38,8 +39,7 @@ class OpenStackImage(OpenStackCloudObject):
         """
         Constructor
         :param os_creds: The OpenStack connection credentials
-        :param image_settings: The image settings
-        :return:
+        :param image_settings: An snaps.config.image.ImageConfig object
         """
         super(self.__class__, self).__init__(os_creds)
 
@@ -57,6 +57,7 @@ class OpenStackImage(OpenStackCloudObject):
         self.__glance = glance_utils.glance_client(self._os_creds)
         self.__image = glance_utils.get_image(
             self.__glance, image_settings=self.image_settings)
+
         if self.__image:
             logger.info('Found image with name - ' + self.image_settings.name)
             return self.__image
@@ -70,6 +71,7 @@ class OpenStackImage(OpenStackCloudObject):
             self.__kernel_image = glance_utils.get_image(
                 self.__glance,
                 image_settings=self.image_settings.kernel_image_settings)
+
         if self.image_settings.ramdisk_image_settings:
             self.__ramdisk_image = glance_utils.get_image(
                 self.__glance,
@@ -97,6 +99,7 @@ class OpenStackImage(OpenStackCloudObject):
                         self.__glance,
                         self.image_settings.kernel_image_settings)
                 extra_properties['kernel_id'] = self.__kernel_image.id
+
             if self.image_settings.ramdisk_image_settings:
                 if not self.__ramdisk_image:
                     logger.info(
@@ -113,6 +116,7 @@ class OpenStackImage(OpenStackCloudObject):
 
             logger.info(
                 'Created image with name - %s', self.image_settings.name)
+
             if self.__image and self.image_active(block=True):
                 logger.info(
                     'Image is now active with name - %s',
@@ -122,8 +126,6 @@ class OpenStackImage(OpenStackCloudObject):
                 raise ImageCreationError(
                     'Image was not created or activated in the alloted amount'
                     'of time')
-        else:
-            logger.info('Did not create image due to cleanup mode')
 
         return self.__image
 
@@ -132,10 +134,10 @@ class OpenStackImage(OpenStackCloudObject):
         Cleanse environment of all artifacts
         :return: void
         """
-        for image in [self.__image, self.__kernel_image, self.__ramdisk_image]:
-            if image:
+        for img in [self.__image, self.__kernel_image, self.__ramdisk_image]:
+            if img:
                 try:
-                    glance_utils.delete_image(self.__glance, image)
+                    glance_utils.delete_image(self.__glance, img)
                 except HTTPNotFound:
                     pass
 
@@ -236,100 +238,14 @@ class OpenStackImage(OpenStackCloudObject):
         return status == expected_status_code
 
 
-class ImageSettings:
-    def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: the image's name (required)
-        :param image_user: the image's default sudo user (required)
-        :param format or img_format: the image type (required)
-        :param url or download_url: the image download location (requires url
-                                    or img_file)
-        :param image_file: the image file location (requires url or img_file)
-        :param extra_properties: dict() object containing extra parameters to
-                                 pass when loading the image;
-                                 can be ids of kernel and initramfs images for
-                                 a 3-part image
-        :param nic_config_pb_loc: the file location to the Ansible Playbook
-                                  that can configure multiple NICs
-        :param kernel_image_settings: the settings for a kernel image
-        :param ramdisk_image_settings: the settings for a kernel image
-        :param exists: When True, an image with the given name must exist
-        :param public: When True, an image will be created with public
-                       visibility
-        """
-
-        self.name = kwargs.get('name')
-        self.image_user = kwargs.get('image_user')
-        self.format = kwargs.get('format')
-        if not self.format:
-            self.format = kwargs.get('img_format')
-
-        self.url = kwargs.get('url')
-        if not self.url:
-            self.url = kwargs.get('download_url')
-        if self.url == 'None':
-            self.url = None
-
-        self.image_file = kwargs.get('image_file')
-        if self.image_file == 'None':
-            self.image_file = None
-
-        self.extra_properties = kwargs.get('extra_properties')
-        self.nic_config_pb_loc = kwargs.get('nic_config_pb_loc')
-
-        kernel_image_settings = kwargs.get('kernel_image_settings')
-        if kernel_image_settings:
-            if isinstance(kernel_image_settings, dict):
-                self.kernel_image_settings = ImageSettings(
-                    **kernel_image_settings)
-            else:
-                self.kernel_image_settings = kernel_image_settings
-        else:
-            self.kernel_image_settings = None
-
-        ramdisk_image_settings = kwargs.get('ramdisk_image_settings')
-        if ramdisk_image_settings:
-            if isinstance(ramdisk_image_settings, dict):
-                self.ramdisk_image_settings = ImageSettings(
-                    **ramdisk_image_settings)
-            else:
-                self.ramdisk_image_settings = ramdisk_image_settings
-        else:
-            self.ramdisk_image_settings = None
-
-        if 'exists' in kwargs and kwargs['exists'] is True:
-            self.exists = True
-        else:
-            self.exists = False
-
-        if 'public' in kwargs and kwargs['public'] is True:
-            self.public = True
-        else:
-            self.public = False
-
-        if not self.name:
-            raise ImageSettingsError("The attribute name is required")
-
-        if not (self.url or self.image_file) and not self.exists:
-            raise ImageSettingsError(
-                'URL or image file must be set or image must already exist')
-
-        if not self.image_user:
-            raise ImageSettingsError('Image user is required')
-
-        if not self.format and not self.exists:
-            raise ImageSettingsError(
-                'Format is required when the image should not already exist')
-
-
-class ImageSettingsError(Exception):
+class ImageSettings(image.ImageConfig):
     """
-    Exception to be thrown when an image settings are incorrect
+    Deprecated, use snaps.config.image.ImageSettings instead
     """
-
-    def __init__(self, message):
-        Exception.__init__(self, message)
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.image.ImageConfig instead', DeprecationWarning)
+        super(ImageSettings, self).__init__(**kwargs)
 
 
 class ImageCreationError(Exception):
index 3d55f42..d91e360 100644 (file)
 import logging
 import time
 
-from neutronclient.common.exceptions import PortNotFoundClient
-from novaclient.exceptions import NotFound
+from novaclient.exceptions import NotFound, BadRequest
 
-from snaps.openstack.create_network import PortSettings
+from snaps.config.vm_inst import VmInstanceConfig, FloatingIpConfig
 from snaps.openstack.openstack_creator import OpenStackComputeObject
-from snaps.openstack.utils import glance_utils
+from snaps.openstack.utils import glance_utils, cinder_utils, settings_utils
 from snaps.openstack.utils import neutron_utils
 from snaps.openstack.utils import nova_utils
+from snaps.openstack.utils.nova_utils import RebootType
 from snaps.provisioning import ansible_utils
 
 __author__ = 'spisarski'
@@ -89,7 +89,8 @@ class OpenStackVmInstance(OpenStackComputeObject):
         self.initialize()
 
         if len(self.__ports) == 0:
-            self.__ports = self.__create_ports(self.instance_settings.port_settings)
+            self.__ports = self.__create_ports(
+                self.instance_settings.port_settings)
         if not self.__vm:
             self.__create_vm(block)
 
@@ -102,7 +103,8 @@ class OpenStackVmInstance(OpenStackComputeObject):
         within the project
         """
         server = nova_utils.get_server(
-            self._nova, vm_inst_settings=self.instance_settings)
+            self._nova, self.__neutron,
+            vm_inst_settings=self.instance_settings)
         if server:
             if server.name == self.instance_settings.name:
                 self.__vm = server
@@ -155,6 +157,26 @@ class OpenStackVmInstance(OpenStackComputeObject):
                     ' to VM that did not activate with name - ' +
                     self.instance_settings.name)
 
+        if self.instance_settings.volume_names:
+            for volume_name in self.instance_settings.volume_names:
+                cinder = cinder_utils.cinder_client(self._os_creds)
+                volume = cinder_utils.get_volume(
+                    cinder, volume_name=volume_name)
+
+                if volume and self.vm_active(block=True):
+                    timeout = 30
+                    vm = nova_utils.attach_volume(
+                        self._nova, self.__neutron, self.__vm, volume, timeout)
+
+                    if vm:
+                        self.__vm = vm
+                    else:
+                        logger.warn('Volume [%s] not attached within timeout '
+                                    'of [%s]', volume.name, timeout)
+                else:
+                    logger.warn('Unable to attach volume named [%s]',
+                                volume_name)
+
         self.__apply_floating_ips()
 
     def __apply_floating_ips(self):
@@ -167,33 +189,47 @@ class OpenStackVmInstance(OpenStackComputeObject):
 
         # Apply floating IPs
         for floating_ip_setting in self.instance_settings.floating_ip_settings:
-            port = port_dict.get(floating_ip_setting.port_name)
+            self.add_floating_ip(floating_ip_setting)
 
-            if not port:
-                raise VmInstanceCreationError(
-                    'Cannot find port object with name - ' +
-                    floating_ip_setting.port_name)
+    def add_floating_ip(self, floating_ip_setting):
+        """
+        Adds a floating IP to a running instance
+        :param floating_ip_setting - the floating IP configuration
+        :return: the floating ip object
+        """
+        port_dict = dict()
+        for key, port in self.__ports:
+            port_dict[key] = port
 
-            # Setup Floating IP only if there is a router with an external
-            # gateway
-            ext_gateway = self.__ext_gateway_by_router(
-                floating_ip_setting.router_name)
-            if ext_gateway:
-                subnet = neutron_utils.get_subnet(
-                    self.__neutron,
-                    subnet_name=floating_ip_setting.subnet_name)
-                floating_ip = neutron_utils.create_floating_ip(
-                    self.__neutron, ext_gateway)
-                self.__floating_ip_dict[floating_ip_setting.name] = floating_ip
+        # Apply floating IP
+        port = port_dict.get(floating_ip_setting.port_name)
 
-                logger.info(
-                    'Created floating IP %s via router - %s', floating_ip.ip,
-                    floating_ip_setting.router_name)
-                self.__add_floating_ip(floating_ip, port, subnet)
-            else:
-                raise VmInstanceCreationError(
-                    'Unable to add floating IP to port, cannot locate router '
-                    'with an external gateway ')
+        if not port:
+            raise VmInstanceCreationError(
+                'Cannot find port object with name - ' +
+                floating_ip_setting.port_name)
+
+        # Setup Floating IP only if there is a router with an external
+        # gateway
+        ext_gateway = self.__ext_gateway_by_router(
+            floating_ip_setting.router_name)
+        if ext_gateway:
+            subnet = neutron_utils.get_subnet(
+                self.__neutron,
+                subnet_name=floating_ip_setting.subnet_name)
+            floating_ip = neutron_utils.create_floating_ip(
+                self.__neutron, ext_gateway)
+            self.__floating_ip_dict[floating_ip_setting.name] = floating_ip
+
+            logger.info(
+                'Created floating IP %s via router - %s', floating_ip.ip,
+                floating_ip_setting.router_name)
+            self.__add_floating_ip(floating_ip, port, subnet)
+            return floating_ip
+        else:
+            raise VmInstanceCreationError(
+                'Unable to add floating IP to port, cannot locate router '
+                'with an external gateway ')
 
     def __ext_gateway_by_router(self, router_name):
         """
@@ -204,10 +240,9 @@ class OpenStackVmInstance(OpenStackComputeObject):
         """
         router = neutron_utils.get_router(
             self.__neutron, router_name=router_name)
-        if router and router.external_gateway_info:
+        if router and router.external_network_id:
             network = neutron_utils.get_network_by_id(
-                self.__neutron,
-                router.external_gateway_info['network_id'])
+                self.__neutron, router.external_network_id)
             if network:
                 return network.name
         return None
@@ -219,50 +254,58 @@ class OpenStackVmInstance(OpenStackComputeObject):
 
         # Cleanup floating IPs
         for name, floating_ip in self.__floating_ip_dict.items():
-            try:
-                logger.info('Deleting Floating IP - ' + floating_ip.ip)
-                neutron_utils.delete_floating_ip(self.__neutron, floating_ip)
-            except Exception as e:
-                logger.error('Error deleting Floating IP - ' + str(e))
+            logger.info('Deleting Floating IP - ' + floating_ip.ip)
+            neutron_utils.delete_floating_ip(self.__neutron, floating_ip)
+
         self.__floating_ip_dict = dict()
 
         # Cleanup ports
         for name, port in self.__ports:
-            logger.info('Deleting Port with ID - %S ' + port.id)
-            try:
-                neutron_utils.delete_port(self.__neutron, port)
-            except PortNotFoundClient as e:
-                logger.warning('Unexpected error deleting port - %s', e)
-                pass
+            logger.info('Deleting Port with ID - %s ', port.id)
+            neutron_utils.delete_port(self.__neutron, port)
+
         self.__ports = list()
 
-        # Cleanup VM
         if self.__vm:
+            # Detach Volume
+            for volume_rec in self.__vm.volume_ids:
+                cinder = cinder_utils.cinder_client(self._os_creds)
+                volume = cinder_utils.get_volume_by_id(
+                    cinder, volume_rec['id'])
+                if volume:
+                    vm = nova_utils.detach_volume(
+                        self._nova, self.__neutron, self.__vm, volume, 30)
+                    if vm:
+                        self.__vm = vm
+                    else:
+                        logger.warn(
+                            'Timeout waiting to detach volume %s', volume.name)
+                else:
+                    logger.warn('Unable to detach volume with ID - [%s]',
+                                volume_rec['id'])
+
+            # Cleanup VM
+            logger.info(
+                'Deleting VM instance - ' + self.instance_settings.name)
+
             try:
-                logger.info(
-                    'Deleting VM instance - ' + self.instance_settings.name)
                 nova_utils.delete_vm_instance(self._nova, self.__vm)
-            except Exception as e:
-                logger.error('Error deleting VM - %s', e)
+            except NotFound as e:
+                logger.warn('Instance already deleted - %s', e)
 
             # Block until instance cannot be found or returns the status of
             # DELETED
             logger.info('Checking deletion status')
 
-            try:
-                if self.vm_deleted(block=True):
-                    logger.info(
-                        'VM has been properly deleted VM with name - %s',
-                        self.instance_settings.name)
-                    self.__vm = None
-                else:
-                    logger.error(
-                        'VM not deleted within the timeout period of %s '
-                        'seconds', self.instance_settings.vm_delete_timeout)
-            except Exception as e:
+            if self.vm_deleted(block=True):
+                logger.info(
+                    'VM has been properly deleted VM with name - %s',
+                    self.instance_settings.name)
+                self.__vm = None
+            else:
                 logger.error(
-                    'Unexpected error while checking VM instance status - %s',
-                    e)
+                    'VM not deleted within the timeout period of %s '
+                    'seconds', self.instance_settings.vm_delete_timeout)
 
     def __query_ports(self, port_settings):
         """
@@ -296,7 +339,8 @@ class OpenStackVmInstance(OpenStackComputeObject):
             port = neutron_utils.get_port(
                 self.__neutron, port_settings=port_setting)
             if not port:
-                port = neutron_utils.create_port(self.__neutron, self._os_creds, port_setting)
+                port = neutron_utils.create_port(
+                    self.__neutron, self._os_creds, port_setting)
                 if port:
                     ports.append((port_setting.name, port))
 
@@ -332,6 +376,9 @@ class OpenStackVmInstance(OpenStackComputeObject):
                         'Added floating IP %s to port IP %s on instance %s',
                         floating_ip.ip, ip, self.instance_settings.name)
                     return
+                except BadRequest as bre:
+                    logger.error('Cannot add floating IP [%s]', bre)
+                    raise
                 except Exception as e:
                     logger.debug(
                         'Retry adding floating IP to instance. Last attempt '
@@ -359,7 +406,8 @@ class OpenStackVmInstance(OpenStackComputeObject):
         Returns the latest version of this server object from OpenStack
         :return: Server object
         """
-        return nova_utils.get_server_object_by_id(self._nova, self.__vm.id)
+        return nova_utils.get_server_object_by_id(
+            self._nova, self.__neutron, self.__vm.id)
 
     def get_console_output(self):
         """
@@ -428,25 +476,6 @@ class OpenStackVmInstance(OpenStackComputeObject):
         """
         return nova_utils.get_server_info(self._nova, self.__vm)
 
-    def config_nics(self):
-        """
-        Responsible for configuring NICs on RPM systems where the instance has
-        more than one configured port
-        :return: the value returned by ansible_utils.apply_ansible_playbook()
-        """
-        if len(self.__ports) > 1 and len(self.__floating_ip_dict) > 0:
-            if self.vm_active(block=True) and self.vm_ssh_active(block=True):
-                for key, port in self.__ports:
-                    port_index = self.__ports.index((key, port))
-                    if port_index > 0:
-                        nic_name = 'eth' + repr(port_index)
-                        retval = self.__config_nic(
-                            nic_name, port,
-                            self.__get_first_provisioning_floating_ip().ip)
-                        logger.info('Configured NIC - %s on VM - %s',
-                                    nic_name, self.instance_settings.name)
-                        return retval
-
     def __get_first_provisioning_floating_ip(self):
         """
         Returns the first floating IP tagged with the Floating IP name if
@@ -462,30 +491,10 @@ class OpenStackVmInstance(OpenStackComputeObject):
                     for key, fip in self.__floating_ip_dict.items():
                         return fip
 
-    def __config_nic(self, nic_name, port, ip):
-        """
-        Although ports/NICs can contain multiple IPs, this code currently only
-        supports the first.
-
-        :param nic_name: Name of the interface
-        :param port: The port information containing the expected IP values.
-        :param ip: The IP on which to apply the playbook.
-        :return: the return value from ansible
-        """
-        port_ip = port.ips[0]['ip_address']
-        variables = {
-            'floating_ip': ip,
-            'nic_name': nic_name,
-            'nic_ip': port_ip
-        }
-
-        if self.image_settings.nic_config_pb_loc and self.keypair_settings:
-            return self.apply_ansible_playbook(
-                self.image_settings.nic_config_pb_loc, variables)
-        else:
-            logger.warning(
-                'VM %s cannot self configure NICs eth1++. No playbook or '
-                'keypairs found.', self.instance_settings.name)
+        # When cannot be found above
+        if len(self.__floating_ip_dict) > 0:
+            for key, fip in self.__floating_ip_dict.items():
+                return fip
 
     def apply_ansible_playbook(self, pb_file_loc, variables=None,
                                fip_name=None):
@@ -541,9 +550,13 @@ class OpenStackVmInstance(OpenStackComputeObject):
         :param poll_interval: The polling interval in seconds
         :return: T/F
         """
-        return self.__vm_status_check(STATUS_ACTIVE, block,
-                                      self.instance_settings.vm_boot_timeout,
-                                      poll_interval)
+        if self.__vm_status_check(
+                STATUS_ACTIVE, block, self.instance_settings.vm_boot_timeout,
+                poll_interval):
+            self.__vm = nova_utils.get_server_object_by_id(
+                self._nova, self.__neutron, self.__vm.id)
+            return True
+        return False
 
     def __vm_status_check(self, expected_status_code, block, timeout,
                           poll_interval):
@@ -652,6 +665,56 @@ class OpenStackVmInstance(OpenStackComputeObject):
                 return True
         return False
 
+    def cloud_init_complete(self, block=False, poll_interval=POLL_INTERVAL):
+        """
+        Returns true when the VM's cloud-init routine has completed.
+        Note: this is currently done via SSH, therefore, if this instance does
+              not have a Floating IP or a running SSH server, this routine
+              will always return False or raise an Exception
+        :param block: When true, thread will block until active or timeout
+                      value in seconds has been exceeded (False)
+        :param poll_interval: The polling interval
+        :return: T/F
+        """
+        # sleep and wait for VM status change
+        logger.info('Checking if cloud-init has completed')
+
+        timeout = self.instance_settings.cloud_init_timeout
+
+        if self.vm_active(block=True) and self.vm_ssh_active(block=True):
+            if block:
+                start = time.time()
+            else:
+                start = time.time() - timeout
+
+            while timeout > time.time() - start:
+                status = self.__cloud_init_complete()
+                if status:
+                    logger.info('cloud-init complete for VM instance')
+                    return True
+
+                logger.debug('Retry cloud-init query in ' + str(
+                    poll_interval) + ' seconds')
+                time.sleep(poll_interval)
+                logger.debug('cloud-init complete timeout in ' + str(
+                    timeout - (time.time() - start)))
+
+        logger.error('Timeout waiting for cloud-init to complete')
+        return False
+
+    def __cloud_init_complete(self):
+        """
+        Returns True when can create a SSH session else False
+        :return: T/F
+        """
+        if len(self.__floating_ip_dict) > 0:
+            ssh = self.ssh_client()
+            if ssh:
+                stdin1, stdout1, sterr1 = ssh.exec_command(
+                    'ls -l /var/lib/cloud/instance/boot-finished')
+                return stdout1.channel.recv_exit_status() == 0
+        return False
+
     def get_floating_ip(self, fip_name=None):
         """
         Returns the floating IP object byt name if found, else the first known,
@@ -659,10 +722,9 @@ class OpenStackVmInstance(OpenStackComputeObject):
         :param fip_name: the name of the floating IP to return
         :return: the SSH client or None
         """
-        fip = None
         if fip_name and self.__floating_ip_dict.get(fip_name):
             return self.__floating_ip_dict.get(fip_name)
-        if not fip:
+        else:
             return self.__get_first_provisioning_floating_ip()
 
     def ssh_client(self, fip_name=None):
@@ -680,7 +742,7 @@ class OpenStackVmInstance(OpenStackComputeObject):
                 self.keypair_settings.private_filepath,
                 proxy_settings=self._os_creds.proxy_settings)
         else:
-            logger.warning(
+            FloatingIPAllocationError(
                 'Cannot return an SSH client. No Floating IP configured')
 
     def add_security_group(self, security_group):
@@ -723,165 +785,67 @@ class OpenStackVmInstance(OpenStackComputeObject):
             logger.warning('Security group not removed - ' + str(e))
             return False
 
-
-class VmInstanceSettings:
-    """
-    Class responsible for holding configuration setting for a VM Instance
-    """
-
-    def __init__(self, **kwargs):
+    def reboot(self, reboot_type=RebootType.soft):
         """
-        Constructor
-        :param name: the name of the VM
-        :param flavor: the VM's flavor name
-        :param port_settings: the port configuration settings (required)
-        :param security_group_names: a set of names of the security groups to
-                                     add to the VM
-        :param floating_ip_settings: the floating IP configuration settings
-        :param sudo_user: the sudo user of the VM that will override the
-                          instance_settings.image_user when trying to
-                          connect to the VM
-        :param vm_boot_timeout: the amount of time a thread will sleep waiting
-                                for an instance to boot
-        :param vm_delete_timeout: the amount of time a thread will sleep
-                                  waiting for an instance to be deleted
-        :param ssh_connect_timeout: the amount of time a thread will sleep
-                                    waiting obtaining an SSH connection to a VM
-        :param availability_zone: the name of the compute server on which to
-                                  deploy the VM (optional)
-        :param userdata: the string contents of any optional cloud-init script
-                         to execute after the VM has been activated.
-                         This value may also contain a dict who's key value
-                         must contain the key 'cloud-init_file' which denotes
-                         the location of some file containing the cloud-init
-                         script
-        """
-        self.name = kwargs.get('name')
-        self.flavor = kwargs.get('flavor')
-        self.sudo_user = kwargs.get('sudo_user')
-        self.userdata = kwargs.get('userdata')
-
-        self.port_settings = list()
-        port_settings = kwargs.get('ports')
-        if not port_settings:
-            port_settings = kwargs.get('port_settings')
-        if port_settings:
-            for port_setting in port_settings:
-                if isinstance(port_setting, dict):
-                    self.port_settings.append(PortSettings(**port_setting))
-                elif isinstance(port_setting, PortSettings):
-                    self.port_settings.append(port_setting)
-
-        if kwargs.get('security_group_names'):
-            if isinstance(kwargs['security_group_names'], list):
-                self.security_group_names = kwargs['security_group_names']
-            elif isinstance(kwargs['security_group_names'], set):
-                self.security_group_names = kwargs['security_group_names']
-            elif isinstance(kwargs['security_group_names'], str):
-                self.security_group_names = [kwargs['security_group_names']]
-            else:
-                raise VmInstanceSettingsError(
-                    'Invalid data type for security_group_names attribute')
-        else:
-            self.security_group_names = set()
-
-        self.floating_ip_settings = list()
-        floating_ip_settings = kwargs.get('floating_ips')
-        if not floating_ip_settings:
-            floating_ip_settings = kwargs.get('floating_ip_settings')
-        if floating_ip_settings:
-            for floating_ip_config in floating_ip_settings:
-                if isinstance(floating_ip_config, FloatingIpSettings):
-                    self.floating_ip_settings.append(floating_ip_config)
-                else:
-                    self.floating_ip_settings.append(FloatingIpSettings(
-                        **floating_ip_config['floating_ip']))
-
-        if kwargs.get('vm_boot_timeout'):
-            self.vm_boot_timeout = kwargs['vm_boot_timeout']
-        else:
-            self.vm_boot_timeout = 900
-
-        if kwargs.get('vm_delete_timeout'):
-            self.vm_delete_timeout = kwargs['vm_delete_timeout']
-        else:
-            self.vm_delete_timeout = 300
-
-        if kwargs.get('ssh_connect_timeout'):
-            self.ssh_connect_timeout = kwargs['ssh_connect_timeout']
-        else:
-            self.ssh_connect_timeout = 180
+        Issues a reboot call
+        :param reboot_type: instance of
+                            snaps.openstack.utils.nova_utils.RebootType
+                            enumeration
+        :return:
+        """
+        nova_utils.reboot_server(
+            self._nova, self.__vm, reboot_type=reboot_type)
 
-        if kwargs.get('availability_zone'):
-            self.availability_zone = kwargs['availability_zone']
-        else:
-            self.availability_zone = None
 
-        if not self.name or not self.flavor:
-            raise VmInstanceSettingsError(
-                'Instance configuration requires the attributes: name, flavor')
+def generate_creator(os_creds, vm_inst, image_config, keypair_config=None):
+    """
+    Initializes an OpenStackVmInstance object
+    :param os_creds: the OpenStack credentials
+    :param vm_inst: the SNAPS-OO VmInst domain object
+    :param image_config: the associated ImageConfig object
+    :param keypair_config: the associated KeypairConfig object (optional)
+    :return: an initialized OpenStackVmInstance object
+    """
+    nova = nova_utils.nova_client(os_creds)
+    neutron = neutron_utils.neutron_client(os_creds)
+    derived_inst_config = settings_utils.create_vm_inst_config(
+        nova, neutron, vm_inst)
 
-        if len(self.port_settings) == 0:
-            raise VmInstanceSettingsError(
-                'Instance configuration requires port settings (aka. NICS)')
+    derived_inst_creator = OpenStackVmInstance(
+        os_creds, derived_inst_config, image_config, keypair_config)
+    derived_inst_creator.initialize()
+    return derived_inst_creator
 
 
-class FloatingIpSettings:
+class VmInstanceSettings(VmInstanceConfig):
     """
-    Class responsible for holding configuration settings for a floating IP
+    Deprecated, use snaps.config.vm_inst.VmInstanceConfig instead
     """
-
     def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: the name of the floating IP
-        :param port_name: the name of the router to the external network
-        :param router_name: the name of the router to the external network
-        :param subnet_name: the name of the subnet on which to attach the
-                            floating IP
-        :param provisioning: when true, this floating IP can be used for
-                             provisioning
-
-        TODO - provisioning flag is a hack as I have only observed a single
-        Floating IPs that actually works on an instance. Multiple floating IPs
-        placed on different subnets from the same port are especially
-        troublesome as you cannot predict which one will actually connect.
-        For now, it is recommended not to setup multiple floating IPs on an
-        instance unless absolutely necessary.
-        """
-        self.name = kwargs.get('name')
-        self.port_name = kwargs.get('port_name')
-        self.port_id = kwargs.get('port_id')
-        self.router_name = kwargs.get('router_name')
-        self.subnet_name = kwargs.get('subnet_name')
-        if kwargs.get('provisioning') is not None:
-            self.provisioning = kwargs['provisioning']
-        else:
-            self.provisioning = True
-
-        # if not self.name or not self.port_name or not self.router_name:
-        if not self.name or not self.router_name:
-            raise FloatingIpSettingsError(
-                'The attributes name, port_name and router_name are required')
+        from warnings import warn
+        warn('Use snaps.config.vm_inst.VmInstanceConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
-        if not self.port_name and not self.port_id:
-            raise FloatingIpSettingsError(
-                'The attributes port_name or port_id are required')
 
-
-class VmInstanceSettingsError(Exception):
+class FloatingIpSettings(FloatingIpConfig):
     """
-    Exception to be thrown when an VM instance settings are incorrect
+    Deprecated, use snaps.config.vm_inst.FloatingIpConfig instead
     """
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.vm_inst.FloatingIpConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
 
-class FloatingIpSettingsError(Exception):
+class VmInstanceCreationError(Exception):
     """
-    Exception to be thrown when an VM instance settings are incorrect
+    Exception to be thrown when an VM instance cannot be created
     """
 
 
-class VmInstanceCreationError(Exception):
+class FloatingIPAllocationError(Exception):
     """
-    Exception to be thrown when an VM instance cannot be created
+    Exception to be thrown when an VM instance cannot allocate a floating IP
     """
index 3869afc..a181a7b 100644 (file)
 import logging
 
 import os
-from neutronclient.common.utils import str2bool
 from novaclient.exceptions import NotFound
 
 from snaps import file_utils
+from snaps.config.keypair import KeypairConfig
 from snaps.openstack.openstack_creator import OpenStackComputeObject
 from snaps.openstack.utils import nova_utils
 
@@ -36,7 +36,7 @@ class OpenStackKeypair(OpenStackComputeObject):
         """
         Constructor - all parameters are required
         :param os_creds: The credentials to connect with OpenStack
-        :param keypair_settings: The settings used to create a keypair
+        :param keypair_settings: a KeypairConfig object
         """
         super(self.__class__, self).__init__(os_creds)
 
@@ -123,6 +123,7 @@ class OpenStackKeypair(OpenStackComputeObject):
                     self.keypair_settings.public_filepath)
                 os.chmod(expanded_path, 0o755)
                 os.remove(expanded_path)
+                logger.info('Deleted public key file [%s]', expanded_path)
             if (self.keypair_settings.private_filepath and
                     file_utils.file_exists(
                         self.keypair_settings.private_filepath)):
@@ -130,6 +131,7 @@ class OpenStackKeypair(OpenStackComputeObject):
                     self.keypair_settings.private_filepath)
                 os.chmod(expanded_path, 0o755)
                 os.remove(expanded_path)
+                logger.info('Deleted private key file [%s]', expanded_path)
 
     def get_keypair(self):
         """
@@ -139,47 +141,13 @@ class OpenStackKeypair(OpenStackComputeObject):
         return self.__keypair
 
 
-class KeypairSettings:
+class KeypairSettings(KeypairConfig):
     """
     Class representing a keypair configuration
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor - all parameters are optional
-        :param name: The keypair name.
-        :param public_filepath: The path to/from the filesystem where the
-                                public key file is or will be stored
-        :param private_filepath: The path where the generated private key file
-                                 will be stored
-        :param key_size: The number of bytes for the key size when it needs to
-                         be generated (Must be >=512 default 1024)
-        :param delete_on_clean: when True, the key files will be deleted when
-                                OpenStackKeypair#clean() is called
-        :return:
-        """
-
-        self.name = kwargs.get('name')
-        self.public_filepath = kwargs.get('public_filepath')
-        self.private_filepath = kwargs.get('private_filepath')
-        self.key_size = int(kwargs.get('key_size', 1024))
-
-        if kwargs.get('delete_on_clean') is not None:
-            if isinstance(kwargs.get('delete_on_clean'), bool):
-                self.delete_on_clean = kwargs.get('delete_on_clean')
-            else:
-                self.delete_on_clean = str2bool(kwargs.get('delete_on_clean'))
-        else:
-            self.delete_on_clean = None
-
-        if not self.name:
-            raise KeypairSettingsError('Name is a required attribute')
-
-        if self.key_size < 512:
-            raise KeypairSettingsError('key_size must be >=512')
-
-
-class KeypairSettingsError(Exception):
-    """
-    Exception to be thrown when keypair settings are incorrect
-    """
+        from warnings import warn
+        warn('Use snaps.config.keypair.KeypairConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
index 437f294..c9c58e8 100644 (file)
 # limitations under the License.
 import logging
 
-from neutronclient.common.exceptions import NotFound
+import enum
+from neutronclient.common.exceptions import NetworkNotFoundClient
 
+from snaps.config.network import NetworkConfig, SubnetConfig, PortConfig
 from snaps.openstack.openstack_creator import OpenStackNetworkObject
-from snaps.openstack.utils import keystone_utils, neutron_utils
+from snaps.openstack.utils import neutron_utils
 
 __author__ = 'spisarski'
 
@@ -41,7 +43,6 @@ class OpenStackNetwork(OpenStackNetworkObject):
 
         # Attributes instantiated on create()
         self.__network = None
-        self.__subnets = list()
 
     def initialize(self):
         """
@@ -54,15 +55,6 @@ class OpenStackNetwork(OpenStackNetworkObject):
             self._neutron, network_settings=self.network_settings,
             project_id=self.network_settings.get_project_id(self._os_creds))
 
-        if self.__network:
-            for subnet_setting in self.network_settings.subnet_settings:
-                sub_inst = neutron_utils.get_subnet(
-                    self._neutron, subnet_settings=subnet_setting)
-                if sub_inst:
-                    self.__subnets.append(sub_inst)
-                    logger.debug(
-                        "Subnet '%s' created successfully" % sub_inst.id)
-
         return self.__network
 
     def create(self):
@@ -77,19 +69,7 @@ class OpenStackNetwork(OpenStackNetworkObject):
             self.__network = neutron_utils.create_network(
                 self._neutron, self._os_creds, self.network_settings)
             logger.debug(
-                "Network '%s' created successfully" % self.__network.id)
-
-        for subnet_setting in self.network_settings.subnet_settings:
-            sub_inst = neutron_utils.get_subnet(
-                self._neutron, subnet_settings=subnet_setting)
-            if not sub_inst:
-                sub_inst = neutron_utils.create_subnet(
-                    self._neutron, subnet_setting, self._os_creds,
-                    self.__network)
-            if sub_inst:
-                self.__subnets.append(sub_inst)
-                logger.debug(
-                    "Subnet '%s' created successfully" % sub_inst.id)
+                'Network [%s] created successfully' % self.__network.id)
 
         return self.__network
 
@@ -97,23 +77,11 @@ class OpenStackNetwork(OpenStackNetworkObject):
         """
         Removes and deletes all items created in reverse order.
         """
-        for subnet in self.__subnets:
-            try:
-                logger.info(
-                    'Deleting subnet with name ' + subnet.name)
-                neutron_utils.delete_subnet(self._neutron, subnet)
-            except NotFound as e:
-                logger.warning(
-                    'Error deleting subnet with message - ' + str(e))
-                pass
-        self.__subnets = list()
-
         if self.__network:
             try:
                 neutron_utils.delete_network(self._neutron, self.__network)
-            except NotFound:
+            except NetworkNotFoundClient:
                 pass
-
             self.__network = None
 
     def get_network(self):
@@ -123,445 +91,54 @@ class OpenStackNetwork(OpenStackNetworkObject):
         """
         return self.__network
 
-    def get_subnets(self):
-        """
-        Returns the OpenStack subnet objects
-        :return:
-        """
-        return self.__subnets
 
-
-class NetworkSettings:
+class NetworkSettings(NetworkConfig):
     """
-    Class representing a network configuration
+    Class to hold the configuration settings required for creating OpenStack
+    Network objects
+    deprecated
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor - all parameters are optional
-        :param name: The network name.
-        :param admin_state_up: The administrative status of the network.
-                               True = up / False = down (default True)
-        :param shared: Boolean value indicating whether this network is shared
-                       across all projects/tenants. By default, only
-                       administrative users can change this value.
-        :param project_name: Admin-only. The name of the project that will own
-                             the network. This project can be different from
-                             the project that makes the create network request.
-                             However, only administrative users can specify a
-                             project ID other than their own. You cannot change
-                             this value through authorization policies.
-        :param external: when true, will setup an external network
-                         (default False).
-        :param network_type: the type of network (i.e. vlan|flat).
-        :param physical_network: the name of the physical network
-                                 (this is required when network_type is 'flat')
-        :param segmentation_id: the id of the segmentation
-                                 (this is required when network_type is 'vlan')
-        :param subnets or subnet_settings: List of SubnetSettings objects.
-        :return:
-        """
-
-        self.project_id = None
-
-        self.name = kwargs.get('name')
-        if kwargs.get('admin_state_up') is not None:
-            self.admin_state_up = bool(kwargs['admin_state_up'])
-        else:
-            self.admin_state_up = True
-
-        if kwargs.get('shared') is not None:
-            self.shared = bool(kwargs['shared'])
-        else:
-            self.shared = None
-
-        self.project_name = kwargs.get('project_name')
-
-        if kwargs.get('external') is not None:
-            self.external = bool(kwargs.get('external'))
-        else:
-            self.external = False
+        from warnings import warn
+        warn('Use snaps.config.network.NetworkConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
-        self.network_type = kwargs.get('network_type')
-        self.physical_network = kwargs.get('physical_network')
-        self.segmentation_id = kwargs.get('segmentation_id')
 
-        self.subnet_settings = list()
-        subnet_settings = kwargs.get('subnets')
-        if not subnet_settings:
-            subnet_settings = kwargs.get('subnet_settings')
-        if subnet_settings:
-            for subnet_config in subnet_settings:
-                if isinstance(subnet_config, SubnetSettings):
-                    self.subnet_settings.append(subnet_config)
-                else:
-                    self.subnet_settings.append(
-                        SubnetSettings(**subnet_config['subnet']))
-
-        if not self.name or len(self.name) < 1:
-            raise NetworkSettingsError('Name required for networks')
-
-    def get_project_id(self, os_creds):
-        """
-        Returns the project ID for a given project_name or None
-        :param os_creds: the credentials required for keystone client retrieval
-        :return: the ID or None
-        """
-        if self.project_id:
-            return self.project_id
-        else:
-            if self.project_name:
-                keystone = keystone_utils.keystone_client(os_creds)
-                project = keystone_utils.get_project(
-                    keystone=keystone, project_name=self.project_name)
-                if project:
-                    return project.id
-
-        return None
-
-    def dict_for_neutron(self, os_creds):
-        """
-        Returns a dictionary object representing this object.
-        This is meant to be converted into JSON designed for use by the Neutron
-        API
-        TODO - expand automated testing to exercise all parameters
-
-        :param os_creds: the OpenStack credentials
-        :return: the dictionary object
-        """
-        out = dict()
-
-        if self.name:
-            out['name'] = self.name
-        if self.admin_state_up is not None:
-            out['admin_state_up'] = self.admin_state_up
-        if self.shared:
-            out['shared'] = self.shared
-        if self.project_name:
-            project_id = self.get_project_id(os_creds)
-            if project_id:
-                out['tenant_id'] = project_id
-            else:
-                raise NetworkSettingsError(
-                    'Could not find project ID for project named - ' +
-                    self.project_name)
-        if self.network_type:
-            out['provider:network_type'] = self.network_type
-        if self.physical_network:
-            out['provider:physical_network'] = self.physical_network
-        if self.segmentation_id:
-            out['provider:segmentation_id'] = self.segmentation_id
-        if self.external:
-            out['router:external'] = self.external
-        return {'network': out}
-
-
-class NetworkSettingsError(Exception):
+class IPv6Mode(enum.Enum):
     """
-    Exception to be thrown when networks settings attributes are incorrect
+    A rule's direction
+    deprecated - use snaps.config.network.IPv6Mode
     """
+    slaac = 'slaac'
+    stateful = 'dhcpv6-stateful'
+    stateless = 'dhcpv6-stateless'
 
 
-class SubnetSettings:
+class SubnetSettings(SubnetConfig):
     """
-    Class representing a subnet configuration
+    Class to hold the configuration settings required for creating OpenStack
+    Subnet objects
+    deprecated
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor - all parameters are optional except cidr (subnet mask)
-        :param cidr: The CIDR. REQUIRED if config parameter is None
-        :param ip_version: The IP version, which is 4 or 6.
-        :param name: The subnet name.
-        :param project_name: The name of the project who owns the network.
-                             Only administrative users can specify a project ID
-                             other than their own. You cannot change this value
-                             through authorization policies.
-        :param start: The start address for the allocation pools.
-        :param end: The end address for the allocation pools.
-        :param gateway_ip: The gateway IP address.
-        :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is
-                            disabled.
-        :param dns_nameservers: A list of DNS name servers for the subnet.
-                                Specify each name server as an IP address
-                                and separate multiple entries with a space.
-                                For example [8.8.8.7 8.8.8.8].
-        :param host_routes: A list of host route dictionaries for the subnet.
-                            For example:
-                                "host_routes":[
-                                    {
-                                        "destination":"0.0.0.0/0",
-                                        "nexthop":"123.456.78.9"
-                                    },
-                                    {
-                                        "destination":"192.168.0.0/24",
-                                        "nexthop":"192.168.0.1"
-                                    }
-                                ]
-        :param destination: The destination for static route
-        :param nexthop: The next hop for the destination.
-        :param ipv6_ra_mode: A valid value is dhcpv6-stateful,
-                             dhcpv6-stateless, or slaac.
-        :param ipv6_address_mode: A valid value is dhcpv6-stateful,
-                                  dhcpv6-stateless, or slaac.
-        :raise: SubnetSettingsError when config does not have or cidr values
-                are None
-        """
-        self.cidr = kwargs.get('cidr')
-        if kwargs.get('ip_version'):
-            self.ip_version = kwargs['ip_version']
-        else:
-            self.ip_version = 4
-
-        # Optional attributes that can be set after instantiation
-        self.name = kwargs.get('name')
-        self.project_name = kwargs.get('project_name')
-        self.start = kwargs.get('start')
-        self.end = kwargs.get('end')
-        self.gateway_ip = kwargs.get('gateway_ip')
-        self.enable_dhcp = kwargs.get('enable_dhcp')
-
-        if kwargs.get('dns_nameservers'):
-            self.dns_nameservers = kwargs.get('dns_nameservers')
-        else:
-            self.dns_nameservers = ['8.8.8.8']
-
-        self.host_routes = kwargs.get('host_routes')
-        self.destination = kwargs.get('destination')
-        self.nexthop = kwargs.get('nexthop')
-        self.ipv6_ra_mode = kwargs.get('ipv6_ra_mode')
-        self.ipv6_address_mode = kwargs.get('ipv6_address_mode')
+        from warnings import warn
+        warn('Use snaps.config.network.SubnetConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
-        if not self.name or not self.cidr:
-            raise SubnetSettingsError('Name and cidr required for subnets')
 
-    def dict_for_neutron(self, os_creds, network=None):
-        """
-        Returns a dictionary object representing this object.
-        This is meant to be converted into JSON designed for use by the Neutron
-        API
-        :param os_creds: the OpenStack credentials
-        :param network: The network object on which the subnet will be created
-                        (optional)
-        :return: the dictionary object
-        """
-        out = {
-            'cidr': self.cidr,
-            'ip_version': self.ip_version,
-        }
-
-        if network:
-            out['network_id'] = network.id
-        if self.name:
-            out['name'] = self.name
-        if self.project_name:
-            keystone = keystone_utils.keystone_client(os_creds)
-            project = keystone_utils.get_project(
-                keystone=keystone, project_name=self.project_name)
-            project_id = None
-            if project:
-                project_id = project.id
-            if project_id:
-                out['tenant_id'] = project_id
-            else:
-                raise SubnetSettingsError(
-                    'Could not find project ID for project named - ' +
-                    self.project_name)
-        if self.start and self.end:
-            out['allocation_pools'] = [{'start': self.start, 'end': self.end}]
-        if self.gateway_ip:
-            out['gateway_ip'] = self.gateway_ip
-        if self.enable_dhcp is not None:
-            out['enable_dhcp'] = self.enable_dhcp
-        if self.dns_nameservers and len(self.dns_nameservers) > 0:
-            out['dns_nameservers'] = self.dns_nameservers
-        if self.host_routes and len(self.host_routes) > 0:
-            out['host_routes'] = self.host_routes
-        if self.destination:
-            out['destination'] = self.destination
-        if self.nexthop:
-            out['nexthop'] = self.nexthop
-        if self.ipv6_ra_mode:
-            out['ipv6_ra_mode'] = self.ipv6_ra_mode
-        if self.ipv6_address_mode:
-            out['ipv6_address_mode'] = self.ipv6_address_mode
-        return out
-
-
-class SubnetSettingsError(Exception):
-    """
-    Exception to be thrown when subnet settings attributes are incorrect
-    """
-
-
-class PortSettings:
+class PortSettings(PortConfig):
     """
-    Class representing a port configuration
+    Class to hold the configuration settings required for creating OpenStack
+    Subnet objects
+    deprecated
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: A symbolic name for the port (optional).
-        :param network_name: The name of the network on which to create the
-                             port (required).
-        :param admin_state_up: A boolean value denoting the administrative
-                               status of the port. True = up / False = down
-        :param project_name: The name of the project who owns the network.
-                             Only administrative users can specify a project ID
-                             other than their own. You cannot change this value
-                             through authorization policies.
-        :param mac_address: The MAC address. If you specify an address that is
-                            not valid, a Bad Request (400) status code is
-                            returned. If you do not specify a MAC address,
-                            OpenStack Networking tries to allocate one. If a
-                            failure occurs, a Service Unavailable (503) status
-                            code is returned.
-        :param ip_addrs: A list of dict objects where each contains two keys
-                         'subnet_name' and 'ip' values which will get mapped to
-                         self.fixed_ips. These values will be directly
-                         translated into the fixed_ips dict
-        :param fixed_ips: A dict where the key is the subnet IDs and value is
-                          the IP address to assign to the port
-        :param security_groups: One or more security group IDs.
-        :param allowed_address_pairs: A dictionary containing a set of zero or
-                                      more allowed address pairs. An address
-                                      pair contains an IP address and MAC
-                                      address.
-        :param opt_value: The extra DHCP option value.
-        :param opt_name: The extra DHCP option name.
-        :param device_owner: The ID of the entity that uses this port.
-                             For example, a DHCP agent.
-        :param device_id: The ID of the device that uses this port.
-                          For example, a virtual server.
-        :return:
-        """
-        if 'port' in kwargs:
-            kwargs = kwargs['port']
-
-        self.network = None
-
-        self.name = kwargs.get('name')
-        self.network_name = kwargs.get('network_name')
-
-        if kwargs.get('admin_state_up') is not None:
-            self.admin_state_up = bool(kwargs['admin_state_up'])
-        else:
-            self.admin_state_up = True
-
-        self.project_name = kwargs.get('project_name')
-        self.mac_address = kwargs.get('mac_address')
-        self.ip_addrs = kwargs.get('ip_addrs')
-        self.fixed_ips = kwargs.get('fixed_ips')
-        self.security_groups = kwargs.get('security_groups')
-        self.allowed_address_pairs = kwargs.get('allowed_address_pairs')
-        self.opt_value = kwargs.get('opt_value')
-        self.opt_name = kwargs.get('opt_name')
-        self.device_owner = kwargs.get('device_owner')
-        self.device_id = kwargs.get('device_id')
-
-        if not self.network_name:
-            raise PortSettingsError(
-                'The attribute network_name is required')
-
-    def __set_fixed_ips(self, neutron):
-        """
-        Sets the self.fixed_ips value
-        :param neutron: the Neutron client
-        :return: None
-        """
-        if not self.fixed_ips and self.ip_addrs:
-            self.fixed_ips = list()
-
-            for ip_addr_dict in self.ip_addrs:
-                subnet = neutron_utils.get_subnet(
-                    neutron, subnet_name=ip_addr_dict['subnet_name'])
-                if subnet and 'ip' in ip_addr_dict:
-                    self.fixed_ips.append({'ip_address': ip_addr_dict['ip'],
-                                           'subnet_id': subnet.id})
-                else:
-                    raise PortSettingsError(
-                        'Invalid port configuration, subnet does not exist '
-                        'with name - ' + ip_addr_dict['subnet_name'])
-
-    def dict_for_neutron(self, neutron, os_creds):
-        """
-        Returns a dictionary object representing this object.
-        This is meant to be converted into JSON designed for use by the Neutron
-        API
-
-        TODO - expand automated testing to exercise all parameters
-        :param neutron: the Neutron client
-        :param os_creds: the OpenStack credentials
-        :return: the dictionary object
-        """
-        self.__set_fixed_ips(neutron)
-
-        out = dict()
-
-        project_id = None
-        if self.project_name:
-            keystone = keystone_utils.keystone_client(os_creds)
-            project = keystone_utils.get_project(
-                keystone=keystone, project_name=self.project_name)
-            if project:
-                project_id = project.id
-
-        if not self.network:
-            self.network = neutron_utils.get_network(
-                neutron, network_name=self.network_name, project_id=project_id)
-        if not self.network:
-            raise PortSettingsError(
-                'Cannot locate network with name - ' + self.network_name)
-
-        out['network_id'] = self.network.id
-
-        if self.admin_state_up is not None:
-            out['admin_state_up'] = self.admin_state_up
-        if self.name:
-            out['name'] = self.name
-        if self.project_name:
-            if project_id:
-                out['tenant_id'] = project_id
-            else:
-                raise PortSettingsError(
-                    'Could not find project ID for project named - ' +
-                    self.project_name)
-        if self.mac_address:
-            out['mac_address'] = self.mac_address
-        if self.fixed_ips and len(self.fixed_ips) > 0:
-            out['fixed_ips'] = self.fixed_ips
-        if self.security_groups:
-            out['security_groups'] = self.security_groups
-        if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0:
-            out['allowed_address_pairs'] = self.allowed_address_pairs
-        if self.opt_value:
-            out['opt_value'] = self.opt_value
-        if self.opt_name:
-            out['opt_name'] = self.opt_name
-        if self.device_owner:
-            out['device_owner'] = self.device_owner
-        if self.device_id:
-            out['device_id'] = self.device_id
-        return {'port': out}
-
-    def __eq__(self, other):
-        return (self.name == other.name and
-                self.network_name == other.network_name and
-                self.admin_state_up == other.admin_state_up and
-                self.project_name == other.project_name and
-                self.mac_address == other.mac_address and
-                self.ip_addrs == other.ip_addrs and
-                self.fixed_ips == other.fixed_ips and
-                self.security_groups == other.security_groups and
-                self.allowed_address_pairs == other.allowed_address_pairs and
-                self.opt_value == other.opt_value and
-                self.opt_name == other.opt_name and
-                self.device_owner == other.device_owner and
-                self.device_id == other.device_id)
-
-
-class PortSettingsError(Exception):
-    """
-    Exception to be thrown when port settings attributes are incorrect
-    """
+        from warnings import warn
+        warn('Use snaps.config.network.PortConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
index 0cf6d4a..0349890 100644 (file)
@@ -16,6 +16,7 @@ import logging
 
 from keystoneclient.exceptions import NotFound, Conflict
 
+from snaps.config.project import ProjectConfig
 from snaps.openstack.openstack_creator import OpenStackIdentityObject
 from snaps.openstack.utils import keystone_utils, neutron_utils, nova_utils
 
@@ -169,44 +170,15 @@ class OpenStackProject(OpenStackIdentityObject):
         neutron_utils.update_quotas(neutron, self.__project.id, network_quotas)
 
 
-class ProjectSettings:
+class ProjectSettings(ProjectConfig):
     """
     Class to hold the configuration settings required for creating OpenStack
     project objects
+    deprecated
     """
 
     def __init__(self, **kwargs):
-
-        """
-        Constructor
-        :param name: the project's name (required)
-        :param domain or domain_name: the project's domain name
-                                      (default = 'Default').
-                                      Field is used for v3 clients
-        :param description: the description (optional)
-        :param users: list of users to associat project to (optional)
-        :param enabled: denotes whether or not the user is enabled
-                        (default True)
-        """
-
-        self.name = kwargs.get('name')
-        self.domain_name = kwargs.get(
-            'domain', kwargs.get('domain', 'Default'))
-
-        self.description = kwargs.get('description')
-        if kwargs.get('enabled') is not None:
-            self.enabled = kwargs['enabled']
-        else:
-            self.enabled = True
-
-        self.users = kwargs.get('users', list())
-
-        if not self.name:
-            raise ProjectSettingsError(
-                "The attribute name is required for ProjectSettings")
-
-
-class ProjectSettingsError(Exception):
-    """
-    Exception to be thrown when project settings attributes are incorrect
-    """
+        from warnings import warn
+        warn('Use snaps.config.project.ProjectConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
\ No newline at end of file
index ea96609..44e35a3 100644 (file)
@@ -18,6 +18,7 @@ import logging
 import enum
 from cinderclient.exceptions import NotFound
 
+from snaps.config.qos import QoSConfig
 from snaps.openstack.openstack_creator import OpenStackVolumeObject
 from snaps.openstack.utils import cinder_utils
 
@@ -101,68 +102,25 @@ class OpenStackQoS(OpenStackVolumeObject):
 class Consumer(enum.Enum):
     """
     QoS Specification consumer types
+    deprecated - use snaps.config.qos.Consumer
     """
     front_end = 'front-end'
     back_end = 'back-end'
     both = 'both'
 
 
-class QoSSettings:
-    def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: the qos's name (required)
-        :param consumer: the qos's consumer type (required)
-        :param specs: dict of key/values
-        """
-
-        self.name = kwargs.get('name')
-
-        if kwargs.get('consumer'):
-            self.consumer = map_consumer(kwargs['consumer'])
-        else:
-            self.consumer = None
-
-        self.specs = kwargs.get('specs')
-        if not self.specs:
-            self.specs = dict()
-
-        if not self.name or not self.consumer:
-            raise QoSSettingsError(
-                "The attributes name and consumer are required")
-
-
-def map_consumer(consumer):
-    """
-    Takes a the protocol value maps it to the Consumer enum. When None return
-    None
-    :param consumer: the value to map to the Enum
-    :return: the Protocol enum object
-    :raise: Exception if value is invalid
-    """
-    if not consumer:
-        return None
-    elif isinstance(consumer, Consumer):
-        return consumer
-    else:
-        proto_str = str(consumer)
-        if proto_str == 'front-end':
-            return Consumer.front_end
-        elif proto_str == 'back-end':
-            return Consumer.back_end
-        elif proto_str == 'both':
-            return Consumer.both
-        else:
-            raise QoSSettingsError('Invalid Consumer - ' + proto_str)
-
-
-class QoSSettingsError(Exception):
+class QoSSettings(QoSConfig):
     """
-    Exception to be thrown when an qos settings are incorrect
+    Class to hold the configuration settings required for creating OpenStack
+    QoS objects
+    deprecated
     """
 
-    def __init__(self, message):
-        Exception.__init__(self, message)
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.qos.QoSConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
 
 class QoSCreationError(Exception):
index 98e3e14..4f95c3b 100644 (file)
 import logging
 
 from neutronclient.common.exceptions import NotFound
-from snaps.openstack.create_network import PortSettings
+
+from snaps.config.router import RouterConfig
 from snaps.openstack.openstack_creator import OpenStackNetworkObject
-from snaps.openstack.utils import neutron_utils, keystone_utils
+from snaps.openstack.utils import neutron_utils
 
 __author__ = 'spisarski'
 
@@ -34,7 +35,7 @@ class OpenStackRouter(OpenStackNetworkObject):
         Constructor - all parameters are required
         :param os_creds: The credentials to connect with OpenStack
         :param router_settings: The settings used to create a router object
-                                (must be an instance of the RouterSettings
+                                (must be an instance of the RouterConfig
                                 class)
         """
         super(self.__class__, self).__init__(os_creds)
@@ -63,20 +64,21 @@ class OpenStackRouter(OpenStackNetworkObject):
         self.__router = neutron_utils.get_router(
             self._neutron, router_settings=self.router_settings)
 
-        for internal_subnet_name in self.router_settings.internal_subnets:
-            internal_subnet = neutron_utils.get_subnet(
-                self._neutron, subnet_name=internal_subnet_name)
-            if internal_subnet:
-                self.__internal_subnets.append(internal_subnet)
-            else:
-                raise RouterCreationError(
-                    'Subnet not found with name ' + internal_subnet_name)
-
-        for port_setting in self.router_settings.port_settings:
-            port = neutron_utils.get_port(
-                self._neutron, port_settings=port_setting)
-            if port:
-                self.__ports.append(port)
+        if self.__router:
+            for internal_subnet_name in self.router_settings.internal_subnets:
+                internal_subnet = neutron_utils.get_subnet(
+                    self._neutron, subnet_name=internal_subnet_name)
+                if internal_subnet:
+                    self.__internal_subnets.append(internal_subnet)
+                else:
+                    raise RouterCreationError(
+                        'Subnet not found with name ' + internal_subnet_name)
+
+            for port_setting in self.router_settings.port_settings:
+                port = neutron_utils.get_port(
+                    self._neutron, port_settings=port_setting)
+                if port:
+                    self.__ports.append(port)
 
         return self.__router
 
@@ -132,6 +134,8 @@ class OpenStackRouter(OpenStackNetworkObject):
                             'Error creating port with name - '
                             + port_setting.name)
 
+        self.__router = neutron_utils.get_router_by_id(
+            self._neutron, self.__router.id)
         return self.__router
 
     def clean(self):
@@ -190,101 +194,15 @@ class RouterCreationError(Exception):
     """
 
 
-class RouterSettings:
+class RouterSettings(RouterConfig):
     """
-    Class representing a router configuration
+    Class to hold the configuration settings required for creating OpenStack
+    router objects
+    deprecated
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor - all parameters are optional
-        :param name: The router name.
-        :param project_name: The name of the project who owns the network. Only
-                             administrative users can specify a project ID
-                             other than their own. You cannot change this value
-                             through authorization policies.
-        :param external_gateway: Name of the external network to which to route
-        :param admin_state_up: The administrative status of the router.
-                               True = up / False = down (default True)
-        :param external_fixed_ips: Dictionary containing the IP address
-                                   parameters.
-        :param internal_subnets: List of subnet names to which to connect this
-                                 router for Floating IP purposes
-        :param port_settings: List of PortSettings objects
-        :return:
-        """
-        self.name = kwargs.get('name')
-        self.project_name = kwargs.get('project_name')
-        self.external_gateway = kwargs.get('external_gateway')
-
-        self.admin_state_up = kwargs.get('admin_state_up')
-        self.enable_snat = kwargs.get('enable_snat')
-        self.external_fixed_ips = kwargs.get('external_fixed_ips')
-        if kwargs.get('internal_subnets'):
-            self.internal_subnets = kwargs['internal_subnets']
-        else:
-            self.internal_subnets = list()
-
-        self.port_settings = list()
-        if kwargs.get('interfaces', kwargs.get('port_settings')):
-            interfaces = kwargs.get('interfaces', kwargs.get('port_settings'))
-            for interface in interfaces:
-                if isinstance(interface, PortSettings):
-                    self.port_settings.append(interface)
-                else:
-                    self.port_settings.append(
-                        PortSettings(**interface['port']))
-
-        if not self.name:
-            raise RouterSettingsError('Name is required')
-
-    def dict_for_neutron(self, neutron, os_creds):
-        """
-        Returns a dictionary object representing this object.
-        This is meant to be converted into JSON designed for use by the Neutron
-        API
-
-        TODO - expand automated testing to exercise all parameters
-        :param neutron: The neutron client to retrieve external network
-                        information if necessary
-        :param os_creds: The OpenStack credentials
-        :return: the dictionary object
-        """
-        out = dict()
-        ext_gw = dict()
-
-        if self.name:
-            out['name'] = self.name
-        if self.project_name:
-            keystone = keystone_utils.keystone_client(os_creds)
-            project = keystone_utils.get_project(
-                keystone=keystone, project_name=self.project_name)
-            project_id = None
-            if project:
-                project_id = project.id
-            if project_id:
-                out['tenant_id'] = project_id
-            else:
-                raise RouterSettingsError(
-                    'Could not find project ID for project named - ' +
-                    self.project_name)
-        if self.admin_state_up is not None:
-            out['admin_state_up'] = self.admin_state_up
-        if self.external_gateway:
-            ext_net = neutron_utils.get_network(
-                neutron, network_name=self.external_gateway)
-            if ext_net:
-                ext_gw['network_id'] = ext_net.id
-                out['external_gateway_info'] = ext_gw
-            else:
-                raise RouterSettingsError(
-                    'Could not find the external network named - ' +
-                    self.external_gateway)
-
-        return {'router': out}
-
-
-class RouterSettingsError(Exception):
-    """
-    Exception to be thrown when router settings attributes are incorrect
-    """
+        from warnings import warn
+        warn('Use snaps.config.router.RouterConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
index 8218c83..7a20fe1 100644 (file)
@@ -17,6 +17,8 @@ import logging
 import enum
 from neutronclient.common.exceptions import NotFound, Conflict
 
+from snaps.config.security_group import (
+    SecurityGroupConfig, SecurityGroupRuleConfig)
 from snaps.openstack.openstack_creator import OpenStackNetworkObject
 from snaps.openstack.utils import keystone_utils
 from snaps.openstack.utils import neutron_utils
@@ -66,6 +68,9 @@ class OpenStackSecurityGroup(OpenStackNetworkObject):
                 rule_setting = self.__get_setting_from_rule(existing_rule)
                 self.__rules[rule_setting] = existing_rule
 
+            self.__security_group = neutron_utils.get_security_group_by_id(
+                self._neutron, self.__security_group.id)
+
         return self.__security_group
 
     def create(self):
@@ -112,15 +117,15 @@ class OpenStackSecurityGroup(OpenStackNetworkObject):
 
     def __generate_rule_setting(self, rule):
         """
-        Creates a SecurityGroupRuleSettings object for a given rule
+        Creates a SecurityGroupRuleConfig object for a given rule
         :param rule: the rule from which to create the
-                    SecurityGroupRuleSettings object
-        :return: the newly instantiated SecurityGroupRuleSettings object
+                    SecurityGroupRuleConfig object
+        :return: the newly instantiated SecurityGroupRuleConfig object
         """
         sec_grp = neutron_utils.get_security_group_by_id(
             self._neutron, rule.security_group_id)
 
-        setting = SecurityGroupRuleSettings(
+        setting = SecurityGroupRuleConfig(
             description=rule.description,
             direction=rule.direction,
             ethertype=rule.ethertype,
@@ -215,82 +220,24 @@ class OpenStackSecurityGroup(OpenStackNetworkObject):
         return None
 
 
-class SecurityGroupSettings:
+class SecurityGroupSettings(SecurityGroupConfig):
     """
-    Class representing a keypair configuration
+    Class to hold the configuration settings required for creating OpenStack
+    SecurityGroup objects
+    deprecated - use snaps.config.security_group.SecurityGroupConfig instead
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor - all parameters are optional
-        :param name: The keypair name.
-        :param description: The security group's description
-        :param project_name: The name of the project under which the security
-                             group will be created
-        :return:
-        """
-        self.name = kwargs.get('name')
-        self.description = kwargs.get('description')
-        self.project_name = kwargs.get('project_name')
-        self.rule_settings = list()
-
-        rule_settings = kwargs.get('rules')
-        if not rule_settings:
-            rule_settings = kwargs.get('rule_settings')
-
-        if rule_settings:
-            for rule_setting in rule_settings:
-                if isinstance(rule_setting, SecurityGroupRuleSettings):
-                    self.rule_settings.append(rule_setting)
-                else:
-                    rule_setting['sec_grp_name'] = self.name
-                    self.rule_settings.append(SecurityGroupRuleSettings(
-                        **rule_setting))
-
-        if not self.name:
-            raise SecurityGroupSettingsError('The attribute name is required')
-
-        for rule_setting in self.rule_settings:
-            if rule_setting.sec_grp_name is not self.name:
-                raise SecurityGroupSettingsError(
-                    'Rule settings must correspond with the name of this '
-                    'security group')
-
-    def dict_for_neutron(self, keystone):
-        """
-        Returns a dictionary object representing this object.
-        This is meant to be converted into JSON designed for use by the Neutron
-        API
-
-        TODO - expand automated testing to exercise all parameters
-        :param keystone: the Keystone client
-        :return: the dictionary object
-        """
-        out = dict()
-
-        if self.name:
-            out['name'] = self.name
-        if self.description:
-            out['description'] = self.description
-        if self.project_name:
-            project = keystone_utils.get_project(
-                keystone=keystone, project_name=self.project_name)
-            project_id = None
-            if project:
-                project_id = project.id
-            if project_id:
-                out['tenant_id'] = project_id
-            else:
-                raise SecurityGroupSettingsError(
-                    'Could not find project ID for project named - ' +
-                    self.project_name)
-
-        return {'security_group': out}
+        from warnings import warn
+        warn('Use snaps.config.security_group.SecurityGroupConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
 
 class Direction(enum.Enum):
     """
     A rule's direction
+    deprecated - use snaps.config.security_group.Direction
     """
     ingress = 'ingress'
     egress = 'egress'
@@ -299,270 +246,53 @@ class Direction(enum.Enum):
 class Protocol(enum.Enum):
     """
     A rule's protocol
+    deprecated - use snaps.config.security_group.Protocol
     """
-    icmp = 'icmp'
-    tcp = 'tcp'
-    udp = 'udp'
+    ah = 51
+    dccp = 33
+    egp = 8
+    esp = 50
+    gre = 47
+    icmp = 1
+    icmpv6 = 58
+    igmp = 2
+    ipv6_encap = 41
+    ipv6_frag = 44
+    ipv6_icmp = 58
+    ipv6_nonxt = 59
+    ipv6_opts = 60
+    ipv6_route = 43
+    ospf = 89
+    pgm = 113
+    rsvp = 46
+    sctp = 132
+    tcp = 6
+    udp = 17
+    udplite = 136
+    vrrp = 112
+    any = 'any'
     null = 'null'
 
 
 class Ethertype(enum.Enum):
     """
     A rule's ethertype
+    deprecated - use snaps.config.security_group.Ethertype
     """
     IPv4 = 4
     IPv6 = 6
 
 
-class SecurityGroupSettingsError(Exception):
-    """
-    Exception to be thrown when security group settings attributes are
-    invalid
-    """
-
-
-class SecurityGroupRuleSettings:
+class SecurityGroupRuleSettings(SecurityGroupRuleConfig):
     """
-    Class representing a keypair configuration
+    Class to hold the configuration settings required for creating OpenStack
+    SecurityGroupRule objects
+    deprecated - use snaps.config.security_group.SecurityGroupRuleConfig
+    instead
     """
 
     def __init__(self, **kwargs):
-        """
-        Constructor - all parameters are optional
-        :param sec_grp_name: The security group's name on which to add the
-                             rule. (required)
-        :param description: The rule's description
-        :param direction: An enumeration of type
-                          create_security_group.RULE_DIRECTION (required)
-        :param remote_group_id: The group ID to associate with this rule
-                                (this should be changed to group name once
-                                snaps support Groups) (optional)
-        :param protocol: An enumeration of type
-                         create_security_group.RULE_PROTOCOL or a string value
-                         that will be mapped accordingly (optional)
-        :param ethertype: An enumeration of type
-                          create_security_group.RULE_ETHERTYPE (optional)
-        :param port_range_min: The minimum port number in the range that is
-                               matched by the security group rule. When the
-                               protocol is TCP or UDP, this value must be <=
-                               port_range_max. When the protocol is ICMP, this
-                               value must be an ICMP type.
-        :param port_range_max: The maximum port number in the range that is
-                               matched by the security group rule. When the
-                               protocol is TCP or UDP, this value must be <=
-                               port_range_max. When the protocol is ICMP, this
-                               value must be an ICMP type.
-        :param sec_grp_rule: The OpenStack rule object to a security group rule
-                             object to associate
-                             (note: Cannot be set using the config object nor
-                             can I see any real uses for this parameter)
-        :param remote_ip_prefix: The remote IP prefix to associate with this
-                                 metering rule packet (optional)
-
-        TODO - Need to support the tenant...
-        """
-
-        self.description = kwargs.get('description')
-        self.sec_grp_name = kwargs.get('sec_grp_name')
-        self.remote_group_id = kwargs.get('remote_group_id')
-        self.direction = None
-        if kwargs.get('direction'):
-            self.direction = map_direction(kwargs['direction'])
-
-        self.protocol = None
-        if kwargs.get('protocol'):
-            self.protocol = map_protocol(kwargs['protocol'])
-        else:
-            self.protocol = Protocol.null
-
-        self.ethertype = None
-        if kwargs.get('ethertype'):
-            self.ethertype = map_ethertype(kwargs['ethertype'])
-
-        self.port_range_min = kwargs.get('port_range_min')
-        self.port_range_max = kwargs.get('port_range_max')
-        self.remote_ip_prefix = kwargs.get('remote_ip_prefix')
-
-        if not self.direction or not self.sec_grp_name:
-            raise SecurityGroupRuleSettingsError(
-                'direction and sec_grp_name are required')
-
-    def dict_for_neutron(self, neutron):
-        """
-        Returns a dictionary object representing this object.
-        This is meant to be converted into JSON designed for use by the Neutron
-        API
-
-        :param neutron: the neutron client for performing lookups
-        :return: the dictionary object
-        """
-        out = dict()
-
-        if self.description:
-            out['description'] = self.description
-        if self.direction:
-            out['direction'] = self.direction.name
-        if self.port_range_min:
-            out['port_range_min'] = self.port_range_min
-        if self.port_range_max:
-            out['port_range_max'] = self.port_range_max
-        if self.ethertype:
-            out['ethertype'] = self.ethertype.name
-        if self.protocol and self.protocol.name != 'null':
-            out['protocol'] = self.protocol.name
-        if self.sec_grp_name:
-            sec_grp = neutron_utils.get_security_group(
-                neutron, sec_grp_name=self.sec_grp_name)
-            if sec_grp:
-                out['security_group_id'] = sec_grp.id
-            else:
-                raise SecurityGroupRuleSettingsError(
-                    'Cannot locate security group with name - ' +
-                    self.sec_grp_name)
-        if self.remote_group_id:
-            out['remote_group_id'] = self.remote_group_id
-        if self.remote_ip_prefix:
-            out['remote_ip_prefix'] = self.remote_ip_prefix
-
-        return {'security_group_rule': out}
-
-    def rule_eq(self, rule):
-        """
-        Returns True if this setting created the rule
-        :param rule: the rule to evaluate
-        :return: T/F
-        """
-        if self.description is not None:
-            if (rule.description is not None and
-                    rule.description != ''):
-                return False
-        elif self.description != rule.description:
-            if rule.description != '':
-                return False
-
-        if self.direction.name != rule.direction:
-            return False
-
-        if self.ethertype and rule.ethertype:
-            if self.ethertype.name != rule.ethertype:
-                return False
-
-        if self.port_range_min and rule.port_range_min:
-            if self.port_range_min != rule.port_range_min:
-                return False
-
-        if self.port_range_max and rule.port_range_max:
-            if self.port_range_max != rule.port_range_max:
-                return False
-
-        if self.protocol and rule.protocol:
-            if self.protocol.name != rule.protocol:
-                return False
-
-        if self.remote_group_id and rule.remote_group_id:
-            if self.remote_group_id != rule.remote_group_id:
-                return False
-
-        if self.remote_ip_prefix and rule.remote_ip_prefix:
-            if self.remote_ip_prefix != rule.remote_ip_prefix:
-                return False
-
-        return True
-
-    def __eq__(self, other):
-        return (
-            self.description == other.description and
-            self.direction == other.direction and
-            self.port_range_min == other.port_range_min and
-            self.port_range_max == other.port_range_max and
-            self.ethertype == other.ethertype and
-            self.protocol == other.protocol and
-            self.sec_grp_name == other.sec_grp_name and
-            self.remote_group_id == other.remote_group_id and
-            self.remote_ip_prefix == other.remote_ip_prefix)
-
-    def __hash__(self):
-        return hash((self.sec_grp_name, self.description, self.direction,
-                     self.remote_group_id,
-                     self.protocol, self.ethertype, self.port_range_min,
-                     self.port_range_max, self.remote_ip_prefix))
-
-
-def map_direction(direction):
-    """
-    Takes a the direction value maps it to the Direction enum. When None return
-    None
-    :param direction: the direction value
-    :return: the Direction enum object
-    :raise: Exception if value is invalid
-    """
-    if not direction:
-        return None
-    if isinstance(direction, Direction):
-        return direction
-    else:
-        dir_str = str(direction)
-        if dir_str == 'egress':
-            return Direction.egress
-        elif dir_str == 'ingress':
-            return Direction.ingress
-        else:
-            raise SecurityGroupRuleSettingsError(
-                'Invalid Direction - ' + dir_str)
-
-
-def map_protocol(protocol):
-    """
-    Takes a the protocol value maps it to the Protocol enum. When None return
-    None
-    :param protocol: the protocol value
-    :return: the Protocol enum object
-    :raise: Exception if value is invalid
-    """
-    if not protocol:
-        return None
-    elif isinstance(protocol, Protocol):
-        return protocol
-    else:
-        proto_str = str(protocol)
-        if proto_str == 'icmp':
-            return Protocol.icmp
-        elif proto_str == 'tcp':
-            return Protocol.tcp
-        elif proto_str == 'udp':
-            return Protocol.udp
-        elif proto_str == 'null':
-            return Protocol.null
-        else:
-            raise SecurityGroupRuleSettingsError(
-                'Invalid Protocol - ' + proto_str)
-
-
-def map_ethertype(ethertype):
-    """
-    Takes a the ethertype value maps it to the Ethertype enum. When None return
-    None
-    :param ethertype: the ethertype value
-    :return: the Ethertype enum object
-    :raise: Exception if value is invalid
-    """
-    if not ethertype:
-        return None
-    elif isinstance(ethertype, Ethertype):
-        return ethertype
-    else:
-        eth_str = str(ethertype)
-        if eth_str == 'IPv6':
-            return Ethertype.IPv6
-        elif eth_str == 'IPv4':
-            return Ethertype.IPv4
-        else:
-            raise SecurityGroupRuleSettingsError(
-                'Invalid Ethertype - ' + eth_str)
-
-
-class SecurityGroupRuleSettingsError(Exception):
-    """
-    Exception to be thrown when security group rule settings attributes are
-    invalid
-    """
+        from warnings import warn
+        warn('Use snaps.config.security_group.SecurityGroupRuleConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
index c161973..d4a6b10 100644 (file)
@@ -18,9 +18,18 @@ import time
 
 from heatclient.exc import HTTPNotFound
 
+import snaps
+from snaps.config.stack import StackConfig
+from snaps.openstack.create_flavor import OpenStackFlavor
 from snaps.openstack.create_instance import OpenStackVmInstance
+from snaps.openstack.create_keypairs import OpenStackKeypair
+from snaps.openstack.create_security_group import OpenStackSecurityGroup
+from snaps.openstack.create_router import OpenStackRouter
+from snaps.openstack.create_volume import OpenStackVolume
+from snaps.openstack.create_volume_type import OpenStackVolumeType
 from snaps.openstack.openstack_creator import OpenStackCloudObject
-from snaps.openstack.utils import nova_utils, settings_utils, glance_utils
+from snaps.openstack.utils import (
+    nova_utils, settings_utils, glance_utils, cinder_utils)
 
 from snaps.openstack.create_network import OpenStackNetwork
 from snaps.openstack.utils import heat_utils, neutron_utils
@@ -29,14 +38,6 @@ __author__ = 'spisarski'
 
 logger = logging.getLogger('create_stack')
 
-STACK_DELETE_TIMEOUT = 1200
-STACK_COMPLETE_TIMEOUT = 1200
-POLL_INTERVAL = 3
-STATUS_CREATE_FAILED = 'CREATE_FAILED'
-STATUS_CREATE_COMPLETE = 'CREATE_COMPLETE'
-STATUS_DELETE_COMPLETE = 'DELETE_COMPLETE'
-STATUS_DELETE_FAILED = 'DELETE_FAILED'
-
 
 class OpenStackHeatStack(OpenStackCloudObject, object):
     """
@@ -49,11 +50,9 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
         Constructor
         :param os_creds: The OpenStack connection credentials
         :param stack_settings: The stack settings
-        :param image_settings: A list of ImageSettings objects that were used
-                               for spawning this stack
-        :param image_settings: A list of ImageSettings objects that were used
+        :param image_settings: A list of ImageConfig objects that were used
                                for spawning this stack
-        :param keypair_settings: A list of KeypairSettings objects that were
+        :param keypair_settings: A list of KeypairConfig objects that were
                                  used for spawning this stack
         :return:
         """
@@ -95,25 +94,22 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
         self.initialize()
 
         if self.__stack:
-            logger.info('Found stack with name - ' + self.stack_settings.name)
+            logger.info('Found stack with name - %s', self.stack_settings.name)
             return self.__stack
         else:
             self.__stack = heat_utils.create_stack(self.__heat_cli,
                                                    self.stack_settings)
             logger.info(
-                'Created stack with name - ' + self.stack_settings.name)
+                'Created stack with name - %s', self.stack_settings.name)
             if self.__stack and self.stack_complete(block=True):
-                logger.info(
-                    'Stack is now active with name - ' +
-                    self.stack_settings.name)
+                logger.info('Stack is now active with name - %s',
+                            self.stack_settings.name)
                 return self.__stack
             else:
                 status = heat_utils.get_stack_status_reason(self.__heat_cli,
                                                             self.__stack.id)
-                logger.error(
-                    'ERROR: STACK CREATION FAILED: ' + status)
-                raise StackCreationError(
-                    'Failure while creating stack')
+                logger.error('ERROR: STACK CREATION FAILED: %s', status)
+                raise StackCreationError('Failure while creating stack')
 
     def clean(self):
         """
@@ -122,7 +118,7 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
         """
         if self.__stack:
             try:
-                logger.info('Deleting stack - %s' + self.__stack.name)
+                logger.info('Deleting stack - %s', self.__stack.name)
                 heat_utils.delete_stack(self.__heat_cli, self.__stack)
 
                 try:
@@ -181,7 +177,7 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
         return heat_utils.get_stack_status(self.__heat_cli, self.__stack.id)
 
     def stack_complete(self, block=False, timeout=None,
-                       poll_interval=POLL_INTERVAL):
+                       poll_interval=snaps.config.stack.POLL_INTERVAL):
         """
         Returns true when the stack status returns the value of
         expected_status_code
@@ -193,11 +189,13 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
         """
         if not timeout:
             timeout = self.stack_settings.stack_create_timeout
-        return self._stack_status_check(STATUS_CREATE_COMPLETE, block, timeout,
-                                        poll_interval, STATUS_CREATE_FAILED)
+        return self._stack_status_check(
+            snaps.config.stack.STATUS_CREATE_COMPLETE, block, timeout,
+            poll_interval, snaps.config.stack.STATUS_CREATE_FAILED)
 
-    def stack_deleted(self, block=False, timeout=STACK_DELETE_TIMEOUT,
-                      poll_interval=POLL_INTERVAL):
+    def stack_deleted(self, block=False,
+                      timeout=snaps.config.stack.STACK_DELETE_TIMEOUT,
+                      poll_interval=snaps.config.stack.POLL_INTERVAL):
         """
         Returns true when the stack status returns the value of
         expected_status_code
@@ -207,8 +205,9 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
         :param poll_interval: The polling interval in seconds
         :return: T/F
         """
-        return self._stack_status_check(STATUS_DELETE_COMPLETE, block, timeout,
-                                        poll_interval, STATUS_DELETE_FAILED)
+        return self._stack_status_check(
+            snaps.config.stack.STATUS_DELETE_COMPLETE, block, timeout,
+            poll_interval, snaps.config.stack.STATUS_DELETE_FAILED)
 
     def get_network_creators(self):
         """
@@ -224,7 +223,7 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
             self.__heat_cli, neutron, self.__stack)
 
         for stack_network in stack_networks:
-            net_settings = settings_utils.create_network_settings(
+            net_settings = settings_utils.create_network_config(
                 neutron, stack_network)
             net_creator = OpenStackNetwork(self._os_creds, net_settings)
             out.append(net_creator)
@@ -232,6 +231,50 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
 
         return out
 
+    def get_security_group_creators(self):
+        """
+        Returns a list of security group creator objects as configured by the
+        heat template
+        :return: list() of OpenStackNetwork objects
+        """
+
+        neutron = neutron_utils.neutron_client(self._os_creds)
+
+        out = list()
+        stack_security_groups = heat_utils.get_stack_security_groups(
+            self.__heat_cli, neutron, self.__stack)
+
+        for stack_security_group in stack_security_groups:
+            settings = settings_utils.create_security_group_config(
+                neutron, stack_security_group)
+            creator = OpenStackSecurityGroup(self._os_creds, settings)
+            out.append(creator)
+            creator.initialize()
+
+        return out
+
+    def get_router_creators(self):
+        """
+        Returns a list of router creator objects as configured by the heat
+        template
+        :return: list() of OpenStackRouter objects
+        """
+
+        neutron = neutron_utils.neutron_client(self._os_creds)
+
+        out = list()
+        stack_routers = heat_utils.get_stack_routers(
+            self.__heat_cli, neutron, self.__stack)
+
+        for routers in stack_routers:
+            settings = settings_utils.create_router_config(
+                neutron, routers)
+            creator = OpenStackRouter(self._os_creds, settings)
+            out.append(creator)
+            creator.initialize()
+
+        return out
+
     def get_vm_inst_creators(self, heat_keypair_option=None):
         """
         Returns a list of VM Instance creator objects as configured by the heat
@@ -241,19 +284,19 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
 
         out = list()
         nova = nova_utils.nova_client(self._os_creds)
+        neutron = neutron_utils.neutron_client(self._os_creds)
 
         stack_servers = heat_utils.get_stack_servers(
-            self.__heat_cli, nova, self.__stack)
+            self.__heat_cli, nova, neutron, self.__stack)
 
-        neutron = neutron_utils.neutron_client(self._os_creds)
         glance = glance_utils.glance_client(self._os_creds)
 
         for stack_server in stack_servers:
-            vm_inst_settings = settings_utils.create_vm_inst_settings(
+            vm_inst_settings = settings_utils.create_vm_inst_config(
                 nova, neutron, stack_server)
-            image_settings = settings_utils.determine_image_settings(
+            image_settings = settings_utils.determine_image_config(
                 glance, stack_server, self.image_settings)
-            keypair_settings = settings_utils.determine_keypair_settings(
+            keypair_settings = settings_utils.determine_keypair_config(
                 self.__heat_cli, self.__stack, stack_server,
                 keypair_settings=self.keypair_settings,
                 priv_key_key=heat_keypair_option)
@@ -265,6 +308,113 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
 
         return out
 
+    def get_volume_creators(self):
+        """
+        Returns a list of Volume creator objects as configured by the heat
+        template
+        :return: list() of OpenStackVolume objects
+        """
+
+        out = list()
+        cinder = cinder_utils.cinder_client(self._os_creds)
+
+        volumes = heat_utils.get_stack_volumes(
+            self.__heat_cli, cinder, self.__stack)
+
+        for volume in volumes:
+            settings = settings_utils.create_volume_config(volume)
+            creator = OpenStackVolume(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume creator - %s', e)
+
+        return out
+
+    def get_volume_type_creators(self):
+        """
+        Returns a list of VolumeType creator objects as configured by the heat
+        template
+        :return: list() of OpenStackVolumeType objects
+        """
+
+        out = list()
+        cinder = cinder_utils.cinder_client(self._os_creds)
+
+        vol_types = heat_utils.get_stack_volume_types(
+            self.__heat_cli, cinder, self.__stack)
+
+        for volume in vol_types:
+            settings = settings_utils.create_volume_type_config(volume)
+            creator = OpenStackVolumeType(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume type creator - %s',
+                    e)
+
+        return out
+
+    def get_keypair_creators(self, outputs_pk_key=None):
+        """
+        Returns a list of keypair creator objects as configured by the heat
+        template
+        :return: list() of OpenStackKeypair objects
+        """
+
+        out = list()
+        nova = nova_utils.nova_client(self._os_creds)
+
+        keypairs = heat_utils.get_stack_keypairs(
+            self.__heat_cli, nova, self.__stack)
+
+        for keypair in keypairs:
+            settings = settings_utils.create_keypair_config(
+                self.__heat_cli, self.__stack, keypair, outputs_pk_key)
+            creator = OpenStackKeypair(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume type creator - %s',
+                    e)
+
+        return out
+
+    def get_flavor_creators(self):
+        """
+        Returns a list of Flavor creator objects as configured by the heat
+        template
+        :return: list() of OpenStackFlavor objects
+        """
+
+        out = list()
+        nova = nova_utils.nova_client(self._os_creds)
+
+        flavors = heat_utils.get_stack_flavors(
+            self.__heat_cli, nova, self.__stack)
+
+        for flavor in flavors:
+            settings = settings_utils.create_flavor_config(flavor)
+            creator = OpenStackFlavor(self._os_creds, settings)
+            out.append(creator)
+
+            try:
+                creator.initialize()
+            except Exception as e:
+                logger.error(
+                    'Unexpected error initializing volume creator - %s', e)
+
+        return out
+
     def _stack_status_check(self, expected_status_code, block, timeout,
                             poll_interval, fail_status):
         """
@@ -302,7 +452,8 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
             'Timeout checking for stack status for ' + expected_status_code)
         return False
 
-    def _status(self, expected_status_code, fail_status=STATUS_CREATE_FAILED):
+    def _status(self, expected_status_code,
+                fail_status=snaps.config.stack.STATUS_CREATE_FAILED):
         """
         Returns True when active else False
         :param expected_status_code: stack status evaluated with this string
@@ -316,52 +467,38 @@ class OpenStackHeatStack(OpenStackCloudObject, object):
             return False
 
         if fail_status and status == fail_status:
+            resources = heat_utils.get_resources(
+                self.__heat_cli, self.__stack.id)
+            logger.error('Stack %s failed', self.__stack.name)
+            for resource in resources:
+                if (resource.status !=
+                        snaps.config.stack.STATUS_CREATE_COMPLETE):
+                    logger.error(
+                        'Resource: [%s] status: [%s] reason: [%s]',
+                        resource.name, resource.status, resource.status_reason)
+                else:
+                    logger.debug(
+                        'Resource: [%s] status: [%s] reason: [%s]',
+                        resource.name, resource.status, resource.status_reason)
+
             raise StackError('Stack had an error')
         logger.debug('Stack status is - ' + status)
         return status == expected_status_code
 
 
-class StackSettings:
-    def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: the stack's name (required)
-        :param template: the heat template in dict() format (required if
-                         template_path attribute is None)
-        :param template_path: the location of the heat template file (required
-                              if template attribute is None)
-        :param env_values: k/v pairs of strings for substitution of template
-                           default values (optional)
-        """
-
-        self.name = kwargs.get('name')
-        self.template = kwargs.get('template')
-        self.template_path = kwargs.get('template_path')
-        self.env_values = kwargs.get('env_values')
-        if 'stack_create_timeout' in kwargs:
-            self.stack_create_timeout = kwargs['stack_create_timeout']
-        else:
-            self.stack_create_timeout = STACK_COMPLETE_TIMEOUT
-
-        if not self.name:
-            raise StackSettingsError('name is required')
-
-        if not self.template and not self.template_path:
-            raise StackSettingsError('A Heat template is required')
-
-    def __eq__(self, other):
-        return (self.name == other.name and
-                self.template == other.template and
-                self.template_path == other.template_path and
-                self.env_values == other.env_values and
-                self.stack_create_timeout == other.stack_create_timeout)
-
-
-class StackSettingsError(Exception):
+class StackSettings(StackConfig):
     """
-    Exception to be thrown when an stack settings are incorrect
+    Class to hold the configuration settings required for creating OpenStack
+    stack objects
+    deprecated
     """
 
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.stack.StackConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
+
 
 class StackCreationError(Exception):
     """
index bcf4790..5da3a5e 100644 (file)
@@ -16,6 +16,7 @@ import logging
 
 from keystoneclient.exceptions import NotFound
 
+from snaps.config.user import UserConfig
 from snaps.openstack.openstack_creator import OpenStackIdentityObject
 from snaps.openstack.os_credentials import OSCreds
 from snaps.openstack.utils import keystone_utils
@@ -96,6 +97,11 @@ class OpenStackUser(OpenStackIdentityObject):
             auth_url=self._os_creds.auth_url,
             project_name=project_name,
             identity_api_version=self._os_creds.identity_api_version,
+            image_api_version=self._os_creds.image_api_version,
+            network_api_version=self._os_creds.network_api_version,
+            compute_api_version=self._os_creds.compute_api_version,
+            heat_api_version=self._os_creds.heat_api_version,
+            volume_api_version=self._os_creds.volume_api_version,
             user_domain_name=self._os_creds.user_domain_name,
             user_domain_id=self._os_creds.user_domain_id,
             project_domain_name=self._os_creds.project_domain_name,
@@ -105,43 +111,15 @@ class OpenStackUser(OpenStackIdentityObject):
             cacert=self._os_creds.cacert)
 
 
-class UserSettings:
-    def __init__(self, **kwargs):
-
-        """
-        Constructor
-        :param name: the user's name (required)
-        :param password: the user's password (required)
-        :param project_name: the user's primary project name (optional)
-        :param domain_name: the user's domain name (default='default'). For v3
-                            APIs
-        :param email: the user's email address (optional)
-        :param enabled: denotes whether or not the user is enabled
-                        (default True)
-        :param roles: dict where key is the role's name and value is the name
-                      the project to associate with the role
-        """
-
-        self.name = kwargs.get('name')
-        self.password = kwargs.get('password')
-        self.project_name = kwargs.get('project_name')
-        self.email = kwargs.get('email')
-        self.domain_name = kwargs.get('domain_name', 'Default')
-        self.enabled = kwargs.get('enabled', True)
-        self.roles = kwargs.get('roles', dict())
-
-        if not self.name or not self.password:
-            raise UserSettingsException(
-                'The attributes name and password are required for '
-                'UserSettings')
-
-        if not isinstance(self.enabled, bool):
-            raise UserSettingsException('The attribute enabled must be of type'
-                                        ' boolean')
-
-
-class UserSettingsException(Exception):
+class UserSettings(UserConfig):
     """
-    Raised when there is a problem with the values set in the UserSettings
-    class
+    Class to hold the configuration settings required for creating OpenStack
+    user objects
+    deprecated
     """
+
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.user.UserConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
diff --git a/snaps/openstack/create_volume.py b/snaps/openstack/create_volume.py
new file mode 100644 (file)
index 0000000..c134ca1
--- /dev/null
@@ -0,0 +1,252 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import time
+
+from cinderclient.exceptions import NotFound
+
+from snaps.config.volume import VolumeConfig
+from snaps.openstack.openstack_creator import OpenStackVolumeObject
+from snaps.openstack.utils import cinder_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_volume')
+
+VOLUME_ACTIVE_TIMEOUT = 300
+VOLUME_DELETE_TIMEOUT = 60
+POLL_INTERVAL = 3
+STATUS_ACTIVE = 'available'
+STATUS_IN_USE = 'in-use'
+STATUS_FAILED = 'error'
+STATUS_DELETED = 'deleted'
+
+
+class OpenStackVolume(OpenStackVolumeObject):
+    """
+    Class responsible for managing an volume in OpenStack
+    """
+
+    def __init__(self, os_creds, volume_settings):
+        """
+        Constructor
+        :param os_creds: The OpenStack connection credentials
+        :param volume_settings: The volume settings
+        :return:
+        """
+        super(self.__class__, self).__init__(os_creds)
+
+        self.volume_settings = volume_settings
+        self.__volume = None
+
+    def initialize(self):
+        """
+        Loads the existing Volume
+        :return: The Volume domain object or None
+        """
+        super(self.__class__, self).initialize()
+
+        self.__volume = cinder_utils.get_volume(
+            self._cinder, volume_settings=self.volume_settings)
+        return self.__volume
+
+    def create(self, block=False):
+        """
+        Creates the volume in OpenStack if it does not already exist and
+        returns the domain Volume object
+        :return: The Volume domain object or None
+        """
+        self.initialize()
+
+        if not self.__volume:
+            self.__volume = cinder_utils.create_volume(
+                self._cinder, self.volume_settings)
+
+            logger.info(
+                'Created volume with name - %s', self.volume_settings.name)
+            if self.__volume:
+                if block:
+                    if self.volume_active(block=True):
+                        logger.info('Volume is now active with name - %s',
+                                    self.volume_settings.name)
+                        return self.__volume
+                    else:
+                        raise VolumeCreationError(
+                            'Volume was not created or activated in the '
+                            'alloted amount of time')
+        else:
+            logger.info('Did not create volume due to cleanup mode')
+
+        return self.__volume
+
+    def clean(self):
+        """
+        Cleanse environment of all artifacts
+        :return: void
+        """
+        if self.__volume:
+            try:
+                if self.volume_active():
+                    cinder_utils.delete_volume(self._cinder, self.__volume)
+                else:
+                    logger.warn('Timeout waiting to delete volume %s',
+                                self.__volume.name)
+            except NotFound:
+                pass
+
+            try:
+                if self.volume_deleted(block=True):
+                    logger.info(
+                        'Volume has been properly deleted with name - %s',
+                        self.volume_settings.name)
+                    self.__vm = None
+                else:
+                    logger.error(
+                        'Volume not deleted within the timeout period of %s '
+                        'seconds', VOLUME_DELETE_TIMEOUT)
+            except Exception as e:
+                logger.error(
+                    'Unexpected error while checking VM instance status - %s',
+                    e)
+
+        self.__volume = None
+
+    def get_volume(self):
+        """
+        Returns the domain Volume object as it was populated when create() was
+        called
+        :return: the object
+        """
+        return self.__volume
+
+    def volume_active(self, block=False, timeout=VOLUME_ACTIVE_TIMEOUT,
+                      poll_interval=POLL_INTERVAL):
+        """
+        Returns true when the volume status returns the value of
+        expected_status_code
+        :param block: When true, thread will block until active or timeout
+                      value in seconds has been exceeded (False)
+        :param timeout: The timeout value
+        :param poll_interval: The polling interval in seconds
+        :return: T/F
+        """
+        return self._volume_status_check(STATUS_ACTIVE, block, timeout,
+                                         poll_interval)
+
+    def volume_in_use(self):
+        """
+        Returns true when the volume status returns the value of
+        expected_status_code
+        :return: T/F
+        """
+        return self._volume_status_check(STATUS_IN_USE, False, 0, 0)
+
+    def volume_deleted(self, block=False, poll_interval=POLL_INTERVAL):
+        """
+        Returns true when the VM status returns the value of
+        expected_status_code or instance retrieval throws a NotFound exception.
+        :param block: When true, thread will block until active or timeout
+                      value in seconds has been exceeded (False)
+        :param poll_interval: The polling interval in seconds
+        :return: T/F
+        """
+        try:
+            return self._volume_status_check(
+                STATUS_DELETED, block, VOLUME_DELETE_TIMEOUT, poll_interval)
+        except NotFound as e:
+            logger.debug(
+                "Volume not found when querying status for %s with message "
+                "%s", STATUS_DELETED, e)
+            return True
+
+    def _volume_status_check(self, expected_status_code, block, timeout,
+                             poll_interval):
+        """
+        Returns true when the volume status returns the value of
+        expected_status_code
+        :param expected_status_code: instance status evaluated with this string
+                                     value
+        :param block: When true, thread will block until active or timeout
+                      value in seconds has been exceeded (False)
+        :param timeout: The timeout value
+        :param poll_interval: The polling interval in seconds
+        :return: T/F
+        """
+        # sleep and wait for volume status change
+        if block:
+            start = time.time()
+        else:
+            start = time.time() - timeout + 1
+
+        while timeout > time.time() - start:
+            status = self._status(expected_status_code)
+            if status:
+                logger.debug('Volume is active with name - %s',
+                             self.volume_settings.name)
+                return True
+
+            logger.debug('Retry querying volume status in %s seconds',
+                         str(poll_interval))
+            time.sleep(poll_interval)
+            logger.debug('Volume status query timeout in %s',
+                         str(timeout - (time.time() - start)))
+
+        logger.error(
+            'Timeout checking for volume status for ' + expected_status_code)
+        return False
+
+    def _status(self, expected_status_code):
+        """
+        Returns True when active else False
+        :param expected_status_code: instance status evaluated with this string
+                                     value
+        :return: T/F
+        """
+        status = cinder_utils.get_volume_status(self._cinder, self.__volume)
+        if not status:
+            logger.warning(
+                'Cannot volume status for volume with ID - %s',
+                self.__volume.id)
+            return False
+
+        if status == 'ERROR':
+            raise VolumeCreationError(
+                'Instance had an error during deployment')
+        logger.debug('Instance status is - ' + status)
+        return status == expected_status_code
+
+
+class VolumeSettings(VolumeConfig):
+    """
+    Class to hold the configuration settings required for creating OpenStack
+    Volume Type Encryption objects
+    deprecated
+    """
+
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.volume.VolumeConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
+
+
+class VolumeCreationError(Exception):
+    """
+    Exception to be thrown when an volume cannot be created
+    """
+
+    def __init__(self, message):
+        Exception.__init__(self, message)
index a60bb1e..a7198d8 100644 (file)
 
 import logging
 
-import enum
 from cinderclient.exceptions import NotFound
-from neutronclient.common.utils import str2bool
 
+from snaps.config.volume_type import (
+    VolumeTypeConfig,  VolumeTypeEncryptionConfig)
 from snaps.openstack.openstack_creator import OpenStackVolumeObject
 from snaps.openstack.utils import cinder_utils
 
@@ -56,7 +56,7 @@ class OpenStackVolumeType(OpenStackVolumeObject):
 
         return self.__volume_type
 
-    def create(self, block=False):
+    def create(self):
         """
         Creates the volume in OpenStack if it does not already exist and
         returns the domain Volume object
@@ -96,134 +96,32 @@ class OpenStackVolumeType(OpenStackVolumeObject):
         return self.__volume_type
 
 
-class VolumeTypeSettings:
-    def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: the volume's name (required)
-        :param description: the volume's name (optional)
-        :param encryption: VolumeTypeEncryptionSettings (optional)
-        :param qos_spec_name: name of the QoS Spec to associate (optional)
-        :param public: When True, an image will be created with public
-                       visibility (default - False)
-
-        TODO - Implement project_access parameter that will associate this
-        VolumeType to a list of project names
-        """
-
-        self.name = kwargs.get('name')
-        self.description = kwargs.get('description')
-        self.qos_spec_name = kwargs.get('qos_spec_name')
-
-        if 'encryption' in kwargs:
-            if isinstance(kwargs['encryption'], dict):
-                self.encryption = VolumeTypeEncryptionSettings(
-                    **kwargs['encryption'])
-            elif isinstance(kwargs['encryption'],
-                            VolumeTypeEncryptionSettings):
-                self.encryption = kwargs['encryption']
-        else:
-            self.encryption = None
-
-        if 'public' in kwargs:
-            if isinstance(kwargs['public'], str):
-                self.public = str2bool(kwargs['public'])
-            else:
-                self.public = kwargs['public']
-        else:
-            self.public = False
-
-        if not self.name:
-            raise VolumeTypeSettingsError("The attribute name is required")
-
-    def __eq__(self, other):
-        return (self.name == other.name
-                and self.description == other.description
-                and self.qos_spec_name == other.qos_spec_name
-                and self.encryption == other.encryption
-                and self.public == other.public)
-
-
-class ControlLocation(enum.Enum):
+class VolumeTypeSettings(VolumeTypeConfig):
     """
-    QoS Specification consumer types
+    Class to hold the configuration settings required for creating OpenStack
+    Volume Type objects
+    deprecated
     """
-    front_end = 'front-end'
-    back_end = 'back-end'
-
 
-class VolumeTypeEncryptionSettings:
     def __init__(self, **kwargs):
-        """
-        Constructor
-        :param name: the volume's name (required)
-        :param provider_class: the volume's provider class (e.g. LuksEncryptor)
-        :param control_location: the notional service where encryption is
-                                 performed (e.g., front-end=Nova). The default
-                                 value is 'front-end.'
-        :param cipher: the encryption algorithm/mode to use
-                       (e.g., aes-xts-plain64). If the field is left empty,
-                       the provider default will be used
-        :param key_size: the size of the encryption key, in bits
-                         (e.g., 128, 256). If the field is left empty, the
-                         provider default will be used
-        """
+        from warnings import warn
+        warn('Use snaps.config.volume_type.VolumeTypeConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
-        self.name = kwargs.get('name')
-        self.provider_class = kwargs.get('provider_class')
-        self.control_location = kwargs.get('control_location')
-        if kwargs.get('control_location'):
-            self.control_location = map_control_location(
-                kwargs['control_location'])
-        else:
-            self.control_location = None
-
-        self.cipher = kwargs.get('cipher')
-        self.key_size = kwargs.get('key_size')
-
-        if (not self.name or not self.provider_class
-                or not self.control_location):
-            raise VolumeTypeSettingsError(
-                'The attributes name, provider_class, and control_location '
-                'are required')
-
-    def __eq__(self, other):
-        return (self.name == other.name
-                and self.provider_class == other.provider_class
-                and self.control_location == other.control_location
-                and self.cipher == other.cipher
-                and self.key_size == other.key_size)
-
-
-def map_control_location(control_location):
-    """
-    Takes a the protocol value maps it to the Consumer enum. When None return
-    None
-    :param control_location: the value to map to the Enum
-    :return: a ControlLocation enum object
-    :raise: Exception if control_location parameter is invalid
-    """
-    if not control_location:
-        return None
-    elif isinstance(control_location, ControlLocation):
-        return control_location
-    else:
-        proto_str = str(control_location)
-        if proto_str == 'front-end':
-            return ControlLocation.front_end
-        elif proto_str == 'back-end':
-            return ControlLocation.back_end
-        else:
-            raise VolumeTypeSettingsError('Invalid Consumer - ' + proto_str)
-
-
-class VolumeTypeSettingsError(Exception):
+
+class VolumeTypeEncryptionSettings(VolumeTypeEncryptionConfig):
     """
-    Exception to be thrown when an volume settings are incorrect
+    Class to hold the configuration settings required for creating OpenStack
+    Volume Type Encryption objects
+    deprecated
     """
 
-    def __init__(self, message):
-        Exception.__init__(self, message)
+    def __init__(self, **kwargs):
+        from warnings import warn
+        warn('Use snaps.config.volume_type.VolumeTypeEncryptionConfig instead',
+             DeprecationWarning)
+        super(self.__class__, self).__init__(**kwargs)
 
 
 class VolumeTypeCreationError(Exception):
index 945a78b..0caee9a 100644 (file)
@@ -13,8 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 from snaps.domain.creator import CloudObject
-from snaps.openstack.utils import (nova_utils, neutron_utils, keystone_utils,
-                                   cinder_utils)
+from snaps.openstack.utils import (
+    nova_utils, neutron_utils, keystone_utils, cinder_utils, magnum_utils)
 
 __author__ = 'spisarski'
 
@@ -132,3 +132,26 @@ class OpenStackVolumeObject(OpenStackCloudObject):
 
     def clean(self):
         raise NotImplementedError('Do not override abstract method')
+
+
+class OpenStackMagnumObject(OpenStackCloudObject):
+    """
+    Abstract class for all OpenStack compute creators
+    """
+
+    def __init__(self, os_creds):
+        """
+        Constructor
+        :param os_creds: the OpenStack credentials object
+        """
+        super(OpenStackMagnumObject, self).__init__(os_creds)
+        self._magnum = None
+
+    def initialize(self):
+        self._magnum = magnum_utils.magnum_client(self._os_creds)
+
+    def create(self):
+        raise NotImplementedError('Do not override abstract method')
+
+    def clean(self):
+        raise NotImplementedError('Do not override abstract method')
index cff2dd8..2553410 100644 (file)
@@ -44,6 +44,8 @@ class OSCreds:
                                     clients
         :param volume_api_version: The OpenStack's API version to use
                                    for Cinder clients
+        :param magnum_api_version: The OpenStack's API version to use
+                                   for magnum clients
         :param user_domain_id: Used for v3 APIs (default='default')
         :param user_domain_name: Used for v3 APIs (default='Default')
         :param project_domain_id: Used for v3 APIs (default='default')
@@ -51,10 +53,8 @@ class OSCreds:
         :param interface: Used to specify the endpoint type for keystone as
                           public, admin, internal
         :param proxy_settings: instance of os_credentials.ProxySettings class
-        :param cacert: Default to be True for http, or the certification file
-                       is specified for https verification, or set to be False
-                       to disable server certificate verification without cert
-                       file
+        :param cacert: True for https or the certification file for https
+                       verification (default=False)
         :param region_name: the region (optional default = None)
         """
         self.username = kwargs.get('username')
@@ -90,8 +90,12 @@ class OSCreds:
         if kwargs.get('volume_api_version') is None:
             self.volume_api_version = cinder_utils.VERSION_2
         else:
-            self.volume_api_version = float(
-                kwargs['volume_api_version'])
+            self.volume_api_version = float(kwargs['volume_api_version'])
+
+        if kwargs.get('magnum_api_version') is None:
+            self.magnum_api_version = 1
+        else:
+            self.magnum_api_version = float(kwargs['magnum_api_version'])
 
         self.user_domain_id = kwargs.get('user_domain_id', 'default')
 
@@ -200,7 +204,6 @@ class ProxySettings:
         :param port: the HTTP proxy port
         :param https_host: the HTTPS proxy host (defaults to host)
         :param https_port: the HTTPS proxy port (defaults to port)
-        :param port: the HTTP proxy port
         :param ssh_proxy_cmd: the SSH proxy command string (optional)
         """
         self.host = kwargs.get('host')
diff --git a/snaps/openstack/tests/cluster_template_tests.py b/snaps/openstack/tests/cluster_template_tests.py
new file mode 100644 (file)
index 0000000..355467d
--- /dev/null
@@ -0,0 +1,282 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from magnumclient.common.apiclient.exceptions import BadRequest
+
+from snaps.config.cluster_template import ClusterTemplateConfig
+from snaps.config.flavor import FlavorConfig
+from snaps.config.keypair import KeypairConfig
+from snaps.openstack.cluster_template import OpenStackClusterTemplate
+from snaps.openstack.create_flavor import OpenStackFlavor
+from snaps.openstack.create_image import OpenStackImage
+from snaps.openstack.create_keypairs import OpenStackKeypair
+from snaps.openstack.tests import openstack_tests
+
+try:
+    from urllib.request import URLError
+except ImportError:
+    from urllib2 import URLError
+
+import logging
+import uuid
+
+from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
+from snaps.openstack.utils import magnum_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('cluster_template_tests')
+
+
+class CreateClusterTemplateTests(OSIntegrationTestCase):
+    """
+    Test for the OpenStackClusterTemplate class defined in py
+    without any QoS Specs or Encryption
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateClusterTemplate object that is responsible for
+        downloading and creating an OS template config file within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.cluster_type_name = self.guid + '-cluster-type'
+        self.magnum = magnum_utils.magnum_client(self.os_creds)
+
+        metadata = self.image_metadata
+        if not metadata:
+            metadata = dict()
+        if 'extra_properties' not in metadata:
+            metadata['extra_properties'] = dict()
+        metadata['extra_properties']['os_distro'] = 'cirros'
+
+        os_image_settings = openstack_tests.cirros_image_settings(
+            name=self.guid + '-image', image_metadata=metadata)
+
+        self.image_creator = OpenStackImage(self.os_creds, os_image_settings)
+
+        self.flavor_creator = OpenStackFlavor(
+            self.os_creds, FlavorConfig(
+                name=self.guid + '-flavor', ram=512, disk=10, vcpus=1))
+
+        keypair_priv_filepath = 'tmp/' + self.guid
+        keypair_pub_filepath = keypair_priv_filepath + '.pub'
+
+        self.keypair_creator = OpenStackKeypair(
+            self.os_creds, KeypairConfig(
+                name=self.guid + '-keypair',
+                public_filepath=keypair_pub_filepath,
+                private_filepath=keypair_priv_filepath))
+
+        self.cluster_template_creator = None
+
+        self.cluster_template_config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name)
+
+        try:
+            self.image_creator.create()
+            self.flavor_creator.create()
+            self.keypair_creator.create()
+        except:
+            self.tearDown()
+            raise
+
+    def tearDown(self):
+        """
+        Cleans the template config
+        """
+        if self.cluster_template_creator:
+            try:
+                self.cluster_template_creator.clean()
+            except:
+                pass
+        if self.keypair_creator:
+            try:
+                self.keypair_creator.clean()
+            except:
+                pass
+        if self.flavor_creator:
+            try:
+                self.flavor_creator.clean()
+            except:
+                pass
+        if self.image_creator:
+            try:
+                self.image_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_create_cluster_template(self):
+        """
+        Tests the creation of an OpenStack cluster template.
+        """
+        # Create ClusterTemplate
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, self.cluster_template_config)
+        created_cluster_template = self.cluster_template_creator.create()
+        self.assertIsNotNone(created_cluster_template)
+        self.assertEqual(self.cluster_template_config.name,
+                         created_cluster_template.name)
+
+        retrieved_cluster_template1 = magnum_utils.get_cluster_template(
+            self.magnum, template_config=self.cluster_template_config)
+        self.assertIsNotNone(retrieved_cluster_template1)
+        self.assertEqual(created_cluster_template, retrieved_cluster_template1)
+
+        retrieved_cluster_template2 = magnum_utils.get_cluster_template_by_id(
+            self.magnum, created_cluster_template.id)
+        self.assertEqual(created_cluster_template, retrieved_cluster_template2)
+
+    def test_create_delete_cluster_template(self):
+        """
+        Tests the creation then deletion of an OpenStack template config to
+        ensure clean() does not raise an Exception.
+        """
+        # Create ClusterTemplate
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, self.cluster_template_config)
+        created_cluster_template = self.cluster_template_creator.create()
+        self.assertIsNotNone(created_cluster_template)
+
+        self.cluster_template_creator.clean()
+
+        tmplt = magnum_utils.get_cluster_template(
+            self.magnum, template_name=self.cluster_template_config.name)
+        self.assertIsNone(tmplt)
+
+    def test_create_same_cluster_template(self):
+        """
+        Tests the creation of an OpenStack cluster_template when one already
+        exists.
+        """
+        # Create ClusterTemplate
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, self.cluster_template_config)
+        cluster_template1 = self.cluster_template_creator.create()
+
+        retrieved_cluster_template = magnum_utils.get_cluster_template(
+            self.magnum, template_config=self.cluster_template_config)
+        self.assertEqual(cluster_template1, retrieved_cluster_template)
+
+        # Should be retrieving the instance data
+        os_cluster_template_2 = OpenStackClusterTemplate(
+            self.os_creds, self.cluster_template_config)
+        cluster_template2 = os_cluster_template_2.create()
+        self.assertEqual(cluster_template2, cluster_template2)
+
+    def test_create_cluster_template_bad_flavor(self):
+        """
+        Tests the creation of an OpenStack cluster template raises an
+        exception with an invalid flavor.
+        """
+        # Create ClusterTemplate
+        cluster_template_config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor='foo')
+
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, cluster_template_config)
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template_creator.create()
+
+    def test_create_cluster_template_bad_master_flavor(self):
+        """
+        Tests the creation of an OpenStack cluster template raises an
+        exception with an invalid master flavor.
+        """
+        # Create ClusterTemplate
+        cluster_template_config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            master_flavor='foo')
+
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, cluster_template_config)
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template_creator.create()
+
+    def test_create_cluster_template_bad_image(self):
+        """
+        Tests the creation of an OpenStack cluster template raises an
+        exception with an invalid image.
+        """
+        # Create ClusterTemplate
+        cluster_template_config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image='foo',
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name)
+
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, cluster_template_config)
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template_creator.create()
+
+    def test_create_cluster_template_bad_network_driver(self):
+        """
+        Tests the creation of an OpenStack cluster template raises an
+        exception with an invalid keypair.
+        """
+        # Create ClusterTemplate
+        cluster_template_config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            network_driver='foo')
+
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, cluster_template_config)
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template_creator.create()
+
+    def test_create_cluster_template_bad_volume_driver(self):
+        """
+        Tests the creation of an OpenStack cluster template raises an
+        exception with an invalid keypair.
+        """
+        # Create ClusterTemplate
+        cluster_template_config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            volume_driver='foo')
+
+        self.cluster_template_creator = OpenStackClusterTemplate(
+            self.os_creds, cluster_template_config)
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template_creator.create()
index 5efb32c..192be86 100644 (file)
@@ -18,6 +18,7 @@ import unittest
 
 from snaps.openstack.os_credentials import (
     OSCredsError, OSCreds, ProxySettings, ProxySettingsError)
+from snaps.openstack.utils import cinder_utils
 
 __author__ = 'spisarski'
 
@@ -147,6 +148,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(2, os_creds.image_api_version)
         self.assertEqual(2, os_creds.compute_api_version)
         self.assertEqual(1, os_creds.heat_api_version)
+        self.assertEqual(cinder_utils.VERSION_2, os_creds.volume_api_version)
+        self.assertEqual(1, os_creds.magnum_api_version)
         self.assertEqual('default', os_creds.user_domain_id)
         self.assertEqual('Default', os_creds.user_domain_name)
         self.assertEqual('default', os_creds.project_domain_id)
@@ -168,6 +171,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(2, os_creds.image_api_version)
         self.assertEqual(2, os_creds.compute_api_version)
         self.assertEqual(1, os_creds.heat_api_version)
+        self.assertEqual(cinder_utils.VERSION_2, os_creds.volume_api_version)
+        self.assertEqual(1, os_creds.magnum_api_version)
         self.assertEqual('default', os_creds.user_domain_id)
         self.assertEqual('Default', os_creds.user_domain_name)
         self.assertEqual('default', os_creds.project_domain_id)
@@ -183,6 +188,7 @@ class OSCredsUnitTests(unittest.TestCase):
                'auth_url': 'http://foo.bar:5000/v2', 'project_name': 'hello',
                'identity_api_version': '5', 'image_api_version': '6',
                'compute_api_version': '7', 'heat_api_version': '8.0',
+               'volume_api_version': '9.5', 'magnum_api_version': '10.6',
                'cacert': 'true', 'region_name': 'test_region'})
         self.assertEqual('foo', os_creds.username)
         self.assertEqual('bar', os_creds.password)
@@ -192,6 +198,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(6, os_creds.image_api_version)
         self.assertEqual(7, os_creds.compute_api_version)
         self.assertEqual(8.0, os_creds.heat_api_version)
+        self.assertEqual(9.5, os_creds.volume_api_version)
+        self.assertEqual(10.6, os_creds.magnum_api_version)
         self.assertEqual('default', os_creds.user_domain_id)
         self.assertEqual('Default', os_creds.user_domain_name)
         self.assertEqual('default', os_creds.project_domain_id)
@@ -207,6 +215,7 @@ class OSCredsUnitTests(unittest.TestCase):
                'auth_url': 'http://foo.bar:5000/v2', 'project_name': 'hello',
                'identity_api_version': 5, 'image_api_version': 6,
                'compute_api_version': 7, 'heat_api_version': 8.0,
+               'volume_api_version': 9.5, 'magnum_api_version': 10.6,
                'cacert': True, 'region_name': 'test_region'})
         self.assertEqual('foo', os_creds.username)
         self.assertEqual('bar', os_creds.password)
@@ -216,6 +225,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(6, os_creds.image_api_version)
         self.assertEqual(7, os_creds.compute_api_version)
         self.assertEqual(8.0, os_creds.heat_api_version)
+        self.assertEqual(9.5, os_creds.volume_api_version)
+        self.assertEqual(10.6, os_creds.magnum_api_version)
         self.assertEqual('default', os_creds.user_domain_id)
         self.assertEqual('Default', os_creds.user_domain_name)
         self.assertEqual('default', os_creds.project_domain_id)
@@ -238,6 +249,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(2, os_creds.image_api_version)
         self.assertEqual(2, os_creds.compute_api_version)
         self.assertEqual(1, os_creds.heat_api_version)
+        self.assertEqual(cinder_utils.VERSION_2, os_creds.volume_api_version)
+        self.assertEqual(1, os_creds.magnum_api_version)
         self.assertEqual('default', os_creds.user_domain_id)
         self.assertEqual('Default', os_creds.user_domain_name)
         self.assertEqual('default', os_creds.project_domain_id)
@@ -266,6 +279,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(2, os_creds.image_api_version)
         self.assertEqual(2, os_creds.compute_api_version)
         self.assertEqual(1, os_creds.heat_api_version)
+        self.assertEqual(cinder_utils.VERSION_2, os_creds.volume_api_version)
+        self.assertEqual(1, os_creds.magnum_api_version)
         self.assertEqual('domain1', os_creds.user_domain_id)
         self.assertEqual('domain2', os_creds.user_domain_name)
         self.assertEqual('domain3', os_creds.project_domain_id)
@@ -291,6 +306,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(2, os_creds.image_api_version)
         self.assertEqual(2, os_creds.compute_api_version)
         self.assertEqual(1, os_creds.heat_api_version)
+        self.assertEqual(cinder_utils.VERSION_2, os_creds.volume_api_version)
+        self.assertEqual(1, os_creds.magnum_api_version)
         self.assertEqual('domain1', os_creds.user_domain_id)
         self.assertEqual('domain2', os_creds.user_domain_name)
         self.assertEqual('domain3', os_creds.project_domain_id)
@@ -315,6 +332,8 @@ class OSCredsUnitTests(unittest.TestCase):
         self.assertEqual(2, os_creds.image_api_version)
         self.assertEqual(2, os_creds.compute_api_version)
         self.assertEqual(1, os_creds.heat_api_version)
+        self.assertEqual(cinder_utils.VERSION_2, os_creds.volume_api_version)
+        self.assertEqual(1, os_creds.magnum_api_version)
         self.assertEqual('default', os_creds.user_domain_id)
         self.assertEqual('Default', os_creds.user_domain_name)
         self.assertEqual('default', os_creds.project_domain_id)
index e88bfaa..36e3cfd 100644 (file)
@@ -1,17 +1,17 @@
 # Keystone v2.0
 #username: admin
 #password: admin
-#os_auth_url: http://<host>:<port>/v2.0/
+#os_auth_url: http://<host>:<port>/
 #project_name: admin
 #ext_net: <external network name>
 #http_proxy: <host>:<port>
 #ssh_proxy_cmd: '/usr/local/bin/corkscrew <host> <port> %h %p'
 #ssh_proxy_cmd: 'ssh <host> nc %h %p'
 
-# Keystone v2.0
+# Keystone v3
 #username: admin
 #password: admin
-#os_auth_url: http://<host>:<port>/v3
+#os_auth_url: http://<host>:<port>/
 #project_name: admin
 #identity_api_version: 3
 #ext_net: <external network name>
\ No newline at end of file
index 4852d06..f84355d 100644 (file)
@@ -15,9 +15,9 @@
 import unittest
 import uuid
 
+from snaps.config.flavor import FlavorConfig, FlavorConfigError
 from snaps.openstack import create_flavor
-from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor, \
-    FlavorSettingsError
+from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
 from snaps.openstack.utils import nova_utils
 
@@ -30,169 +30,169 @@ class FlavorSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(config=dict())
 
     def test_name_only(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(config={'name': 'foo'})
 
     def test_name_ram_only(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1)
 
     def test_config_with_name_ram_only(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(config={'name': 'foo', 'ram': 1})
 
     def test_name_ram_disk_only(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=1)
 
     def test_config_with_name_ram_disk_only(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 1})
 
     def test_ram_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram='bar', disk=2, vcpus=3, ephemeral=4,
                            swap=5, rxtx_factor=6.0,
                            is_public=False)
 
     def test_config_ram_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 'bar', 'disk': 2, 'vcpus': 3,
                         'ephemeral': 4, 'swap': 5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_ram_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1.5, disk=2, vcpus=3, ephemeral=4,
                            swap=5, rxtx_factor=6.0, is_public=False)
 
     def test_config_ram_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1.5, 'disk': 2, 'vcpus': 3,
                         'ephemeral': 4, 'swap': 5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_disk_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk='bar', vcpus=3, ephemeral=4,
                            swap=5, rxtx_factor=6.0,
                            is_public=False)
 
     def test_config_disk_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 'bar', 'vcpus': 3,
                         'ephemeral': 4, 'swap': 5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_disk_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2.5, vcpus=3, ephemeral=4,
                            swap=5, rxtx_factor=6.0, is_public=False)
 
     def test_config_disk_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2.5, 'vcpus': 3,
                         'ephemeral': 4, 'swap': 5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_vcpus_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2, vcpus='bar', ephemeral=4,
                            swap=5, rxtx_factor=6.0,
                            is_public=False)
 
     def test_config_vcpus_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 'bar',
                         'ephemeral': 4, 'swap': 5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_ephemeral_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral='bar',
                            swap=5, rxtx_factor=6.0,
                            is_public=False)
 
     def test_config_ephemeral_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
                         'ephemeral': 'bar', 'swap': 5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_ephemeral_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4.5,
                            swap=5, rxtx_factor=6.0, is_public=False)
 
     def test_config_ephemeral_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
                         'ephemeral': 4.5, 'swap': 5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_swap_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
                            swap='bar', rxtx_factor=6.0,
                            is_public=False)
 
     def test_config_swap_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
                         'ephemeral': 4, 'swap': 'bar',
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_swap_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
                            swap=5.5, rxtx_factor=6.0, is_public=False)
 
     def test_config_swap_float(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
                         'ephemeral': 4, 'swap': 5.5,
                         'rxtx_factor': 6.0, 'is_public': False})
 
     def test_rxtx_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
                            swap=5, rxtx_factor='bar', is_public=False)
 
     def test_config_rxtx_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
                         'ephemeral': 4, 'swap': 5,
                         'rxtx_factor': 'bar', 'is_public': False})
 
     def test_is_pub_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4,
                            swap=5, rxtx_factor=6.0, is_public='bar')
 
     def test_config_is_pub_string(self):
-        with self.assertRaises(FlavorSettingsError):
+        with self.assertRaises(FlavorConfigError):
             FlavorSettings(
                 config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3,
                         'ephemeral': 4, 'swap': 5,
@@ -291,8 +291,8 @@ class CreateFlavorTests(OSComponentTestCase):
         Tests the creation of an OpenStack flavor.
         """
         # Create Flavor
-        flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1,
-                                         vcpus=1)
+        flavor_settings = FlavorConfig(
+            name=self.flavor_name, ram=1, disk=1, vcpus=1)
         self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings)
         flavor = self.flavor_creator.create()
         self.assertTrue(validate_flavor(self.nova, flavor_settings, flavor))
@@ -303,8 +303,8 @@ class CreateFlavorTests(OSComponentTestCase):
         to ensure it has not been done twice.
         """
         # Create Flavor
-        flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1,
-                                         vcpus=1)
+        flavor_settings = FlavorConfig(
+            name=self.flavor_name, ram=1, disk=1, vcpus=1)
         self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings)
         flavor = self.flavor_creator.create()
         self.assertTrue(validate_flavor(self.nova, flavor_settings, flavor))
@@ -319,8 +319,8 @@ class CreateFlavorTests(OSComponentTestCase):
         Tests the creation and cleanup of an OpenStack flavor.
         """
         # Create Flavor
-        flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1,
-                                         vcpus=1)
+        flavor_settings = FlavorConfig(
+            name=self.flavor_name, ram=1, disk=1, vcpus=1)
         self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings)
         flavor = self.flavor_creator.create()
         self.assertTrue(validate_flavor(self.nova, flavor_settings, flavor))
@@ -339,8 +339,8 @@ class CreateFlavorTests(OSComponentTestCase):
         raise any exceptions.
         """
         # Create Flavor
-        flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1,
-                                         vcpus=1)
+        flavor_settings = FlavorConfig(
+            name=self.flavor_name, ram=1, disk=1, vcpus=1)
         self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings)
         flavor = self.flavor_creator.create()
         self.assertTrue(validate_flavor(self.nova, flavor_settings, flavor))
@@ -362,7 +362,7 @@ class CreateFlavorTests(OSComponentTestCase):
         raise any exceptions.
         """
         # Create Flavor
-        flavor_settings = FlavorSettings(
+        flavor_settings = FlavorConfig(
             name=self.flavor_name, ram=1, disk=1, vcpus=1, ephemeral=2, swap=3,
             rxtx_factor=2.2, is_public=False,
             metadata=create_flavor.MEM_PAGE_SIZE_ANY)
@@ -399,7 +399,7 @@ def validate_flavor(nova, flavor_settings, flavor):
             equals = False
             break
 
-    swap = str()
+    swap = None
     if flavor_settings.swap != 0:
         swap = flavor_settings.swap
 
index f70a71c..9965f87 100644 (file)
@@ -27,9 +27,10 @@ import uuid
 import os
 
 from snaps import file_utils
+from snaps.config.image import ImageConfigError
 from snaps.openstack import create_image
-from snaps.openstack.create_image import (ImageSettings, ImageCreationError,
-                                          ImageSettingsError)
+from snaps.openstack.create_image import ImageSettings, ImageCreationError
+from snaps.config.image import ImageConfig
 from snaps.openstack.tests import openstack_tests
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
 from snaps.openstack.utils import glance_utils
@@ -42,38 +43,40 @@ logger = logging.getLogger('create_image_tests')
 class ImageSettingsUnitTests(unittest.TestCase):
     """
     Tests the construction of the ImageSettings class
+    To be removed once the deprecated class ImageSettings is finally removed
+    from the source tree
     """
 
     def test_no_params(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings(**dict())
 
     def test_name_only(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings(name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings(**{'name': 'foo'})
 
     def test_name_user_only(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings(name='foo', image_user='bar')
 
     def test_config_with_name_user_only(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings(**{'name': 'foo', 'image_user': 'bar'})
 
     def test_name_user_format_only(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings(name='foo', image_user='bar', img_format='qcow2')
 
     def test_config_with_name_user_format_only(self):
-        with self.assertRaises(ImageSettingsError):
+        with self.assertRaises(ImageConfigError):
             ImageSettings(
                 **{'name': 'foo', 'image_user': 'bar', 'format': 'qcow2'})
 
@@ -448,8 +451,8 @@ class CreateImageSuccessTests(OSIntegrationTestCase):
         self.assertEqual(image1.properties, retrieved_image.properties)
 
         # Should be retrieving the instance data
-        image_2_settings = ImageSettings(name=self.image_settings.name,
-                                         image_user='foo', exists=True)
+        image_2_settings = ImageConfig(name=self.image_settings.name,
+                                       image_user='foo', exists=True)
         os_image_2 = create_image.OpenStackImage(self.os_creds,
                                                  image_2_settings)
         image2 = os_image_2.create()
@@ -478,8 +481,8 @@ class CreateImageNegativeTests(OSIntegrationTestCase):
         Expect an ImageCreationError when the image name does not exist when a
         file or URL has not been configured
         """
-        os_image_settings = ImageSettings(name='foo', image_user='bar',
-                                          exists=True)
+        os_image_settings = ImageConfig(name='foo', image_user='bar',
+                                        exists=True)
         self.image_creator = create_image.OpenStackImage(self.os_creds,
                                                          os_image_settings)
 
@@ -497,10 +500,11 @@ class CreateImageNegativeTests(OSIntegrationTestCase):
             name=self.image_name)
         self.image_creator = create_image.OpenStackImage(
             self.os_creds,
-            create_image.ImageSettings(name=os_image_settings.name,
-                                       image_user=os_image_settings.image_user,
-                                       img_format=os_image_settings.format,
-                                       url="http://foo.bar"))
+            ImageConfig(
+                name=os_image_settings.name,
+                image_user=os_image_settings.image_user,
+                img_format=os_image_settings.format,
+                url="http://foo.bar"))
 
         try:
             self.image_creator.create()
@@ -519,10 +523,10 @@ class CreateImageNegativeTests(OSIntegrationTestCase):
             name=self.image_name)
         self.image_creator = create_image.OpenStackImage(
             self.os_creds,
-            create_image.ImageSettings(name=os_image_settings.name,
-                                       image_user=os_image_settings.image_user,
-                                       img_format='foo',
-                                       url=os_image_settings.url))
+            ImageConfig(
+                name=os_image_settings.name,
+                image_user=os_image_settings.image_user,
+                img_format='foo', url=os_image_settings.url))
 
         with self.assertRaises(Exception):
             self.image_creator.create()
@@ -535,10 +539,10 @@ class CreateImageNegativeTests(OSIntegrationTestCase):
             name=self.image_name)
         self.image_creator = create_image.OpenStackImage(
             self.os_creds,
-            create_image.ImageSettings(name=os_image_settings.name,
-                                       image_user=os_image_settings.image_user,
-                                       img_format=os_image_settings.format,
-                                       image_file="/foo/bar.qcow"))
+            ImageConfig(
+                name=os_image_settings.name,
+                image_user=os_image_settings.image_user,
+                img_format=os_image_settings.format, image_file="/foo/bar.qcow"))
         with self.assertRaises(IOError):
             self.image_creator.create()
 
index 9c872bc..55da144 100644 (file)
@@ -21,25 +21,36 @@ import uuid
 
 import os
 from neutronclient.common.exceptions import InvalidIpForSubnetClient
+from novaclient.exceptions import BadRequest
 
 from snaps import file_utils
-from snaps.openstack import create_network, create_router
-from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings
-from snaps.openstack.create_image import OpenStackImage, ImageSettings
+from snaps.config.flavor import FlavorConfig
+from snaps.config.image import ImageConfig
+from snaps.config.keypair import KeypairConfig
+from snaps.config.network import PortConfig, NetworkConfig, SubnetConfig
+from snaps.config.router import RouterConfig
+from snaps.config.security_group import (
+    Protocol, SecurityGroupRuleConfig, Direction, SecurityGroupConfig)
+from snaps.config.vm_inst import (
+    VmInstanceConfig, FloatingIpConfig,  VmInstanceConfigError,
+    FloatingIpConfigError)
+from snaps.config.volume import VolumeConfig
+from snaps.openstack import create_network, create_router, create_instance
+from snaps.openstack.create_flavor import OpenStackFlavor
+from snaps.openstack.create_image import OpenStackImage
 from snaps.openstack.create_instance import (
-    VmInstanceSettings, OpenStackVmInstance, FloatingIpSettings,
-    VmInstanceSettingsError, FloatingIpSettingsError)
-from snaps.openstack.create_keypairs import OpenStackKeypair, KeypairSettings
-from snaps.openstack.create_network import (
-    OpenStackNetwork, PortSettings, NetworkSettings)
-from snaps.openstack.create_router import OpenStackRouter, RouterSettings
-from snaps.openstack.create_security_group import (
-    SecurityGroupSettings, OpenStackSecurityGroup, SecurityGroupRuleSettings,
-    Direction, Protocol)
+    VmInstanceSettings, OpenStackVmInstance, FloatingIpSettings)
+from snaps.openstack.create_keypairs import OpenStackKeypair
+from snaps.openstack.create_network import OpenStackNetwork
+from snaps.openstack.create_router import OpenStackRouter
+from snaps.openstack.create_security_group import OpenStackSecurityGroup
+from snaps.openstack.create_volume import OpenStackVolume
 from snaps.openstack.tests import openstack_tests, validation_utils
 from snaps.openstack.tests.os_source_file_test import (
     OSIntegrationTestCase, OSComponentTestCase)
 from snaps.openstack.utils import nova_utils
+from snaps.openstack.utils.nova_utils import RebootType
+from snaps.openstack.utils import nova_utils, settings_utils, neutron_utils
 
 __author__ = 'spisarski'
 
@@ -54,31 +65,31 @@ class VmInstanceSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(VmInstanceSettingsError):
+        with self.assertRaises(VmInstanceConfigError):
             VmInstanceSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(VmInstanceSettingsError):
+        with self.assertRaises(VmInstanceConfigError):
             VmInstanceSettings(config=dict())
 
     def test_name_only(self):
-        with self.assertRaises(VmInstanceSettingsError):
+        with self.assertRaises(VmInstanceConfigError):
             VmInstanceSettings(name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(VmInstanceSettingsError):
+        with self.assertRaises(VmInstanceConfigError):
             VmInstanceSettings(config={'name': 'foo'})
 
     def test_name_flavor_only(self):
-        with self.assertRaises(VmInstanceSettingsError):
+        with self.assertRaises(VmInstanceConfigError):
             VmInstanceSettings(name='foo', flavor='bar')
 
     def test_config_with_name_flavor_only(self):
-        with self.assertRaises(VmInstanceSettingsError):
+        with self.assertRaises(VmInstanceConfigError):
             VmInstanceSettings(config={'name': 'foo', 'flavor': 'bar'})
 
     def test_name_flavor_port_only(self):
-        port_settings = PortSettings(name='foo-port', network_name='bar-net')
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
         settings = VmInstanceSettings(name='foo', flavor='bar',
                                       port_settings=[port_settings])
         self.assertEqual('foo', settings.name)
@@ -93,9 +104,10 @@ class VmInstanceSettingsUnitTests(unittest.TestCase):
         self.assertEqual(300, settings.vm_delete_timeout)
         self.assertEqual(180, settings.ssh_connect_timeout)
         self.assertIsNone(settings.availability_zone)
+        self.assertIsNone(settings.volume_names)
 
     def test_config_with_name_flavor_port_only(self):
-        port_settings = PortSettings(name='foo-port', network_name='bar-net')
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
         settings = VmInstanceSettings(
             **{'name': 'foo', 'flavor': 'bar', 'ports': [port_settings]})
         self.assertEqual('foo', settings.name)
@@ -110,20 +122,20 @@ class VmInstanceSettingsUnitTests(unittest.TestCase):
         self.assertEqual(300, settings.vm_delete_timeout)
         self.assertEqual(180, settings.ssh_connect_timeout)
         self.assertIsNone(settings.availability_zone)
+        self.assertIsNone(settings.volume_names)
 
     def test_all(self):
-        port_settings = PortSettings(name='foo-port', network_name='bar-net')
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
         fip_settings = FloatingIpSettings(name='foo-fip', port_name='bar-port',
                                           router_name='foo-bar-router')
 
-        settings = VmInstanceSettings(name='foo', flavor='bar',
-                                      port_settings=[port_settings],
-                                      security_group_names=['sec_grp_1'],
-                                      floating_ip_settings=[fip_settings],
-                                      sudo_user='joe', vm_boot_timeout=999,
-                                      vm_delete_timeout=333,
-                                      ssh_connect_timeout=111,
-                                      availability_zone='server name')
+        settings = VmInstanceSettings(
+            name='foo', flavor='bar', port_settings=[port_settings],
+            security_group_names=['sec_grp_1'],
+            floating_ip_settings=[fip_settings], sudo_user='joe',
+            vm_boot_timeout=999, vm_delete_timeout=333,
+            ssh_connect_timeout=111, availability_zone='server name',
+            volume_names=['vol1'])
         self.assertEqual('foo', settings.name)
         self.assertEqual('bar', settings.flavor)
         self.assertEqual(1, len(settings.port_settings))
@@ -142,9 +154,10 @@ class VmInstanceSettingsUnitTests(unittest.TestCase):
         self.assertEqual(333, settings.vm_delete_timeout)
         self.assertEqual(111, settings.ssh_connect_timeout)
         self.assertEqual('server name', settings.availability_zone)
+        self.assertEqual('vol1', settings.volume_names[0])
 
     def test_config_all(self):
-        port_settings = PortSettings(name='foo-port', network_name='bar-net')
+        port_settings = PortConfig(name='foo-port', network_name='bar-net')
         fip_settings = FloatingIpSettings(name='foo-fip', port_name='bar-port',
                                           router_name='foo-bar-router')
 
@@ -153,7 +166,8 @@ class VmInstanceSettingsUnitTests(unittest.TestCase):
                'security_group_names': ['sec_grp_1'],
                'floating_ips': [fip_settings], 'sudo_user': 'joe',
                'vm_boot_timeout': 999, 'vm_delete_timeout': 333,
-               'ssh_connect_timeout': 111, 'availability_zone': 'server name'})
+               'ssh_connect_timeout': 111, 'availability_zone': 'server name',
+               'volume_names': ['vol2']})
         self.assertEqual('foo', settings.name)
         self.assertEqual('bar', settings.flavor)
         self.assertEqual(1, len(settings.port_settings))
@@ -171,6 +185,7 @@ class VmInstanceSettingsUnitTests(unittest.TestCase):
         self.assertEqual(333, settings.vm_delete_timeout)
         self.assertEqual(111, settings.ssh_connect_timeout)
         self.assertEqual('server name', settings.availability_zone)
+        self.assertEqual('vol2', settings.volume_names[0])
 
 
 class FloatingIpSettingsUnitTests(unittest.TestCase):
@@ -179,35 +194,35 @@ class FloatingIpSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings(**dict())
 
     def test_name_only(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings(name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings(**{'name': 'foo'})
 
     def test_name_port_only(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings(name='foo', port_name='bar')
 
     def test_config_with_name_port_only(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings(**{'name': 'foo', 'port_name': 'bar'})
 
     def test_name_router_only(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings(name='foo', router_name='bar')
 
     def test_config_with_name_router_only(self):
-        with self.assertRaises(FloatingIpSettingsError):
+        with self.assertRaises(FloatingIpConfigError):
             FloatingIpSettings(**{'name': 'foo', 'router_name': 'bar'})
 
     def test_name_port_router_name_only(self):
@@ -291,8 +306,10 @@ class SimpleHealthCheck(OSIntegrationTestCase):
         self.inst_creator = None
 
         self.priv_net_config = openstack_tests.get_priv_net_config(
-            net_name=guid + '-priv-net', subnet_name=guid + '-priv-subnet')
-        self.port_settings = PortSettings(
+            net_name=guid + '-priv-net',
+            subnet_name=guid + '-priv-subnet',
+            netconf_override=self.netconf_override)
+        self.port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.priv_net_config.network_settings.name)
 
@@ -313,10 +330,14 @@ class SimpleHealthCheck(OSIntegrationTestCase):
             self.network_creator.create()
 
             # Create Flavor
+            self.flavor_ram = 256
+            if (self.flavor_metadata and
+               self.flavor_metadata.get('hw:mem_page_size') == 'large'):
+                self.flavor_ram = 1024
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=guid + '-flavor-name', ram=256, disk=10,
-                               vcpus=1, metadata=self.flavor_metadata))
+                FlavorConfig(name=guid + '-flavor-name', ram=self.flavor_ram,
+                             disk=10, vcpus=1, metadata=self.flavor_metadata))
             self.flavor_creator.create()
         except Exception as e:
             self.tearDown()
@@ -365,7 +386,7 @@ class SimpleHealthCheck(OSIntegrationTestCase):
         Tests the creation of an OpenStack instance with a single port and
         ensures that it's assigned IP address is the actual.
         """
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -399,12 +420,14 @@ class CreateInstanceSimpleTests(OSIntegrationTestCase):
         guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
         self.vm_inst_name = guid + '-inst'
         self.nova = nova_utils.nova_client(self.os_creds)
+        self.neutron = neutron_utils.neutron_client(self.os_creds)
         os_image_settings = openstack_tests.cirros_image_settings(
             name=guid + '-image', image_metadata=self.image_metadata)
 
         net_config = openstack_tests.get_priv_net_config(
             net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet',
-            router_name=guid + '-pub-router', external_net=self.ext_net_name)
+            router_name=guid + '-pub-router', external_net=self.ext_net_name,
+            netconf_override=self.netconf_override)
 
         # Initialize for tearDown()
         self.image_creator = None
@@ -422,8 +445,8 @@ class CreateInstanceSimpleTests(OSIntegrationTestCase):
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=guid + '-flavor-name', ram=256, disk=10,
-                               vcpus=2, metadata=self.flavor_metadata))
+                FlavorConfig(name=guid + '-flavor-name', ram=256, disk=10,
+                             vcpus=2, metadata=self.flavor_metadata))
             self.flavor_creator.create()
 
             # Create Network
@@ -431,7 +454,7 @@ class CreateInstanceSimpleTests(OSIntegrationTestCase):
                 self.os_creds, net_config.network_settings)
             self.network_creator.create()
 
-            self.port_settings = PortSettings(
+            self.port_settings = PortConfig(
                 name=guid + '-port',
                 network_name=net_config.network_settings.name)
 
@@ -481,7 +504,7 @@ class CreateInstanceSimpleTests(OSIntegrationTestCase):
         Tests the creation of an OpenStack instance with a single port with a
         static IP without a Floating IP.
         """
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -492,14 +515,14 @@ class CreateInstanceSimpleTests(OSIntegrationTestCase):
 
         vm_inst = self.inst_creator.create()
         self.assertIsNotNone(nova_utils.get_server(
-            self.nova, vm_inst_settings=instance_settings))
+            self.nova, self.neutron, vm_inst_settings=instance_settings))
 
         # Delete instance
         nova_utils.delete_vm_instance(self.nova, vm_inst)
 
         self.assertTrue(self.inst_creator.vm_deleted(block=True))
         self.assertIsNone(nova_utils.get_server(
-            self.nova, vm_inst_settings=instance_settings))
+            self.nova, self.neutron, vm_inst_settings=instance_settings))
 
         # Exception should not be thrown
         self.inst_creator.clean()
@@ -538,7 +561,8 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
 
         self.pub_net_config = openstack_tests.get_pub_net_config(
             net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet',
-            router_name=guid + '-pub-router', external_net=self.ext_net_name)
+            router_name=guid + '-pub-router', external_net=self.ext_net_name,
+            netconf_override=self.netconf_override)
         os_image_settings = openstack_tests.cirros_image_settings(
             name=guid + '-image', image_metadata=self.image_metadata)
         try:
@@ -560,30 +584,28 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=guid + '-flavor-name', ram=256, disk=10,
-                               vcpus=2, metadata=self.flavor_metadata))
+                FlavorConfig(name=guid + '-flavor-name', ram=256, disk=10,
+                             vcpus=2, metadata=self.flavor_metadata))
             self.flavor_creator.create()
 
             self.keypair_creator = OpenStackKeypair(
-                self.os_creds, KeypairSettings(
+                self.os_creds, KeypairConfig(
                     name=self.keypair_name,
                     public_filepath=self.keypair_pub_filepath,
                     private_filepath=self.keypair_priv_filepath))
             self.keypair_creator.create()
 
             sec_grp_name = guid + '-sec-grp'
-            rule1 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.icmp)
-            rule2 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.tcp,
-                                              port_range_min=22,
-                                              port_range_max=22)
+            rule1 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.icmp)
+            rule2 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.tcp, port_range_min=22, port_range_max=22)
             self.sec_grp_creator = OpenStackSecurityGroup(
                 self.os_creds,
-                SecurityGroupSettings(name=sec_grp_name,
-                                      rule_settings=[rule1, rule2]))
+                SecurityGroupConfig(
+                    name=sec_grp_name, rule_settings=[rule1, rule2]))
             self.sec_grp_creator.create()
         except Exception as e:
             self.tearDown()
@@ -609,12 +631,6 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
                     'Unexpected exception cleaning keypair with message - %s',
                     e)
 
-        if os.path.isfile(self.keypair_pub_filepath):
-            os.remove(self.keypair_pub_filepath)
-
-        if os.path.isfile(self.keypair_priv_filepath):
-            os.remove(self.keypair_priv_filepath)
-
         if self.flavor_creator:
             try:
                 self.flavor_creator.clean()
@@ -663,17 +679,17 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
         """
         ip_1 = '10.55.1.100'
         sub_settings = self.pub_net_config.network_settings.subnet_settings
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.pub_net_config.network_settings.name,
             ip_addrs=[
                 {'subnet_name': sub_settings[0].name, 'ip': ip_1}])
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings],
-            floating_ip_settings=[FloatingIpSettings(
+            floating_ip_settings=[FloatingIpConfig(
                 name=self.floating_ip_name, port_name=self.port_1_name,
                 router_name=self.pub_net_config.router_settings.name)])
 
@@ -682,7 +698,7 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
             self.image_creator.image_settings,
             keypair_settings=self.keypair_creator.keypair_settings)
         self.inst_creators.append(inst_creator)
-        vm_inst = inst_creator.create()
+        vm_inst = inst_creator.create(block=True)
 
         self.assertEqual(ip_1, inst_creator.get_port_ip(self.port_1_name))
         self.assertTrue(inst_creator.vm_active(block=True))
@@ -693,15 +709,16 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
         Tests the ability to access a VM via SSH and a floating IP when it has
         been assigned prior to being active.
         """
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.pub_net_config.network_settings.name)
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings],
-            floating_ip_settings=[FloatingIpSettings(
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name],
+            floating_ip_settings=[FloatingIpConfig(
                 name=self.floating_ip_name, port_name=self.port_1_name,
                 router_name=self.pub_net_config.router_settings.name)])
 
@@ -718,8 +735,6 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
         ip = inst_creator.get_port_ip(port_settings.name)
         self.assertTrue(check_dhcp_lease(inst_creator, ip))
 
-        inst_creator.add_security_group(
-            self.sec_grp_creator.get_security_group())
         self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id)
 
         self.assertTrue(validate_ssh_client(inst_creator))
@@ -729,15 +744,53 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
         Tests the ability to access a VM via SSH and a floating IP when it has
         been assigned prior to being active.
         """
-        port_settings = PortSettings(
+        port_settings = PortConfig(
+            name=self.port_1_name,
+            network_name=self.pub_net_config.network_settings.name)
+
+        instance_settings = VmInstanceConfig(
+            name=self.vm_inst_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            port_settings=[port_settings],
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name],
+            floating_ip_settings=[FloatingIpConfig(
+                name=self.floating_ip_name, port_name=self.port_1_name,
+                router_name=self.pub_net_config.router_settings.name)])
+
+        inst_creator = OpenStackVmInstance(
+            self.os_creds, instance_settings,
+            self.image_creator.image_settings,
+            keypair_settings=self.keypair_creator.keypair_settings)
+        self.inst_creators.append(inst_creator)
+
+        # block=True will force the create() method to block until the
+        vm_inst = inst_creator.create(block=True)
+        self.assertIsNotNone(vm_inst)
+
+        self.assertTrue(inst_creator.vm_active(block=True))
+
+        ip = inst_creator.get_port_ip(port_settings.name)
+        self.assertTrue(check_dhcp_lease(inst_creator, ip))
+
+        self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id)
+
+        self.assertTrue(validate_ssh_client(inst_creator))
+
+    def test_ssh_client_fip_after_reboot(self):
+        """
+        Tests the ability to access a VM via SSH and a floating IP after it has
+        been rebooted.
+        """
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.pub_net_config.network_settings.name)
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings],
-            floating_ip_settings=[FloatingIpSettings(
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name],
+            floating_ip_settings=[FloatingIpConfig(
                 name=self.floating_ip_name, port_name=self.port_1_name,
                 router_name=self.pub_net_config.router_settings.name)])
 
@@ -756,26 +809,121 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
         ip = inst_creator.get_port_ip(port_settings.name)
         self.assertTrue(check_dhcp_lease(inst_creator, ip))
 
-        inst_creator.add_security_group(
-            self.sec_grp_creator.get_security_group())
         self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id)
 
         self.assertTrue(validate_ssh_client(inst_creator))
 
+        # Test default reboot which should be 'SOFT'
+        inst_creator.reboot()
+        # Lag time to allow for shutdown routine to take effect
+        time.sleep(10)
+        self.assertTrue(check_dhcp_lease(inst_creator, ip))
+        self.assertTrue(validate_ssh_client(inst_creator))
+
+        # Test 'SOFT' reboot
+        inst_creator.reboot(reboot_type=RebootType.soft)
+        time.sleep(10)
+        self.assertTrue(check_dhcp_lease(inst_creator, ip))
+        self.assertTrue(validate_ssh_client(inst_creator))
+
+        # Test 'HARD' reboot
+        inst_creator.reboot(reboot_type=RebootType.hard)
+        time.sleep(10)
+        self.assertTrue(check_dhcp_lease(inst_creator, ip))
+        self.assertTrue(validate_ssh_client(inst_creator))
+
+    def test_ssh_client_fip_after_init(self):
+        """
+        Tests the ability to assign a floating IP to an already initialized
+        OpenStackVmInstance object. After the floating IP has been allocated
+        and assigned, this test will ensure that it can be accessed via SSH.
+        """
+        port_settings = PortConfig(
+            name=self.port_1_name,
+            network_name=self.pub_net_config.network_settings.name)
+
+        instance_settings = VmInstanceConfig(
+            name=self.vm_inst_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            port_settings=[port_settings],
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name])
+
+        inst_creator = OpenStackVmInstance(
+            self.os_creds, instance_settings,
+            self.image_creator.image_settings,
+            keypair_settings=self.keypair_creator.keypair_settings)
+        self.inst_creators.append(inst_creator)
+
+        # block=True will force the create() method to block until the
+        vm_inst = inst_creator.create(block=True)
+        self.assertIsNotNone(vm_inst)
+
+        self.assertTrue(inst_creator.vm_active(block=True))
+        ip = inst_creator.get_port_ip(port_settings.name)
+        self.assertTrue(check_dhcp_lease(inst_creator, ip))
+        self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id)
+
+        inst_creator.add_floating_ip(FloatingIpConfig(
+            name=self.floating_ip_name, port_name=self.port_1_name,
+            router_name=self.pub_net_config.router_settings.name))
+
+        self.assertTrue(validate_ssh_client(inst_creator))
+
+    def test_ssh_client_fip_reverse_engineer(self):
+        """
+        Tests the ability to assign a floating IP to a reverse engineered
+        OpenStackVmInstance object. After the floating IP has been allocated
+        and assigned, this test will ensure that it can be accessed via SSH.
+        """
+        port_settings = PortConfig(
+            name=self.port_1_name,
+            network_name=self.pub_net_config.network_settings.name)
+
+        instance_settings = VmInstanceConfig(
+            name=self.vm_inst_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            port_settings=[port_settings],
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name])
+
+        inst_creator = OpenStackVmInstance(
+            self.os_creds, instance_settings,
+            self.image_creator.image_settings,
+            keypair_settings=self.keypair_creator.keypair_settings)
+        self.inst_creators.append(inst_creator)
+
+        # block=True will force the create() method to block until the
+        vm_inst = inst_creator.create(block=True)
+        self.assertIsNotNone(vm_inst)
+
+        self.assertTrue(inst_creator.vm_active(block=True))
+
+        derived_inst_creator = create_instance.generate_creator(
+            self.os_creds, vm_inst, self.image_creator.image_settings,
+            self.keypair_creator.keypair_settings)
+
+        derived_inst_creator.add_floating_ip(FloatingIpConfig(
+            name=self.floating_ip_name, port_name=self.port_1_name,
+            router_name=self.pub_net_config.router_settings.name))
+        self.inst_creators.append(derived_inst_creator)
+
+        self.assertTrue(validate_ssh_client(
+            derived_inst_creator, fip_name=self.floating_ip_name))
+
     def test_ssh_client_fip_second_creator(self):
         """
         Tests the ability to access a VM via SSH and a floating IP via a
         creator that is identical to the original creator.
         """
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.pub_net_config.network_settings.name)
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings],
-            floating_ip_settings=[FloatingIpSettings(
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name],
+            floating_ip_settings=[FloatingIpConfig(
                 name=self.floating_ip_name, port_name=self.port_1_name,
                 router_name=self.pub_net_config.router_settings.name)])
 
@@ -794,8 +942,6 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
         ip = inst_creator.get_port_ip(port_settings.name)
         self.assertTrue(check_dhcp_lease(inst_creator, ip))
 
-        inst_creator.add_security_group(
-            self.sec_grp_creator.get_security_group())
         self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id)
 
         self.assertTrue(validate_ssh_client(inst_creator))
@@ -808,6 +954,228 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase):
         self.assertTrue(validate_ssh_client(inst_creator2))
 
 
+class CreateInstanceIPv6NetworkTests(OSIntegrationTestCase):
+    """
+    Test for the CreateInstance class with a single NIC/Port with Floating IPs
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateImage object that is responsible for downloading
+        and creating an OS image file within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        self.nova = nova_utils.nova_client(self.os_creds)
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.keypair_priv_filepath = 'tmp/' + self.guid
+        self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub'
+        self.keypair_name = self.guid + '-kp'
+        self.vm_inst_name = self.guid + '-inst'
+        self.port1_name = self.guid + 'port1'
+        self.port2_name = self.guid + 'port2'
+
+        # Initialize for tearDown()
+        self.image_creator = None
+        self.network_creator = None
+        self.router_creator = None
+        self.flavor_creator = None
+        self.keypair_creator = None
+        self.sec_grp_creator = None
+        self.inst_creator = None
+
+        os_image_settings = openstack_tests.cirros_image_settings(
+            name=self.guid + '-image', image_metadata=self.image_metadata)
+        try:
+            self.image_creator = OpenStackImage(
+                self.os_creds, os_image_settings)
+            self.image_creator.create()
+
+            self.flavor_creator = OpenStackFlavor(
+                self.admin_os_creds,
+                FlavorConfig(
+                    name=self.guid + '-flavor-name', ram=256, disk=10, vcpus=2,
+                    metadata=self.flavor_metadata))
+            self.flavor_creator.create()
+
+            self.keypair_creator = OpenStackKeypair(
+                self.os_creds, KeypairConfig(
+                    name=self.keypair_name,
+                    public_filepath=self.keypair_pub_filepath,
+                    private_filepath=self.keypair_priv_filepath))
+            self.keypair_creator.create()
+
+            sec_grp_name = self.guid + '-sec-grp'
+            rule1 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.icmp)
+            rule2 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.tcp, port_range_min=22, port_range_max=22)
+            self.sec_grp_creator = OpenStackSecurityGroup(
+                self.os_creds,
+                SecurityGroupConfig(
+                    name=sec_grp_name, rule_settings=[rule1, rule2]))
+            self.sec_grp_creator.create()
+        except Exception as e:
+            self.tearDown()
+            raise e
+
+    def tearDown(self):
+        """
+        Cleans the created object
+        """
+        if self.inst_creator:
+            try:
+                self.inst_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning VM instance with message '
+                    '- %s', e)
+
+        if self.keypair_creator:
+            try:
+                self.keypair_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning keypair with message - %s',
+                    e)
+
+        if self.flavor_creator:
+            try:
+                self.flavor_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning flavor with message - %s',
+                    e)
+
+        if self.sec_grp_creator:
+            try:
+                self.sec_grp_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning security group with message'
+                    ' - %s', e)
+
+        if self.router_creator:
+            try:
+                self.router_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning router with message - %s',
+                    e)
+
+        if self.network_creator:
+            try:
+                self.network_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning network with message - %s',
+                    e)
+
+        if self.image_creator and not self.image_creator.image_settings.exists:
+            try:
+                self.image_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning image with message - %s', e)
+
+        super(self.__class__, self).__clean__()
+
+    def test_v4fip_v6overlay(self):
+        """
+        Tests the ability to assign an IPv4 floating IP to an IPv6 overlay
+        network when the external network does not have an IPv6 subnet.
+        """
+        subnet_settings = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6)
+        network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[subnet_settings])
+        router_settings = RouterConfig(
+            name=self.guid + '-router', external_gateway=self.ext_net_name,
+            internal_subnets=[subnet_settings.name])
+
+        # Create Network
+        self.network_creator = OpenStackNetwork(
+            self.os_creds, network_settings)
+        self.network_creator.create()
+
+        # Create Router
+        self.router_creator = OpenStackRouter(
+            self.os_creds, router_settings)
+        self.router_creator.create()
+
+        port_settings = PortConfig(
+            name=self.port1_name, network_name=network_settings.name)
+
+        instance_settings = VmInstanceConfig(
+            name=self.vm_inst_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            port_settings=[port_settings],
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name],
+            floating_ip_settings=[FloatingIpConfig(
+                name='fip1', port_name=self.port1_name,
+                router_name=router_settings.name)])
+
+        self.inst_creator = OpenStackVmInstance(
+            self.os_creds, instance_settings,
+            self.image_creator.image_settings,
+            keypair_settings=self.keypair_creator.keypair_settings)
+
+        with self.assertRaises(BadRequest):
+            self.inst_creator.create(block=True)
+
+    def test_fip_v4and6_overlay(self):
+        """
+        Tests the ability to assign an IPv4 floating IP to an IPv6 overlay
+        network when the external network does not have an IPv6 subnet.
+        """
+        subnet4_settings = SubnetConfig(
+            name=self.guid + '-subnet4', cidr='10.0.1.0/24',
+            ip_version=4)
+        subnet6_settings = SubnetConfig(
+            name=self.guid + '-subnet6', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6)
+        network_settings = NetworkConfig(
+            name=self.guid + '-net',
+            subnet_settings=[subnet4_settings, subnet6_settings])
+        router_settings = RouterConfig(
+            name=self.guid + '-router', external_gateway=self.ext_net_name,
+            internal_subnets=[subnet4_settings.name])
+
+        # Create Network
+        self.network_creator = OpenStackNetwork(
+            self.os_creds, network_settings)
+        self.network_creator.create()
+
+        # Create Router
+        self.router_creator = OpenStackRouter(
+            self.os_creds, router_settings)
+        self.router_creator.create()
+
+        port_settings = PortConfig(
+            name=self.port1_name, network_name=network_settings.name)
+
+        instance_settings = VmInstanceConfig(
+            name=self.vm_inst_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            port_settings=[port_settings],
+            security_group_names=[self.sec_grp_creator.sec_grp_settings.name],
+            floating_ip_settings=[FloatingIpConfig(
+                name='fip1', port_name=self.port1_name,
+                router_name=router_settings.name)])
+
+        self.inst_creator = OpenStackVmInstance(
+            self.os_creds, instance_settings,
+            self.image_creator.image_settings,
+            keypair_settings=self.keypair_creator.keypair_settings)
+
+        self.inst_creator.create(block=True)
+        ssh_client = self.inst_creator.ssh_client()
+        self.assertIsNotNone(ssh_client)
+
+
 class CreateInstancePortManipulationTests(OSIntegrationTestCase):
     """
     Test for the CreateInstance class with a single NIC/Port where mac and IP
@@ -835,7 +1203,8 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
 
         self.net_config = openstack_tests.get_priv_net_config(
             net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet',
-            router_name=guid + '-pub-router', external_net=self.ext_net_name)
+            router_name=guid + '-pub-router', external_net=self.ext_net_name,
+            netconf_override=self.netconf_override)
         os_image_settings = openstack_tests.cirros_image_settings(
             name=guid + '-image', image_metadata=self.image_metadata)
 
@@ -853,8 +1222,8 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=guid + '-flavor-name', ram=256, disk=10,
-                               vcpus=2, metadata=self.flavor_metadata))
+                FlavorConfig(name=guid + '-flavor-name', ram=256, disk=10,
+                             vcpus=2, metadata=self.flavor_metadata))
             self.flavor_creator.create()
         except Exception as e:
             self.tearDown()
@@ -904,12 +1273,12 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         """
         ip = '10.55.0.101'
         sub_settings = self.net_config.network_settings.subnet_settings
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             ip_addrs=[{'subnet_name': sub_settings[0].name, 'ip': ip}])
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -931,12 +1300,12 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         """
         ip = '10.66.0.101'
         sub_settings = self.net_config.network_settings.subnet_settings
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             ip_addrs=[{'subnet_name': sub_settings[0].name, 'ip': ip}])
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -954,12 +1323,12 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         the MAC address is assigned.
         """
         mac_addr = '0a:1b:2c:3d:4e:5f'
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             mac_address=mac_addr)
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -978,12 +1347,12 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         invalid MAC address value is being
         assigned. This should raise an Exception
         """
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             mac_address='foo')
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -1003,13 +1372,13 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         ip = '10.55.0.101'
         mac_addr = '0a:1b:2c:3d:4e:5f'
         sub_settings = self.net_config.network_settings.subnet_settings
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             mac_address=mac_addr,
             ip_addrs=[{'subnet_name': sub_settings[0].name, 'ip': ip}])
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -1034,12 +1403,12 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         ip = '10.55.0.101'
         mac_addr = '0a:1b:2c:3d:4e:5f'
         pair = {'ip_address': ip, 'mac_address': mac_addr}
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             allowed_address_pairs=[pair])
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -1066,12 +1435,12 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         pair = {'ip_address': ip, 'mac_address': mac_addr}
         pairs = set()
         pairs.add((ip, mac_addr))
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             allowed_address_pairs=[pair])
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -1092,12 +1461,12 @@ class CreateInstancePortManipulationTests(OSIntegrationTestCase):
         pair = {'ip_address': ip, 'mac_address': mac_addr}
         pairs = set()
         pairs.add((ip, mac_addr))
-        port_settings = PortSettings(
+        port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.net_config.network_settings.name,
             allowed_address_pairs=[pair])
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[port_settings])
@@ -1126,249 +1495,52 @@ class CreateInstanceOnComputeHost(OSIntegrationTestCase):
         self.port_base_name = guid + 'port'
 
         # Initialize for tearDown()
-        self.image_creator = None
-        self.flavor_creator = None
-        self.network_creator = None
-        self.inst_creators = list()
-
-        self.priv_net_config = openstack_tests.get_priv_net_config(
-            net_name=guid + '-priv-net', subnet_name=guid + '-priv-subnet')
-
-        os_image_settings = openstack_tests.cirros_image_settings(
-            name=guid + '-image', image_metadata=self.image_metadata)
-
-        try:
-            # Create Network
-            self.network_creator = OpenStackNetwork(
-                self.admin_os_creds, self.priv_net_config.network_settings)
-            self.network_creator.create()
-
-            # Create Flavor
-            self.flavor_creator = OpenStackFlavor(
-                self.admin_os_creds,
-                FlavorSettings(name=guid + '-flavor-name', ram=512, disk=1,
-                               vcpus=1, metadata=self.flavor_metadata))
-            self.flavor_creator.create()
-
-            # Create Image
-            self.image_creator = OpenStackImage(self.os_creds,
-                                                os_image_settings)
-            self.image_creator.create()
-
-        except Exception as e:
-            self.tearDown()
-            raise e
-
-    def tearDown(self):
-        """
-        Cleans the created object
-        """
-        for inst_creator in self.inst_creators:
-            try:
-                inst_creator.clean()
-            except Exception as e:
-                logger.error(
-                    'Unexpected exception cleaning VM instance with message '
-                    '- %s', e)
-
-        if self.flavor_creator:
-            try:
-                self.flavor_creator.clean()
-            except Exception as e:
-                logger.error(
-                    'Unexpected exception cleaning flavor with message - %s',
-                    e)
-
-        if self.network_creator:
-            try:
-                self.network_creator.clean()
-            except Exception as e:
-                logger.error(
-                    'Unexpected exception cleaning network with message - %s',
-                    e)
-
-        if self.image_creator and not self.image_creator.image_settings.exists:
-            try:
-                self.image_creator.clean()
-            except Exception as e:
-                logger.error(
-                    'Unexpected exception cleaning image with message - %s', e)
-
-        super(self.__class__, self).__clean__()
-
-    def test_deploy_vm_to_each_compute_node(self):
-        """
-        Tests the creation of OpenStack VM instances to each compute node.
-        """
-        from snaps.openstack.utils import nova_utils
-        nova = nova_utils.nova_client(self.admin_os_creds)
-        zone_hosts = nova_utils.get_availability_zone_hosts(nova)
-
-        # Create Instance on each server/zone
-        ctr = 0
-        for zone in zone_hosts:
-            inst_name = self.vm_inst_name + '-' + zone
-            ctr += 1
-            port_settings = PortSettings(
-                name=self.port_base_name + '-' + str(ctr),
-                network_name=self.priv_net_config.network_settings.name)
-
-            instance_settings = VmInstanceSettings(
-                name=inst_name,
-                flavor=self.flavor_creator.flavor_settings.name,
-                availability_zone=zone,
-                port_settings=[port_settings])
-            inst_creator = OpenStackVmInstance(
-                self.admin_os_creds, instance_settings,
-                self.image_creator.image_settings)
-            self.inst_creators.append(inst_creator)
-            inst_creator.create()
-
-        # Validate instances to ensure they've been deployed to the correct
-        # server
-        index = 0
-        for zone in zone_hosts:
-            creator = self.inst_creators[index]
-            self.assertTrue(creator.vm_active(block=True))
-            info = creator.get_vm_info()
-            deployed_zone = info['OS-EXT-AZ:availability_zone']
-            deployed_host = info['OS-EXT-SRV-ATTR:host']
-            self.assertEqual(zone, deployed_zone + ':' + deployed_host)
-            index += 1
-
-
-class CreateInstancePubPrivNetTests(OSIntegrationTestCase):
-    """
-    Test for the CreateInstance class with two NIC/Ports, eth0 with floating IP
-    and eth1 w/o.
-    These tests require a Centos image
-    """
-
-    def setUp(self):
-        """
-        Instantiates the CreateImage object that is responsible for downloading
-        and creating an OS image file within OpenStack
-        """
-        super(self.__class__, self).__start__()
-
-        self.nova = nova_utils.nova_client(self.os_creds)
-
-        # Initialize for tearDown()
-        self.image_creator = None
-        self.network_creators = list()
-        self.router_creators = list()
-        self.flavor_creator = None
-        self.keypair_creator = None
-        self.sec_grp_creator = None
-        self.inst_creator = None
-
-        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
-        self.keypair_priv_filepath = 'tmp/' + self.guid
-        self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub'
-        self.keypair_name = self.guid + '-kp'
-        self.vm_inst_name = self.guid + '-inst'
-        self.port_1_name = self.guid + '-port-1'
-        self.port_2_name = self.guid + '-port-2'
-        self.floating_ip_name = self.guid + 'fip1'
-        self.priv_net_config = openstack_tests.get_priv_net_config(
-            net_name=self.guid + '-priv-net',
-            subnet_name=self.guid + '-priv-subnet',
-            router_name=self.guid + '-priv-router',
-            external_net=self.ext_net_name)
-        self.pub_net_config = openstack_tests.get_pub_net_config(
-            net_name=self.guid + '-pub-net',
-            subnet_name=self.guid + '-pub-subnet',
-            router_name=self.guid + '-pub-router',
-            external_net=self.ext_net_name)
-
-        image_name = self.__class__.__name__ + '-' + str(uuid.uuid4())
-        os_image_settings = openstack_tests.centos_image_settings(
-            name=image_name, image_metadata=self.image_metadata)
-
-        try:
-            # Create Image
-            self.image_creator = OpenStackImage(self.os_creds,
-                                                os_image_settings)
-            self.image_creator.create()
+        self.image_creator = None
+        self.flavor_creator = None
+        self.network_creator = None
+        self.inst_creators = list()
 
-            # First network is public
-            self.network_creators.append(OpenStackNetwork(
-                self.os_creds, self.pub_net_config.network_settings))
-            # Second network is private
-            self.network_creators.append(OpenStackNetwork(
-                self.os_creds, self.priv_net_config.network_settings))
-            for network_creator in self.network_creators:
-                network_creator.create()
+        self.priv_net_config = openstack_tests.get_priv_net_config(
+            net_name=guid + '-priv-net', subnet_name=guid + '-priv-subnet',
+            netconf_override=self.netconf_override)
 
-            self.router_creators.append(OpenStackRouter(
-                self.os_creds, self.pub_net_config.router_settings))
-            self.router_creators.append(OpenStackRouter(
-                self.os_creds, self.priv_net_config.router_settings))
+        os_image_settings = openstack_tests.cirros_image_settings(
+            name=guid + '-image', image_metadata=self.image_metadata)
 
-            # Create Routers
-            for router_creator in self.router_creators:
-                router_creator.create()
+        try:
+            # Create Network
+            self.network_creator = OpenStackNetwork(
+                self.admin_os_creds, self.priv_net_config.network_settings)
+            self.network_creator.create()
 
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=self.guid + '-flavor-name', ram=512,
-                               disk=10, vcpus=2,
-                               metadata=self.flavor_metadata))
+                FlavorConfig(name=guid + '-flavor-name', ram=512, disk=1,
+                             vcpus=1, metadata=self.flavor_metadata))
             self.flavor_creator.create()
 
-            # Create Keypair
-            self.keypair_creator = OpenStackKeypair(
-                self.os_creds, KeypairSettings(
-                    name=self.keypair_name,
-                    public_filepath=self.keypair_pub_filepath,
-                    private_filepath=self.keypair_priv_filepath))
-            self.keypair_creator.create()
+            # Create Image
+            self.image_creator = OpenStackImage(self.os_creds,
+                                                os_image_settings)
+            self.image_creator.create()
 
-            sec_grp_name = self.guid + '-sec-grp'
-            rule1 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.icmp)
-            rule2 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.tcp,
-                                              port_range_min=22,
-                                              port_range_max=22)
-            self.sec_grp_creator = OpenStackSecurityGroup(
-                self.os_creds,
-                SecurityGroupSettings(name=sec_grp_name,
-                                      rule_settings=[rule1, rule2]))
-            self.sec_grp_creator.create()
-        except:
+        except Exception as e:
             self.tearDown()
-            raise
+            raise e
 
     def tearDown(self):
         """
-        Cleans the created objects
+        Cleans the created object
         """
-        if self.inst_creator:
+        for inst_creator in self.inst_creators:
             try:
-                self.inst_creator.clean()
+                inst_creator.clean()
             except Exception as e:
                 logger.error(
                     'Unexpected exception cleaning VM instance with message '
                     '- %s', e)
 
-        if self.keypair_creator:
-            try:
-                self.keypair_creator.clean()
-            except Exception as e:
-                logger.error(
-                    'Unexpected exception cleaning keypair with message - %s',
-                    e)
-
-        if os.path.isfile(self.keypair_pub_filepath):
-            os.remove(self.keypair_pub_filepath)
-
-        if os.path.isfile(self.keypair_priv_filepath):
-            os.remove(self.keypair_priv_filepath)
-
         if self.flavor_creator:
             try:
                 self.flavor_creator.clean()
@@ -1377,30 +1549,14 @@ class CreateInstancePubPrivNetTests(OSIntegrationTestCase):
                     'Unexpected exception cleaning flavor with message - %s',
                     e)
 
-        for router_creator in self.router_creators:
-            try:
-                router_creator.clean()
-            except Exception as e:
-                logger.error(
-                    'Unexpected exception cleaning router with message - %s',
-                    e)
-
-        for network_creator in self.network_creators:
+        if self.network_creator:
             try:
-                network_creator.clean()
+                self.network_creator.clean()
             except Exception as e:
                 logger.error(
                     'Unexpected exception cleaning network with message - %s',
                     e)
 
-        if self.sec_grp_creator:
-            try:
-                self.sec_grp_creator.clean()
-            except Exception as e:
-                logger.error(
-                    'Unexpected exception cleaning security group with message'
-                    ' - %s', e)
-
         if self.image_creator and not self.image_creator.image_settings.exists:
             try:
                 self.image_creator.clean()
@@ -1410,60 +1566,45 @@ class CreateInstancePubPrivNetTests(OSIntegrationTestCase):
 
         super(self.__class__, self).__clean__()
 
-    def test_dual_ports_dhcp(self):
+    def test_deploy_vm_to_each_compute_node(self):
         """
-        Tests the creation of an OpenStack instance with a dual ports/NICs with
-        a DHCP assigned IP.
-        NOTE: This test and any others that call ansible will most likely fail
-        unless you do one of two things:
-        1. Have a ~/.ansible.cfg (or alternate means) to
-           set host_key_checking = False
-        2. Set the following environment variable in your executing shell:
-           ANSIBLE_HOST_KEY_CHECKING=False
-        Should this not be performed, the creation of the host ssh key will
-        cause your ansible calls to fail.
+        Tests the creation of OpenStack VM instances to each compute node.
         """
-        # Create ports/NICs for instance
-        ports_settings = []
-        ctr = 1
-        for network_creator in self.network_creators:
-            ports_settings.append(PortSettings(
-                name=self.guid + '-port-' + str(ctr),
-                network_name=network_creator.network_settings.name))
-            ctr += 1
-
-        # Create instance
-        instance_settings = VmInstanceSettings(
-            name=self.vm_inst_name,
-            flavor=self.flavor_creator.flavor_settings.name,
-            port_settings=ports_settings,
-            floating_ip_settings=[FloatingIpSettings(
-                name=self.floating_ip_name, port_name=self.port_1_name,
-                router_name=self.pub_net_config.router_settings.name)])
-
-        self.inst_creator = OpenStackVmInstance(
-            self.os_creds, instance_settings,
-            self.image_creator.image_settings,
-            keypair_settings=self.keypair_creator.keypair_settings)
-
-        vm_inst = self.inst_creator.create(block=True)
-
-        self.assertEqual(vm_inst.id, self.inst_creator.get_vm_inst().id)
-
-        # Effectively blocks until VM has been properly activated
-        self.assertTrue(self.inst_creator.vm_active(block=True))
-
-        ip = self.inst_creator.get_port_ip(ports_settings[0].name)
-        self.assertTrue(check_dhcp_lease(self.inst_creator, ip))
+        from snaps.openstack.utils import nova_utils
+        nova = nova_utils.nova_client(self.admin_os_creds)
+        zone_hosts = nova_utils.get_availability_zone_hosts(nova)
 
-        # Add security group to VM
-        self.inst_creator.add_security_group(
-            self.sec_grp_creator.get_security_group())
+        # Create Instance on each server/zone
+        ctr = 0
+        for zone in zone_hosts:
+            inst_name = self.vm_inst_name + '-' + zone
+            ctr += 1
+            port_settings = PortConfig(
+                name=self.port_base_name + '-' + str(ctr),
+                network_name=self.priv_net_config.network_settings.name)
 
-        # Effectively blocks until VM's ssh port has been opened
-        self.assertTrue(self.inst_creator.vm_ssh_active(block=True))
+            instance_settings = VmInstanceConfig(
+                name=inst_name,
+                flavor=self.flavor_creator.flavor_settings.name,
+                availability_zone=zone,
+                port_settings=[port_settings])
+            inst_creator = OpenStackVmInstance(
+                self.admin_os_creds, instance_settings,
+                self.image_creator.image_settings)
+            self.inst_creators.append(inst_creator)
+            inst_creator.create()
 
-        self.assertEqual(0, self.inst_creator.config_nics())
+        # Validate instances to ensure they've been deployed to the correct
+        # server
+        index = 0
+        for zone in zone_hosts:
+            creator = self.inst_creators[index]
+            self.assertTrue(creator.vm_active(block=True))
+            info = creator.get_vm_info()
+            deployed_zone = info['OS-EXT-AZ:availability_zone']
+            deployed_host = info['OS-EXT-SRV-ATTR:host']
+            self.assertEqual(zone, deployed_zone + ':' + deployed_host)
+            index += 1
 
 
 class InstanceSecurityGroupTests(OSIntegrationTestCase):
@@ -1493,7 +1634,8 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
             net_name=self.guid + '-pub-net',
             subnet_name=self.guid + '-pub-subnet',
             router_name=self.guid + '-pub-router',
-            external_net=self.ext_net_name)
+            external_net=self.ext_net_name,
+            netconf_override=self.netconf_override)
 
         # Initialize for tearDown()
         self.image_creator = None
@@ -1517,12 +1659,12 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=self.guid + '-flavor-name', ram=256,
-                               disk=10, vcpus=2,
-                               metadata=self.flavor_metadata))
+                FlavorConfig(name=self.guid + '-flavor-name', ram=256,
+                             disk=10, vcpus=2,
+                             metadata=self.flavor_metadata))
             self.flavor_creator.create()
 
-            self.port_settings = PortSettings(
+            self.port_settings = PortConfig(
                 name=self.guid + '-port',
                 network_name=net_config.network_settings.name)
         except Exception as e:
@@ -1579,7 +1721,7 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
         Tests the addition of a security group created after the instance.
         """
         # Create instance
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -1590,8 +1732,8 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
         self.assertIsNotNone(vm_inst)
 
         # Create security group object to add to instance
-        sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name',
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(
+            name=self.guid + '-name', description='hello group')
         sec_grp_creator = OpenStackSecurityGroup(self.os_creds,
                                                  sec_grp_settings)
         sec_grp = sec_grp_creator.create()
@@ -1613,7 +1755,7 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
         Tests the addition of a security group that no longer exists.
         """
         # Create instance
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -1624,8 +1766,8 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
         self.assertIsNotNone(vm_inst)
 
         # Create security group object to add to instance
-        sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name',
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(
+            name=self.guid + '-name', description='hello group')
         sec_grp_creator = OpenStackSecurityGroup(self.os_creds,
                                                  sec_grp_settings)
         sec_grp = sec_grp_creator.create()
@@ -1649,15 +1791,15 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
         instance.
         """
         # Create security group object to add to instance
-        sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name',
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(
+            name=self.guid + '-name', description='hello group')
         sec_grp_creator = OpenStackSecurityGroup(self.os_creds,
                                                  sec_grp_settings)
         sec_grp = sec_grp_creator.create()
         self.sec_grp_creators.append(sec_grp_creator)
 
         # Create instance
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             security_group_names=[sec_grp_settings.name],
@@ -1685,15 +1827,15 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
         place.
         """
         # Create security group object to add to instance
-        sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name',
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(
+            name=self.guid + '-name', description='hello group')
         sec_grp_creator = OpenStackSecurityGroup(self.os_creds,
                                                  sec_grp_settings)
         sec_grp = sec_grp_creator.create()
         self.sec_grp_creators.append(sec_grp_creator)
 
         # Create instance
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -1720,15 +1862,15 @@ class InstanceSecurityGroupTests(OSIntegrationTestCase):
         instance.
         """
         # Create security group object to add to instance
-        sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name',
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(
+            name=self.guid + '-name', description='hello group')
         sec_grp_creator = OpenStackSecurityGroup(self.os_creds,
                                                  sec_grp_settings)
         sec_grp = sec_grp_creator.create()
         self.sec_grp_creators.append(sec_grp_creator)
 
         # Create instance
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             security_group_names=[sec_grp_settings.name],
@@ -1766,17 +1908,18 @@ def inst_has_sec_grp(nova, vm_inst, sec_grp_name):
     return False
 
 
-def validate_ssh_client(instance_creator):
+def validate_ssh_client(instance_creator, fip_name=None):
     """
     Returns True if instance_creator returns an SSH client that is valid
     :param instance_creator: the object responsible for creating the VM
                              instance
+    :param fip_name: the name of the floating IP to use
     :return: T/F
     """
     ssh_active = instance_creator.vm_ssh_active(block=True)
 
     if ssh_active:
-        ssh_client = instance_creator.ssh_client()
+        ssh_client = instance_creator.ssh_client(fip_name=fip_name)
         if ssh_client:
             try:
                 out = ssh_client.exec_command('pwd')[1]
@@ -1813,7 +1956,8 @@ class CreateInstanceFromThreePartImage(OSIntegrationTestCase):
 
         net_config = openstack_tests.get_priv_net_config(
             net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet',
-            router_name=guid + '-pub-router', external_net=self.ext_net_name)
+            router_name=guid + '-pub-router', external_net=self.ext_net_name,
+            netconf_override=self.netconf_override)
 
         # Initialize for tearDown()
         self.image_creator = None
@@ -1851,8 +1995,8 @@ class CreateInstanceFromThreePartImage(OSIntegrationTestCase):
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=guid + '-flavor-name', ram=256, disk=10,
-                               vcpus=2, metadata=self.flavor_metadata))
+                FlavorConfig(name=guid + '-flavor-name', ram=256, disk=10,
+                             vcpus=2, metadata=self.flavor_metadata))
             self.flavor_creator.create()
 
             # Create Network
@@ -1860,7 +2004,7 @@ class CreateInstanceFromThreePartImage(OSIntegrationTestCase):
                 self.os_creds, net_config.network_settings)
             self.network_creator.create()
 
-            self.port_settings = PortSettings(
+            self.port_settings = PortConfig(
                 name=guid + '-port',
                 network_name=net_config.network_settings.name)
         except Exception as e:
@@ -1908,7 +2052,7 @@ class CreateInstanceFromThreePartImage(OSIntegrationTestCase):
         """
         Tests the creation of an OpenStack instance from a 3-part image.
         """
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -1955,7 +2099,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         self.priv_net_config = openstack_tests.get_priv_net_config(
             net_name=self.guid + '-priv-net',
             subnet_name=self.guid + '-priv-subnet')
-        self.port_settings = PortSettings(
+        self.port_settings = PortConfig(
             name=self.port_1_name,
             network_name=self.priv_net_config.network_settings.name)
 
@@ -1972,7 +2116,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.os_creds,
-                FlavorSettings(
+                FlavorConfig(
                     name=self.guid + '-flavor-name', ram=256, disk=10,
                     vcpus=1))
             self.flavor_creator.create()
@@ -2043,7 +2187,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         self.image_creator = OpenStackImage(self.os_creds, os_image_settings)
         self.image_creator.create()
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2079,7 +2223,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         self.image_creator = OpenStackImage(self.os_creds, os_image_settings)
         self.image_creator.create()
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2104,14 +2248,14 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         image_settings = self.image_creator.image_settings
         test_image_creator = OpenStackImage(
             self.os_creds,
-            ImageSettings(name=image_settings.name,
-                          image_user=image_settings.image_user,
-                          exists=True))
+            ImageConfig(
+                name=image_settings.name, image_user=image_settings.image_user,
+                exists=True))
         test_image_creator.create()
         self.assertEqual(self.image_creator.get_image().id,
                          test_image_creator.get_image().id)
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2146,7 +2290,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         test_image = OpenStackImage(self.os_creds, test_image_settings)
         test_image.create()
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2226,7 +2370,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         self.image_creator = OpenStackImage(self.os_creds, os_image_settings)
         self.image_creator.create()
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2296,7 +2440,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         self.assertIsNotNone(self.image_creator.get_kernel_image())
         self.assertIsNotNone(self.image_creator.get_ramdisk_image())
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2366,7 +2510,7 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         self.assertIsNotNone(self.image_creator.get_kernel_image())
         self.assertIsNotNone(self.image_creator.get_ramdisk_image())
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2399,14 +2543,14 @@ class CreateInstanceMockOfflineTests(OSComponentTestCase):
         image_settings = self.image_creator.image_settings
         test_image_creator = OpenStackImage(
             self.os_creds,
-            ImageSettings(name=image_settings.name,
-                          image_user=image_settings.image_user,
-                          exists=True))
+            ImageConfig(
+                name=image_settings.name, image_user=image_settings.image_user,
+                exists=True))
         test_image_creator.create()
         self.assertEqual(self.image_creator.get_image().id,
                          test_image_creator.get_image().id)
 
-        instance_settings = VmInstanceSettings(
+        instance_settings = VmInstanceConfig(
             name=self.vm_inst_name,
             flavor=self.flavor_creator.flavor_settings.name,
             port_settings=[self.port_settings])
@@ -2453,16 +2597,16 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
         self.vm_inst2_name = self.guid + '-inst2'
         self.port_1_name = self.guid + '-vm1-port'
         self.port_2_name = self.guid + '-vm2-port'
-        self.net_config_1 = NetworkSettings(
+        self.net_config_1 = NetworkConfig(
             name=self.guid + '-net1',
             subnet_settings=[
-                create_network.SubnetSettings(
+                create_network.SubnetConfig(
                     cidr=cidr1, name=self.guid + '-subnet1',
                     gateway_ip=static_gateway_ip1)])
-        self.net_config_2 = NetworkSettings(
+        self.net_config_2 = NetworkConfig(
             name=self.guid + '-net2',
             subnet_settings=[
-                create_network.SubnetSettings(
+                create_network.SubnetConfig(
                     cidr=cidr2, name=self.guid + '-subnet2',
                     gateway_ip=static_gateway_ip2)])
 
@@ -2486,7 +2630,7 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
                 network_creator.create()
 
             port_settings = [
-                create_network.PortSettings(
+                create_network.PortConfig(
                     name=self.guid + '-router-port1',
                     ip_addrs=[{
                         'subnet_name':
@@ -2495,7 +2639,7 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
                     }],
                     network_name=self.net_config_1.name,
                     project_name=self.os_creds.project_name),
-                create_network.PortSettings(
+                create_network.PortConfig(
                     name=self.guid + '-router-port2',
                     ip_addrs=[{
                         'subnet_name':
@@ -2505,8 +2649,8 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
                     network_name=self.net_config_2.name,
                     project_name=self.os_creds.project_name)]
 
-            router_settings = RouterSettings(name=self.guid + '-pub-router',
-                                             port_settings=port_settings)
+            router_settings = RouterConfig(
+                name=self.guid + '-pub-router', port_settings=port_settings)
             self.router_creator = create_router.OpenStackRouter(
                 self.os_creds, router_settings)
             self.router_creator.create()
@@ -2514,19 +2658,19 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
             # Create Flavor
             self.flavor_creator = OpenStackFlavor(
                 self.admin_os_creds,
-                FlavorSettings(name=self.guid + '-flavor-name', ram=512,
-                               disk=10, vcpus=2,
-                               metadata=self.flavor_metadata))
+                FlavorConfig(name=self.guid + '-flavor-name', ram=512,
+                             disk=10, vcpus=2,
+                             metadata=self.flavor_metadata))
             self.flavor_creator.create()
 
             sec_grp_name = self.guid + '-sec-grp'
-            rule1 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.icmp)
+            rule1 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.icmp)
             self.sec_grp_creator = OpenStackSecurityGroup(
                 self.os_creds,
-                SecurityGroupSettings(name=sec_grp_name,
-                                      rule_settings=[rule1]))
+                SecurityGroupConfig(
+                    name=sec_grp_name, rule_settings=[rule1]))
             self.sec_grp_creator.create()
         except:
             self.tearDown()
@@ -2596,17 +2740,17 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
         ports_settings = []
         ctr = 1
         for network_creator in self.network_creators:
-            ports_settings.append(PortSettings(
+            ports_settings.append(PortConfig(
                 name=self.guid + '-port-' + str(ctr),
                 network_name=network_creator.network_settings.name))
             ctr += 1
 
         # Configure instances
-        instance1_settings = VmInstanceSettings(
+        instance1_settings = VmInstanceConfig(
             name=self.vm_inst1_name,
             flavor=self.flavor_creator.flavor_settings.name,
             userdata=_get_ping_userdata(self.ip2),
-            port_settings=[PortSettings(
+            port_settings=[PortConfig(
                 name=self.port_1_name,
                 ip_addrs=[{
                     'subnet_name':
@@ -2614,11 +2758,11 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
                     'ip': self.ip1
                 }],
                 network_name=self.network_creators[0].network_settings.name)])
-        instance2_settings = VmInstanceSettings(
+        instance2_settings = VmInstanceConfig(
             name=self.vm_inst2_name,
             flavor=self.flavor_creator.flavor_settings.name,
             userdata=_get_ping_userdata(self.ip1),
-            port_settings=[PortSettings(
+            port_settings=[PortConfig(
                 name=self.port_2_name,
                 ip_addrs=[{
                     'subnet_name':
@@ -2647,6 +2791,182 @@ class CreateInstanceTwoNetTests(OSIntegrationTestCase):
         self.assertTrue(check_ping(self.inst_creators[1]))
 
 
+class CreateInstanceVolumeTests(OSIntegrationTestCase):
+    """
+    Simple instance creation with an attached volume
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateImage object that is responsible for downloading
+        and creating an OS image file
+        within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.vm_inst_name = guid + '-inst'
+        self.nova = nova_utils.nova_client(self.os_creds)
+        self.neutron = neutron_utils.neutron_client(self.os_creds)
+        os_image_settings = openstack_tests.cirros_image_settings(
+            name=guid + '-image', image_metadata=self.image_metadata)
+
+        net_config = openstack_tests.get_priv_net_config(
+            net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet',
+            router_name=guid + '-pub-router', external_net=self.ext_net_name,
+            netconf_override=self.netconf_override)
+
+        self.volume_settings1 = VolumeConfig(
+            name=self.__class__.__name__ + '-' + str(guid) + '-1')
+        self.volume_settings2 = VolumeConfig(
+            name=self.__class__.__name__ + '-' + str(guid) + '-2')
+
+        # Initialize for tearDown()
+        self.image_creator = None
+        self.flavor_creator = None
+
+        self.network_creator = None
+        self.inst_creator = None
+        self.volume_creator1 = None
+        self.volume_creator2 = None
+
+        try:
+            # Create Image
+            self.image_creator = OpenStackImage(self.os_creds,
+                                                os_image_settings)
+            self.image_creator.create()
+
+            # Create Flavor
+            self.flavor_creator = OpenStackFlavor(
+                self.admin_os_creds,
+                FlavorConfig(name=guid + '-flavor-name', ram=256, disk=1,
+                             vcpus=2, metadata=self.flavor_metadata))
+            self.flavor_creator.create()
+
+            # Create Network
+            self.network_creator = OpenStackNetwork(
+                self.os_creds, net_config.network_settings)
+            self.network_creator.create()
+
+            self.port_settings = PortConfig(
+                name=guid + '-port',
+                network_name=net_config.network_settings.name)
+
+            self.volume_creator1 = OpenStackVolume(
+                self.os_creds, self.volume_settings1)
+            self.volume_creator1.create(block=True)
+
+            self.volume_creator2 = OpenStackVolume(
+                self.os_creds, self.volume_settings2)
+            self.volume_creator2.create(block=True)
+
+        except Exception as e:
+            self.tearDown()
+            raise e
+
+    def tearDown(self):
+        """
+        Cleans the created object
+        """
+        if self.inst_creator:
+            try:
+                self.inst_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning VM instance with message '
+                    '- %s', e)
+
+        if self.flavor_creator:
+            try:
+                self.flavor_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning flavor with message - %s',
+                    e)
+
+        if self.network_creator:
+            try:
+                self.network_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning network with message - %s',
+                    e)
+
+        if self.volume_creator2:
+            try:
+                self.volume_creator2.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning volume with message - %s',
+                    e)
+
+        if self.volume_creator1:
+            try:
+                self.volume_creator1.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning volume with message - %s',
+                    e)
+
+        if self.image_creator and not self.image_creator.image_settings.exists:
+            try:
+                self.image_creator.clean()
+            except Exception as e:
+                logger.error(
+                    'Unexpected exception cleaning image with message - %s', e)
+
+        super(self.__class__, self).__clean__()
+
+    def test_create_instance_with_one_volume(self):
+        """
+        Tests the creation of an OpenStack instance with a single volume.
+        """
+        instance_settings = VmInstanceConfig(
+            name=self.vm_inst_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            port_settings=[self.port_settings],
+            volume_names=[self.volume_settings1.name])
+
+        self.inst_creator = OpenStackVmInstance(
+            self.os_creds, instance_settings,
+            self.image_creator.image_settings)
+
+        vm_inst = self.inst_creator.create(block=True)
+        self.assertIsNotNone(nova_utils.get_server(
+            self.nova, self.neutron, vm_inst_settings=instance_settings))
+
+        self.assertIsNotNone(vm_inst)
+        self.assertEqual(1, len(vm_inst.volume_ids))
+        self.assertEqual(self.volume_creator1.get_volume().id,
+                         vm_inst.volume_ids[0]['id'])
+
+    def test_create_instance_with_two_volumes(self):
+        """
+        Tests the creation of an OpenStack instance with a single volume.
+        """
+        instance_settings = VmInstanceConfig(
+            name=self.vm_inst_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            port_settings=[self.port_settings],
+            volume_names=[self.volume_settings1.name,
+                          self.volume_settings2.name])
+
+        self.inst_creator = OpenStackVmInstance(
+            self.os_creds, instance_settings,
+            self.image_creator.image_settings)
+
+        vm_inst = self.inst_creator.create(block=True)
+        self.assertIsNotNone(nova_utils.get_server(
+            self.nova, self.neutron, vm_inst_settings=instance_settings))
+
+        self.assertIsNotNone(vm_inst)
+        self.assertEqual(2, len(vm_inst.volume_ids))
+        self.assertEqual(self.volume_creator1.get_volume().id,
+                         vm_inst.volume_ids[0]['id'])
+        self.assertEqual(self.volume_creator2.get_volume().id,
+                         vm_inst.volume_ids[1]['id'])
+
+
 def check_dhcp_lease(inst_creator, ip, timeout=160):
     """
     Returns true if the expected DHCP lease has been acquired
index d2de6fe..63e0bcc 100644 (file)
@@ -18,8 +18,9 @@ import uuid
 import os
 
 from snaps import file_utils
+from snaps.config.keypair import KeypairConfig, KeypairConfigError
 from snaps.openstack.create_keypairs import (
-    KeypairSettings, OpenStackKeypair, KeypairSettingsError)
+    KeypairSettings, OpenStackKeypair)
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
 from snaps.openstack.utils import nova_utils
 
@@ -32,15 +33,15 @@ class KeypairSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(KeypairSettingsError):
+        with self.assertRaises(KeypairConfigError):
             KeypairSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(KeypairSettingsError):
+        with self.assertRaises(KeypairConfigError):
             KeypairSettings(**dict())
 
     def test_small_key_size(self):
-        with self.assertRaises(KeypairSettingsError):
+        with self.assertRaises(KeypairConfigError):
             KeypairSettings(name='foo', key_size=511)
 
     def test_name_only(self):
@@ -228,7 +229,7 @@ class CreateKeypairsTests(OSIntegrationTestCase):
         Tests the creation of a generated keypair without saving to file
         :return:
         """
-        self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(
+        self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairConfig(
             name=self.keypair_name))
         self.keypair_creator.create()
 
@@ -241,7 +242,7 @@ class CreateKeypairsTests(OSIntegrationTestCase):
         Tests the creation of a generated keypair without saving to file
         :return:
         """
-        self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(
+        self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairConfig(
             name=self.keypair_name, key_size=10000))
         self.keypair_creator.create()
 
@@ -255,7 +256,7 @@ class CreateKeypairsTests(OSIntegrationTestCase):
         clean() does not raise an Exception.
         """
         # Create Image
-        self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(
+        self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairConfig(
             name=self.keypair_name))
         created_keypair = self.keypair_creator.create()
         self.assertIsNotNone(created_keypair)
@@ -277,8 +278,8 @@ class CreateKeypairsTests(OSIntegrationTestCase):
         :return:
         """
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(name=self.keypair_name,
-                                           public_filepath=self.pub_file_path))
+            self.os_creds, KeypairConfig(
+                name=self.keypair_name, public_filepath=self.pub_file_path))
         self.keypair_creator.create()
 
         keypair = nova_utils.keypair_exists(self.nova,
@@ -302,7 +303,7 @@ class CreateKeypairsTests(OSIntegrationTestCase):
         :return:
         """
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(
+            self.os_creds, KeypairConfig(
                 name=self.keypair_name, public_filepath=self.pub_file_path,
                 private_filepath=self.priv_file_path))
         self.keypair_creator.create()
@@ -335,8 +336,8 @@ class CreateKeypairsTests(OSIntegrationTestCase):
         file_utils.save_keys_to_files(keys=keys,
                                       pub_file_path=self.pub_file_path)
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(name=self.keypair_name,
-                                           public_filepath=self.pub_file_path))
+            self.os_creds, KeypairConfig(
+                name=self.keypair_name, public_filepath=self.pub_file_path))
         self.keypair_creator.create()
 
         keypair = nova_utils.keypair_exists(self.nova,
@@ -400,7 +401,7 @@ class CreateKeypairsCleanupTests(OSIntegrationTestCase):
         :return:
         """
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(
+            self.os_creds, KeypairConfig(
                 name=self.keypair_name, public_filepath=self.pub_file_path,
                 private_filepath=self.priv_file_path))
         self.keypair_creator.create()
@@ -416,7 +417,7 @@ class CreateKeypairsCleanupTests(OSIntegrationTestCase):
         :return:
         """
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(
+            self.os_creds, KeypairConfig(
                 name=self.keypair_name, public_filepath=self.pub_file_path,
                 private_filepath=self.priv_file_path, delete_on_clean=True))
         self.keypair_creator.create()
@@ -432,7 +433,7 @@ class CreateKeypairsCleanupTests(OSIntegrationTestCase):
         :return:
         """
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(
+            self.os_creds, KeypairConfig(
                 name=self.keypair_name, public_filepath=self.pub_file_path,
                 private_filepath=self.priv_file_path, delete_on_clean=False))
         self.keypair_creator.create()
@@ -452,7 +453,7 @@ class CreateKeypairsCleanupTests(OSIntegrationTestCase):
             keys=keys, pub_file_path=self.pub_file_path,
             priv_file_path=self.priv_file_path)
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(
+            self.os_creds, KeypairConfig(
                 name=self.keypair_name, public_filepath=self.pub_file_path,
                 private_filepath=self.priv_file_path, delete_on_clean=False))
         self.keypair_creator.create()
@@ -472,7 +473,7 @@ class CreateKeypairsCleanupTests(OSIntegrationTestCase):
             keys=keys, pub_file_path=self.pub_file_path,
             priv_file_path=self.priv_file_path)
         self.keypair_creator = OpenStackKeypair(
-            self.os_creds, KeypairSettings(
+            self.os_creds, KeypairConfig(
                 name=self.keypair_name, public_filepath=self.pub_file_path,
                 private_filepath=self.priv_file_path, delete_on_clean=True))
         self.keypair_creator.create()
index 9cee130..966cbd0 100644 (file)
 import unittest
 import uuid
 
+from snaps.config.network import (
+    NetworkConfig, SubnetConfig, SubnetConfigError, NetworkConfigError,
+    PortConfigError, IPv6Mode)
 from snaps.openstack import create_router
-from snaps.openstack.create_network import (OpenStackNetwork, NetworkSettings,
-                                            SubnetSettings, PortSettings,
-                                            NetworkSettingsError,
-                                            SubnetSettingsError,
-                                            PortSettingsError)
+from snaps.openstack.create_network import (
+    OpenStackNetwork, NetworkSettings, SubnetSettings, PortSettings)
 from snaps.openstack.tests import openstack_tests
-from snaps.openstack.tests.os_source_file_test import (OSIntegrationTestCase,
-                                                       OSComponentTestCase)
+from snaps.openstack.tests.os_source_file_test import (
+    OSIntegrationTestCase, OSComponentTestCase)
 from snaps.openstack.utils import neutron_utils
 from snaps.openstack.utils.tests import neutron_utils_tests
+from snaps.openstack.create_network import IPv6Mode as IPv6Mode_old
 
 __author__ = 'spisarski'
 
@@ -36,11 +37,11 @@ class NetworkSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(NetworkSettingsError):
+        with self.assertRaises(NetworkConfigError):
             NetworkSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(NetworkSettingsError):
+        with self.assertRaises(NetworkConfigError):
             NetworkSettings(**dict())
 
     def test_name_only(self):
@@ -111,19 +112,19 @@ class SubnetSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(SubnetSettingsError):
+        with self.assertRaises(SubnetConfigError):
             SubnetSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(SubnetSettingsError):
+        with self.assertRaises(SubnetConfigError):
             SubnetSettings(**dict())
 
     def test_name_only(self):
-        with self.assertRaises(SubnetSettingsError):
+        with self.assertRaises(SubnetConfigError):
             SubnetSettings(name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(SubnetSettingsError):
+        with self.assertRaises(SubnetConfigError):
             SubnetSettings(**{'name': 'foo'})
 
     def test_name_cidr_only(self):
@@ -161,18 +162,44 @@ class SubnetSettingsUnitTests(unittest.TestCase):
         self.assertIsNone(settings.ipv6_ra_mode)
         self.assertIsNone(settings.ipv6_address_mode)
 
-    def test_all(self):
+    def test_all_string_enums(self):
+        host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'}
+        settings = SubnetSettings(
+            name='foo', cidr='10.0.0.0/24', ip_version=6,
+            project_name='bar-project', start='10.0.0.2', end='10.0.0.101',
+            gateway_ip='10.0.0.1', enable_dhcp=False,
+            dns_nameservers=['8.8.8.8'], host_routes=[host_routes],
+            destination='dest', nexthop='hop', ipv6_ra_mode='dhcpv6-stateful',
+            ipv6_address_mode='slaac')
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('10.0.0.0/24', settings.cidr)
+        self.assertEqual(6, settings.ip_version)
+        self.assertEqual('bar-project', settings.project_name)
+        self.assertEqual('10.0.0.2', settings.start)
+        self.assertEqual('10.0.0.101', settings.end)
+        self.assertEqual('10.0.0.1', settings.gateway_ip)
+        self.assertEqual(False, settings.enable_dhcp)
+        self.assertEqual(1, len(settings.dns_nameservers))
+        self.assertEqual('8.8.8.8', settings.dns_nameservers[0])
+        self.assertEqual(1, len(settings.host_routes))
+        self.assertEqual(host_routes, settings.host_routes[0])
+        self.assertEqual('dest', settings.destination)
+        self.assertEqual('hop', settings.nexthop)
+        self.assertEqual(IPv6Mode_old.stateful.value,
+                         settings.ipv6_ra_mode.value)
+        self.assertEqual(IPv6Mode_old.slaac.value,
+                         settings.ipv6_address_mode.value)
+
+    def test_all_type_enums(self):
         host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'}
-        settings = SubnetSettings(name='foo', cidr='10.0.0.0/24', ip_version=6,
-                                  project_name='bar-project',
-                                  start='10.0.0.2', end='10.0.0.101',
-                                  gateway_ip='10.0.0.1', enable_dhcp=False,
-                                  dns_nameservers=['8.8.8.8'],
-                                  host_routes=[host_routes],
-                                  destination='dest',
-                                  nexthop='hop',
-                                  ipv6_ra_mode='dhcpv6-stateful',
-                                  ipv6_address_mode='slaac')
+        settings = SubnetSettings(
+            name='foo', cidr='10.0.0.0/24', ip_version=6,
+            project_name='bar-project', start='10.0.0.2', end='10.0.0.101',
+            gateway_ip='10.0.0.1', enable_dhcp=False,
+            dns_nameservers=['8.8.8.8'], host_routes=[host_routes],
+            destination='dest', nexthop='hop',
+            ipv6_ra_mode=IPv6Mode_old.stateful,
+            ipv6_address_mode=IPv6Mode.slaac)
         self.assertEqual('foo', settings.name)
         self.assertEqual('10.0.0.0/24', settings.cidr)
         self.assertEqual(6, settings.ip_version)
@@ -187,8 +214,9 @@ class SubnetSettingsUnitTests(unittest.TestCase):
         self.assertEqual(host_routes, settings.host_routes[0])
         self.assertEqual('dest', settings.destination)
         self.assertEqual('hop', settings.nexthop)
-        self.assertEqual('dhcpv6-stateful', settings.ipv6_ra_mode)
-        self.assertEqual('slaac', settings.ipv6_address_mode)
+        self.assertEqual(IPv6Mode.stateful.value, settings.ipv6_ra_mode.value)
+        self.assertEqual(IPv6Mode.slaac.value,
+                         settings.ipv6_address_mode.value)
 
     def test_config_all(self):
         host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'}
@@ -199,7 +227,7 @@ class SubnetSettingsUnitTests(unittest.TestCase):
                'gateway_ip': '10.0.0.1', 'enable_dhcp': False,
                'dns_nameservers': ['8.8.8.8'], 'host_routes': [host_routes],
                'destination': 'dest', 'nexthop': 'hop',
-               'ipv6_ra_mode': 'dhcpv6-stateful',
+               'ipv6_ra_mode': 'dhcpv6-stateless',
                'ipv6_address_mode': 'slaac'})
         self.assertEqual('foo', settings.name)
         self.assertEqual('10.0.0.0/24', settings.cidr)
@@ -215,8 +243,9 @@ class SubnetSettingsUnitTests(unittest.TestCase):
         self.assertEqual(host_routes, settings.host_routes[0])
         self.assertEqual('dest', settings.destination)
         self.assertEqual('hop', settings.nexthop)
-        self.assertEqual('dhcpv6-stateful', settings.ipv6_ra_mode)
-        self.assertEqual('slaac', settings.ipv6_address_mode)
+        self.assertEqual(IPv6Mode.stateless.value, settings.ipv6_ra_mode.value)
+        self.assertEqual(IPv6Mode.slaac.value,
+                         settings.ipv6_address_mode.value)
 
 
 class PortSettingsUnitTests(unittest.TestCase):
@@ -225,19 +254,19 @@ class PortSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(PortSettingsError):
+        with self.assertRaises(PortConfigError):
             PortSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(PortSettingsError):
+        with self.assertRaises(PortConfigError):
             PortSettings(**dict())
 
     def test_name_only(self):
-        with self.assertRaises(PortSettingsError):
+        with self.assertRaises(PortConfigError):
             PortSettings(name='foo')
 
     def test_config_name_only(self):
-        with self.assertRaises(PortSettingsError):
+        with self.assertRaises(PortConfigError):
             PortSettings(**{'name': 'foo'})
 
     def test_name_netname_only(self):
@@ -248,7 +277,6 @@ class PortSettingsUnitTests(unittest.TestCase):
         self.assertIsNone(settings.project_name)
         self.assertIsNone(settings.mac_address)
         self.assertIsNone(settings.ip_addrs)
-        self.assertIsNone(settings.fixed_ips)
         self.assertIsNone(settings.security_groups)
         self.assertIsNone(settings.allowed_address_pairs)
         self.assertIsNone(settings.opt_value)
@@ -264,7 +292,6 @@ class PortSettingsUnitTests(unittest.TestCase):
         self.assertIsNone(settings.project_name)
         self.assertIsNone(settings.mac_address)
         self.assertIsNone(settings.ip_addrs)
-        self.assertIsNone(settings.fixed_ips)
         self.assertIsNone(settings.security_groups)
         self.assertIsNone(settings.allowed_address_pairs)
         self.assertIsNone(settings.opt_value)
@@ -274,14 +301,12 @@ class PortSettingsUnitTests(unittest.TestCase):
 
     def test_all(self):
         ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}]
-        fixed_ips = {'sub_id', '10.0.0.10'}
         allowed_address_pairs = {'10.0.0.101', '1234.5678'}
 
         settings = PortSettings(name='foo', network_name='bar',
                                 admin_state_up=False,
                                 project_name='foo-project',
                                 mac_address='1234', ip_addrs=ip_addrs,
-                                fixed_ips=fixed_ips,
                                 security_groups=['foo_grp_id'],
                                 allowed_address_pairs=allowed_address_pairs,
                                 opt_value='opt value', opt_name='opt name',
@@ -293,7 +318,6 @@ class PortSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo-project', settings.project_name)
         self.assertEqual('1234', settings.mac_address)
         self.assertEqual(ip_addrs, settings.ip_addrs)
-        self.assertEqual(fixed_ips, settings.fixed_ips)
         self.assertEqual(1, len(settings.security_groups))
         self.assertEqual('foo_grp_id', settings.security_groups[0])
         self.assertEqual(allowed_address_pairs, settings.allowed_address_pairs)
@@ -304,25 +328,21 @@ class PortSettingsUnitTests(unittest.TestCase):
 
     def test_config_all(self):
         ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}]
-        fixed_ips = {'sub_id', '10.0.0.10'}
         allowed_address_pairs = {'10.0.0.101', '1234.5678'}
 
         settings = PortSettings(
             **{'name': 'foo', 'network_name': 'bar', 'admin_state_up': False,
                'project_name': 'foo-project', 'mac_address': '1234',
-               'ip_addrs': ip_addrs,
-               'fixed_ips': fixed_ips, 'security_groups': ['foo_grp_id'],
+               'ip_addrs': ip_addrs, 'security_groups': ['foo_grp_id'],
                'allowed_address_pairs': allowed_address_pairs,
-               'opt_value': 'opt value',
-               'opt_name': 'opt name', 'device_owner': 'owner',
-               'device_id': 'device number'})
+               'opt_value': 'opt value', 'opt_name': 'opt name',
+               'device_owner': 'owner', 'device_id': 'device number'})
         self.assertEqual('foo', settings.name)
         self.assertEqual('bar', settings.network_name)
         self.assertFalse(settings.admin_state_up)
         self.assertEqual('foo-project', settings.project_name)
         self.assertEqual('1234', settings.mac_address)
         self.assertEqual(ip_addrs, settings.ip_addrs)
-        self.assertEqual(fixed_ips, settings.fixed_ips)
         self.assertEqual(1, len(settings.security_groups))
         self.assertEqual('foo_grp_id', settings.security_groups[0])
         self.assertEqual(allowed_address_pairs, settings.allowed_address_pairs)
@@ -334,7 +354,7 @@ class PortSettingsUnitTests(unittest.TestCase):
 
 class CreateNetworkSuccessTests(OSIntegrationTestCase):
     """
-    Test for the CreateNework class defined in create_nework.py
+    Test for the CreateNetwork class defined in create_nework.py
     """
 
     def setUp(self):
@@ -346,14 +366,14 @@ class CreateNetworkSuccessTests(OSIntegrationTestCase):
         guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
         self.net_config = openstack_tests.get_pub_net_config(
             net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet',
-            router_name=guid + '-pub-router', external_net=self.ext_net_name)
+            router_name=guid + '-pub-router', external_net=self.ext_net_name,
+            netconf_override=self.netconf_override)
 
         self.neutron = neutron_utils.neutron_client(self.os_creds)
 
         # Initialize for cleanup
         self.net_creator = None
         self.router_creator = None
-        self.neutron = neutron_utils.neutron_client(self.os_creds)
 
     def tearDown(self):
         """
@@ -371,7 +391,7 @@ class CreateNetworkSuccessTests(OSIntegrationTestCase):
         """
         Tests the creation of an OpenStack network without a router.
         """
-        # Create Nework
+        # Create Network
         self.net_creator = OpenStackNetwork(self.os_creds,
                                             self.net_config.network_settings)
         self.net_creator.create()
@@ -390,7 +410,7 @@ class CreateNetworkSuccessTests(OSIntegrationTestCase):
         """
         Tests the creation of an OpenStack network, it's deletion, then cleanup
         """
-        # Create Nework
+        # Create Network
         self.net_creator = OpenStackNetwork(self.os_creds,
                                             self.net_config.network_settings)
         self.net_creator.create()
@@ -438,14 +458,14 @@ class CreateNetworkSuccessTests(OSIntegrationTestCase):
         neutron_utils_tests.validate_interface_router(
             self.router_creator.get_internal_router_interface(),
             self.router_creator.get_router(),
-            self.net_creator.get_subnets()[0])
+            self.net_creator.get_network().subnets[0])
 
     def test_create_networks_same_name(self):
         """
         Tests the creation of an OpenStack network and ensures that the
         OpenStackNetwork object will not create a second.
         """
-        # Create Nework
+        # Create Network
         self.net_creator = OpenStackNetwork(self.os_creds,
                                             self.net_config.network_settings)
         self.net_creator.create()
@@ -518,9 +538,108 @@ class CreateNetworkSuccessTests(OSIntegrationTestCase):
             self.router_creator.get_router().id, retrieved_router.id)
 
 
+class CreateNetworkIPv6Tests(OSIntegrationTestCase):
+    """
+    Test for the CreateNetwork class defined in create_nework.py when 
+    """
+
+    def setUp(self):
+        """
+        Sets up object for test
+        """
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.neutron = neutron_utils.neutron_client(self.os_creds)
+
+        # Initialize for cleanup
+        self.net_creator = None
+
+    def tearDown(self):
+        """
+        Cleans the network
+        """
+        if self.net_creator:
+            self.net_creator.clean()
+
+        super(self.__class__, self).__clean__()
+
+    def test_create_network_one_ipv6_subnet(self):
+        """
+        Tests the creation of an OpenStack network without a router.
+        """
+        # Create Network
+        subnet_settings = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6)
+        network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[subnet_settings])
+
+        self.net_creator = OpenStackNetwork(self.os_creds, network_settings)
+        self.net_creator.create()
+
+        # Validate network was created
+        self.assertTrue(neutron_utils_tests.validate_network(
+            self.neutron, self.net_creator.network_settings.name, True))
+
+        network = self.net_creator.get_network()
+        self.assertEqual(1, len(network.subnets))
+        subnet = network.subnets[0]
+
+        self.assertEqual(network.id, subnet.network_id)
+        self.assertEqual(subnet_settings.name, subnet.name)
+        self.assertEqual('1:1::/64', subnet.cidr)
+        self.assertEqual(6, subnet.ip_version)
+        self.assertEqual(0, len(subnet.dns_nameservers))
+
+    def test_create_network_ipv4_ipv6_subnet(self):
+        """
+        Tests the creation of an OpenStack network without a router.
+        """
+        # Create Network
+        subnet4_settings = SubnetConfig(
+            name=self.guid + '-subnet4', cidr='10.0.1.0/24', ip_version=4)
+        subnet6_settings = SubnetConfig(
+            name=self.guid + '-subnet6', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6)
+
+        network_settings = NetworkConfig(
+            name=self.guid + '-net',
+            subnet_settings=[subnet4_settings, subnet6_settings])
+
+        self.net_creator = OpenStackNetwork(self.os_creds, network_settings)
+        self.net_creator.create()
+
+        # Validate network was created
+        network = self.net_creator.get_network()
+        self.assertEqual(2, len(network.subnets))
+
+        subnet4 = None
+        subnet6 = None
+        for subnet in network.subnets:
+            if subnet.name == subnet4_settings.name:
+                subnet4 = subnet
+            if subnet.name == subnet6_settings.name:
+                subnet6 = subnet
+
+        # Validate IPv4 subnet
+        self.assertEqual(network.id, subnet4.network_id)
+        self.assertEqual(subnet4_settings.name, subnet4.name)
+        self.assertEqual(subnet4_settings.cidr, subnet4.cidr)
+        self.assertEqual(4, subnet4.ip_version)
+        self.assertEqual(1, len(subnet4.dns_nameservers))
+
+        # Validate IPv6 subnet
+        self.assertEqual(network.id, subnet6.network_id)
+        self.assertEqual(subnet6_settings.name, subnet6.name)
+        self.assertEqual('1:1::/64', subnet6.cidr)
+        self.assertEqual(6, subnet6.ip_version)
+        self.assertEqual(0, len(subnet6.dns_nameservers))
+
+
 class CreateNetworkTypeTests(OSComponentTestCase):
     """
-    Test for the CreateNework class defined in create_nework.py for testing
+    Test for the CreateNetwork class defined in create_nework.py for testing
     creating networks of different types
     """
 
@@ -536,7 +655,6 @@ class CreateNetworkTypeTests(OSComponentTestCase):
 
         # Initialize for cleanup
         self.net_creator = None
-        self.neutron = neutron_utils.neutron_client(self.os_creds)
 
     def tearDown(self):
         """
@@ -551,7 +669,7 @@ class CreateNetworkTypeTests(OSComponentTestCase):
         """
         # Create Network
         network_type = 'vlan'
-        net_settings = NetworkSettings(
+        net_settings = NetworkConfig(
             name=self.net_config.network_settings.name,
             subnet_settings=self.net_config.network_settings.subnet_settings,
             network_type=network_type)
@@ -578,7 +696,7 @@ class CreateNetworkTypeTests(OSComponentTestCase):
         physical_network = 'datacentre'
         segmentation_id = 2366
 
-        net_settings = NetworkSettings(
+        net_settings = NetworkConfig(
             name=self.net_config.network_settings.name,
             subnet_settings=self.net_config.network_settings.subnet_settings,
             network_type=network_type,
@@ -601,7 +719,7 @@ class CreateNetworkTypeTests(OSComponentTestCase):
         """
         # Create Network
         network_type = 'vxlan'
-        net_settings = NetworkSettings(
+        net_settings = NetworkConfig(
             name=self.net_config.network_settings.name,
             subnet_settings=self.net_config.network_settings.subnet_settings,
             network_type=network_type)
@@ -626,7 +744,7 @@ class CreateNetworkTypeTests(OSComponentTestCase):
         # This value must be variable to work on all OpenStack pods
         physical_network = 'datacentre'
 
-        net_settings = NetworkSettings(
+        net_settings = NetworkConfig(
             name=self.net_config.network_settings.name,
             subnet_settings=self.net_config.network_settings.subnet_settings,
             network_type=network_type, physical_network=physical_network)
@@ -646,7 +764,7 @@ class CreateNetworkTypeTests(OSComponentTestCase):
         """
         # Create Network
         network_type = 'foo'
-        net_settings = NetworkSettings(
+        net_settings = NetworkConfig(
             name=self.net_config.network_settings.name,
             subnet_settings=self.net_config.network_settings.subnet_settings,
             network_type=network_type)
index aa9dcfb..2c10311 100644 (file)
@@ -17,13 +17,14 @@ import uuid
 
 from keystoneclient.exceptions import BadRequest
 
+from snaps.config.security_group import SecurityGroupConfig
+from snaps.config.user import UserConfig
+from snaps.config.project import ProjectConfigError, ProjectConfig
 from snaps.domain.project import ComputeQuotas, NetworkQuotas
 from snaps.openstack.create_project import (
-    OpenStackProject, ProjectSettings, ProjectSettingsError)
+    OpenStackProject, ProjectSettings)
 from snaps.openstack.create_security_group import OpenStackSecurityGroup
-from snaps.openstack.create_security_group import SecurityGroupSettings
 from snaps.openstack.create_user import OpenStackUser
-from snaps.openstack.create_user import UserSettings
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
 from snaps.openstack.utils import keystone_utils, nova_utils, neutron_utils
 
@@ -36,11 +37,11 @@ class ProjectSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(ProjectSettingsError):
+        with self.assertRaises(ProjectConfigError):
             ProjectSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(ProjectSettingsError):
+        with self.assertRaises(ProjectConfigError):
             ProjectSettings(**dict())
 
     def test_name_only(self):
@@ -94,7 +95,7 @@ class CreateProjectSuccessTests(OSComponentTestCase):
         """
         guid = str(uuid.uuid4())[:-19]
         guid = self.__class__.__name__ + '-' + guid
-        self.project_settings = ProjectSettings(
+        self.project_settings = ProjectConfig(
             name=guid + '-name',
             domain=self.os_creds.project_domain_name)
 
@@ -236,7 +237,7 @@ class CreateProjectUserTests(OSComponentTestCase):
         """
         guid = str(uuid.uuid4())[:-19]
         self.guid = self.__class__.__name__ + '-' + guid
-        self.project_settings = ProjectSettings(
+        self.project_settings = ProjectConfig(
             name=self.guid + '-name',
             domain=self.os_creds.project_domain_name)
 
@@ -272,9 +273,10 @@ class CreateProjectUserTests(OSComponentTestCase):
         self.assertIsNotNone(created_project)
 
         user_creator = OpenStackUser(
-            self.os_creds, UserSettings(
+            self.os_creds, UserConfig(
                 name=self.guid + '-user',
-                password=self.guid, roles={'admin':  self.project_settings.name},
+                password=self.guid,
+                roles={'admin':  self.project_settings.name},
                 domain_name=self.os_creds.user_domain_name))
         self.project_creator.assoc_user(user_creator.create())
         self.user_creators.append(user_creator)
@@ -282,8 +284,8 @@ class CreateProjectUserTests(OSComponentTestCase):
         sec_grp_os_creds = user_creator.get_os_creds(
             self.project_creator.get_project().name)
         sec_grp_creator = OpenStackSecurityGroup(
-            sec_grp_os_creds, SecurityGroupSettings(name=self.guid + '-name',
-                                                    description='hello group'))
+            sec_grp_os_creds, SecurityGroupConfig(
+                name=self.guid + '-name', description='hello group'))
         sec_grp = sec_grp_creator.create()
         self.assertIsNotNone(sec_grp)
         self.sec_grp_creators.append(sec_grp_creator)
@@ -302,7 +304,7 @@ class CreateProjectUserTests(OSComponentTestCase):
         self.assertIsNotNone(created_project)
 
         user_creator_1 = OpenStackUser(
-            self.os_creds, UserSettings(
+            self.os_creds, UserConfig(
                 name=self.guid + '-user1', password=self.guid,
                 roles={'admin': self.project_settings.name},
                 domain_name=self.os_creds.user_domain_name))
@@ -310,7 +312,7 @@ class CreateProjectUserTests(OSComponentTestCase):
         self.user_creators.append(user_creator_1)
 
         user_creator_2 = OpenStackUser(
-            self.os_creds, UserSettings(
+            self.os_creds, UserConfig(
                 name=self.guid + '-user2', password=self.guid,
                 roles={'admin': self.project_settings.name},
                 domain_name=self.os_creds.user_domain_name))
@@ -325,8 +327,8 @@ class CreateProjectUserTests(OSComponentTestCase):
 
             sec_grp_creator = OpenStackSecurityGroup(
                 sec_grp_os_creds,
-                SecurityGroupSettings(name=self.guid + '-name',
-                                      description='hello group'))
+                SecurityGroupConfig(
+                    name=self.guid + '-name', description='hello group'))
             sec_grp = sec_grp_creator.create()
             self.assertIsNotNone(sec_grp)
             self.sec_grp_creators.append(sec_grp_creator)
index 6c0a056..68737f8 100644 (file)
@@ -12,6 +12,8 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+from snaps.config.qos import Consumer, QoSConfigError, QoSConfig
+from snaps.openstack.create_qos import Consumer as Consumer_old
 
 try:
     from urllib.request import URLError
@@ -23,8 +25,7 @@ import unittest
 import uuid
 
 from snaps.openstack import create_qos
-from snaps.openstack.create_qos import (QoSSettings, QoSSettingsError,
-                                        Consumer)
+from snaps.openstack.create_qos import QoSSettings
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
 from snaps.openstack.utils import cinder_utils
 
@@ -39,64 +40,64 @@ class QoSSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(QoSSettingsError):
+        with self.assertRaises(QoSConfigError):
             QoSSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(QoSSettingsError):
+        with self.assertRaises(QoSConfigError):
             QoSSettings(**dict())
 
     def test_name_only(self):
-        with self.assertRaises(QoSSettingsError):
+        with self.assertRaises(QoSConfigError):
             QoSSettings(name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(QoSSettingsError):
+        with self.assertRaises(QoSConfigError):
             QoSSettings(**{'name': 'foo'})
 
     def test_invalid_consumer(self):
-        with self.assertRaises(QoSSettingsError):
+        with self.assertRaises(QoSConfigError):
             QoSSettings(name='foo', consumer='bar')
 
     def test_config_with_invalid_consumer(self):
-        with self.assertRaises(QoSSettingsError):
+        with self.assertRaises(QoSConfigError):
             QoSSettings(**{'name': 'foo', 'consumer': 'bar'})
 
     def test_name_consumer(self):
-        settings = QoSSettings(name='foo', consumer=Consumer.front_end)
+        settings = QoSSettings(name='foo', consumer=Consumer_old.front_end)
 
         self.assertEqual('foo', settings.name)
-        self.assertEqual(Consumer.front_end, settings.consumer)
+        self.assertEqual(Consumer_old.front_end.value, settings.consumer.value)
         self.assertEqual(dict(), settings.specs)
 
     def test_name_consumer_front_end_strings(self):
         settings = QoSSettings(name='foo', consumer='front-end')
 
         self.assertEqual('foo', settings.name)
-        self.assertEqual(Consumer.front_end, settings.consumer)
+        self.assertEqual(Consumer_old.front_end.value, settings.consumer.value)
         self.assertEqual(dict(), settings.specs)
 
     def test_name_consumer_back_end_strings(self):
         settings = QoSSettings(name='foo', consumer='back-end')
 
         self.assertEqual('foo', settings.name)
-        self.assertEqual(Consumer.back_end, settings.consumer)
+        self.assertEqual(Consumer_old.back_end.value, settings.consumer.value)
         self.assertEqual(dict(), settings.specs)
 
     def test_name_consumer_both_strings(self):
         settings = QoSSettings(name='foo', consumer='both')
 
         self.assertEqual('foo', settings.name)
-        self.assertEqual(Consumer.both, settings.consumer)
+        self.assertEqual(Consumer_old.both.value, settings.consumer.value)
         self.assertEqual(dict(), settings.specs)
 
     def test_all(self):
         specs = {'spec1': 'val1', 'spec2': 'val2'}
-        settings = QoSSettings(name='foo', consumer=Consumer.both,
+        settings = QoSSettings(name='foo', consumer=Consumer_old.both,
                                specs=specs)
 
         self.assertEqual('foo', settings.name)
-        self.assertEqual(Consumer.both, settings.consumer)
+        self.assertEqual(Consumer_old.both.value, settings.consumer.value)
         self.assertEqual(specs, settings.specs)
 
     def test_config_all(self):
@@ -121,7 +122,7 @@ class CreateQoSTests(OSIntegrationTestCase):
         super(self.__class__, self).__start__()
 
         guid = uuid.uuid4()
-        self.qos_settings = QoSSettings(
+        self.qos_settings = QoSConfig(
             name=self.__class__.__name__ + '-' + str(guid),
             consumer=Consumer.both)
 
index db3170e..09471a3 100644 (file)
 import unittest
 import uuid
 
+from snaps.config.network import PortConfig, NetworkConfig
+from snaps.config.router import RouterConfigError, RouterConfig
 from snaps.openstack import create_network
 from snaps.openstack import create_router
-from snaps.openstack.create_network import (
-    NetworkSettings, PortSettings)
 from snaps.openstack.create_network import OpenStackNetwork
-from snaps.openstack.create_router import (
-    RouterSettings, RouterSettingsError)
+from snaps.openstack.create_router import RouterSettings
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
-from snaps.openstack.utils import neutron_utils
+from snaps.openstack.utils import neutron_utils, settings_utils
 
 __author__ = 'mmakati'
 
@@ -39,11 +38,11 @@ class RouterSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(RouterSettingsError):
+        with self.assertRaises(RouterConfigError):
             RouterSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(RouterSettingsError):
+        with self.assertRaises(RouterConfigError):
             RouterSettings(**dict())
 
     def test_name_only(self):
@@ -51,9 +50,8 @@ class RouterSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo', settings.name)
         self.assertIsNone(settings.project_name)
         self.assertIsNone(settings.external_gateway)
-        self.assertIsNone(settings.admin_state_up)
+        self.assertTrue(settings.admin_state_up)
         self.assertIsNone(settings.enable_snat)
-        self.assertIsNone(settings.external_fixed_ips)
         self.assertIsNotNone(settings.internal_subnets)
         self.assertTrue(isinstance(settings.internal_subnets, list))
         self.assertEqual(0, len(settings.internal_subnets))
@@ -66,9 +64,8 @@ class RouterSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo', settings.name)
         self.assertIsNone(settings.project_name)
         self.assertIsNone(settings.external_gateway)
-        self.assertIsNone(settings.admin_state_up)
+        self.assertTrue(settings.admin_state_up)
         self.assertIsNone(settings.enable_snat)
-        self.assertIsNone(settings.external_fixed_ips)
         self.assertIsNotNone(settings.internal_subnets)
         self.assertTrue(isinstance(settings.internal_subnets, list))
         self.assertEqual(0, len(settings.internal_subnets))
@@ -77,17 +74,16 @@ class RouterSettingsUnitTests(unittest.TestCase):
         self.assertEqual(0, len(settings.port_settings))
 
     def test_all(self):
-        port_settings = PortSettings(name='foo', network_name='bar')
+        port_settings = PortConfig(name='foo', network_name='bar')
         settings = RouterSettings(
             name='foo', project_name='bar', external_gateway='foo_gateway',
-            admin_state_up=True, enable_snat=False, external_fixed_ips=['ip1'],
+            admin_state_up=True, enable_snat=False,
             internal_subnets=['10.0.0.1/24'], interfaces=[port_settings])
         self.assertEqual('foo', settings.name)
         self.assertEqual('bar', settings.project_name)
         self.assertEqual('foo_gateway', settings.external_gateway)
         self.assertTrue(settings.admin_state_up)
         self.assertFalse(settings.enable_snat)
-        self.assertEqual(['ip1'], settings.external_fixed_ips)
         self.assertIsNotNone(settings.internal_subnets)
         self.assertTrue(isinstance(settings.internal_subnets, list))
         self.assertEqual(1, len(settings.internal_subnets))
@@ -98,8 +94,7 @@ class RouterSettingsUnitTests(unittest.TestCase):
         settings = RouterSettings(
             **{'name': 'foo', 'project_name': 'bar',
                'external_gateway': 'foo_gateway', 'admin_state_up': True,
-               'enable_snat': False, 'external_fixed_ips': ['ip1'],
-               'internal_subnets': ['10.0.0.1/24'],
+               'enable_snat': False, 'internal_subnets': ['10.0.0.1/24'],
                'interfaces':
                    [{'port': {'name': 'foo-port',
                               'network_name': 'bar-net'}}]})
@@ -108,14 +103,13 @@ class RouterSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo_gateway', settings.external_gateway)
         self.assertTrue(settings.admin_state_up)
         self.assertFalse(settings.enable_snat)
-        self.assertEqual(['ip1'], settings.external_fixed_ips)
         self.assertIsNotNone(settings.internal_subnets)
         self.assertTrue(isinstance(settings.internal_subnets, list))
         self.assertEqual(1, len(settings.internal_subnets))
         self.assertEqual(['10.0.0.1/24'], settings.internal_subnets)
-        self.assertEqual([PortSettings(**{'name': 'foo-port',
-                                          'network_name': 'bar-net'})],
-                         settings.port_settings)
+        self.assertEqual(
+            [PortConfig(**{'name': 'foo-port', 'network_name': 'bar-net'})],
+            settings.port_settings)
 
 
 class CreateRouterSuccessTests(OSIntegrationTestCase):
@@ -155,8 +149,8 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
         """
         Test creation of a most basic router with minimal options.
         """
-        router_settings = RouterSettings(name=self.guid + '-pub-router',
-                                         external_gateway=self.ext_net_name)
+        router_settings = RouterConfig(
+            name=self.guid + '-pub-router', external_gateway=self.ext_net_name)
 
         self.router_creator = create_router.OpenStackRouter(self.os_creds,
                                                             router_settings)
@@ -166,15 +160,16 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
                                           router_settings=router_settings)
         self.assertIsNotNone(router)
 
-        self.assertTrue(verify_router_attributes(
-            router, self.router_creator, ext_gateway=self.ext_net_name))
+        self.assertEqual(self.router_creator.get_router(), router)
+
+        self.check_router_recreation(router, router_settings)
 
     def test_create_router_admin_user_to_new_project(self):
         """
         Test creation of a most basic router with the admin user pointing
         to the new project.
         """
-        router_settings = RouterSettings(
+        router_settings = RouterConfig(
             name=self.guid + '-pub-router', external_gateway=self.ext_net_name,
             project_name=self.os_creds.project_name)
 
@@ -186,15 +181,16 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
                                           router_settings=router_settings)
         self.assertIsNotNone(router)
 
-        self.assertTrue(verify_router_attributes(
-            router, self.router_creator, ext_gateway=self.ext_net_name))
+        self.assertEqual(self.router_creator.get_router(), router)
+
+        self.check_router_recreation(router, router_settings)
 
     def test_create_router_new_user_to_admin_project(self):
         """
         Test creation of a most basic router with the new user pointing
         to the admin project.
         """
-        router_settings = RouterSettings(
+        router_settings = RouterConfig(
             name=self.guid + '-pub-router', external_gateway=self.ext_net_name,
             project_name=self.admin_os_creds.project_name)
 
@@ -206,15 +202,16 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
                                           router_settings=router_settings)
         self.assertIsNotNone(router)
 
-        self.assertTrue(verify_router_attributes(
-            router, self.router_creator, ext_gateway=self.ext_net_name))
+        self.assertEqual(self.router_creator.get_router(), router)
+
+        self.check_router_recreation(router, router_settings)
 
     def test_create_delete_router(self):
         """
         Test that clean() will not raise an exception if the router is deleted
         by another process.
         """
-        self.router_settings = RouterSettings(
+        self.router_settings = RouterConfig(
             name=self.guid + '-pub-router', external_gateway=self.ext_net_name)
 
         self.router_creator = create_router.OpenStackRouter(
@@ -238,8 +235,8 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
         """
         Test creation of a basic router with admin state down.
         """
-        router_settings = RouterSettings(name=self.guid + '-pub-router',
-                                         admin_state_up=False)
+        router_settings = RouterConfig(
+            name=self.guid + '-pub-router', admin_state_up=False)
 
         self.router_creator = create_router.OpenStackRouter(self.os_creds,
                                                             router_settings)
@@ -249,42 +246,44 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
                                           router_settings=router_settings)
         self.assertIsNotNone(router)
 
-        self.assertTrue(verify_router_attributes(router, self.router_creator,
-                                                 admin_state=False))
+        self.assertEqual(self.router_creator.get_router(), router)
+
+        self.check_router_recreation(router, router_settings)
 
     def test_create_router_admin_state_True(self):
         """
         Test creation of a basic router with admin state Up.
         """
-        router_settings = RouterSettings(name=self.guid + '-pub-router',
-                                         admin_state_up=True)
+        router_settings = RouterConfig(
+            name=self.guid + '-pub-router', admin_state_up=True)
 
-        self.router_creator = create_router.OpenStackRouter(self.os_creds,
-                                                            router_settings)
+        self.router_creator = create_router.OpenStackRouter(
+            self.os_creds, router_settings)
         self.router_creator.create()
 
-        router = neutron_utils.get_router(self.neutron,
-                                          router_settings=router_settings)
+        router = neutron_utils.get_router(
+            self.neutron, router_settings=router_settings)
         self.assertIsNotNone(router)
 
-        self.assertTrue(verify_router_attributes(router, self.router_creator,
-                                                 admin_state=True))
+        self.assertEqual(self.router_creator.get_router(), router)
+
+        self.check_router_recreation(router, router_settings)
 
     def test_create_router_private_network(self):
         """
         Test creation of a router connected with two private networks and no
         external gateway
         """
-        network_settings1 = NetworkSettings(
+        network_settings1 = NetworkConfig(
             name=self.guid + '-pub-net1',
             subnet_settings=[
-                create_network.SubnetSettings(
+                create_network.SubnetConfig(
                     cidr=cidr1, name=self.guid + '-pub-subnet1',
                     gateway_ip=static_gateway_ip1)])
-        network_settings2 = NetworkSettings(
+        network_settings2 = NetworkConfig(
             name=self.guid + '-pub-net2',
             subnet_settings=[
-                create_network.SubnetSettings(
+                create_network.SubnetConfig(
                     cidr=cidr2, name=self.guid + '-pub-subnet2',
                     gateway_ip=static_gateway_ip2)])
 
@@ -297,7 +296,7 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
         self.network_creator2.create()
 
         port_settings = [
-            create_network.PortSettings(
+            create_network.PortConfig(
                 name=self.guid + '-port1',
                 ip_addrs=[{
                     'subnet_name':
@@ -306,7 +305,7 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
                 }],
                 network_name=network_settings1.name,
                 project_name=self.os_creds.project_name),
-            create_network.PortSettings(
+            create_network.PortConfig(
                 name=self.guid + '-port2',
                 ip_addrs=[{
                     'subnet_name': network_settings2.subnet_settings[0].name,
@@ -315,16 +314,16 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
                 network_name=network_settings2.name,
                 project_name=self.os_creds.project_name)]
 
-        router_settings = RouterSettings(name=self.guid + '-pub-router',
-                                         port_settings=port_settings)
+        router_settings = RouterConfig(
+            name=self.guid + '-pub-router', port_settings=port_settings)
         self.router_creator = create_router.OpenStackRouter(self.os_creds,
                                                             router_settings)
         self.router_creator.create()
 
-        router = neutron_utils.get_router(self.neutron,
-                                          router_settings=router_settings)
+        router = neutron_utils.get_router(
+            self.neutron, router_settings=router_settings)
 
-        self.assertTrue(verify_router_attributes(router, self.router_creator))
+        self.assertEqual(router, self.router_creator.get_router())
 
         # Instantiate second identical creator to ensure a second router
         # has not been created
@@ -333,15 +332,17 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
         router2 = router_creator2.create()
         self.assertIsNotNone(self.router_creator.get_router(), router2)
 
+        self.check_router_recreation(router2, router_settings)
+
     def test_create_router_external_network(self):
         """
         Test creation of a router connected to an external network and a
         private network.
         """
-        network_settings = NetworkSettings(
+        network_settings = NetworkConfig(
             name=self.guid + '-pub-net1',
             subnet_settings=[
-                create_network.SubnetSettings(
+                create_network.SubnetConfig(
                     cidr=cidr1, name=self.guid + '-pub-subnet1',
                     gateway_ip=static_gateway_ip1)])
         self.network_creator1 = OpenStackNetwork(self.os_creds,
@@ -349,7 +350,7 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
         self.network_creator1.create()
 
         port_settings = [
-            create_network.PortSettings(
+            create_network.PortConfig(
                 name=self.guid + '-port1',
                 ip_addrs=[{
                     'subnet_name': network_settings.subnet_settings[0].name,
@@ -357,18 +358,55 @@ class CreateRouterSuccessTests(OSIntegrationTestCase):
                 network_name=network_settings.name,
                 project_name=self.os_creds.project_name)]
 
-        router_settings = RouterSettings(
+        router_settings = RouterConfig(
             name=self.guid + '-pub-router', external_gateway=self.ext_net_name,
             port_settings=port_settings)
-        self.router_creator = create_router.OpenStackRouter(self.os_creds,
-                                                            router_settings)
+        self.router_creator = create_router.OpenStackRouter(
+            self.os_creds, router_settings)
         self.router_creator.create()
 
-        router = neutron_utils.get_router(self.neutron,
-                                          router_settings=router_settings)
+        router = neutron_utils.get_router(
+            self.neutron, router_settings=router_settings)
+
+        self.assertEquals(router, self.router_creator.get_router())
 
-        self.assertTrue(verify_router_attributes(
-            router, self.router_creator, ext_gateway=self.ext_net_name))
+        self.check_router_recreation(router, router_settings)
+
+    def check_router_recreation(self, router, orig_settings):
+        """
+        Validates the derived RouterConfig with the original
+        :param router: the Router domain object to test
+        :param orig_settings: the original RouterConfig object that was
+                              responsible for creating the router
+        :return: the derived RouterConfig object
+        """
+        derived_settings = settings_utils.create_router_config(
+            self.neutron, router)
+        self.assertIsNotNone(derived_settings)
+        self.assertEqual(
+            orig_settings.enable_snat, derived_settings.enable_snat)
+        self.assertEqual(orig_settings.external_gateway,
+                         derived_settings.external_gateway)
+        self.assertEqual(orig_settings.name, derived_settings.name)
+        self.assertEqual(orig_settings.internal_subnets,
+                         derived_settings.internal_subnets)
+
+        if orig_settings.external_gateway:
+            self.assertEqual(len(orig_settings.port_settings),
+                             len(derived_settings.port_settings))
+        else:
+            self.assertEqual(len(orig_settings.port_settings),
+                             len(derived_settings.port_settings))
+
+        if len(orig_settings.port_settings) > 0:
+            self.assertEqual(orig_settings.port_settings[0].name,
+                             derived_settings.port_settings[0].name)
+
+        if len(orig_settings.port_settings) > 1:
+            self.assertEqual(orig_settings.port_settings[1].name,
+                             derived_settings.port_settings[1].name)
+
+        return derived_settings
 
 
 class CreateRouterNegativeTests(OSIntegrationTestCase):
@@ -398,8 +436,8 @@ class CreateRouterNegativeTests(OSIntegrationTestCase):
         """
         Test creating a router without a name.
         """
-        with self.assertRaises(RouterSettingsError):
-            router_settings = RouterSettings(
+        with self.assertRaises(RouterConfigError):
+            router_settings = RouterConfig(
                 name=None, external_gateway=self.ext_net_name)
             self.router_creator = create_router.OpenStackRouter(
                 self.os_creds, router_settings)
@@ -409,48 +447,10 @@ class CreateRouterNegativeTests(OSIntegrationTestCase):
         """
         Test creating a router without a valid network gateway name.
         """
-        with self.assertRaises(RouterSettingsError):
-            router_settings = RouterSettings(name=self.guid + '-pub-router',
-                                             external_gateway="Invalid_name")
+        with self.assertRaises(RouterConfigError):
+            router_settings = RouterConfig(
+                name=self.guid + '-pub-router',
+                external_gateway="Invalid_name")
             self.router_creator = create_router.OpenStackRouter(
                 self.os_creds, router_settings)
             self.router_creator.create()
-
-
-def verify_router_attributes(router_operational, router_creator,
-                             admin_state=True, ext_gateway=None):
-    """
-    Helper function to validate the attributes of router created with the one
-    operational
-    :param router_operational: Operational Router object returned from neutron
-                               utils of type snaps.domain.Router
-    :param router_creator: router_creator object returned from creating a
-                           router in the router test functions
-    :param admin_state: True if router is expected to be Up, else False
-    :param ext_gateway: None if router is not connected to external gateway
-    :return:
-    """
-
-    router = router_creator.get_router()
-
-    if not router_operational:
-        return False
-    elif not router_creator:
-        return False
-    elif not (router_operational.name == router_creator.router_settings.name):
-        return False
-    elif not (router_operational.id == router.id):
-        return False
-    elif not (router_operational.status == router.status):
-        return False
-    elif not (router_operational.tenant_id == router.tenant_id):
-        return False
-    elif not (admin_state == router_operational.admin_state_up):
-        return False
-    elif (ext_gateway is None) and \
-            (router_operational.external_gateway_info is not None):
-        return False
-    elif ext_gateway is not None:
-        if router_operational.external_gateway_info is None:
-            return False
-    return True
index 99ea53a..090d736 100644 (file)
 import unittest
 import uuid
 
+from snaps.config.security_group import (
+    SecurityGroupConfig,  SecurityGroupRuleConfig,
+    SecurityGroupRuleConfigError, SecurityGroupConfigError)
 from snaps.openstack import create_security_group
 from snaps.openstack.create_security_group import (
     SecurityGroupSettings, SecurityGroupRuleSettings, Direction, Ethertype,
-    Protocol, SecurityGroupRuleSettingsError, SecurityGroupSettingsError)
+    Protocol)
 from snaps.openstack.tests import validation_utils
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
 from snaps.openstack.utils import neutron_utils
@@ -32,32 +35,64 @@ class SecurityGroupRuleSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(SecurityGroupRuleSettingsError):
+        with self.assertRaises(SecurityGroupRuleConfigError):
             SecurityGroupRuleSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(SecurityGroupRuleSettingsError):
+        with self.assertRaises(SecurityGroupRuleConfigError):
             SecurityGroupRuleSettings(**dict())
 
     def test_name_only(self):
-        with self.assertRaises(SecurityGroupRuleSettingsError):
+        with self.assertRaises(SecurityGroupRuleConfigError):
             SecurityGroupRuleSettings(sec_grp_name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(SecurityGroupRuleSettingsError):
+        with self.assertRaises(SecurityGroupRuleConfigError):
             SecurityGroupRuleSettings(**{'sec_grp_name': 'foo'})
 
     def test_name_and_direction(self):
         settings = SecurityGroupRuleSettings(sec_grp_name='foo',
                                              direction=Direction.ingress)
         self.assertEqual('foo', settings.sec_grp_name)
-        self.assertEqual(Direction.ingress, settings.direction)
+        self.assertEqual(Direction.ingress.value, settings.direction.value)
 
     def test_config_name_and_direction(self):
         settings = SecurityGroupRuleSettings(
             **{'sec_grp_name': 'foo', 'direction': 'ingress'})
         self.assertEqual('foo', settings.sec_grp_name)
-        self.assertEqual(Direction.ingress, settings.direction)
+        self.assertEqual(Direction.ingress.value, settings.direction.value)
+
+    def test_proto_ah_str(self):
+        settings = SecurityGroupRuleSettings(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 'ah'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress.value, settings.direction.value)
+        self.assertEqual(Protocol.ah.value, settings.protocol.value)
+
+    def test_proto_ah_value(self):
+        settings = SecurityGroupRuleSettings(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 51})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress.value, settings.direction.value)
+        self.assertEqual(Protocol.ah.value, settings.protocol.value)
+
+    def test_proto_any(self):
+        settings = SecurityGroupRuleSettings(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 'any'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress.value, settings.direction.value)
+        self.assertEqual(Protocol.null.value, settings.protocol.value)
+
+    def test_proto_null(self):
+        settings = SecurityGroupRuleSettings(
+            **{'sec_grp_name': 'foo', 'direction': 'ingress',
+               'protocol': 'null'})
+        self.assertEqual('foo', settings.sec_grp_name)
+        self.assertEqual(Direction.ingress.value, settings.direction.value)
+        self.assertEqual(Protocol.null.value, settings.protocol.value)
 
     def test_all(self):
         settings = SecurityGroupRuleSettings(
@@ -68,10 +103,10 @@ class SecurityGroupRuleSettingsUnitTests(unittest.TestCase):
             remote_ip_prefix='prfx')
         self.assertEqual('foo', settings.sec_grp_name)
         self.assertEqual('fubar', settings.description)
-        self.assertEqual(Direction.egress, settings.direction)
+        self.assertEqual(Direction.egress.value, settings.direction.value)
         self.assertEqual('rgi', settings.remote_group_id)
-        self.assertEqual(Protocol.icmp, settings.protocol)
-        self.assertEqual(Ethertype.IPv6, settings.ethertype)
+        self.assertEqual(Protocol.icmp.value, settings.protocol.value)
+        self.assertEqual(Ethertype.IPv6.value, settings.ethertype.value)
         self.assertEqual(1, settings.port_range_min)
         self.assertEqual(2, settings.port_range_max)
         self.assertEqual('prfx', settings.remote_ip_prefix)
@@ -89,10 +124,10 @@ class SecurityGroupRuleSettingsUnitTests(unittest.TestCase):
                'remote_ip_prefix': 'prfx'})
         self.assertEqual('foo', settings.sec_grp_name)
         self.assertEqual('fubar', settings.description)
-        self.assertEqual(Direction.egress, settings.direction)
+        self.assertEqual(Direction.egress.value, settings.direction.value)
         self.assertEqual('rgi', settings.remote_group_id)
-        self.assertEqual(Protocol.tcp, settings.protocol)
-        self.assertEqual(Ethertype.IPv6, settings.ethertype)
+        self.assertEqual(Protocol.tcp.value, settings.protocol.value)
+        self.assertEqual(Ethertype.IPv6.value, settings.ethertype.value)
         self.assertEqual(1, settings.port_range_min)
         self.assertEqual(2, settings.port_range_max)
         self.assertEqual('prfx', settings.remote_ip_prefix)
@@ -104,11 +139,11 @@ class SecurityGroupSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(SecurityGroupSettingsError):
+        with self.assertRaises(SecurityGroupConfigError):
             SecurityGroupSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(SecurityGroupSettingsError):
+        with self.assertRaises(SecurityGroupConfigError):
             SecurityGroupSettings(**dict())
 
     def test_name_only(self):
@@ -123,7 +158,7 @@ class SecurityGroupSettingsUnitTests(unittest.TestCase):
         rule_setting = SecurityGroupRuleSettings(
             sec_grp_name='bar', direction=Direction.ingress,
             description='test_rule_1')
-        with self.assertRaises(SecurityGroupSettingsError):
+        with self.assertRaises(SecurityGroupConfigError):
             SecurityGroupSettings(name='foo', rule_settings=[rule_setting])
 
     def test_all(self):
@@ -157,8 +192,8 @@ class SecurityGroupSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo', settings.project_name)
         self.assertEqual(1, len(settings.rule_settings))
         self.assertEqual('bar', settings.rule_settings[0].sec_grp_name)
-        self.assertEqual(Direction.ingress,
-                         settings.rule_settings[0].direction)
+        self.assertEqual(Direction.ingress.value,
+                         settings.rule_settings[0].direction.value)
 
 
 class CreateSecurityGroupTests(OSIntegrationTestCase):
@@ -194,8 +229,8 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         Tests the creation of an OpenStack Security Group without custom rules.
         """
         # Create Image
-        sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name,
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(name=self.sec_grp_name,
+                                               description='hello group')
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
             self.os_creds, sec_grp_settings)
         self.sec_grp_creator.create()
@@ -222,7 +257,7 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         Tests the creation of an OpenStack Security Group without custom rules.
         """
         # Create Image
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             project_name=self.admin_os_creds.project_name)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -251,7 +286,7 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         Tests the creation of an OpenStack Security Group without custom rules.
         """
         # Create Image
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             project_name=self.os_creds.project_name)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -280,8 +315,8 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         Tests the creation of an OpenStack Security Group without custom rules.
         """
         # Create Image
-        sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name,
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(name=self.sec_grp_name,
+                                               description='hello group')
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
             self.os_creds, sec_grp_settings)
         created_sec_grp = self.sec_grp_creator.create()
@@ -307,10 +342,10 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         # Create Image
         sec_grp_rule_settings = list()
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.ingress,
                 description='test_rule_1'))
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             rule_settings=sec_grp_rule_settings)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -340,12 +375,12 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         # Create Image
         sec_grp_rule_settings = list()
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.egress,
                 protocol=Protocol.udp, ethertype=Ethertype.IPv4,
                 port_range_min=10, port_range_max=20,
                 description='test_rule_1'))
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             rule_settings=sec_grp_rule_settings)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -375,21 +410,21 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         # Create Image
         sec_grp_rule_settings = list()
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.ingress,
                 description='test_rule_1'))
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.egress,
                 protocol=Protocol.udp, ethertype=Ethertype.IPv6,
                 description='test_rule_2'))
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.egress,
                 protocol=Protocol.udp, ethertype=Ethertype.IPv4,
                 port_range_min=10, port_range_max=20,
                 description='test_rule_3'))
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             rule_settings=sec_grp_rule_settings)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -419,10 +454,10 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         # Create Image
         sec_grp_rule_settings = list()
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.ingress,
                 description='test_rule_1'))
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             rule_settings=sec_grp_rule_settings)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -448,7 +483,7 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(),
                                             rules)
 
-        self.sec_grp_creator.add_rule(SecurityGroupRuleSettings(
+        self.sec_grp_creator.add_rule(SecurityGroupRuleConfig(
             sec_grp_name=self.sec_grp_creator.sec_grp_settings.name,
             direction=Direction.egress, protocol=Protocol.icmp,
             description='test_rule_2'))
@@ -464,21 +499,21 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         # Create Image
         sec_grp_rule_settings = list()
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.ingress,
                 description='test_rule_1'))
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.egress,
                 protocol=Protocol.udp, ethertype=Ethertype.IPv6,
                 description='test_rule_2'))
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.egress,
                 protocol=Protocol.udp, ethertype=Ethertype.IPv4,
                 port_range_min=10, port_range_max=20,
                 description='test_rule_3'))
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             rule_settings=sec_grp_rule_settings)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -515,21 +550,21 @@ class CreateSecurityGroupTests(OSIntegrationTestCase):
         # Create Image
         sec_grp_rule_settings = list()
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.ingress,
                 description='test_rule_1'))
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.egress,
                 protocol=Protocol.udp, ethertype=Ethertype.IPv6,
                 description='test_rule_2'))
         sec_grp_rule_settings.append(
-            SecurityGroupRuleSettings(
+            SecurityGroupRuleConfig(
                 sec_grp_name=self.sec_grp_name, direction=Direction.egress,
                 protocol=Protocol.udp, ethertype=Ethertype.IPv4,
                 port_range_min=10, port_range_max=20,
                 description='test_rule_3'))
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             rule_settings=sec_grp_rule_settings)
         self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(
@@ -583,7 +618,7 @@ def validate_sec_grp_rules(neutron, rule_settings, rules):
     this is the only means to tell if the rule is custom or defaulted by
     OpenStack
     :param neutron: the neutron client
-    :param rule_settings: collection of SecurityGroupRuleSettings objects
+    :param rule_settings: collection of SecurityGroupRuleConfig objects
     :param rules: a collection of SecurityGroupRule domain objects
     :return: T/F
     """
@@ -592,11 +627,6 @@ def validate_sec_grp_rules(neutron, rule_settings, rules):
         if rule_setting.description:
             match = False
             for rule in rules:
-                if rule_setting.protocol == Protocol.null:
-                    setting_proto = None
-                else:
-                    setting_proto = rule_setting.protocol.name
-
                 sec_grp = neutron_utils.get_security_group(
                     neutron, sec_grp_name=rule_setting.sec_grp_name)
 
@@ -607,15 +637,19 @@ def validate_sec_grp_rules(neutron, rule_settings, rules):
                 if not sec_grp:
                     return False
 
+                proto_str = 'null'
+                if rule.protocol:
+                    proto_str = rule.protocol
+
                 if (rule.description == rule_setting.description and
                     rule.direction == rule_setting.direction.name and
                     rule.ethertype == setting_eth_type.name and
                     rule.port_range_max == rule_setting.port_range_max and
                     rule.port_range_min == rule_setting.port_range_min and
-                    rule.protocol == setting_proto and
+                    proto_str == str(rule_setting.protocol.value) and
                     rule.remote_group_id == rule_setting.remote_group_id and
                     rule.remote_ip_prefix == rule_setting.remote_ip_prefix and
-                    rule.security_group_id == sec_grp.id):
+                        rule.security_group_id == sec_grp.id):
                     match = True
                     break
 
index f4eb1eb..6041735 100644 (file)
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import os
 import time
 
 import pkg_resources
 from heatclient.exc import HTTPBadRequest
+
+import snaps
 from snaps import file_utils
-from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings
+from snaps.config.flavor import FlavorConfig
+from snaps.config.image import ImageConfig
+from snaps.config.stack import StackConfigError, StackConfig
+from snaps.openstack.create_flavor import OpenStackFlavor
 from snaps.openstack.create_image import OpenStackImage
 
 try:
@@ -29,9 +35,8 @@ import logging
 import unittest
 import uuid
 
-from snaps.openstack import create_stack
 from snaps.openstack.create_stack import (
-    StackSettings, StackSettingsError, StackCreationError)
+    StackSettings, StackCreationError, StackError, OpenStackHeatStack)
 from snaps.openstack.tests import openstack_tests, create_instance_tests
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
 from snaps.openstack.utils import heat_utils, neutron_utils, nova_utils
@@ -47,19 +52,19 @@ class StackSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(StackSettingsError):
+        with self.assertRaises(StackConfigError):
             StackSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(StackSettingsError):
+        with self.assertRaises(StackConfigError):
             StackSettings(**dict())
 
     def test_name_only(self):
-        with self.assertRaises(StackSettingsError):
+        with self.assertRaises(StackConfigError):
             StackSettings(name='foo')
 
     def test_config_with_name_only(self):
-        with self.assertRaises(StackSettingsError):
+        with self.assertRaises(StackConfigError):
             StackSettings(**{'name': 'foo'})
 
     def test_config_minimum_template(self):
@@ -68,7 +73,7 @@ class StackSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo', settings.template)
         self.assertIsNone(settings.template_path)
         self.assertIsNone(settings.env_values)
-        self.assertEqual(create_stack.STACK_COMPLETE_TIMEOUT,
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
                          settings.stack_create_timeout)
 
     def test_config_minimum_template_path(self):
@@ -77,7 +82,7 @@ class StackSettingsUnitTests(unittest.TestCase):
         self.assertIsNone(settings.template)
         self.assertEqual('foo', settings.template_path)
         self.assertIsNone(settings.env_values)
-        self.assertEqual(create_stack.STACK_COMPLETE_TIMEOUT,
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
                          settings.stack_create_timeout)
 
     def test_minimum_template(self):
@@ -86,7 +91,7 @@ class StackSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo', settings.template)
         self.assertIsNone(settings.template_path)
         self.assertIsNone(settings.env_values)
-        self.assertEqual(create_stack.STACK_COMPLETE_TIMEOUT,
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
                          settings.stack_create_timeout)
 
     def test_minimum_template_path(self):
@@ -95,7 +100,7 @@ class StackSettingsUnitTests(unittest.TestCase):
         self.assertEqual('foo', settings.template_path)
         self.assertIsNone(settings.template)
         self.assertIsNone(settings.env_values)
-        self.assertEqual(create_stack.STACK_COMPLETE_TIMEOUT,
+        self.assertEqual(snaps.config.stack.STACK_COMPLETE_TIMEOUT,
                          settings.stack_create_timeout)
 
     def test_all(self):
@@ -123,14 +128,11 @@ class StackSettingsUnitTests(unittest.TestCase):
 
 class CreateStackSuccessTests(OSIntegrationTestCase):
     """
-    Tests for the CreateStack class defined in create_stack.py
+    Tests for the OpenStackHeatStack class defined in create_stack.py
     """
 
     def setUp(self):
-        """
-        Instantiates the CreateStack object that is responsible for downloading
-        and creating an OS stack file within OpenStack
-        """
+
         super(self.__class__, self).__start__()
 
         self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
@@ -150,8 +152,8 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         # Create Flavor
         self.flavor_creator = OpenStackFlavor(
             self.admin_os_creds,
-            FlavorSettings(name=self.guid + '-flavor-name', ram=256, disk=10,
-                           vcpus=1))
+            FlavorConfig(
+                name=self.guid + '-flavor-name', ram=256, disk=10, vcpus=1))
         self.flavor_creator.create()
 
         self.network_name = self.guid + '-net'
@@ -199,12 +201,12 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         # Create Stack
         # Set the default stack settings, then set any custom parameters sent
         # from the app
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template_path=self.heat_tmplt_path,
             env_values=self.env_values)
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         created_stack = self.stack_creator.create()
         self.assertIsNotNone(created_stack)
 
@@ -222,13 +224,13 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         # Create Stack
         # Set the default stack settings, then set any custom parameters sent
         # from the app
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template_path=self.heat_tmplt_path,
             env_values=self.env_values, stack_create_timeout=0)
 
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         with self.assertRaises(StackCreationError):
             self.stack_creator.create()
 
@@ -241,12 +243,12 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         # from the app
         template_dict = heat_utils.parse_heat_template_str(
             file_utils.read_file(self.heat_tmplt_path))
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template=template_dict,
             env_values=self.env_values)
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         created_stack = self.stack_creator.create()
         self.assertIsNotNone(created_stack)
 
@@ -265,12 +267,12 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         # Create Stack
         template_dict = heat_utils.parse_heat_template_str(
             file_utils.read_file(self.heat_tmplt_path))
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template=template_dict,
             env_values=self.env_values)
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         created_stack = self.stack_creator.create()
         self.assertIsNotNone(created_stack)
 
@@ -280,7 +282,7 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         self.assertEqual(created_stack.name, retrieved_stack.name)
         self.assertEqual(created_stack.id, retrieved_stack.id)
         self.assertEqual(0, len(self.stack_creator.get_outputs()))
-        self.assertEqual(create_stack.STATUS_CREATE_COMPLETE,
+        self.assertEqual(snaps.config.stack.STATUS_CREATE_COMPLETE,
                          self.stack_creator.get_status())
 
         # Delete Stack manually
@@ -291,7 +293,7 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         while time.time() < end_time:
             status = heat_utils.get_stack_status(self.heat_cli,
                                                  retrieved_stack.id)
-            if status == create_stack.STATUS_DELETE_COMPLETE:
+            if status == snaps.config.stack.STATUS_DELETE_COMPLETE:
                 deleted = True
                 break
 
@@ -309,12 +311,12 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         # Create Stack
         template_dict = heat_utils.parse_heat_template_str(
             file_utils.read_file(self.heat_tmplt_path))
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template=template_dict,
             env_values=self.env_values)
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         created_stack1 = self.stack_creator.create()
 
         retrieved_stack = heat_utils.get_stack_by_id(self.heat_cli,
@@ -325,8 +327,7 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         self.assertEqual(0, len(self.stack_creator.get_outputs()))
 
         # Should be retrieving the instance data
-        stack_creator2 = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                         stack_settings)
+        stack_creator2 = OpenStackHeatStack(self.heat_creds, stack_settings)
         stack2 = stack_creator2.create()
         self.assertEqual(created_stack1.id, stack2.id)
 
@@ -335,12 +336,12 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         Tests the creation of an OpenStack stack from Heat template file and
         the retrieval of the network creator.
         """
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template_path=self.heat_tmplt_path,
             env_values=self.env_values)
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         created_stack = self.stack_creator.create()
         self.assertIsNotNone(created_stack)
 
@@ -356,8 +357,8 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         self.assertIsNotNone(neutron_utils.get_network_by_id(
             neutron, net_creators[0].get_network().id))
 
-        self.assertEqual(1, len(net_creators[0].get_subnets()))
-        subnet = net_creators[0].get_subnets()[0]
+        self.assertEqual(1, len(net_creators[0].get_network().subnets))
+        subnet = net_creators[0].get_network().subnets[0]
         subnet_by_name = neutron_utils.get_subnet(
             neutron, subnet_name=subnet.name)
         self.assertEqual(subnet, subnet_by_name)
@@ -371,12 +372,12 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
         Tests the creation of an OpenStack stack from Heat template file and
         the retrieval of the network creator.
         """
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template_path=self.heat_tmplt_path,
             env_values=self.env_values)
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         created_stack = self.stack_creator.create()
         self.assertIsNotNone(created_stack)
 
@@ -387,23 +388,22 @@ class CreateStackSuccessTests(OSIntegrationTestCase):
                          vm_inst_creators[0].get_vm_inst().name)
 
         nova = nova_utils.nova_client(self.admin_os_creds)
+        neutron = neutron_utils.neutron_client(self.admin_os_creds)
         vm_inst_by_name = nova_utils.get_server(
-            nova, server_name=vm_inst_creators[0].get_vm_inst().name)
+            nova, neutron, server_name=vm_inst_creators[0].get_vm_inst().name)
         self.assertEqual(vm_inst_creators[0].get_vm_inst(), vm_inst_by_name)
         self.assertIsNotNone(nova_utils.get_server_object_by_id(
-            nova, vm_inst_creators[0].get_vm_inst().id))
+            nova, neutron, vm_inst_creators[0].get_vm_inst().id))
 
 
-class CreateComplexStackTests(OSIntegrationTestCase):
+class CreateStackFloatingIpTests(OSIntegrationTestCase):
     """
-    Tests for the CreateStack class defined in create_stack.py
+    Tests to ensure that floating IPs can be accessed via an
+    OpenStackVmInstance object obtained from the OpenStackHeatStack instance
     """
 
     def setUp(self):
-        """
-        Instantiates the CreateStack object that is responsible for downloading
-        and creating an OS stack file within OpenStack
-        """
+
         super(self.__class__, self).__start__()
 
         self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
@@ -424,6 +424,7 @@ class CreateComplexStackTests(OSIntegrationTestCase):
         self.subnet_name = self.guid + '-subnet'
         self.flavor1_name = self.guid + '-flavor1'
         self.flavor2_name = self.guid + '-flavor2'
+        self.sec_grp_name = self.guid + '-sec_grp'
         self.vm_inst1_name = self.guid + '-inst1'
         self.vm_inst2_name = self.guid + '-inst2'
         self.keypair_name = self.guid + '-kp'
@@ -437,11 +438,15 @@ class CreateComplexStackTests(OSIntegrationTestCase):
             'subnet_name': self.subnet_name,
             'inst1_name': self.vm_inst1_name,
             'inst2_name': self.vm_inst2_name,
-            'keypair_name': self.keypair_name}
+            'keypair_name': self.keypair_name,
+            'external_net_name': self.ext_net_name,
+            'security_group_name': self.sec_grp_name}
 
         self.heat_tmplt_path = pkg_resources.resource_filename(
             'snaps.openstack.tests.heat', 'floating_ip_heat_template.yaml')
 
+        self.vm_inst_creators = list()
+
     def tearDown(self):
         """
         Cleans the stack and downloaded stack file
@@ -458,6 +463,17 @@ class CreateComplexStackTests(OSIntegrationTestCase):
             except:
                 pass
 
+        for vm_inst_creator in self.vm_inst_creators:
+            try:
+                keypair_settings = vm_inst_creator.keypair_settings
+                if keypair_settings and keypair_settings.private_filepath:
+                    expanded_path = os.path.expanduser(
+                        keypair_settings.private_filepath)
+                    os.chmod(expanded_path, 0o755)
+                    os.remove(expanded_path)
+            except:
+                pass
+
         super(self.__class__, self).__clean__()
 
     def test_connect_via_ssh_heat_vm(self):
@@ -466,22 +482,22 @@ class CreateComplexStackTests(OSIntegrationTestCase):
         the retrieval of two VM instance creators and attempt to connect via
         SSH to the first one with a floating IP.
         """
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
             template_path=self.heat_tmplt_path,
             env_values=self.env_values)
-        self.stack_creator = create_stack.OpenStackHeatStack(
+        self.stack_creator = OpenStackHeatStack(
             self.heat_creds, stack_settings,
             [self.image_creator.image_settings])
         created_stack = self.stack_creator.create()
         self.assertIsNotNone(created_stack)
 
-        vm_inst_creators = self.stack_creator.get_vm_inst_creators(
+        self.vm_inst_creators = self.stack_creator.get_vm_inst_creators(
             heat_keypair_option='private_key')
-        self.assertIsNotNone(vm_inst_creators)
-        self.assertEqual(2, len(vm_inst_creators))
+        self.assertIsNotNone(self.vm_inst_creators)
+        self.assertEqual(2, len(self.vm_inst_creators))
 
-        for vm_inst_creator in vm_inst_creators:
+        for vm_inst_creator in self.vm_inst_creators:
             if vm_inst_creator.get_vm_inst().name == self.vm_inst1_name:
                 self.assertTrue(
                     create_instance_tests.validate_ssh_client(vm_inst_creator))
@@ -490,12 +506,525 @@ class CreateComplexStackTests(OSIntegrationTestCase):
                 self.assertEqual(0, len(vm_settings.floating_ip_settings))
 
 
+class CreateStackNestedResourceTests(OSIntegrationTestCase):
+    """
+    Tests to ensure that nested heat templates work
+    """
+
+    def setUp(self):
+
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.stack_creator = None
+
+        self.image_creator = OpenStackImage(
+            self.heat_creds, openstack_tests.cirros_image_settings(
+                name=self.guid + '-image',
+                image_metadata=self.image_metadata))
+        self.image_creator.create()
+
+        self.flavor_creator = OpenStackFlavor(
+            self.admin_os_creds,
+            FlavorConfig(
+                name=self.guid + '-flavor-name', ram=256, disk=10, vcpus=1))
+        self.flavor_creator.create()
+
+        env_values = {
+            'public_network': self.ext_net_name,
+            'agent_image': self.image_creator.image_settings.name,
+            'agent_flavor': self.flavor_creator.flavor_settings.name,
+        }
+
+        heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'agent-group.yaml')
+        heat_resource_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'agent.yaml')
+
+        stack_settings = StackConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
+            template_path=heat_tmplt_path,
+            resource_files=[heat_resource_path],
+            env_values=env_values)
+
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings,
+            [self.image_creator.image_settings])
+
+        self.vm_inst_creators = list()
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+
+        if self.image_creator:
+            try:
+                self.image_creator.clean()
+            except:
+                pass
+
+        if self.flavor_creator:
+            try:
+                self.flavor_creator.clean()
+            except:
+                pass
+
+        for vm_inst_creator in self.vm_inst_creators:
+            try:
+                keypair_settings = vm_inst_creator.keypair_settings
+                if keypair_settings and keypair_settings.private_filepath:
+                    expanded_path = os.path.expanduser(
+                        keypair_settings.private_filepath)
+                    os.chmod(expanded_path, 0o755)
+                    os.remove(expanded_path)
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_nested(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of two VM instance creators and attempt to connect via
+        SSH to the first one with a floating IP.
+        """
+        created_stack = self.stack_creator.create()
+        self.assertIsNotNone(created_stack)
+
+        self.vm_inst_creators = self.stack_creator.get_vm_inst_creators(
+            heat_keypair_option='private_key')
+        self.assertIsNotNone(self.vm_inst_creators)
+        self.assertEqual(1, len(self.vm_inst_creators))
+
+        for vm_inst_creator in self.vm_inst_creators:
+            self.assertTrue(
+                create_instance_tests.validate_ssh_client(vm_inst_creator))
+
+
+class CreateStackRouterTests(OSIntegrationTestCase):
+    """
+    Tests for the CreateStack class defined in create_stack.py where the
+    target is a Network, Subnet, and Router
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateStack object that is responsible for downloading
+        and creating an OS stack file within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.neutron = neutron_utils.neutron_client(self.os_creds)
+        self.stack_creator = None
+
+        self.net_name = self.guid + '-net'
+        self.subnet_name = self.guid + '-subnet'
+        self.router_name = self.guid + '-router'
+
+        self.env_values = {
+            'net_name': self.net_name,
+            'subnet_name': self.subnet_name,
+            'router_name': self.router_name,
+            'external_net_name': self.ext_net_name}
+
+        self.heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'router_heat_template.yaml')
+
+        stack_settings = StackConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
+            template_path=self.heat_tmplt_path,
+            env_values=self.env_values)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
+        self.created_stack = self.stack_creator.create()
+        self.assertIsNotNone(self.created_stack)
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_retrieve_router_creator(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of an OpenStackRouter creator/state machine instance
+        """
+        router_creators = self.stack_creator.get_router_creators()
+        self.assertEqual(1, len(router_creators))
+
+        creator = router_creators[0]
+        self.assertEqual(self.router_name, creator.router_settings.name)
+
+        router = creator.get_router()
+
+        ext_net = neutron_utils.get_network(
+            self.neutron, network_name=self.ext_net_name)
+        self.assertEqual(ext_net.id, router.external_network_id)
+
+
+class CreateStackVolumeTests(OSIntegrationTestCase):
+    """
+    Tests to ensure that floating IPs can be accessed via an
+    OpenStackVolume object obtained from the OpenStackHeatStack instance
+    """
+
+    def setUp(self):
+
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.stack_creator = None
+
+        self.volume_name = self.guid + '-volume'
+        self.volume_type_name = self.guid + '-volume-type'
+
+        self.env_values = {
+            'volume_name': self.volume_name,
+            'volume_type_name': self.volume_type_name}
+
+        self.heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'volume_heat_template.yaml')
+
+        stack_settings = StackConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
+            template_path=self.heat_tmplt_path,
+            env_values=self.env_values)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
+        self.created_stack = self.stack_creator.create()
+        self.assertIsNotNone(self.created_stack)
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_retrieve_volume_creator(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of an OpenStackVolume creator/state machine instance
+        """
+        volume_creators = self.stack_creator.get_volume_creators()
+        self.assertEqual(1, len(volume_creators))
+
+        creator = volume_creators[0]
+        self.assertEqual(self.volume_name, creator.volume_settings.name)
+        self.assertEqual(self.volume_name, creator.get_volume().name)
+        self.assertEqual(self.volume_type_name,
+                         creator.volume_settings.type_name)
+        self.assertEqual(self.volume_type_name, creator.get_volume().type)
+        self.assertEqual(1, creator.volume_settings.size)
+        self.assertEqual(1, creator.get_volume().size)
+
+    def test_retrieve_volume_type_creator(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of an OpenStackVolume creator/state machine instance
+        """
+        volume_type_creators = self.stack_creator.get_volume_type_creators()
+        self.assertEqual(1, len(volume_type_creators))
+
+        creator = volume_type_creators[0]
+        self.assertIsNotNone(creator)
+
+        volume_type = creator.get_volume_type()
+        self.assertIsNotNone(volume_type)
+
+        self.assertEqual(self.volume_type_name, volume_type.name)
+        self.assertTrue(volume_type.public)
+        self.assertIsNone(volume_type.qos_spec)
+
+        # TODO - Add encryption back and find out why it broke in Pike
+        # encryption = volume_type.encryption
+        # self.assertIsNotNone(encryption)
+        # self.assertIsNone(encryption.cipher)
+        # self.assertEqual('front-end', encryption.control_location)
+        # self.assertIsNone(encryption.key_size)
+        # self.assertEqual(u'nova.volume.encryptors.luks.LuksEncryptor',
+        #                  encryption.provider)
+        # self.assertEqual(volume_type.id, encryption.volume_type_id)
+
+
+class CreateStackFlavorTests(OSIntegrationTestCase):
+    """
+    Tests to ensure that floating IPs can be accessed via an
+    OpenStackFlavor object obtained from the OpenStackHeatStack instance
+    """
+
+    def setUp(self):
+
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.stack_creator = None
+
+        self.heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'flavor_heat_template.yaml')
+
+        stack_settings = StackConfig(
+            name=self.guid + '-stack',
+            template_path=self.heat_tmplt_path)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
+        self.created_stack = self.stack_creator.create()
+        self.assertIsNotNone(self.created_stack)
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_retrieve_flavor_creator(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of an OpenStackVolume creator/state machine instance
+        """
+        flavor_creators = self.stack_creator.get_flavor_creators()
+        self.assertEqual(1, len(flavor_creators))
+
+        creator = flavor_creators[0]
+        self.assertTrue(creator.get_flavor().name.startswith(self.guid))
+        self.assertEqual(1024, creator.get_flavor().ram)
+        self.assertEqual(200, creator.get_flavor().disk)
+        self.assertEqual(8, creator.get_flavor().vcpus)
+        self.assertEqual(0, creator.get_flavor().ephemeral)
+        self.assertIsNone(creator.get_flavor().swap)
+        self.assertEqual(1.0, creator.get_flavor().rxtx_factor)
+        self.assertTrue(creator.get_flavor().is_public)
+
+
+class CreateStackKeypairTests(OSIntegrationTestCase):
+    """
+    Tests to ensure that floating IPs can be accessed via an
+    OpenStackKeypair object obtained from the OpenStackHeatStack instance
+    """
+
+    def setUp(self):
+
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.nova = nova_utils.nova_client(self.heat_creds)
+        self.stack_creator = None
+
+        self.keypair_name = self.guid + '-kp'
+
+        self.env_values = {
+            'keypair_name': self.keypair_name}
+
+        self.heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'keypair_heat_template.yaml')
+
+        stack_settings = StackConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
+            template_path=self.heat_tmplt_path,
+            env_values=self.env_values)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
+        self.created_stack = self.stack_creator.create()
+        self.assertIsNotNone(self.created_stack)
+
+        self.keypair_creators = list()
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+        for keypair_creator in self.keypair_creators:
+            try:
+                keypair_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_retrieve_keypair_creator(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of an OpenStackKeypair creator/state machine instance
+        """
+        self.kp_creators = self.stack_creator.get_keypair_creators(
+            'private_key')
+        self.assertEqual(1, len(self.kp_creators))
+
+        self.keypair_creator = self.kp_creators[0]
+
+        self.assertEqual(self.keypair_name,
+                         self.keypair_creator.get_keypair().name)
+        self.assertIsNotNone(
+            self.keypair_creator.keypair_settings.private_filepath)
+
+        private_file_contents = file_utils.read_file(
+            self.keypair_creator.keypair_settings.private_filepath)
+        self.assertTrue(private_file_contents.startswith(
+            '-----BEGIN RSA PRIVATE KEY-----'))
+
+        keypair = nova_utils.get_keypair_by_id(
+            self.nova, self.keypair_creator.get_keypair().id)
+        self.assertIsNotNone(keypair)
+        self.assertEqual(self.keypair_creator.get_keypair(), keypair)
+
+
+class CreateStackSecurityGroupTests(OSIntegrationTestCase):
+    """
+    Tests for the OpenStackHeatStack class to ensure it returns an
+    OpenStackSecurityGroup object
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateStack object that is responsible for downloading
+        and creating an OS stack file within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.nova = nova_utils.nova_client(self.heat_creds)
+        self.stack_creator = None
+
+        self.security_group_name = self.guid + '-sec-grp'
+
+        self.env_values = {
+            'security_group_name': self.security_group_name}
+
+        self.heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'security_group_heat_template.yaml')
+
+        stack_settings = StackConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
+            template_path=self.heat_tmplt_path,
+            env_values=self.env_values)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
+        self.created_stack = self.stack_creator.create()
+        self.assertIsNotNone(self.created_stack)
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_retrieve_security_group_creator(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file and
+        the retrieval of an OpenStackSecurityGroup creator/state machine
+        instance
+        """
+        sec_grp_creators = self.stack_creator.get_security_group_creators()
+        self.assertEqual(1, len(sec_grp_creators))
+
+        creator = sec_grp_creators[0]
+        sec_grp = creator.get_security_group()
+
+        self.assertEqual(self.security_group_name, sec_grp.name)
+        self.assertEqual('Test description', sec_grp.description)
+        self.assertEqual(2, len(sec_grp.rules))
+
+        has_ssh_rule = False
+        has_icmp_rule = False
+
+        for rule in sec_grp.rules:
+            if (rule.security_group_id == sec_grp.id
+                    and rule.direction == 'egress'
+                    and rule.ethertype == 'IPv4'
+                    and rule.port_range_min == 22
+                    and rule.port_range_max == 22
+                    and rule.protocol == 'tcp'
+                    and rule.remote_group_id is None
+                    and rule.remote_ip_prefix == '0.0.0.0/0'):
+                has_ssh_rule = True
+            if (rule.security_group_id == sec_grp.id
+                    and rule.direction == 'ingress'
+                    and rule.ethertype == 'IPv4'
+                    and rule.port_range_min is None
+                    and rule.port_range_max is None
+                    and rule.protocol == 'icmp'
+                    and rule.remote_group_id is None
+                    and rule.remote_ip_prefix == '0.0.0.0/0'):
+                has_icmp_rule = True
+
+        self.assertTrue(has_ssh_rule)
+        self.assertTrue(has_icmp_rule)
+
+
 class CreateStackNegativeTests(OSIntegrationTestCase):
     """
-    Negative test cases for the CreateStack class
+    Negative test cases for the OpenStackHeatStack class with poor
+    configuration
     """
 
     def setUp(self):
+
         super(self.__class__, self).__start__()
 
         self.heat_creds = self.admin_os_creds
@@ -515,10 +1044,10 @@ class CreateStackNegativeTests(OSIntegrationTestCase):
         """
         Expect an StackCreationError when the stack file does not exist
         """
-        stack_settings = StackSettings(name=self.stack_name,
-                                       template_path=self.heat_tmplt_path)
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        stack_settings = StackConfig(name=self.stack_name,
+                                     template_path=self.heat_tmplt_path)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         with self.assertRaises(HTTPBadRequest):
             self.stack_creator.create()
 
@@ -526,9 +1055,119 @@ class CreateStackNegativeTests(OSIntegrationTestCase):
         """
         Expect an StackCreationError when the stack file does not exist
         """
-        stack_settings = StackSettings(name=self.stack_name,
-                                       template_path='foo')
-        self.stack_creator = create_stack.OpenStackHeatStack(self.heat_creds,
-                                                             stack_settings)
+        stack_settings = StackConfig(
+            name=self.stack_name, template_path='foo')
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
         with self.assertRaises(IOError):
             self.stack_creator.create()
+
+
+class CreateStackFailureTests(OSIntegrationTestCase):
+    """
+    Tests for the OpenStackHeatStack class defined in create_stack.py for
+    when failures occur. Failures are being triggered by allocating 1 million
+    CPUs.
+    """
+
+    def setUp(self):
+
+        super(self.__class__, self).__start__()
+
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.heat_creds = self.admin_os_creds
+        self.heat_creds.project_name = self.admin_os_creds.project_name
+
+        self.heat_cli = heat_utils.heat_client(self.heat_creds)
+        self.stack_creator = None
+
+        self.tmp_file = file_utils.save_string_to_file(
+            ' ', str(uuid.uuid4()) + '-bad-image')
+        self.image_creator = OpenStackImage(
+            self.heat_creds, ImageConfig(
+                name=self.guid + 'image', image_file=self.tmp_file.name,
+                image_user='foo', img_format='qcow2'))
+        self.image_creator.create()
+
+        # Create Flavor
+        self.flavor_creator = OpenStackFlavor(
+            self.admin_os_creds,
+            FlavorConfig(
+                name=self.guid + '-flavor-name', ram=256, disk=10,
+                vcpus=1000000))
+        self.flavor_creator.create()
+
+        self.network_name = self.guid + '-net'
+        self.subnet_name = self.guid + '-subnet'
+        self.vm_inst_name = self.guid + '-inst'
+
+        self.env_values = {
+            'image_name': self.image_creator.image_settings.name,
+            'flavor_name': self.flavor_creator.flavor_settings.name,
+            'net_name': self.network_name,
+            'subnet_name': self.subnet_name,
+            'inst_name': self.vm_inst_name}
+
+        self.heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'test_heat_template.yaml')
+
+    def tearDown(self):
+        """
+        Cleans the stack and downloaded stack file
+        """
+        if self.stack_creator:
+            try:
+                self.stack_creator.clean()
+            except:
+                pass
+
+        if self.image_creator:
+            try:
+                self.image_creator.clean()
+            except:
+                pass
+
+        if self.flavor_creator:
+            try:
+                self.flavor_creator.clean()
+            except:
+                pass
+
+        if self.tmp_file:
+            try:
+                os.remove(self.tmp_file.name)
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_stack_failure(self):
+        """
+        Tests the creation of an OpenStack stack from Heat template file that
+        should always fail due to too many CPU cores
+        """
+        # Create Stack
+        # Set the default stack settings, then set any custom parameters sent
+        # from the app
+        stack_settings = StackConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid) + '-stack',
+            template_path=self.heat_tmplt_path,
+            env_values=self.env_values)
+        self.stack_creator = OpenStackHeatStack(
+            self.heat_creds, stack_settings)
+
+        with self.assertRaises(StackError):
+            try:
+                self.stack_creator.create()
+            except StackError:
+                resources = heat_utils.get_resources(
+                    self.heat_cli, self.stack_creator.get_stack().id)
+
+                found = False
+                for resource in resources:
+                    if (resource.status ==
+                            snaps.config.stack.STATUS_CREATE_COMPLETE):
+                        found = True
+                self.assertTrue(found)
+                raise
index ffae596..d3eb4a6 100644 (file)
@@ -15,6 +15,7 @@
 import unittest
 import uuid
 
+from snaps.config.user import UserConfig
 from snaps.openstack.create_user import OpenStackUser, UserSettings
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
 from snaps.openstack.utils import keystone_utils
@@ -102,7 +103,7 @@ class CreateUserSuccessTests(OSComponentTestCase):
         """
         guid = str(uuid.uuid4())[:-19]
         guid = self.__class__.__name__ + '-' + guid
-        self.user_settings = UserSettings(
+        self.user_settings = UserConfig(
             name=guid + '-name',
             password=guid + '-password',
             roles={'admin': self.os_creds.project_name},
diff --git a/snaps/openstack/tests/create_volume_tests.py b/snaps/openstack/tests/create_volume_tests.py
new file mode 100644 (file)
index 0000000..ca13860
--- /dev/null
@@ -0,0 +1,410 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from cinderclient.exceptions import NotFound, BadRequest
+
+from snaps.config.volume import VolumeConfig, VolumeConfigError
+from snaps.config.volume_type import VolumeTypeConfig
+from snaps.openstack.create_image import OpenStackImage
+from snaps.openstack.create_volume_type import OpenStackVolumeType
+from snaps.openstack.tests import openstack_tests
+
+try:
+    from urllib.request import URLError
+except ImportError:
+    from urllib2 import URLError
+
+import logging
+import unittest
+import uuid
+
+from snaps.openstack.create_volume import (
+    VolumeSettings, OpenStackVolume)
+from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
+from snaps.openstack.utils import cinder_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_volume_tests')
+
+
+class VolumeSettingsUnitTests(unittest.TestCase):
+    """
+    Tests the construction of the VolumeSettings class
+    """
+
+    def test_no_params(self):
+        with self.assertRaises(VolumeConfigError):
+            VolumeSettings()
+
+    def test_empty_config(self):
+        with self.assertRaises(VolumeConfigError):
+            VolumeSettings(**dict())
+
+    def test_name_only(self):
+        settings = VolumeSettings(name='foo')
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.description)
+        self.assertEquals(1, settings.size)
+        self.assertIsNone(settings.image_name)
+        self.assertIsNone(settings.type_name)
+        self.assertIsNone(settings.availability_zone)
+        self.assertFalse(settings.multi_attach)
+
+    def test_config_with_name_only(self):
+        settings = VolumeSettings(**{'name': 'foo'})
+        self.assertEqual('foo', settings.name)
+        self.assertIsNone(settings.description)
+        self.assertEquals(1, settings.size)
+        self.assertIsNone(settings.image_name)
+        self.assertIsNone(settings.type_name)
+        self.assertIsNone(settings.availability_zone)
+        self.assertFalse(settings.multi_attach)
+
+    def test_all_strings(self):
+        settings = VolumeSettings(
+            name='foo', description='desc', size='2', image_name='image',
+            type_name='type', availability_zone='zone1', multi_attach='true')
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual(2, settings.size)
+        self.assertEqual('image', settings.image_name)
+        self.assertEqual('type', settings.type_name)
+        self.assertEqual('zone1', settings.availability_zone)
+        self.assertTrue(settings.multi_attach)
+
+    def test_all_correct_type(self):
+        settings = VolumeSettings(
+            name='foo', description='desc', size=2, image_name='image',
+            type_name='bar', availability_zone='zone1', multi_attach=True)
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual(2, settings.size)
+        self.assertEqual('image', settings.image_name)
+        self.assertEqual('bar', settings.type_name)
+        self.assertEqual('zone1', settings.availability_zone)
+        self.assertTrue(settings.multi_attach)
+
+    def test_config_all(self):
+        settings = VolumeSettings(
+            **{'name': 'foo', 'description': 'desc', 'size': '2',
+               'image_name': 'foo', 'type_name': 'bar',
+               'availability_zone': 'zone1', 'multi_attach': 'true'})
+
+        self.assertEqual('foo', settings.name)
+        self.assertEqual('desc', settings.description)
+        self.assertEqual(2, settings.size)
+        self.assertEqual('foo', settings.image_name)
+        self.assertEqual('bar', settings.type_name)
+        self.assertEqual('zone1', settings.availability_zone)
+        self.assertTrue(settings.multi_attach)
+
+
+class CreateSimpleVolumeSuccessTests(OSIntegrationTestCase):
+    """
+    Test for the CreateVolume class defined in create_volume.py
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateVolume object that is responsible for
+        downloading and creating an OS volume file within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        guid = uuid.uuid4()
+        self.volume_settings = VolumeConfig(
+            name=self.__class__.__name__ + '-' + str(guid))
+
+        self.cinder = cinder_utils.cinder_client(self.os_creds)
+        self.volume_creator = None
+
+    def tearDown(self):
+        """
+        Cleans the volume and downloaded volume file
+        """
+        if self.volume_creator:
+            self.volume_creator.clean()
+
+        super(self.__class__, self).__clean__()
+
+    def test_create_volume_simple(self):
+        """
+        Tests the creation of a simple OpenStack volume.
+        """
+        # Create Volume
+        self.volume_creator = OpenStackVolume(
+            self.os_creds, self.volume_settings)
+        created_volume = self.volume_creator.create(block=True)
+        self.assertIsNotNone(created_volume)
+
+        retrieved_volume = cinder_utils.get_volume(
+            self.cinder, volume_settings=self.volume_settings)
+
+        self.assertIsNotNone(retrieved_volume)
+        self.assertEqual(created_volume.id, retrieved_volume.id)
+        self.assertTrue(created_volume == retrieved_volume)
+
+    def test_create_delete_volume(self):
+        """
+        Tests the creation then deletion of an OpenStack volume to ensure
+        clean() does not raise an Exception.
+        """
+        # Create Volume
+        self.volume_creator = OpenStackVolume(
+            self.os_creds, self.volume_settings)
+        created_volume = self.volume_creator.create(block=True)
+        self.assertIsNotNone(created_volume)
+
+        retrieved_volume = cinder_utils.get_volume(
+            self.cinder, volume_settings=self.volume_settings)
+        self.assertIsNotNone(retrieved_volume)
+        self.assertEqual(created_volume, retrieved_volume)
+
+        # Delete Volume manually
+        self.volume_creator.clean()
+
+        self.assertIsNone(cinder_utils.get_volume(
+            self.cinder, volume_settings=self.volume_settings))
+
+        # Must not throw an exception when attempting to cleanup non-existent
+        # volume
+        self.volume_creator.clean()
+        self.assertIsNone(self.volume_creator.get_volume())
+
+    def test_create_same_volume(self):
+        """
+        Tests the creation of an OpenStack volume when one already exists.
+        """
+        # Create Volume
+        self.volume_creator = OpenStackVolume(
+            self.os_creds, self.volume_settings)
+        volume1 = self.volume_creator.create(block=True)
+
+        retrieved_volume = cinder_utils.get_volume(
+            self.cinder, volume_settings=self.volume_settings)
+        self.assertEqual(volume1, retrieved_volume)
+
+        # Should be retrieving the instance data
+        os_volume_2 = OpenStackVolume(
+            self.os_creds, self.volume_settings)
+        volume2 = os_volume_2.create(block=True)
+        self.assertEqual(volume1, volume2)
+
+
+class CreateSimpleVolumeFailureTests(OSIntegrationTestCase):
+    """
+    Test for the CreateVolume class defined in create_volume.py
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateVolume object that is responsible for
+        downloading and creating an OS volume file within OpenStack
+        """
+        super(self.__class__, self).__start__()
+
+        self.guid = uuid.uuid4()
+        self.cinder = cinder_utils.cinder_client(self.os_creds)
+        self.volume_creator = None
+
+    def tearDown(self):
+        """
+        Cleans the volume and downloaded volume file
+        """
+        if self.volume_creator:
+            self.volume_creator.clean()
+
+        super(self.__class__, self).__clean__()
+
+    def test_create_volume_bad_size(self):
+        """
+        Tests the creation of an OpenStack volume with a negative size to
+        ensure it raises a BadRequest exception.
+        """
+        volume_settings = VolumeConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid), size=-1)
+
+        # Create Volume
+        self.volume_creator = OpenStackVolume(self.os_creds, volume_settings)
+
+        with self.assertRaises(BadRequest):
+            self.volume_creator.create(block=True)
+
+    def test_create_volume_bad_type(self):
+        """
+        Tests the creation of an OpenStack volume with a type that does not
+        exist to ensure it raises a NotFound exception.
+        """
+        volume_settings = VolumeConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid),
+            type_name='foo')
+
+        # Create Volume
+        self.volume_creator = OpenStackVolume(self.os_creds, volume_settings)
+
+        with self.assertRaises(NotFound):
+            self.volume_creator.create(block=True)
+
+    def test_create_volume_bad_image(self):
+        """
+        Tests the creation of an OpenStack volume with an image that does not
+        exist to ensure it raises a BadRequest exception.
+        """
+        volume_settings = VolumeConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid),
+            image_name='foo')
+
+        # Create Volume
+        self.volume_creator = OpenStackVolume(self.os_creds, volume_settings)
+
+        with self.assertRaises(BadRequest):
+            self.volume_creator.create(block=True)
+
+    def test_create_volume_bad_zone(self):
+        """
+        Tests the creation of an OpenStack volume with an availability zone
+        that does not exist to ensure it raises a BadRequest exception.
+        """
+        volume_settings = VolumeConfig(
+            name=self.__class__.__name__ + '-' + str(self.guid),
+            availability_zone='foo')
+
+        # Create Volume
+        self.volume_creator = OpenStackVolume(self.os_creds, volume_settings)
+
+        with self.assertRaises(BadRequest):
+            self.volume_creator.create(block=True)
+
+
+class CreateVolumeWithTypeTests(OSIntegrationTestCase):
+    """
+    Test cases for the CreateVolume when attempting to associate it to a
+    Volume Type
+    """
+
+    def setUp(self):
+        super(self.__class__, self).__start__()
+
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.volume_name = guid + '-vol'
+        self.volume_type_name = guid + '-vol-type'
+
+        self.volume_type_creator = OpenStackVolumeType(
+            self.os_creds, VolumeTypeConfig(name=self.volume_type_name))
+        self.volume_type_creator.create()
+        self.volume_creator = None
+
+    def tearDown(self):
+        if self.volume_creator:
+            self.volume_creator.clean()
+        if self.volume_type_creator:
+            self.volume_type_creator.clean()
+
+        super(self.__class__, self).__clean__()
+
+    def test_bad_volume_type(self):
+        """
+        Expect a NotFound to be raised when the volume type does not exist
+        """
+        self.volume_creator = OpenStackVolume(
+            self.os_creds,
+            VolumeConfig(name=self.volume_name, type_name='foo'))
+
+        with self.assertRaises(NotFound):
+            self.volume_creator.create()
+
+    def test_valid_volume_type(self):
+        """
+        Expect a NotFound to be raised when the volume type does not exist
+        """
+        self.volume_creator = OpenStackVolume(
+            self.os_creds,
+            VolumeConfig(
+                name=self.volume_name, type_name=self.volume_type_name))
+
+        created_volume = self.volume_creator.create(block=True)
+        self.assertIsNotNone(created_volume)
+        self.assertEqual(self.volume_type_name, created_volume.type)
+
+
+class CreateVolumeWithImageTests(OSIntegrationTestCase):
+    """
+    Test cases for the CreateVolume when attempting to associate it to an Image
+    """
+
+    def setUp(self):
+        super(self.__class__, self).__start__()
+
+        self.cinder = cinder_utils.cinder_client(self.os_creds)
+
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.volume_name = guid + '-vol'
+        self.image_name = guid + '-image'
+
+        os_image_settings = openstack_tests.cirros_image_settings(
+            name=self.image_name, image_metadata=self.image_metadata)
+        # Create Image
+        self.image_creator = OpenStackImage(self.os_creds,
+                                            os_image_settings)
+        self.image_creator.create()
+        self.volume_creator = None
+
+    def tearDown(self):
+        if self.volume_creator:
+            try:
+                self.volume_creator.clean()
+            except:
+                pass
+        if self.image_creator:
+            try:
+                self.image_creator.clean()
+            except:
+                pass
+
+        super(self.__class__, self).__clean__()
+
+    def test_bad_image_name(self):
+        """
+        Tests OpenStackVolume#create() method to ensure a volume is NOT created
+        when associating it to an invalid image name
+        """
+        self.volume_creator = OpenStackVolume(
+            self.os_creds,
+            VolumeConfig(name=self.volume_name, image_name='foo'))
+
+        with self.assertRaises(BadRequest):
+            self.volume_creator.create(block=True)
+
+    def test_valid_volume_image(self):
+        """
+        Tests OpenStackVolume#create() method to ensure a volume is NOT created
+        when associating it to an invalid image name
+        """
+        self.volume_creator = OpenStackVolume(
+            self.os_creds,
+            VolumeConfig(name=self.volume_name, image_name=self.image_name))
+
+        created_volume = self.volume_creator.create(block=True)
+        self.assertIsNotNone(created_volume)
+        self.assertEqual(
+            self.volume_creator.volume_settings.name, created_volume.name)
+        self.assertTrue(self.volume_creator.volume_active())
+
+        retrieved_volume = cinder_utils.get_volume_by_id(
+            self.cinder, created_volume.id)
+
+        self.assertEqual(created_volume, retrieved_volume)
index 93e9351..70c40cc 100644 (file)
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-from snaps.openstack.create_qos import QoSSettings, Consumer, OpenStackQoS
+from snaps.config.volume_type import (
+    VolumeTypeConfig, VolumeTypeEncryptionConfig, VolumeTypeConfigError,
+    ControlLocation)
+from snaps.config.qos import QoSConfig, Consumer
+from snaps.openstack.create_qos import OpenStackQoS
 
 try:
     from urllib.request import URLError
@@ -25,8 +29,7 @@ import uuid
 
 from snaps.openstack import create_volume_type
 from snaps.openstack.create_volume_type import (
-    VolumeTypeSettings, VolumeTypeSettingsError, VolumeTypeEncryptionSettings,
-    ControlLocation)
+    VolumeTypeSettings, VolumeTypeEncryptionSettings)
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
 from snaps.openstack.utils import cinder_utils
 
@@ -41,11 +44,11 @@ class VolumeTypeSettingsUnitTests(unittest.TestCase):
     """
 
     def test_no_params(self):
-        with self.assertRaises(VolumeTypeSettingsError):
+        with self.assertRaises(VolumeTypeConfigError):
             VolumeTypeSettings()
 
     def test_empty_config(self):
-        with self.assertRaises(VolumeTypeSettingsError):
+        with self.assertRaises(VolumeTypeConfigError):
             VolumeTypeSettings(**dict())
 
     def test_name_only(self):
@@ -109,7 +112,7 @@ class VolumeTypeSettingsUnitTests(unittest.TestCase):
 
 class CreateSimpleVolumeTypeSuccessTests(OSIntegrationTestCase):
     """
-    Test for the OpenStackVolumeType class defined in create_volume_type.py
+    Test for the OpenStackVolumeType class defined in py
     without any QoS Specs or Encryption
     """
 
@@ -121,7 +124,7 @@ class CreateSimpleVolumeTypeSuccessTests(OSIntegrationTestCase):
         super(self.__class__, self).__start__()
 
         guid = uuid.uuid4()
-        self.volume_type_settings = VolumeTypeSettings(
+        self.volume_type_settings = VolumeTypeConfig(
             name=self.__class__.__name__ + '-' + str(guid))
 
         self.cinder = cinder_utils.cinder_client(self.os_creds)
@@ -220,7 +223,7 @@ class CreateVolumeTypeComplexTests(OSIntegrationTestCase):
         self.volume_type_name = guid + '-vol_type'
         self.volume_type_creator = None
 
-        qos_settings = QoSSettings(
+        qos_settings = QoSConfig(
             name=guid + '-qos-spec', consumer=Consumer.both)
         self.qos_creator = OpenStackQoS(self.os_creds, qos_settings)
         self.qos_creator.create()
@@ -240,7 +243,7 @@ class CreateVolumeTypeComplexTests(OSIntegrationTestCase):
         """
         self.volume_type_creator = create_volume_type.OpenStackVolumeType(
             self.os_creds,
-            VolumeTypeSettings(
+            VolumeTypeConfig(
                 name=self.volume_type_name,
                 qos_spec_name=self.qos_creator.qos_settings.name))
 
@@ -264,12 +267,12 @@ class CreateVolumeTypeComplexTests(OSIntegrationTestCase):
         """
         Creates a Volume Type object with encryption
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name='foo', provider_class='bar',
             control_location=ControlLocation.back_end)
         self.volume_type_creator = create_volume_type.OpenStackVolumeType(
             self.os_creds,
-            VolumeTypeSettings(
+            VolumeTypeConfig(
                 name=self.volume_type_name,
                 encryption=encryption_settings))
 
@@ -293,12 +296,12 @@ class CreateVolumeTypeComplexTests(OSIntegrationTestCase):
         """
         Creates a Volume Type object with encryption and an associated QoS Spec
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name='foo', provider_class='bar',
             control_location=ControlLocation.back_end)
         self.volume_type_creator = create_volume_type.OpenStackVolumeType(
             self.os_creds,
-            VolumeTypeSettings(
+            VolumeTypeConfig(
                 name=self.volume_type_name,
                 encryption=encryption_settings,
                 qos_spec_name=self.qos_creator.qos_settings.name))
diff --git a/snaps/openstack/tests/heat/agent-group.yaml b/snaps/openstack/tests/heat/agent-group.yaml
new file mode 100644 (file)
index 0000000..540ea93
--- /dev/null
@@ -0,0 +1,115 @@
+##############################################################################
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##############################################################################
+heat_template_version: 2013-05-23
+
+parameters:
+  public_network:
+    type: string
+    constraints:
+        - custom_constraint: neutron.network
+  agent_flavor:
+    type: string
+  agent_image:
+    type: string
+  volume_size:
+    type: number
+    description: Size of the volume to be created.
+    default: 1
+    constraints:
+      - range: { min: 1, max: 1024 }
+        description: must be between 1 and 1024 Gb.
+  agent_count:
+    type: number
+    default: 1
+    constraints:
+      - range: { min: 1, max: 512 }
+        description: must be between 1 and 512 agents.
+  availability_zone:
+    type: string
+    default: nova
+
+resources:
+  slaves:
+    type: OS::Heat::ResourceGroup
+    depends_on: [subnet, network_router_interface,
+      open_security_group, key_pair]
+    properties:
+      count: {get_param: agent_count}
+      resource_def: {
+        type: "agent.yaml",
+        properties: {
+          public_network: {get_param: public_network},
+          agent_network: {get_resource: network},
+          flavor: {get_param: agent_flavor},
+          image: {get_param: agent_image},
+          availability_zone: {get_param: availability_zone},
+          open_security_group: {get_resource: open_security_group},
+          key_name: {get_resource: key_pair},
+          volume_size: {get_param: volume_size}
+        }
+      }
+
+  network:
+        type: OS::Neutron::Net
+        properties:
+          name: network
+
+  subnet:
+        type: OS::Neutron::Subnet
+        properties:
+          network_id: { get_resource: network }
+          cidr: 172.16.0.0/16
+          gateway_ip: 172.16.0.1
+
+  network_router:
+        type: OS::Neutron::Router
+        properties:
+          external_gateway_info:
+                network: { get_param: public_network }
+
+  network_router_interface:
+        type: OS::Neutron::RouterInterface
+        properties:
+          router_id: { get_resource: network_router }
+          subnet_id: { get_resource: subnet }
+
+  key_pair:
+    type: OS::Nova::KeyPair
+    properties:
+      save_private_key: true
+      name: agent_keypair
+
+  open_security_group:
+    type: OS::Neutron::SecurityGroup
+    properties:
+      description: An open security group to allow all access to the slaves
+      rules:
+        - remote_ip_prefix: 0.0.0.0/0
+          protocol: tcp
+          port_range_min: 22
+          port_range_max: 22
+        - remote_ip_prefix: 0.0.0.0/0
+          protocol: icmp
+
+outputs:
+  slave_ips: {
+      description: "Slave addresses",
+      value: { get_attr: [ slaves, agent_ip] }
+  }
+  private_key:
+    description: "SSH Private Key"
+    value: { get_attr: [ key_pair, private_key ]}
diff --git a/snaps/openstack/tests/heat/agent.yaml b/snaps/openstack/tests/heat/agent.yaml
new file mode 100644 (file)
index 0000000..014b14f
--- /dev/null
@@ -0,0 +1,110 @@
+##############################################################################
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##############################################################################
+heat_template_version: 2013-05-23
+
+parameters:
+  flavor:
+    type: string
+    default: test
+  image:
+    type: string
+    default: 'Ubuntu 16.04'
+  key_name:
+    type: string
+    default: test_key
+  username:
+    type: string
+    default: test_user
+  open_security_group:
+    type: string
+  volume_size:
+    type: number
+    description: Size of the volume to be created.
+    default: 1
+    constraints:
+      - range: { min: 1, max: 1024 }
+        description: must be between 1 and 1024 Gb.
+  agent_network:
+    type: string
+    constraints:
+        - custom_constraint: neutron.network
+  public_network:
+    type: string
+    constraints:
+        - custom_constraint: neutron.network
+  availability_zone:
+    type: string
+    default: nova
+
+resources:
+  agent:
+    type: "OS::Nova::Server"
+    properties:
+      name: agent
+      image: { get_param: image }
+      flavor: { get_param: flavor }
+      key_name: { get_param: key_name }
+      networks:
+        - port: { get_resource: agent_port }
+      user_data: { get_resource: agent_config }
+      user_data_format: RAW
+      availability_zone: { get_param: availability_zone}
+
+  agent_config:
+    type: "OS::Heat::CloudConfig"
+    properties:
+      cloud_config:
+        users:
+        - name: { get_param: username }
+          groups: users
+          shell: /bin/bash
+          sudo: "ALL=(ALL) NOPASSWD:ALL"
+          ssh_authorized_keys:
+          - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEbnDiqZ8RjQJJzJPf074J41XlYED+zYBzaUZ5UkkUquXzymyUmoWaFBXJP+XPu4Ns44U/S8614+JxGk96tjUdJlIjL0Ag8HP6KLtTNCabucKcEASpgJIVWqJvE3E9upZLIEiTGsF8I8S67T2qq1J1uvtxyeZmyjm7NMamjyFXE53dhR2EHqSutyKK1CK74NkRY9wr3qWUIt35kLdKSVSfrr4gOOicDALbIRu77skHIvrjt+wK1VWphBdMg6ytuq5mIE6pjWAU3Gwl4aTxOU0z43ARzCLq8HVf8s/dKjYMj8plNqaIfceMbaEUqpNHv/xbvtGNG7N0aB/a4pkUQL07
+        - default
+        package_update: false
+        package_upgrade: false
+        manage_etc_hosts: localhost
+
+  agent_port:
+    type: "OS::Neutron::Port"
+    properties:
+      network_id: { get_param: agent_network }
+      security_groups:
+        - { get_param: open_security_group }
+
+  floating_ip:
+    type: OS::Neutron::FloatingIP
+    properties:
+      floating_network_id: { get_param: public_network }
+      port_id: { get_resource: agent_port }
+
+  agent_volume:
+    type: OS::Cinder::Volume
+    properties:
+      size: { get_param: volume_size }
+
+  agent_volume_att:
+    type: OS::Cinder::VolumeAttachment
+    properties:
+      instance_uuid: { get_resource: agent }
+      volume_id: { get_resource: agent_volume}
+
+outputs:
+  agent_ip:
+    description: The floating IP address of the agent on the public network
+    value: { get_attr: [ floating_ip, floating_ip_address ] }
@@ -1,4 +1,5 @@
-# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
+##############################################################################
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
 #                    and others.  All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
----
-- name: Configure NIC
-  hosts: all
-  become: yes
-  become_method: sudo
-  become_user: root
+##############################################################################
+heat_template_version: 2015-04-30
 
-  tasks:
-   - name: Setup /etc/sysconfig/network-scripts/ifcfg-eth1 file
-     action: template owner=root group=root mode=644 src=../templates/ifcfg-interface dest=/etc/sysconfig/network-scripts/ifcfg-{{nic_name}}
-   - name : Restart Network
-     command: systemctl restart network
\ No newline at end of file
+description: Simple template to deploy a single volume with encryption
+
+resources:
+  flavor:
+    type: OS::Nova::Flavor
+    properties:
+      ram: 1024
+      vcpus: 8
+      disk: 200
index e09515e..a191acc 100644 (file)
@@ -61,6 +61,11 @@ parameters:
     label: Keypair name
     description: The name of the stack's keypair
     default: keypair_name
+  security_group_name:
+    type: string
+    label: Security Group name
+    description: The name of the stack's security group
+    default: security_group_name
   inst1_name:
     type: string
     label: First VM name
@@ -116,6 +121,19 @@ resources:
       router: { get_resource: management_router }
       subnet: { get_resource: subnet }
 
+  server_security_group:
+    type: OS::Neutron::SecurityGroup
+    properties:
+      description: Add security group rules for server
+      name: { get_param: security_group_name }
+      rules:
+        - remote_ip_prefix: 0.0.0.0/0
+          protocol: tcp
+          port_range_min: 22
+          port_range_max: 22
+        - remote_ip_prefix: 0.0.0.0/0
+          protocol: icmp
+
   floating_ip:
     type: OS::Neutron::FloatingIP
     properties:
@@ -141,6 +159,7 @@ resources:
       image: { get_param: image1_name }
       flavor: { get_resource: flavor1 }
       key_name: {get_resource: keypair}
+      security_groups: [{ get_resource: server_security_group }]
       networks:
         - network: { get_resource: network }
 
diff --git a/snaps/openstack/tests/heat/keypair_heat_template.yaml b/snaps/openstack/tests/heat/keypair_heat_template.yaml
new file mode 100644 (file)
index 0000000..ffb8892
--- /dev/null
@@ -0,0 +1,39 @@
+##############################################################################
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##############################################################################
+heat_template_version: 2015-04-30
+
+description: >
+  Test template that simply deploys a keypair with a generated key
+
+parameters:
+  keypair_name:
+    type: string
+    label: Keypair name
+    description: The name of the stack's keypair
+    default: keypair_name
+
+resources:
+  keypair:
+    type: OS::Nova::KeyPair
+    properties:
+      name: { get_param: keypair_name }
+      save_private_key: True
+
+outputs:
+  private_key:
+    description: "SSH Private Key"
+    value: { get_attr: [ keypair, private_key ]}
diff --git a/snaps/openstack/tests/heat/router_heat_template.yaml b/snaps/openstack/tests/heat/router_heat_template.yaml
new file mode 100644 (file)
index 0000000..ee7f60c
--- /dev/null
@@ -0,0 +1,69 @@
+##############################################################################
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##############################################################################
+heat_template_version: 2015-04-30
+
+description: >
+  Sample template with two VMs instantiated against different images and
+  flavors on the same network and the first one has a floating IP
+
+parameters:
+  net_name:
+    type: string
+    label: Test network name
+    description: The name of the stack's network
+    default: test_net
+  subnet_name:
+    type: string
+    label: Test subnet name
+    description: The name of the stack's subnet
+    default: test_subnet
+  router_name:
+    type: string
+    label: Test router name
+    description: The name of the stack's router
+    default: mgmt_router
+  external_net_name:
+    type: string
+    description: Name of the external network which management network will connect to
+    default: external
+
+resources:
+  network:
+    type: OS::Neutron::Net
+    properties:
+      name: { get_param: net_name }
+
+  subnet:
+    type: OS::Neutron::Subnet
+    properties:
+      name: { get_param: subnet_name }
+      ip_version: 4
+      cidr: 10.1.2.0/24
+      network: { get_resource: network }
+
+  management_router:
+    type: OS::Neutron::Router
+    properties:
+      name: { get_param: router_name }
+      external_gateway_info:
+        network: { get_param: external_net_name }
+
+  management_router_interface:
+    type: OS::Neutron::RouterInterface
+    properties:
+      router: { get_resource: management_router }
+      subnet: { get_resource: subnet }
diff --git a/snaps/openstack/tests/heat/security_group_heat_template.yaml b/snaps/openstack/tests/heat/security_group_heat_template.yaml
new file mode 100644 (file)
index 0000000..0c4f07b
--- /dev/null
@@ -0,0 +1,45 @@
+##############################################################################
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##############################################################################
+heat_template_version: 2015-04-30
+
+description: >
+  Sample template for creating a single SecurityGroup
+
+parameters:
+  security_group_name:
+    type: string
+    label: Security Group name
+    description: The name of the stack's security group
+    default: security_group_name
+
+resources:
+  server_security_group:
+    type: OS::Neutron::SecurityGroup
+    properties:
+      description: Test description
+      name: { get_param: security_group_name }
+      rules:
+        - direction: egress
+          ethertype: IPv4
+          port_range_min: 22
+          port_range_max: 22
+          protocol: tcp
+          remote_ip_prefix: 0.0.0.0/0
+        - direction: ingress
+          ethertype: IPv4
+          protocol: icmp
+          remote_ip_prefix: 0.0.0.0/0
diff --git a/snaps/openstack/tests/heat/volume_heat_template.yaml b/snaps/openstack/tests/heat/volume_heat_template.yaml
new file mode 100644 (file)
index 0000000..1200476
--- /dev/null
@@ -0,0 +1,52 @@
+##############################################################################
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##############################################################################
+heat_template_version: 2015-04-30
+
+description: Simple template to deploy a single volume with encryption
+
+parameters:
+  volume_name:
+    type: string
+    label: Volume name
+    description: The name of the volume
+    default: test-vol-name
+  volume_type_name:
+    type: string
+    label: Volume Type name
+    description: The name of the volume type
+    default: test-vol-type-name
+
+resources:
+  volume_type:
+      type: OS::Cinder::VolumeType
+      properties:
+        name: { get_param: volume_type_name }
+
+#  encryption_vol_type:
+#    type: OS::Cinder::EncryptedVolumeType
+#    properties:
+#      provider: nova.volume.encryptors.luks.LuksEncryptor
+#      control_location: front-end
+#      volume_type: { get_resource: volume_type }
+
+  volume:
+    type: OS::Cinder::Volume
+    properties:
+      name: { get_param: volume_name }
+      size: 1
+#      volume_type: { get_resource: encryption_vol_type }
+      volume_type: { get_resource: volume_type }
index 16fb0b5..a3dec11 100644 (file)
@@ -17,9 +17,9 @@ import re
 
 import pkg_resources
 from snaps import file_utils
-from snaps.openstack.create_image import ImageSettings
-from snaps.openstack.create_network import NetworkSettings, SubnetSettings
-from snaps.openstack.create_router import RouterSettings
+from snaps.config.image import ImageConfig
+from snaps.config.network import NetworkConfig, SubnetConfig
+from snaps.config.router import RouterConfig
 from snaps.openstack.os_credentials import OSCreds, ProxySettings
 
 __author__ = 'spisarski'
@@ -81,7 +81,7 @@ def get_credentials(os_env_file=None, proxy_settings_str=None,
         elif config.get('OS_INSECURE'):
             https_cacert = False
 
-        interface = 'admin'
+        interface = 'public'
         if config.get('OS_INTERFACE'):
             interface = config.get('OS_INTERFACE')
 
@@ -171,32 +171,29 @@ def create_image_settings(image_name, image_user, image_format, metadata,
     logger.debug('Image metadata - ' + str(metadata))
 
     if metadata and 'config' in metadata:
-        return ImageSettings(**metadata['config'])
+        return ImageConfig(**metadata['config'])
 
     disk_file = None
-    if metadata:
+    if metadata and ('disk_url' in metadata or 'disk_file' in metadata):
         disk_url = metadata.get('disk_url')
         disk_file = metadata.get('disk_file')
     elif not disk_url:
         disk_url = default_url
-    else:
-        disk_url = disk_url
 
-    if metadata and \
-            ('kernel_file' in metadata or 'kernel_url' in metadata) and \
-            kernel_settings is None:
-        kernel_image_settings = ImageSettings(
+    if (metadata
+            and ('kernel_file' in metadata or 'kernel_url' in metadata)
+            and kernel_settings is None):
+        kernel_image_settings = ImageConfig(
             name=image_name + '-kernel', image_user=image_user,
-            img_format=image_format,
-            image_file=metadata.get('kernel_file'),
+            img_format=image_format, image_file=metadata.get('kernel_file'),
             url=metadata.get('kernel_url'), public=public)
     else:
         kernel_image_settings = kernel_settings
 
-    if metadata and \
-            ('ramdisk_file' in metadata or 'ramdisk_url' in metadata) and \
-            ramdisk_settings is None:
-        ramdisk_image_settings = ImageSettings(
+    if (metadata
+            and ('ramdisk_file' in metadata or 'ramdisk_url' in metadata)
+            and ramdisk_settings is None):
+        ramdisk_image_settings = ImageConfig(
             name=image_name + '-ramdisk', image_user=image_user,
             img_format=image_format,
             image_file=metadata.get('ramdisk_file'),
@@ -208,13 +205,13 @@ def create_image_settings(image_name, image_user, image_format, metadata,
     if metadata and 'extra_properties' in metadata:
         extra_properties = metadata['extra_properties']
 
-    return ImageSettings(name=image_name, image_user=image_user,
-                         img_format=image_format, image_file=disk_file,
-                         url=disk_url, extra_properties=extra_properties,
-                         kernel_image_settings=kernel_image_settings,
-                         ramdisk_image_settings=ramdisk_image_settings,
-                         public=public,
-                         nic_config_pb_loc=nic_config_pb_loc)
+    return ImageConfig(name=image_name, image_user=image_user,
+                       img_format=image_format, image_file=disk_file,
+                       url=disk_url, extra_properties=extra_properties,
+                       kernel_image_settings=kernel_image_settings,
+                       ramdisk_image_settings=ramdisk_image_settings,
+                       public=public,
+                       nic_config_pb_loc=nic_config_pb_loc)
 
 
 def cirros_image_settings(name=None, url=None, image_metadata=None,
@@ -248,8 +245,8 @@ def cirros_image_settings(name=None, url=None, image_metadata=None,
 
 
 def file_image_test_settings(name, file_path, image_user=CIRROS_USER):
-    return ImageSettings(name=name, image_user=image_user,
-                         img_format=DEFAULT_IMAGE_FORMAT, image_file=file_path)
+    return ImageConfig(name=name, image_user=image_user,
+                       img_format=DEFAULT_IMAGE_FORMAT, image_file=file_path)
 
 
 def centos_image_settings(name, url=None, image_metadata=None,
@@ -274,15 +271,12 @@ def centos_image_settings(name, url=None, image_metadata=None,
     else:
         metadata = image_metadata
 
-    pb_path = pkg_resources.resource_filename(
-        'snaps.provisioning.ansible_pb.centos-network-setup.playbooks',
-        'configure_host.yml')
     return create_image_settings(
         image_name=name, image_user=CENTOS_USER,
         image_format=DEFAULT_IMAGE_FORMAT, metadata=metadata, disk_url=url,
         default_url=CENTOS_DEFAULT_IMAGE_URL,
         kernel_settings=kernel_settings, ramdisk_settings=ramdisk_settings,
-        public=public, nic_config_pb_loc=pb_path)
+        public=public)
 
 
 def ubuntu_image_settings(name, url=None, image_metadata=None,
@@ -307,49 +301,65 @@ def ubuntu_image_settings(name, url=None, image_metadata=None,
     else:
         metadata = image_metadata
 
-    pb_path = pkg_resources.resource_filename(
-        'snaps.provisioning.ansible_pb.ubuntu-network-setup.playbooks',
-        'configure_host.yml')
     return create_image_settings(
         image_name=name, image_user=UBUNTU_USER,
         image_format=DEFAULT_IMAGE_FORMAT, metadata=metadata, disk_url=url,
         default_url=UBUNTU_DEFAULT_IMAGE_URL,
         kernel_settings=kernel_settings, ramdisk_settings=ramdisk_settings,
-        public=public, nic_config_pb_loc=pb_path)
+        public=public)
 
 
 def get_priv_net_config(net_name, subnet_name, router_name=None,
-                        cidr='10.55.0.0/24', external_net=None):
+                        cidr='10.55.0.0/24', external_net=None,
+                        netconf_override=None):
     return OSNetworkConfig(net_name, subnet_name, cidr, router_name,
-                           external_gateway=external_net)
+                           external_gateway=external_net,
+                           netconf_override=netconf_override)
 
 
 def get_pub_net_config(net_name, subnet_name=None, router_name=None,
-                       cidr='10.55.1.0/24', external_net=None):
+                       cidr='10.55.1.0/24', external_net=None,
+                       netconf_override=None):
     return OSNetworkConfig(net_name, subnet_name, cidr, router_name,
-                           external_gateway=external_net)
+                           external_gateway=external_net,
+                           netconf_override=netconf_override)
 
 
 class OSNetworkConfig:
     """
     Represents the settings required for the creation of a network in OpenStack
+    where netconf_override is used to reconfigure the network_type,
+    physical_network and segmentation_id
     """
 
     def __init__(self, net_name, subnet_name=None, subnet_cidr=None,
-                 router_name=None, external_gateway=None):
-
+                 router_name=None, external_gateway=None,
+                 netconf_override=None):
+        """
+        :param netconf_override: dict() containing the reconfigured network_type,
+                                 physical_network and segmentation_id
+        """
+
+        network_conf = None
         if subnet_name and subnet_cidr:
-            self.network_settings = NetworkSettings(
+            network_conf = NetworkConfig(
                 name=net_name, subnet_settings=[
-                    SubnetSettings(cidr=subnet_cidr, name=subnet_name)])
+                    SubnetConfig(cidr=subnet_cidr, name=subnet_name)])
         else:
-            self.network_settings = NetworkSettings(name=net_name)
+            network_conf = NetworkConfig(name=net_name)
+        if netconf_override:
+            network_conf.network_type = netconf_override.get('network_type')
+            network_conf.physical_network = netconf_override.get(
+                'physical_network')
+            network_conf.segmentation_id = netconf_override.get(
+                'segmentation_id')
+        self.network_settings = network_conf
 
         if router_name:
             if subnet_name:
-                self.router_settings = RouterSettings(
+                self.router_settings = RouterConfig(
                     name=router_name, external_gateway=external_gateway,
                     internal_subnets=[subnet_name])
             else:
-                self.router_settings = RouterSettings(
+                self.router_settings = RouterConfig(
                     name=router_name, external_gateway=external_gateway)
index 1617f91..7e910a4 100644 (file)
 # limitations under the License.
 import logging
 import pkg_resources
-import requests
-from requests.packages.urllib3.exceptions import InsecureRequestWarning
 import uuid
 import unittest
 
 from snaps import file_utils
-from snaps.openstack.create_project import ProjectSettings
-from snaps.openstack.create_user import UserSettings
+from snaps.config.project import ProjectConfig
+from snaps.config.user import UserConfig
 from snaps.openstack.tests import openstack_tests
 from snaps.openstack.utils import deploy_utils, keystone_utils
 
@@ -29,8 +27,6 @@ from snaps.openstack.utils import deploy_utils, keystone_utils
 dev_os_env_file = pkg_resources.resource_filename(
     'snaps.openstack.tests.conf', 'os_env.yaml')
 
-requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
-
 
 class OSComponentTestCase(unittest.TestCase):
 
@@ -84,7 +80,7 @@ class OSIntegrationTestCase(OSComponentTestCase):
 
     def __init__(self, method_name='runTest', os_creds=None, ext_net_name=None,
                  use_keystone=True, flavor_metadata=None, image_metadata=None,
-                 log_level=logging.DEBUG):
+                 netconf_override=None, log_level=logging.DEBUG):
         """
         Super for integration tests requiring a connection to OpenStack
         :param method_name: default 'runTest'
@@ -102,12 +98,15 @@ class OSIntegrationTestCase(OSComponentTestCase):
                         'ramdisk_url': '{URI}/cirros-0.3.4-x86_64-initramfs'})
         :param flavor_metadata: dict() to be sent directly into the Nova client
                                 generally used for page sizes
+        :param netconf_override: dict() containing the configured network_type,
+                               physical_network and segmentation_id
         :param log_level: the logging level of your test run (default DEBUG)
         """
         super(OSIntegrationTestCase, self).__init__(
             method_name=method_name, os_creds=os_creds,
             ext_net_name=ext_net_name, image_metadata=image_metadata,
             log_level=log_level)
+        self.netconf_override = netconf_override
         self.use_keystone = use_keystone
         self.keystone = None
         self.flavor_metadata = flavor_metadata
@@ -115,7 +114,8 @@ class OSIntegrationTestCase(OSComponentTestCase):
     @staticmethod
     def parameterize(testcase_klass, os_creds, ext_net_name,
                      use_keystone=False, flavor_metadata=None,
-                     image_metadata=None, log_level=logging.DEBUG):
+                     image_metadata=None, netconf_override=None,
+                     log_level=logging.DEBUG):
         """
         Create a suite containing all tests taken from the given
         subclass, passing them the parameter 'param'.
@@ -126,7 +126,8 @@ class OSIntegrationTestCase(OSComponentTestCase):
         for name in test_names:
             suite.addTest(testcase_klass(name, os_creds, ext_net_name,
                                          use_keystone, flavor_metadata,
-                                         image_metadata, log_level))
+                                         image_metadata, netconf_override,
+                                         log_level))
         return suite
 
     """
@@ -149,12 +150,12 @@ class OSIntegrationTestCase(OSComponentTestCase):
             guid = self.__class__.__name__ + '-' + str(uuid.uuid4())[:-19]
             project_name = guid + '-proj'
             self.project_creator = deploy_utils.create_project(
-                self.admin_os_creds, ProjectSettings(
+                self.admin_os_creds, ProjectConfig(
                     name=project_name,
                     domain=self.admin_os_creds.project_domain_name))
 
             self.user_creator = deploy_utils.create_user(
-                self.admin_os_creds, UserSettings(
+                self.admin_os_creds, UserConfig(
                     name=guid + '-user', password=guid,
                     project_name=project_name, roles={
                         'admin': self.project_creator.project_settings.name},
index d13277d..c50a166 100644 (file)
@@ -17,7 +17,8 @@ import logging
 from cinderclient.client import Client
 from cinderclient.exceptions import NotFound
 
-from snaps.domain.volume import QoSSpec, VolumeType, VolumeTypeEncryption
+from snaps.domain.volume import (
+    QoSSpec, VolumeType, VolumeTypeEncryption, Volume)
 from snaps.openstack.utils import keystone_utils
 
 __author__ = 'spisarski'
@@ -42,6 +43,98 @@ def cinder_client(os_creds):
                   region_name=os_creds.region_name)
 
 
+def get_volume(cinder, volume_name=None, volume_settings=None):
+    """
+    Returns an OpenStack volume object for a given name
+    :param cinder: the Cinder client
+    :param volume_name: the volume name to lookup
+    :param volume_settings: the volume settings used for lookups
+    :return: the volume object or None
+    """
+    if volume_settings:
+        volume_name = volume_settings.name
+
+    volumes = cinder.volumes.list()
+    for volume in volumes:
+        if volume.name == volume_name:
+            return Volume(
+                name=volume.name, volume_id=volume.id,
+                description=volume.description, size=volume.size,
+                vol_type=volume.volume_type,
+                availability_zone=volume.availability_zone,
+                multi_attach=volume.multiattach,
+                attachments=volume.attachments)
+
+
+def __get_os_volume_by_id(cinder, volume_id):
+    """
+    Returns an OpenStack volume object for a given name
+    :param cinder: the Cinder client
+    :param volume_id: the volume ID to lookup
+    :return: the SNAPS-OO Domain Volume object or None
+    """
+    return cinder.volumes.get(volume_id)
+
+
+def get_volume_by_id(cinder, volume_id):
+    """
+    Returns an OpenStack volume object for a given name
+    :param cinder: the Cinder client
+    :param volume_id: the volume ID to lookup
+    :return: the SNAPS-OO Domain Volume object or None
+    """
+    volume = __get_os_volume_by_id(cinder, volume_id)
+    return Volume(
+        name=volume.name, volume_id=volume.id, description=volume.description,
+        size=volume.size, vol_type=volume.volume_type,
+        availability_zone=volume.availability_zone,
+        multi_attach=volume.multiattach, attachments=volume.attachments)
+
+
+def get_volume_status(cinder, volume):
+    """
+    Returns a new OpenStack Volume object for a given OpenStack volume object
+    :param cinder: the Cinder client
+    :param volume: the domain Volume object
+    :return: the OpenStack Volume object
+    """
+    os_volume = cinder.volumes.get(volume.id)
+    return os_volume.status
+
+
+def create_volume(cinder, volume_settings):
+    """
+    Creates and returns OpenStack volume object with an external URL
+    :param cinder: the cinder client
+    :param volume_settings: the volume settings object
+    :return: the OpenStack volume object
+    :raise Exception if using a file and it cannot be found
+    """
+    volume = cinder.volumes.create(
+        name=volume_settings.name, description=volume_settings.description,
+        size=volume_settings.size, imageRef=volume_settings.image_name,
+        volume_type=volume_settings.type_name,
+        availability_zone=volume_settings.availability_zone,
+        multiattach=volume_settings.multi_attach)
+
+    return Volume(
+        name=volume.name, volume_id=volume.id,
+        description=volume.description,
+        size=volume.size, vol_type=volume.volume_type,
+        availability_zone=volume.availability_zone,
+        multi_attach=volume.multiattach, attachments=volume.attachments)
+
+
+def delete_volume(cinder, volume):
+    """
+    Deletes an volume from OpenStack
+    :param cinder: the cinder client
+    :param volume: the volume to delete
+    """
+    logger.info('Deleting volume named - %s', volume.name)
+    return cinder.volumes.delete(volume.id)
+
+
 def get_volume_type(cinder, volume_type_name=None, volume_type_settings=None):
     """
     Returns an OpenStack volume type object for a given name
index c936c1f..8cd6dd3 100644 (file)
@@ -78,7 +78,7 @@ def create_router(os_creds, router_settings, cleanup=False):
     """
     Creates a network on which the CMTSs can attach
     :param os_creds: The OpenStack credentials object
-    :param router_settings: The RouterSettings instance
+    :param router_settings: The RouterConfig instance
     :param cleanup: Denotes whether or not this is being called for cleanup
     :return: A reference to the network creator objects for each network from
              which network elements such as the subnet, router, interface
@@ -103,7 +103,7 @@ def create_keypair(os_creds, keypair_settings, cleanup=False):
     """
     Creates a keypair that can be applied to an instance
     :param os_creds: The OpenStack credentials object
-    :param keypair_settings: The KeypairSettings object
+    :param keypair_settings: The KeypairConfig object
     :param cleanup: Denotes whether or not this is being called for cleanup
     :return: A reference to the keypair creator object
     """
@@ -121,7 +121,7 @@ def create_vm_instance(os_creds, instance_settings, image_settings,
     """
     Creates a VM instance
     :param os_creds: The OpenStack credentials
-    :param instance_settings: Instance of VmInstanceSettings
+    :param instance_settings: Instance of VmInstanceConfig
     :param image_settings: The object containing image settings
     :param keypair_creator: The object responsible for creating the keypair
                             associated with this VM instance. (optional)
index 8b9395b..e440717 100644 (file)
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
+import os
 
 import yaml
 from heatclient.client import Client
@@ -23,7 +24,8 @@ from oslo_serialization import jsonutils
 from snaps import file_utils
 from snaps.domain.stack import Stack, Resource, Output
 
-from snaps.openstack.utils import keystone_utils, neutron_utils, nova_utils
+from snaps.openstack.utils import (
+    keystone_utils, neutron_utils, nova_utils, cinder_utils)
 
 __author__ = 'spisarski'
 
@@ -36,7 +38,7 @@ def heat_client(os_creds):
     :param os_creds: the OpenStack credentials
     :return: the client
     """
-    logger.debug('Retrieving Nova Client')
+    logger.debug('Retrieving Heat Client')
     return Client(os_creds.heat_api_version,
                   session=keystone_utils.keystone_session(os_creds),
                   region_name=os_creds.region_name)
@@ -116,6 +118,16 @@ def create_stack(heat_cli, stack_settings):
     if stack_settings.env_values:
         args['parameters'] = stack_settings.env_values
 
+    if stack_settings.resource_files:
+        resources = dict()
+        for res_file in stack_settings.resource_files:
+            heat_resource_contents = file_utils.read_file(res_file)
+            base_filename = os.path.basename(res_file)
+
+            if heat_resource_contents and base_filename:
+                resources[base_filename] = heat_resource_contents
+        args['files'] = resources
+
     stack = heat_cli.stacks.create(**args)
 
     return get_stack_by_id(heat_cli, stack_id=stack['stack']['id'])
@@ -130,30 +142,37 @@ def delete_stack(heat_cli, stack):
     heat_cli.stacks.delete(stack.id)
 
 
-def __get_os_resources(heat_cli, stack):
+def __get_os_resources(heat_cli, res_id):
     """
     Returns all of the OpenStack resource objects for a given stack
     :param heat_cli: the OpenStack heat client
-    :param stack: the SNAPS-OO Stack domain object
+    :param res_id: the resource ID
     :return: a list
     """
-    return heat_cli.resources.list(stack.id)
+    return heat_cli.resources.list(res_id)
 
 
-def get_resources(heat_cli, stack):
+def get_resources(heat_cli, res_id, res_type=None):
     """
     Returns all of the OpenStack resource objects for a given stack
     :param heat_cli: the OpenStack heat client
-    :param stack: the SNAPS-OO Stack domain object
-    :return: a list
+    :param res_id: the SNAPS-OO Stack domain object
+    :param res_type: the type name to filter
+    :return: a list of Resource domain objects
     """
-    os_resources = __get_os_resources(heat_cli, stack)
+    os_resources = __get_os_resources(heat_cli, res_id)
 
     if os_resources:
         out = list()
         for os_resource in os_resources:
-            out.append(Resource(resource_type=os_resource.resource_type,
-                                resource_id=os_resource.physical_resource_id))
+            if ((res_type and os_resource.resource_type == res_type)
+                    or not res_type):
+                out.append(Resource(
+                    name=os_resource.resource_name,
+                    resource_type=os_resource.resource_type,
+                    resource_id=os_resource.physical_resource_id,
+                    status=os_resource.resource_status,
+                    status_reason=os_resource.resource_status_reason))
         return out
 
 
@@ -163,7 +182,7 @@ def get_outputs(heat_cli, stack):
     for given stack
     :param heat_cli: the OpenStack heat client
     :param stack: the SNAPS-OO Stack domain object
-    :return: a list
+    :return: a list of Output domain objects
     """
     out = list()
 
@@ -182,46 +201,183 @@ def get_outputs(heat_cli, stack):
 
 def get_stack_networks(heat_cli, neutron, stack):
     """
-    Returns an instance of NetworkSettings for each network owned by this stack
+    Returns a list of Network domain objects deployed by this stack
     :param heat_cli: the OpenStack heat client object
     :param neutron: the OpenStack neutron client object
     :param stack: the SNAPS-OO Stack domain object
-    :return: a list of NetworkSettings
+    :return: a list of Network objects
     """
 
     out = list()
-    resources = get_resources(heat_cli, stack)
+    resources = get_resources(heat_cli, stack.id, 'OS::Neutron::Net')
     for resource in resources:
-        if resource.type == 'OS::Neutron::Net':
-            network = neutron_utils.get_network_by_id(
-                neutron, resource.id)
-            if network:
-                out.append(network)
+        network = neutron_utils.get_network_by_id(neutron, resource.id)
+        if network:
+            out.append(network)
 
     return out
 
 
-def get_stack_servers(heat_cli, nova, stack):
+def get_stack_routers(heat_cli, neutron, stack):
     """
-    Returns an instance of NetworkSettings for each network owned by this stack
+    Returns a list of Network domain objects deployed by this stack
     :param heat_cli: the OpenStack heat client object
-    :param nova: the OpenStack nova client object
+    :param neutron: the OpenStack neutron client object
     :param stack: the SNAPS-OO Stack domain object
-    :return: a list of NetworkSettings
+    :return: a list of Network objects
     """
 
     out = list()
-    resources = get_resources(heat_cli, stack)
+    resources = get_resources(heat_cli, stack.id, 'OS::Neutron::Router')
     for resource in resources:
-        if resource.type == 'OS::Nova::Server':
-            try:
+        router = neutron_utils.get_router_by_id(neutron, resource.id)
+        if router:
+            out.append(router)
+
+    return out
+
+
+def get_stack_security_groups(heat_cli, neutron, stack):
+    """
+    Returns a list of SecurityGroup domain objects deployed by this stack
+    :param heat_cli: the OpenStack heat client object
+    :param neutron: the OpenStack neutron client object
+    :param stack: the SNAPS-OO Stack domain object
+    :return: a list of SecurityGroup objects
+    """
+
+    out = list()
+    resources = get_resources(heat_cli, stack.id, 'OS::Neutron::SecurityGroup')
+    for resource in resources:
+        security_group = neutron_utils.get_security_group_by_id(
+            neutron, resource.id)
+        if security_group:
+            out.append(security_group)
+
+    return out
+
+
+def get_stack_servers(heat_cli, nova, neutron, stack):
+    """
+    Returns a list of VMInst domain objects associated with a Stack
+    :param heat_cli: the OpenStack heat client object
+    :param nova: the OpenStack nova client object
+    :param neutron: the OpenStack neutron client object
+    :param stack: the SNAPS-OO Stack domain object
+    :return: a list of VMInst domain objects
+    """
+
+    out = list()
+    srvr_res = get_resources(heat_cli, stack.id, 'OS::Nova::Server')
+    for resource in srvr_res:
+        try:
+            server = nova_utils.get_server_object_by_id(
+                nova, neutron, resource.id)
+            if server:
+                out.append(server)
+        except NotFound:
+            logger.warn('VmInst cannot be located with ID %s', resource.id)
+
+    res_grps = get_resources(heat_cli, stack.id, 'OS::Heat::ResourceGroup')
+    for res_grp in res_grps:
+        res_ress = get_resources(heat_cli, res_grp.id)
+        for res_res in res_ress:
+            res_res_srvrs = get_resources(
+                heat_cli, res_res.id, 'OS::Nova::Server')
+            for res_srvr in res_res_srvrs:
                 server = nova_utils.get_server_object_by_id(
-                    nova, resource.id)
+                    nova, neutron, res_srvr.id)
                 if server:
                     out.append(server)
-            except NotFound:
-                logger.warn(
-                    'VmInst cannot be located with ID %s', resource.id)
+
+    return out
+
+
+def get_stack_keypairs(heat_cli, nova, stack):
+    """
+    Returns a list of Keypair domain objects associated with a Stack
+    :param heat_cli: the OpenStack heat client object
+    :param nova: the OpenStack nova client object
+    :param stack: the SNAPS-OO Stack domain object
+    :return: a list of VMInst domain objects
+    """
+
+    out = list()
+    resources = get_resources(heat_cli, stack.id, 'OS::Nova::KeyPair')
+    for resource in resources:
+        try:
+            keypair = nova_utils.get_keypair_by_id(nova, resource.id)
+            if keypair:
+                out.append(keypair)
+        except NotFound:
+            logger.warn('Keypair cannot be located with ID %s', resource.id)
+
+    return out
+
+
+def get_stack_volumes(heat_cli, cinder, stack):
+    """
+    Returns an instance of Volume domain objects created by this stack
+    :param heat_cli: the OpenStack heat client object
+    :param cinder: the OpenStack cinder client object
+    :param stack: the SNAPS-OO Stack domain object
+    :return: a list of Volume domain objects
+    """
+
+    out = list()
+    resources = get_resources(heat_cli, stack.id, 'OS::Cinder::Volume')
+    for resource in resources:
+        try:
+            server = cinder_utils.get_volume_by_id(cinder, resource.id)
+            if server:
+                out.append(server)
+        except NotFound:
+            logger.warn('Volume cannot be located with ID %s', resource.id)
+
+    return out
+
+
+def get_stack_volume_types(heat_cli, cinder, stack):
+    """
+    Returns an instance of VolumeType domain objects created by this stack
+    :param heat_cli: the OpenStack heat client object
+    :param cinder: the OpenStack cinder client object
+    :param stack: the SNAPS-OO Stack domain object
+    :return: a list of VolumeType domain objects
+    """
+
+    out = list()
+    resources = get_resources(heat_cli, stack.id, 'OS::Cinder::VolumeType')
+    for resource in resources:
+        try:
+            vol_type = cinder_utils.get_volume_type_by_id(cinder, resource.id)
+            if vol_type:
+                out.append(vol_type)
+        except NotFound:
+            logger.warn('VolumeType cannot be located with ID %s', resource.id)
+
+    return out
+
+
+def get_stack_flavors(heat_cli, nova, stack):
+    """
+    Returns an instance of Flavor SNAPS domain object for each flavor created
+    by this stack
+    :param heat_cli: the OpenStack heat client object
+    :param nova: the OpenStack cinder client object
+    :param stack: the SNAPS-OO Stack domain object
+    :return: a list of Volume domain objects
+    """
+
+    out = list()
+    resources = get_resources(heat_cli, stack.id, 'OS::Nova::Flavor')
+    for resource in resources:
+        try:
+            flavor = nova_utils.get_flavor_by_id(nova, resource.id)
+            if flavor:
+                out.append(flavor)
+        except NotFound:
+            logger.warn('Flavor cannot be located with ID %s', resource.id)
 
     return out
 
index 46f6fb8..b8769c0 100644 (file)
@@ -114,7 +114,7 @@ def get_project(keystone=None, os_creds=None, project_settings=None,
     :param keystone: the Keystone client
     :param os_creds: the OpenStack credentials used to obtain the Keystone
                      client if the keystone parameter is None
-    :param project_settings: a ProjectSettings object
+    :param project_settings: a ProjectConfig object
     :param project_name: the name to query
     :return: the SNAPS-OO Project domain object or None
     """
@@ -367,7 +367,7 @@ def grant_user_role_to_project(keystone, role, user, project):
     """
 
     os_role = get_role_by_id(keystone, role.id)
-    logger.info('Granting role %s to project %s', role.name, project)
+    logger.info('Granting role %s to project %s', role.name, project.name)
     if keystone.version == V2_VERSION_STR:
         keystone.roles.add_user_role(user, os_role, tenant=project)
     else:
diff --git a/snaps/openstack/utils/launch_utils.py b/snaps/openstack/utils/launch_utils.py
new file mode 100644 (file)
index 0000000..e10cf48
--- /dev/null
@@ -0,0 +1,815 @@
+#
+# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# This utility makes it easy to create OpenStack objects
+import logging
+import re
+import socket
+import struct
+
+import os
+import time
+from keystoneauth1.exceptions import Unauthorized
+
+from snaps.config.flavor import FlavorConfig
+from snaps.config.image import ImageConfig
+from snaps.config.keypair import KeypairConfig
+from snaps.config.network import PortConfig, NetworkConfig
+from snaps.config.project import ProjectConfig
+from snaps.config.qos import QoSConfig
+from snaps.config.router import RouterConfig
+from snaps.config.security_group import SecurityGroupConfig
+from snaps.config.user import UserConfig
+from snaps.config.vm_inst import VmInstanceConfig
+from snaps.config.volume import VolumeConfig
+from snaps.config.volume_type import VolumeTypeConfig
+from snaps.openstack.create_flavor import OpenStackFlavor
+from snaps.openstack.create_image import OpenStackImage
+from snaps.openstack.create_keypairs import OpenStackKeypair
+from snaps.openstack.create_network import OpenStackNetwork
+from snaps.openstack.create_project import OpenStackProject
+from snaps.openstack.create_qos import OpenStackQoS
+from snaps.openstack.create_router import OpenStackRouter
+from snaps.openstack.create_security_group import OpenStackSecurityGroup
+from snaps.openstack.create_user import OpenStackUser
+from snaps.openstack.create_volume import OpenStackVolume
+from snaps.openstack.create_volume_type import OpenStackVolumeType
+from snaps.openstack.os_credentials import OSCreds, ProxySettings
+from snaps.openstack.utils import deploy_utils, neutron_utils
+from snaps.provisioning import ansible_utils
+
+logger = logging.getLogger('lanuch_utils')
+DEFAULT_CREDS_KEY = 'admin'
+
+
+def launch_config(config, tmplt_file, deploy, clean, clean_image):
+    """
+    Launches all objects and applies any configured ansible playbooks
+    :param config: the environment configuration dict object
+    :param tmplt_file: the path to the SNAPS-OO template file
+    :param deploy: when True deploy
+    :param clean: when True clean
+    :param clean_image: when True clean the image when clean is True
+    """
+    os_config = config.get('openstack')
+
+    creators = list()
+    vm_dict = dict()
+    images_dict = dict()
+    flavors_dict = dict()
+    networks_dict = dict()
+    routers_dict = dict()
+    os_creds_dict = dict()
+
+    if os_config:
+        os_creds_dict = __get_creds_dict(os_config)
+
+        # Create projects
+        projects_dict = __create_instances(
+            os_creds_dict, OpenStackProject, ProjectConfig,
+            os_config.get('projects'), 'project', clean)
+        creators.append(projects_dict)
+
+        # Create users
+        users_dict = __create_instances(
+            os_creds_dict, OpenStackUser, UserConfig,
+            os_config.get('users'), 'user', clean)
+        creators.append(users_dict)
+
+        # Associate new users to projects
+        if not clean:
+            for project_creator in projects_dict.values():
+                users = project_creator.project_settings.users
+                for user_name in users:
+                    user_creator = users_dict.get(user_name)
+                    if user_creator:
+                        project_creator.assoc_user(
+                            user_creator.get_user())
+
+        # Create flavors
+        flavors_dict = __create_instances(
+            os_creds_dict, OpenStackFlavor, FlavorConfig,
+            os_config.get('flavors'), 'flavor', clean, users_dict)
+        creators.append(flavors_dict)
+
+        # Create QoS specs
+        qos_dict = __create_instances(
+            os_creds_dict, OpenStackQoS, QoSConfig,
+            os_config.get('qos_specs'), 'qos_spec', clean, users_dict)
+        creators.append(qos_dict)
+
+        # Create volume types
+        vol_type_dict = __create_instances(
+            os_creds_dict, OpenStackVolumeType, VolumeTypeConfig,
+            os_config.get('volume_types'), 'volume_type', clean,
+            users_dict)
+        creators.append(vol_type_dict)
+
+        # Create volume types
+        vol_dict = __create_instances(
+            os_creds_dict, OpenStackVolume, VolumeConfig,
+            os_config.get('volumes'), 'volume', clean, users_dict)
+        creators.append(vol_dict)
+
+        # Create images
+        images_dict = __create_instances(
+            os_creds_dict, OpenStackImage, ImageConfig,
+            os_config.get('images'), 'image', clean, users_dict)
+        creators.append(images_dict)
+
+        # Create networks
+        networks_dict = __create_instances(
+            os_creds_dict, OpenStackNetwork, NetworkConfig,
+            os_config.get('networks'), 'network', clean, users_dict)
+        creators.append(networks_dict)
+
+        # Create routers
+        routers_dict = __create_instances(
+            os_creds_dict, OpenStackRouter, RouterConfig,
+            os_config.get('routers'), 'router', clean, users_dict)
+        creators.append(routers_dict)
+
+        # Create keypairs
+        keypairs_dict = __create_instances(
+            os_creds_dict, OpenStackKeypair, KeypairConfig,
+            os_config.get('keypairs'), 'keypair', clean, users_dict)
+        creators.append(keypairs_dict)
+
+        # Create security groups
+        creators.append(__create_instances(
+            os_creds_dict, OpenStackSecurityGroup,
+            SecurityGroupConfig,
+            os_config.get('security_groups'), 'security_group', clean,
+            users_dict))
+
+        # Create instance
+        vm_dict = __create_vm_instances(
+            os_creds_dict, users_dict, os_config.get('instances'),
+            images_dict, keypairs_dict, clean)
+        creators.append(vm_dict)
+        logger.info(
+            'Completed creating/retrieving all configured instances')
+
+    # Must enter either block
+    if clean:
+        # Cleanup Environment
+        __cleanup(creators, clean_image)
+    elif deploy:
+        # Provision VMs
+        ansible_config = config.get('ansible')
+        if ansible_config and vm_dict:
+            if not __apply_ansible_playbooks(
+                    ansible_config, os_creds_dict, vm_dict, images_dict,
+                    flavors_dict, networks_dict, routers_dict, tmplt_file):
+                logger.error("Problem applying ansible playbooks")
+
+
+def __get_creds_dict(os_conn_config):
+    """
+    Returns a dict of OSCreds where the key is the creds name.
+    For backwards compatibility, credentials not contained in a list (only
+    one) will be returned with the key of None
+    :param os_conn_config: the credential configuration
+    :return: a dict of OSCreds objects
+    """
+    if 'connection' in os_conn_config:
+        return {DEFAULT_CREDS_KEY: __get_os_credentials(os_conn_config)}
+    elif 'connections' in os_conn_config:
+        out = dict()
+        for os_conn_dict in os_conn_config['connections']:
+            config = os_conn_dict.get('connection')
+            if not config:
+                raise Exception('Invalid connection format')
+
+            name = config.get('name')
+            if not name:
+                raise Exception('Connection config requires a name field')
+
+            out[name] = __get_os_credentials(os_conn_dict)
+        return out
+
+
+def __get_creds(os_creds_dict, os_user_dict, inst_config):
+    """
+    Returns the appropriate credentials
+    :param os_creds_dict: a dictionary of OSCreds objects where the name is the
+                          key
+    :param os_user_dict: a dictionary of OpenStackUser objects where the name
+                         is the key
+    :param inst_config:
+    :return: an OSCreds instance or None
+    """
+    os_creds = os_creds_dict.get(DEFAULT_CREDS_KEY)
+    if 'os_user' in inst_config:
+        os_user_conf = inst_config['os_user']
+        if 'name' in os_user_conf:
+            user_creator = os_user_dict.get(os_user_conf['name'])
+            if user_creator:
+                return user_creator.get_os_creds(
+                    project_name=os_user_conf.get('project_name'))
+    elif 'os_creds_name' in inst_config:
+        if 'os_creds_name' in inst_config:
+            os_creds = os_creds_dict[inst_config['os_creds_name']]
+    return os_creds
+
+
+def __get_os_credentials(os_conn_config):
+    """
+    Returns an object containing all of the information required to access
+    OpenStack APIs
+    :param os_conn_config: The configuration holding the credentials
+    :return: an OSCreds instance
+    """
+    config = os_conn_config.get('connection')
+    if not config:
+        raise Exception('Invalid connection configuration')
+
+    proxy_settings = None
+    http_proxy = config.get('http_proxy')
+    if http_proxy:
+        tokens = re.split(':', http_proxy)
+        ssh_proxy_cmd = config.get('ssh_proxy_cmd')
+        proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
+                                       ssh_proxy_cmd=ssh_proxy_cmd)
+    else:
+        if 'proxy_settings' in config:
+            host = config['proxy_settings'].get('host')
+            port = config['proxy_settings'].get('port')
+            if host and host != 'None' and port and port != 'None':
+                proxy_settings = ProxySettings(**config['proxy_settings'])
+
+    if proxy_settings:
+        config['proxy_settings'] = proxy_settings
+    else:
+        if config.get('proxy_settings'):
+            del config['proxy_settings']
+
+    return OSCreds(**config)
+
+
+def __parse_ports_config(config):
+    """
+    Parses the "ports" configuration
+    :param config: The dictionary to parse
+    :return: a list of PortConfig objects
+    """
+    out = list()
+    for port_config in config:
+        out.append(PortConfig(**port_config.get('port')))
+    return out
+
+
+def __create_instances(os_creds_dict, creator_class, config_class, config,
+                       config_key, cleanup=False, os_users_dict=None):
+    """
+    Returns a dictionary of SNAPS creator objects where the key is the name
+    :param os_creds_dict: Dictionary of OSCreds objects where the key is the
+                          name
+    :param config: The list of configurations for the same type
+    :param config_key: The list of configurations for the same type
+    :param cleanup: Denotes whether or not this is being called for cleanup
+    :return: dictionary
+    """
+    out = {}
+
+    if config:
+        for config_dict in config:
+            inst_config = config_dict.get(config_key)
+            if inst_config:
+                creds = __get_creds(os_creds_dict, os_users_dict, inst_config)
+                if creds:
+                    creator = creator_class(
+                        creds,
+                        config_class(**inst_config))
+
+                    if creator:
+                        if cleanup:
+                            try:
+                                creator.initialize()
+                            except Unauthorized as e:
+                                logger.warn(
+                                    'Unable to initialize creator [%s] - %s',
+                                    creator, e)
+                        else:
+                            creator.create()
+
+                        out[inst_config['name']] = creator
+
+        logger.info('Initialized configured %ss', config_key)
+
+    return out
+
+
+def __create_vm_instances(os_creds_dict, os_users_dict, instances_config,
+                          image_dict, keypairs_dict, cleanup=False):
+    """
+    Returns a dictionary of OpenStackVmInstance objects where the key is the
+    instance name
+    :param os_creds_dict: Dictionary of OSCreds objects where the key is the
+                          name
+    :param os_users_dict: Dictionary of OpenStackUser objects where the key is
+                          the username
+    :param instances_config: The list of VM instance configurations
+    :param image_dict: A dictionary of images that will probably be used to
+                       instantiate the VM instance
+    :param keypairs_dict: A dictionary of keypairs that will probably be used
+                          to instantiate the VM instance
+    :param cleanup: Denotes whether or not this is being called for cleanup
+    :return: dictionary
+    """
+    vm_dict = {}
+
+    if instances_config:
+        for instance_config in instances_config:
+            conf = instance_config.get('instance')
+            if conf:
+                if image_dict:
+                    image_creator = image_dict.get(conf.get('imageName'))
+                    if image_creator:
+                        instance_settings = VmInstanceConfig(
+                            **instance_config['instance'])
+                        kp_creator = keypairs_dict.get(
+                            conf.get('keypair_name'))
+
+                        try:
+                            vm_dict[conf[
+                                'name']] = deploy_utils.create_vm_instance(
+                                __get_creds(
+                                    os_creds_dict, os_users_dict, conf),
+                                instance_settings,
+                                image_creator.image_settings,
+                                keypair_creator=kp_creator,
+                                init_only=cleanup)
+                        except Unauthorized as e:
+                            if not cleanup:
+                                logger.warn('Unable to initialize VM - %s', e)
+                                raise
+                    else:
+                        raise Exception('Image creator instance not found.'
+                                        ' Cannot instantiate')
+                else:
+                    if not cleanup:
+                        raise Exception('Image dictionary is None. Cannot '
+                                        'instantiate')
+            else:
+                raise Exception('Instance configuration is None. Cannot '
+                                'instantiate')
+        logger.info('Created configured instances')
+
+    return vm_dict
+
+
+def __apply_ansible_playbooks(ansible_configs, os_creds_dict, vm_dict,
+                              image_dict, flavor_dict, networks_dict,
+                              routers_dict, tmplt_file):
+    """
+    Applies ansible playbooks to running VMs with floating IPs
+    :param ansible_configs: a list of Ansible configurations
+    :param os_creds_dict: Dictionary of OSCreds objects where the key is the
+                          name
+    :param vm_dict: the dictionary of newly instantiated VMs where the name is
+                    the key
+    :param image_dict: the dictionary of newly instantiated images where the
+                       name is the key
+    :param flavor_dict: the dictionary of newly instantiated flavors where the
+                        name is the key
+    :param networks_dict: the dictionary of newly instantiated networks where
+                          the name is the key
+    :param routers_dict: the dictionary of newly instantiated routers where
+                          the name is the key
+    :param tmplt_file: the path of the SNAPS-OO template file for setting the
+                       CWD so playbook location is relative to the deployment
+                       file
+    :return: t/f - true if successful
+    """
+    logger.info("Applying Ansible Playbooks")
+    if ansible_configs:
+        # Set CWD so the deployment file's playbook location can leverage
+        # relative paths
+        orig_cwd = os.getcwd()
+        env_dir = os.path.dirname(tmplt_file)
+        os.chdir(env_dir)
+
+        # Apply playbooks
+        for ansible_config in ansible_configs:
+            # Ensure all hosts are accepting SSH session requests
+            for vm_name in ansible_config['hosts']:
+                vm_inst = vm_dict.get(vm_name)
+                if vm_inst:
+                    if not vm_inst.vm_ssh_active(block=True):
+                        logger.warning(
+                            'Timeout waiting for instance to respond to '
+                            'SSH requests')
+                        return False
+
+            os_creds = os_creds_dict.get('admin-creds')
+            __apply_ansible_playbook(
+                ansible_config, os_creds, vm_dict, image_dict, flavor_dict,
+                networks_dict, routers_dict)
+
+        # Return to original directory
+        os.chdir(orig_cwd)
+
+    return True
+
+
+def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
+                             flavor_dict, networks_dict, routers_dict):
+    """
+    Applies an Ansible configuration setting
+    :param ansible_config: the configuration settings
+    :param os_creds: the OpenStack admin credentials object
+    :param vm_dict: the dictionary of newly instantiated VMs where the name is
+                    the key
+    :param image_dict: the dictionary of newly instantiated images where the
+                       name is the key
+    :param flavor_dict: the dictionary of newly instantiated flavors where the
+                        name is the key
+    :param networks_dict: the dictionary of newly instantiated networks where
+                          the name is the key
+    :param routers_dict: the dictionary of newly instantiated routers where
+                          the name is the key
+    """
+    if ansible_config:
+        (remote_user, floating_ips, private_key_filepath,
+         proxy_settings) = __get_connection_info(
+            ansible_config, vm_dict)
+        if floating_ips:
+            for key, vm_creator in vm_dict.items():
+                fip = vm_creator.get_floating_ip()
+                if fip and fip.ip in floating_ips:
+                    if not vm_creator.cloud_init_complete(block=True):
+                        raise Exception(
+                            'Cannot apply playbooks as cloud-init has not '
+                            'completed')
+
+            variables = __get_variables(
+                ansible_config.get('variables'), os_creds, vm_dict, image_dict,
+                flavor_dict, networks_dict, routers_dict)
+
+            retval = ansible_utils.apply_playbook(
+                ansible_config['playbook_location'], floating_ips, remote_user,
+                private_key_filepath,
+                variables=variables,
+                proxy_setting=proxy_settings)
+            if retval != 0:
+                # Not a fatal type of event
+                raise Exception(
+                    'Error applying playbook found at location - %s',
+                    ansible_config.get('playbook_location'))
+            elif ansible_config.get('post_processing'):
+                post_proc_config = ansible_config['post_processing']
+                if 'sleep' in post_proc_config:
+                    time.sleep(post_proc_config['sleep'])
+                if 'reboot' in post_proc_config:
+                    for vm_name in post_proc_config['reboot']:
+                        if vm_name in vm_dict:
+                            logger.info('Rebooting VM - %s', vm_name)
+                            vm_dict[vm_name].reboot()
+
+            return retval
+
+
+def __get_connection_info(ansible_config, vm_dict):
+    """
+    Returns a tuple of data required for connecting to the running VMs
+    (remote_user, [floating_ips], private_key_filepath, proxy_settings)
+    :param ansible_config: the configuration settings
+    :param vm_dict: the dictionary of VMs where the VM name is the key
+    :return: tuple where the first element is the user and the second is a list
+             of floating IPs and the third is the
+    private key file location and the fourth is an instance of the
+    snaps.ProxySettings class
+    (note: in order to work, each of the hosts need to have the same sudo_user
+    and private key file location values)
+    """
+    if ansible_config.get('hosts'):
+        hosts = ansible_config['hosts']
+        if len(hosts) > 0:
+            floating_ips = list()
+            remote_user = None
+            pk_file = None
+            proxy_settings = None
+            for host in hosts:
+                vm = vm_dict.get(host)
+                if vm:
+                    fip = vm.get_floating_ip()
+                    if fip:
+                        remote_user = vm.get_image_user()
+
+                        if fip:
+                            floating_ips.append(fip.ip)
+                        else:
+                            raise Exception(
+                                'Could not find floating IP for VM - ' +
+                                vm.name)
+
+                        pk_file = vm.keypair_settings.private_filepath
+                        proxy_settings = vm.get_os_creds().proxy_settings
+                else:
+                    logger.error('Could not locate VM with name - ' + host)
+
+            return remote_user, floating_ips, pk_file, proxy_settings
+    return None
+
+
+def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict,
+                    networks_dict, routers_dict):
+    """
+    Returns a dictionary of substitution variables to be used for Ansible
+    templates
+    :param var_config: the variable configuration settings
+    :param os_creds: the OpenStack admin credentials object
+    :param vm_dict: the dictionary of newly instantiated VMs where the name is
+                    the key
+    :param image_dict: the dictionary of newly instantiated images where the
+                       name is the key
+    :param flavor_dict: the dictionary of newly instantiated flavors where the
+                        name is the key
+    :param networks_dict: the dictionary of newly instantiated networks where
+                          the name is the key
+    :param routers_dict: the dictionary of newly instantiated routers where
+                          the name is the key
+    :return: dictionary or None
+    """
+    if var_config and vm_dict and len(vm_dict) > 0:
+        variables = dict()
+        for key, value in var_config.items():
+            value = __get_variable_value(
+                value, os_creds, vm_dict, image_dict, flavor_dict,
+                networks_dict, routers_dict)
+            if key and value:
+                variables[key] = value
+                logger.info(
+                    "Set Jinga2 variable with key [%s] the value [%s]",
+                    key, value)
+            else:
+                raise Exception(
+                    'Key - [' + str(key) + '] or Value [' + str(value)
+                    + '] must not be None')
+        return variables
+    return None
+
+
+def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
+                         flavor_dict, networks_dict, routers_dict):
+    """
+    Returns the associated variable value for use by Ansible for substitution
+    purposes
+    :param var_config_values: the configuration dictionary
+    :param os_creds: the OpenStack admin credentials object
+    :param vm_dict: the dictionary of newly instantiated VMs where the name is
+                    the key
+    :param image_dict: the dictionary of newly instantiated images where the
+                       name is the key
+    :param flavor_dict: the dictionary of newly instantiated flavors where the
+                        name is the key
+    :param networks_dict: the dictionary of newly instantiated networks where
+                          the name is the key
+    :param routers_dict: the dictionary of newly instantiated routers where
+                          the name is the key
+    :return:
+    """
+    if var_config_values['type'] == 'string':
+        return __get_string_variable_value(var_config_values)
+    if var_config_values['type'] == 'vm-attr':
+        return __get_vm_attr_variable_value(var_config_values, vm_dict)
+    if var_config_values['type'] == 'os_creds':
+        return __get_os_creds_variable_value(var_config_values, os_creds)
+    if var_config_values['type'] == 'network':
+        return __get_network_variable_value(var_config_values, networks_dict)
+    if var_config_values['type'] == 'router':
+        return __get_router_variable_value(var_config_values, routers_dict,
+                                           os_creds)
+    if var_config_values['type'] == 'port':
+        return __get_vm_port_variable_value(var_config_values, vm_dict)
+    if var_config_values['type'] == 'floating_ip':
+        return __get_vm_fip_variable_value(var_config_values, vm_dict)
+    if var_config_values['type'] == 'image':
+        return __get_image_variable_value(var_config_values, image_dict)
+    if var_config_values['type'] == 'flavor':
+        return __get_flavor_variable_value(var_config_values, flavor_dict)
+    return None
+
+
+def __get_string_variable_value(var_config_values):
+    """
+    Returns the associated string value
+    :param var_config_values: the configuration dictionary
+    :return: the value contained in the dictionary with the key 'value'
+    """
+    return var_config_values['value']
+
+
+def __get_vm_attr_variable_value(var_config_values, vm_dict):
+    """
+    Returns the associated value contained on a VM instance
+    :param var_config_values: the configuration dictionary
+    :param vm_dict: the dictionary containing all VMs where the key is the VM's
+                    name
+    :return: the value
+    """
+    vm = vm_dict.get(var_config_values['vm_name'])
+    if vm:
+        if var_config_values['value'] == 'floating_ip':
+            return vm.get_floating_ip().ip
+        if var_config_values['value'] == 'image_user':
+            return vm.get_image_user()
+
+
+def __get_os_creds_variable_value(var_config_values, os_creds):
+    """
+    Returns the associated OS credentials value
+    :param var_config_values: the configuration dictionary
+    :param os_creds: the admin OpenStack OSCreds object
+    :return: the value
+    """
+    if os_creds:
+        if var_config_values['value'] == 'username':
+            logger.info("Returning OS username")
+            return os_creds.username
+        elif var_config_values['value'] == 'password':
+            logger.info("Returning OS password")
+            return os_creds.password
+        elif var_config_values['value'] == 'auth_url':
+            logger.info("Returning OS auth_url")
+            return os_creds.auth_url
+        elif var_config_values['value'] == 'project_name':
+            logger.info("Returning OS project_name")
+            return os_creds.project_name
+
+
+def __get_network_variable_value(var_config_values, networks_dict):
+    """
+    Returns the associated network value
+    :param var_config_values: the configuration dictionary
+    :param networks_dict: the dictionary containing all networks where the key
+                          is the network name
+    :return: the value
+    """
+    net_name = var_config_values.get('network_name')
+
+    if net_name and networks_dict.get(net_name):
+        network_creator = networks_dict[net_name]
+
+        if 'subnet_name' in var_config_values:
+            subnet_name = var_config_values.get('subnet_name')
+            if subnet_name:
+                for subnet in network_creator.get_network().subnets:
+                    if subnet_name == subnet.name:
+                        if 'value' in var_config_values:
+                            if 'gateway_ip' == var_config_values['value']:
+                                return subnet.gateway_ip
+                            if 'ip_range' == var_config_values['value']:
+                                return subnet.start + ' ' + subnet.end
+                            if 'cidr_ip' == var_config_values['value']:
+                                cidr_split = subnet.cidr.split('/')
+                                return cidr_split[0]
+                            if 'netmask' == var_config_values['value']:
+                                cidr_split = subnet.cidr.split('/')
+                                cidr_bits = 32 - int(cidr_split[1])
+                                netmask = socket.inet_ntoa(
+                                    struct.pack(
+                                        '!I', (1 << 32) - (1 << cidr_bits)))
+                                return netmask
+                            if 'broadcast_ip' == var_config_values['value']:
+                                end_split = subnet.end.split('.')
+                                broadcast_ip = (
+                                    end_split[0] + '.' + end_split[1] + '.'
+                                    + end_split[2] + '.255')
+                                return broadcast_ip
+
+
+def __get_router_variable_value(var_config_values, routers_dict, os_creds):
+    """
+    Returns the associated network value
+    :param var_config_values: the configuration dictionary
+    :param routers_dict: the dictionary containing all networks where the key
+                          is the network name
+    :param os_creds: the admin OpenStack credentials
+    :return: the value
+    """
+    router_name = var_config_values.get('router_name')
+    router_creator = routers_dict[router_name]
+
+    if router_creator:
+        if 'external_fixed_ip' == var_config_values.get('attr'):
+            neutron = neutron_utils.neutron_client(os_creds)
+            ext_nets = neutron_utils.get_external_networks(neutron)
+
+            subnet_name = var_config_values.get('subnet_name')
+
+            for ext_net in ext_nets:
+                for subnet in ext_net.subnets:
+                    if subnet_name == subnet.name:
+                        router = router_creator.get_router()
+                        for fixed_ips in router.external_fixed_ips:
+                            if subnet.id == fixed_ips['subnet_id']:
+                                return fixed_ips['ip_address']
+
+
+def __get_vm_port_variable_value(var_config_values, vm_dict):
+    """
+    Returns the associated OS credentials value
+    :param var_config_values: the configuration dictionary
+    :param vm_dict: the dictionary containing all VMs where the key is the VM's
+                    name
+    :return: the value
+    """
+    port_name = var_config_values.get('port_name')
+    vm_name = var_config_values.get('vm_name')
+
+    if port_name and vm_name:
+        vm = vm_dict.get(vm_name)
+        if vm:
+            for vm_port in vm.get_vm_inst().ports:
+                if vm_port.name == port_name:
+                    port_value_id = var_config_values.get('port_value')
+                    if port_value_id:
+                        if port_value_id == 'mac_address':
+                            return vm_port.mac_address
+                        if port_value_id == 'ip_address':
+                            return vm_port.ips[0]['ip_address']
+
+
+def __get_vm_fip_variable_value(var_config_values, vm_dict):
+    """
+    Returns the floating IP value if found
+    :param var_config_values: the configuration dictionary
+    :param vm_dict: the dictionary containing all VMs where the key is the VM's
+                    name
+    :return: the floating IP string value or None
+    """
+    fip_name = var_config_values.get('fip_name')
+    vm_name = var_config_values.get('vm_name')
+
+    if vm_name:
+        vm = vm_dict.get(vm_name)
+        if vm:
+            fip = vm.get_floating_ip(fip_name)
+            if fip:
+                return fip.ip
+
+
+def __get_image_variable_value(var_config_values, image_dict):
+    """
+    Returns the associated image value
+    :param var_config_values: the configuration dictionary
+    :param image_dict: the dictionary containing all images where the key is
+                       the name
+    :return: the value
+    """
+    if image_dict:
+        if var_config_values.get('image_name'):
+            image_creator = image_dict.get(var_config_values['image_name'])
+            if image_creator:
+                if (var_config_values.get('value')
+                        and var_config_values['value'] == 'id'):
+                    return image_creator.get_image().id
+                if (var_config_values.get('value')
+                        and var_config_values['value'] == 'user'):
+                    return image_creator.image_settings.image_user
+
+
+def __get_flavor_variable_value(var_config_values, flavor_dict):
+    """
+    Returns the associated flavor value
+    :param var_config_values: the configuration dictionary
+    :param flavor_dict: the dictionary containing all flavor creators where the
+                        key is the name
+    :return: the value or None
+    """
+    if flavor_dict:
+        if var_config_values.get('flavor_name'):
+            flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
+            if flavor_creator:
+                if (var_config_values.get('value')
+                        and var_config_values['value'] == 'id'):
+                    return flavor_creator.get_flavor().id
+
+
+def __cleanup(creators, clean_image=False):
+    """
+    Cleans up environment
+    :param creators: the list of creators by type
+    :param clean_image: when true
+    :return:
+    """
+    for creator_dict in reversed(creators):
+        for key, creator in creator_dict.items():
+            if ((isinstance(creator, OpenStackImage) and clean_image)
+                    or not isinstance(creator, OpenStackImage)):
+                creator.clean()
diff --git a/snaps/openstack/utils/magnum_utils.py b/snaps/openstack/utils/magnum_utils.py
new file mode 100644 (file)
index 0000000..96ba6d1
--- /dev/null
@@ -0,0 +1,126 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import logging
+
+from magnumclient.client import Client
+
+from snaps.domain.cluster_template import ClusterTemplate
+from snaps.openstack.utils import keystone_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('magnum_utils')
+
+
+def magnum_client(os_creds):
+    """
+    Retrieves the Magnum client
+    :param os_creds: the OpenStack credentialsf
+    :return: the client
+    """
+    logger.debug('Retrieving Magnum Client')
+    return Client(str(os_creds.magnum_api_version),
+                  session=keystone_utils.keystone_session(os_creds))
+
+
+def get_cluster_template(magnum, template_config=None, template_name=None):
+    """
+    Returns the first ClusterTemplate domain object that matches the parameters
+    :param magnum: the Magnum client
+    :param template_config: a ClusterTemplateConfig object (optional)
+    :param template_name: the name of the template to lookup
+    :return: ClusterTemplate object or None
+    """
+    name = None
+    if template_config:
+        name = template_config.name
+    elif template_name:
+        name = template_name
+
+    os_templates = magnum.cluster_templates.list()
+    for os_template in os_templates:
+        if os_template.name == name:
+            return __map_os_cluster_template(os_template)
+
+
+def get_cluster_template_by_id(magnum, tmplt_id):
+    """
+    Returns the first ClusterTemplate domain object that matches the parameters
+    :param magnum: the Magnum client
+    :param tmplt_id: the template's ID
+    :return: ClusterTemplate object or None
+    """
+    return __map_os_cluster_template(magnum.cluster_templates.get(tmplt_id))
+
+
+def create_cluster_template(magnum, cluster_template_config):
+    """
+    Creates a Magnum Cluster Template object in OpenStack
+    :param magnum: the Magnum client
+    :param cluster_template_config: a ClusterTemplateConfig object
+    :return: a SNAPS ClusterTemplate domain object
+    """
+    config_dict = cluster_template_config.magnum_dict()
+    os_cluster_template = magnum.cluster_templates.create(**config_dict)
+    logger.info('Creating cluster template named [%s]',
+                cluster_template_config.name)
+    return __map_os_cluster_template(os_cluster_template)
+
+
+def delete_cluster_template(magnum, tmplt_id):
+    """
+    Deletes a Cluster Template from OpenStack
+    :param magnum: the Magnum client
+    :param tmplt_id: the cluster template ID to delete
+    """
+    logger.info('Deleting cluster template with ID [%s]', tmplt_id)
+    magnum.cluster_templates.delete(tmplt_id)
+
+
+def __map_os_cluster_template(os_tmplt):
+    """
+    Returns a SNAPS ClusterTemplate object from an OpenStack ClusterTemplate
+    object
+    :param os_tmplt: the OpenStack ClusterTemplate object
+    :return: SNAPS ClusterTemplate object
+    """
+    return ClusterTemplate(
+        id=os_tmplt.uuid,
+        name=os_tmplt.name,
+        image=os_tmplt.image_id,
+        keypair=os_tmplt.keypair_id,
+        network_driver=os_tmplt.network_driver,
+        external_net=os_tmplt.external_network_id,
+        floating_ip_enabled=os_tmplt.floating_ip_enabled,
+        docker_volume_size=os_tmplt.docker_volume_size,
+        server_type=os_tmplt.server_type,
+        flavor=os_tmplt.flavor_id,
+        master_flavor=os_tmplt.master_flavor_id,
+        coe=os_tmplt.coe,
+        fixed_net=os_tmplt.fixed_network,
+        fixed_subnet=os_tmplt.fixed_subnet,
+        registry_enabled=os_tmplt.registry_enabled,
+        insecure_registry=os_tmplt.insecure_registry,
+        docker_storage_driver=os_tmplt.docker_storage_driver,
+        dns_nameserver=os_tmplt.dns_nameserver,
+        public=os_tmplt.public,
+        tls_disabled=os_tmplt.tls_disabled,
+        http_proxy=os_tmplt.http_proxy,
+        https_proxy=os_tmplt.https_proxy,
+        no_proxy=os_tmplt.no_proxy,
+        volume_driver=os_tmplt.volume_driver,
+        master_lb_enabled=os_tmplt.master_lb_enabled,
+        labels=os_tmplt.labels
+    )
index 806bb53..e94a40e 100644 (file)
@@ -53,15 +53,33 @@ def create_network(neutron, os_creds, network_settings):
     :param network_settings: A dictionary containing the network configuration
                              and is responsible for creating the network
                             request JSON body
-    :return: a SNAPS-OO Network domain object
+    :return: a SNAPS-OO Network domain object if found else None
     """
-    if neutron and network_settings:
-        logger.info('Creating network with name ' + network_settings.name)
-        json_body = network_settings.dict_for_neutron(os_creds)
-        os_network = neutron.create_network(body=json_body)
-        return Network(**os_network['network'])
-    else:
-        raise NeutronException('Failded to create network')
+    logger.info('Creating network with name ' + network_settings.name)
+    json_body = network_settings.dict_for_neutron(os_creds)
+    os_network = neutron.create_network(body=json_body)
+
+    if os_network:
+        network = get_network_by_id(neutron, os_network['network']['id'])
+
+        subnets = list()
+        for subnet_settings in network_settings.subnet_settings:
+            try:
+                subnets.append(
+                    create_subnet(neutron, subnet_settings, os_creds, network))
+            except:
+                logger.error(
+                    'Unexpected error creating subnet [%s]  for network [%s]',
+                    subnet_settings.name, network.name)
+
+                for subnet in subnets:
+                    delete_subnet(neutron, subnet)
+
+                delete_network(neutron, network)
+
+                raise
+
+        return get_network_by_id(neutron, network.id)
 
 
 def delete_network(neutron, network):
@@ -71,6 +89,14 @@ def delete_network(neutron, network):
     :param network: a SNAPS-OO Network domain object
     """
     if neutron and network:
+        if network.subnets:
+            for subnet in network.subnets:
+                logger.info('Deleting subnet with name ' + subnet.name)
+                try:
+                    delete_subnet(neutron, subnet)
+                except NotFound:
+                    pass
+
         logger.info('Deleting network with name ' + network.name)
         neutron.delete_network(network.id)
 
@@ -83,7 +109,7 @@ def get_network(neutron, network_settings=None, network_name=None,
     else the query will use just the name from the network_name parameter.
     When the project_id is included, that will be added to the query filter.
     :param neutron: the client
-    :param network_settings: the NetworkSettings object used to create filter
+    :param network_settings: the NetworkConfig object used to create filter
     :param network_name: the name of the network to retrieve
     :param project_id: the id of the network's project
     :return: a SNAPS-OO Network domain object
@@ -100,12 +126,13 @@ def get_network(neutron, network_settings=None, network_name=None,
     networks = neutron.list_networks(**net_filter)
     for network, netInsts in networks.items():
         for inst in netInsts:
-            return Network(**inst)
+            return __map_network(neutron, inst)
 
 
-def get_network_by_id(neutron, network_id):
+def __get_os_network_by_id(neutron, network_id):
     """
-    Returns the network object (dictionary) with the given ID else None
+    Returns the OpenStack network object (dictionary) with the given ID else
+    None
     :param neutron: the client
     :param network_id: the id of the network to retrieve
     :return: a SNAPS-OO Network domain object
@@ -113,18 +140,42 @@ def get_network_by_id(neutron, network_id):
     networks = neutron.list_networks(**{'id': network_id})
     for network in networks['networks']:
         if network['id'] == network_id:
-            return Network(**network)
+            return network
+
+
+def get_network_by_id(neutron, network_id):
+    """
+    Returns the SNAPS Network domain object for the given ID else None
+    :param neutron: the client
+    :param network_id: the id of the network to retrieve
+    :return: a SNAPS-OO Network domain object
+    """
+    os_network = __get_os_network_by_id(neutron, network_id)
+    if os_network:
+        return __map_network(neutron, os_network)
 
 
-def create_subnet(neutron, subnet_settings, os_creds, network=None):
+def __map_network(neutron, os_network):
+    """
+    Returns the network object (dictionary) with the given ID else None
+    :param neutron: the client
+    :param os_network: the OpenStack Network dict
+    :return: a SNAPS-OO Network domain object
+    """
+    subnets = get_subnets_by_network_id(neutron, os_network['id'])
+    os_network['subnets'] = subnets
+    return Network(**os_network)
+
+
+def create_subnet(neutron, subnet_settings, os_creds, network):
     """
     Creates a network subnet for OpenStack
     :param neutron: the client
-    :param network: the network object
     :param subnet_settings: A dictionary containing the subnet configuration
                             and is responsible for creating the subnet request
                             JSON body
     :param os_creds: the OpenStack credentials
+    :param network: the network object
     :return: a SNAPS-OO Subnet domain object
     """
     if neutron and network and subnet_settings:
@@ -207,9 +258,19 @@ def get_subnets_by_network(neutron, network):
     :param network: the SNAPS-OO Network domain object
     :return: a list of Subnet objects
     """
+    return get_subnets_by_network_id(neutron, network.id)
+
+
+def get_subnets_by_network_id(neutron, network_id):
+    """
+    Returns a list of SNAPS-OO Subnet domain objects
+    :param neutron: the OpenStack neutron client
+    :param network_id: the subnet's ID
+    :return: a list of Subnet objects
+    """
     out = list()
 
-    os_subnets = neutron.list_subnets(network_id=network.id)
+    os_subnets = neutron.list_subnets(network_id=network_id)
 
     for os_subnet in os_subnets['subnets']:
         out.append(Subnet(**os_subnet))
@@ -231,7 +292,7 @@ def create_router(neutron, os_creds, router_settings):
         json_body = router_settings.dict_for_neutron(neutron, os_creds)
         logger.info('Creating router with name - ' + router_settings.name)
         os_router = neutron.create_router(json_body)
-        return Router(**os_router['router'])
+        return __map_router(neutron, os_router['router'])
     else:
         logger.error("Failed to create router.")
         raise NeutronException('Failed to create router')
@@ -257,7 +318,7 @@ def get_router_by_id(neutron, router_id):
     """
     router = neutron.show_router(router_id)
     if router:
-        return Router(**router['router'])
+        return __map_router(neutron, router['router'])
 
 
 def get_router(neutron, router_settings=None, router_name=None):
@@ -266,7 +327,7 @@ def get_router(neutron, router_settings=None, router_name=None):
     values if not None, else finds the first with the value of the router_name
     parameter, else None
     :param neutron: the client
-    :param router_settings: the RouterSettings object
+    :param router_settings: the RouterConfig object
     :param router_name: the name of the network to retrieve
     :return: a SNAPS-OO Router domain object
     """
@@ -281,11 +342,41 @@ def get_router(neutron, router_settings=None, router_name=None):
         return None
 
     routers = neutron.list_routers(**router_filter)
+
     for routerInst in routers['routers']:
-        return Router(**routerInst)
+        return __map_router(neutron, routerInst)
+
     return None
 
 
+def __map_router(neutron, os_router):
+    """
+    Takes an OpenStack router instance and maps it to a SNAPS Router domain
+    object
+    :param neutron: the neutron client
+    :param os_router: the OpenStack Router object
+    :return:
+    """
+    device_ports = neutron.list_ports(
+        **{'device_id': os_router['id']})['ports']
+    port_subnets = list()
+
+    # Order by create date
+    sorted_ports = sorted(
+        device_ports, key=lambda dev_port: dev_port['created_at'])
+
+    for port in sorted_ports:
+        subnets = list()
+        for fixed_ip in port['fixed_ips']:
+            subnet = get_subnet_by_id(neutron, fixed_ip['subnet_id'])
+            if subnet and subnet.network_id == port['network_id']:
+                subnets.append(subnet)
+        port_subnets.append((Port(**port), subnets))
+
+    os_router['port_subnets'] = port_subnets
+    return Router(**os_router)
+
+
 def add_interface_router(neutron, router, subnet=None, port=None):
     """
     Adds an interface router for OpenStack for either a subnet or port.
@@ -389,7 +480,7 @@ def get_port(neutron, port_settings=None, port_name=None):
     """
     Returns the first port object (dictionary) found for the given query
     :param neutron: the client
-    :param port_settings: the PortSettings object used for generating the query
+    :param port_settings: the PortConfig object used for generating the query
     :param port_name: if port_settings is None, this name is the value to place
                       into the query
     :return: a SNAPS-OO Port domain object
@@ -408,7 +499,8 @@ def get_port(neutron, port_settings=None, port_name=None):
         if port_settings.network_name:
             network = get_network(neutron,
                                   network_name=port_settings.network_name)
-            port_filter['network_id'] = network.id
+            if network:
+                port_filter['network_id'] = network.id
     elif port_name:
         port_filter['name'] = port_name
 
@@ -467,7 +559,7 @@ def create_security_group(neutron, keystone, sec_grp_settings):
                 sec_grp_settings.name)
     os_group = neutron.create_security_group(
         sec_grp_settings.dict_for_neutron(keystone))
-    return SecurityGroup(**os_group['security_group'])
+    return __map_os_security_group(neutron, os_group['security_group'])
 
 
 def delete_security_group(neutron, sec_grp):
@@ -488,7 +580,7 @@ def get_security_group(neutron, sec_grp_settings=None, sec_grp_name=None,
     the security group will be used, else if the query parameters are None then
     None will be returned
     :param neutron: the client
-    :param sec_grp_settings: an instance of SecurityGroupSettings config object
+    :param sec_grp_settings: an instance of SecurityGroupConfig object
     :param sec_grp_name: the name of security group object to retrieve
     :param project_id: the ID of the project/tentant object that owns the
                        secuity group to retrieve
@@ -511,7 +603,20 @@ def get_security_group(neutron, sec_grp_settings=None, sec_grp_name=None,
 
     groups = neutron.list_security_groups(**sec_grp_filter)
     for group in groups['security_groups']:
-        return SecurityGroup(**group)
+        return __map_os_security_group(neutron, group)
+
+
+def __map_os_security_group(neutron, os_sec_grp):
+    """
+    Creates a SecurityGroup SNAPS domain object from an OpenStack Security
+    Group dict
+    :param neutron: the neutron client for performing rule lookups
+    :param os_sec_grp: the OpenStack Security Group dict object
+    :return: a SecurityGroup object
+    """
+    os_sec_grp['rules'] = get_rules_by_security_group_id(
+        neutron, os_sec_grp['id'])
+    return SecurityGroup(**os_sec_grp)
 
 
 def get_security_group_by_id(neutron, sec_grp_id):
@@ -526,13 +631,13 @@ def get_security_group_by_id(neutron, sec_grp_id):
     groups = neutron.list_security_groups(**{'id': sec_grp_id})
     for group in groups['security_groups']:
         if group['id'] == sec_grp_id:
-            return SecurityGroup(**group)
+            return __map_os_security_group(neutron, group)
     return None
 
 
 def create_security_group_rule(neutron, sec_grp_rule_settings):
     """
-    Creates a security group object in OpenStack
+    Creates a security group rule in OpenStack
     :param neutron: the client
     :param sec_grp_rule_settings: the security group rule settings
     :return: a SNAPS-OO SecurityGroupRule domain object
@@ -546,7 +651,7 @@ def create_security_group_rule(neutron, sec_grp_rule_settings):
 
 def delete_security_group_rule(neutron, sec_grp_rule):
     """
-    Deletes a security group object from OpenStack
+    Deletes a security group rule object from OpenStack
     :param neutron: the client
     :param sec_grp_rule: the SNAPS SecurityGroupRule object to delete
     """
@@ -561,20 +666,29 @@ def get_rules_by_security_group(neutron, sec_grp):
     :param neutron: the client
     :param sec_grp: a list of SNAPS SecurityGroupRule domain objects
     """
+    return get_rules_by_security_group_id(neutron, sec_grp.id)
+
+
+def get_rules_by_security_group_id(neutron, sec_grp_id):
+    """
+    Retrieves all of the rules for a given security group by it's ID
+    :param neutron: the client
+    :param sec_grp_id: the ID of the associated security group
+    """
     logger.info('Retrieving security group rules associate with the '
-                'security group - %s', sec_grp.name)
+                'security group with ID - %s', sec_grp_id)
     out = list()
     rules = neutron.list_security_group_rules(
-        **{'security_group_id': sec_grp.id})
+        **{'security_group_id': sec_grp_id})
     for rule in rules['security_group_rules']:
-        if rule['security_group_id'] == sec_grp.id:
+        if rule['security_group_id'] == sec_grp_id:
             out.append(SecurityGroupRule(**rule))
     return out
 
 
 def get_rule_by_id(neutron, sec_grp, rule_id):
     """
-    Deletes a security group object from OpenStack
+    Returns a SecurityGroupRule object from OpenStack
     :param neutron: the client
     :param sec_grp: the SNAPS SecurityGroup domain object
     :param rule_id: the rule's ID
@@ -598,7 +712,7 @@ def get_external_networks(neutron):
     out = list()
     for network in neutron.list_networks(
             **{'router:external': True})['networks']:
-        out.append(Network(**network))
+        out.append(__map_network(neutron, network))
     return out
 
 
@@ -614,7 +728,7 @@ def get_floating_ips(neutron, ports=None):
     :param ports: a list of tuple 2 where index 0 is the port name and index 1
                   is the SNAPS-OO Port object
     :return: a list of tuple 2 (port_id, SNAPS FloatingIp) objects when ports
-             is not None else a list of Port objects
+             is not None else a list of FloatingIp objects
     """
     out = list()
     fips = neutron.list_floatingips()
@@ -698,7 +812,7 @@ def delete_floating_ip(neutron, floating_ip):
 
 def get_network_quotas(neutron, project_id):
     """
-    Returns a list of all available keypairs
+    Returns a list of NetworkQuotas objects
     :param neutron: the neutron client
     :param project_id: the project's ID of the quotas to lookup
     :return: an object of type NetworkQuotas or None if not found
index 1665fd0..e15484c 100644 (file)
 
 import logging
 
+import enum
 import os
+import time
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import serialization
 from cryptography.hazmat.primitives.asymmetric import rsa
 from novaclient.client import Client
-from novaclient.exceptions import NotFound
+from novaclient.exceptions import NotFound, ClientException
 
 from snaps import file_utils
 from snaps.domain.flavor import Flavor
@@ -51,22 +53,22 @@ def nova_client(os_creds):
                   region_name=os_creds.region_name)
 
 
-def create_server(nova, neutron, glance, instance_settings, image_settings,
-                  keypair_settings=None):
+def create_server(nova, neutron, glance, instance_config, image_config,
+                  keypair_config=None):
     """
     Creates a VM instance
     :param nova: the nova client (required)
     :param neutron: the neutron client for retrieving ports (required)
     :param glance: the glance client (required)
-    :param instance_settings: the VM instance settings object (required)
-    :param image_settings: the VM's image settings object (required)
-    :param keypair_settings: the VM's keypair settings object (optional)
+    :param instance_config: the VMInstConfig object (required)
+    :param image_config: the VM's ImageConfig object (required)
+    :param keypair_config: the VM's KeypairConfig object (optional)
     :return: a snaps.domain.VmInst object
     """
 
     ports = list()
 
-    for port_setting in instance_settings.port_settings:
+    for port_setting in instance_config.port_settings:
         ports.append(neutron_utils.get_port(
             neutron, port_settings=port_setting))
     nics = []
@@ -75,56 +77,57 @@ def create_server(nova, neutron, glance, instance_settings, image_settings,
         kv['port-id'] = port.id
         nics.append(kv)
 
-    logger.info('Creating VM with name - ' + instance_settings.name)
+    logger.info('Creating VM with name - ' + instance_config.name)
     keypair_name = None
-    if keypair_settings:
-        keypair_name = keypair_settings.name
+    if keypair_config:
+        keypair_name = keypair_config.name
 
-    flavor = get_flavor_by_name(nova, instance_settings.flavor)
+    flavor = get_flavor_by_name(nova, instance_config.flavor)
     if not flavor:
         raise NovaException(
-            'Flavor not found with name - %s', instance_settings.flavor)
+            'Flavor not found with name - %s', instance_config.flavor)
 
-    image = glance_utils.get_image(glance, image_settings=image_settings)
+    image = glance_utils.get_image(glance, image_settings=image_config)
     if image:
         userdata = None
-        if instance_settings.userdata:
-            if isinstance(instance_settings.userdata, str):
-                userdata = instance_settings.userdata + '\n'
-            elif (isinstance(instance_settings.userdata, dict) and
-                  'script_file' in instance_settings.userdata):
+        if instance_config.userdata:
+            if isinstance(instance_config.userdata, str):
+                userdata = instance_config.userdata + '\n'
+            elif (isinstance(instance_config.userdata, dict) and
+                  'script_file' in instance_config.userdata):
                 try:
                     userdata = file_utils.read_file(
-                        instance_settings.userdata['script_file'])
+                        instance_config.userdata['script_file'])
                 except Exception as e:
                     logger.warn('error reading userdata file %s - %s',
-                                instance_settings.userdata, e)
-        args = {'name': instance_settings.name,
+                                instance_config.userdata, e)
+        args = {'name': instance_config.name,
                 'flavor': flavor,
                 'image': image,
                 'nics': nics,
                 'key_name': keypair_name,
                 'security_groups':
-                    instance_settings.security_group_names,
+                    instance_config.security_group_names,
                 'userdata': userdata}
 
-        if instance_settings.availability_zone:
-            args['availability_zone'] = instance_settings.availability_zone
+        if instance_config.availability_zone:
+            args['availability_zone'] = instance_config.availability_zone
 
         server = nova.servers.create(**args)
 
-        return __map_os_server_obj_to_vm_inst(server)
+        return __map_os_server_obj_to_vm_inst(neutron, server)
     else:
         raise NovaException(
             'Cannot create instance, image cannot be located with name %s',
-            image_settings.name)
+            image_config.name)
 
 
-def get_server(nova, vm_inst_settings=None, server_name=None):
+def get_server(nova, neutron, vm_inst_settings=None, server_name=None):
     """
     Returns a VmInst object for the first server instance found.
     :param nova: the Nova client
-    :param vm_inst_settings: the VmInstanceSettings object from which to build
+    :param neutron: the Neutron client
+    :param vm_inst_settings: the VmInstanceConfig object from which to build
                              the query if not None
     :param server_name: the server with this name to return if vm_inst_settings
                         is not None
@@ -138,12 +141,34 @@ def get_server(nova, vm_inst_settings=None, server_name=None):
 
     servers = nova.servers.list(search_opts=search_opts)
     for server in servers:
-        return __map_os_server_obj_to_vm_inst(server)
+        return __map_os_server_obj_to_vm_inst(neutron, server)
 
 
-def __map_os_server_obj_to_vm_inst(os_server):
+def get_server_connection(nova, vm_inst_settings=None, server_name=None):
+    """
+    Returns a VmInst object for the first server instance found.
+    :param nova: the Nova client
+    :param vm_inst_settings: the VmInstanceConfig object from which to build
+                             the query if not None
+    :param server_name: the server with this name to return if vm_inst_settings
+                        is not None
+    :return: a snaps.domain.VmInst object or None if not found
+    """
+    search_opts = dict()
+    if vm_inst_settings:
+        search_opts['name'] = vm_inst_settings.name
+    elif server_name:
+        search_opts['name'] = server_name
+
+    servers = nova.servers.list(search_opts=search_opts)
+    for server in servers:
+        return server.links[0]
+
+
+def __map_os_server_obj_to_vm_inst(neutron, os_server):
     """
     Returns a VmInst object for an OpenStack Server object
+    :param neutron: the Neutron client (when None, ports will be empty)
     :param os_server: the OpenStack server object
     :return: an equivalent SNAPS-OO VmInst domain object
     """
@@ -154,11 +179,23 @@ def __map_os_server_obj_to_vm_inst(os_server):
             if sec_group.get('name'):
                 sec_grp_names.append(sec_group.get('name'))
 
+    out_ports = list()
+    if len(os_server.networks) > 0:
+        for net_name, ips in os_server.networks.items():
+            network = neutron_utils.get_network(neutron, network_name=net_name)
+            ports = neutron_utils.get_ports(neutron, network, ips)
+            for port in ports:
+                out_ports.append(port)
+
+    volumes = None
+    if hasattr(os_server, 'os-extended-volumes:volumes_attached'):
+        volumes = getattr(os_server, 'os-extended-volumes:volumes_attached')
+
     return VmInst(
         name=os_server.name, inst_id=os_server.id,
         image_id=os_server.image['id'], flavor_id=os_server.flavor['id'],
-        networks=os_server.networks, keypair_name=os_server.key_name,
-        sec_grp_names=sec_grp_names)
+        ports=out_ports, keypair_name=os_server.key_name,
+        sec_grp_names=sec_grp_names, volume_ids=volumes)
 
 
 def __get_latest_server_os_object(nova, server):
@@ -207,26 +244,28 @@ def get_server_console_output(nova, server):
     return None
 
 
-def get_latest_server_object(nova, server):
+def get_latest_server_object(nova, neutron, server):
     """
     Returns a server with a given id
     :param nova: the Nova client
+    :param neutron: the Neutron client
     :param server: the old server object
     :return: the list of servers or None if not found
     """
     server = __get_latest_server_os_object(nova, server)
-    return __map_os_server_obj_to_vm_inst(server)
+    return __map_os_server_obj_to_vm_inst(neutron, server)
 
 
-def get_server_object_by_id(nova, server_id):
+def get_server_object_by_id(nova, neutron, server_id):
     """
     Returns a server with a given id
     :param nova: the Nova client
+    :param neutron: the Neutron client
     :param server_id: the server's id
     :return: an SNAPS-OO VmInst object or None if not found
     """
     server = __get_latest_server_os_object_by_id(nova, server_id)
-    return __map_os_server_obj_to_vm_inst(server)
+    return __map_os_server_obj_to_vm_inst(neutron, server)
 
 
 def get_server_security_group_names(nova, server):
@@ -256,6 +295,22 @@ def get_server_info(nova, server):
     return None
 
 
+def reboot_server(nova, server, reboot_type=None):
+    """
+    Returns a dictionary of a VMs info as returned by OpenStack
+    :param nova: the Nova client
+    :param server: the old server object
+    :param reboot_type: Acceptable values 'SOFT', 'HARD'
+                        (api uses SOFT as the default)
+    :return: a dict of the info if VM exists else None
+    """
+    vm = __get_latest_server_os_object(nova, server)
+    if vm:
+        vm.reboot(reboot_type=reboot_type.value)
+    else:
+        raise ServerNotFoundError('Cannot locate server')
+
+
 def create_keys(key_size=2048):
     """
     Generates public and private keys
@@ -393,6 +448,18 @@ def get_keypair_by_name(nova, name):
     return None
 
 
+def get_keypair_by_id(nova, kp_id):
+    """
+    Returns a list of all available keypairs
+    :param nova: the Nova client
+    :param kp_id: the ID of the keypair to return
+    :return: the keypair object
+    """
+    keypair = nova.keypairs.get(kp_id)
+    return Keypair(name=keypair.name, kp_id=keypair.id,
+                   public_key=keypair.public_key)
+
+
 def delete_keypair(nova, key):
     """
     Deletes a keypair object from OpenStack
@@ -421,6 +488,21 @@ def get_availability_zone_hosts(nova, zone_name='nova'):
     return out
 
 
+def get_hypervisor_hosts(nova):
+    """
+    Returns the host names of all nova nodes with active hypervisors
+    :param nova: the Nova client
+    :return: a list of hypervisor host names
+    """
+    out = list()
+    hypervisors = nova.hypervisors.list()
+    for hypervisor in hypervisors:
+        if hypervisor.state == "up":
+            out.append(hypervisor.hypervisor_hostname)
+
+    return out
+
+
 def delete_vm_instance(nova, vm_inst):
     """
     Deletes a VM instance
@@ -567,7 +649,18 @@ def add_security_group(nova, vm, security_group_name):
     :param vm: the OpenStack server object (VM) to alter
     :param security_group_name: the name of the security group to add
     """
-    nova.servers.add_security_group(str(vm.id), security_group_name)
+    try:
+        nova.servers.add_security_group(str(vm.id), security_group_name)
+    except ClientException as e:
+        sec_grp_names = get_server_security_group_names(nova, vm)
+        if security_group_name in sec_grp_names:
+            logger.warn('Security group [%s] already added to VM [%s]',
+                        security_group_name, vm.name)
+            return
+
+        logger.error('Unexpected error while adding security group [%s] - %s',
+                     security_group_name, e)
+        raise
 
 
 def remove_security_group(nova, vm, security_group):
@@ -618,7 +711,8 @@ def update_quotas(nova, project_id, compute_quotas):
     update_values['cores'] = compute_quotas.cores
     update_values['instances'] = compute_quotas.instances
     update_values['injected_files'] = compute_quotas.injected_files
-    update_values['injected_file_content_bytes'] = compute_quotas.injected_file_content_bytes
+    update_values['injected_file_content_bytes'] = (
+        compute_quotas.injected_file_content_bytes)
     update_values['ram'] = compute_quotas.ram
     update_values['fixed_ips'] = compute_quotas.fixed_ips
     update_values['key_pairs'] = compute_quotas.key_pairs
@@ -626,7 +720,78 @@ def update_quotas(nova, project_id, compute_quotas):
     return nova.quotas.update(project_id, **update_values)
 
 
+def attach_volume(nova, neutron, server, volume, timeout=None):
+    """
+    Attaches a volume to a server
+    :param nova: the nova client
+    :param neutron: the neutron client
+    :param server: the VMInst domain object
+    :param volume: the Volume domain object
+    :param timeout: denotes the amount of time to block to determine if the
+                    has been properly attached. When None, do not wait.
+    :return: the value from the nova call
+    """
+    nova.volumes.create_server_volume(server.id, volume.id)
+
+    if timeout:
+        start_time = time.time()
+        while time.time() < start_time + timeout:
+            vm = get_server_object_by_id(nova, neutron, server.id)
+            for vol_dict in vm.volume_ids:
+                if volume.id == vol_dict['id']:
+                    return vm
+
+        return None
+    else:
+        return get_server_object_by_id(nova, neutron, server.id)
+
+
+def detach_volume(nova, neutron, server, volume, timeout=None):
+    """
+    Attaches a volume to a server
+    :param nova: the nova client
+    :param neutron: the neutron client
+    :param server: the VMInst domain object
+    :param volume: the Volume domain object
+    :param timeout: denotes the amount of time to block to determine if the
+                    has been properly detached. When None, do not wait.
+    :return: the value from the nova call
+    """
+    nova.volumes.delete_server_volume(server.id, volume.id)
+
+    if timeout:
+        start_time = time.time()
+        while time.time() < start_time + timeout:
+            vm = get_server_object_by_id(nova, neutron, server.id)
+            found = False
+            for vol_dict in vm.volume_ids:
+                if volume.id == vol_dict['id']:
+                    found = True
+
+            if not found:
+                return vm
+
+        return None
+    else:
+        return get_server_object_by_id(nova, neutron, server.id)
+
+
+class RebootType(enum.Enum):
+    """
+    A rule's direction
+    """
+    soft = 'SOFT'
+    hard = 'HARD'
+
+
 class NovaException(Exception):
     """
     Exception when calls to the Keystone client cannot be served properly
     """
+
+
+class ServerNotFoundError(Exception):
+    """
+    Exception when operations to a VM/Server is requested and the OpenStack
+    Server instance cannot be located
+    """
index 7f00075..2cf6047 100644 (file)
 import uuid
 
 from snaps import file_utils
-from snaps.openstack.create_instance import (
-    VmInstanceSettings, FloatingIpSettings)
-from snaps.openstack.create_keypairs import KeypairSettings
-from snaps.openstack.create_network import (
-    PortSettings, SubnetSettings, NetworkSettings)
+from snaps.config.flavor import FlavorConfig
+from snaps.config.keypair import KeypairConfig
+from snaps.config.network import SubnetConfig, PortConfig, NetworkConfig
+from snaps.config.router import RouterConfig
+from snaps.config.security_group import (
+    SecurityGroupRuleConfig, SecurityGroupConfig)
+from snaps.config.vm_inst import VmInstanceConfig, FloatingIpConfig
+from snaps.config.volume import VolumeConfig
+from snaps.config.volume_type import (
+    ControlLocation,  VolumeTypeEncryptionConfig, VolumeTypeConfig)
 from snaps.openstack.utils import (
     neutron_utils, nova_utils, heat_utils, glance_utils)
 
 
-def create_network_settings(neutron, network):
+def create_network_config(neutron, network):
     """
-    Returns a NetworkSettings object
+    Returns a NetworkConfig object
     :param neutron: the neutron client
     :param network: a SNAPS-OO Network domain object
     :return:
     """
-    return NetworkSettings(
+    return NetworkConfig(
         name=network.name, network_type=network.type,
-        subnet_settings=create_subnet_settings(neutron, network))
+        subnet_settings=create_subnet_config(neutron, network))
 
 
-def create_subnet_settings(neutron, network):
+def create_security_group_config(neutron, security_group):
     """
-    Returns a list of SubnetSettings objects for a given network
+    Returns a SecurityGroupConfig object
+    :param neutron: the neutron client
+    :param security_group: a SNAPS-OO SecurityGroup domain object
+    :return:
+    """
+    rules = neutron_utils.get_rules_by_security_group(neutron, security_group)
+
+    rule_settings = list()
+    for rule in rules:
+        rule_settings.append(SecurityGroupRuleConfig(
+            sec_grp_name=security_group.name, description=rule.description,
+            direction=rule.direction, ethertype=rule.ethertype,
+            port_range_min=rule.port_range_min,
+            port_range_max=rule.port_range_max, protocol=rule.protocol,
+            remote_group_id=rule.remote_group_id,
+            remote_ip_prefix=rule.remote_ip_prefix))
+
+    return SecurityGroupConfig(
+        name=security_group.name, description=security_group.description,
+        rule_settings=rule_settings)
+
+
+def create_subnet_config(neutron, network):
+    """
+    Returns a list of SubnetConfig objects for a given network
     :param neutron: the OpenStack neutron client
     :param network: the SNAPS-OO Network domain object
     :return: a list
@@ -59,13 +88,147 @@ def create_subnet_settings(neutron, network):
         kwargs['host_routes'] = subnet.host_routes
         kwargs['ipv6_ra_mode'] = subnet.ipv6_ra_mode
         kwargs['ipv6_address_mode'] = subnet.ipv6_address_mode
-        out.append(SubnetSettings(**kwargs))
+        out.append(SubnetConfig(**kwargs))
     return out
 
 
-def create_vm_inst_settings(nova, neutron, server):
+def create_router_config(neutron, router):
+    """
+    Returns a RouterConfig object
+    :param neutron: the neutron client
+    :param router: a SNAPS-OO Router domain object
+    :return:
+    """
+    ext_net_name = None
+
+    if router.external_network_id:
+        network = neutron_utils.get_network_by_id(
+            neutron, router.external_network_id)
+        if network:
+            ext_net_name = network.name
+
+    out_ports = list()
+    if router.port_subnets:
+        for port, subnets in router.port_subnets:
+            network = neutron_utils.get_network_by_id(
+                neutron, port.network_id)
+
+            ip_addrs = list()
+            if network and router.external_fixed_ips:
+                for ext_fixed_ips in router.external_fixed_ips:
+                    for subnet in subnets:
+                        if ext_fixed_ips['subnet_id'] == subnet.id:
+                            ip_addrs.append(ext_fixed_ips['ip_address'])
+            else:
+                for ip in port.ips:
+                    ip_addrs.append(ip['ip_address'])
+
+            ports = neutron_utils.get_ports(neutron, network, ip_addrs)
+            for out_port in ports:
+                out_ports.append(out_port)
+
+    port_settings = __create_port_configs(neutron, out_ports)
+
+    filtered_settings = list()
+    for port_setting in port_settings:
+        if port_setting.network_name != ext_net_name:
+            filtered_settings.append(port_setting)
+
+    return RouterConfig(
+        name=router.name, external_gateway=ext_net_name,
+        admin_state_up=router.admin_state_up,
+        port_settings=filtered_settings)
+
+
+def create_volume_config(volume):
+    """
+    Returns a VolumeConfig object
+    :param volume: a SNAPS-OO Volume object
+    """
+
+    return VolumeConfig(
+        name=volume.name, description=volume.description,
+        size=volume.size, type_name=volume.type,
+        availability_zone=volume.availability_zone,
+        multi_attach=volume.multi_attach)
+
+
+def create_volume_type_config(volume_type):
     """
-    Returns a NetworkSettings object
+    Returns a VolumeTypeConfig object
+    :param volume_type: a SNAPS-OO VolumeType object
+    """
+
+    control = None
+    if volume_type.encryption:
+        if (volume_type.encryption.control_location
+                == ControlLocation.front_end.value):
+            control = ControlLocation.front_end
+        else:
+            control = ControlLocation.back_end
+
+    if volume_type and volume_type.encryption:
+        encrypt_settings = VolumeTypeEncryptionConfig(
+            name=volume_type.encryption.__class__,
+            provider_class=volume_type.encryption.provider,
+            control_location=control,
+            cipher=volume_type.encryption.cipher,
+            key_size=volume_type.encryption.key_size)
+    else:
+        encrypt_settings = None
+
+    qos_spec_name = None
+    if volume_type.qos_spec:
+        qos_spec_name = volume_type.qos_spec.name
+
+    return VolumeTypeConfig(
+        name=volume_type.name, encryption=encrypt_settings,
+        qos_spec_name=qos_spec_name, public=volume_type.public)
+
+
+def create_flavor_config(flavor):
+    """
+    Returns a FlavorConfig object
+    :param flavor: a FlavorConfig object
+    """
+    return FlavorConfig(
+        name=flavor.name, flavor_id=flavor.id, ram=flavor.ram,
+        disk=flavor.disk, vcpus=flavor.vcpus, ephemeral=flavor.ephemeral,
+        swap=flavor.swap, rxtx_factor=flavor.rxtx_factor,
+        is_public=flavor.is_public)
+
+
+def create_keypair_config(heat_cli, stack, keypair, pk_output_key):
+    """
+    Instantiates a KeypairConfig object from a Keypair domain objects
+    :param heat_cli: the heat client
+    :param stack: the Stack domain object
+    :param keypair: the Keypair SNAPS domain object
+    :param pk_output_key: the key to the heat template's outputs for retrieval
+                          of the private key file
+    :return: a KeypairConfig object
+    """
+    if pk_output_key:
+        outputs = heat_utils.get_outputs(heat_cli, stack)
+        for output in outputs:
+            if output.key == pk_output_key:
+                # Save to file
+                guid = uuid.uuid4()
+                key_file = file_utils.save_string_to_file(
+                    output.value, str(guid), 0o400)
+
+                # Use outputs, file and resources for the KeypairConfig
+                return KeypairConfig(
+                    name=keypair.name, private_filepath=key_file.name)
+
+    return KeypairConfig(name=keypair.name)
+
+
+def create_vm_inst_config(nova, neutron, server):
+    """
+    Returns a VmInstanceConfig object
+    note: if the server instance is not active, the PortSettings objects will
+    not be generated resulting in an invalid configuration
     :param nova: the nova client
     :param neutron: the neutron client
     :param server: a SNAPS-OO VmInst domain object
@@ -77,29 +240,34 @@ def create_vm_inst_settings(nova, neutron, server):
     kwargs = dict()
     kwargs['name'] = server.name
     kwargs['flavor'] = flavor_name
-    kwargs['port_settings'] = __create_port_settings(
-        neutron, server.networks)
+
+    kwargs['port_settings'] = __create_port_configs(neutron, server.ports)
     kwargs['security_group_names'] = server.sec_grp_names
-    kwargs['floating_ip_settings'] = __create_floatingip_settings(
+    kwargs['floating_ip_settings'] = __create_floatingip_config(
         neutron, kwargs['port_settings'])
 
-    return VmInstanceSettings(**kwargs)
+    return VmInstanceConfig(**kwargs)
 
 
-def __create_port_settings(neutron, networks):
+def __create_port_configs(neutron, ports):
     """
-    Returns a list of port settings based on the networks parameter
+    Returns a list of PortConfig objects based on the networks parameter
     :param neutron: the neutron client
-    :param networks: a dict where the key is the network name and the value
-                     is a list of IP addresses
+    :param ports: a list of SNAPS-OO Port domain objects
     :return:
     """
     out = list()
 
-    for net_name, ips in networks.items():
-        network = neutron_utils.get_network(neutron, network_name=net_name)
-        ports = neutron_utils.get_ports(neutron, network, ips)
-        for port in ports:
+    for port in ports:
+        if port.device_owner != 'network:dhcp':
+            ip_addrs = list()
+            for ip_dict in port.ips:
+                subnet = neutron_utils.get_subnet_by_id(
+                    neutron, ip_dict['subnet_id'])
+                ip_addrs.append({'subnet_name': subnet.name,
+                                 'ip': ip_dict['ip_address']})
+
+            network = neutron_utils.get_network_by_id(neutron, port.network_id)
             kwargs = dict()
             if port.name:
                 kwargs['name'] = port.name
@@ -107,18 +275,19 @@ def __create_port_settings(neutron, networks):
             kwargs['mac_address'] = port.mac_address
             kwargs['allowed_address_pairs'] = port.allowed_address_pairs
             kwargs['admin_state_up'] = port.admin_state_up
-            out.append(PortSettings(**kwargs))
+            kwargs['ip_addrs'] = ip_addrs
+            out.append(PortConfig(**kwargs))
 
     return out
 
 
-def __create_floatingip_settings(neutron, port_settings):
+def __create_floatingip_config(neutron, port_settings):
     """
-    Returns a list of FloatingIPSettings objects as they pertain to an
+    Returns a list of FloatingIpConfig objects as they pertain to an
     existing deployed server instance
     :param neutron: the neutron client
-    :param port_settings: list of SNAPS-OO PortSettings objects
-    :return: a list of FloatingIPSettings objects or an empty list if no
+    :param port_settings: list of SNAPS-OO PortConfig objects
+    :return: a list of FloatingIpConfig objects or an empty list if no
              floating IPs have been created
     """
     base_fip_name = 'fip-'
@@ -160,21 +329,21 @@ def __create_floatingip_settings(neutron, port_settings):
                     if subnet:
                         kwargs['subnet_name'] = subnet.name
 
-        out.append(FloatingIpSettings(**kwargs))
+        out.append(FloatingIpConfig(**kwargs))
 
         fip_ctr += 1
 
     return out
 
 
-def determine_image_settings(glance, server, image_settings):
+def determine_image_config(glance, server, image_settings):
     """
-    Returns a ImageSettings object from the list that matches the name in one
+    Returns a ImageConfig object from the list that matches the name in one
     of the image_settings parameter
     :param glance: the glance client
     :param server: a SNAPS-OO VmInst domain object
-    :param image_settings: list of ImageSettings objects
-    :return: ImageSettings or None
+    :param image_settings: list of ImageConfig objects
+    :return: ImageConfig or None
     """
     if image_settings:
         for image_setting in image_settings:
@@ -183,20 +352,20 @@ def determine_image_settings(glance, server, image_settings):
                 return image_setting
 
 
-def determine_keypair_settings(heat_cli, stack, server, keypair_settings=None,
-                               priv_key_key=None):
+def determine_keypair_config(heat_cli, stack, server, keypair_settings=None,
+                             priv_key_key=None):
     """
-    Returns a KeypairSettings object from the list that matches the
+    Returns a KeypairConfig object from the list that matches the
     server.keypair_name value in the keypair_settings parameter if not None,
     else if the output_key is not None, the output's value when contains the
     string 'BEGIN RSA PRIVATE KEY', this value will be stored into a file and
-    encoded into the KeypairSettings object returned
+    encoded into the KeypairConfig object returned
     :param heat_cli: the OpenStack heat client
     :param stack: a SNAPS-OO Stack domain object
     :param server: a SNAPS-OO VmInst domain object
-    :param keypair_settings: list of KeypairSettings objects
+    :param keypair_settings: list of KeypairConfig objects
     :param priv_key_key: the stack options that holds the private key value
-    :return: KeypairSettings or None
+    :return: KeypairConfig or None
     """
     # Existing keypair being used by Heat Template
     if keypair_settings:
@@ -214,6 +383,6 @@ def determine_keypair_settings(heat_cli, stack, server, keypair_settings=None,
                 key_file = file_utils.save_string_to_file(
                     output.value, str(guid), 0o400)
 
-                # Use outputs, file and resources for the KeypairSettings
-                return KeypairSettings(
+                # Use outputs, file and resources for the KeypairConfig
+                return KeypairConfig(
                     name=server.keypair_name, private_filepath=key_file.name)
index a45167e..b624b09 100644 (file)
 import logging
 import uuid
 
+import time
 from cinderclient.exceptions import NotFound, BadRequest
 
-from snaps.openstack.create_qos import QoSSettings, Consumer
-from snaps.openstack.create_volume_type import (
-    VolumeTypeSettings, VolumeTypeEncryptionSettings, ControlLocation)
+from snaps.config.volume import VolumeConfig
+from snaps.config.volume_type import (
+    VolumeTypeConfig, ControlLocation, VolumeTypeEncryptionConfig)
+from snaps.config.qos import Consumer, QoSConfig
+from snaps.openstack import create_volume
+from snaps.openstack.create_qos import Consumer
 from snaps.openstack.tests import validation_utils
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
 from snaps.openstack.utils import cinder_utils
@@ -57,6 +61,113 @@ class CinderSmokeTests(OSComponentTestCase):
             cinder.volumes.list()
 
 
+class CinderUtilsVolumeTests(OSComponentTestCase):
+    """
+    Test for the CreateVolume class defined in create_volume.py
+    """
+
+    def setUp(self):
+        """
+        Instantiates the CreateVolume object that is responsible for
+        downloading and creating an OS volume file within OpenStack
+        """
+        guid = uuid.uuid4()
+        self.volume_name = self.__class__.__name__ + '-' + str(guid)
+        self.volume = None
+        self.cinder = cinder_utils.cinder_client(self.os_creds)
+
+    def tearDown(self):
+        """
+        Cleans the remote OpenStack objects
+        """
+        if self.volume:
+            try:
+                cinder_utils.delete_volume(self.cinder, self.volume)
+            except NotFound:
+                pass
+
+        self.assertTrue(volume_deleted(self.cinder, self.volume))
+
+    def test_create_simple_volume(self):
+        """
+        Tests the cinder_utils.create_volume()
+        """
+        volume_settings = VolumeConfig(name=self.volume_name)
+        self.volume = cinder_utils.create_volume(
+            self.cinder, volume_settings)
+        self.assertIsNotNone(self.volume)
+        self.assertEqual(self.volume_name, self.volume.name)
+
+        self.assertTrue(volume_active(self.cinder, self.volume))
+
+        volume = cinder_utils.get_volume(
+            self.cinder, volume_settings=volume_settings)
+        self.assertIsNotNone(volume)
+        validation_utils.objects_equivalent(self.volume, volume)
+
+    def test_create_delete_volume(self):
+        """
+        Tests the cinder_utils.create_volume()
+        """
+        volume_settings = VolumeConfig(name=self.volume_name)
+        self.volume = cinder_utils.create_volume(
+            self.cinder, volume_settings)
+        self.assertIsNotNone(self.volume)
+        self.assertEqual(self.volume_name, self.volume.name)
+
+        self.assertTrue(volume_active(self.cinder, self.volume))
+
+        volume = cinder_utils.get_volume(
+            self.cinder, volume_settings=volume_settings)
+        self.assertIsNotNone(volume)
+        validation_utils.objects_equivalent(self.volume, volume)
+
+        cinder_utils.delete_volume(self.cinder, self.volume)
+        self.assertTrue(volume_deleted(self.cinder, self.volume))
+        self.assertIsNone(
+            cinder_utils.get_volume(self.cinder, volume_settings))
+
+
+def volume_active(cinder, volume):
+    """
+    Returns true if volume becomes active
+    :param cinder:
+    :param volume:
+    :return:
+    """
+    end_time = time.time() + create_volume.VOLUME_ACTIVE_TIMEOUT
+    while time.time() < end_time:
+        status = cinder_utils.get_volume_status(cinder, volume)
+        if status == create_volume.STATUS_ACTIVE:
+            return True
+        elif status == create_volume.STATUS_FAILED:
+            return False
+        time.sleep(3)
+
+    return False
+
+
+def volume_deleted(cinder, volume):
+    """
+    Returns true if volume becomes active
+    :param cinder:
+    :param volume:
+    :return:
+    """
+    end_time = time.time() + create_volume.VOLUME_ACTIVE_TIMEOUT
+    while time.time() < end_time:
+        try:
+            status = cinder_utils.get_volume_status(cinder, volume)
+            if status == create_volume.STATUS_DELETED:
+                return True
+        except NotFound:
+            return True
+
+        time.sleep(3)
+
+    return False
+
+
 class CinderUtilsQoSTests(OSComponentTestCase):
     """
     Test for the CreateQos class defined in create_qos.py
@@ -86,8 +197,8 @@ class CinderUtilsQoSTests(OSComponentTestCase):
         """
         Tests the cinder_utils.create_qos()
         """
-        qos_settings = QoSSettings(name=self.qos_name, specs=self.specs,
-                                   consumer=Consumer.both)
+        qos_settings = QoSConfig(
+            name=self.qos_name, specs=self.specs, consumer=Consumer.both)
         self.qos = cinder_utils.create_qos(self.cinder, qos_settings)
         self.assertIsNotNone(self.qos)
 
@@ -103,8 +214,8 @@ class CinderUtilsQoSTests(OSComponentTestCase):
         """
         Tests the cinder_utils.create_qos()
         """
-        qos_settings = QoSSettings(name=self.qos_name, specs=self.specs,
-                                   consumer=Consumer.front_end)
+        qos_settings = QoSConfig(
+            name=self.qos_name, specs=self.specs, consumer=Consumer.front_end)
         self.qos = cinder_utils.create_qos(self.cinder, qos_settings)
         self.assertIsNotNone(self.qos)
 
@@ -120,8 +231,8 @@ class CinderUtilsQoSTests(OSComponentTestCase):
         """
         Tests the cinder_utils.create_qos()
         """
-        qos_settings = QoSSettings(name=self.qos_name, specs=self.specs,
-                                   consumer=Consumer.back_end)
+        qos_settings = QoSConfig(
+            name=self.qos_name, specs=self.specs, consumer=Consumer.back_end)
         self.qos = cinder_utils.create_qos(self.cinder, qos_settings)
         self.assertIsNotNone(self.qos)
 
@@ -137,7 +248,7 @@ class CinderUtilsQoSTests(OSComponentTestCase):
         """
         Tests the cinder_utils.create_qos()
         """
-        qos_settings = QoSSettings(name=self.qos_name, consumer=Consumer.both)
+        qos_settings = QoSConfig(name=self.qos_name, consumer=Consumer.both)
         self.qos = cinder_utils.create_qos(self.cinder, qos_settings)
         self.assertIsNotNone(self.qos)
         self.assertEqual(self.qos_name, self.qos.name)
@@ -165,7 +276,7 @@ class CinderUtilsSimpleVolumeTypeTests(OSComponentTestCase):
         """
         guid = uuid.uuid4()
         volume_type_name = self.__class__.__name__ + '-' + str(guid)
-        self.volume_type_settings = VolumeTypeSettings(name=volume_type_name)
+        self.volume_type_settings = VolumeTypeConfig(name=volume_type_name)
         self.volume_type = None
         self.cinder = cinder_utils.cinder_client(self.os_creds)
 
@@ -239,7 +350,7 @@ class CinderUtilsAddEncryptionTests(OSComponentTestCase):
 
         volume_type_name = self.__class__.__name__ + '-' + str(guid) + '-type'
         self.volume_type = cinder_utils.create_volume_type(
-            self.cinder, VolumeTypeSettings(name=volume_type_name))
+            self.cinder, VolumeTypeConfig(name=volume_type_name))
 
     def tearDown(self):
         """
@@ -263,7 +374,7 @@ class CinderUtilsAddEncryptionTests(OSComponentTestCase):
         Tests the cinder_utils.create_volume_encryption(),
         get_volume_encryption(), and get_volume_encryption_by_id()
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name=self.encryption_name, provider_class='foo',
             control_location=ControlLocation.front_end)
         self.encryption = cinder_utils.create_volume_encryption(
@@ -281,7 +392,7 @@ class CinderUtilsAddEncryptionTests(OSComponentTestCase):
         """
         Primarily tests the cinder_utils.delete_volume_type_encryption()
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name=self.encryption_name, provider_class='LuksEncryptor',
             control_location=ControlLocation.back_end)
         self.encryption = cinder_utils.create_volume_encryption(
@@ -307,7 +418,7 @@ class CinderUtilsAddEncryptionTests(OSComponentTestCase):
         Tests the cinder_utils.create_volume_encryption() with all valid
         settings
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name=self.encryption_name, provider_class='foo',
             cipher='bar', control_location=ControlLocation.back_end,
             key_size=1)
@@ -329,7 +440,7 @@ class CinderUtilsAddEncryptionTests(OSComponentTestCase):
         Tests the cinder_utils.create_volume_encryption() raises an exception
         when the provider class does not exist
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name=self.encryption_name, provider_class='foo',
             cipher='bar', control_location=ControlLocation.back_end,
             key_size=-1)
@@ -353,8 +464,8 @@ class CinderUtilsVolumeTypeCompleteTests(OSComponentTestCase):
         self.vol_type_name = self.__class__.__name__ + '-' + str(guid)
         self.specs = {'foo': 'bar'}
         self.cinder = cinder_utils.cinder_client(self.os_creds)
-        qos_settings = QoSSettings(name=self.qos_name, specs=self.specs,
-                                   consumer=Consumer.both)
+        qos_settings = QoSConfig(
+            name=self.qos_name, specs=self.specs, consumer=Consumer.both)
         self.qos = cinder_utils.create_qos(self.cinder, qos_settings)
         self.volume_type = None
 
@@ -385,10 +496,10 @@ class CinderUtilsVolumeTypeCompleteTests(OSComponentTestCase):
         Tests the cinder_utils.create_volume_type() where encryption has been
         configured
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name='foo', provider_class='bar',
             control_location=ControlLocation.back_end)
-        volume_type_settings = VolumeTypeSettings(
+        volume_type_settings = VolumeTypeConfig(
             name=self.vol_type_name, encryption=encryption_settings)
         self.volume_type = cinder_utils.create_volume_type(
             self.cinder, volume_type_settings)
@@ -404,7 +515,7 @@ class CinderUtilsVolumeTypeCompleteTests(OSComponentTestCase):
         """
         Tests the cinder_utils.create_volume_type() with an associated QoS Spec
         """
-        volume_type_settings = VolumeTypeSettings(
+        volume_type_settings = VolumeTypeConfig(
             name=self.vol_type_name, qos_spec_name=self.qos_name)
         self.volume_type = cinder_utils.create_volume_type(
             self.cinder, volume_type_settings)
@@ -419,7 +530,7 @@ class CinderUtilsVolumeTypeCompleteTests(OSComponentTestCase):
         Tests the cinder_utils.create_volume_type() when the QoS Spec name
         does not exist
         """
-        volume_type_settings = VolumeTypeSettings(
+        volume_type_settings = VolumeTypeConfig(
             name=self.vol_type_name, qos_spec_name='foo')
 
         self.volume_type = cinder_utils.create_volume_type(
@@ -432,10 +543,10 @@ class CinderUtilsVolumeTypeCompleteTests(OSComponentTestCase):
         Tests the cinder_utils.create_volume_type() with encryption and an
         associated QoS Spec
         """
-        encryption_settings = VolumeTypeEncryptionSettings(
+        encryption_settings = VolumeTypeEncryptionConfig(
             name='foo', provider_class='bar',
             control_location=ControlLocation.back_end)
-        volume_type_settings = VolumeTypeSettings(
+        volume_type_settings = VolumeTypeConfig(
             name=self.vol_type_name, qos_spec_name=self.qos_name,
             encryption=encryption_settings)
         self.volume_type = cinder_utils.create_volume_type(
index 44235ff..67fbdec 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
+import os
+
 import pkg_resources
 import uuid
 
 import time
 
-from snaps.openstack import create_stack
-from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings
+import snaps.config.stack as stack_config
+from snaps.config.flavor import FlavorConfig
+from snaps.openstack.create_flavor import OpenStackFlavor
 
 from snaps.openstack.create_image import OpenStackImage
 from snaps.openstack.create_instance import OpenStackVmInstance
-from snaps.openstack.create_stack import StackSettings
+from snaps.openstack.create_stack import StackConfig
 from snaps.openstack.tests import openstack_tests
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
 from snaps.openstack.utils import (
-    heat_utils, neutron_utils, nova_utils, settings_utils, glance_utils)
+    heat_utils, neutron_utils, nova_utils, settings_utils, glance_utils,
+    cinder_utils)
 
 __author__ = 'spisarski'
 
@@ -93,7 +97,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
         # Create Flavor
         self.flavor_creator = OpenStackFlavor(
             self.os_creds,
-            FlavorSettings(name=guid + '-flavor', ram=256, disk=10, vcpus=1))
+            FlavorConfig(name=guid + '-flavor', ram=256, disk=10, vcpus=1))
         self.flavor_creator.create()
 
         env_values = {'image_name': self.image_creator.image_settings.name,
@@ -103,10 +107,10 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
                       'inst_name': self.vm_inst_name}
         heat_tmplt_path = pkg_resources.resource_filename(
             'snaps.openstack.tests.heat', 'test_heat_template.yaml')
-        self.stack_settings1 = StackSettings(
+        self.stack_settings1 = StackConfig(
             name=stack_name1, template_path=heat_tmplt_path,
             env_values=env_values)
-        self.stack_settings2 = StackSettings(
+        self.stack_settings2 = StackConfig(
             name=stack_name2, template_path=heat_tmplt_path,
             env_values=env_values)
         self.stack1 = None
@@ -115,7 +119,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
 
     def tearDown(self):
         """
-        Cleans the image and downloaded image file
+        Cleans the stack and image
         """
         if self.stack1:
             try:
@@ -160,7 +164,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
                                                    self.stack1.id)
         self.assertEqual(self.stack1, stack_query_3)
 
-        resources = heat_utils.get_resources(self.heat_client, self.stack1)
+        resources = heat_utils.get_resources(self.heat_client, self.stack1.id)
         self.assertIsNotNone(resources)
         self.assertEqual(4, len(resources))
 
@@ -168,22 +172,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
         self.assertIsNotNone(outputs)
         self.assertEqual(0, len(outputs))
 
-        # Wait until stack deployment has completed
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack1.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack1))
 
         neutron = neutron_utils.neutron_client(self.os_creds)
         networks = heat_utils.get_stack_networks(
@@ -198,7 +187,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
 
         nova = nova_utils.nova_client(self.os_creds)
         servers = heat_utils.get_stack_servers(
-            self.heat_client, nova, self.stack1)
+            self.heat_client, nova, neutron, self.stack1)
         self.assertIsNotNone(servers)
         self.assertEqual(1, len(servers))
         self.assertEqual(self.vm_inst_name, servers[0].name)
@@ -222,21 +211,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
                                                     self.stack1.id)
         self.assertEqual(self.stack1, stack1_query_3)
 
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack1.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack1))
 
         self.stack2 = heat_utils.create_stack(self.heat_client,
                                               self.stack_settings2)
@@ -253,22 +228,7 @@ class HeatUtilsCreateSimpleStackTests(OSComponentTestCase):
                                                     self.stack2.id)
         self.assertEqual(self.stack2, stack2_query_3)
 
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack2.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
-
-            time.sleep(3)
-
-        self.assertTrue(is_active)
+        self.assertTrue(stack_active(self.heat_client, self.stack2))
 
 
 class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
@@ -312,45 +272,35 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
                       'external_net_name': self.ext_net_name}
         heat_tmplt_path = pkg_resources.resource_filename(
             'snaps.openstack.tests.heat', 'floating_ip_heat_template.yaml')
-        stack_settings = StackSettings(
+        stack_settings = StackConfig(
             name=stack_name, template_path=heat_tmplt_path,
             env_values=env_values)
         self.heat_client = heat_utils.heat_client(self.os_creds)
         self.stack = heat_utils.create_stack(self.heat_client, stack_settings)
 
-        # Wait until stack deployment has completed
-        end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
-        is_active = False
-        while time.time() < end_time:
-            status = heat_utils.get_stack_status(self.heat_client,
-                                                 self.stack.id)
-            if status == create_stack.STATUS_CREATE_COMPLETE:
-                is_active = True
-                break
-            elif status == create_stack.STATUS_CREATE_FAILED:
-                is_active = False
-                break
+        self.assertTrue(stack_active(self.heat_client, self.stack))
 
-            time.sleep(3)
-        self.assertTrue(is_active)
+        self.keypair1_settings = None
+        self.keypair2_settings = None
 
     def tearDown(self):
         """
-        Cleans the image and downloaded image file
+        Cleans the stack and image
         """
         if self.stack:
             try:
                 heat_utils.delete_stack(self.heat_client, self.stack)
                 # Wait until stack deployment has completed
-                end_time = time.time() + create_stack.STACK_COMPLETE_TIMEOUT
+                end_time = (time.time() +
+                            stack_config.STACK_COMPLETE_TIMEOUT)
                 is_deleted = False
                 while time.time() < end_time:
                     status = heat_utils.get_stack_status(self.heat_client,
                                                          self.stack.id)
-                    if status == create_stack.STATUS_DELETE_COMPLETE:
+                    if status == stack_config.STATUS_DELETE_COMPLETE:
                         is_deleted = True
                         break
-                    elif status == create_stack.STATUS_DELETE_FAILED:
+                    elif status == stack_config.STATUS_DELETE_FAILED:
                         is_deleted = False
                         break
 
@@ -361,11 +311,11 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
                     neutron = neutron_utils.neutron_client(self.os_creds)
                     glance = glance_utils.glance_client(self.os_creds)
                     servers = heat_utils.get_stack_servers(
-                        self.heat_client, nova, self.stack)
+                        self.heat_client, nova, neutron, self.stack)
                     for server in servers:
-                        vm_settings = settings_utils.create_vm_inst_settings(
+                        vm_settings = settings_utils.create_vm_inst_config(
                             nova, neutron, server)
-                        img_settings = settings_utils.determine_image_settings(
+                        img_settings = settings_utils.determine_image_config(
                             glance, server,
                             [self.image_creator1.image_settings,
                              self.image_creator2.image_settings])
@@ -392,14 +342,26 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
             except:
                 pass
 
+        if self.keypair1_settings:
+            expanded_path = os.path.expanduser(
+                self.keypair1_settings.private_filepath)
+            os.chmod(expanded_path, 0o755)
+            os.remove(expanded_path)
+
+        if self.keypair2_settings:
+            expanded_path = os.path.expanduser(
+                self.keypair2_settings.private_filepath)
+            os.chmod(expanded_path, 0o755)
+            os.remove(expanded_path)
+
     def test_get_settings_from_stack(self):
         """
         Tests that a heat template with floating IPs and can have the proper
         settings derived from settings_utils.py.
         """
-        resources = heat_utils.get_resources(self.heat_client, self.stack)
+        resources = heat_utils.get_resources(self.heat_client, self.stack.id)
         self.assertIsNotNone(resources)
-        self.assertEqual(11, len(resources))
+        self.assertEqual(12, len(resources))
 
         options = heat_utils.get_outputs(self.heat_client, self.stack)
         self.assertIsNotNone(options)
@@ -412,7 +374,7 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
         self.assertEqual(1, len(networks))
         self.assertEqual(self.network_name, networks[0].name)
 
-        network_settings = settings_utils.create_network_settings(
+        network_settings = settings_utils.create_network_config(
             neutron, networks[0])
         self.assertIsNotNone(network_settings)
         self.assertEqual(self.network_name, network_settings.name)
@@ -421,11 +383,11 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
         glance = glance_utils.glance_client(self.os_creds)
 
         servers = heat_utils.get_stack_servers(
-            self.heat_client, nova, self.stack)
+            self.heat_client, nova, neutron, self.stack)
         self.assertIsNotNone(servers)
         self.assertEqual(2, len(servers))
 
-        image_settings = settings_utils.determine_image_settings(
+        image_settings = settings_utils.determine_image_config(
             glance, servers[0],
             [self.image_creator1.image_settings,
              self.image_creator2.image_settings])
@@ -438,7 +400,7 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
             self.assertEqual(
                 self.image_creator2.image_settings.name, image_settings.name)
 
-        image_settings = settings_utils.determine_image_settings(
+        image_settings = settings_utils.determine_image_config(
             glance, servers[1],
             [self.image_creator1.image_settings,
              self.image_creator2.image_settings])
@@ -449,14 +411,398 @@ class HeatUtilsCreateComplexStackTests(OSComponentTestCase):
             self.assertEqual(
                 self.image_creator2.image_settings.name, image_settings.name)
 
-        keypair1_settings = settings_utils.determine_keypair_settings(
+        self.keypair1_settings = settings_utils.determine_keypair_config(
             self.heat_client, self.stack, servers[0],
             priv_key_key='private_key')
-        self.assertIsNotNone(keypair1_settings)
-        self.assertEqual(self.keypair_name, keypair1_settings.name)
+        self.assertIsNotNone(self.keypair1_settings)
+        self.assertEqual(self.keypair_name, self.keypair1_settings.name)
 
-        keypair2_settings = settings_utils.determine_keypair_settings(
+        self.keypair2_settings = settings_utils.determine_keypair_config(
             self.heat_client, self.stack, servers[1],
             priv_key_key='private_key')
-        self.assertIsNotNone(keypair2_settings)
-        self.assertEqual(self.keypair_name, keypair2_settings.name)
+        self.assertIsNotNone(self.keypair2_settings)
+        self.assertEqual(self.keypair_name, self.keypair2_settings.name)
+
+
+class HeatUtilsRouterTests(OSComponentTestCase):
+    """
+    Test Heat volume functionality
+    """
+
+    def setUp(self):
+        """
+        Instantiates OpenStack instances that cannot be spawned by Heat
+        """
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        stack_name = guid + '-stack'
+
+        self.net_name = guid + '-net'
+        self.subnet_name = guid + '-subnet'
+        self.router_name = guid + '-router'
+
+        env_values = {
+            'net_name': self.net_name,
+            'subnet_name': self.subnet_name,
+            'router_name': self.router_name,
+            'external_net_name': self.ext_net_name}
+
+        heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'router_heat_template.yaml')
+        self.stack_settings = StackConfig(
+            name=stack_name, template_path=heat_tmplt_path,
+            env_values=env_values)
+        self.stack = None
+        self.heat_client = heat_utils.heat_client(self.os_creds)
+        self.neutron = neutron_utils.neutron_client(self.os_creds)
+
+    def tearDown(self):
+        """
+        Cleans the image and downloaded image file
+        """
+        if self.stack:
+            try:
+                heat_utils.delete_stack(self.heat_client, self.stack)
+            except:
+                pass
+
+    def test_create_router_with_stack(self):
+        """
+        Tests the creation of an OpenStack router with Heat and the retrieval
+        of the Router Domain objects from heat_utils#get_stack_routers().
+        """
+        self.stack = heat_utils.create_stack(
+            self.heat_client, self.stack_settings)
+
+        # Wait until stack deployment has completed
+        end_time = time.time() + stack_config.STACK_COMPLETE_TIMEOUT
+        is_active = False
+        while time.time() < end_time:
+            status = heat_utils.get_stack_status(self.heat_client,
+                                                 self.stack.id)
+            if status == stack_config.STATUS_CREATE_COMPLETE:
+                is_active = True
+                break
+            elif status == stack_config.STATUS_CREATE_FAILED:
+                is_active = False
+                break
+
+            time.sleep(3)
+
+        self.assertTrue(is_active)
+
+        routers = heat_utils.get_stack_routers(
+            self.heat_client, self.neutron, self.stack)
+
+        self.assertEqual(1, len(routers))
+
+        router = routers[0]
+        self.assertEqual(self.router_name, router.name)
+
+        ext_net = neutron_utils.get_network(
+            self.neutron, network_name=self.ext_net_name)
+        self.assertEqual(ext_net.id, router.external_network_id)
+
+
+class HeatUtilsVolumeTests(OSComponentTestCase):
+    """
+    Test Heat volume functionality
+    """
+
+    def setUp(self):
+        """
+        Instantiates OpenStack instances that cannot be spawned by Heat
+        """
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        stack_name = guid + '-stack'
+        self.volume_name = guid + '-vol'
+        self.volume_type_name = guid + '-vol-type'
+
+        env_values = {
+            'volume_name': self.volume_name,
+            'volume_type_name': self.volume_type_name}
+
+        heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'volume_heat_template.yaml')
+        self.stack_settings = StackConfig(
+            name=stack_name, template_path=heat_tmplt_path,
+            env_values=env_values)
+        self.stack = None
+        self.heat_client = heat_utils.heat_client(self.os_creds)
+        self.cinder = cinder_utils.cinder_client(self.os_creds)
+
+    def tearDown(self):
+        """
+        Cleans the stack
+        """
+        if self.stack:
+            try:
+                heat_utils.delete_stack(self.heat_client, self.stack)
+            except:
+                pass
+
+    def test_create_vol_with_stack(self):
+        """
+        Tests the creation of an OpenStack volume with Heat.
+        """
+        self.stack = heat_utils.create_stack(
+            self.heat_client, self.stack_settings)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
+
+        volumes = heat_utils.get_stack_volumes(
+            self.heat_client, self.cinder, self.stack)
+
+        self.assertEqual(1, len(volumes))
+
+        volume = volumes[0]
+        self.assertEqual(self.volume_name, volume.name)
+        self.assertEqual(self.volume_type_name, volume.type)
+        self.assertEqual(1, volume.size)
+        self.assertEqual(False, volume.multi_attach)
+
+    def test_create_vol_types_with_stack(self):
+        """
+        Tests the creation of an OpenStack volume with Heat.
+        """
+        self.stack = heat_utils.create_stack(
+            self.heat_client, self.stack_settings)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
+
+        volume_types = heat_utils.get_stack_volume_types(
+            self.heat_client, self.cinder, self.stack)
+
+        self.assertEqual(1, len(volume_types))
+
+        volume_type = volume_types[0]
+
+        self.assertEqual(self.volume_type_name, volume_type.name)
+        self.assertTrue(volume_type.public)
+        self.assertIsNone(volume_type.qos_spec)
+
+        # TODO - Add encryption back and find out why it broke in Pike
+        # encryption = volume_type.encryption
+        # self.assertIsNotNone(encryption)
+        # self.assertIsNone(encryption.cipher)
+        # self.assertEqual('front-end', encryption.control_location)
+        # self.assertIsNone(encryption.key_size)
+        # self.assertEqual(u'nova.volume.encryptors.luks.LuksEncryptor',
+        #                  encryption.provider)
+        # self.assertEqual(volume_type.id, encryption.volume_type_id)
+
+
+class HeatUtilsFlavorTests(OSComponentTestCase):
+    """
+    Test Heat volume functionality
+    """
+
+    def setUp(self):
+        """
+        Instantiates OpenStack instances that cannot be spawned by Heat
+        """
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.name_prefix = guid
+        stack_name = guid + '-stack'
+
+        heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'flavor_heat_template.yaml')
+        self.stack_settings = StackConfig(
+            name=stack_name, template_path=heat_tmplt_path)
+        self.stack = None
+        self.heat_client = heat_utils.heat_client(self.os_creds)
+        self.nova = nova_utils.nova_client(self.os_creds)
+
+    def tearDown(self):
+        """
+        Cleans the stack
+        """
+        if self.stack:
+            try:
+                heat_utils.delete_stack(self.heat_client, self.stack)
+            except:
+                pass
+
+    def test_create_flavor_with_stack(self):
+        """
+        Tests the creation of an OpenStack volume with Heat.
+        """
+        self.stack = heat_utils.create_stack(
+            self.heat_client, self.stack_settings)
+
+        self.assertTrue(stack_active(self.heat_client, self.stack))
+
+        flavors = heat_utils.get_stack_flavors(
+            self.heat_client, self.nova, self.stack)
+
+        self.assertEqual(1, len(flavors))
+
+        flavor = flavors[0]
+        self.assertTrue(flavor.name.startswith(self.name_prefix))
+        self.assertEqual(1024, flavor.ram)
+        self.assertEqual(200, flavor.disk)
+        self.assertEqual(8, flavor.vcpus)
+        self.assertEqual(0, flavor.ephemeral)
+        self.assertIsNone(flavor.swap)
+        self.assertEqual(1.0, flavor.rxtx_factor)
+        self.assertTrue(flavor.is_public)
+
+
+class HeatUtilsKeypairTests(OSComponentTestCase):
+    """
+    Test Heat volume functionality
+    """
+
+    def setUp(self):
+        """
+        Instantiates OpenStack instances that cannot be spawned by Heat
+        """
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        stack_name = guid + '-stack'
+        self.keypair_name = guid + '-kp'
+
+        env_values = {'keypair_name': self.keypair_name}
+
+        heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'keypair_heat_template.yaml')
+        self.stack_settings = StackConfig(
+            name=stack_name, template_path=heat_tmplt_path,
+            env_values=env_values)
+        self.stack = None
+        self.heat_client = heat_utils.heat_client(self.os_creds)
+        self.nova = nova_utils.nova_client(self.os_creds)
+
+    def tearDown(self):
+        """
+        Cleans the stack
+        """
+        if self.stack:
+            try:
+                heat_utils.delete_stack(self.heat_client, self.stack)
+            except:
+                pass
+
+    def test_create_keypair_with_stack(self):
+        """
+        Tests the creation of an OpenStack keypair with Heat.
+        """
+        self.stack = heat_utils.create_stack(
+            self.heat_client, self.stack_settings)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
+
+        keypairs = heat_utils.get_stack_keypairs(
+            self.heat_client, self.nova, self.stack)
+
+        self.assertEqual(1, len(keypairs))
+        keypair = keypairs[0]
+
+        self.assertEqual(self.keypair_name, keypair.name)
+
+        outputs = heat_utils.get_outputs(self.heat_client, self.stack)
+
+        for output in outputs:
+            if output.key == 'private_key':
+                self.assertTrue(output.value.startswith(
+                    '-----BEGIN RSA PRIVATE KEY-----'))
+
+        keypair = nova_utils.get_keypair_by_id(self.nova, keypair.id)
+        self.assertIsNotNone(keypair)
+
+        self.assertEqual(self.keypair_name, keypair.name)
+
+
+class HeatUtilsSecurityGroupTests(OSComponentTestCase):
+    """
+    Test Heat volume functionality
+    """
+
+    def setUp(self):
+        """
+        Instantiates OpenStack instances that cannot be spawned by Heat
+        """
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        stack_name = guid + '-stack'
+        self.sec_grp_name = guid + '-sec-grp'
+
+        env_values = {'security_group_name': self.sec_grp_name}
+
+        heat_tmplt_path = pkg_resources.resource_filename(
+            'snaps.openstack.tests.heat', 'security_group_heat_template.yaml')
+        self.stack_settings = StackConfig(
+            name=stack_name, template_path=heat_tmplt_path,
+            env_values=env_values)
+        self.stack = None
+        self.heat_client = heat_utils.heat_client(self.os_creds)
+        self.neutron = neutron_utils.neutron_client(self.os_creds)
+
+    def tearDown(self):
+        """
+        Cleans the stack
+        """
+        if self.stack:
+            try:
+                heat_utils.delete_stack(self.heat_client, self.stack)
+            except:
+                pass
+
+    def test_create_security_group_with_stack(self):
+        """
+        Tests the creation of an OpenStack SecurityGroup with Heat.
+        """
+        self.stack = heat_utils.create_stack(
+            self.heat_client, self.stack_settings)
+        self.assertTrue(stack_active(self.heat_client, self.stack))
+
+        sec_grp = heat_utils.get_stack_security_groups(
+            self.heat_client, self.neutron, self.stack)[0]
+
+        self.assertEqual(self.sec_grp_name, sec_grp.name)
+        self.assertEqual('Test description', sec_grp.description)
+        self.assertEqual(2, len(sec_grp.rules))
+
+        has_ssh_rule = False
+        has_icmp_rule = False
+
+        for rule in sec_grp.rules:
+            if (rule.security_group_id == sec_grp.id
+                    and rule.direction == 'egress'
+                    and rule.ethertype == 'IPv4'
+                    and rule.port_range_min == 22
+                    and rule.port_range_max == 22
+                    and rule.protocol == 'tcp'
+                    and rule.remote_group_id is None
+                    and rule.remote_ip_prefix == '0.0.0.0/0'):
+                has_ssh_rule = True
+            if (rule.security_group_id == sec_grp.id
+                    and rule.direction == 'ingress'
+                    and rule.ethertype == 'IPv4'
+                    and rule.port_range_min is None
+                    and rule.port_range_max is None
+                    and rule.protocol == 'icmp'
+                    and rule.remote_group_id is None
+                    and rule.remote_ip_prefix == '0.0.0.0/0'):
+                has_icmp_rule = True
+
+        self.assertTrue(has_ssh_rule)
+        self.assertTrue(has_icmp_rule)
+
+
+def stack_active(heat_cli, stack):
+    """
+    Blocks until stack application has successfully completed or failed
+    :param heat_cli: the Heat client
+    :param stack: the Stack domain object
+    :return: T/F
+    """
+    # Wait until stack deployment has completed
+    end_time = time.time() + stack_config.STACK_COMPLETE_TIMEOUT
+    is_active = False
+    while time.time() < end_time:
+        status = heat_utils.get_stack_status(heat_cli, stack.id)
+        if status == stack_config.STATUS_CREATE_COMPLETE:
+            is_active = True
+            break
+        elif status == stack_config.STATUS_CREATE_FAILED:
+            is_active = False
+            break
+
+        time.sleep(3)
+
+    return is_active
index bd0086b..b7f024d 100644 (file)
@@ -14,8 +14,8 @@
 # limitations under the License.
 import uuid
 
-from snaps.openstack.create_project import ProjectSettings
-from snaps.openstack.create_user import UserSettings
+from snaps.config.project import ProjectConfig
+from snaps.config.user import UserConfig
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
 from snaps.openstack.utils import keystone_utils, neutron_utils
 
@@ -96,7 +96,7 @@ class KeystoneUtilsTests(OSComponentTestCase):
         """
         Tests the keystone_utils.create_user() function
         """
-        user_settings = UserSettings(
+        user_settings = UserConfig(
             name=self.username,
             password=str(uuid.uuid4()),
             domain_name=self.os_creds.user_domain_name)
@@ -111,7 +111,7 @@ class KeystoneUtilsTests(OSComponentTestCase):
         """
         Tests the keyston_utils.create_project() funtion
         """
-        project_settings = ProjectSettings(
+        project_settings = ProjectConfig(
             name=self.project_name, domain=self.os_creds.project_domain_name)
         self.project = keystone_utils.create_project(self.keystone,
                                                      project_settings)
@@ -180,13 +180,13 @@ class KeystoneUtilsTests(OSComponentTestCase):
         Tests the keystone_utils function grant_user_role_to_project()
         :return:
         """
-        user_settings = UserSettings(
+        user_settings = UserConfig(
             name=self.username, password=str(uuid.uuid4()),
             domain_name=self.os_creds.user_domain_name)
         self.user = keystone_utils.create_user(self.keystone, user_settings)
         self.assertEqual(self.username, self.user.name)
 
-        project_settings = ProjectSettings(
+        project_settings = ProjectConfig(
             name=self.project_name, domain=self.os_creds.project_domain_name)
         self.project = keystone_utils.create_project(self.keystone,
                                                      project_settings)
diff --git a/snaps/openstack/utils/tests/magnum_utils_tests.py b/snaps/openstack/utils/tests/magnum_utils_tests.py
new file mode 100644 (file)
index 0000000..f841c48
--- /dev/null
@@ -0,0 +1,299 @@
+# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
+#                    and others.  All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import logging
+import uuid
+
+from magnumclient.common.apiclient.exceptions import BadRequest
+
+from snaps.config.cluster_template import (
+    ClusterTemplateConfig, ServerType,  ContainerOrchestrationEngine,
+    DockerStorageDriver)
+from snaps.config.flavor import FlavorConfig
+from snaps.config.keypair import KeypairConfig
+from snaps.openstack.create_flavor import OpenStackFlavor
+from snaps.openstack.create_image import OpenStackImage
+from snaps.openstack.create_keypairs import OpenStackKeypair
+from snaps.openstack.os_credentials import OSCreds
+from snaps.openstack.tests import openstack_tests
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
+from snaps.openstack.utils import magnum_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('magnum_utils_tests')
+
+
+class MagnumSmokeTests(OSComponentTestCase):
+    """
+    Tests to ensure that the magnum client can communicate with the cloud
+    """
+
+    def test_connect_success(self):
+        """
+        Tests to ensure that the proper credentials can connect.
+        """
+        magnum = magnum_utils.magnum_client(self.os_creds)
+
+        # This should not throw an exception
+        self.assertIsNotNone(magnum.clusters.list())
+
+    def test_nova_connect_fail(self):
+        """
+        Tests to ensure that the improper credentials cannot connect.
+        """
+
+        with self.assertRaises(RuntimeError):
+            magnum_utils.magnum_client(
+                OSCreds(username='user', password='pass',
+                        auth_url=self.os_creds.auth_url,
+                        project_name=self.os_creds.project_name,
+                        proxy_settings=self.os_creds.proxy_settings))
+
+
+class MagnumUtilsClusterTypeTests(OSComponentTestCase):
+    """
+    Tests individual functions within magnum_utils.py
+    """
+
+    def setUp(self):
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.cluster_type_name = self.guid + '-cluster-type'
+        self.magnum = magnum_utils.magnum_client(self.os_creds)
+
+        metadata = self.image_metadata
+        if not metadata:
+            metadata = dict()
+        if 'extra_properties' not in metadata:
+            metadata['extra_properties'] = dict()
+        metadata['extra_properties']['os_distro'] = 'cirros'
+
+        os_image_settings = openstack_tests.cirros_image_settings(
+            name=self.guid + '-image', image_metadata=metadata)
+
+        self.image_creator = OpenStackImage(self.os_creds, os_image_settings)
+
+        self.flavor_creator = OpenStackFlavor(
+            self.os_creds, FlavorConfig(
+                name=self.guid + '-flavor', ram=512, disk=10, vcpus=1))
+
+        keypair_priv_filepath = 'tmp/' + self.guid
+        keypair_pub_filepath = keypair_priv_filepath + '.pub'
+
+        self.keypair_creator = OpenStackKeypair(
+            self.os_creds, KeypairConfig(
+                name=self.guid + '-keypair',
+                public_filepath=keypair_pub_filepath,
+                private_filepath=keypair_priv_filepath))
+
+        self.cluster_template = None
+
+        try:
+            self.image_creator.create()
+            self.flavor_creator.create()
+            self.keypair_creator.create()
+        except:
+            self.tearDown()
+            raise
+
+    def tearDown(self):
+        if self.cluster_template:
+            try:
+                magnum_utils.delete_cluster_template(
+                    self.magnum, self.cluster_template.id)
+            except:
+                pass
+        if self.keypair_creator:
+            try:
+                self.keypair_creator.clean()
+            except:
+                pass
+        if self.flavor_creator:
+            try:
+                self.flavor_creator.clean()
+            except:
+                pass
+        if self.image_creator:
+            try:
+                self.image_creator.clean()
+            except:
+                pass
+
+    def test_create_cluster_template_simple(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name)
+
+        self.cluster_template = magnum_utils.create_cluster_template(
+            self.magnum, config)
+        self.assertIsNotNone(self.cluster_template)
+        self.assertTrue(
+            validate_cluster_template(config, self.cluster_template))
+
+        template_by_name = magnum_utils.get_cluster_template(
+            self.magnum, template_name=config.name)
+        self.assertEqual(self.cluster_template, template_by_name)
+        template_by_id = magnum_utils.get_cluster_template_by_id(
+            self.magnum, self.cluster_template.id)
+        self.assertEqual(self.cluster_template, template_by_id)
+
+    def test_create_cluster_template_all(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            network_driver='flannel', external_net=self.ext_net_name,
+            floating_ip_enabled=True, docker_volume_size=100,
+            server_type=ServerType.vm,
+            flavor=self.flavor_creator.flavor_settings.name,
+            master_flavor=self.flavor_creator.flavor_settings.name,
+            coe=ContainerOrchestrationEngine.kubernetes,
+            fixed_net='foo', fixed_subnet='bar',
+            registry_enabled=True, insecure_registry='localhost',
+            docker_storage_driver=DockerStorageDriver.overlay,
+            dns_nameserver='8.8.4.4', public=True, tls_disabled=True,
+            http_proxy=None, https_proxy=None, volume_driver='cinder',
+            master_lb_enabled=False, labels={'foo': 'bar'})
+
+        self.cluster_template = magnum_utils.create_cluster_template(
+            self.magnum, config)
+        self.assertIsNotNone(self.cluster_template)
+        self.assertTrue(
+            validate_cluster_template(config, self.cluster_template))
+
+        template_by_name = magnum_utils.get_cluster_template(
+            self.magnum, template_name=config.name)
+        self.assertEqual(self.cluster_template, template_by_name)
+        template_by_id = magnum_utils.get_cluster_template_by_id(
+            self.magnum, self.cluster_template.id)
+        self.assertEqual(self.cluster_template, template_by_id)
+
+    def test_create_cluster_template_bad_image(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image='foo',
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name)
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template = magnum_utils.create_cluster_template(
+                self.magnum, config)
+
+    def test_create_cluster_template_bad_ext_net(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net='foo',
+            flavor=self.flavor_creator.flavor_settings.name)
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template = magnum_utils.create_cluster_template(
+                self.magnum, config)
+
+    def test_create_cluster_template_bad_flavor(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor='foo')
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template = magnum_utils.create_cluster_template(
+                self.magnum, config)
+
+    def test_create_cluster_template_bad_master_flavor(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            flavor=self.flavor_creator.flavor_settings.name,
+            master_flavor='foo')
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template = magnum_utils.create_cluster_template(
+                self.magnum, config)
+
+    def test_create_cluster_template_bad_network_driver(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            network_driver='foo')
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template = magnum_utils.create_cluster_template(
+                self.magnum, config)
+
+    def test_create_cluster_template_bad_volume_driver(self):
+        config = ClusterTemplateConfig(
+            name=self.cluster_type_name,
+            image=self.image_creator.image_settings.name,
+            keypair=self.keypair_creator.keypair_settings.name,
+            external_net=self.ext_net_name,
+            volume_driver='foo')
+
+        with self.assertRaises(BadRequest):
+            self.cluster_template = magnum_utils.create_cluster_template(
+                self.magnum, config)
+
+
+def validate_cluster_template(tmplt_config, tmplt_obj):
+    """
+    Returns true if the configuration matches the ClusterTemplate object
+    :param tmplt_config: the ClusterTemplateConfig object
+    :param tmplt_obj: the ClusterTemplate domain object
+    :return: T/F
+    """
+    if not tmplt_config.network_driver:
+        network_driver = 'flannel'
+    else:
+        network_driver = tmplt_config.network_driver
+
+    return (
+        tmplt_config.coe.value == tmplt_obj.coe and
+        tmplt_config.dns_nameserver == tmplt_obj.dns_nameserver and
+        tmplt_config.docker_storage_driver.value
+        == tmplt_obj.docker_storage_driver and
+        tmplt_config.docker_volume_size == tmplt_obj.docker_volume_size and
+        tmplt_config.external_net == tmplt_obj.external_net and
+        tmplt_config.fixed_net == tmplt_obj.fixed_net and
+        tmplt_config.fixed_subnet == tmplt_obj.fixed_subnet and
+        tmplt_config.flavor == tmplt_obj.flavor and
+        tmplt_config.floating_ip_enabled == tmplt_obj.floating_ip_enabled and
+        tmplt_config.http_proxy == tmplt_obj.http_proxy and
+        tmplt_config.https_proxy == tmplt_obj.https_proxy and
+        tmplt_config.no_proxy == tmplt_obj.no_proxy and
+        tmplt_config.image == tmplt_obj.image and
+        tmplt_config.insecure_registry == tmplt_obj.insecure_registry and
+        tmplt_config.keypair == tmplt_obj.keypair and
+        tmplt_config.labels == tmplt_obj.labels and
+        tmplt_config.master_flavor == tmplt_obj.master_flavor and
+        tmplt_config.master_lb_enabled == tmplt_obj.master_lb_enabled and
+        tmplt_config.name == tmplt_obj.name and
+        network_driver == tmplt_obj.network_driver and
+        tmplt_config.no_proxy == tmplt_obj.no_proxy and
+        tmplt_config.public == tmplt_obj.public and
+        tmplt_config.registry_enabled == tmplt_obj.registry_enabled and
+        tmplt_config.server_type.value == tmplt_obj.server_type and
+        tmplt_config.tls_disabled == tmplt_obj.tls_disabled and
+        tmplt_config.volume_driver == tmplt_obj.volume_driver
+    )
index 05d508d..38faf71 100644 (file)
 # limitations under the License.
 import uuid
 
-from snaps.openstack import create_router
-from snaps.openstack.create_network import NetworkSettings, SubnetSettings, \
-    PortSettings
-from snaps.openstack.create_security_group import SecurityGroupSettings, \
-    SecurityGroupRuleSettings, Direction
+from neutronclient.common.exceptions import NotFound, BadRequest
+
+from snaps.config.network import NetworkConfig, SubnetConfig, PortConfig
+from snaps.config.security_group import (
+    SecurityGroupConfig, SecurityGroupRuleConfig, Direction)
 from snaps.openstack.tests import openstack_tests
 from snaps.openstack.tests import validation_utils
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
@@ -102,7 +102,7 @@ class NeutronUtilsNetworkTests(OSComponentTestCase):
 
     def test_create_network(self):
         """
-        Tests the neutron_utils.create_neutron_net() function
+        Tests the neutron_utils.create_network() function
         """
         self.network = neutron_utils.create_network(
             self.neutron, self.os_creds, self.net_config.network_settings)
@@ -110,26 +110,28 @@ class NeutronUtilsNetworkTests(OSComponentTestCase):
                          self.network.name)
         self.assertTrue(validate_network(
             self.neutron, self.net_config.network_settings.name, True))
+        self.assertEqual(len(self.net_config.network_settings.subnet_settings),
+                         len(self.network.subnets))
 
     def test_create_network_empty_name(self):
         """
-        Tests the neutron_utils.create_neutron_net() function with an empty
+        Tests the neutron_utils.create_network() function with an empty
         network name
         """
         with self.assertRaises(Exception):
             self.network = neutron_utils.create_network(
                 self.neutron, self.os_creds,
-                network_settings=NetworkSettings(name=''))
+                network_settings=NetworkConfig(name=''))
 
     def test_create_network_null_name(self):
         """
-        Tests the neutron_utils.create_neutron_net() function when the network
+        Tests the neutron_utils.create_network() function when the network
         name is None
         """
         with self.assertRaises(Exception):
             self.network = neutron_utils.create_network(
                 self.neutron, self.os_creds,
-                network_settings=NetworkSettings())
+                network_settings=NetworkConfig())
 
 
 class NeutronUtilsSubnetTests(OSComponentTestCase):
@@ -142,7 +144,6 @@ class NeutronUtilsSubnetTests(OSComponentTestCase):
         self.port_name = str(guid) + '-port'
         self.neutron = neutron_utils.neutron_client(self.os_creds)
         self.network = None
-        self.subnet = None
         self.net_config = openstack_tests.get_pub_net_config(
             net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet',
             external_net=self.ext_net_name)
@@ -151,11 +152,6 @@ class NeutronUtilsSubnetTests(OSComponentTestCase):
         """
         Cleans the remote OpenStack objects
         """
-        if self.subnet:
-            try:
-                neutron_utils.delete_subnet(self.neutron, self.subnet)
-            except:
-                pass
         if self.network:
             try:
                 neutron_utils.delete_network(self.neutron, self.network)
@@ -164,7 +160,7 @@ class NeutronUtilsSubnetTests(OSComponentTestCase):
 
     def test_create_subnet(self):
         """
-        Tests the neutron_utils.create_neutron_net() function
+        Tests the neutron_utils.create_network() function
         """
         self.network = neutron_utils.create_network(
             self.neutron, self.os_creds, self.net_config.network_settings)
@@ -174,20 +170,18 @@ class NeutronUtilsSubnetTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting, self.os_creds, network=self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
         subnet_query1 = neutron_utils.get_subnet(
             self.neutron, subnet_name=subnet_setting.name)
-        self.assertEqual(self.subnet, subnet_query1)
+        self.assertEqual(self.network.subnets[0], subnet_query1)
 
         subnet_query2 = neutron_utils.get_subnets_by_network(self.neutron,
                                                              self.network)
         self.assertIsNotNone(subnet_query2)
         self.assertEqual(1, len(subnet_query2))
-        self.assertEqual(self.subnet, subnet_query2[0])
+        self.assertEqual(self.network.subnets[0], subnet_query2[0])
 
     def test_create_subnet_null_name(self):
         """
@@ -202,11 +196,11 @@ class NeutronUtilsSubnetTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         with self.assertRaises(Exception):
-            SubnetSettings(cidr=self.net_config.subnet_cidr)
+            SubnetConfig(cidr=self.net_config.subnet_cidr)
 
     def test_create_subnet_empty_name(self):
         """
-        Tests the neutron_utils.create_neutron_net() function with an empty
+        Tests the neutron_utils.create_network() function with an empty
         name
         """
         self.network = neutron_utils.create_network(
@@ -217,8 +211,6 @@ class NeutronUtilsSubnetTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting, self.os_creds, network=self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
         self.assertFalse(validate_subnet(
@@ -226,49 +218,265 @@ class NeutronUtilsSubnetTests(OSComponentTestCase):
 
         subnet_query1 = neutron_utils.get_subnet(
             self.neutron, subnet_name=subnet_setting.name)
-        self.assertEqual(self.subnet, subnet_query1)
+        self.assertEqual(self.network.subnets[0], subnet_query1)
 
         subnet_query2 = neutron_utils.get_subnets_by_network(self.neutron,
                                                              self.network)
         self.assertIsNotNone(subnet_query2)
         self.assertEqual(1, len(subnet_query2))
-        self.assertEqual(self.subnet, subnet_query2[0])
+        self.assertEqual(self.network.subnets[0], subnet_query2[0])
 
     def test_create_subnet_null_cidr(self):
         """
         Tests the neutron_utils.create_neutron_subnet() function for an
         Exception when the subnet CIDR value is None
         """
-        self.network = neutron_utils.create_network(
-            self.neutron, self.os_creds, self.net_config.network_settings)
-        self.assertEqual(self.net_config.network_settings.name,
-                         self.network.name)
-        self.assertTrue(validate_network(
-            self.neutron, self.net_config.network_settings.name, True))
-
+        self.net_config.network_settings.subnet_settings[0].cidr = None
         with self.assertRaises(Exception):
-            sub_sets = SubnetSettings(
-                cidr=None, name=self.net_config.subnet_name)
-            neutron_utils.create_subnet(
-                self.neutron, sub_sets, self.os_creds, network=self.network)
+            self.network = neutron_utils.create_network(
+                self.neutron, self.os_creds, self.net_config.network_settings)
 
     def test_create_subnet_empty_cidr(self):
         """
         Tests the neutron_utils.create_neutron_subnet() function for an
         Exception when the subnet CIDR value is empty
         """
+        self.net_config.network_settings.subnet_settings[0].cidr = ''
+        with self.assertRaises(Exception):
+            self.network = neutron_utils.create_network(
+                self.neutron, self.os_creds, self.net_config.network_settings)
+
+
+class NeutronUtilsIPv6Tests(OSComponentTestCase):
+    """
+    Test for creating IPv6 networks with subnets via neutron_utils.py
+    """
+
+    def setUp(self):
+        self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+        self.neutron = neutron_utils.neutron_client(self.os_creds)
+        self.network = None
+
+    def tearDown(self):
+        """
+        Cleans the remote OpenStack objects
+        """
+        if self.network:
+            try:
+                neutron_utils.delete_network(self.neutron, self.network)
+            except:
+                pass
+
+    def test_create_network_slaac(self):
+        """
+        Tests the neutron_utils.create_network() with an IPv6 subnet where DHCP
+        is True and IPv6 modes are slaac
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6, dns_nameservers=['2620:0:ccc:0:0:0:0:2'],
+            gateway_ip='1:1:0:0:0:0:0:1', start='1:1::ff', end='1:1::ffff',
+            enable_dhcp=True, ipv6_ra_mode='slaac', ipv6_address_mode='slaac')
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
         self.network = neutron_utils.create_network(
-            self.neutron, self.os_creds, self.net_config.network_settings)
-        self.assertEqual(self.net_config.network_settings.name,
-                         self.network.name)
-        self.assertTrue(validate_network(
-            self.neutron, self.net_config.network_settings.name, True))
+            self.neutron, self.os_creds, self.network_settings)
+        self.assertEqual(self.network_settings.name, self.network.name)
+
+        subnet_settings = self.network_settings.subnet_settings[0]
+        self.assertEqual(1, len(self.network.subnets))
+        subnet = self.network.subnets[0]
+
+        self.assertEqual(self.network.id, subnet.network_id)
+        self.assertEqual(subnet_settings.name, subnet.name)
+        self.assertEqual(subnet_settings.start, subnet.start)
+        self.assertEqual(subnet_settings.end, subnet.end)
+        self.assertEqual('1:1::/64', subnet.cidr)
+        self.assertEqual(6, subnet.ip_version)
+        self.assertEqual(1, len(subnet.dns_nameservers))
+        self.assertEqual(
+            sub_setting.dns_nameservers[0], subnet.dns_nameservers[0])
+        self.assertTrue(subnet.enable_dhcp)
+        self.assertEqual(
+            subnet_settings.ipv6_ra_mode.value, subnet.ipv6_ra_mode)
+        self.assertEqual(
+            subnet_settings.ipv6_address_mode.value, subnet.ipv6_address_mode)
 
-        with self.assertRaises(Exception):
-            sub_sets = SubnetSettings(
-                cidr='', name=self.net_config.subnet_name)
-            neutron_utils.create_subnet(self.neutron, sub_sets, self.os_creds,
-                                        network=self.network)
+    def test_create_network_stateful(self):
+        """
+        Tests the neutron_utils.create_network() with an IPv6 subnet where DHCP
+        is True and IPv6 modes are stateful
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6, dns_nameservers=['2620:0:ccc:0:0:0:0:2'],
+            gateway_ip='1:1:0:0:0:0:0:1', start='1:1::ff', end='1:1::ffff',
+            enable_dhcp=True, ipv6_ra_mode='dhcpv6-stateful',
+            ipv6_address_mode='dhcpv6-stateful')
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        self.network = neutron_utils.create_network(
+            self.neutron, self.os_creds, self.network_settings)
+
+        self.assertEqual(self.network_settings.name, self.network.name)
+
+        subnet_settings = self.network_settings.subnet_settings[0]
+        self.assertEqual(1, len(self.network.subnets))
+        subnet = self.network.subnets[0]
+
+        self.assertEqual(self.network.id, subnet.network_id)
+        self.assertEqual(subnet_settings.name, subnet.name)
+        self.assertEqual(subnet_settings.start, subnet.start)
+        self.assertEqual(subnet_settings.end, subnet.end)
+        self.assertEqual('1:1::/64', subnet.cidr)
+        self.assertEqual(6, subnet.ip_version)
+        self.assertEqual(1, len(subnet.dns_nameservers))
+        self.assertEqual(
+            sub_setting.dns_nameservers[0], subnet.dns_nameservers[0])
+        self.assertTrue(subnet.enable_dhcp)
+        self.assertEqual(
+            subnet_settings.ipv6_ra_mode.value, subnet.ipv6_ra_mode)
+        self.assertEqual(
+            subnet_settings.ipv6_address_mode.value, subnet.ipv6_address_mode)
+
+    def test_create_network_stateless(self):
+        """
+        Tests the neutron_utils.create_network() when DHCP is enabled and
+        the RA and address modes are both 'slaac'
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6, dns_nameservers=['2620:0:ccc:0:0:0:0:2'],
+            gateway_ip='1:1:0:0:0:0:0:1', start='1:1::ff', end='1:1::ffff',
+            enable_dhcp=True, ipv6_ra_mode='dhcpv6-stateless',
+            ipv6_address_mode='dhcpv6-stateless')
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        self.network = neutron_utils.create_network(
+            self.neutron, self.os_creds, self.network_settings)
+
+        self.assertEqual(self.network_settings.name, self.network.name)
+
+        subnet_settings = self.network_settings.subnet_settings[0]
+        self.assertEqual(1, len(self.network.subnets))
+        subnet = self.network.subnets[0]
+
+        self.assertEqual(self.network.id, subnet.network_id)
+        self.assertEqual(subnet_settings.name, subnet.name)
+        self.assertEqual(subnet_settings.start, subnet.start)
+        self.assertEqual(subnet_settings.end, subnet.end)
+        self.assertEqual('1:1::/64', subnet.cidr)
+        self.assertEqual(6, subnet.ip_version)
+        self.assertEqual(1, len(subnet.dns_nameservers))
+        self.assertEqual(
+            sub_setting.dns_nameservers[0], subnet.dns_nameservers[0])
+        self.assertTrue(subnet.enable_dhcp)
+        self.assertEqual(
+            subnet_settings.ipv6_ra_mode.value, subnet.ipv6_ra_mode)
+        self.assertEqual(
+            subnet_settings.ipv6_address_mode.value, subnet.ipv6_address_mode)
+
+    def test_create_network_no_dhcp_slaac(self):
+        """
+        Tests the neutron_utils.create_network() for a BadRequest when
+        DHCP is not enabled and the RA and address modes are both 'slaac'
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1:0:0:0:0:0:0/64',
+            ip_version=6, dns_nameservers=['2620:0:ccc:0:0:0:0:2'],
+            gateway_ip='1:1:0:0:0:0:0:1', start='1:1::ff', end='1:1::ffff',
+            enable_dhcp=False, ipv6_ra_mode='slaac', ipv6_address_mode='slaac')
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        with self.assertRaises(BadRequest):
+            self.network = neutron_utils.create_network(
+                self.neutron, self.os_creds, self.network_settings)
+
+    def test_create_network_invalid_start_ip(self):
+        """
+        Tests the neutron_utils.create_network() that contains one IPv6 subnet
+        with an invalid start IP to ensure Neutron assigns it the smallest IP
+        possible
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1::/48', ip_version=6,
+            start='foo')
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        self.network = neutron_utils.create_network(
+            self.neutron, self.os_creds, self.network_settings)
+
+        self.assertEqual('1:1::2', self.network.subnets[0].start)
+        self.assertEqual(
+            '1:1:0:ffff:ffff:ffff:ffff:ffff', self.network.subnets[0].end)
+
+    def test_create_network_invalid_end_ip(self):
+        """
+        Tests the neutron_utils.create_network() that contains one IPv6 subnet
+        with an invalid end IP to ensure Neutron assigns it the largest IP
+        possible
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1::/48', ip_version=6,
+            end='bar')
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        self.network = neutron_utils.create_network(
+            self.neutron, self.os_creds, self.network_settings)
+
+        self.assertEqual('1:1::2', self.network.subnets[0].start)
+        self.assertEqual(
+            '1:1:0:ffff:ffff:ffff:ffff:ffff', self.network.subnets[0].end)
+
+    def test_create_network_with_bad_cidr(self):
+        """
+        Tests the neutron_utils.create_network() for a BadRequest when
+        the subnet CIDR is invalid
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1:1:/48', ip_version=6)
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        with self.assertRaises(BadRequest):
+            self.network = neutron_utils.create_network(
+                self.neutron, self.os_creds, self.network_settings)
+
+    def test_create_network_invalid_gateway_ip(self):
+        """
+        Tests the neutron_utils.create_network() for a BadRequest when
+        the subnet gateway IP is invalid
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1::/48', ip_version=6,
+            gateway_ip='1:2::1')
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        with self.assertRaises(BadRequest):
+            self.network = neutron_utils.create_network(
+                self.neutron, self.os_creds, self.network_settings)
+
+    def test_create_network_with_bad_dns(self):
+        """
+        Tests the neutron_utils.create_network() for a BadRequest when
+        the DNS IP is invalid
+        """
+        sub_setting = SubnetConfig(
+            name=self.guid + '-subnet', cidr='1:1::/48', ip_version=6,
+            dns_nameservers=['foo'])
+        self.network_settings = NetworkConfig(
+            name=self.guid + '-net', subnet_settings=[sub_setting])
+
+        with self.assertRaises(BadRequest):
+            self.network = neutron_utils.create_network(
+                    self.neutron, self.os_creds, self.network_settings)
 
 
 class NeutronUtilsRouterTests(OSComponentTestCase):
@@ -281,7 +489,6 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
         self.port_name = str(guid) + '-port'
         self.neutron = neutron_utils.neutron_client(self.os_creds)
         self.network = None
-        self.subnet = None
         self.port = None
         self.router = None
         self.interface_router = None
@@ -294,8 +501,8 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
         Cleans the remote OpenStack objects
         """
         if self.interface_router:
-            neutron_utils.remove_interface_router(self.neutron, self.router,
-                                                  self.subnet)
+            neutron_utils.remove_interface_router(
+                self.neutron, self.router, self.network.subnets[0])
 
         if self.router:
             try:
@@ -310,22 +517,12 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             except:
                 pass
 
-        if self.subnet:
-            try:
-                neutron_utils.delete_subnet(self.neutron, self.subnet)
-            except:
-                pass
-
         if self.network:
-            try:
-                neutron_utils.delete_network(self.neutron, self.network)
-            except:
-                pass
+            neutron_utils.delete_network(self.neutron, self.network)
 
     def test_create_router_simple(self):
         """
-        Tests the neutron_utils.create_neutron_net() function when an external
-        gateway is requested
+        Tests the neutron_utils.create_router()
         """
         self.router = neutron_utils.create_router(
             self.neutron, self.os_creds, self.net_config.router_settings)
@@ -334,8 +531,7 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
 
     def test_create_router_with_public_interface(self):
         """
-        Tests the neutron_utils.create_neutron_net() function when an external
-        gateway is requested
+        Tests the neutron_utils.create_router() function with a pubic interface
         """
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
         self.net_config = openstack_tests.OSNetworkConfig(
@@ -351,30 +547,7 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
 
         ext_net = neutron_utils.get_network(
             self.neutron, network_name=self.ext_net_name)
-        self.assertEqual(
-            self.router.external_gateway_info['network_id'], ext_net.id)
-
-    def test_create_router_empty_name(self):
-        """
-        Tests the neutron_utils.create_neutron_net() function
-        """
-        with self.assertRaises(Exception):
-            this_router_settings = create_router.RouterSettings(name='')
-            self.router = neutron_utils.create_router(self.neutron,
-                                                      self.os_creds,
-                                                      this_router_settings)
-
-    def test_create_router_null_name(self):
-        """
-        Tests the neutron_utils.create_neutron_subnet() function when the
-        subnet CIDR value is None
-        """
-        with self.assertRaises(Exception):
-            this_router_settings = create_router.RouterSettings()
-            self.router = neutron_utils.create_router(self.neutron,
-                                                      self.os_creds,
-                                                      this_router_settings)
-            validate_router(self.neutron, None, True)
+        self.assertEqual(self.router.external_network_id, ext_net.id)
 
     def test_add_interface_router(self):
         """
@@ -388,9 +561,6 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting,
-            self.os_creds, self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
@@ -400,9 +570,9 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
                         True)
 
         self.interface_router = neutron_utils.add_interface_router(
-            self.neutron, self.router, self.subnet)
+            self.neutron, self.router, self.network.subnets[0])
         validate_interface_router(self.interface_router, self.router,
-                                  self.subnet)
+                                  self.network.subnets[0])
 
     def test_add_interface_router_null_router(self):
         """
@@ -417,15 +587,12 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting,
-            self.os_creds, self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
         with self.assertRaises(NeutronException):
             self.interface_router = neutron_utils.add_interface_router(
-                self.neutron, self.router, self.subnet)
+                self.neutron, self.router, self.network.subnets[0])
 
     def test_add_interface_router_null_subnet(self):
         """
@@ -446,7 +613,31 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
 
         with self.assertRaises(NeutronException):
             self.interface_router = neutron_utils.add_interface_router(
-                self.neutron, self.router, self.subnet)
+                self.neutron, self.router, None)
+
+    def test_add_interface_router_missing_subnet(self):
+        """
+        Tests the neutron_utils.add_interface_router() function for an
+        Exception when the subnet object has been deleted
+        """
+        self.network = neutron_utils.create_network(
+            self.neutron, self.os_creds, self.net_config.network_settings)
+        self.assertEqual(self.net_config.network_settings.name,
+                         self.network.name)
+        self.assertTrue(validate_network(
+            self.neutron, self.net_config.network_settings.name, True))
+
+        self.router = neutron_utils.create_router(
+            self.neutron, self.os_creds, self.net_config.router_settings)
+        validate_router(self.neutron, self.net_config.router_settings.name,
+                        True)
+
+        for subnet in self.network.subnets:
+            neutron_utils.delete_subnet(self.neutron, subnet)
+
+        with self.assertRaises(NotFound):
+            self.interface_router = neutron_utils.add_interface_router(
+                self.neutron, self.router, self.network.subnets[0])
 
     def test_create_port(self):
         """
@@ -460,13 +651,11 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting, self.os_creds, self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
         self.port = neutron_utils.create_port(
-            self.neutron, self.os_creds, PortSettings(
+            self.neutron, self.os_creds, PortConfig(
                 name=self.port_name,
                 ip_addrs=[{
                     'subnet_name': subnet_setting.name,
@@ -486,13 +675,11 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting, self.os_creds, self.network)
         self.assertTrue(validate_subnet(self.neutron, subnet_setting.name,
                                         subnet_setting.cidr, True))
 
         self.port = neutron_utils.create_port(
-            self.neutron, self.os_creds, PortSettings(
+            self.neutron, self.os_creds, PortConfig(
                 name=self.port_name,
                 network_name=self.net_config.network_settings.name,
                 ip_addrs=[{
@@ -512,15 +699,12 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting,
-            self.os_creds, self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
         self.port = neutron_utils.create_port(
             self.neutron, self.os_creds,
-            PortSettings(
+            PortConfig(
                 network_name=self.net_config.network_settings.name,
                 ip_addrs=[{
                     'subnet_name': subnet_setting.name,
@@ -537,7 +721,7 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
         with self.assertRaises(Exception):
             self.port = neutron_utils.create_port(
                 self.neutron, self.os_creds,
-                PortSettings(
+                PortConfig(
                     name=self.port_name,
                     network_name=self.net_config.network_settings.name,
                     ip_addrs=[{
@@ -559,16 +743,13 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting,
-            self.os_creds, self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
         with self.assertRaises(Exception):
             self.port = neutron_utils.create_port(
                 self.neutron, self.os_creds,
-                PortSettings(
+                PortConfig(
                     name=self.port_name,
                     network_name=self.net_config.network_settings.name,
                     ip_addrs=[{
@@ -588,15 +769,13 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting, self.os_creds, self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
         with self.assertRaises(Exception):
             self.port = neutron_utils.create_port(
                 self.neutron, self.os_creds,
-                PortSettings(
+                PortConfig(
                     name=self.port_name,
                     network_name=self.net_config.network_settings.name,
                     ip_addrs=[{
@@ -616,15 +795,13 @@ class NeutronUtilsRouterTests(OSComponentTestCase):
             self.neutron, self.net_config.network_settings.name, True))
 
         subnet_setting = self.net_config.network_settings.subnet_settings[0]
-        self.subnet = neutron_utils.create_subnet(
-            self.neutron, subnet_setting, self.os_creds, self.network)
         self.assertTrue(validate_subnet(
             self.neutron, subnet_setting.name, subnet_setting.cidr, True))
 
         with self.assertRaises(Exception):
             self.port = neutron_utils.create_port(
                 self.neutron, self.os_creds,
-                PortSettings(
+                PortConfig(
                     name=self.port_name,
                     network_name=self.net_config.network_settings.name,
                     ip_addrs=[{
@@ -664,7 +841,7 @@ class NeutronUtilsSecurityGroupTests(OSComponentTestCase):
         """
         Tests the neutron_utils.create_security_group() function
         """
-        sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name)
+        sec_grp_settings = SecurityGroupConfig(name=self.sec_grp_name)
         security_group = neutron_utils.create_security_group(self.neutron,
                                                              self.keystone,
                                                              sec_grp_settings)
@@ -684,13 +861,13 @@ class NeutronUtilsSecurityGroupTests(OSComponentTestCase):
 
     def test_create_sec_grp_no_name(self):
         """
-        Tests the SecurityGroupSettings constructor and
+        Tests the SecurityGroupConfig constructor and
         neutron_utils.create_security_group() function to ensure that
         attempting to create a security group without a name will raise an
         exception
         """
         with self.assertRaises(Exception):
-            sec_grp_settings = SecurityGroupSettings()
+            sec_grp_settings = SecurityGroupConfig()
             self.security_groups.append(
                 neutron_utils.create_security_group(self.neutron,
                                                     self.keystone,
@@ -700,8 +877,8 @@ class NeutronUtilsSecurityGroupTests(OSComponentTestCase):
         """
         Tests the neutron_utils.create_security_group() function
         """
-        sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name,
-                                                 description='hello group')
+        sec_grp_settings = SecurityGroupConfig(
+            name=self.sec_grp_name, description='hello group')
         self.security_groups.append(
             neutron_utils.create_security_group(self.neutron, self.keystone,
                                                 sec_grp_settings))
@@ -719,9 +896,9 @@ class NeutronUtilsSecurityGroupTests(OSComponentTestCase):
         Tests the neutron_utils.create_security_group() function
         """
 
-        sec_grp_rule_settings = SecurityGroupRuleSettings(
+        sec_grp_rule_settings = SecurityGroupRuleConfig(
             sec_grp_name=self.sec_grp_name, direction=Direction.ingress)
-        sec_grp_settings = SecurityGroupSettings(
+        sec_grp_settings = SecurityGroupConfig(
             name=self.sec_grp_name, description='hello group',
             rule_settings=[sec_grp_rule_settings])
 
@@ -762,12 +939,12 @@ class NeutronUtilsSecurityGroupTests(OSComponentTestCase):
 
         self.security_groups.append(neutron_utils.create_security_group(
             self.neutron, self.keystone,
-            SecurityGroupSettings(name=self.sec_grp_name + '-1',
-                                  description='hello group')))
+            SecurityGroupConfig(
+                name=self.sec_grp_name + '-1', description='hello group')))
         self.security_groups.append(neutron_utils.create_security_group(
             self.neutron, self.keystone,
-            SecurityGroupSettings(name=self.sec_grp_name + '-2',
-                                  description='hello group')))
+            SecurityGroupConfig(
+                name=self.sec_grp_name + '-2', description='hello group')))
 
         sec_grp_1b = neutron_utils.get_security_group_by_id(
             self.neutron, self.security_groups[0].id)
index c5b29b5..8cb0812 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
+import time
 import uuid
 
 import os
-import time
 
 from snaps import file_utils
+from snaps.config.flavor import FlavorConfig
+from snaps.config.network import PortConfig
+from snaps.config.vm_inst import VmInstanceConfig
+from snaps.config.volume import VolumeConfig
 from snaps.openstack import create_instance
-from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
+from snaps.openstack.create_flavor import OpenStackFlavor
 from snaps.openstack.create_image import OpenStackImage
-from snaps.openstack.create_instance import VmInstanceSettings
-from snaps.openstack.create_network import OpenStackNetwork, PortSettings
+from snaps.openstack.create_instance import OpenStackVmInstance
+from snaps.openstack.create_network import OpenStackNetwork
+from snaps.openstack.create_volume import OpenStackVolume
 from snaps.openstack.tests import openstack_tests
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
-from snaps.openstack.utils import nova_utils, neutron_utils, glance_utils
+from snaps.openstack.utils import (
+    nova_utils, neutron_utils, glance_utils, cinder_utils)
 
 __author__ = 'spisarski'
 
@@ -47,6 +53,16 @@ class NovaSmokeTests(OSComponentTestCase):
         # This should not throw an exception
         nova.flavors.list()
 
+    def test_nova_get_hypervisor_hosts(self):
+        """
+        Tests to ensure that get_hypervisors() function works.
+        """
+        nova = nova_utils.nova_client(self.os_creds)
+
+        hosts = nova_utils.get_hypervisor_hosts(nova)
+        # This should not throw an exception
+        self.assertGreaterEqual(len(hosts), 1)
+
     def test_nova_connect_fail(self):
         """
         Tests to ensure that the improper credentials cannot connect.
@@ -156,11 +172,9 @@ class NovaUtilsFlavorTests(OSComponentTestCase):
         and creating an OS image file within OpenStack
         """
         guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
-        self.flavor_settings = FlavorSettings(name=guid + '-name',
-                                              flavor_id=guid + '-id', ram=1,
-                                              disk=1, vcpus=1,
-                                              ephemeral=1, swap=2,
-                                              rxtx_factor=3.0, is_public=False)
+        self.flavor_settings = FlavorConfig(
+            name=guid + '-name', flavor_id=guid + '-id', ram=1, disk=1,
+            vcpus=1, ephemeral=1, swap=2, rxtx_factor=3.0, is_public=False)
         self.nova = nova_utils.nova_client(self.os_creds)
         self.flavor = None
 
@@ -252,16 +266,16 @@ class NovaUtilsInstanceTests(OSComponentTestCase):
 
             self.flavor_creator = OpenStackFlavor(
                 self.os_creds,
-                FlavorSettings(
+                FlavorConfig(
                     name=guid + '-flavor-name', ram=256, disk=10, vcpus=1))
             self.flavor_creator.create()
 
-            port_settings = PortSettings(name=guid + '-port',
-                                         network_name=network_settings.name)
+            port_settings = PortConfig(
+                name=guid + '-port', network_name=network_settings.name)
             self.port = neutron_utils.create_port(
                 self.neutron, self.os_creds, port_settings)
 
-            self.instance_settings = VmInstanceSettings(
+            self.instance_settings = VmInstanceConfig(
                 name=guid + '-vm_inst',
                 flavor=self.flavor_creator.flavor_settings.name,
                 port_settings=[port_settings])
@@ -325,7 +339,148 @@ class NovaUtilsInstanceTests(OSComponentTestCase):
             iters += 1
 
         self.assertTrue(active)
-        vm_inst = nova_utils.get_latest_server_object(self.nova, self.vm_inst)
+        vm_inst = nova_utils.get_latest_server_object(
+            self.nova, self.neutron, self.vm_inst)
 
         self.assertEqual(self.vm_inst.name, vm_inst.name)
         self.assertEqual(self.vm_inst.id, vm_inst.id)
+
+
+class NovaUtilsInstanceVolumeTests(OSComponentTestCase):
+    """
+    Tests the creation of VM instances via nova_utils.py
+    """
+
+    def setUp(self):
+        """
+        Setup objects required by VM instances
+        :return:
+        """
+
+        guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+
+        self.nova = nova_utils.nova_client(self.os_creds)
+        self.cinder = cinder_utils.cinder_client(self.os_creds)
+
+        self.image_creator = None
+        self.network_creator = None
+        self.flavor_creator = None
+        self.volume_creator = None
+        self.instance_creator = None
+
+        try:
+            image_settings = openstack_tests.cirros_image_settings(
+                name=guid + '-image', image_metadata=self.image_metadata)
+            self.image_creator = OpenStackImage(
+                self.os_creds, image_settings=image_settings)
+            self.image_creator.create()
+
+            network_settings = openstack_tests.get_priv_net_config(
+                guid + '-net', guid + '-subnet').network_settings
+            self.network_creator = OpenStackNetwork(
+                self.os_creds, network_settings)
+            self.network_creator.create()
+
+            self.flavor_creator = OpenStackFlavor(
+                self.os_creds,
+                FlavorConfig(
+                    name=guid + '-flavor-name', ram=256, disk=10, vcpus=1))
+            self.flavor_creator.create()
+
+            # Create Volume
+            volume_settings = VolumeConfig(
+                name=self.__class__.__name__ + '-' + str(guid))
+            self.volume_creator = OpenStackVolume(
+                self.os_creds, volume_settings)
+            self.volume_creator.create(block=True)
+
+            port_settings = PortConfig(
+                name=guid + '-port', network_name=network_settings.name)
+            instance_settings = VmInstanceConfig(
+                name=guid + '-vm_inst',
+                flavor=self.flavor_creator.flavor_settings.name,
+                port_settings=[port_settings])
+            self.instance_creator = OpenStackVmInstance(
+                self.os_creds, instance_settings, image_settings)
+            self.instance_creator.create(block=True)
+        except:
+            self.tearDown()
+            raise
+
+    def tearDown(self):
+        """
+        Cleanup deployed resources
+        :return:
+        """
+        if self.instance_creator:
+            try:
+                self.instance_creator.clean()
+            except:
+                pass
+        if self.volume_creator:
+            try:
+                self.volume_creator.clean()
+            except:
+                pass
+        if self.flavor_creator:
+            try:
+                self.flavor_creator.clean()
+            except:
+                pass
+        if self.network_creator:
+            try:
+                self.network_creator.clean()
+            except:
+                pass
+        if self.image_creator:
+            try:
+                self.image_creator.clean()
+            except:
+                pass
+
+    def test_add_remove_volume(self):
+        """
+        Tests the nova_utils.create_server() method
+        :return:
+        """
+
+        self.assertIsNotNone(self.volume_creator.get_volume())
+        self.assertEqual(0, len(self.volume_creator.get_volume().attachments))
+
+        # Attach volume to VM
+        neutron = neutron_utils.neutron_client(self.os_creds)
+        nova_utils.attach_volume(
+            self.nova, neutron, self.instance_creator.get_vm_inst(),
+            self.volume_creator.get_volume())
+
+        time.sleep(10)
+
+        vol_attach = cinder_utils.get_volume_by_id(
+            self.cinder, self.volume_creator.get_volume().id)
+        vm_attach = nova_utils.get_server_object_by_id(
+            self.nova, neutron, self.instance_creator.get_vm_inst().id)
+
+        # Detach volume to VM
+        nova_utils.detach_volume(
+            self.nova, neutron, self.instance_creator.get_vm_inst(),
+            self.volume_creator.get_volume())
+
+        time.sleep(10)
+
+        vol_detach = cinder_utils.get_volume_by_id(
+            self.cinder, self.volume_creator.get_volume().id)
+        vm_detach = nova_utils.get_server_object_by_id(
+            self.nova, neutron, self.instance_creator.get_vm_inst().id)
+
+        # Validate Attachment
+        self.assertIsNotNone(vol_attach)
+        self.assertEqual(self.volume_creator.get_volume().id, vol_attach.id)
+        self.assertEqual(1, len(vol_attach.attachments))
+        self.assertEqual(vm_attach.volume_ids[0]['id'],
+                         vol_attach.attachments[0]['volume_id'])
+
+        # Validate Detachment
+        self.assertIsNotNone(vol_detach)
+        self.assertEqual(self.volume_creator.get_volume().id, vol_detach.id)
+        self.assertEqual(0, len(vol_detach.attachments))
+        self.assertEqual(0, len(vm_detach.volume_ids))
index f84e6a0..cbd78d8 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import logging
+import unittest
+
 import os
 import uuid
 
+from snaps.config.network import SubnetConfig, NetworkConfig, PortConfig
+from snaps.config.flavor import FlavorConfig
+from snaps.config.keypair import KeypairConfig
+from snaps.config.qos import Consumer
+from snaps.config.security_group import (
+    SecurityGroupRuleConfig, Direction, Protocol, SecurityGroupConfig)
+from snaps.config.vm_inst import VmInstanceConfig, FloatingIpConfig
+from snaps.domain.flavor import Flavor
+from snaps.domain.volume import (
+    Volume, VolumeType, VolumeTypeEncryption, QoSSpec)
 from snaps.openstack import (
     create_image, create_network, create_router, create_flavor,
     create_keypairs, create_instance)
-from snaps.openstack.create_network import (
-    NetworkSettings, OpenStackNetwork, SubnetSettings)
-from snaps.openstack.create_security_group import (
-    SecurityGroupRuleSettings,  Direction, Protocol, OpenStackSecurityGroup,
-    SecurityGroupSettings)
+from snaps.openstack.create_qos import Consumer
+from snaps.openstack.create_network import OpenStackNetwork
+from snaps.openstack.create_security_group import OpenStackSecurityGroup
 from snaps.openstack.tests import openstack_tests
 from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
 from snaps.openstack.utils import (
@@ -36,7 +46,7 @@ logger = logging.getLogger('nova_utils_tests')
 
 class SettingsUtilsNetworkingTests(OSComponentTestCase):
     """
-    Tests the ability to reverse engineer NetworkSettings objects from existing
+    Tests the ability to reverse engineer NetworkConfig objects from existing
     networks deployed to OpenStack
     """
 
@@ -62,16 +72,16 @@ class SettingsUtilsNetworkingTests(OSComponentTestCase):
 
     def test_derive_net_settings_no_subnet(self):
         """
-        Validates the utility function settings_utils#create_network_settings
-        returns an acceptable NetworkSettings object and ensures that the
+        Validates the utility function settings_utils#create_network_config
+        returns an acceptable NetworkConfig object and ensures that the
         new settings object will not cause the new OpenStackNetwork instance
         to create another network
         """
-        net_settings = NetworkSettings(name=self.network_name)
+        net_settings = NetworkConfig(name=self.network_name)
         self.net_creator = OpenStackNetwork(self.os_creds, net_settings)
         network = self.net_creator.create()
 
-        derived_settings = settings_utils.create_network_settings(
+        derived_settings = settings_utils.create_network_config(
             self.neutron, network)
 
         self.assertIsNotNone(derived_settings)
@@ -89,18 +99,18 @@ class SettingsUtilsNetworkingTests(OSComponentTestCase):
 
     def test_derive_net_settings_two_subnets(self):
         """
-        Validates the utility function settings_utils#create_network_settings
-        returns an acceptable NetworkSettings object
+        Validates the utility function settings_utils#create_network_config
+        returns an acceptable NetworkConfig object
         """
         subnet_settings = list()
-        subnet_settings.append(SubnetSettings(name='sub1', cidr='10.0.0.0/24'))
-        subnet_settings.append(SubnetSettings(name='sub2', cidr='10.0.1.0/24'))
-        net_settings = NetworkSettings(name=self.network_name,
-                                       subnet_settings=subnet_settings)
+        subnet_settings.append(SubnetConfig(name='sub1', cidr='10.0.0.0/24'))
+        subnet_settings.append(SubnetConfig(name='sub2', cidr='10.0.1.0/24'))
+        net_settings = NetworkConfig(
+            name=self.network_name, subnet_settings=subnet_settings)
         self.net_creator = OpenStackNetwork(self.os_creds, net_settings)
         network = self.net_creator.create()
 
-        derived_settings = settings_utils.create_network_settings(
+        derived_settings = settings_utils.create_network_config(
             self.neutron, network)
 
         self.assertIsNotNone(derived_settings)
@@ -135,7 +145,7 @@ class SettingsUtilsNetworkingTests(OSComponentTestCase):
 
 class SettingsUtilsVmInstTests(OSComponentTestCase):
     """
-    Tests the ability to reverse engineer VmInstanceSettings objects from
+    Tests the ability to reverse engineer VmInstanceConfig objects from
     existing VMs/servers deployed to OpenStack
     """
 
@@ -144,8 +154,6 @@ class SettingsUtilsVmInstTests(OSComponentTestCase):
         Instantiates the CreateImage object that is responsible for downloading
         and creating an OS image file within OpenStack
         """
-        # super(self.__class__, self).__start__()
-
         self.nova = nova_utils.nova_client(self.os_creds)
         self.glance = glance_utils.glance_client(self.os_creds)
         self.neutron = neutron_utils.neutron_client(self.os_creds)
@@ -196,13 +204,13 @@ class SettingsUtilsVmInstTests(OSComponentTestCase):
             # Create Flavor
             self.flavor_creator = create_flavor.OpenStackFlavor(
                 self.os_creds,
-                create_flavor.FlavorSettings(name=guid + '-flavor-name',
-                                             ram=256, disk=1, vcpus=1))
+                FlavorConfig(
+                    name=guid + '-flavor-name', ram=256, disk=1, vcpus=1))
             self.flavor_creator.create()
 
             # Create Key/Pair
             self.keypair_creator = create_keypairs.OpenStackKeypair(
-                self.os_creds, create_keypairs.KeypairSettings(
+                self.os_creds, KeypairConfig(
                     name=self.keypair_name,
                     public_filepath=self.keypair_pub_filepath,
                     private_filepath=self.keypair_priv_filepath))
@@ -210,32 +218,30 @@ class SettingsUtilsVmInstTests(OSComponentTestCase):
 
             # Create Security Group
             sec_grp_name = guid + '-sec-grp'
-            rule1 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.icmp)
-            rule2 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.tcp,
-                                              port_range_min=22,
-                                              port_range_max=22)
+            rule1 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.icmp)
+            rule2 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.tcp, port_range_min=22, port_range_max=22)
             self.sec_grp_creator = OpenStackSecurityGroup(
                 self.os_creds,
-                SecurityGroupSettings(name=sec_grp_name,
-                                      rule_settings=[rule1, rule2]))
+                SecurityGroupConfig(
+                    name=sec_grp_name, rule_settings=[rule1, rule2]))
             self.sec_grp_creator.create()
 
             # Create instance
             ports_settings = list()
             ports_settings.append(
-                create_network.PortSettings(
+                PortConfig(
                     name=self.port_1_name,
                     network_name=self.pub_net_config.network_settings.name))
 
-            instance_settings = create_instance.VmInstanceSettings(
+            instance_settings = VmInstanceConfig(
                 name=self.vm_inst_name,
                 flavor=self.flavor_creator.flavor_settings.name,
                 port_settings=ports_settings,
-                floating_ip_settings=[create_instance.FloatingIpSettings(
+                floating_ip_settings=[FloatingIpConfig(
                     name=self.floating_ip_name, port_name=self.port_1_name,
                     router_name=self.pub_net_config.router_settings.name)])
 
@@ -310,16 +316,17 @@ class SettingsUtilsVmInstTests(OSComponentTestCase):
 
         # super(self.__class__, self).__clean__()
 
-    def test_derive_vm_inst_settings(self):
+    def test_derive_vm_inst_config(self):
         """
-        Validates the utility function settings_utils#create_vm_inst_settings
-        returns an acceptable VmInstanceSettings object
+        Validates the utility function settings_utils#create_vm_inst_config
+        returns an acceptable VmInstanceConfig object
         """
         self.inst_creator.create(block=True)
 
         server = nova_utils.get_server(
-            self.nova, vm_inst_settings=self.inst_creator.instance_settings)
-        derived_vm_settings = settings_utils.create_vm_inst_settings(
+            self.nova, self.neutron,
+            vm_inst_settings=self.inst_creator.instance_settings)
+        derived_vm_settings = settings_utils.create_vm_inst_config(
             self.nova, self.neutron, server)
         self.assertIsNotNone(derived_vm_settings)
         self.assertIsNotNone(derived_vm_settings.port_settings)
@@ -328,14 +335,72 @@ class SettingsUtilsVmInstTests(OSComponentTestCase):
     def test_derive_image_settings(self):
         """
         Validates the utility function settings_utils#create_image_settings
-        returns an acceptable ImageSettings object
+        returns an acceptable ImageConfig object
         """
         self.inst_creator.create(block=True)
 
         server = nova_utils.get_server(
-            self.nova, vm_inst_settings=self.inst_creator.instance_settings)
-        derived_image_settings = settings_utils.determine_image_settings(
+            self.nova, self.neutron,
+            vm_inst_settings=self.inst_creator.instance_settings)
+        derived_image_settings = settings_utils.determine_image_config(
             self.glance, server, [self.image_creator.image_settings])
         self.assertIsNotNone(derived_image_settings)
         self.assertEqual(self.image_creator.image_settings.name,
                          derived_image_settings.name)
+
+
+class SettingsUtilsUnitTests(unittest.TestCase):
+    """
+    Exercises the settings_utils.py functions around volumes
+    """
+
+    def test_vol_settings_from_vol(self):
+        volume = Volume(
+            name='vol-name', volume_id='vol-id', description='desc', size=99,
+            vol_type='vol-type', availability_zone='zone1', multi_attach=True)
+        settings = settings_utils.create_volume_config(volume)
+        self.assertEqual(volume.name, settings.name)
+        self.assertEqual(volume.description, settings.description)
+        self.assertEqual(volume.size, settings.size)
+        self.assertEqual(volume.type, settings.type_name)
+        self.assertEqual(volume.availability_zone, settings.availability_zone)
+        self.assertEqual(volume.multi_attach, settings.multi_attach)
+
+    def test_vol_type_settings_from_vol(self):
+        encryption = VolumeTypeEncryption(
+            volume_encryption_id='vol-encrypt-id', volume_type_id='vol-typ-id',
+            control_location='front-end', provider='FooClass', cipher='1',
+            key_size=1)
+        qos_spec = QoSSpec(name='qos-spec-name', spec_id='qos-spec-id',
+                           consumer=Consumer.back_end)
+        volume_type = VolumeType(
+            name='vol-type-name', volume_type_id='vol-type-id', public=True,
+            encryption=encryption, qos_spec=qos_spec)
+
+        settings = settings_utils.create_volume_type_config(volume_type)
+        self.assertEqual(volume_type.name, settings.name)
+        self.assertEqual(volume_type.public, settings.public)
+
+        encrypt_settings = settings.encryption
+        self.assertIsNotNone(encrypt_settings)
+        self.assertEqual(encryption.control_location,
+                         encrypt_settings.control_location.value)
+        self.assertEqual(encryption.cipher, encrypt_settings.cipher)
+        self.assertEqual(encryption.key_size, encrypt_settings.key_size)
+
+        self.assertEqual(qos_spec.name, settings.qos_spec_name)
+
+    def test_flavor_settings_from_flavor(self):
+        flavor = Flavor(
+            name='flavor-name', flavor_id='flavor-id', ram=99, disk=101,
+            vcpus=9, ephemeral=3, swap=5, rxtx_factor=7, is_public=False)
+        settings = settings_utils.create_flavor_config(flavor)
+        self.assertEqual(flavor.name, settings.name)
+        self.assertEqual(flavor.id, settings.flavor_id)
+        self.assertEqual(flavor.ram, settings.ram)
+        self.assertEqual(flavor.disk, settings.disk)
+        self.assertEqual(flavor.vcpus, settings.vcpus)
+        self.assertEqual(flavor.ephemeral, settings.ephemeral)
+        self.assertEqual(flavor.swap, settings.swap)
+        self.assertEqual(flavor.rxtx_factor, settings.rxtx_factor)
+        self.assertEqual(flavor.is_public, settings.is_public)
index 4dba550..87321f5 100644 (file)
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 import argparse
+import ast
 import logging
 
 import re
@@ -45,9 +46,15 @@ def main(parsed_args):
     if ssh:
         ssh.close()
 
+    vars = dict()
+    if args.vars:
+        vars = ast.literal_eval(args.vars)
+        if not isinstance(vars, dict):
+            vars = dict()
+
     retval = ansible_utils.apply_playbook(
         parsed_args.playbook, [parsed_args.ip_addr], parsed_args.host_user,
-        parsed_args.priv_key, variables={'name': 'Foo'},
+        parsed_args.priv_key, variables=vars,
         proxy_setting=proxy_settings)
     exit(retval)
 
@@ -66,6 +73,9 @@ if __name__ == '__main__':
                         required=False, help='<host>:<port>')
     parser.add_argument('-s', '--ssh-proxy-cmd', dest='ssh_proxy_cmd',
                         required=False)
+    parser.add_argument('-v', '--vars', dest='vars',
+                        required=False)
     args = parser.parse_args()
 
     main(args)
+
diff --git a/snaps/provisioning/ansible_pb/__init__.py b/snaps/provisioning/ansible_pb/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/snaps/provisioning/ansible_pb/centos-network-setup/__init__.py b/snaps/provisioning/ansible_pb/centos-network-setup/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/snaps/provisioning/ansible_pb/centos-network-setup/playbooks/__init__.py b/snaps/provisioning/ansible_pb/centos-network-setup/playbooks/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/snaps/provisioning/ansible_pb/centos-network-setup/templates/ifcfg-interface b/snaps/provisioning/ansible_pb/centos-network-setup/templates/ifcfg-interface
deleted file mode 100644 (file)
index 47aa3fa..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-DEVICE={{ nic_name }}
-NAME={{ nic_name }}
-IPADDR={{ nic_ip }}
-
-DEFROUTE=no
-NETMASK=255.255.255.0
-NM_CONTROLLED=no
-IPV6INIT=yes
-IPV6_AUTOCONF=yes
-IPV6_DEFROUTE=yes
-IPV6_PEERDNS=yes
-IPV6_PEERROUTES=yes
-IPV6_FAILURE_FATAL=no
-ONBOOT=yes
\ No newline at end of file
diff --git a/snaps/provisioning/ansible_pb/ubuntu-network-setup/__init__.py b/snaps/provisioning/ansible_pb/ubuntu-network-setup/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/snaps/provisioning/ansible_pb/ubuntu-network-setup/playbooks/__init__.py b/snaps/provisioning/ansible_pb/ubuntu-network-setup/playbooks/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/snaps/provisioning/ansible_pb/ubuntu-network-setup/templates/ethN.cfg b/snaps/provisioning/ansible_pb/ubuntu-network-setup/templates/ethN.cfg
deleted file mode 100644 (file)
index 3fa7708..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-auto {{ nic_name }}
-iface {{ nic_name }} inet dhcp
index da056b2..7600002 100644 (file)
@@ -18,15 +18,21 @@ import uuid
 import os
 import pkg_resources
 from scp import SCPClient
+
+from snaps.config.flavor import FlavorConfig
+from snaps.config.keypair import KeypairConfig
+from snaps.config.network import PortConfig
+from snaps.config.security_group import (
+    Direction, Protocol, SecurityGroupConfig, SecurityGroupRuleConfig)
+from snaps.config.vm_inst import VmInstanceConfig, FloatingIpConfig
+
 from snaps.openstack import create_flavor
 from snaps.openstack import create_image
 from snaps.openstack import create_instance
 from snaps.openstack import create_keypairs
 from snaps.openstack import create_network
 from snaps.openstack import create_router
-from snaps.openstack.create_security_group import (
-    SecurityGroupRuleSettings,  Direction, Protocol, OpenStackSecurityGroup,
-    SecurityGroupSettings)
+from snaps.openstack.create_security_group import OpenStackSecurityGroup
 from snaps.openstack.tests import openstack_tests
 from snaps.openstack.tests.create_instance_tests import check_dhcp_lease
 from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
@@ -100,14 +106,14 @@ class AnsibleProvisioningTests(OSIntegrationTestCase):
             # Create Flavor
             self.flavor_creator = create_flavor.OpenStackFlavor(
                 self.admin_os_creds,
-                create_flavor.FlavorSettings(name=guid + '-flavor-name',
-                                             ram=2048, disk=10, vcpus=2,
-                                             metadata=self.flavor_metadata))
+                FlavorConfig(
+                    name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2,
+                    metadata=self.flavor_metadata))
             self.flavor_creator.create()
 
             # Create Key/Pair
             self.keypair_creator = create_keypairs.OpenStackKeypair(
-                self.os_creds, create_keypairs.KeypairSettings(
+                self.os_creds, KeypairConfig(
                     name=self.keypair_name,
                     public_filepath=self.keypair_pub_filepath,
                     private_filepath=self.keypair_priv_filepath))
@@ -115,32 +121,30 @@ class AnsibleProvisioningTests(OSIntegrationTestCase):
 
             # Create Security Group
             sec_grp_name = guid + '-sec-grp'
-            rule1 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.icmp)
-            rule2 = SecurityGroupRuleSettings(sec_grp_name=sec_grp_name,
-                                              direction=Direction.ingress,
-                                              protocol=Protocol.tcp,
-                                              port_range_min=22,
-                                              port_range_max=22)
+            rule1 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.icmp)
+            rule2 = SecurityGroupRuleConfig(
+                sec_grp_name=sec_grp_name, direction=Direction.ingress,
+                protocol=Protocol.tcp, port_range_min=22, port_range_max=22)
             self.sec_grp_creator = OpenStackSecurityGroup(
                 self.os_creds,
-                SecurityGroupSettings(name=sec_grp_name,
-                                      rule_settings=[rule1, rule2]))
+                SecurityGroupConfig(
+                    name=sec_grp_name, rule_settings=[rule1, rule2]))
             self.sec_grp_creator.create()
 
             # Create instance
             ports_settings = list()
             ports_settings.append(
-                create_network.PortSettings(
+                PortConfig(
                     name=self.port_1_name,
                     network_name=self.pub_net_config.network_settings.name))
 
-            instance_settings = create_instance.VmInstanceSettings(
+            instance_settings = VmInstanceConfig(
                 name=self.vm_inst_name,
                 flavor=self.flavor_creator.flavor_settings.name,
                 port_settings=ports_settings,
-                floating_ip_settings=[create_instance.FloatingIpSettings(
+                floating_ip_settings=[FloatingIpConfig(
                     name=self.floating_ip_name, port_name=self.port_1_name,
                     router_name=self.pub_net_config.router_settings.name)])
 
@@ -237,6 +241,9 @@ class AnsibleProvisioningTests(OSIntegrationTestCase):
         # Block until VM's ssh port has been opened
         self.assertTrue(self.inst_creator.vm_ssh_active(block=True))
 
+        # Block until cloud-init has completed
+        self.assertTrue(self.inst_creator.cloud_init_complete(block=True))
+
         ssh_client = self.inst_creator.ssh_client()
         self.assertIsNotNone(ssh_client)
 
@@ -262,12 +269,13 @@ class AnsibleProvisioningTests(OSIntegrationTestCase):
         ssh = ansible_utils.ssh_client(ip, user, priv_key,
                                        self.os_creds.proxy_settings)
         self.assertIsNotNone(ssh)
-
+        scp = None
         try:
             scp = SCPClient(ssh.get_transport())
             scp.get('~/hello.txt', self.test_file_local_path)
         finally:
-            scp.close()
+            if scp:
+                scp.close()
             ssh.close()
 
         self.assertTrue(os.path.isfile(self.test_file_local_path))
@@ -305,6 +313,9 @@ class AnsibleProvisioningTests(OSIntegrationTestCase):
         # Block until VM's ssh port has been opened
         self.assertTrue(self.inst_creator.vm_ssh_active(block=True))
 
+        # Block until cloud-init has completed
+        self.assertTrue(self.inst_creator.cloud_init_complete(block=True))
+
         # Apply Security Group
         self.inst_creator.add_security_group(
             self.sec_grp_creator.get_security_group())
@@ -326,12 +337,14 @@ class AnsibleProvisioningTests(OSIntegrationTestCase):
         ssh = ansible_utils.ssh_client(ip, user, priv_key,
                                        self.os_creds.proxy_settings)
         self.assertIsNotNone(ssh)
+        scp = None
 
         try:
             scp = SCPClient(ssh.get_transport())
             scp.get('/tmp/hello.txt', self.test_file_local_path)
         finally:
-            scp.close()
+            if scp:
+                scp.close()
             ssh.close()
 
         self.assertTrue(os.path.isfile(self.test_file_local_path))
index a1b72aa..7b3ece7 100644 (file)
 import logging
 import unittest
 
+from snaps.config.tests.cluster_template_tests import (
+    ClusterTemplateConfigUnitTests)
+from snaps.config.tests.network_tests import (
+    NetworkConfigUnitTests, SubnetConfigUnitTests, PortConfigUnitTests)
+from snaps.config.tests.security_group_tests import (
+    SecurityGroupConfigUnitTests, SecurityGroupRuleConfigUnitTests)
+from snaps.config.tests.vm_inst_tests import (
+    VmInstanceConfigUnitTests, FloatingIpConfigUnitTests)
+from snaps.config.tests.volume_tests import VolumeConfigUnitTests
+from snaps.config.tests.volume_type_tests import VolumeTypeConfigUnitTests
+from snaps.config.tests.qos_tests import QoSConfigUnitTests
+from snaps.config.tests.stack_tests import StackConfigUnitTests
+from snaps.config.tests.router_tests import RouterConfigUnitTests
+from snaps.config.tests.user_tests import UserConfigUnitTests
+from snaps.config.tests.project_tests import ProjectConfigUnitTests
+from snaps.config.tests.keypair_tests import KeypairConfigUnitTests
+from snaps.config.tests.flavor_tests import FlavorConfigUnitTests
+import snaps.config.tests.image_tests as image_tests
+import snaps.openstack.tests.create_image_tests as creator_tests
+from snaps.domain.test.cluster_template_tests import ClusterTemplateUnitTests
 from snaps.domain.test.flavor_tests import FlavorDomainObjectTests
 from snaps.domain.test.image_tests import ImageDomainObjectTests
 from snaps.domain.test.keypair_tests import KeypairDomainObjectTests
@@ -34,26 +54,29 @@ from snaps.domain.test.vm_inst_tests import (
     VmInstDomainObjectTests, FloatingIpDomainObjectTests)
 from snaps.domain.test.volume_tests import (
     QoSSpecDomainObjectTests, VolumeTypeDomainObjectTests,
-    VolumeTypeEncryptionObjectTests)
+    VolumeTypeEncryptionObjectTests, VolumeDomainObjectTests)
+from snaps.openstack.tests.cluster_template_tests import (
+    CreateClusterTemplateTests)
 from snaps.openstack.tests.conf.os_credentials_tests import (
     ProxySettingsUnitTests, OSCredsUnitTests)
 from snaps.openstack.tests.create_flavor_tests import (
     CreateFlavorTests, FlavorSettingsUnitTests)
 from snaps.openstack.tests.create_image_tests import (
-    CreateImageSuccessTests, CreateImageNegativeTests, ImageSettingsUnitTests,
+    CreateImageSuccessTests, CreateImageNegativeTests,
     CreateMultiPartImageTests)
 from snaps.openstack.tests.create_instance_tests import (
-    CreateInstanceSingleNetworkTests, CreateInstancePubPrivNetTests,
-    CreateInstanceOnComputeHost, CreateInstanceSimpleTests,
-    FloatingIpSettingsUnitTests, InstanceSecurityGroupTests,
-    VmInstanceSettingsUnitTests, CreateInstancePortManipulationTests,
-    SimpleHealthCheck, CreateInstanceFromThreePartImage,
-    CreateInstanceMockOfflineTests, CreateInstanceTwoNetTests)
+    CreateInstanceSingleNetworkTests,  CreateInstanceOnComputeHost,
+    CreateInstanceSimpleTests, FloatingIpSettingsUnitTests,
+    InstanceSecurityGroupTests, VmInstanceSettingsUnitTests,
+    CreateInstancePortManipulationTests, SimpleHealthCheck,
+    CreateInstanceFromThreePartImage, CreateInstanceMockOfflineTests,
+    CreateInstanceTwoNetTests, CreateInstanceVolumeTests,
+    CreateInstanceIPv6NetworkTests)
 from snaps.openstack.tests.create_keypairs_tests import (
     CreateKeypairsTests, KeypairSettingsUnitTests, CreateKeypairsCleanupTests)
 from snaps.openstack.tests.create_network_tests import (
     CreateNetworkSuccessTests, NetworkSettingsUnitTests, PortSettingsUnitTests,
-    SubnetSettingsUnitTests, CreateNetworkTypeTests)
+    SubnetSettingsUnitTests, CreateNetworkTypeTests, CreateNetworkIPv6Tests)
 from snaps.openstack.tests.create_project_tests import (
     CreateProjectSuccessTests, ProjectSettingsUnitTests,
     CreateProjectUserTests)
@@ -67,9 +90,15 @@ from snaps.openstack.tests.create_security_group_tests import (
     SecurityGroupSettingsUnitTests)
 from snaps.openstack.tests.create_stack_tests import (
     StackSettingsUnitTests, CreateStackSuccessTests, CreateStackNegativeTests,
-    CreateComplexStackTests)
+    CreateStackFlavorTests, CreateStackFloatingIpTests,
+    CreateStackNestedResourceTests, CreateStackKeypairTests,
+    CreateStackVolumeTests, CreateStackSecurityGroupTests)
 from snaps.openstack.tests.create_user_tests import (
     UserSettingsUnitTests, CreateUserSuccessTests)
+from snaps.openstack.tests.create_volume_tests import (
+    VolumeSettingsUnitTests, CreateSimpleVolumeSuccessTests,
+    CreateVolumeWithTypeTests, CreateVolumeWithImageTests,
+    CreateSimpleVolumeFailureTests)
 from snaps.openstack.tests.create_volume_type_tests import (
     VolumeTypeSettingsUnitTests, CreateSimpleVolumeTypeSuccessTests,
     CreateVolumeTypeComplexTests)
@@ -77,21 +106,27 @@ from snaps.openstack.tests.os_source_file_test import (
     OSComponentTestCase, OSIntegrationTestCase)
 from snaps.openstack.utils.tests.cinder_utils_tests import (
     CinderSmokeTests, CinderUtilsQoSTests, CinderUtilsSimpleVolumeTypeTests,
-    CinderUtilsAddEncryptionTests, CinderUtilsVolumeTypeCompleteTests)
+    CinderUtilsAddEncryptionTests, CinderUtilsVolumeTypeCompleteTests,
+    CinderUtilsVolumeTests)
 from snaps.openstack.utils.tests.glance_utils_tests import (
     GlanceSmokeTests, GlanceUtilsTests)
 from snaps.openstack.utils.tests.heat_utils_tests import (
     HeatSmokeTests, HeatUtilsCreateSimpleStackTests,
-    HeatUtilsCreateComplexStackTests)
+    HeatUtilsCreateComplexStackTests, HeatUtilsFlavorTests,
+    HeatUtilsKeypairTests, HeatUtilsVolumeTests, HeatUtilsSecurityGroupTests)
 from snaps.openstack.utils.tests.keystone_utils_tests import (
     KeystoneSmokeTests, KeystoneUtilsTests)
 from snaps.openstack.utils.tests.neutron_utils_tests import (
     NeutronSmokeTests, NeutronUtilsNetworkTests, NeutronUtilsSubnetTests,
     NeutronUtilsRouterTests, NeutronUtilsSecurityGroupTests,
-    NeutronUtilsFloatingIpTests)
+    NeutronUtilsFloatingIpTests, NeutronUtilsIPv6Tests)
 from snaps.openstack.utils.tests.nova_utils_tests import (
     NovaSmokeTests, NovaUtilsKeypairTests, NovaUtilsFlavorTests,
-    NovaUtilsInstanceTests)
+    NovaUtilsInstanceTests, NovaUtilsInstanceVolumeTests)
+from snaps.openstack.utils.tests.settings_utils_tests import (
+    SettingsUtilsUnitTests)
+from snaps.openstack.utils.tests.magnum_utils_tests import (
+    MagnumSmokeTests, MagnumUtilsClusterTypeTests)
 from snaps.provisioning.tests.ansible_utils_tests import (
     AnsibleProvisioningTests)
 from snaps.tests.file_utils_tests import FileUtilsTests
@@ -106,34 +141,48 @@ def add_unit_tests(suite):
     :return: None as the tests will be adding to the 'suite' parameter object
     """
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(FileUtilsTests))
-    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
-        SecurityGroupRuleSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         ProxySettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         OSCredsUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        SecurityGroupConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         SecurityGroupSettingsUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        SecurityGroupRuleConfigUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        SecurityGroupRuleSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         SecurityGroupDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         SecurityGroupRuleDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
-        ImageSettingsUnitTests))
+        image_tests.ImageConfigUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        creator_tests.ImageSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         ImageDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        FlavorConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         FlavorSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         FlavorDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        KeypairConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         KeypairSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         KeypairDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        UserConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         UserSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         UserDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        ProjectConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         ProjectSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
@@ -146,48 +195,78 @@ def add_unit_tests(suite):
         NetworkQuotasDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         RoleDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        NetworkConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         NetworkSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         NetworkObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        SubnetConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         SubnetSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         SubnetObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        PortConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         PortSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         PortDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        RouterConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         RouterSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         RouterDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         InterfaceRouterDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        FloatingIpConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         FloatingIpSettingsUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        VmInstanceConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         VmInstanceSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         StackDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         ResourceDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        StackConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         StackSettingsUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         VolumeTypeDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         VolumeTypeEncryptionObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        VolumeDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         QoSSpecDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         VmInstDomainObjectTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         FloatingIpDomainObjectTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        QoSConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         QoSSettingsUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        VolumeTypeConfigUnitTests))
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
         VolumeTypeSettingsUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        VolumeConfigUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        VolumeSettingsUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        ClusterTemplateConfigUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        ClusterTemplateUnitTests))
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(
+        SettingsUtilsUnitTests))
 
 
 def add_openstack_client_tests(suite, os_creds, ext_net_name,
@@ -278,6 +357,9 @@ def add_openstack_api_tests(suite, os_creds, ext_net_name, use_keystone=True,
     suite.addTest(OSComponentTestCase.parameterize(
         NeutronUtilsSubnetTests, os_creds=os_creds, ext_net_name=ext_net_name,
         log_level=log_level))
+    suite.addTest(OSComponentTestCase.parameterize(
+        NeutronUtilsIPv6Tests, os_creds=os_creds, ext_net_name=ext_net_name,
+        log_level=log_level))
     suite.addTest(OSComponentTestCase.parameterize(
         NeutronUtilsRouterTests, os_creds=os_creds, ext_net_name=ext_net_name,
         log_level=log_level))
@@ -296,6 +378,10 @@ def add_openstack_api_tests(suite, os_creds, ext_net_name, use_keystone=True,
     suite.addTest(OSComponentTestCase.parameterize(
         NovaUtilsInstanceTests, os_creds=os_creds, ext_net_name=ext_net_name,
         log_level=log_level, image_metadata=image_metadata))
+    suite.addTest(OSComponentTestCase.parameterize(
+        NovaUtilsInstanceVolumeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level,
+        image_metadata=image_metadata))
     suite.addTest(OSComponentTestCase.parameterize(
         CreateFlavorTests, os_creds=os_creds, ext_net_name=ext_net_name,
         log_level=log_level))
@@ -307,10 +393,30 @@ def add_openstack_api_tests(suite, os_creds, ext_net_name, use_keystone=True,
         HeatUtilsCreateComplexStackTests, os_creds=os_creds,
         ext_net_name=ext_net_name, log_level=log_level,
         image_metadata=image_metadata))
+    suite.addTest(OSComponentTestCase.parameterize(
+        HeatUtilsFlavorTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level,
+        image_metadata=image_metadata))
+    suite.addTest(OSComponentTestCase.parameterize(
+        HeatUtilsKeypairTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level,
+        image_metadata=image_metadata))
+    suite.addTest(OSComponentTestCase.parameterize(
+        HeatUtilsSecurityGroupTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level,
+        image_metadata=image_metadata))
+    suite.addTest(OSComponentTestCase.parameterize(
+        HeatUtilsVolumeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level,
+        image_metadata=image_metadata))
     suite.addTest(OSComponentTestCase.parameterize(
         CinderUtilsQoSTests, os_creds=os_creds,
         ext_net_name=ext_net_name, log_level=log_level,
         image_metadata=image_metadata))
+    suite.addTest(OSComponentTestCase.parameterize(
+        CinderUtilsVolumeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level,
+        image_metadata=image_metadata))
     suite.addTest(OSComponentTestCase.parameterize(
         CinderUtilsSimpleVolumeTypeTests, os_creds=os_creds,
         ext_net_name=ext_net_name, log_level=log_level,
@@ -392,6 +498,11 @@ def add_openstack_integration_tests(suite, os_creds, ext_net_name,
         ext_net_name=ext_net_name, use_keystone=use_keystone,
         flavor_metadata=flavor_metadata, image_metadata=image_metadata,
         log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateNetworkIPv6Tests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
     suite.addTest(OSIntegrationTestCase.parameterize(
         CreateRouterSuccessTests, os_creds=os_creds, ext_net_name=ext_net_name,
         use_keystone=use_keystone,
@@ -417,6 +528,26 @@ def add_openstack_integration_tests(suite, os_creds, ext_net_name,
         ext_net_name=ext_net_name, use_keystone=use_keystone,
         flavor_metadata=flavor_metadata, image_metadata=image_metadata,
         log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateSimpleVolumeSuccessTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateSimpleVolumeFailureTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateVolumeWithTypeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateVolumeWithImageTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
 
     # VM Instances
     suite.addTest(OSIntegrationTestCase.parameterize(
@@ -454,11 +585,41 @@ def add_openstack_integration_tests(suite, os_creds, ext_net_name,
         ext_net_name=ext_net_name, use_keystone=use_keystone,
         flavor_metadata=flavor_metadata, image_metadata=image_metadata,
         log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateInstanceVolumeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateInstanceIPv6NetworkTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
     suite.addTest(OSIntegrationTestCase.parameterize(
         CreateStackSuccessTests, os_creds=os_creds, ext_net_name=ext_net_name,
         use_keystone=use_keystone,
         flavor_metadata=flavor_metadata, image_metadata=image_metadata,
         log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateStackVolumeTests, os_creds=os_creds, ext_net_name=ext_net_name,
+        use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateStackFlavorTests, os_creds=os_creds, ext_net_name=ext_net_name,
+        use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateStackKeypairTests, os_creds=os_creds, ext_net_name=ext_net_name,
+        use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
+    suite.addTest(OSIntegrationTestCase.parameterize(
+        CreateStackSecurityGroupTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, use_keystone=use_keystone,
+        flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+        log_level=log_level))
     suite.addTest(OSIntegrationTestCase.parameterize(
         CreateStackNegativeTests, os_creds=os_creds, ext_net_name=ext_net_name,
         use_keystone=use_keystone,
@@ -472,7 +633,12 @@ def add_openstack_integration_tests(suite, os_creds, ext_net_name,
             flavor_metadata=flavor_metadata, image_metadata=image_metadata,
             log_level=log_level))
         suite.addTest(OSIntegrationTestCase.parameterize(
-            CreateComplexStackTests, os_creds=os_creds,
+            CreateStackFloatingIpTests, os_creds=os_creds,
+            ext_net_name=ext_net_name, use_keystone=use_keystone,
+            flavor_metadata=flavor_metadata, image_metadata=image_metadata,
+            log_level=log_level))
+        suite.addTest(OSIntegrationTestCase.parameterize(
+            CreateStackNestedResourceTests, os_creds=os_creds,
             ext_net_name=ext_net_name, use_keystone=use_keystone,
             flavor_metadata=flavor_metadata, image_metadata=image_metadata,
             log_level=log_level))
@@ -556,11 +722,17 @@ def add_openstack_staging_tests(suite, os_creds, ext_net_name,
     :return: None as the tests will be adding to the 'suite' parameter object
     """
     suite.addTest(OSComponentTestCase.parameterize(
-        CreateNetworkTypeTests, os_creds=os_creds, ext_net_name=ext_net_name,
-        log_level=log_level))
+        CreateNetworkTypeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level))
     suite.addTest(OSComponentTestCase.parameterize(
         CreateInstanceMockOfflineTests, os_creds=os_creds,
         ext_net_name=ext_net_name, log_level=log_level))
-    suite.addTest(OSIntegrationTestCase.parameterize(
-            CreateInstancePubPrivNetTests, os_creds=os_creds,
-            ext_net_name=ext_net_name, log_level=log_level))
+    suite.addTest(OSComponentTestCase.parameterize(
+        MagnumSmokeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level))
+    suite.addTest(OSComponentTestCase.parameterize(
+        MagnumUtilsClusterTypeTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level))
+    suite.addTest(OSComponentTestCase.parameterize(
+        CreateClusterTemplateTests, os_creds=os_creds,
+        ext_net_name=ext_net_name, log_level=log_level))