summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/how-to-use/APITests.rst261
-rw-r--r--docs/how-to-use/IntegrationTests.rst290
-rw-r--r--docs/how-to-use/Testing.rst44
-rw-r--r--docs/how-to-use/UnitTests.rst91
-rw-r--r--docs/how-to-use/VirtEnvDeploy.rst275
-rw-r--r--docs/how-to-use/index.rst26
-rw-r--r--examples/complex-network/deploy-complex-network.yaml234
-rw-r--r--examples/complex-network/main.yml16
-rw-r--r--examples/complex-network/playbooks/sample-playbook.yml20
-rw-r--r--examples/external-network/deploy-ext-net.yaml77
-rw-r--r--examples/simple/deploy-simple.yaml101
-rw-r--r--examples/simple/files/motd8
-rw-r--r--examples/simple/main.yml16
-rw-r--r--examples/simple/playbooks/sample-playbook.yml23
-rw-r--r--examples/two-network/deploy-two-net-centos.yaml96
-rw-r--r--examples/two-network/deploy-two-net-ubuntu.yaml96
-rw-r--r--snaps/__init__.py15
-rw-r--r--snaps/deploy_venv.py573
-rw-r--r--snaps/file_utils.py108
-rw-r--r--snaps/openstack/__init__.py15
-rw-r--r--snaps/openstack/create_flavor.py179
-rw-r--r--snaps/openstack/create_image.py188
-rw-r--r--snaps/openstack/create_instance.py739
-rw-r--r--snaps/openstack/create_keypairs.py121
-rw-r--r--snaps/openstack/create_network.py519
-rw-r--r--snaps/openstack/create_project.py139
-rw-r--r--snaps/openstack/create_router.py244
-rw-r--r--snaps/openstack/create_security_group.py521
-rw-r--r--snaps/openstack/create_user.py137
-rw-r--r--snaps/openstack/os_credentials.py103
-rw-r--r--snaps/openstack/tests/__init__.py15
-rw-r--r--snaps/openstack/tests/conf/os_env.yaml.template17
-rw-r--r--snaps/openstack/tests/conf/overcloudrc_test9
-rw-r--r--snaps/openstack/tests/create_flavor_tests.py319
-rw-r--r--snaps/openstack/tests/create_image_tests.py362
-rw-r--r--snaps/openstack/tests/create_instance_tests.py1474
-rw-r--r--snaps/openstack/tests/create_keypairs_tests.py203
-rw-r--r--snaps/openstack/tests/create_network_tests.py533
-rw-r--r--snaps/openstack/tests/create_project_tests.py228
-rw-r--r--snaps/openstack/tests/create_router_tests.py264
-rw-r--r--snaps/openstack/tests/create_security_group_tests.py355
-rw-r--r--snaps/openstack/tests/create_user_tests.py155
-rw-r--r--snaps/openstack/tests/openstack_tests.py144
-rw-r--r--snaps/openstack/tests/os_source_file_test.py131
-rw-r--r--snaps/openstack/tests/validation_utils.py69
-rw-r--r--snaps/openstack/utils/__init__.py15
-rw-r--r--snaps/openstack/utils/deploy_utils.py151
-rw-r--r--snaps/openstack/utils/glance_utils.py78
-rw-r--r--snaps/openstack/utils/keystone_utils.py204
-rw-r--r--snaps/openstack/utils/neutron_utils.py405
-rw-r--r--snaps/openstack/utils/nova_utils.py282
-rw-r--r--snaps/openstack/utils/tests/__init__.py15
-rw-r--r--snaps/openstack/utils/tests/glance_utils_tests.py115
-rw-r--r--snaps/openstack/utils/tests/keystone_utils_tests.py100
-rw-r--r--snaps/openstack/utils/tests/neutron_utils_tests.py651
-rw-r--r--snaps/openstack/utils/tests/nova_utils_tests.py208
-rw-r--r--snaps/playbook_runner.py58
-rw-r--r--snaps/provisioning/__init__.py15
-rw-r--r--snaps/provisioning/ansible/centos-network-setup/playbooks/configure_host.yml26
-rw-r--r--snaps/provisioning/ansible/centos-network-setup/templates/ifcfg-interface14
-rw-r--r--snaps/provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml26
-rw-r--r--snaps/provisioning/ansible/ubuntu-network-setup/templates/ethN.cfg2
-rw-r--r--snaps/provisioning/ansible_utils.py114
-rw-r--r--snaps/provisioning/tests/__init__.py15
-rw-r--r--snaps/provisioning/tests/ansible_utils_tests.py217
-rw-r--r--snaps/provisioning/tests/playbooks/simple_playbook.yml21
-rw-r--r--snaps/provisioning/tests/playbooks/template_playbook.yml23
-rw-r--r--snaps/provisioning/tests/scripts/hello.txt1
-rw-r--r--snaps/provisioning/tests/scripts/template.txt1
-rw-r--r--snaps/test_suite_builder.py208
-rw-r--r--snaps/tests/__init__.py15
-rw-r--r--snaps/tests/file_utils_tests.py102
-rw-r--r--snaps/unit_test_suite.py131
73 files changed, 12766 insertions, 0 deletions
diff --git a/docs/how-to-use/APITests.rst b/docs/how-to-use/APITests.rst
new file mode 100644
index 0000000..599325f
--- /dev/null
+++ b/docs/how-to-use/APITests.rst
@@ -0,0 +1,261 @@
+SNAPS OpenStack API Testing
+===========================
+
+Tests designated as component tests extend the snaps.openstack.tests.OSComponentTestCase class and must be exercised
+with OpenStack credentials for all as well as an external network for many. When leveraging the unit\_test\_suite.py
+application, the -e argument and -n arguments will suffice. When attempting to execute these tests within your IDE
+of choice (tested on IntelliJ), you will need to edit the [repo\_dir]/snaps/openstack/tests/conf/os\_env.yaml file as well
+as ensuring that your run configuration's working directory is set to [repo\_dir]/snaps.
+
+The Test Classes
+================
+
+glance_utils_tests.py - GlanceSmokeTests
+----------------------------------------
+
+Ensures that a Glance client can be obtained as well as the proper
+exceptions thrown with the wrong credentials.
+
+keystone_utils_tests.py - KeystoneSmokeTests
+--------------------------------------------
+
+Ensures that a Keystone client can be obtained as well as the proper
+exceptions thrown with the wrong credentials.
+
+neutron_utils_tests.py - NeutronSmokeTests
+------------------------------------------
+
+Ensures that a Neutron client can be obtained as well as the proper
+exceptions thrown with the wrong credentials.
+
+nova_utils_tests.py - NovaSmokeTests
+------------------------------------
+
+Ensures that a Nova client can be obtained as well as the proper
+exceptions thrown with the wrong credentials.
+
+keystone_utils_tests.py - KeystoneUtilsTests
+--------------------------------------------
+
++----------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Keystone API | Description |
++==================================+===============+===========================================================+
+| test_create_user_minimal | 2 & 3 | Tests the creation of a user with minimal configuration |
+| | | settings via the utility functions |
++----------------------------------+---------------+-----------------------------------------------------------+
+| test_create_project_minimal | 2 & 3 | Tests the creation of a project with minimal configuration|
+| | | settings via the utility functions |
++----------------------------------+---------------+-----------------------------------------------------------+
+
+create_user_tests.py - CreateUserSuccessTests
+---------------------------------------------
++----------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Keystone API | Description |
++==================================+===============+===========================================================+
+| test_create_user | 2 & 3 | Tests the creation of a user with minimal configuration |
+| | | settings via the utility functions |
++----------------------------------+---------------+-----------------------------------------------------------+
+
+create_project_tests.py - CreateProjectSuccessTests
+---------------------------------------------------
+
++----------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Keystone API | Description |
++==================================+===============+===========================================================+
+| test_create_user_minimal | 2 & 3 | Tests the creation of a user via the OpenStackUser class |
++----------------------------------+---------------+-----------------------------------------------------------+
+| test_create_user_2x | 2 & 3 | Tests the creation of a user a second time via the |
+| | | OpenStackUser class to ensure it is only created once |
++----------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_user | 2 & 3 | Tests the creation and deletion of a user via the |
+| | | OpenStackUser class to ensure that clean will not raise |
+| | | an exception |
++----------------------------------+---------------+-----------------------------------------------------------+
+
+create_project_tests.py - CreateProjectUserTests
+------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Keystone API | Description |
++=======================================+===============+===========================================================+
+| test_create_project_sec_grp_one_user | 2 & 3 | Tests the creation of an OpenStack object to a project |
+| | | with a new users and to create a security group |
+| | | |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_project_sec_grp_two_users | 2 & 3 | Tests the creation of an OpenStack object to a project |
+| | | with two new users and to create a security group under |
+| | | each |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+glance_utils_tests.py - GlanceUtilsTests
+------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Glance API | Description |
++=======================================+===============+===========================================================+
+| test_create_image_minimal_url | 1 | Tests the glance_utils.create_image() function with a URL |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_image_minimal_file | 1 | Tests the glance_utils.create_image() function with a file|
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+neutron_utils_tests.py - NeutronUtilsNetworkTests
+-------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Neutron API | Description |
++=======================================+===============+===========================================================+
+| test_create_network | 2 | Ensures neutron_utils.create_network() properly creates a |
+| | | network |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_empty_name | 2 | Ensures neutron_utils.create_network() raises an exception|
+| | | when the network name is an empty string |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_null_name | 2 | Ensures neutron_utils.create_network() raises an exception|
+| | | when the network name is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+neutron_utils_tests.py - NeutronUtilsSubnetTests
+------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Neutron API | Description |
++=======================================+===============+===========================================================+
+| test_create_subnet | 2 | Ensures neutron_utils.create_subnet() can properly create |
+| | | an OpenStack subnet object |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_subnet_null_name | 2 | Ensures neutron_utils.create_subnet() raises an exception |
+| | | when the subnet name is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_subnet_empty_name | 2 | Ensures neutron_utils.create_subnet() 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 |
+| | | when the subnet CIDR is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_subnet_empty_cidr | 2 | Ensures neutron_utils.create_subnet() raises an exception |
+| | | when the subnet CIDR is an empty string |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+neutron_utils_tests.py - NeutronUtilsRouterTests
+------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Neutron API | Description |
++=======================================+===============+===========================================================+
+| test_create_router_simple | 2 | Ensures neutron_utils.create_router() can properly create |
+| | | a simple OpenStack router object |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_router_with_public_inter | 2 | Ensures neutron_utils.create_router() can properly create |
+| 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 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_add_interface_router_null_router | 2 | Ensures neutron_utils.add_interface_router() raises an |
+| | | exception when the router object is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_add_interface_router_null_subnet | 2 | Ensures neutron_utils.add_interface_router() raises an |
+| | | exception when the subnet object is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_port | 2 | Ensures neutron_utils.create_port() can properly create an|
+| | | OpenStack port object |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_port_empty_name | 2 | Ensures neutron_utils.create_port() raises an exception |
+| | | when the port name is an empty string |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_port_null_name | 2 | Ensures neutron_utils.create_port() raises an exception |
+| | | when the port name is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_port_null_network_object | 2 | Ensures neutron_utils.create_port() raises an exception |
+| | | when the network object is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_port_null_ip | 2 | Ensures neutron_utils.create_port() raises an exception |
+| | | when the assigned IP value is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_port_invalid_ip | 2 | Ensures neutron_utils.create_port() raises an exception |
+| | | when the assigned IP value is invalid |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_port_invalid_ip_to_subnet | 2 | Ensures neutron_utils.create_port() raises an exception |
+| | | when the assigned IP value is not part of CIDR |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+neutron_utils_tests.py - NeutronUtilsSecurityGroupTests
+-------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Neutron API | Description |
++=======================================+===============+===========================================================+
+| test_create_delete_simple_sec_grp | 2 | Ensures that a security group can be created |
+| | | (neutron_utils.create_security_group() and deleted via |
+| | | neutron_utils.delete_security_group() |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_sec_grp_no_name | 2 | Ensures that neutron_utils.create_security_group() raises |
+| | | an exception when attempting to create a security group |
+| | | without a name |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_sec_grp_no_rules | 2 | Ensures that neutron_utils.create_security_group() can |
+| | | create a security group without any rules |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_sec_grp_one_rule | 2 | Ensures that neutron_utils.create_security_group_rule() |
+| | | can add a rule to a security group |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+nova_utils_tests.py - NovaUtilsKeypairTests
+-------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Nova API | Description |
++=======================================+===============+===========================================================+
+| test_create_keypair | 2 | Ensures that a keypair can be properly created via |
+| | | nova_utils.upload_keypair() with a public_key object |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_keypair | 2 | Ensures that a keypair can be properly deleted via |
+| | | nova_utils.delete_keypair() |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_key_from_file | 2 | Ensures that a keypair can be properly created via |
+| | | nova_utils.upload_keypair_file() |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_floating_ips | 2 | Ensures that a floating IP can be properly created via |
+| | | nova_utils.create_floating_ip() [note: this test should |
+| | | be moved to a new class] |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+nova_utils_tests.py - NovaUtilsFlavorTests
+------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Nova API | Description |
++=======================================+===============+===========================================================+
+| test_create_flavor | 2 | Ensures that a flavor can be properly created via |
+| | | nova_utils.create_flavor() |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_flavor | 2 | Ensures that a flavor can be properly deleted via |
+| | | nova_utils.delete_flavor() |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_flavor_tests.py - CreateFlavorTests
+------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Nova API | Description |
++=======================================+===============+===========================================================+
+| test_create_flavor | 2 | Ensures that the OpenStackFlavor class's create() method |
+| | | creates an OpenStack flavor object |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_flavor_existing | 2 | Ensures that the OpenStackFlavor class's create() will not|
+| | | create a flavor with the same name more than once |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_clean_flavor | 2 | Ensures that the OpenStackFlavor class's clean() method |
+| | | will delete the flavor object |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_flavor | 2 | Ensures that the OpenStackFlavor class's clean() method |
+| | | will not raise an exception when called and the object no |
+| | | longer exists |
++---------------------------------------+---------------+-----------------------------------------------------------+
diff --git a/docs/how-to-use/IntegrationTests.rst b/docs/how-to-use/IntegrationTests.rst
new file mode 100644
index 0000000..983c165
--- /dev/null
+++ b/docs/how-to-use/IntegrationTests.rst
@@ -0,0 +1,290 @@
+SNAPS OpenStack Integration Testing
+===================================
+
+These tests are ones designed to be run within their own dynamically created project along with a newly generated user
+account and generally require other OpenStack object creators.
+
+The Test Classes
+================
+
+create_security_group_tests.py - CreateSecurityGroupTests
+---------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_create_group_without_rules | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class can create a |
+| | Neutron 2 | security group without any rules |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_group | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class clean() method |
+| | Neutron 2 | will not raise an exception should the group be deleted by|
+| | | some other process |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_group_with_one_simple_rule| Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class can create a |
+| | Neutron 2 | security group with a single rule |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_group_with_several_rules | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup class can create a |
+| | Neutron 2 | security group with several rules |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_add_rule | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup#add_rule() method |
+| | Neutron 2 | properly creates and associates the new rule |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_remove_rule_by_id | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup#remove_rule() method |
+| | Neutron 2 | properly deletes and disassociates the old rule via its ID|
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_remove_rule_by_setting | Keysone 2 & 3 | Ensures the OpenStackSecurityGroup#remove_rule() method |
+| | Neutron 2 | properly deletes and disassociates the old rule via its |
+| | | setting object |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_image_tests.py - CreateImageSuccessTests
+-----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Glance API | Description |
++=======================================+===============+===========================================================+
+| test_create_image_clean_url | 1 | Ensures the OpenStackImage class can create an image from |
+| | | a download URL location |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_image_clean_file | 1 | Ensures the OpenStackImage class can create an image from |
+| | | a locally sourced image file |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_image | 1 | Ensures the OpenStackImage.clean() method deletes an image|
+| | | and does not raise an exception on subsequent calls to the|
+| | | clean() method |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_same_image | 1 | Ensures the OpenStackImage.create() method does not create|
+| | | another image when one already exists with the same name |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_image_tests.py - CreateImageNegativeTests
+------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Glance API | Description |
++=======================================+===============+===========================================================+
+| test_none_image_name | 1 | Ensures OpenStackImage.create() results in an Exception |
+| | | being raised when the ImageSettings.name attribute has |
+| | | not been set |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_bad_image_url | 1 | Ensures OpenStackImage.create() results in an Exception |
+| | | being raised when the download URL is invalid |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_bad_image_file | 1 | Ensures OpenStackImage.create() results in an Exception |
+| | | being raised when the image file does not exist |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_none_proj_name | 1 | Ensures OpenStackImage.create() results in an Exception |
+| | | being raised when the credentials project name is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_none_auth_url | 1 | Ensures OpenStackImage.create() results in an Exception |
+| | | being raised when the credentials URL is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_none_password | 1 | Ensures OpenStackImage.create() results in an Exception |
+| | | being raised when the credentials password is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_none_user | 1 | Ensures OpenStackImage.create() results in an Exception |
+| | | being raised when the credentials user is None |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_keypairs_tests.py - CreateKeypairsTests
+----------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Nova API | Description |
++=======================================+===============+===========================================================+
+| test_create_keypair_only | 2 | Ensures that a keypair object can be created simply by |
+| | | only configuring a name |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_keypair | 2 | Ensures that a keypair object is deleted via |
+| | | OpenStackKeypair.clean() and subsequent calls do not |
+| | | result in exceptions |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_keypair_save_pub_only | 2 | Ensures that a keypair object can be created when the only|
+| | | the public key is cached to disk |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_keypair_save_both | 2 | Ensures that a keypair object can be created when both the|
+| | | public and private keys are cached to disk |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_keypair_from_file | 2 | Ensures that a keypair object can be created with an |
+| | | existing public key file |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_network_tests.py - CreateNetworkSuccessTests
+---------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Neutron API | Description |
++=======================================+===============+===========================================================+
+| test_create_network_without_router | 2 | Ensures that a network can be created via the |
+| | | OpenStackNetwork class without any routers |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_network | 2 | Ensures that a router can be deleted via the |
+| | | OpenStackNetwork.clean() method |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_network_with_router | 2 | Ensures that a network can be created via the |
+| | | OpenStackNetwork class with a router |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_networks_same_name | 2 | Ensures that the OpenStackNetwork.create() method will not|
+| | | create a network with the same name |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_router_tests.py - CreateRouterSuccessTests
+-------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Neutron API | Description |
++=======================================+===============+===========================================================+
+| test_create_router_vanilla | 2 | Ensures that a router can be created via the |
+| | | OpenStackRouter class with minimal settings |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_delete_router | 2 | Ensures that a router can be deleted via the |
+| | | OpenStackRouter.clean() method |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_router_admin_state_false | 2 | Ensures that a router can created with |
+| | | admin_state_up = False |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_router_admin_state_True | 2 | Ensures that a router can created with |
+| | | admin_state_up = True |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_router_private_network | 2 | Ensures that a router port can be created against a |
+| | | private network |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_router_external_network | 2 | Ensures that a router can be created that is connected to |
+| | | both external and private internal networks |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_router_tests.py - CreateRouterNegativeTests
+--------------------------------------------------
+
++----------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | Neutron API | Description |
++========================================+===============+===========================================================+
+| test_create_router_noname | 2 | Ensures that an exception is raised when attempting to |
+| | | create a router without a name |
++----------------------------------------+---------------+-----------------------------------------------------------+
+| test_create_router_invalid_gateway_name| 2 | Ensures that an exception is raised when attempting to |
+| | | create a router to an external network that does not exist|
++----------------------------------------+---------------+-----------------------------------------------------------+
+
+create_instance_tests.py - CreateInstanceSimpleTests
+----------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_create_delete_instance | Nova 2 | Ensures that the OpenStackVmInstance.clean() method |
+| | Neutron 2 | deletes the instance |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_instance_tests.py - SimpleHealthCheck
+--------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_check_vm_ip_dhcp | Nova 2 | Tests the creation of an OpenStack instance with a single |
+| | Neutron 2 | port and it's assigned IP address |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_instance_tests.py - CreateInstanceSingleNetworkTests
+-----------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_single_port_static | Nova 2 | Ensures that an instance with a single port/NIC with a |
+| | Neutron 2 | static IP can be created |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_ssh_client_fip_before_active | Nova 2 | Ensures that an instance can be reached over SSH when the |
+| | Neutron 2 | floating IP is assigned prior to the VM becoming ACTIVE |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| 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 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_instance_tests.py - CreateInstancePortManipulationTests
+--------------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_set_custom_valid_ip_one_subnet | Nova 2 | Ensures that an instance's can have a valid static IP is |
+| | Neutron 2 | properly assigned |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_set_custom_invalid_ip_one_subnet | Nova 2 | Ensures that an instance's port with an invalid static IP |
+| | Neutron 2 | raises an exception |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_set_custom_valid_mac | Nova 2 | Ensures that an instance's port can have a valid MAC |
+| | Neutron 2 | address properly assigned |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_set_custom_invalid_mac | Nova 2 | Ensures that an instance's port with an invalid MAC |
+| | Neutron 2 | address raises and exception |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_set_custom_mac_and_ip | Nova 2 | Ensures that an instance's port with a valid static IP and|
+| | Neutron 2 | MAC are properly assigned |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_set_allowed_address_pairs | Nova 2 | Ensures the configured allowed_address_pairs is properly |
+| | Neutron 2 | set on a VMs port |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_set_allowed_address_pairs_bad_mac| Nova 2 | Ensures the port cannot be created when a bad MAC address |
+| | Neutron 2 | format is used in the allowed_address_pairs port attribute|
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_set_allowed_address_pairs_bad_ip | Nova 2 | Ensures the port cannot be created when a bad IP address |
+| | Neutron 2 | format is used in the allowed_address_pairs port attribute|
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_instance_tests.py - CreateInstanceOnComputeHost
+------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_deploy_vm_to_each_compute_node | Nova 2 | Tests to ensure that one can fire up an instance on each |
+| | Neutron 2 | active compute node |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_instance_tests.py - CreateInstancePubPrivNetTests
+--------------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| 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 |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+create_instance_tests.py - InstanceSecurityGroupTests
+-----------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_add_security_group | Nova 2 | Ensures that a VM instance can have security group added |
+| | Neutron 2 | to it while its running |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_add_invalid_security_group | Nova 2 | Ensures that a VM instance does not accept the addition of|
+| | Neutron 2 | a security group that no longer exists |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_remove_security_group | Nova 2 | Ensures that a VM instance accepts the removal of a |
+| | Neutron 2 | security group |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_remove_security_group_never_added| Nova 2 | Ensures that a VM instance does not accept the removal of |
+| | Neutron 2 | a security group that was never added in the first place |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_add_same_security_group | Nova 2 | Ensures that a VM instance does not add a security group |
+| | Neutron 2 | that has already been added to the instance |
++---------------------------------------+---------------+-----------------------------------------------------------+
+
+ansible_utils_tests.py - AnsibleProvisioningTests
+-------------------------------------------------
+
++---------------------------------------+---------------+-----------------------------------------------------------+
+| Test Name | API Versions | Description |
++=======================================+===============+===========================================================+
+| test_apply_simple_playbook | Nova 2 | Ensures that an instance assigned with a floating IP will |
+| | Neutron 2 | apply a simple Ansible playbook |
++---------------------------------------+---------------+-----------------------------------------------------------+
+| test_apply_template_playbook | Nova 2 | Ensures that an instance assigned with a floating IP will |
+| | Neutron 2 | apply a Ansible playbook containing Jinga2 substitution |
+| | | values |
++---------------------------------------+---------------+-----------------------------------------------------------+
diff --git a/docs/how-to-use/Testing.rst b/docs/how-to-use/Testing.rst
new file mode 100644
index 0000000..586974a
--- /dev/null
+++ b/docs/how-to-use/Testing.rst
@@ -0,0 +1,44 @@
+Running Unit Test Suite
+=======================
+
+These tests are written in Python and require an that it is setup before running the tests.
+See `install directions <index.md>`__ for Python installation instructions.
+
+Start by cloning the snaps-provisioning repository
+--------------------------------------------------
+
+``git clone https://gerrit.cablelabs.com/snaps-provisioning``
+
+Install Library
+---------------
+
+``pip install -e <path to repo>/``
+
+Execute the tests
+-----------------
+
+| ``cd <path to repo> python snaps/unit_test_suite.py -e [path to RC file] -n [external network name]``
+| \* All Supported Arguments
+| \* -e [required - The path to the OpenStack RC file]
+| \* -n [required - The name of the external network to use for routers
+ and floating IPs]
+| \* -p [optional - the proxy settings if required. Format :
+| \* -s [optional - the proxy command used for SSH connections]
+| \* -l [(default INFO) The log level]
+| \* -k [optional - When set, tests project and user creation. Use only
+ if host running tests has access to the cloud's admin network]
+| \* -f [optional - When set, will not execute tests requiring Floating
+ IPS]
+| \* -u [optional - When set, the unit tests will be executed]
+
+Test descriptions
+=================
+
+`Unit Testing <UnitTests.rst>`__ - Tests that do not require a connection to OpenStack
+--------------------------------------------------------------------------------------
+
+`OpenStack API Tests <APITests.rst>`__ - Tests many individual OpenStack API calls
+----------------------------------------------------------------------------------
+
+`Integration Tests <IntegrationTests.rst>`__ - Tests OpenStack object creation in a context. These tests will be run within a custom project as a specific user.
+----------------------------------------------------------------------------------------------------------------------------------------------------------------
diff --git a/docs/how-to-use/UnitTests.rst b/docs/how-to-use/UnitTests.rst
new file mode 100644
index 0000000..efd6426
--- /dev/null
+++ b/docs/how-to-use/UnitTests.rst
@@ -0,0 +1,91 @@
+SNAPS Unit Testing
+==================
+
+| Tests designated as Unit tests extend the unittest.TestCase class and
+ can be exercised without any external resources
+| other than the filesystem. Most of these tests simply ensure that the
+ configuration settings classes check their
+| constructor arguments properly.
+
+The Test Classes
+================
+
+FileUtilsTests
+--------------
+
+- testFileIsDirectory - ensures that the expected path is a directory
+- testFileNotExist - ensures that a file that does not exist returns
+ False
+- testFileExists - ensures that a file that does exist returns True
+- testDownloadBadUrl - ensures that an Exception is thrown when
+ attempting to download a file with a bad URL
+- testCirrosImageDownload - ensures that the Cirros image can be
+ downloaded
+- testReadOSEnvFile - ensures that an OpenStack RC file can be properly
+ parsed
+
+SecurityGroupRuleSettingsUnitTests
+----------------------------------
+
+Ensures that all required members are included when constructing a
+SecurityGroupRuleSettings object
+
+SecurityGroupSettingsUnitTests
+------------------------------
+
+Ensures that all required members are included when constructing a
+SecuirtyGroupSettings object
+
+ImageSettingsUnitTests
+----------------------
+
+Ensures that all required members are included when constructing a
+ImageSettings object
+
+KeypairSettingsUnitTests
+------------------------
+
+Ensures that all required members are included when constructing a
+KeypairSettings object
+
+UserSettingsUnitTests
+---------------------
+
+Ensures that all required members are included when constructing a
+UserSettings object
+
+ProjectSettingsUnitTests
+------------------------
+
+Ensures that all required members are included when constructing a
+ProjectSettings object
+
+NetworkSettingsUnitTests
+------------------------
+
+Ensures that all required members are included when constructing a
+NetworkSettings object
+
+SubnetSettingsUnitTests
+-----------------------
+
+Ensures that all required members are included when constructing a
+SubnetSettings object
+
+PortSettingsUnitTests
+---------------------
+
+Ensures that all required members are included when constructing a
+PortSettings object
+
+FloatingIpSettingsUnitTests
+---------------------------
+
+Ensures that all required members are included when constructing a
+FloatingIpSettings object
+
+VmInstanceSettingsUnitTests
+---------------------------
+
+Ensures that all required members are included when constructing a
+VmInstanceSettings object
diff --git a/docs/how-to-use/VirtEnvDeploy.rst b/docs/how-to-use/VirtEnvDeploy.rst
new file mode 100644
index 0000000..7f55f0c
--- /dev/null
+++ b/docs/how-to-use/VirtEnvDeploy.rst
@@ -0,0 +1,275 @@
+Overview
+========
+
+The main purpose of this project is to enable one to describe a virtual environment in a YAML file and enable the
+user to deploy it to an OpenStack cloud in a repeatable manner. There are also options to un-deploy that same
+environment by leveraging the original YAML file.
+
+To deploy/clean virtual environments
+====================================
+
+- Clone Repository
+
+ - git clone https://gerrit.cablelabs.com/snaps-provisioning
+
+- Install Library
+
+ - pip install -e /
+
+- Deploy
+
+ - cd
+ - python snaps/deploy\_venv.py -e -d
+ - Working example:
+
+::
+
+ python deploy_venv.py -e <path to repo>/examples/complex-network/deploy-complex-network.yaml -d
+
+- Clean
+
+ - python deploy\_venv.py -e -c
+ - Working example (cleanup of a previously deployed virtual
+ environment where the VM has Yardstick installed):
+
+::
+
+ python deploy_venv.py -e <path to repo>/examples/complex-network/deploy-complex-network.yaml -c
+
+Environment Configuration YAML File
+===================================
+
+The configuration file used to deploy and provision a virtual environment has been designed to describe the required
+images, networks, SSH public and private keys, associated VMs, as well as any required post deployment provisioning
+tasks.
+
+\*\*\* Please note that many of the more esoteric optional supported
+attributes still have not been fully tested. ***
+*** Some of the nested bullets are being hidden by GitLabs, please see
+doc/VirtEnvDeploy.md.\*\*\*
+
+- 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".
+
+ - 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)
+
+ - 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)
+
+ - 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)
+
+ - 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)
+
+- ansible: Each set of attributes below are contained in a list
+
+ - playbook\_location: Full path or relative to the directory in
+ which the deployment file resides (required)
+ - hosts: A list of hosts to which the playbook will be executed
+ (required)
+ - variables: Should your Ansible scripts require any substitution
+ values to be applied with Jinga2templates, the values defined here
+ will be used to for substitution
+ - tag name = substitution variable names. For instance, for any file
+ being pushed to the host being provisioned containing a value such
+ as {{ foo }}, you must specify a tag name of "foo"
+
+ - vm\_name:
+ - type: string\|port\|os\_creds\|vm-attr (note: will need to make
+ changes to deploy\_venv.py#\_\_get\_variable\_value() for
+ additional support)
+ - when type == string, an tag name "value" must exist and its
+ value will be used for template substituion
+ - when type == port, custom code has been written to extract
+ certain assigned values to the port:
+
+ - vm\_name: must correspond to a VM's name as configured in
+ this file
+ - port\_name: The name of the port from which to extract the
+ substitution values (required)
+ - port\_value: The port value. Currently only supporting
+ "mac\_address" and "ip\_address" (only the first)
+
+ - when type == os\_creds, custom code has been written to extract
+ the file's connection values:
+
+ - username: connection's user
+ - password: connection's password
+ - auth\_url: connection's URL
+ - project\_name: connection's project
+
+ - when type == vm-attr, custom code has been written to extract
+ the following attributes from the vm:
+
+ - vm\_name: must correspond to a VM's name as configured in
+ this file
+ - value -> floating\_ip: is currently the only vm-attr
+ supported
diff --git a/docs/how-to-use/index.rst b/docs/how-to-use/index.rst
new file mode 100644
index 0000000..58b67a3
--- /dev/null
+++ b/docs/how-to-use/index.rst
@@ -0,0 +1,26 @@
+*************************
+Runtime Environment Setup
+*************************
+
+- Python 2.7 (recommend leveraging a Virtual Python runtime, e.g.
+ `Virtualenv <https://virtualenv.pypa.io>`__, in your development
+ environment)
+- Development packages for python and openssl. On CentOS/RHEL:
+
+ # yum install python-devel openssl-devel
+
+On Ubuntu:
+
+::
+
+ # apt-get install python2.7-dev libssl-dev
+
+- Install SNAPS Library
+
+ - pip install -e <path to repo>/snaps/
+
+`Testing <Testing.rst>`__
+-------------------------
+
+`Virtual Environment Deployment <VirtEnvDeploy.rst>`__
+------------------------------------------------------
diff --git a/examples/complex-network/deploy-complex-network.yaml b/examples/complex-network/deploy-complex-network.yaml
new file mode 100644
index 0000000..42559e8
--- /dev/null
+++ b/examples/complex-network/deploy-complex-network.yaml
@@ -0,0 +1,234 @@
+# 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:
+ connection:
+ # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host.
+ username: admin
+ password: cable123
+ auth_url: http://10.197.103.50:5000/v2.0/
+ project_name: admin
+ http_proxy: localhost:3128
+ images:
+ - image:
+ name: centos-inst-test
+ format: qcow2
+ image_user: centos
+ download_url: http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2
+ nic_config_pb_loc: provisioning/ansible/centos-network-setup/playbooks/configure_host.yml
+ - image:
+ name: Ubuntu-14.04
+ format: qcow2
+ image_user: ubuntu
+ download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img
+ nic_config_pb_loc: provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml
+ networks:
+ - network:
+ name: mgr-net
+ subnets:
+ - subnet:
+ name: mgr-subnet
+ cidr: 10.0.1.0/24
+ dns_nameservers: [8.8.8.8]
+ - subnet:
+ name: mgr-subnet-2
+ cidr: 10.0.2.0/24
+ dns_nameservers: [8.8.8.8]
+ - network:
+ name: site1-net
+ subnets:
+ - subnet:
+ name: site1-subnet
+ cidr: 192.168.0.0/24
+ gateway_ip: 192.168.0.1
+ - subnet:
+ name: site1-subnet-2
+ cidr: 192.168.1.0/24
+ gateway_ip: 192.168.1.1
+ - network:
+ name: site2-net
+ subnets:
+ - subnet:
+ name: site2-subnet
+ cidr: 192.169.0.0/24
+ gateway_ip: 192.169.0.1
+ routers:
+ # Note: Routers between internal networks not being used but put in here as an example on how to do that.
+ - router:
+ name: mgr-router
+ external_gateway: external
+ internal_subnets:
+ - mgr-subnet
+ - mgr-subnet-2
+ interfaces:
+ - port:
+ name: mgr-router-to-site1
+ network_name: site1-net
+ ip_addrs:
+ - subnet_name: site1-subnet
+ ip: 192.168.0.10
+ - router:
+ name: site1-router
+ external_gateway: external
+ internal_subnets:
+ - site1-subnet
+ - router:
+ name: site2-router
+ external_gateway: external
+ internal_subnets:
+ - site2-subnet
+ - router:
+ name: site-to-site-router
+ interfaces:
+ - port:
+ name: site1-router-port
+ network_name: site1-net
+ ip_addrs:
+ - subnet_name: site1-subnet
+ ip: 192.168.0.100
+ - port:
+ name: site2-router-port
+ network_name: site2-net
+ ip_addrs:
+ - subnet_name: site2-subnet
+ ip: 192.169.0.100
+ keypairs:
+ - keypair:
+ name: cmplx-net-kp
+ public_filepath: /tmp/cmplx-net-kp.pub
+ private_filepath: /tmp/cmplx-net-kp
+ instances:
+ - instance:
+ name: mgr-app
+ flavor: m1.small
+ imageName: centos-inst-test
+ keypair_name: cmplx-net-kp
+ vm_boot_timeout: 600
+ vm_delete_timeout: 120
+ ssh_connect_timeout: 120
+ ports:
+ - port:
+ name: mgr-app-port
+ network_name: mgr-net
+ ip_addrs:
+ - subnet_name: mgr-subnet
+ ip: 10.0.1.30
+ - subnet_name: mgr-subnet-2
+ ip: 10.0.2.30
+ floating_ips:
+# TODO - Why is only one of these floating IPs not working and why does it vary which one?
+# - floating_ip:
+# name: fip1
+# port_name: mgr-app-port
+# subnet_name: mgr-subnet
+# router_name: mgr-router
+# provisioning: False
+ - floating_ip:
+ name: fip2
+ port_name: mgr-app-port
+ subnet_name: mgr-subnet-2
+ router_name: mgr-router
+ - instance:
+ name: site1-ovs
+ flavor: m1.small
+ imageName: centos-inst-test
+ keypair_name: cmplx-net-kp
+ vm_boot_timeout: 600
+ vm_delete_timeout: 120
+ ssh_connect_timeout: 120
+ ports:
+ - port:
+ name: site1-ovs-mgr-port
+ network_name: mgr-net
+ - port:
+ name: site1-ovs-site1-port
+ network_name: site1-net
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: site1-ovs-mgr-port
+ router_name: mgr-router
+ - instance:
+ name: site2-ovs
+ flavor: m1.small
+ imageName: Ubuntu-14.04
+ keypair_name: cmplx-net-kp
+ vm_boot_timeout: 600
+ vm_delete_timeout: 120
+ ssh_connect_timeout: 120
+ ports:
+ - port:
+ name: site2-ovs-mgr-port
+ network_name: mgr-net
+ - port:
+ name: site2-ovs-site2-port
+ network_name: site2-net
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: site2-ovs-mgr-port
+ subnet_name: mgr-subnet-2
+ router_name: mgr-router
+ - instance:
+ name: site2-host
+ flavor: m1.small
+ imageName: Ubuntu-14.04
+ keypair_name: cmplx-net-kp
+ vm_boot_timeout: 600
+ vm_delete_timeout: 120
+ ssh_connect_timeout: 120
+ ports:
+ - port:
+ name: site2-host-port
+ network_name: site2-net
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: site2-host-port
+ subnet_name: site2-subnet
+ router_name: site2-router
+# TODO - Add a playbook here...
+#ansible:
+# - playbook_location: main.yml
+# hosts:
+# - mgr-app
+# - site1-ovs
+# - site2-ovs
+# - site2-host
+# variables:
+# mac1:
+# type: port
+# vm_name: site1-ovs
+# port_name: site1-ovs-site1-port
+# port_value: mac_address
+# ip1:
+# type: port
+# vm_name: site1-ovs
+# port_name: site1-ovs-mgr-port
+# port_value: ip_address
+# mac2:
+# type: port
+# vm_name: site2-ovs
+# port_name: site2-ovs-site2-port
+# port_value: mac_address
+# ip2:
+# type: port
+# vm_name: site2-ovs
+# port_name: site2-ovs-mgr-port
+# port_value: ip_address
+# - playbook_location: ./main.yml
+# hosts:
+# - site1-ovs
+# - site2-ovs
diff --git a/examples/complex-network/main.yml b/examples/complex-network/main.yml
new file mode 100644
index 0000000..7f213ea
--- /dev/null
+++ b/examples/complex-network/main.yml
@@ -0,0 +1,16 @@
+# 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.
+---
+- include: playbooks/sample-playbook.yml \ No newline at end of file
diff --git a/examples/complex-network/playbooks/sample-playbook.yml b/examples/complex-network/playbooks/sample-playbook.yml
new file mode 100644
index 0000000..726f213
--- /dev/null
+++ b/examples/complex-network/playbooks/sample-playbook.yml
@@ -0,0 +1,20 @@
+# 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.
+---
+- hosts: all
+
+ tasks:
+ - name: Say hello
+ command: echo 'hello world' > ~/hello.out
diff --git a/examples/external-network/deploy-ext-net.yaml b/examples/external-network/deploy-ext-net.yaml
new file mode 100644
index 0000000..31c41ec
--- /dev/null
+++ b/examples/external-network/deploy-ext-net.yaml
@@ -0,0 +1,77 @@
+# 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:
+ connection:
+ # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host.
+ username: admin
+ password: cable123
+ auth_url: http://10.197.103.50:5000/v2.0/
+ project_name: admin
+ http_proxy: localhost:3128
+ images:
+ - image:
+ name: Ubuntu
+ format: qcow2
+ image_user: ubuntu
+ download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img
+ networks:
+ - network:
+ name: ext-net
+ external: True
+ network_type: vlan
+ project_name: service
+ subnets:
+ - subnet:
+ name: ext-subnet
+ cidr: 10.197.101.0/24
+ gateway_ip: 10.197.101.1
+ start: 10.197.101.101
+ end: 10.197.101.200
+ - network:
+ name: internal-net
+ subnets:
+ - subnet:
+ name: internal-subnet
+ cidr: 10.0.1.0/24
+ dns_nameservers: [8.8.8.8]
+ routers:
+ - router:
+ name: ext-net-router
+ external_gateway: ext-net
+ internal_subnets:
+ - internal-subnet
+ keypairs:
+ - keypair:
+ name: ext-net-kp
+ public_filepath: /tmp/ext-net.pub
+ private_filepath: /tmp/ext-net
+ instances:
+ - instance:
+ name: ext-net-app
+ flavor: m1.small
+ imageName: Ubuntu
+ keypair_name: ext-net-kp
+ ports:
+ - port:
+ name: internal-net-port
+ network_name: internal-net
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: internal-net-port
+ router_name: ext-net-router
+ subnet_name: internal-subnet
+
diff --git a/examples/simple/deploy-simple.yaml b/examples/simple/deploy-simple.yaml
new file mode 100644
index 0000000..ae946de
--- /dev/null
+++ b/examples/simple/deploy-simple.yaml
@@ -0,0 +1,101 @@
+# 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:
+ connection:
+ # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host.
+ username: admin
+ password: cable123
+# auth_url: http://10.197.103.50:5000/v2.0/
+ auth_url: http://192.168.67.10:5000/v2.0
+ project_name: admin
+ http_proxy: 10.197.123.27:3128
+ ssh_proxy_cmd: '/usr/local/bin/corkscrew 10.197.123.27 3128 %h %p'
+ images:
+ - image:
+ name: Ubuntu14
+ format: qcow2
+ image_user: ubuntu
+ download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img
+ networks:
+ - network:
+ name: simple-net
+ subnets:
+ - subnet:
+ name: simple-subnet
+ cidr: 10.0.1.0/24
+ dns_nameservers: [10.5.0.8, 8.8.8.8]
+ routers:
+ - router:
+ name: simple-router
+ external_gateway: external
+ internal_subnets:
+ - simple-subnet
+ keypairs:
+ - keypair:
+ name: simple
+ public_filepath: /tmp/simple.pub
+ private_filepath: /tmp/simple
+ instances:
+ - instance:
+ name: simple-1
+ flavor: m1.small
+ imageName: Ubuntu14
+ keypair_name: simple
+ userdata: "#cloud-config\npassword: cable123\nchpasswd: { expire: False }\nsshr_pwauth: True"
+ ports:
+ - port:
+ name: simple-net-port
+ network_name: simple-net
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: simple-net-port
+ router_name: simple-router
+ subnet_name: simple-subnet
+ansible:
+ - playbook_location: main.yml
+ hosts:
+ - simple-1
+ variables:
+ greeting_msg:
+ type: string
+ value: Greetings
+ os_user:
+ type: os_creds
+ value: username
+ os_pass:
+ type: os_creds
+ value: password
+ os_auth_url:
+ type: os_creds
+ value: auth_url
+ os_project:
+ type: os_creds
+ value: project_name
+ fip1:
+ type: vm-attr
+ vm_name: simple-1
+ value: floating_ip
+ mac1:
+ type: port
+ vm_name: simple-1
+ port_name: simple-net-port
+ port_value: mac_address
+ ip1:
+ type: port
+ vm_name: simple-1
+ port_name: simple-net-port
+ port_value: ip_address \ No newline at end of file
diff --git a/examples/simple/files/motd b/examples/simple/files/motd
new file mode 100644
index 0000000..cee5d06
--- /dev/null
+++ b/examples/simple/files/motd
@@ -0,0 +1,8 @@
+{{ greeting_msg }}
+OS_USER - {{ os_user }}
+OS_PASSWORD - {{ os_pass }}
+AUTH_URL - {{ os_auth_url }}
+PROJECT_NAME - {{ os_project }}
+Floating IP - {{ fip1 }}
+Port MAC = {{ mac1 }}
+Port IP = {{ ip1 }}
diff --git a/examples/simple/main.yml b/examples/simple/main.yml
new file mode 100644
index 0000000..7f213ea
--- /dev/null
+++ b/examples/simple/main.yml
@@ -0,0 +1,16 @@
+# 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.
+---
+- include: playbooks/sample-playbook.yml \ No newline at end of file
diff --git a/examples/simple/playbooks/sample-playbook.yml b/examples/simple/playbooks/sample-playbook.yml
new file mode 100644
index 0000000..84c46e4
--- /dev/null
+++ b/examples/simple/playbooks/sample-playbook.yml
@@ -0,0 +1,23 @@
+# 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.
+---
+- hosts: all
+ become: yes
+ become_method: sudo
+ become_user: root
+
+ tasks:
+ - name: Create MOTD
+ action: template owner=root group=root mode=666 src=../files/motd dest=/etc/motd
diff --git a/examples/two-network/deploy-two-net-centos.yaml b/examples/two-network/deploy-two-net-centos.yaml
new file mode 100644
index 0000000..4fae4aa
--- /dev/null
+++ b/examples/two-network/deploy-two-net-centos.yaml
@@ -0,0 +1,96 @@
+# 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:
+ connection:
+ # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host.
+ username: admin
+ password: cable123
+ auth_url: http://10.197.103.50:5000/v2.0/
+ project_name: admin
+ http_proxy: localhost:3128
+ images:
+ - image:
+ name: centos
+ format: qcow2
+ image_user: centos
+ download_url: http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2
+ nic_config_pb_loc: provisioning/ansible/centos-network-setup/playbooks/configure_host.yml
+ networks:
+ - network:
+ name: net-1
+ subnets:
+ - subnet:
+ name: subnet-1
+ cidr: 10.0.1.0/24
+ dns_nameservers: [8.8.8.8]
+ - network:
+ name: net-2
+ subnets:
+ - subnet:
+ name: subnet-2
+ cidr: 10.0.2.0/24
+ dns_nameservers: [8.8.8.8]
+ routers:
+ - router:
+ name: router-1
+ external_gateway: external
+ internal_subnets:
+ - subnet-1
+ keypairs:
+ - keypair:
+ name: two-net
+ public_filepath: /tmp/two-net.pub
+ private_filepath: /tmp/two-net
+ instances:
+ - instance:
+ name: vm1
+ flavor: m1.small
+ imageName: centos
+ keypair_name: two-net
+ ports:
+ - port:
+ name: port-1-vm1
+ network_name: net-1
+ - port:
+ name: port-2-vm1
+ network_name: net-2
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: port-1-vm1
+ router_name: router-1
+ subnet_name: subnet-1
+ - instance:
+ name: vm2
+ flavor: m1.small
+ imageName: centos
+ keypair_name: two-net
+ ports:
+ - port:
+ name: port-1-vm2
+ network_name: net-1
+ - port:
+ name: port-2-vm2
+ network_name: net-2
+ ip_addrs:
+ - subnet_name: subnet-2
+ ip: 10.0.2.101
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: port-1-vm2
+ router_name: router-1
+ subnet_name: subnet-1 \ No newline at end of file
diff --git a/examples/two-network/deploy-two-net-ubuntu.yaml b/examples/two-network/deploy-two-net-ubuntu.yaml
new file mode 100644
index 0000000..ffcb05d
--- /dev/null
+++ b/examples/two-network/deploy-two-net-ubuntu.yaml
@@ -0,0 +1,96 @@
+# 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:
+ connection:
+ # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host.
+ username: admin
+ password: cable123
+ auth_url: http://10.197.103.50:5000/v2.0/
+ project_name: admin
+ http_proxy: localhost:3128
+ images:
+ - image:
+ name: Ubuntu
+ format: qcow2
+ image_user: ubuntu
+ download_url: http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img
+ nic_config_pb_loc: provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml
+ networks:
+ - network:
+ name: net-1
+ subnets:
+ - subnet:
+ name: subnet-1
+ cidr: 10.0.1.0/24
+ dns_nameservers: [8.8.8.8]
+ - network:
+ name: net-2
+ subnets:
+ - subnet:
+ name: subnet-2
+ cidr: 10.0.2.0/24
+ dns_nameservers: [8.8.8.8]
+ routers:
+ - router:
+ name: router-1
+ external_gateway: external
+ internal_subnets:
+ - subnet-1
+ keypairs:
+ - keypair:
+ name: simple
+ public_filepath: /tmp/simple.pub
+ private_filepath: /tmp/simple
+ instances:
+ - instance:
+ name: vm1
+ flavor: m1.small
+ imageName: Ubuntu
+ keypair_name: simple
+ ports:
+ - port:
+ name: port-1-vm1
+ network_name: net-1
+ - port:
+ name: port-2-vm1
+ network_name: net-2
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: port-1-vm1
+ router_name: router-1
+ subnet_name: subnet-1
+ - instance:
+ name: vm2
+ flavor: m1.small
+ imageName: Ubuntu
+ keypair_name: simple
+ ports:
+ - port:
+ name: port-1-vm2
+ network_name: net-1
+ - port:
+ name: port-2-vm2
+ network_name: net-2
+ ip_addrs:
+ - subnet_name: subnet-2
+ ip: 10.0.2.101
+ floating_ips:
+ - floating_ip:
+ name: fip1
+ port_name: port-1-vm2
+ router_name: router-1
+ subnet_name: subnet-1 \ No newline at end of file
diff --git a/snaps/__init__.py b/snaps/__init__.py
new file mode 100644
index 0000000..e3e876e
--- /dev/null
+++ b/snaps/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski'
diff --git a/snaps/deploy_venv.py b/snaps/deploy_venv.py
new file mode 100644
index 0000000..10e444f
--- /dev/null
+++ b/snaps/deploy_venv.py
@@ -0,0 +1,573 @@
+#!/usr/bin/python
+#
+# 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 script is responsible for deploying virtual environments
+import argparse
+import logging
+import os
+import re
+
+from snaps import file_utils
+from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
+from snaps.openstack.create_image import ImageSettings
+from snaps.openstack.create_instance import VmInstanceSettings
+from snaps.openstack.create_network import PortSettings, NetworkSettings
+from snaps.openstack.create_router import RouterSettings
+from snaps.openstack.create_keypairs import KeypairSettings
+from snaps.openstack.os_credentials import OSCreds, ProxySettings
+from snaps.openstack.utils import deploy_utils
+from snaps.provisioning import ansible_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('deploy_venv')
+
+ARG_NOT_SET = "argument not set"
+
+
+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
+ """
+ proxy_settings = None
+ http_proxy = os_conn_config.get('http_proxy')
+ if http_proxy:
+ tokens = re.split(':', http_proxy)
+ ssh_proxy_cmd = os_conn_config.get('ssh_proxy_cmd')
+ proxy_settings = ProxySettings(tokens[0], tokens[1], ssh_proxy_cmd)
+
+ return OSCreds(username=os_conn_config.get('username'),
+ password=os_conn_config.get('password'),
+ auth_url=os_conn_config.get('auth_url'),
+ project_name=os_conn_config.get('project_name'),
+ proxy_settings=proxy_settings)
+
+
+def __parse_ports_config(config):
+ """
+ Parses the "ports" configuration
+ :param config: The dictionary to parse
+ :param os_creds: The OpenStack credentials object
+ :return: a list of PortConfig objects
+ """
+ out = list()
+ for port_config in config:
+ out.append(PortSettings(config=port_config.get('port')))
+ return out
+
+
+def __create_flavors(os_conn_config, flavors_config, cleanup=False):
+ """
+ Returns a dictionary of flavors where the key is the image name and the value is the image object
+ :param os_conn_config: The OpenStack connection credentials
+ :param flavors_config: The list of image configurations
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: dictionary
+ """
+ flavors = {}
+
+ if flavors_config:
+ try:
+ for flavor_config_dict in flavors_config:
+ flavor_config = flavor_config_dict.get('flavor')
+ if flavor_config and flavor_config.get('name'):
+ flavor_creator = OpenStackFlavor(__get_os_credentials(os_conn_config),
+ FlavorSettings(flavor_config))
+ flavor_creator.create(cleanup=cleanup)
+ flavors[flavor_config['name']] = flavor_creator
+ except Exception as e:
+ for key, flavor_creator in flavors.iteritems():
+ flavor_creator.clean()
+ raise e
+ logger.info('Created configured flavors')
+
+ return flavors
+
+
+def __create_images(os_conn_config, images_config, cleanup=False):
+ """
+ Returns a dictionary of images where the key is the image name and the value is the image object
+ :param os_conn_config: The OpenStack connection credentials
+ :param images_config: The list of image configurations
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: dictionary
+ """
+ images = {}
+
+ if images_config:
+ try:
+ for image_config_dict in images_config:
+ image_config = image_config_dict.get('image')
+ if image_config and image_config.get('name'):
+ images[image_config['name']] = deploy_utils.create_image(__get_os_credentials(os_conn_config),
+ ImageSettings(image_config), cleanup)
+ except Exception as e:
+ for key, image_creator in images.iteritems():
+ image_creator.clean()
+ raise e
+ logger.info('Created configured images')
+
+ return images
+
+
+def __create_networks(os_conn_config, network_confs, cleanup=False):
+ """
+ Returns a dictionary of networks where the key is the network name and the value is the network object
+ :param os_conn_config: The OpenStack connection credentials
+ :param network_confs: The list of network configurations
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: dictionary
+ """
+ network_dict = {}
+
+ if network_confs:
+ try:
+ for network_conf in network_confs:
+ net_name = network_conf['network']['name']
+ os_creds = __get_os_credentials(os_conn_config)
+ network_dict[net_name] = deploy_utils.create_network(
+ os_creds, NetworkSettings(config=network_conf['network']), cleanup)
+ except Exception as e:
+ for key, net_creator in network_dict.iteritems():
+ net_creator.clean()
+ raise e
+
+ logger.info('Created configured networks')
+
+ return network_dict
+
+
+def __create_routers(os_conn_config, router_confs, cleanup=False):
+ """
+ Returns a dictionary of networks where the key is the network name and the value is the network object
+ :param os_conn_config: The OpenStack connection credentials
+ :param router_confs: The list of router configurations
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: dictionary
+ """
+ router_dict = {}
+ os_creds = __get_os_credentials(os_conn_config)
+
+ if router_confs:
+ try:
+ for router_conf in router_confs:
+ router_name = router_conf['router']['name']
+ router_dict[router_name] = deploy_utils.create_router(
+ os_creds, RouterSettings(config=router_conf['router']), cleanup)
+ except Exception as e:
+ for key, router_creator in router_dict.iteritems():
+ router_creator.clean()
+ raise e
+
+ logger.info('Created configured networks')
+
+ return router_dict
+
+
+def __create_keypairs(os_conn_config, keypair_confs, cleanup=False):
+ """
+ Returns a dictionary of keypairs where the key is the keypair name and the value is the keypair object
+ :param os_conn_config: The OpenStack connection credentials
+ :param keypair_confs: The list of keypair configurations
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: dictionary
+ """
+ keypairs_dict = {}
+ if keypair_confs:
+ try:
+ for keypair_dict in keypair_confs:
+ keypair_config = keypair_dict['keypair']
+ kp_settings = KeypairSettings(keypair_config)
+ keypairs_dict[keypair_config['name']] = deploy_utils.create_keypair(
+ __get_os_credentials(os_conn_config), kp_settings, cleanup)
+ except Exception as e:
+ for key, keypair_creator in keypairs_dict.iteritems():
+ keypair_creator.clean()
+ raise e
+
+ logger.info('Created configured keypairs')
+
+ return keypairs_dict
+
+
+def __create_instances(os_conn_config, instances_config, image_dict, keypairs_dict, cleanup=False):
+ """
+ Returns a dictionary of instances where the key is the instance name and the value is the VM object
+ :param os_conn_config: The OpenStack connection credentials
+ :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 or not
+ :return: dictionary
+ """
+ os_creds = __get_os_credentials(os_conn_config)
+
+ 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(config=instance_config['instance'])
+ kp_name = conf.get('keypair_name')
+ vm_dict[conf['name']] = deploy_utils.create_vm_instance(
+ os_creds, instance_settings, image_creator.image_settings,
+ keypair_creator=keypairs_dict[kp_name], cleanup=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')
+ except Exception as e:
+ logger.error('Unexpected error creating instances. Attempting to cleanup environment - ' + e.message)
+ for key, inst_creator in vm_dict.iteritems():
+ inst_creator.clean()
+ raise e
+
+ logger.info('Created configured instances')
+
+ return vm_dict
+
+
+def __apply_ansible_playbooks(ansible_configs, vm_dict, env_file):
+ """
+ Applies ansible playbooks to running VMs with floating IPs
+ :param ansible_configs: a list of Ansible configurations
+ :param vm_dict: the dictionary of newly instantiated VMs where the VM 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 vm_dict.values():
+ if not vm_inst.vm_ssh_active(block=True):
+ logger.warn("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:
+ __apply_ansible_playbook(ansible_config, vm_dict)
+
+ # Return to original directory
+ os.chdir(orig_cwd)
+
+ return True
+
+
+def __apply_ansible_playbook(ansible_config, vm_dict):
+ """
+ Applies an Ansible configuration setting
+ :param ansible_config: the configuration settings
+ :param vm_dict: the dictionary of newly instantiated VMs where the VM name is the key
+ :return:
+ """
+ if ansible_config:
+ remote_user, floating_ips, private_key_filepath, proxy_settings = __get_connection_info(ansible_config, vm_dict)
+ if floating_ips:
+ ansible_utils.apply_playbook(ansible_config['playbook_location'], floating_ips, remote_user,
+ private_key_filepath,
+ variables=__get_variables(ansible_config.get('variables'), vm_dict),
+ proxy_setting=proxy_settings)
+
+
+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
+ private_key_filepath = 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)
+
+ private_key_filepath = 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, private_key_filepath, proxy_settings
+ return None
+
+
+def __get_variables(var_config, vm_dict):
+ """
+ Returns a dictionary of substitution variables to be used for Ansible templates
+ :param var_config: the variable configuration settings
+ :param vm_dict: the dictionary of VMs where the VM 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.iteritems():
+ value = __get_variable_value(value, vm_dict)
+ if key and value:
+ variables[key] = value
+ logger.info("Set Jinga2 variable with key [" + key + "] the value [" + value + ']')
+ else:
+ logger.warn('Key [' + str(key) + '] or Value [' + str(value) + '] must not be None')
+ return variables
+ return None
+
+
+def __get_variable_value(var_config_values, vm_dict):
+ """
+ Returns the associated variable value for use by Ansible for substitution purposes
+ :param var_config_values: the configuration dictionary
+ :param vm_dict: the dictionary containing all VMs where the key is the VM's name
+ :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, vm_dict)
+ if var_config_values['type'] == 'port':
+ return __get_vm_port_variable_value(var_config_values, vm_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
+
+
+def __get_os_creds_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
+ """
+ logger.info("Retrieving OS Credentials")
+ vm = vm_dict.values()[0]
+
+ if vm:
+ if var_config_values['value'] == 'username':
+ logger.info("Returning OS username")
+ return vm.get_os_creds().username
+ elif var_config_values['value'] == 'password':
+ logger.info("Returning OS password")
+ return vm.get_os_creds().password
+ elif var_config_values['value'] == 'auth_url':
+ logger.info("Returning OS auth_url")
+ return vm.get_os_creds().auth_url
+ elif var_config_values['value'] == 'project_name':
+ logger.info("Returning OS project_name")
+ return vm.get_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 main(arguments):
+ """
+ Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or ...
+ Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg containing the following content:
+
+ [defaults]
+ host_key_checking = False
+
+ CWD must be this directory where this script is located.
+
+ :return: To the OS
+ """
+ log_level = logging.INFO
+ if arguments.log_level != 'INFO':
+ log_level = logging.DEBUG
+ logging.basicConfig(level=log_level)
+
+ logger.info('Starting to Deploy')
+ config = file_utils.read_yaml(arguments.environment)
+ logger.info('Read configuration file - ' + arguments.environment)
+
+ if config:
+ os_config = config.get('openstack')
+
+ image_dict = {}
+ network_dict = {}
+ router_dict = {}
+ keypairs_dict = {}
+ vm_dict = {}
+
+ if os_config:
+ try:
+ os_conn_config = os_config.get('connection')
+
+ # Create flavors
+ flavor_dict = __create_flavors(os_conn_config, os_config.get('flavors'),
+ arguments.clean is not ARG_NOT_SET)
+
+ # Create images
+ image_dict = __create_images(os_conn_config, os_config.get('images'),
+ arguments.clean is not ARG_NOT_SET)
+
+ # Create network
+ network_dict = __create_networks(os_conn_config, os_config.get('networks'),
+ arguments.clean is not ARG_NOT_SET)
+
+ # Create network
+ router_dict = __create_routers(os_conn_config, os_config.get('routers'),
+ arguments.clean is not ARG_NOT_SET)
+
+ # Create keypairs
+ keypairs_dict = __create_keypairs(os_conn_config, os_config.get('keypairs'),
+ arguments.clean is not ARG_NOT_SET)
+
+ # Create instance
+ vm_dict = __create_instances(os_conn_config, os_config.get('instances'), image_dict, keypairs_dict,
+ arguments.clean is not ARG_NOT_SET)
+ logger.info('Completed creating/retrieving all configured instances')
+ except Exception as e:
+ logger.error('Unexpected error deploying environment. Rolling back due to - ' + e.message)
+ __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict, True)
+ raise e
+
+
+ # Must enter either block
+ if arguments.clean is not ARG_NOT_SET:
+ # Cleanup Environment
+ __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict,
+ 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.itervalues():
+ 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, vm_dict, arguments.environment):
+ logger.error("Problem applying ansible playbooks")
+ else:
+ logger.error('Unable to read configuration file - ' + arguments.environment)
+ exit(1)
+
+ exit(0)
+
+
+def __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict, clean_image=False):
+ for key, vm_inst in vm_dict.iteritems():
+ vm_inst.clean()
+ for key, kp_inst in keypairs_dict.iteritems():
+ kp_inst.clean()
+ for key, router_inst in router_dict.iteritems():
+ router_inst.clean()
+ for key, net_inst in network_dict.iteritems():
+ net_inst.clean()
+ if clean_image:
+ for key, image_inst in image_dict.iteritems():
+ image_inst.clean()
+ for key, flavor_inst in flavor_dict.iteritems():
+ flavor_inst.clean()
+
+
+if __name__ == '__main__':
+ # To ensure any files referenced via a relative path will begin from the diectory in which this file resides
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
+ help='When used, environment will be deployed and provisioned')
+ parser.add_argument('-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
+ help='When used, the environment will be removed')
+ parser.add_argument('-i', '--clean-image', dest='clean_image', nargs='?', default=ARG_NOT_SET,
+ help='When cleaning, if this is set, the image will be cleaned too')
+ parser.add_argument('-e', '--env', dest='environment', required=True,
+ help='The environment configuration YAML file - REQUIRED')
+ parser.add_argument('-l', '--log-level', dest='log_level', default='INFO', help='Logging Level (INFO|DEBUG)')
+ args = parser.parse_args()
+
+ if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
+ print 'Must enter either -d for deploy or -c for cleaning up and environment'
+ exit(1)
+ if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
+ print 'Cannot enter both options -d/--deploy and -c/--clean'
+ exit(1)
+ main(args)
diff --git a/snaps/file_utils.py b/snaps/file_utils.py
new file mode 100644
index 0000000..f66ac17
--- /dev/null
+++ b/snaps/file_utils.py
@@ -0,0 +1,108 @@
+# 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 os
+import urllib2
+import logging
+
+import yaml
+
+__author__ = 'spisarski'
+
+"""
+Utilities for basic file handling
+"""
+
+logger = logging.getLogger('file_utils')
+
+
+def file_exists(file_path):
+ """
+ Returns True if the image file already exists and throws an exception if the path is a directory
+ :return:
+ """
+ if os.path.exists(file_path):
+ if os.path.isdir(file_path):
+ return False
+ return os.path.isfile(file_path)
+ return False
+
+
+def get_file(file_path):
+ """
+ Returns True if the image file has already been downloaded
+ :return: the image file object
+ :raise Exception when file cannot be found
+ """
+ if file_exists(file_path):
+ return open(file_path, 'r')
+ else:
+ raise Exception('File with path cannot be found - ' + file_path)
+
+
+def download(url, dest_path):
+ """
+ Download a file to a destination path given a URL
+ :rtype : File object
+ """
+ name = url.rsplit('/')[-1]
+ dest = dest_path + '/' + name
+ try:
+ # Override proxy settings to use localhost to download file
+ proxy_handler = urllib2.ProxyHandler({})
+ opener = urllib2.build_opener(proxy_handler)
+ urllib2.install_opener(opener)
+ response = urllib2.urlopen(url)
+ except (urllib2.HTTPError, urllib2.URLError):
+ raise Exception
+
+ with open(dest, 'wb') as f:
+ f.write(response.read())
+ return f
+
+
+def read_yaml(config_file_path):
+ """
+ Reads the yaml file and returns a dictionary object representation
+ :param config_file_path: The file path to config
+ :return: a dictionary
+ """
+ logger.debug('Attempting to load configuration file - ' + config_file_path)
+ with open(config_file_path) as config_file:
+ config = yaml.safe_load(config_file)
+ logger.info('Loaded configuration')
+ config_file.close()
+ logger.info('Closing configuration file')
+ return config
+
+
+def read_os_env_file(os_env_filename):
+ """
+ Reads the OS environment source file and returns a map of each key/value
+ Will ignore lines beginning with a '#' and will replace any single or double quotes contained within the value
+ :param os_env_filename: The name of the OS environment file to read
+ :return: a dictionary
+ """
+ if os_env_filename:
+ logger.info('Attempting to read OS environment file - ' + os_env_filename)
+ out = {}
+ for line in open(os_env_filename):
+ line = line.lstrip()
+ if not line.startswith('#') and line.startswith('export '):
+ line = line.lstrip('export ').strip()
+ tokens = line.split('=')
+ if len(tokens) > 1:
+ # Remove leading and trailing ' & " characters from value
+ out[tokens[0]] = tokens[1].lstrip('\'').lstrip('\"').rstrip('\'').rstrip('\"')
+ return out
diff --git a/snaps/openstack/__init__.py b/snaps/openstack/__init__.py
new file mode 100644
index 0000000..e3e876e
--- /dev/null
+++ b/snaps/openstack/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski'
diff --git a/snaps/openstack/create_flavor.py b/snaps/openstack/create_flavor.py
new file mode 100644
index 0000000..68a7080
--- /dev/null
+++ b/snaps/openstack/create_flavor.py
@@ -0,0 +1,179 @@
+# 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 logging
+
+from novaclient.exceptions import NotFound
+
+from snaps.openstack.utils import nova_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_image')
+
+DEFAULT_METADATA = {'hw:mem_page_size': 'any'}
+
+
+class OpenStackFlavor:
+ """
+ Class responsible for creating a user in OpenStack
+ """
+
+ def __init__(self, os_creds, flavor_settings):
+ """
+ Constructor
+ :param os_creds: The OpenStack connection credentials
+ :param flavor_settings: The flavor settings
+ :return:
+ """
+ self.__os_creds = os_creds
+ self.flavor_settings = flavor_settings
+ self.__flavor = None
+ self.__nova = nova_utils.nova_client(self.__os_creds)
+
+ def create(self, cleanup=False):
+ """
+ Creates the image in OpenStack if it does not already exist
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: The OpenStack flavor object
+ """
+ self.__flavor = nova_utils.get_flavor_by_name(self.__nova, self.flavor_settings.name)
+ if self.__flavor:
+ logger.info('Found flavor with name - ' + self.flavor_settings.name)
+ elif not cleanup:
+ self.__flavor = nova_utils.create_flavor(self.__nova, self.flavor_settings)
+ if self.flavor_settings.metadata:
+ self.__flavor.set_keys(self.flavor_settings.metadata)
+ self.__flavor = nova_utils.get_flavor_by_name(self.__nova, self.flavor_settings.name)
+ else:
+ logger.info('Did not create flavor due to cleanup mode')
+
+ return self.__flavor
+
+ def clean(self):
+ """
+ Cleanse environment of all artifacts
+ :return: void
+ """
+ if self.__flavor:
+ try:
+ nova_utils.delete_flavor(self.__nova, self.__flavor)
+ except NotFound:
+ pass
+
+ self.__flavor = None
+
+ def get_flavor(self):
+ """
+ Returns the OpenStack flavor object
+ :return:
+ """
+ return self.__flavor
+
+
+class FlavorSettings:
+ """
+ Configuration settings for OpenStack flavor creation
+ """
+
+ def __init__(self, config=None, name=None, flavor_id='auto', ram=None, disk=None, vcpus=None, ephemeral=0, swap=0,
+ rxtx_factor=1.0, is_public=True, metadata=DEFAULT_METADATA):
+ """
+ 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 (default hw:mem_page_size=any)
+ """
+
+ if config:
+ self.name = config.get('name')
+
+ if config.get('flavor_id'):
+ self.flavor_id = config['flavor_id']
+ else:
+ self.flavor_id = flavor_id
+
+ self.ram = config.get('ram')
+ self.disk = config.get('disk')
+ self.vcpus = config.get('vcpus')
+
+ if config.get('ephemeral'):
+ self.ephemeral = config['ephemeral']
+ else:
+ self.ephemeral = ephemeral
+
+ if config.get('swap'):
+ self.swap = config['swap']
+ else:
+ self.swap = swap
+
+ if config.get('rxtx_factor'):
+ self.rxtx_factor = config['rxtx_factor']
+ else:
+ self.rxtx_factor = rxtx_factor
+
+ if config.get('is_public') is not None:
+ self.is_public = config['is_public']
+ else:
+ self.is_public = is_public
+
+ if config.get('metadata'):
+ self.metadata = config['metadata']
+ else:
+ self.metadata = metadata
+ else:
+ self.name = name
+ self.flavor_id = flavor_id
+ self.ram = ram
+ self.disk = disk
+ self.vcpus = vcpus
+ self.ephemeral = ephemeral
+ self.swap = swap
+ self.rxtx_factor = rxtx_factor
+ self.is_public = is_public
+ self.metadata = metadata
+
+ if not self.name or not self.ram or not self.disk or not self.vcpus:
+ raise Exception('The attributes name, ram, disk, and vcpus are required for FlavorSettings')
+
+ if not isinstance(self.ram, int):
+ raise Exception('The ram attribute must be a integer')
+
+ if not isinstance(self.disk, int):
+ raise Exception('The ram attribute must be a integer')
+
+ if not isinstance(self.vcpus, int):
+ raise Exception('The vcpus attribute must be a integer')
+
+ if self.ephemeral and not isinstance(self.ephemeral, int):
+ raise Exception('The ephemeral attribute must be an integer')
+
+ if self.swap and not isinstance(self.swap, int):
+ raise Exception('The swap attribute must be an integer')
+
+ if self.rxtx_factor and not isinstance(self.rxtx_factor, (int, float)):
+ raise Exception('The is_public attribute must be an integer or float')
+
+ if self.is_public and not isinstance(self.is_public, bool):
+ raise Exception('The is_public attribute must be a boolean')
diff --git a/snaps/openstack/create_image.py b/snaps/openstack/create_image.py
new file mode 100644
index 0000000..e1b8d94
--- /dev/null
+++ b/snaps/openstack/create_image.py
@@ -0,0 +1,188 @@
+# 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 logging
+import time
+
+from glanceclient.exc import HTTPNotFound
+
+from snaps.openstack.utils import glance_utils, nova_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_image')
+
+IMAGE_ACTIVE_TIMEOUT = 600
+POLL_INTERVAL = 3
+STATUS_ACTIVE = 'active'
+
+
+class OpenStackImage:
+ """
+ Class responsible for creating an image in OpenStack
+ """
+
+ def __init__(self, os_creds, image_settings):
+ """
+ Constructor
+ :param os_creds: The OpenStack connection credentials
+ :param image_settings: The image settings
+ :return:
+ """
+ self.__os_creds = os_creds
+ self.image_settings = image_settings
+ self.__image = None
+ self.__glance = glance_utils.glance_client(os_creds)
+
+ def create(self, cleanup=False):
+ """
+ Creates the image in OpenStack if it does not already exist
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: The OpenStack Image object
+ """
+ from snaps.openstack.utils import nova_utils
+ nova = nova_utils.nova_client(self.__os_creds)
+
+ self.__image = glance_utils.get_image(nova, self.__glance, self.image_settings.name)
+ if self.__image:
+ logger.info('Found image with name - ' + self.image_settings.name)
+ return self.__image
+ elif not cleanup:
+ self.__image = glance_utils.create_image(self.__glance, self.image_settings)
+ logger.info('Creating image')
+ if self.image_active(block=True):
+ logger.info('Image is now active with name - ' + self.image_settings.name)
+ return self.__image
+ else:
+ raise Exception('Image did not activate in the alloted amount of time')
+ else:
+ logger.info('Did not create image due to cleanup mode')
+
+ return self.__image
+
+ def clean(self):
+ """
+ Cleanse environment of all artifacts
+ :return: void
+ """
+ if self.__image:
+ try:
+ glance_utils.delete_image(self.__glance, self.__image)
+ except HTTPNotFound:
+ pass
+ self.__image = None
+
+ def get_image(self):
+ """
+ Returns the OpenStack image object as it was populated when create() was called
+ :return: the object
+ """
+ return self.__image
+
+ def image_active(self, block=False, timeout=IMAGE_ACTIVE_TIMEOUT, poll_interval=POLL_INTERVAL):
+ """
+ Returns true when the image 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._image_status_check(STATUS_ACTIVE, block, timeout, poll_interval)
+
+ def _image_status_check(self, expected_status_code, block, timeout, poll_interval):
+ """
+ Returns true when the image 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 image status change
+ if block:
+ start = time.time()
+ else:
+ start = time.time() - timeout
+
+ while timeout > time.time() - start:
+ status = self._status(expected_status_code)
+ if status:
+ logger.info('Image is active with name - ' + self.image_settings.name)
+ return True
+
+ logger.debug('Retry querying image status in ' + str(poll_interval) + ' seconds')
+ time.sleep(poll_interval)
+ logger.debug('Image status query timeout in ' + str(timeout - (time.time() - start)))
+
+ logger.error('Timeout checking for image 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
+ """
+ # TODO - Place this API call into glance_utils.
+ nova = nova_utils.nova_client(self.__os_creds)
+ instance = glance_utils.get_image(nova, self.__glance, self.image_settings.name)
+ # instance = self.__glance.images.get(self.__image)
+ if not instance:
+ logger.warn('Cannot find instance with id - ' + self.__image.id)
+ return False
+
+ if instance.status == 'ERROR':
+ raise Exception('Instance had an error during deployment')
+ logger.debug('Instance status is - ' + instance.status)
+ return instance.status == expected_status_code
+
+
+class ImageSettings:
+ def __init__(self, config=None, name=None, image_user=None, img_format=None, url=None, image_file=None,
+ nic_config_pb_loc=None):
+ """
+
+ :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 image's name (required)
+ :param image_user: the image's default sudo user (required)
+ :param img_format: the image type (required)
+ :param url: the image download location (requires url or img_file)
+ :param image_file: the image file location (requires url or img_file)
+ :param nic_config_pb_loc: the file location to the Ansible Playbook that can configure multiple NICs
+ """
+
+ if config:
+ self.name = config.get('name')
+ self.image_user = config.get('image_user')
+ self.format = config.get('format')
+ self.url = config.get('download_url')
+ self.image_file = config.get('image_file')
+ self.nic_config_pb_loc = config.get('nic_config_pb_loc')
+ else:
+ self.name = name
+ self.image_user = image_user
+ self.format = img_format
+ self.url = url
+ self.image_file = image_file
+ self.nic_config_pb_loc = nic_config_pb_loc
+
+ if not self.name or not self.image_user or not self.format:
+ raise Exception("The attributes name, image_user, format, and url are required for ImageSettings")
+
+ if not self.url and not self.image_file:
+ raise Exception('URL or image file must be set')
+
+ if self.url and self.image_file:
+ raise Exception('Please set either URL or image file, not both')
diff --git a/snaps/openstack/create_instance.py b/snaps/openstack/create_instance.py
new file mode 100644
index 0000000..caddc05
--- /dev/null
+++ b/snaps/openstack/create_instance.py
@@ -0,0 +1,739 @@
+# 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 logging
+import time
+
+from neutronclient.common.exceptions import PortNotFoundClient
+from novaclient.exceptions import NotFound
+
+from snaps.openstack.utils import glance_utils
+from snaps.openstack.utils import neutron_utils
+from snaps.openstack.create_network import PortSettings
+from snaps.provisioning import ansible_utils
+from snaps.openstack.utils import nova_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_instance')
+
+POLL_INTERVAL = 3
+STATUS_ACTIVE = 'ACTIVE'
+STATUS_DELETED = 'DELETED'
+
+
+class OpenStackVmInstance:
+ """
+ Class responsible for creating a VM instance in OpenStack
+ """
+
+ def __init__(self, os_creds, instance_settings, image_settings, keypair_settings=None):
+ """
+ Constructor
+ :param os_creds: The connection credentials to the OpenStack API
+ :param instance_settings: Contains the settings for this VM
+ :param image_settings: The OpenStack image object settings
+ :param keypair_settings: The keypair metadata (Optional)
+ :raises Exception
+ """
+ self.__os_creds = os_creds
+
+ self.__nova = nova_utils.nova_client(self.__os_creds)
+ self.__neutron = neutron_utils.neutron_client(self.__os_creds)
+
+ self.instance_settings = instance_settings
+ self.image_settings = image_settings
+ self.keypair_settings = keypair_settings
+
+ # TODO - get rid of FIP list and only use the dict(). Need to fix populating this object when already exists
+ self.__floating_ips = list()
+ self.__floating_ip_dict = dict()
+
+ # Instantiated in self.create()
+ self.__ports = list()
+
+ # Note: this object does not change after the VM becomes active
+ self.__vm = None
+
+ def create(self, cleanup=False, block=False):
+ """
+ Creates a VM instance
+ :param cleanup: When true, only perform lookups for OpenStack objects.
+ :param block: Thread will block until instance has either become active, error, or timeout waiting.
+ Additionally, when True, floating IPs will not be applied until VM is active.
+ :return: The VM reference object
+ """
+ try:
+ self.__ports = self.__setup_ports(self.instance_settings.port_settings, cleanup)
+ self.__lookup_existing_vm_by_name()
+ if not self.__vm and not cleanup:
+ self.__create_vm(block)
+ return self.__vm
+ except Exception as e:
+ logger.exception('Error occurred while setting up instance')
+ self.clean()
+ raise e
+
+ def __lookup_existing_vm_by_name(self):
+ """
+ Populates the member variables 'self.vm' and 'self.floating_ips' if a VM with the same name already exists
+ within the project
+ """
+ servers = nova_utils.get_servers_by_name(self.__nova, self.instance_settings.name)
+ for server in servers:
+ if server.name == self.instance_settings.name:
+ self.__vm = server
+ logger.info('Found existing machine with name - ' + self.instance_settings.name)
+ fips = self.__nova.floating_ips.list()
+ for fip in fips:
+ if fip.instance_id == server.id:
+ self.__floating_ips.append(fip)
+ # TODO - Determine a means to associate to the FIP configuration and add to FIP map
+
+ def __create_vm(self, block=False):
+ """
+ Responsible for creating the VM instance
+ :param block: Thread will block until instance has either become active, error, or timeout waiting.
+ Floating IPs will be assigned after active when block=True
+ """
+ nics = []
+ for key, port in self.__ports:
+ kv = dict()
+ kv['port-id'] = port['port']['id']
+ nics.append(kv)
+
+ logger.info('Creating VM with name - ' + self.instance_settings.name)
+ keypair_name = None
+ if self.keypair_settings:
+ keypair_name = self.keypair_settings.name
+
+ flavor = nova_utils.get_flavor_by_name(self.__nova, self.instance_settings.flavor)
+ if not flavor:
+ raise Exception('Flavor not found with name - ' + self.instance_settings.flavor)
+
+ image = glance_utils.get_image(self.__nova, glance_utils.glance_client(self.__os_creds),
+ self.image_settings.name)
+ if image:
+ self.__vm = self.__nova.servers.create(
+ name=self.instance_settings.name,
+ flavor=flavor,
+ image=image,
+ nics=nics,
+ key_name=keypair_name,
+ security_groups=list(self.instance_settings.security_group_names),
+ userdata=self.instance_settings.userdata,
+ availability_zone=self.instance_settings.availability_zone)
+ else:
+ raise Exception('Cannot create instance, image cannot be located with name ' + self.image_settings.name)
+
+ logger.info('Created instance with name - ' + self.instance_settings.name)
+
+ if block:
+ self.vm_active(block=True)
+
+ self.__apply_floating_ips()
+
+ def __apply_floating_ips(self):
+ """
+ Applies the configured floating IPs to the necessary ports
+ """
+ port_dict = dict()
+ for key, port in self.__ports:
+ port_dict[key] = port
+
+ # Apply floating IPs
+ for floating_ip_setting in self.instance_settings.floating_ip_settings:
+ port = port_dict.get(floating_ip_setting.port_name)
+
+ if not port:
+ raise Exception('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_by_name(self.__neutron, floating_ip_setting.subnet_name)
+ floating_ip = nova_utils.create_floating_ip(self.__nova, ext_gateway)
+ self.__floating_ips.append(floating_ip)
+ self.__floating_ip_dict[floating_ip_setting.name] = floating_ip
+
+ logger.info('Created floating IP ' + floating_ip.ip + ' via router - ' +
+ floating_ip_setting.router_name)
+ self.__add_floating_ip(floating_ip, port, subnet)
+ else:
+ raise Exception('Unable to add floating IP to port,' +
+ ' cannot locate router with an external gateway ')
+
+ def __ext_gateway_by_router(self, router_name):
+ """
+ Returns network name for the external network attached to a router or None if not found
+ :param router_name: The name of the router to lookup
+ :return: the external network name or None
+ """
+ router = neutron_utils.get_router_by_name(self.__neutron, router_name)
+ if router and router['router'].get('external_gateway_info'):
+ network = neutron_utils.get_network_by_id(self.__neutron,
+ router['router']['external_gateway_info']['network_id'])
+ if network:
+ return network['network']['name']
+ return None
+
+ def clean(self):
+ """
+ Destroys the VM instance
+ """
+
+ # Cleanup floating IPs
+ for floating_ip in self.__floating_ips:
+ try:
+ logger.info('Deleting Floating IP - ' + floating_ip.ip)
+ nova_utils.delete_floating_ip(self.__nova, floating_ip)
+ except Exception as e:
+ logger.error('Error deleting Floating IP - ' + e.message)
+ self.__floating_ips = list()
+ self.__floating_ip_dict = dict()
+
+ # Cleanup ports
+ for name, port in self.__ports:
+ logger.info('Deleting Port - ' + name)
+ try:
+ neutron_utils.delete_port(self.__neutron, port)
+ except PortNotFoundClient as e:
+ logger.warn('Unexpected error deleting port - ' + e.message)
+ pass
+ self.__ports = list()
+
+ # Cleanup VM
+ if self.__vm:
+ 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 - ' + str(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 - ' + self.instance_settings.name)
+ self.__vm = None
+ else:
+ logger.error('VM not deleted within the timeout period of ' +
+ str(self.instance_settings.vm_delete_timeout) + ' seconds')
+ except Exception as e:
+ logger.error('Unexpected error while checking VM instance status - ' + e.message)
+
+ def __setup_ports(self, port_settings, cleanup):
+ """
+ Returns the previously configured ports or creates them if they do not exist
+ :param port_settings: A list of PortSetting objects
+ :param cleanup: When true, only perform lookups for OpenStack objects.
+ :return: a list of OpenStack port tuples where the first member is the port name and the second is the port
+ object
+ """
+ ports = list()
+
+ for port_setting in port_settings:
+ # First check to see if network already has this port
+ # TODO/FIXME - this could potentially cause problems if another port with the same name exists
+ # VM has the same network/port name pair
+ found = False
+
+ # TODO/FIXME - should we not be iterating on ports for the specific network in question as unique port names
+ # seem to only be important by network
+ existing_ports = self.__neutron.list_ports()['ports']
+ for existing_port in existing_ports:
+ if existing_port['name'] == port_setting.name:
+ ports.append((port_setting.name, {'port': existing_port}))
+ found = True
+ break
+
+ if not found and not cleanup:
+ ports.append((port_setting.name, neutron_utils.create_port(self.__neutron, self.__os_creds,
+ port_setting)))
+
+ return ports
+
+ def __add_floating_ip(self, floating_ip, port, subnet, timeout=30, poll_interval=POLL_INTERVAL):
+ """
+ Returns True when active else False
+ TODO - Make timeout and poll_interval configurable...
+ """
+ ip = None
+
+ if subnet:
+ # Take IP of subnet if there is one configured on which to place the floating IP
+ for fixed_ip in port['port']['fixed_ips']:
+ if fixed_ip['subnet_id'] == subnet['subnet']['id']:
+ ip = fixed_ip['ip_address']
+ break
+ else:
+ # Simply take the first
+ ip = port['port']['fixed_ips'][0]['ip_address']
+
+ if ip:
+ count = timeout / poll_interval
+ while count > 0:
+ logger.debug('Attempting to add floating IP to instance')
+ try:
+ self.__vm.add_floating_ip(floating_ip, ip)
+ logger.info('Added floating IP ' + floating_ip.ip + ' to port IP - ' + ip +
+ ' on instance - ' + self.instance_settings.name)
+ return
+ except Exception as e:
+ logger.debug('Retry adding floating IP to instance. Last attempt failed with - ' + e.message)
+ time.sleep(poll_interval)
+ count -= 1
+ pass
+ else:
+ raise Exception('Unable find IP address on which to place the floating IP')
+
+ logger.error('Timeout attempting to add the floating IP to instance.')
+ raise Exception('Timeout while attempting add floating IP to instance')
+
+ def get_os_creds(self):
+ """
+ Returns the OpenStack credentials used to create these objects
+ :return: the credentials
+ """
+ return self.__os_creds
+
+ def get_vm_inst(self):
+ """
+ Returns the latest version of this server object from OpenStack
+ :return: Server object
+ """
+ return nova_utils.get_latest_server_object(self.__nova, self.__vm)
+
+ def get_port_ip(self, port_name, subnet_name=None):
+ """
+ Returns the first IP for the port corresponding with the port_name parameter when subnet_name is None
+ else returns the IP address that corresponds to the subnet_name parameter
+ :param port_name: the name of the port from which to return the IP
+ :param subnet_name: the name of the subnet attached to this IP
+ :return: the IP or None if not found
+ """
+ port = self.get_port_by_name(port_name)
+ if port:
+ port_dict = port['port']
+ if subnet_name:
+ subnet = neutron_utils.get_subnet_by_name(self.__neutron, subnet_name)
+ if not subnet:
+ logger.warn('Cannot retrieve port IP as subnet could not be located with name - ' + subnet_name)
+ return None
+ for fixed_ip in port_dict['fixed_ips']:
+ if fixed_ip['subnet_id'] == subnet['subnet']['id']:
+ return fixed_ip['ip_address']
+ else:
+ fixed_ips = port_dict['fixed_ips']
+ if fixed_ips and len(fixed_ips) > 0:
+ return fixed_ips[0]['ip_address']
+ return None
+
+ def get_port_mac(self, port_name):
+ """
+ Returns the first IP for the port corresponding with the port_name parameter
+ TODO - Add in the subnet as an additional parameter as a port may have multiple fixed_ips
+ :param port_name: the name of the port from which to return the IP
+ :return: the IP or None if not found
+ """
+ port = self.get_port_by_name(port_name)
+ if port:
+ port_dict = port['port']
+ return port_dict['mac_address']
+ return None
+
+ def get_port_by_name(self, port_name):
+ """
+ Retrieves the OpenStack port object by its given name
+ :param port_name: the name of the port
+ :return: the OpenStack port object or None if not exists
+ """
+ for key, port in self.__ports:
+ if key == port_name:
+ return port
+ logger.warn('Cannot find port with name - ' + port_name)
+ return None
+
+ def config_nics(self):
+ """
+ Responsible for configuring NICs on RPM systems where the instance has more than one configured port
+ :return: None
+ """
+ if len(self.__ports) > 1 and len(self.__floating_ips) > 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)
+ self.__config_nic(nic_name, port, self.__get_first_provisioning_floating_ip().ip)
+ logger.info('Configured NIC - ' + nic_name + ' on VM - ' + self.instance_settings.name)
+
+ def __get_first_provisioning_floating_ip(self):
+ """
+ Returns the first floating IP tagged with the Floating IP name if exists else the first one found
+ :return:
+ """
+ for floating_ip_setting in self.instance_settings.floating_ip_settings:
+ if floating_ip_setting.provisioning:
+ fip = self.__floating_ip_dict.get(floating_ip_setting.name)
+ if fip:
+ return fip
+ elif len(self.__floating_ips) > 0:
+ return self.__floating_ips[0]
+
+ def __config_nic(self, nic_name, port, floating_ip):
+ """
+ Although ports/NICs can contain multiple IPs, this code currently only supports the first.
+
+ Your CWD at this point must be the <repo dir>/python directory.
+ TODO - fix this restriction.
+
+ :param nic_name: Name of the interface
+ :param port: The port information containing the expected IP values.
+ :param floating_ip: The floating IP on which to apply the playbook.
+ """
+ ip = port['port']['fixed_ips'][0]['ip_address']
+ variables = {
+ 'floating_ip': floating_ip,
+ 'nic_name': nic_name,
+ 'nic_ip': ip
+ }
+
+ if self.image_settings.nic_config_pb_loc and self.keypair_settings:
+ ansible_utils.apply_playbook(self.image_settings.nic_config_pb_loc,
+ [floating_ip], self.get_image_user(), self.keypair_settings.private_filepath,
+ variables, self.__os_creds.proxy_settings)
+ else:
+ logger.warn('VM ' + self.instance_settings.name + ' cannot self configure NICs eth1++. ' +
+ 'No playbook or keypairs found.')
+
+ def get_image_user(self):
+ """
+ Returns the instance sudo_user if it has been configured in the instance_settings else it returns the
+ image_settings.image_user value
+ """
+ if self.instance_settings.sudo_user:
+ return self.instance_settings.sudo_user
+ else:
+ return self.image_settings.image_user
+
+ def vm_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.__vm_status_check(STATUS_DELETED, block, self.instance_settings.vm_delete_timeout,
+ poll_interval)
+ except NotFound as e:
+ logger.debug("Instance not found when querying status for " + STATUS_DELETED + ' with message ' + e.message)
+ return True
+
+ def vm_active(self, block=False, poll_interval=POLL_INTERVAL):
+ """
+ Returns true when the VM 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 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)
+
+ def __vm_status_check(self, expected_status_code, block, timeout, poll_interval):
+ """
+ Returns true when the VM 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 VM status change
+ if block:
+ start = time.time()
+ else:
+ start = time.time() - timeout
+
+ while timeout > time.time() - start:
+ status = self.__status(expected_status_code)
+ if status:
+ logger.info('VM is - ' + expected_status_code)
+ return True
+
+ logger.debug('Retry querying VM status in ' + str(poll_interval) + ' seconds')
+ time.sleep(poll_interval)
+ logger.debug('VM status query timeout in ' + str(timeout - (time.time() - start)))
+
+ logger.error('Timeout checking for VM 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
+ """
+ instance = self.__nova.servers.get(self.__vm.id)
+ if not instance:
+ logger.warn('Cannot find instance with id - ' + self.__vm.id)
+ return False
+
+ if instance.status == 'ERROR':
+ raise Exception('Instance had an error during deployment')
+ logger.debug('Instance status [' + self.instance_settings.name + '] is - ' + instance.status)
+ return instance.status == expected_status_code
+
+ def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL):
+ """
+ Returns true when the VM can be accessed via SSH
+ :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 VM is active')
+
+ timeout = self.instance_settings.ssh_connect_timeout
+
+ if self.vm_active(block=True):
+ if block:
+ start = time.time()
+ else:
+ start = time.time() - timeout
+
+ while timeout > time.time() - start:
+ status = self.__ssh_active()
+ if status:
+ logger.info('SSH is active for VM instance')
+ return True
+
+ logger.debug('Retry SSH connection in ' + str(poll_interval) + ' seconds')
+ time.sleep(poll_interval)
+ logger.debug('SSH connection timeout in ' + str(timeout - (time.time() - start)))
+
+ logger.error('Timeout attempting to connect with VM via SSH')
+ return False
+
+ def __ssh_active(self):
+ """
+ Returns True when can create a SSH session else False
+ :return: T/F
+ """
+ if len(self.__floating_ips) > 0:
+ ssh = self.ssh_client()
+ if ssh:
+ return True
+ return False
+
+ def get_floating_ip(self, fip_name=None):
+ """
+ Returns the floating IP object byt name if found, else the first known, else None
+ :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 and len(self.__floating_ips) > 0:
+ return self.__floating_ips[0]
+ return None
+
+ def ssh_client(self, fip_name=None):
+ """
+ Returns an SSH client using the name or the first known floating IP if exists, else None
+ :param fip_name: the name of the floating IP to return
+ :return: the SSH client or None
+ """
+ fip = self.get_floating_ip(fip_name)
+ if fip:
+ return ansible_utils.ssh_client(self.__floating_ips[0].ip, self.get_image_user(),
+ self.keypair_settings.private_filepath,
+ proxy_settings=self.__os_creds.proxy_settings)
+ else:
+ logger.warn('Cannot return an SSH client. No Floating IP configured')
+
+ def add_security_group(self, security_group):
+ """
+ Adds a security group to this VM. Call will block until VM is active.
+ :param security_group: the OpenStack security group object
+ :return True if successful else False
+ """
+ self.vm_active(block=True)
+
+ if not security_group:
+ logger.warn('Security group object is None, cannot add')
+ return False
+
+ try:
+ nova_utils.add_security_group(self.__nova, self.get_vm_inst(), security_group['security_group']['name'])
+ return True
+ except NotFound as e:
+ logger.warn('Security group not added - ' + e.message)
+ return False
+
+ def remove_security_group(self, security_group):
+ """
+ Removes a security group to this VM. Call will block until VM is active.
+ :param security_group: the OpenStack security group object
+ :return True if successful else False
+ """
+ self.vm_active(block=True)
+
+ if not security_group:
+ logger.warn('Security group object is None, cannot add')
+ return False
+
+ try:
+ nova_utils.remove_security_group(self.__nova, self.get_vm_inst(), security_group['security_group']['name'])
+ return True
+ except NotFound as e:
+ logger.warn('Security group not added - ' + e.message)
+ return False
+
+
+class VmInstanceSettings:
+ """
+ Class responsible for holding configuration setting for a VM Instance
+ """
+ def __init__(self, config=None, name=None, flavor=None, port_settings=list(), security_group_names=set(),
+ floating_ip_settings=list(), sudo_user=None, vm_boot_timeout=900,
+ vm_delete_timeout=300, ssh_connect_timeout=180, availability_zone=None, userdata=None):
+ """
+ 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 name of the VM
+ :param flavor: the VM's flavor
+ :param port_settings: the port configuration settings
+ :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 cloud-init script to run after the VM has been started
+ """
+ if config:
+ self.name = config.get('name')
+ self.flavor = config.get('flavor')
+ self.sudo_user = config.get('sudo_user')
+ self.userdata = config.get('userdata')
+
+ self.port_settings = list()
+ if config.get('ports'):
+ for port_config in config['ports']:
+ if isinstance(port_config, PortSettings):
+ self.port_settings.append(port_config)
+ else:
+ self.port_settings.append(PortSettings(config=port_config['port']))
+
+ if config.get('security_group_names'):
+ if isinstance(config['security_group_names'], list):
+ self.security_group_names = set(config['security_group_names'])
+ elif isinstance(config['security_group_names'], set):
+ self.security_group_names = config['security_group_names']
+ elif isinstance(config['security_group_names'], basestring):
+ self.security_group_names = [config['security_group_names']]
+ else:
+ raise Exception('Invalid data type for security_group_names attribute')
+ else:
+ self.security_group_names = set()
+
+ self.floating_ip_settings = list()
+ if config.get('floating_ips'):
+ for floating_ip_config in config['floating_ips']:
+ if isinstance(floating_ip_config, FloatingIpSettings):
+ self.floating_ip_settings.append(floating_ip_config)
+ else:
+ self.floating_ip_settings.append(FloatingIpSettings(config=floating_ip_config['floating_ip']))
+
+ if config.get('vm_boot_timeout'):
+ self.vm_boot_timeout = config['vm_boot_timeout']
+ else:
+ self.vm_boot_timeout = vm_boot_timeout
+
+ if config.get('vm_delete_timeout'):
+ self.vm_delete_timeout = config['vm_delete_timeout']
+ else:
+ self.vm_delete_timeout = vm_delete_timeout
+
+ if config.get('ssh_connect_timeout'):
+ self.ssh_connect_timeout = config['ssh_connect_timeout']
+ else:
+ self.ssh_connect_timeout = ssh_connect_timeout
+
+ if config.get('availability_zone'):
+ self.availability_zone = config['availability_zone']
+ else:
+ self.availability_zone = None
+ else:
+ self.name = name
+ self.flavor = flavor
+ self.port_settings = port_settings
+ self.security_group_names = security_group_names
+ self.floating_ip_settings = floating_ip_settings
+ self.sudo_user = sudo_user
+ self.vm_boot_timeout = vm_boot_timeout
+ self.vm_delete_timeout = vm_delete_timeout
+ self.ssh_connect_timeout = ssh_connect_timeout
+ self.availability_zone = availability_zone
+ self.userdata = userdata
+
+ if not self.name or not self.flavor:
+ raise Exception('Instance configuration requires the attributes: name, flavor')
+
+
+class FloatingIpSettings:
+ """
+ Class responsible for holding configuration settings for a floating IP
+ """
+ def __init__(self, config=None, name=None, port_name=None, router_name=None, subnet_name=None, provisioning=True):
+ """
+ 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 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.
+ """
+ if config:
+ self.name = config.get('name')
+ self.port_name = config.get('port_name')
+ self.router_name = config.get('router_name')
+ self.subnet_name = config.get('subnet_name')
+ if config.get('provisioning') is not None:
+ self.provisioning = config['provisioning']
+ else:
+ self.provisioning = provisioning
+ else:
+ self.name = name
+ self.port_name = port_name
+ self.router_name = router_name
+ self.subnet_name = subnet_name
+ self.provisioning = provisioning
+
+ if not self.name or not self.port_name or not self.router_name:
+ raise Exception('The attributes name, port_name and router_name are required for FloatingIPSettings')
diff --git a/snaps/openstack/create_keypairs.py b/snaps/openstack/create_keypairs.py
new file mode 100644
index 0000000..ea7c811
--- /dev/null
+++ b/snaps/openstack/create_keypairs.py
@@ -0,0 +1,121 @@
+# 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 logging
+import os
+
+from Crypto.PublicKey import RSA
+from novaclient.exceptions import NotFound
+
+from snaps.openstack.utils import nova_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('OpenStackKeypair')
+
+
+class OpenStackKeypair:
+ """
+ Class responsible for creating a keypair in OpenStack
+ """
+
+ def __init__(self, os_creds, keypair_settings):
+ """
+ Constructor - all parameters are required
+ :param os_creds: The credentials to connect with OpenStack
+ :param keypair_settings: The settings used to create a keypair
+ """
+ self.__os_creds = os_creds
+ self.keypair_settings = keypair_settings
+ self.__nova = nova_utils.nova_client(os_creds)
+
+ # Attributes instantiated on create()
+ self.__keypair = None
+
+ def create(self, cleanup=False):
+ """
+ Responsible for creating the keypair object.
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ """
+ logger.info('Creating keypair %s...' % self.keypair_settings.name)
+
+ try:
+ self.__keypair = nova_utils.get_keypair_by_name(self.__nova, self.keypair_settings.name)
+
+ if not self.__keypair and not cleanup:
+ if self.keypair_settings.public_filepath and os.path.isfile(self.keypair_settings.public_filepath):
+ logger.info("Uploading existing keypair")
+ self.__keypair = nova_utils.upload_keypair_file(self.__nova, self.keypair_settings.name,
+ self.keypair_settings.public_filepath)
+ else:
+ logger.info("Creating new keypair")
+ # TODO - Make this value configurable
+ keys = RSA.generate(1024)
+ self.__keypair = nova_utils.upload_keypair(self.__nova, self.keypair_settings.name,
+ keys.publickey().exportKey('OpenSSH'))
+ nova_utils.save_keys_to_files(keys, self.keypair_settings.public_filepath,
+ self.keypair_settings.private_filepath)
+
+ return self.__keypair
+ except Exception as e:
+ logger.error('Unexpected error creating keypair named - ' + self.keypair_settings.name)
+ self.clean()
+ raise Exception(e.message)
+
+ def clean(self):
+ """
+ Removes and deletes the keypair.
+ """
+ if self.__keypair:
+ try:
+ nova_utils.delete_keypair(self.__nova, self.__keypair)
+ except NotFound:
+ pass
+ self.__keypair = None
+
+ def get_keypair(self):
+ """
+ Returns the OpenStack keypair object
+ :return:
+ """
+ return self.__keypair
+
+
+class KeypairSettings:
+ """
+ Class representing a keypair configuration
+ """
+
+ def __init__(self, config=None, name=None, public_filepath=None, private_filepath=None):
+ """
+ Constructor - all parameters are optional
+ :param config: Should be a 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 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
+ :return:
+ """
+
+ if config:
+ self.name = config.get('name')
+ self.public_filepath = config.get('public_filepath')
+ self.private_filepath = config.get('private_filepath')
+ else:
+ self.name = name
+ self.public_filepath = public_filepath
+ self.private_filepath = private_filepath
+
+ if not self.name:
+ raise Exception('The attributes name, public_filepath, and private_filepath are required')
diff --git a/snaps/openstack/create_network.py b/snaps/openstack/create_network.py
new file mode 100644
index 0000000..a214ba1
--- /dev/null
+++ b/snaps/openstack/create_network.py
@@ -0,0 +1,519 @@
+# 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 logging
+
+from neutronclient.common.exceptions import NotFound
+
+from snaps.openstack.utils import keystone_utils, neutron_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('OpenStackNetwork')
+
+
+class OpenStackNetwork:
+ """
+ Class responsible for creating a network in OpenStack
+ """
+
+ def __init__(self, os_creds, network_settings):
+ """
+ Constructor - all parameters are required
+ :param os_creds: The credentials to connect with OpenStack
+ :param network_settings: The settings used to create a network
+ """
+ self.__os_creds = os_creds
+ self.network_settings = network_settings
+ self.__neutron = neutron_utils.neutron_client(self.__os_creds)
+
+ # Attributes instantiated on create()
+ self.__network = None
+ self.__subnets = list()
+
+ def create(self, cleanup=False):
+ """
+ Responsible for creating not only the network but then a private subnet, router, and an interface to the router.
+ :param cleanup: When true, only perform lookups for OpenStack objects.
+ :return: the created network object or None
+ """
+ try:
+ logger.info('Creating neutron network %s...' % self.network_settings.name)
+ net_inst = neutron_utils.get_network(self.__neutron, self.network_settings.name,
+ self.network_settings.get_project_id(self.__os_creds))
+ if net_inst:
+ self.__network = net_inst
+ else:
+ if not cleanup:
+ self.__network = neutron_utils.create_network(self.__neutron, self.__os_creds,
+ self.network_settings)
+ else:
+ logger.info('Network does not exist and will not create as in cleanup mode')
+ return
+ logger.debug("Network '%s' created successfully" % self.__network['network']['id'])
+
+ logger.debug('Creating Subnets....')
+ for subnet_setting in self.network_settings.subnet_settings:
+ sub_inst = neutron_utils.get_subnet_by_name(self.__neutron, subnet_setting.name)
+ if sub_inst:
+ self.__subnets.append(sub_inst)
+ logger.debug("Subnet '%s' created successfully" % sub_inst['subnet']['id'])
+ else:
+ if not cleanup:
+ self.__subnets.append(neutron_utils.create_subnet(self.__neutron, subnet_setting,
+ self.__os_creds, self.__network))
+
+ return self.__network
+ except Exception as e:
+ logger.error('Unexpected exception thrown while creating network - ' + str(e))
+ self.clean()
+ raise e
+
+ def clean(self):
+ """
+ Removes and deletes all items created in reverse order.
+ """
+ for subnet in self.__subnets:
+ try:
+ logger.info('Deleting subnet with name ' + subnet['subnet']['name'])
+ neutron_utils.delete_subnet(self.__neutron, subnet)
+ except NotFound as e:
+ logger.warn('Error deleting subnet with message - ' + e.message)
+ pass
+ self.__subnets = list()
+
+ if self.__network:
+ try:
+ neutron_utils.delete_network(self.__neutron, self.__network)
+ except NotFound:
+ pass
+
+ self.__network = None
+
+ def get_network(self):
+ """
+ Returns the created OpenStack network object
+ :return: the OpenStack network object
+ """
+ return self.__network
+
+ def get_subnets(self):
+ """
+ Returns the OpenStack subnet objects
+ :return:
+ """
+ return self.__subnets
+
+
+class NetworkSettings:
+ """
+ Class representing a network configuration
+ """
+
+ def __init__(self, config=None, name=None, admin_state_up=True, shared=None, project_name=None,
+ external=False, network_type=None, physical_network=None, subnet_settings=list()):
+ """
+ Constructor - all parameters are optional
+ :param config: Should be a 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 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 subnet_settings: List of SubnetSettings objects.
+ :return:
+ """
+
+ self.project_id = None
+
+ if config:
+ self.name = config.get('name')
+ if config.get('admin_state_up') is not None:
+ self.admin_state_up = bool(config['admin_state_up'])
+ else:
+ self.admin_state_up = admin_state_up
+
+ if config.get('shared') is not None:
+ self.shared = bool(config['shared'])
+ else:
+ self.shared = None
+
+ self.project_name = config.get('project_name')
+
+ if config.get('external') is not None:
+ self.external = bool(config.get('external'))
+ else:
+ self.external = external
+
+ self.network_type = config.get('network_type')
+ self.physical_network = config.get('physical_network')
+
+ self.subnet_settings = list()
+ if config.get('subnets'):
+ for subnet_config in config['subnets']:
+ self.subnet_settings.append(SubnetSettings(config=subnet_config['subnet']))
+
+ else:
+ self.name = name
+ self.admin_state_up = admin_state_up
+ self.shared = shared
+ self.project_name = project_name
+ self.external = external
+ self.network_type = network_type
+ self.physical_network = physical_network
+ self.subnet_settings = subnet_settings
+
+ if not self.name or len(self.name) < 1:
+ raise Exception('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, 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['project_id'] = project_id
+ else:
+ raise Exception('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.external:
+ out['router:external'] = self.external
+ return {'network': out}
+
+
+class SubnetSettings:
+ """
+ Class representing a subnet configuration
+ """
+
+ def __init__(self, config=None, cidr=None, ip_version=4, name=None, project_name=None, start=None,
+ end=None, gateway_ip=None, enable_dhcp=None, dns_nameservers=None, host_routes=None, destination=None,
+ nexthop=None, ipv6_ra_mode=None, ipv6_address_mode=None):
+ """
+ Constructor - all parameters are optional except cidr (subnet mask)
+ :param config: Should be a 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 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: Exception when config does not have or cidr values are None
+ """
+ if not dns_nameservers:
+ dns_nameservers = ['8.8.8.8']
+
+ if config:
+ self.cidr = config['cidr']
+ if config.get('ip_version'):
+ self.ip_version = config['ip_version']
+ else:
+ self.ip_version = ip_version
+
+ # Optional attributes that can be set after instantiation
+ self.name = config.get('name')
+ self.project_name = config.get('project_name')
+ self.start = config.get('start')
+ self.end = config.get('end')
+ self.gateway_ip = config.get('gateway_ip')
+ self.enable_dhcp = config.get('enable_dhcp')
+
+ if config.get('dns_nameservers'):
+ self.dns_nameservers = config.get('dns_nameservers')
+ else:
+ self.dns_nameservers = dns_nameservers
+
+ self.host_routes = config.get('host_routes')
+ self.destination = config.get('destination')
+ self.nexthop = config.get('nexthop')
+ self.ipv6_ra_mode = config.get('ipv6_ra_mode')
+ self.ipv6_address_mode = config.get('ipv6_address_mode')
+ else:
+ # Required attributes
+ self.cidr = cidr
+ self.ip_version = ip_version
+
+ # Optional attributes that can be set after instantiation
+ self.name = name
+ self.project_name = project_name
+ self.start = start
+ self.end = end
+ self.gateway_ip = gateway_ip
+ self.enable_dhcp = enable_dhcp
+ self.dns_nameservers = dns_nameservers
+ self.host_routes = host_routes
+ self.destination = destination
+ self.nexthop = nexthop
+ self.ipv6_ra_mode = ipv6_ra_mode
+ self.ipv6_address_mode = ipv6_address_mode
+
+ if not self.name or not self.cidr:
+ raise Exception('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: (Optional) the network object on which the subnet will be created
+ :return: the dictionary object
+ """
+ out = {
+ 'cidr': self.cidr,
+ 'ip_version': self.ip_version,
+ }
+
+ if network:
+ out['network_id'] = network['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, self.project_name)
+ project_id = None
+ if project:
+ project_id = project.id
+ if project_id:
+ out['project_id'] = project_id
+ else:
+ raise Exception('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 PortSettings:
+ """
+ Class representing a port configuration
+ """
+
+ def __init__(self, config=None, name=None, network_name=None, admin_state_up=True, project_name=None,
+ mac_address=None, ip_addrs=None, fixed_ips=None, security_groups=None, allowed_address_pairs=None,
+ opt_value=None, opt_name=None, device_owner=None, device_id=None):
+ """
+ Constructor - all parameters are optional
+ :param config: Should be a 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: A symbolic name for the port.
+ :param network_name: The name of the network on which to create the port.
+ :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:
+ """
+ self.network = None
+
+ if config:
+ self.name = config.get('name')
+ self.network_name = config.get('network_name')
+
+ if config.get('admin_state_up') is not None:
+ self.admin_state_up = bool(config['admin_state_up'])
+ else:
+ self.admin_state_up = admin_state_up
+
+ self.project_name = config.get('project_name')
+ self.mac_address = config.get('mac_address')
+ self.ip_addrs = config.get('ip_addrs')
+ self.fixed_ips = config.get('fixed_ips')
+ self.security_groups = config.get('security_groups')
+ self.allowed_address_pairs = config.get('allowed_address_pairs')
+ self.opt_value = config.get('opt_value')
+ self.opt_name = config.get('opt_name')
+ self.device_owner = config.get('device_owner')
+ self.device_id = config.get('device_id')
+ else:
+ self.name = name
+ self.network_name = network_name
+ self.admin_state_up = admin_state_up
+ self.project_name = project_name
+ self.mac_address = mac_address
+ self.ip_addrs = ip_addrs
+ self.fixed_ips = fixed_ips
+ self.security_groups = security_groups
+ self.allowed_address_pairs = allowed_address_pairs
+ self.opt_value = opt_value
+ self.opt_name = opt_name
+ self.device_owner = device_owner
+ self.device_id = device_id
+
+ if not self.name or not self.network_name:
+ raise Exception('The attributes neutron, name, and network_name are required for PortSettings')
+
+ 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_by_name(neutron, ip_addr_dict['subnet_name'])
+ if subnet:
+ self.fixed_ips.append({'ip_address': ip_addr_dict['ip'], 'subnet_id': subnet['subnet']['id']})
+ else:
+ raise Exception('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, self.project_name)
+ if project:
+ project_id = project.id
+
+ if not self.network:
+ self.network = neutron_utils.get_network(neutron, self.network_name, project_id)
+ if not self.network:
+ raise Exception('Cannot locate network with name - ' + self.network_name)
+
+ out['network_id'] = self.network['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['project_id'] = project_id
+ else:
+ raise Exception('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}
diff --git a/snaps/openstack/create_project.py b/snaps/openstack/create_project.py
new file mode 100644
index 0000000..60f9ed0
--- /dev/null
+++ b/snaps/openstack/create_project.py
@@ -0,0 +1,139 @@
+# 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 logging
+
+from keystoneclient.exceptions import NotFound
+
+from snaps.openstack.utils import keystone_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_image')
+
+
+class OpenStackProject:
+ """
+ Class responsible for creating a project/project in OpenStack
+ """
+
+ def __init__(self, os_creds, project_settings):
+ """
+ Constructor
+ :param os_creds: The OpenStack connection credentials
+ :param project_settings: The project's settings
+ :return:
+ """
+ self.__os_creds = os_creds
+ self.project_settings = project_settings
+ self.__project = None
+ self.__role = None
+ self.__keystone = keystone_utils.keystone_client(self.__os_creds)
+
+ def create(self, cleanup=False):
+ """
+ Creates the image in OpenStack if it does not already exist
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: The OpenStack Image object
+ """
+ try:
+ self.__project = keystone_utils.get_project(keystone=self.__keystone,
+ project_name=self.project_settings.name)
+ if self.__project:
+ logger.info('Found project with name - ' + self.project_settings.name)
+ elif not cleanup:
+ self.__project = keystone_utils.create_project(self.__keystone, self.project_settings)
+ else:
+ logger.info('Did not create image due to cleanup mode')
+ except Exception as e:
+ logger.error('Unexpected error. Rolling back')
+ self.clean()
+ raise Exception(e.message)
+
+ return self.__project
+
+ def clean(self):
+ """
+ Cleanse environment of all artifacts
+ :return: void
+ """
+ if self.__project:
+ try:
+ keystone_utils.delete_project(self.__keystone, self.__project)
+ except NotFound:
+ pass
+ self.__project = None
+
+ if self.__role:
+ try:
+ keystone_utils.delete_role(self.__keystone, self.__role)
+ except NotFound:
+ pass
+ self.__project = None
+
+ def get_project(self):
+ """
+ Returns the OpenStack project object populated on create()
+ :return:
+ """
+ return self.__project
+
+ def assoc_user(self, user):
+ """
+ The user object to associate with the project
+ :param user: the OpenStack user object to associate with project
+ :return:
+ """
+ if not self.__role:
+ self.__role = keystone_utils.create_role(self.__keystone, self.project_settings.name + '-role')
+
+ keystone_utils.assoc_user_to_project(self.__keystone, self.__role, user, self.__project)
+
+
+class ProjectSettings:
+ """
+ Class to hold the configuration settings required for creating OpenStack project objects
+ """
+ def __init__(self, config=None, name=None, domain='default', description=None, enabled=True):
+
+ """
+ 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 project's name (required)
+ :param domain: the project's domain name (default 'default'). Field is used for v3 clients
+ :param description: the description (optional)
+ :param enabled: denotes whether or not the user is enabled (default True)
+ """
+
+ if config:
+ self.name = config.get('name')
+ if config.get('domain'):
+ self.domain = config['domain']
+ else:
+ self.domain = domain
+
+ self.description = config.get('description')
+ if config.get('enabled') is not None:
+ self.enabled = config['enabled']
+ else:
+ self.enabled = enabled
+ else:
+ self.name = name
+ self.domain = domain
+ self.description = description
+ self.enabled = enabled
+
+ if not self.name:
+ raise Exception("The attribute name is required for ProjectSettings")
diff --git a/snaps/openstack/create_router.py b/snaps/openstack/create_router.py
new file mode 100644
index 0000000..70c6b76
--- /dev/null
+++ b/snaps/openstack/create_router.py
@@ -0,0 +1,244 @@
+# 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 logging
+
+from neutronclient.common.exceptions import NotFound
+
+from snaps.openstack.create_network import PortSettings
+from snaps.openstack.utils import neutron_utils, keystone_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('OpenStackNetwork')
+
+
+class OpenStackRouter:
+ """
+ Class responsible for creating a router in OpenStack
+ """
+
+ def __init__(self, os_creds, router_settings):
+ """
+ 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 class)
+ """
+ self.__os_creds = os_creds
+
+ if not router_settings:
+ raise Exception('router_settings is required')
+
+ self.router_settings = router_settings
+ self.__neutron = neutron_utils.neutron_client(os_creds)
+
+ # Attributes instantiated on create()
+ self.__router = None
+ self.__internal_subnets = list()
+ self.__internal_router_interface = None
+
+ # Dict where the port object is the key and any newly created router interfaces are the value
+ self.__ports = list()
+
+ def create(self, cleanup=False):
+ """
+ Responsible for creating the router.
+ :param cleanup: When true, only perform lookups for OpenStack objects.
+ :return: the router object
+ """
+ logger.debug('Creating Router with name - ' + self.router_settings.name)
+ try:
+ existing = False
+ router_inst = neutron_utils.get_router_by_name(self.__neutron, self.router_settings.name)
+ if router_inst:
+ self.__router = router_inst
+ existing = True
+ else:
+ if not cleanup:
+ self.__router = neutron_utils.create_router(self.__neutron, self.__os_creds, self.router_settings)
+
+ for internal_subnet_name in self.router_settings.internal_subnets:
+ internal_subnet = neutron_utils.get_subnet_by_name(self.__neutron, internal_subnet_name)
+ if internal_subnet:
+ self.__internal_subnets.append(internal_subnet)
+ if internal_subnet and not cleanup and not existing:
+ logger.debug('Adding router to subnet...')
+ self.__internal_router_interface = neutron_utils.add_interface_router(
+ self.__neutron, self.__router, subnet=internal_subnet)
+ else:
+ raise Exception('Subnet not found with name ' + internal_subnet_name)
+
+ for port_setting in self.router_settings.port_settings:
+ port = neutron_utils.get_port_by_name(self.__neutron, port_setting.name)
+ logger.info('Retrieved port ' + port_setting.name + ' for router - ' + self.router_settings.name)
+ if port:
+ self.__ports.append(port)
+
+ if not port and not cleanup and not existing:
+ port = neutron_utils.create_port(self.__neutron, self.__os_creds, port_setting)
+ if port:
+ logger.info('Created port ' + port_setting.name + ' for router - ' + self.router_settings.name)
+ self.__ports.append(port)
+ neutron_utils.add_interface_router(self.__neutron, self.__router, port=port)
+ else:
+ raise Exception('Error creating port with name - ' + port_setting.name)
+
+ return self.__router
+ except Exception as e:
+ self.clean()
+ raise Exception(e.message)
+
+ def clean(self):
+ """
+ Removes and deletes all items created in reverse order.
+ """
+ for port in self.__ports:
+ logger.info('Removing router interface from router ' + self.router_settings.name +
+ ' and port ' + port['port']['name'])
+ try:
+ neutron_utils.remove_interface_router(self.__neutron, self.__router, port=port)
+ except NotFound:
+ pass
+ self.__ports = list()
+
+ for internal_subnet in self.__internal_subnets:
+ logger.info('Removing router interface from router ' + self.router_settings.name +
+ ' and subnet ' + internal_subnet['subnet']['name'])
+ try:
+ neutron_utils.remove_interface_router(self.__neutron, self.__router, subnet=internal_subnet)
+ except NotFound:
+ pass
+ self.__internal_subnets = list()
+
+ if self.__router:
+ logger.info('Removing router ' + self.router_settings.name)
+ try:
+ neutron_utils.delete_router(self.__neutron, self.__router)
+ except NotFound:
+ pass
+ self.__router = None
+
+ def get_router(self):
+ """
+ Returns the OpenStack router object
+ :return:
+ """
+ return self.__router
+
+ def get_internal_router_interface(self):
+ """
+ Returns the OpenStack internal router interface object
+ :return:
+ """
+ return self.__internal_router_interface
+
+
+class RouterSettings:
+ """
+ Class representing a router configuration
+ """
+
+ def __init__(self, config=None, name=None, project_name=None, external_gateway=None,
+ admin_state_up=True, external_fixed_ips=None, internal_subnets=list(),
+ port_settings=list()):
+ """
+ Constructor - all parameters are optional
+ :param config: Should be a 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 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 enable_snat: Boolean value. Enable Source NAT (SNAT) attribute. Default is True. To persist this
+ attribute value, set the enable_snat_by_default option in the neutron.conf file.
+ :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:
+ """
+ if config:
+ self.name = config.get('name')
+ self.project_name = config.get('project_name')
+ self.external_gateway = config.get('external_gateway')
+
+ self.admin_state_up = config.get('admin_state_up')
+ self.enable_snat = config.get('enable_snat')
+ self.external_fixed_ips = config.get('external_fixed_ips')
+ if config.get('internal_subnets'):
+ self.internal_subnets = config['internal_subnets']
+ else:
+ self.internal_subnets = internal_subnets
+
+ self.port_settings = list()
+ if config.get('interfaces'):
+ interfaces = config['interfaces']
+ for interface in interfaces:
+ if interface.get('port'):
+ self.port_settings.append(PortSettings(config=interface['port']))
+ else:
+ self.name = name
+ self.project_name = project_name
+ self.external_gateway = external_gateway
+ self.admin_state_up = admin_state_up
+ self.external_fixed_ips = external_fixed_ips
+ self.internal_subnets = internal_subnets
+ self.port_settings = port_settings
+
+ if not self.name:
+ raise Exception('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()
+
+ project_id = None
+
+ if self.name:
+ out['name'] = self.name
+ if self.project_name:
+ keystone = keystone_utils.keystone_client(os_creds)
+ project = keystone_utils.get_project(keystone, self.project_name)
+ project_id = None
+ if project:
+ project_id = project.id
+ if project_id:
+ out['project_id'] = project_id
+ else:
+ raise Exception('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, self.external_gateway, project_id)
+ if ext_net:
+ ext_gw['network_id'] = ext_net['network']['id']
+ out['external_gateway_info'] = ext_gw
+ else:
+ raise Exception('Could not find the external network named - ' + self.external_gateway)
+
+ #TODO: Enable SNAT option for Router
+ #TODO: Add external_fixed_ips Tests
+
+ return {'router': out}
diff --git a/snaps/openstack/create_security_group.py b/snaps/openstack/create_security_group.py
new file mode 100644
index 0000000..fc1ee98
--- /dev/null
+++ b/snaps/openstack/create_security_group.py
@@ -0,0 +1,521 @@
+# 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 logging
+
+import enum
+from neutronclient.common.exceptions import NotFound
+from snaps.openstack.utils import neutron_utils
+from snaps.openstack.utils import keystone_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('OpenStackSecurityGroup')
+
+
+class OpenStackSecurityGroup:
+ """
+ Class responsible for creating Security Groups
+ """
+
+ def __init__(self, os_creds, sec_grp_settings):
+ """
+ Constructor - all parameters are required
+ :param os_creds: The credentials to connect with OpenStack
+ :param sec_grp_settings: The settings used to create a security group
+ """
+ self.__os_creds = os_creds
+ self.sec_grp_settings = sec_grp_settings
+ self.__neutron = neutron_utils.neutron_client(os_creds)
+ self.__keystone = keystone_utils.keystone_client(os_creds)
+
+ # Attributes instantiated on create()
+ self.__security_group = None
+
+ # dict where the rule settings object is the key
+ self.__rules = dict()
+
+ def create(self, cleanup=False):
+ """
+ Responsible for creating the security group.
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: the OpenStack security group object
+ """
+ logger.info('Creating security group %s...' % self.sec_grp_settings.name)
+
+ self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name)
+ if not self.__security_group and not cleanup:
+ # Create the security group
+ self.__security_group = neutron_utils.create_security_group(self.__neutron, self.__keystone,
+ self.sec_grp_settings)
+
+ # Get the rules added for free
+ auto_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
+
+ ctr = 0
+ for auto_rule in auto_rules:
+ auto_rule_setting = self.__generate_rule_setting(auto_rule)
+ self.__rules[auto_rule_setting] = auto_rule
+ ctr += 1
+
+ # Create the custom rules
+ for sec_grp_rule_setting in self.sec_grp_settings.rule_settings:
+ custom_rule = neutron_utils.create_security_group_rule(self.__neutron, sec_grp_rule_setting)
+ self.__rules[sec_grp_rule_setting] = custom_rule
+
+ # Refresh security group object to reflect the new rules added to it
+ self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name)
+ else:
+ # Populate rules
+ existing_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
+
+ for existing_rule in existing_rules:
+ # For Custom Rules
+ rule_setting = self.__get_setting_from_rule(existing_rule)
+ ctr = 0
+ if not rule_setting:
+ # For Free Rules
+ rule_setting = self.__generate_rule_setting(existing_rule)
+ ctr += 1
+
+ self.__rules[rule_setting] = existing_rule
+
+ return self.__security_group
+
+ def __generate_rule_setting(self, rule):
+ """
+ Creates a SecurityGroupRuleSettings object for a given rule
+ :param rule: the rule from which to create the SecurityGroupRuleSettings object
+ :return: the newly instantiated SecurityGroupRuleSettings object
+ """
+ rule_dict = rule['security_group_rule']
+ sec_grp_name = None
+ if rule_dict['security_group_id']:
+ sec_grp = neutron_utils.get_security_group_by_id(self.__neutron, rule_dict['security_group_id'])
+ if sec_grp:
+ sec_grp_name = sec_grp['security_group']['name']
+
+ setting = SecurityGroupRuleSettings(description=rule_dict['description'],
+ direction=rule_dict['direction'], ethertype=rule_dict['ethertype'],
+ port_range_min=rule_dict['port_range_min'],
+ port_range_max=rule_dict['port_range_max'], protocol=rule_dict['protocol'],
+ remote_group_id=rule_dict['remote_group_id'],
+ remote_ip_prefix=rule_dict['remote_ip_prefix'], sec_grp_name=sec_grp_name)
+ return setting
+
+ def clean(self):
+ """
+ Removes and deletes the rules then the security group.
+ """
+ for setting, rule in self.__rules.iteritems():
+ try:
+ neutron_utils.delete_security_group_rule(self.__neutron, rule)
+ except NotFound as e:
+ logger.warn('Rule not found, cannot delete - ' + e.message)
+ pass
+ self.__rules = dict()
+
+ if self.__security_group:
+ try:
+ neutron_utils.delete_security_group(self.__neutron, self.__security_group)
+ except NotFound as e:
+ logger.warn('Security Group not found, cannot delete - ' + e.message)
+
+ self.__security_group = None
+
+ def get_security_group(self):
+ """
+ Returns the OpenStack security group object
+ :return:
+ """
+ return self.__security_group
+
+ def get_rules(self):
+ """
+ Returns the associated rules
+ :return:
+ """
+ return self.__rules
+
+ def add_rule(self, rule_setting):
+ """
+ Adds a rule to this security group
+ :param rule_setting: the rule configuration
+ """
+ rule_setting.sec_grp_name = self.sec_grp_settings.name
+ new_rule = neutron_utils.create_security_group_rule(self.__neutron, rule_setting)
+ self.__rules[rule_setting] = new_rule
+ self.sec_grp_settings.rule_settings.append(rule_setting)
+
+ def remove_rule(self, rule_id=None, rule_setting=None):
+ """
+ Removes a rule to this security group by id, name, or rule_setting object
+ :param rule_id: the rule's id
+ :param rule_setting: the rule's setting object
+ """
+ rule_to_remove = None
+ if rule_id or rule_setting:
+ if rule_id:
+ rule_to_remove = neutron_utils.get_rule_by_id(self.__neutron, self.__security_group, rule_id)
+ elif rule_setting:
+ rule_to_remove = self.__rules.get(rule_setting)
+
+ if rule_to_remove:
+ neutron_utils.delete_security_group_rule(self.__neutron, rule_to_remove)
+ rule_setting = self.__get_setting_from_rule(rule_to_remove)
+ if rule_setting:
+ self.__rules.pop(rule_setting)
+ else:
+ logger.warn('Rule setting is None, cannot remove rule')
+
+ def __get_setting_from_rule(self, rule):
+ """
+ Returns the associated RuleSetting object for a given rule
+ :param rule: the Rule object
+ :return: the associated RuleSetting object or None
+ """
+ for rule_setting in self.sec_grp_settings.rule_settings:
+ if rule_setting.rule_eq(rule):
+ return rule_setting
+ return None
+
+
+class SecurityGroupSettings:
+ """
+ Class representing a keypair configuration
+ """
+
+ def __init__(self, config=None, name=None, description=None, project_name=None,
+ rule_settings=list()):
+ """
+ Constructor - all parameters are optional
+ :param config: Should be a 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 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:
+ """
+ if config:
+ self.name = config.get('name')
+ self.description = config.get('description')
+ self.project_name = config.get('project_name')
+ self.rule_settings = list()
+ if config.get('rules') and type(config['rules']) is list:
+ for config_rule in config['rules']:
+ self.rule_settings.append(SecurityGroupRuleSettings(config=config_rule))
+ else:
+ self.name = name
+ self.description = description
+ self.project_name = project_name
+ self.rule_settings = rule_settings
+
+ if not self.name:
+ raise Exception('The attribute name is required')
+
+ for rule_setting in self.rule_settings:
+ if rule_setting.sec_grp_name is not self.name:
+ raise Exception('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, self.project_name)
+ project_id = None
+ if project:
+ project_id = project.id
+ if project_id:
+ out['project_id'] = project_id
+ else:
+ raise Exception('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
+ """
+ icmp = 'icmp'
+ tcp = 'tcp'
+ udp = 'udp'
+ null = 'null'
+
+
+class Ethertype(enum.Enum):
+ """
+ A rule's ethertype
+ """
+ IPv4 = 4
+ IPv6 = 6
+
+
+class SecurityGroupRuleSettings:
+ """
+ Class representing a keypair configuration
+ """
+
+ def __init__(self, config=None, sec_grp_name=None, description=None, direction=None,
+ remote_group_id=None, protocol=None, ethertype=None, port_range_min=None, port_range_max=None,
+ sec_grp_rule=None, remote_ip_prefix=None):
+ """
+ Constructor - all parameters are optional
+ :param config: Should be a 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 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...
+ """
+
+ if config:
+ self.description = config.get('description')
+ self.sec_grp_name = config.get('sec_grp_name')
+ self.remote_group_id = config.get('remote_group_id')
+ self.direction = None
+ if config.get('direction'):
+ self.direction = map_direction(config['direction'])
+
+ self.protocol = None
+ if config.get('protocol'):
+ self.protocol = map_protocol(config['protocol'])
+ else:
+ self.protocol = Protocol.null
+
+ self.ethertype = None
+ if config.get('ethertype'):
+ self.ethertype = map_ethertype(config['ethertype'])
+
+ self.port_range_min = config.get('port_range_min')
+ self.port_range_max = config.get('port_range_max')
+ self.remote_ip_prefix = config.get('remote_ip_prefix')
+ else:
+ self.description = description
+ self.sec_grp_name = sec_grp_name
+ self.remote_group_id = remote_group_id
+ self.direction = map_direction(direction)
+ self.protocol = map_protocol(protocol)
+ self.ethertype = map_ethertype(ethertype)
+ self.port_range_min = port_range_min
+ self.port_range_max = port_range_max
+ self.sec_grp_rule = sec_grp_rule
+ self.remote_ip_prefix = remote_ip_prefix
+
+ if not self.direction or not self.sec_grp_name:
+ raise Exception('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:
+ out['protocol'] = self.protocol.name
+ if self.sec_grp_name:
+ sec_grp = neutron_utils.get_security_group(neutron, self.sec_grp_name)
+ if sec_grp:
+ out['security_group_id'] = sec_grp['security_group']['id']
+ else:
+ raise Exception('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.sec_grp_rule:
+ out['security_group_rule'] = self.sec_grp_rule
+ 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
+ """
+ rule_dict = rule['security_group_rule']
+
+ if self.description is not None:
+ if rule_dict['description'] is not None and rule_dict['description'] != '':
+ return False
+ elif self.description != rule_dict['description']:
+ if rule_dict['description'] != '':
+ return False
+
+ if self.direction.name != rule_dict['direction']:
+ return False
+
+ if self.ethertype and rule_dict.get('ethertype'):
+ if self.ethertype.name != rule_dict['ethertype']:
+ return False
+
+ if self.port_range_min and rule_dict.get('port_range_min'):
+ if self.port_range_min != rule_dict['port_range_min']:
+ return False
+
+ if self.port_range_max and rule_dict.get('port_range_max'):
+ if self.port_range_max != rule_dict['port_range_max']:
+ return False
+
+ if self.protocol and rule_dict.get('protocol'):
+ if self.protocol.name != rule_dict['protocol']:
+ return False
+
+ if self.remote_group_id and rule_dict.get('remote_group_id'):
+ if self.remote_group_id != rule_dict['remote_group_id']:
+ return False
+
+ if self.remote_ip_prefix and rule_dict.get('remote_ip_prefix'):
+ if self.remote_ip_prefix != rule_dict['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.sec_grp_rule == other.sec_grp_rule 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.sec_grp_rule,
+ 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 type(direction) is Direction:
+ return direction
+ elif isinstance(direction, basestring):
+ if direction == 'egress':
+ return Direction.egress
+ elif direction == 'ingress':
+ return Direction.ingress
+ else:
+ raise Exception('Invalid Direction - ' + direction)
+ else:
+ raise Exception('Invalid Direction object - ' + str(direction))
+
+
+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 type(protocol) is Protocol:
+ return protocol
+ elif isinstance(protocol, basestring):
+ if protocol == 'icmp':
+ return Protocol.icmp
+ elif protocol == 'tcp':
+ return Protocol.tcp
+ elif protocol == 'udp':
+ return Protocol.udp
+ elif protocol == 'null':
+ return Protocol.null
+ else:
+ raise Exception('Invalid Protocol - ' + protocol)
+ else:
+ raise Exception('Invalid Protocol object - ' + str(protocol))
+
+
+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 type(ethertype) is Ethertype:
+ return ethertype
+ elif isinstance(ethertype, basestring):
+ if ethertype == 'IPv6':
+ return Ethertype.IPv6
+ elif ethertype == 'IPv4':
+ return Ethertype.IPv4
+ else:
+ raise Exception('Invalid Ethertype - ' + ethertype)
+ else:
+ raise Exception('Invalid Ethertype object - ' + str(ethertype))
diff --git a/snaps/openstack/create_user.py b/snaps/openstack/create_user.py
new file mode 100644
index 0000000..a8d0fcc
--- /dev/null
+++ b/snaps/openstack/create_user.py
@@ -0,0 +1,137 @@
+# 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 logging
+from keystoneclient.exceptions import NotFound
+from snaps.openstack.os_credentials import OSCreds
+
+from snaps.openstack.utils import keystone_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('create_user')
+
+
+class OpenStackUser:
+ """
+ Class responsible for creating a user in OpenStack
+ """
+
+ def __init__(self, os_creds, user_settings):
+ """
+ Constructor
+ :param os_creds: The OpenStack connection credentials
+ :param user_settings: The user settings
+ :return:
+ """
+ self.__os_creds = os_creds
+ self.user_settings = user_settings
+ self.__user = None
+ self.__keystone = keystone_utils.keystone_client(self.__os_creds)
+
+ def create(self, cleanup=False):
+ """
+ Creates the user in OpenStack if it does not already exist
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: The OpenStack user object
+ """
+ self.__user = keystone_utils.get_user(self.__keystone, self.user_settings.name)
+ if self.__user:
+ logger.info('Found user with name - ' + self.user_settings.name)
+ elif not cleanup:
+ self.__user = keystone_utils.create_user(self.__keystone, self.user_settings)
+ else:
+ logger.info('Did not create user due to cleanup mode')
+
+ return self.__user
+
+ def clean(self):
+ """
+ Cleanse environment of user
+ :return: void
+ """
+ if self.__user:
+ try:
+ keystone_utils.delete_user(self.__keystone, self.__user)
+ except NotFound:
+ pass
+ self.__user = None
+
+ def get_user(self):
+ """
+ Returns the OpenStack user object populated in create()
+ :return: the Object or None if not created
+ """
+ return self.__user
+
+ def get_os_creds(self, project_name=None):
+ """
+ Returns an OSCreds object based on this user account and a project
+ :param project_name: the name of the project to leverage in the credentials
+ :return:
+ """
+ return OSCreds(username=self.user_settings.name,
+ password=self.user_settings.password,
+ auth_url=self.__os_creds.auth_url,
+ project_name=project_name,
+ identity_api_version=self.__os_creds.identity_api_version,
+ user_domain_id=self.__os_creds.user_domain_id,
+ project_domain_id=self.__os_creds.project_domain_id,
+ proxy_settings=self.__os_creds.proxy_settings)
+
+
+class UserSettings:
+ def __init__(self, config=None, name=None, password=None, project_name=None, domain_name='default', email=None,
+ enabled=True):
+
+ """
+ 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 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)
+ """
+
+ if config:
+ self.name = config.get('name')
+ self.password = config.get('password')
+ self.project_name = config.get('project_name')
+ self.email = config.get('email')
+
+ if config.get('domain_name'):
+ self.domain_name = config['domain_name']
+ else:
+ self.domain_name = domain_name
+
+ if config.get('enabled') is not None:
+ self.enabled = config['enabled']
+ else:
+ self.enabled = enabled
+ else:
+ self.name = name
+ self.password = password
+ self.project_name = project_name
+ self.email = email
+ self.enabled = enabled
+ self.domain_name = domain_name
+
+ if not self.name or not self.password:
+ raise Exception('The attributes name and password are required for UserSettings')
+
+ if not isinstance(self.enabled, bool):
+ raise Exception('The attribute enabled must be of type boolean')
diff --git a/snaps/openstack/os_credentials.py b/snaps/openstack/os_credentials.py
new file mode 100644
index 0000000..c173bf7
--- /dev/null
+++ b/snaps/openstack/os_credentials.py
@@ -0,0 +1,103 @@
+# 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.
+__author__ = 'spisarski'
+
+
+class OSCreds:
+ """
+ Represents the credentials required to connect with OpenStack servers
+ """
+
+ def __init__(self, username, password, auth_url, project_name, identity_api_version=2, image_api_version=1,
+ network_api_version=2, compute_api_version=2, user_domain_id='default', project_domain_id='default',
+ proxy_settings=None):
+ """
+ Constructor
+ :param username: The user (required)
+ :param password: The user's password (required)
+ :param auth_url: The OpenStack cloud's authorization URL (required)
+ :param project_name: The project/tenant name
+ :param identity_api_version: The OpenStack's API version to use for Keystone clients
+ :param image_api_version: The OpenStack's API version to use for Glance clients
+ :param network_api_version: The OpenStack's API version to use for Neutron clients
+ :param compute_api_version: The OpenStack's API version to use for Nova clients
+ :param user_domain_id: Used for v3 APIs
+ :param project_domain_id: Used for v3 APIs
+ :param proxy_settings: instance of os_credentials.ProxySettings class
+ """
+ self.username = username
+ self.password = password
+ self.auth_url = auth_url
+ self.project_name = project_name
+ self.identity_api_version = identity_api_version
+ self.image_api_version = image_api_version
+ self.network_api_version = network_api_version
+ self.compute_api_version = compute_api_version
+ self.user_domain_id = user_domain_id
+ self.project_domain_id = project_domain_id
+ self.proxy_settings = proxy_settings
+
+ if self.proxy_settings and not isinstance(self.proxy_settings, ProxySettings):
+ raise Exception('proxy_settings must be an instance of the class ProxySettings')
+
+ if self.auth_url:
+ auth_url_tokens = self.auth_url.split('/')
+ last_token = auth_url_tokens[len(auth_url_tokens) - 1]
+ if len(last_token) == 0:
+ last_token = auth_url_tokens[len(auth_url_tokens) - 2]
+
+ if not last_token.startswith('v'):
+ raise Exception('auth_url last toke must start with \'v\'')
+
+ def __str__(self):
+ """Converts object to a string"""
+ return 'OSCreds - username=' + str(self.username) + \
+ ', password=' + str(self.password) + \
+ ', auth_url=' + str(self.auth_url) + \
+ ', project_name=' + str(self.project_name) + \
+ ', identity_api_version=' + str(self.identity_api_version) + \
+ ', image_api_version=' + str(self.image_api_version) + \
+ ', network_api_version=' + str(self.network_api_version) + \
+ ', compute_api_version=' + str(self.compute_api_version) + \
+ ', user_domain_id=' + str(self.user_domain_id) + \
+ ', proxy_settings=' + str(self.proxy_settings)
+
+
+class ProxySettings:
+ """
+ Represents the information required for sending traffic (HTTP & SSH) through a proxy
+ """
+
+ def __init__(self, host, port, ssh_proxy_cmd=None):
+ """
+ Constructor
+ :param host: the HTTP proxy host
+ :param port: the HTTP proxy port
+ :param ssh_proxy_cmd: the SSH proxy command string (optional)
+ """
+ # TODO - Add necessary fields here when adding support for secure proxies
+
+ self.host = host
+ self.port = port
+ self.ssh_proxy_cmd = ssh_proxy_cmd
+
+ if not self.host and not self.port:
+ raise Exception('host & port are required')
+
+ def __str__(self):
+ """Converts object to a string"""
+ return 'ProxySettings - host=' + str(self.host) + \
+ ', port=' + str(self.port) + \
+ ', ssh_proxy_cmd=' + str(self.ssh_proxy_cmd)
diff --git a/snaps/openstack/tests/__init__.py b/snaps/openstack/tests/__init__.py
new file mode 100644
index 0000000..e3e876e
--- /dev/null
+++ b/snaps/openstack/tests/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski'
diff --git a/snaps/openstack/tests/conf/os_env.yaml.template b/snaps/openstack/tests/conf/os_env.yaml.template
new file mode 100644
index 0000000..e88bfaa
--- /dev/null
+++ b/snaps/openstack/tests/conf/os_env.yaml.template
@@ -0,0 +1,17 @@
+# Keystone v2.0
+#username: admin
+#password: admin
+#os_auth_url: http://<host>:<port>/v2.0/
+#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
+#username: admin
+#password: admin
+#os_auth_url: http://<host>:<port>/v3
+#project_name: admin
+#identity_api_version: 3
+#ext_net: <external network name> \ No newline at end of file
diff --git a/snaps/openstack/tests/conf/overcloudrc_test b/snaps/openstack/tests/conf/overcloudrc_test
new file mode 100644
index 0000000..87746d8
--- /dev/null
+++ b/snaps/openstack/tests/conf/overcloudrc_test
@@ -0,0 +1,9 @@
+export NOVA_VERSION=1.1
+export OS_PASSWORD=test_pw
+export OS_AUTH_URL=http://foo:5000/v2.0/
+export OS_USERNAME=admin
+export OS_TENANT_NAME=admin
+export COMPUTE_API_VERSION=1.1
+export OS_NO_CACHE=True
+export OS_CLOUDNAME=undercloud
+export OS_IMAGE_API_VERSION=1 \ No newline at end of file
diff --git a/snaps/openstack/tests/create_flavor_tests.py b/snaps/openstack/tests/create_flavor_tests.py
new file mode 100644
index 0000000..7660665
--- /dev/null
+++ b/snaps/openstack/tests/create_flavor_tests.py
@@ -0,0 +1,319 @@
+# 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 uuid
+import unittest
+
+from snaps.openstack import create_flavor
+from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
+from snaps.openstack.utils import nova_utils
+
+__author__ = 'spisarski'
+
+
+class FlavorSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the FlavorSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ FlavorSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ FlavorSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ FlavorSettings(name='foo')
+
+ def test_config_with_name_only(self):
+ with self.assertRaises(Exception):
+ FlavorSettings(config={'name': 'foo'})
+
+ def test_name_ram_only(self):
+ with self.assertRaises(Exception):
+ FlavorSettings(name='foo', ram=1)
+
+ def test_config_with_name_ram_only(self):
+ with self.assertRaises(Exception):
+ FlavorSettings(config={'name': 'foo', 'ram': 1})
+
+ def test_name_ram_disk_only(self):
+ with self.assertRaises(Exception):
+ FlavorSettings(name='foo', ram=1, disk=1)
+
+ def test_config_with_name_ram_disk_only(self):
+ with self.assertRaises(Exception):
+ FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 1})
+
+ def test_ram_string(self):
+ with self.assertRaises(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ 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(Exception):
+ FlavorSettings(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 = FlavorSettings(name='foo', ram=1, disk=2, vcpus=3)
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('auto', settings.flavor_id)
+ self.assertEquals(1, settings.ram)
+ self.assertEquals(2, settings.disk)
+ self.assertEquals(3, settings.vcpus)
+ self.assertEquals(0, settings.ephemeral)
+ self.assertEquals(0, settings.swap)
+ self.assertEquals(1.0, settings.rxtx_factor)
+ self.assertEquals(True, settings.is_public)
+ self.assertEquals(create_flavor.DEFAULT_METADATA, settings.metadata)
+
+ def test_config_with_name_ram_disk_vcpus_only(self):
+ settings = FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('auto', settings.flavor_id)
+ self.assertEquals(1, settings.ram)
+ self.assertEquals(2, settings.disk)
+ self.assertEquals(3, settings.vcpus)
+ self.assertEquals(0, settings.ephemeral)
+ self.assertEquals(0, settings.swap)
+ self.assertEquals(1.0, settings.rxtx_factor)
+ self.assertEquals(True, settings.is_public)
+ self.assertEquals(create_flavor.DEFAULT_METADATA, settings.metadata)
+
+ def test_all(self):
+ metadata = {'foo': 'bar'}
+ settings = FlavorSettings(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.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.flavor_id)
+ self.assertEquals(1, settings.ram)
+ self.assertEquals(2, settings.disk)
+ self.assertEquals(3, settings.vcpus)
+ self.assertEquals(4, settings.ephemeral)
+ self.assertEquals(5, settings.swap)
+ self.assertEquals(6.0, settings.rxtx_factor)
+ self.assertEquals(False, settings.is_public)
+ self.assertEquals(metadata, settings.metadata)
+
+ def test_config_all(self):
+ metadata = {'foo': 'bar'}
+ settings = FlavorSettings(config={'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.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.flavor_id)
+ self.assertEquals(1, settings.ram)
+ self.assertEquals(2, settings.disk)
+ self.assertEquals(3, settings.vcpus)
+ self.assertEquals(4, settings.ephemeral)
+ self.assertEquals(5, settings.swap)
+ self.assertEquals(6.0, settings.rxtx_factor)
+ self.assertEquals(False, settings.is_public)
+ self.assertEquals(metadata, settings.metadata)
+
+
+class CreateFlavorTests(OSComponentTestCase):
+ """
+ Test for the CreateSecurityGroup class defined in create_security_group.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateSecurityGroup object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.flavor_name = guid + 'name'
+
+ self.nova = nova_utils.nova_client(self.os_creds)
+
+ # Initialize for cleanup
+ self.flavor_creator = None
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ if self.flavor_creator:
+ self.flavor_creator.clean()
+
+ def test_create_flavor(self):
+ """
+ Tests the creation of an OpenStack flavor.
+ """
+ # Create Flavor
+ flavor_settings = FlavorSettings(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(flavor_settings, flavor))
+
+ def test_create_flavor_existing(self):
+ """
+ Tests the creation of an OpenStack flavor then starts another creator to ensure it has not been done twice.
+ """
+ # Create Flavor
+ flavor_settings = FlavorSettings(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(flavor_settings, flavor))
+
+ flavor_creator_2 = OpenStackFlavor(self.os_creds, flavor_settings)
+ flavor2 = flavor_creator_2.create()
+
+ self.assertEquals(flavor.id, flavor2.id)
+
+ def test_create_clean_flavor(self):
+ """
+ Tests the creation and cleanup of an OpenStack flavor.
+ """
+ # Create Flavor
+ flavor_settings = FlavorSettings(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(flavor_settings, flavor))
+
+ # Clean Flavor
+ self.flavor_creator.clean()
+
+ self.assertIsNone(self.flavor_creator.get_flavor())
+ self.assertIsNone(nova_utils.get_flavor_by_name(self.nova, flavor_settings.name))
+
+ def test_create_delete_flavor(self):
+ """
+ Tests the creation of an OpenStack Security Group, the deletion, then cleanup to ensure clean() does not
+ raise any exceptions.
+ """
+ # Create Flavor
+ flavor_settings = FlavorSettings(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(flavor_settings, flavor))
+
+ # Delete Flavor
+ nova_utils.delete_flavor(self.nova, flavor)
+ self.assertIsNone(nova_utils.get_flavor_by_name(self.nova, flavor_settings.name))
+
+ # Attempt to cleanup
+ self.flavor_creator.clean()
+
+ self.assertIsNone(self.flavor_creator.get_flavor())
+
+ # TODO - Add more tests to exercise all configuration options
+
+
+def validate_flavor(flavor_settings, flavor):
+ """
+ Validates the flavor_settings against the OpenStack flavor object
+ :param flavor_settings: the settings used to create the flavor
+ :param flavor: the OpenStack flavor object
+ """
+ return flavor is not None \
+ and flavor_settings.name == flavor.name \
+ and flavor_settings.ram == flavor.ram \
+ and flavor_settings.disk == flavor.disk \
+ and flavor_settings.vcpus == flavor.vcpus
diff --git a/snaps/openstack/tests/create_image_tests.py b/snaps/openstack/tests/create_image_tests.py
new file mode 100644
index 0000000..24bf0f2
--- /dev/null
+++ b/snaps/openstack/tests/create_image_tests.py
@@ -0,0 +1,362 @@
+# 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 os
+import shutil
+import uuid
+import unittest
+
+from snaps import file_utils
+from snaps.openstack.create_image import ImageSettings
+
+import openstack_tests
+from snaps.openstack.utils import glance_utils, nova_utils
+from snaps.openstack import create_image
+from snaps.openstack import os_credentials
+from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
+
+__author__ = 'spisarski'
+
+
+class ImageSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the ImageSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ ImageSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ ImageSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(name='foo')
+
+ def test_config_with_name_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(config={'name': 'foo'})
+
+ def test_name_user_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(name='foo', image_user='bar')
+
+ def test_config_with_name_user_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(config={'name': 'foo', 'image_user': 'bar'})
+
+ def test_name_user_format_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(name='foo', image_user='bar', img_format='qcow2')
+
+ def test_config_with_name_user_format_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2'})
+
+ def test_name_user_format_url_file_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com',
+ image_file='/foo/bar.qcow')
+
+ def test_config_with_name_user_format_url_file_only(self):
+ with self.assertRaises(Exception):
+ ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+ 'download_url': 'http://foo.com', 'image_file': '/foo/bar.qcow'})
+
+ def test_name_user_format_url_only(self):
+ settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertEquals('http://foo.com', settings.url)
+ self.assertIsNone(settings.image_file)
+ self.assertIsNone(settings.nic_config_pb_loc)
+
+ def test_config_with_name_user_format_url_only(self):
+ settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+ 'download_url': 'http://foo.com'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertEquals('http://foo.com', settings.url)
+ self.assertIsNone(settings.image_file)
+ self.assertIsNone(settings.nic_config_pb_loc)
+
+ def test_name_user_format_file_only(self):
+ settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', image_file='/foo/bar.qcow')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertIsNone(settings.url)
+ self.assertEquals('/foo/bar.qcow', settings.image_file)
+ self.assertIsNone(settings.nic_config_pb_loc)
+
+ def test_config_with_name_user_format_file_only(self):
+ settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+ 'image_file': '/foo/bar.qcow'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertIsNone(settings.url)
+ self.assertEquals('/foo/bar.qcow', settings.image_file)
+ self.assertIsNone(settings.nic_config_pb_loc)
+
+ def test_all_url(self):
+ settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com',
+ nic_config_pb_loc='/foo/bar')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertEquals('http://foo.com', settings.url)
+ self.assertIsNone(settings.image_file)
+ self.assertEquals('/foo/bar', settings.nic_config_pb_loc)
+
+ def test_config_all_url(self):
+ settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+ 'download_url': 'http://foo.com', 'nic_config_pb_loc': '/foo/bar'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertEquals('http://foo.com', settings.url)
+ self.assertIsNone(settings.image_file)
+ self.assertEquals('/foo/bar', settings.nic_config_pb_loc)
+
+ def test_all_file(self):
+ settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', image_file='/foo/bar.qcow',
+ nic_config_pb_loc='/foo/bar')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertIsNone(settings.url)
+ self.assertEquals('/foo/bar.qcow', settings.image_file)
+ self.assertEquals('/foo/bar', settings.nic_config_pb_loc)
+
+ def test_config_all_file(self):
+ settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2',
+ 'image_file': '/foo/bar.qcow', 'nic_config_pb_loc': '/foo/bar'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.image_user)
+ self.assertEquals('qcow2', settings.format)
+ self.assertIsNone(settings.url)
+ self.assertEquals('/foo/bar.qcow', settings.image_file)
+ self.assertEquals('/foo/bar', settings.nic_config_pb_loc)
+
+
+class CreateImageSuccessTests(OSIntegrationTestCase):
+ """
+ Test for the CreateImage class defined in create_image.py
+ """
+
+ 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 = uuid.uuid4()
+ self.image_name = self.__class__.__name__ + '-' + str(guid)
+
+ self.nova = nova_utils.nova_client(self.os_creds)
+ self.glance = glance_utils.glance_client(self.os_creds)
+
+ self.tmp_dir = 'tmp/' + str(guid)
+ if not os.path.exists(self.tmp_dir):
+ os.makedirs(self.tmp_dir)
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ if self.image_creator:
+ self.image_creator.clean()
+
+ if os.path.exists(self.tmp_dir) and os.path.isdir(self.tmp_dir):
+ shutil.rmtree(self.tmp_dir)
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_image_clean_url(self):
+ """
+ Tests the creation of an OpenStack image from a URL.
+ """
+ # Create Image
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings)
+
+ created_image = self.image_creator.create()
+ self.assertIsNotNone(created_image)
+
+ retrieved_image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name)
+ self.assertIsNotNone(retrieved_image)
+
+ self.assertEquals(created_image.name, retrieved_image.name)
+ self.assertEquals(created_image.id, retrieved_image.id)
+
+ def test_create_image_clean_file(self):
+ """
+ Tests the creation of an OpenStack image from a file.
+ """
+ url_image_settings = openstack_tests.cirros_url_image('foo')
+ image_file = file_utils.download(url_image_settings.url, self.tmp_dir)
+ file_image_settings = openstack_tests.file_image_test_settings(name=self.image_name, file_path=image_file.name)
+ self.image_creator = create_image.OpenStackImage(self.os_creds, file_image_settings)
+
+ self.image = self.image_creator.create()
+ self.assertIsNotNone(self.image)
+ self.assertEqual(self.image_name, self.image.name)
+
+ created_image = self.image_creator.create()
+ self.assertIsNotNone(created_image)
+
+ retrieved_image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name)
+ self.assertIsNotNone(retrieved_image)
+
+ self.assertEquals(created_image.name, retrieved_image.name)
+ self.assertEquals(created_image.id, retrieved_image.id)
+
+ def test_create_delete_image(self):
+ """
+ Tests the creation then deletion of an OpenStack image to ensure clean() does not raise an Exception.
+ """
+ # Create Image
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings)
+ created_image = self.image_creator.create()
+ self.assertIsNotNone(created_image)
+
+ # Delete Image manually
+ glance_utils.delete_image(self.glance, created_image)
+
+ self.assertIsNone(glance_utils.get_image(self.nova, self.glance, self.image_creator.image_settings.name))
+
+ # Must not throw an exception when attempting to cleanup non-existent image
+ self.image_creator.clean()
+ self.assertIsNone(self.image_creator.get_image())
+
+ def test_create_same_image(self):
+ """
+ Tests the creation of an OpenStack image when the image already exists.
+ """
+ # Create Image
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings)
+ image1 = self.image_creator.create()
+ # Should be retrieving the instance data
+ os_image_2 = create_image.OpenStackImage(self.os_creds, os_image_settings)
+ image2 = os_image_2.create()
+ self.assertEquals(image1.id, image2.id)
+
+
+class CreateImageNegativeTests(OSIntegrationTestCase):
+ """
+ Negative test cases for the CreateImage class
+ """
+
+ def setUp(self):
+ super(self.__class__, self).__start__()
+
+ self.image_name = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.image_creator = None
+
+ def tearDown(self):
+ if self.image_creator:
+ self.image_creator.clean()
+
+ super(self.__class__, self).__clean__()
+
+ def test_none_image_name(self):
+ """
+ Expect an exception when the image name is None
+ """
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ with self.assertRaises(Exception):
+ self.image_creator = create_image.OpenStackImage(
+ self.os_creds, create_image.ImageSettings(
+ name=None, image_user=os_image_settings.image_user, img_format=os_image_settings.format,
+ url=os_image_settings.url))
+
+ self.fail('Exception should have been thrown prior to this line')
+
+ def test_bad_image_url(self):
+ """
+ Expect an exception when the image download url is bad
+ """
+ os_image_settings = openstack_tests.cirros_url_image(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"))
+ with self.assertRaises(Exception):
+ self.image_creator.create()
+
+ def test_bad_image_file(self):
+ """
+ Expect an exception when the image file does not exist
+ """
+ os_image_settings = openstack_tests.cirros_url_image(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"))
+ with self.assertRaises(Exception):
+ self.image_creator.create()
+
+ def test_none_proj_name(self):
+ """
+ Expect an exception when the project name is None
+ """
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ with self.assertRaises(Exception):
+ self.image_creator = create_image.OpenStackImage(
+ os_credentials.OSCreds(self.os_creds.username, self.os_creds.password, self.os_creds.auth_url, None,
+ proxy_settings=self.os_creds.proxy_settings),
+ os_image_settings)
+ self.image_creator.create()
+
+ def test_none_auth_url(self):
+ """
+ Expect an exception when the project name is None
+ """
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ with self.assertRaises(Exception):
+ self.image_creator = create_image.OpenStackImage(
+ os_credentials.OSCreds(self.os_creds.username, self.os_creds.password, None,
+ self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings),
+ os_image_settings)
+ self.image_creator.create()
+
+ def test_none_password(self):
+ """
+ Expect an exception when the project name is None
+ """
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ with self.assertRaises(Exception):
+ self.image_creator = create_image.OpenStackImage(
+ os_credentials.OSCreds(self.os_creds.username, None, self.os_creds.os_auth_url,
+ self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings),
+ os_image_settings)
+
+ def test_none_user(self):
+ """
+ Expect an exception when the project name is None
+ """
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+ with self.assertRaises(Exception):
+ self.image_creator = create_image.OpenStackImage(
+ os_credentials.OSCreds(None, self.os_creds.password, self.os_creds.os_auth_url,
+ self.os_creds.project_name,
+ proxy_settings=self.os_creds.proxy_settings),
+ os_image_settings)
diff --git a/snaps/openstack/tests/create_instance_tests.py b/snaps/openstack/tests/create_instance_tests.py
new file mode 100644
index 0000000..756b45f
--- /dev/null
+++ b/snaps/openstack/tests/create_instance_tests.py
@@ -0,0 +1,1474 @@
+# 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 logging
+import os
+import time
+import unittest
+import uuid
+
+from snaps.openstack.create_instance import VmInstanceSettings, OpenStackVmInstance, FloatingIpSettings
+from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings
+from snaps.openstack.create_keypairs import OpenStackKeypair, KeypairSettings
+from snaps.openstack.create_network import OpenStackNetwork, PortSettings
+from snaps.openstack.create_router import OpenStackRouter
+from snaps.openstack.create_image import OpenStackImage
+from snaps.openstack.create_security_group import SecurityGroupSettings, OpenStackSecurityGroup
+from snaps.openstack.tests import openstack_tests, validation_utils
+from snaps.openstack.utils import nova_utils
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase, OSIntegrationTestCase
+
+__author__ = 'spisarski'
+
+VM_BOOT_TIMEOUT = 600
+
+logger = logging.getLogger('create_instance_tests')
+
+
+class VmInstanceSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the VmInstanceSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ VmInstanceSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ VmInstanceSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ VmInstanceSettings(name='foo')
+
+ def test_config_with_name_only(self):
+ with self.assertRaises(Exception):
+ VmInstanceSettings(config={'name': 'foo'})
+
+ def test_name_flavor_only(self):
+ settings = VmInstanceSettings(name='foo', flavor='bar')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.flavor)
+ self.assertEquals(0, len(settings.port_settings))
+ self.assertEquals(0, len(settings.security_group_names))
+ self.assertEquals(0, len(settings.floating_ip_settings))
+ self.assertIsNone(settings.sudo_user)
+ self.assertEquals(900, settings.vm_boot_timeout)
+ self.assertEquals(300, settings.vm_delete_timeout)
+ self.assertEquals(180, settings.ssh_connect_timeout)
+ self.assertIsNone(settings.availability_zone)
+
+ def test_config_with_name_flavor_only(self):
+ settings = VmInstanceSettings(config={'name': 'foo', 'flavor': 'bar'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.flavor)
+ self.assertEquals(0, len(settings.port_settings))
+ self.assertEquals(0, len(settings.security_group_names))
+ self.assertEquals(0, len(settings.floating_ip_settings))
+ self.assertIsNone(settings.sudo_user)
+ self.assertEquals(900, settings.vm_boot_timeout)
+ self.assertEquals(300, settings.vm_delete_timeout)
+ self.assertEquals(180, settings.ssh_connect_timeout)
+ self.assertIsNone(settings.availability_zone)
+
+ def test_all(self):
+ port_settings = PortSettings(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')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.flavor)
+ self.assertEquals(1, len(settings.port_settings))
+ self.assertEquals('foo-port', settings.port_settings[0].name)
+ self.assertEquals('bar-net', settings.port_settings[0].network_name)
+ self.assertEquals(1, len(settings.security_group_names))
+ self.assertEquals('sec_grp_1', settings.security_group_names[0])
+ self.assertEquals(1, len(settings.floating_ip_settings))
+ self.assertEquals('foo-fip', settings.floating_ip_settings[0].name)
+ self.assertEquals('bar-port', settings.floating_ip_settings[0].port_name)
+ self.assertEquals('foo-bar-router', settings.floating_ip_settings[0].router_name)
+ self.assertEquals('joe', settings.sudo_user)
+ self.assertEquals(999, settings.vm_boot_timeout)
+ self.assertEquals(333, settings.vm_delete_timeout)
+ self.assertEquals(111, settings.ssh_connect_timeout)
+ self.assertEquals('server name', settings.availability_zone)
+
+ def test_config_all(self):
+ port_settings = PortSettings(name='foo-port', network_name='bar-net')
+ fip_settings = FloatingIpSettings(name='foo-fip', port_name='bar-port', router_name='foo-bar-router')
+
+ settings = VmInstanceSettings(config={'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, 'availability_zone': 'server name'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.flavor)
+ self.assertEquals(1, len(settings.port_settings))
+ self.assertEquals('foo-port', settings.port_settings[0].name)
+ self.assertEquals('bar-net', settings.port_settings[0].network_name)
+ self.assertEquals(1, len(settings.security_group_names))
+ self.assertEquals(1, len(settings.floating_ip_settings))
+ self.assertEquals('foo-fip', settings.floating_ip_settings[0].name)
+ self.assertEquals('bar-port', settings.floating_ip_settings[0].port_name)
+ self.assertEquals('foo-bar-router', settings.floating_ip_settings[0].router_name)
+ self.assertEquals('joe', settings.sudo_user)
+ self.assertEquals(999, settings.vm_boot_timeout)
+ self.assertEquals(333, settings.vm_delete_timeout)
+ self.assertEquals(111, settings.ssh_connect_timeout)
+ self.assertEquals('server name', settings.availability_zone)
+
+
+class FloatingIpSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the FloatingIpSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings(name='foo')
+
+ def test_config_with_name_only(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings(config={'name': 'foo'})
+
+ def test_name_port_only(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings(name='foo', port_name='bar')
+
+ def test_config_with_name_port_only(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings(config={'name': 'foo', 'port_name': 'bar'})
+
+ def test_name_router_only(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings(name='foo', router_name='bar')
+
+ def test_config_with_name_router_only(self):
+ with self.assertRaises(Exception):
+ FloatingIpSettings(config={'name': 'foo', 'router_name': 'bar'})
+
+ def test_name_port_router_only(self):
+ settings = FloatingIpSettings(name='foo', port_name='foo-port', router_name='bar-router')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('foo-port', settings.port_name)
+ self.assertEquals('bar-router', settings.router_name)
+ self.assertIsNone(settings.subnet_name)
+ self.assertTrue(settings.provisioning)
+
+ def test_config_with_name_port_router_only(self):
+ settings = FloatingIpSettings(config={'name': 'foo', 'port_name': 'foo-port', 'router_name': 'bar-router'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('foo-port', settings.port_name)
+ self.assertEquals('bar-router', settings.router_name)
+ self.assertIsNone(settings.subnet_name)
+ self.assertTrue(settings.provisioning)
+
+ def test_all(self):
+ settings = FloatingIpSettings(name='foo', port_name='foo-port', router_name='bar-router',
+ subnet_name='bar-subnet', provisioning=False)
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('foo-port', settings.port_name)
+ self.assertEquals('bar-router', settings.router_name)
+ self.assertEquals('bar-subnet', settings.subnet_name)
+ self.assertFalse(settings.provisioning)
+
+ def test_config_all(self):
+ settings = FloatingIpSettings(config={'name': 'foo', 'port_name': 'foo-port', 'router_name': 'bar-router',
+ 'subnet_name': 'bar-subnet', 'provisioning': False})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('foo-port', settings.port_name)
+ self.assertEquals('bar-router', settings.router_name)
+ self.assertEquals('bar-subnet', settings.subnet_name)
+ self.assertFalse(settings.provisioning)
+
+
+class SimpleHealthCheck(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__()
+
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.keypair_priv_filepath = 'tmp/' + guid
+ self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub'
+ self.keypair_name = guid + '-kp'
+ self.vm_inst_name = guid + '-inst'
+ self.port_1_name = guid + 'port-1'
+ self.port_2_name = guid + 'port-2'
+ self.floating_ip_name = guid + 'fip1'
+
+ # Initialize for tearDown()
+ self.image_creator = None
+ self.network_creator = None
+ self.flavor_creator = None
+ 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(
+ name=self.port_1_name, network_name=self.priv_net_config.network_settings.name)
+
+ self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image')
+
+ try:
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings)
+ self.image_creator.create()
+
+ # Create Network
+ self.network_creator = OpenStackNetwork(self.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=1024, disk=10, vcpus=1))
+ self.flavor_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 - ' + e.message)
+
+ 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()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning flavor with message - ' + e.message)
+
+ if self.network_creator:
+ try:
+ self.network_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning network with message - ' + e.message)
+
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning image with message - ' + e.message)
+
+ super(self.__class__, self).__clean__()
+
+ def test_check_vm_ip_dhcp(self):
+ """
+ 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(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[self.port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ vm = self.inst_creator.create()
+
+ ip = self.inst_creator.get_port_ip(self.port_settings.name)
+ self.assertIsNotNone(ip)
+
+ self.assertTrue(self.inst_creator.vm_active(block=True))
+
+ found = False
+ timeout = 100
+ start_time = time.time()
+ match_value = 'Lease of ' + ip + ' obtained,'
+
+ while timeout > time.time() - start_time:
+ output = vm.get_console_output()
+ if match_value in output:
+ found = True
+ break
+ self.assertTrue(found)
+
+
+class CreateInstanceSimpleTests(OSIntegrationTestCase):
+ """
+ Simple instance creation tests without any other objects
+ """
+ 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.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image')
+
+ # Initialize for tearDown()
+ self.image_creator = None
+ self.flavor_creator = None
+ self.inst_creator = None
+
+ try:
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings)
+ self.image_creator.create()
+ # Create Flavor
+ self.flavor_creator = OpenStackFlavor(
+ self.admin_os_creds,
+ FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2))
+ self.flavor_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 - ' + e.message)
+
+ if self.flavor_creator:
+ try:
+ self.flavor_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning flavor with message - ' + e.message)
+
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning image with message - ' + e.message)
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_delete_instance(self):
+ """
+ Tests the creation of an OpenStack instance with a single port with a static IP without a Floating IP.
+ """
+ instance_settings = VmInstanceSettings(name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name)
+
+ self.inst_creator = OpenStackVmInstance(
+ self.os_creds, instance_settings, self.image_creator.image_settings)
+
+ vm_inst = self.inst_creator.create()
+ self.assertEquals(1, len(nova_utils.get_servers_by_name(self.nova, instance_settings.name)))
+
+ # Delete instance
+ nova_utils.delete_vm_instance(self.nova, vm_inst)
+
+ self.assertTrue(self.inst_creator.vm_deleted(block=True))
+ self.assertEquals(0, len(nova_utils.get_servers_by_name(self.nova, instance_settings.name)))
+
+ # Exception should not be thrown
+ self.inst_creator.clean()
+
+
+class CreateInstanceSingleNetworkTests(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__()
+
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.keypair_priv_filepath = 'tmp/' + guid
+ self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub'
+ self.keypair_name = guid + '-kp'
+ self.vm_inst_name = guid + '-inst'
+ self.port_1_name = guid + 'port-1'
+ self.port_2_name = guid + 'port-2'
+ self.floating_ip_name = guid + 'fip1'
+
+ # Initialize for tearDown()
+ self.image_creator = None
+ self.network_creator = None
+ self.router_creator = None
+ self.flavor_creator = None
+ self.keypair_creator = None
+ self.inst_creators = list()
+
+ 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)
+ self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image')
+
+ try:
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings)
+ self.image_creator.create()
+
+ # Create Network
+ self.network_creator = OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings)
+ self.network_creator.create()
+
+ # Create Router
+ self.router_creator = OpenStackRouter(self.os_creds, self.pub_net_config.router_settings)
+ self.router_creator.create()
+
+ # Create Flavor
+ self.flavor_creator = OpenStackFlavor(
+ self.admin_os_creds,
+ FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2))
+ self.flavor_creator.create()
+
+ 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()
+ 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 - ' + e.message)
+
+ if self.keypair_creator:
+ try:
+ self.keypair_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning keypair with message - ' + e.message)
+
+ 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()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning flavor with message - ' + e.message)
+
+ if self.router_creator:
+ try:
+ self.router_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning router with message - ' + e.message)
+
+ if self.network_creator:
+ try:
+ self.network_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning network with message - ' + e.message)
+
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning image with message - ' + e.message)
+
+ super(self.__class__, self).__clean__()
+
+ def test_single_port_static(self):
+ """
+ Tests the creation of an OpenStack instance with a single port with a static IP without a Floating IP.
+ """
+ ip_1 = '10.55.1.100'
+
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.pub_net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.pub_net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}])
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_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)])
+
+ 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)
+ vm_inst = inst_creator.create()
+
+ self.assertEquals(ip_1, inst_creator.get_port_ip(self.port_1_name))
+ self.assertTrue(inst_creator.vm_active(block=True))
+ self.assertEquals(vm_inst, inst_creator.get_vm_inst())
+
+ def test_ssh_client_fip_before_active(self):
+ """
+ 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(
+ name=self.port_1_name, network_name=self.pub_net_config.network_settings.name)
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_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)])
+
+ 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)
+ vm_inst = inst_creator.create()
+ self.assertIsNotNone(vm_inst)
+
+ self.assertTrue(inst_creator.vm_active(block=True))
+ self.assertEquals(vm_inst, inst_creator.get_vm_inst())
+
+ validate_ssh_client(inst_creator)
+
+ def test_ssh_client_fip_after_active(self):
+ """
+ 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(
+ name=self.port_1_name, network_name=self.pub_net_config.network_settings.name)
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_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)])
+
+ 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))
+ self.assertEquals(vm_inst, inst_creator.get_vm_inst())
+
+ validate_ssh_client(inst_creator)
+
+ # TODO - Determine how allowed_address_pairs is supposed to operate before continuing this test
+ # see http://docs.openstack.org/developer/dragonflow/specs/allowed_address_pairs.html for a functional description
+ # def test_allowed_address_port_access(self):
+ # """
+ # Tests to ensure that setting allowed_address_pairs on a port functions as designed
+ # """
+ # port_settings_1 = PortSettings(
+ # name=self.port_1_name + '-1', network_name=self.pub_net_config.network_settings.name)
+ #
+ # instance_settings_1 = VmInstanceSettings(
+ # name=self.vm_inst_name + '-1', flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings_1],
+ # floating_ip_settings=[FloatingIpSettings(
+ # name=self.floating_ip_name + '-1', port_name=port_settings_1.name,
+ # router_name=self.pub_net_config.router_settings.name)])
+ #
+ # inst_creator_1 = OpenStackVmInstance(
+ # self.os_creds, instance_settings_1, self.image_creator.image_settings,
+ # keypair_settings=self.keypair_creator.keypair_settings)
+ # self.inst_creators.append(inst_creator_1)
+ #
+ # # block=True will force the create() method to block until the
+ # vm_inst_1 = inst_creator_1.create(block=True)
+ # self.assertIsNotNone(vm_inst_1)
+ #
+ # port_settings_1 = PortSettings(
+ # name=self.port_1_name + '-1', network_name=self.pub_net_config.network_settings.name)
+ #
+ # instance_settings_1 = VmInstanceSettings(
+ # name=self.vm_inst_name + '-1', flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings_1],
+ # floating_ip_settings=[FloatingIpSettings(
+ # name=self.floating_ip_name + '-1', port_name=port_settings_1.name,
+ # router_name=self.pub_net_config.router_settings.name)])
+ #
+ # inst_creator_1 = OpenStackVmInstance(
+ # self.os_creds, instance_settings_1, self.image_creator.image_settings,
+ # keypair_settings=self.keypair_creator.keypair_settings)
+ # self.inst_creators.append(inst_creator_1)
+ # inst_creator_1.create(block=True)
+ #
+ # ip = inst_creator_1.get_port_ip(port_settings_1.name,
+ # subnet_name=self.pub_net_config.network_settings.subnet_settings[0].name)
+ # self.assertIsNotNone(ip)
+ # mac_addr = inst_creator_1.get_port_mac(port_settings_1.name)
+ # self.assertIsNotNone(mac_addr)
+ #
+ # allowed_address_pairs = [{'ip_address': ip, 'mac_address': mac_addr}]
+ #
+ # # Create VM that can be accessed by vm_inst_1
+ # port_settings_2 = PortSettings(
+ # name=self.port_1_name + '-2', network_name=self.pub_net_config.network_settings.name,
+ # allowed_address_pairs=allowed_address_pairs)
+ #
+ # instance_settings_2 = VmInstanceSettings(
+ # name=self.vm_inst_name + '-2', flavor=self.flavor_creator.flavor_settings.name,
+ # port_settings=[port_settings_2])
+ #
+ # inst_creator_2 = OpenStackVmInstance(
+ # self.os_creds, instance_settings_2, self.image_creator.image_settings)
+ # self.inst_creators.append(inst_creator_2)
+ # inst_creator_2.create(block=True)
+ #
+ # # Create VM that cannot be accessed by vm_inst_1
+ # ip = '10.55.0.101'
+ # mac_addr = '0a:1b:2c:3d:4e:5f'
+ # invalid_address_pairs = [{'ip_address': ip, 'mac_address': mac_addr}]
+ #
+ # port_settings_3 = PortSettings(
+ # name=self.port_1_name + '-3', network_name=self.pub_net_config.network_settings.name,
+ # allowed_address_pairs=invalid_address_pairs)
+ #
+ # instance_settings_3 = VmInstanceSettings(
+ # name=self.vm_inst_name + '-3', flavor=self.flavor_creator.flavor_settings.name,
+ # port_settings=[port_settings_3])
+ #
+ # inst_creator_3 = OpenStackVmInstance(
+ # self.os_creds, instance_settings_3, self.image_creator.image_settings)
+ # self.inst_creators.append(inst_creator_3)
+ # inst_creator_3.create(block=True)
+ #
+ # print 'foo'
+ # I expected that this feature would block/allow traffic from specific endpoints (VMs). In this case, I would expect
+ # inst_1 to be able to access inst_2 but not inst_3; however, they all can access each other.
+ # TODO - Add validation
+
+
+class CreateInstancePortManipulationTests(OSIntegrationTestCase):
+ """
+ Test for the CreateInstance class with a single NIC/Port where mac and IP values are manually set
+ """
+
+ 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.port_1_name = guid + 'port-1'
+ self.port_2_name = guid + 'port-2'
+ self.floating_ip_name = guid + 'fip1'
+
+ # Initialize for tearDown()
+ self.image_creator = None
+ self.network_creator = None
+ self.flavor_creator = None
+ self.inst_creator = None
+
+ 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)
+ self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image')
+
+ try:
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings)
+ self.image_creator.create()
+
+ # Create Network
+ self.network_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings)
+ self.network_creator.create()
+
+ # Create Flavor
+ self.flavor_creator = OpenStackFlavor(
+ self.admin_os_creds,
+ FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2))
+ self.flavor_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 - ' + e.message)
+
+ if self.flavor_creator:
+ try:
+ self.flavor_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning flavor with message - ' + e.message)
+
+ if self.network_creator:
+ try:
+ self.network_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning network with message - ' + e.message)
+
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning image with message - ' + e.message)
+
+ super(self.__class__, self).__clean__()
+
+ def test_set_custom_valid_ip_one_subnet(self):
+ """
+ Tests the creation of an OpenStack instance with a single port with a static IP on a network with one subnet.
+ """
+ ip = '10.55.0.101'
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}])
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ self.inst_creator.create()
+
+ self.assertEquals(ip, self.inst_creator.get_port_ip(
+ self.port_1_name, subnet_name=self.net_config.network_settings.subnet_settings[0].name))
+
+ def test_set_custom_invalid_ip_one_subnet(self):
+ """
+ Tests the creation of an OpenStack instance with a single port with a static IP on a network with one subnet.
+ """
+ ip = '10.66.0.101'
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}])
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+
+ with self.assertRaises(Exception):
+ self.inst_creator.create()
+
+ def test_set_custom_valid_mac(self):
+ """
+ Tests the creation of an OpenStack instance with a single port where the MAC address is assigned.
+ """
+ mac_addr = '0a:1b:2c:3d:4e:5f'
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address=mac_addr)
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ self.inst_creator.create()
+
+ self.assertEquals(mac_addr, self.inst_creator.get_port_mac(self.port_1_name))
+
+ def test_set_custom_invalid_mac(self):
+ """
+ Tests the creation of an OpenStack instance with a single port where an invalid MAC address value is being
+ assigned. This should raise an Exception
+ """
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address='foo')
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(
+ self.os_creds, instance_settings, self.image_creator.image_settings)
+
+ with self.assertRaises(Exception):
+ self.inst_creator.create()
+
+ def test_set_custom_mac_and_ip(self):
+ """
+ Tests the creation of an OpenStack instance with a single port where the IP and MAC address is assigned.
+ """
+ ip = '10.55.0.101'
+ mac_addr = '0a:1b:2c:3d:4e:5f'
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address=mac_addr,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}])
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ self.inst_creator.create()
+
+ self.assertEquals(ip, self.inst_creator.get_port_ip(
+ self.port_1_name, subnet_name=self.net_config.network_settings.subnet_settings[0].name))
+ self.assertEquals(mac_addr, self.inst_creator.get_port_mac(self.port_1_name))
+
+ def test_set_allowed_address_pairs(self):
+ """
+ Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set.
+ """
+ ip = '10.55.0.101'
+ mac_addr = '0a:1b:2c:3d:4e:5f'
+ pair = {'ip_address': ip, 'mac_address': mac_addr}
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair])
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ self.inst_creator.create()
+
+ port = self.inst_creator.get_port_by_name(port_settings.name)
+ self.assertIsNotNone(port)
+ self.assertIsNotNone(port['port'].get('allowed_address_pairs'))
+ self.assertEquals(1, len(port['port']['allowed_address_pairs']))
+ validation_utils.objects_equivalent(pair, port['port']['allowed_address_pairs'][0])
+
+ def test_set_allowed_address_pairs_bad_mac(self):
+ """
+ Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set with an
+ invalid MAC address.
+ """
+ ip = '10.55.0.101'
+ mac_addr = 'foo'
+ pair = {'ip_address': ip, 'mac_address': mac_addr}
+ pairs = set()
+ pairs.add((ip, mac_addr))
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair])
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ with self.assertRaises(Exception):
+ self.inst_creator.create()
+
+ def test_set_allowed_address_pairs_bad_ip(self):
+ """
+ Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set with an
+ invalid MAC address.
+ """
+ ip = 'foo'
+ mac_addr = '0a:1b:2c:3d:4e:5f'
+ pair = {'ip_address': ip, 'mac_address': mac_addr}
+ pairs = set()
+ pairs.add((ip, mac_addr))
+ port_settings = PortSettings(
+ name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair])
+
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings])
+
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ with self.assertRaises(Exception):
+ self.inst_creator.create()
+
+
+class CreateInstanceOnComputeHost(OSComponentTestCase):
+ """
+ Test for the CreateInstance where one VM is deployed to each compute node
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.vm_inst_name = guid + '-inst'
+ 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')
+
+ self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image')
+
+ try:
+ # Create Network
+ self.network_creator = OpenStackNetwork(self.os_creds, self.priv_net_config.network_settings)
+ self.network_creator.create()
+
+ # Create Flavor
+ self.flavor_creator = OpenStackFlavor(
+ self.os_creds,
+ FlavorSettings(name=guid + '-flavor-name', ram=512, disk=1, vcpus=1))
+ self.flavor_creator.create()
+
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds, self.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 - ' + e.message)
+
+ if self.flavor_creator:
+ try:
+ self.flavor_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning flavor with message - ' + e.message)
+
+ if self.network_creator:
+ try:
+ self.network_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning network with message - ' + e.message)
+
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning image with message - ' + e.message)
+
+ 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.os_creds)
+ zones = nova_utils.get_nova_availability_zones(nova)
+
+ # Create Instance on each server/zone
+ ctr = 0
+ for zone in zones:
+ 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.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 zones:
+ creator = self.inst_creators[index]
+ self.assertTrue(creator.vm_active(block=True))
+ vm = creator.get_vm_inst()
+ deployed_zone = vm._info['OS-EXT-AZ:availability_zone']
+ deployed_host = vm._info['OS-EXT-SRV-ATTR:host']
+ self.assertEquals(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__()
+
+ # Initialize for tearDown()
+ self.image_creator = None
+ self.network_creators = list()
+ self.router_creators = list()
+ self.flavor_creator = None
+ self.keypair_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())
+ self.os_image_settings = openstack_tests.centos_url_image(name=image_name)
+
+ try:
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings)
+ self.image_creator.create()
+
+ # 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.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))
+
+ # Create Routers
+ for router_creator in self.router_creators:
+ router_creator.create()
+
+ # Create Flavor
+ self.flavor_creator = OpenStackFlavor(
+ self.admin_os_creds,
+ FlavorSettings(name=self.guid + '-flavor-name', ram=2048, disk=10, vcpus=2))
+ 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()
+ except Exception as e:
+ self.tearDown()
+ raise Exception(e.message)
+
+ def tearDown(self):
+ """
+ Cleans the created objects
+ """
+ if self.inst_creator:
+ try:
+ self.inst_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning VM instance with message - ' + e.message)
+
+ if self.keypair_creator:
+ try:
+ self.keypair_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning keypair with message - ' + e.message)
+
+ 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()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning flavor with message - ' + e.message)
+
+ for router_creator in self.router_creators:
+ try:
+ router_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning router with message - ' + e.message)
+
+ for network_creator in self.network_creators:
+ try:
+ network_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning network with message - ' + e.message)
+
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning image with message - ' + e.message)
+
+ super(self.__class__, self).__clean__()
+
+ def test_dual_ports_dhcp(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.
+ """
+ # 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.assertEquals(vm_inst, self.inst_creator.get_vm_inst())
+
+ # Effectively blocks until VM has been properly activated
+ self.assertTrue(self.inst_creator.vm_active(block=True))
+
+ # Effectively blocks until VM's ssh port has been opened
+ self.assertTrue(self.inst_creator.vm_ssh_active(block=True))
+
+ self.inst_creator.config_nics()
+
+ # TODO - *** ADD VALIDATION HERE ***
+ # TODO - Add validation that both floating IPs work
+ # TODO - Add tests where only one NIC has a floating IP
+ # TODO - Add tests where one attempts to place a floating IP on a network/router without an external gateway
+
+
+class InstanceSecurityGroupTests(OSIntegrationTestCase):
+ """
+ Tests that include, add, and remove security groups from VM instances
+ """
+ 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.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.vm_inst_name = self.guid + '-inst'
+ self.nova = nova_utils.nova_client(self.os_creds)
+ self.os_image_settings = openstack_tests.cirros_url_image(name=self.guid + '-image')
+
+ 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.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)
+
+ # Initialize for tearDown()
+ self.image_creator = None
+ self.keypair_creator = None
+ self.flavor_creator = None
+ self.network_creator = None
+ self.router_creator = None
+ self.inst_creator = None
+ self.sec_grp_creators = list()
+
+ try:
+ # Create Image
+ self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings)
+ self.image_creator.create()
+
+ # Create Network
+ self.network_creator = OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings)
+ self.network_creator.create()
+
+ # Create Router
+ self.router_creator = OpenStackRouter(self.os_creds, self.pub_net_config.router_settings)
+ self.router_creator.create()
+
+ # Create Flavor
+ self.flavor_creator = OpenStackFlavor(
+ self.admin_os_creds,
+ FlavorSettings(name=self.guid + '-flavor-name', ram=2048, disk=10, vcpus=2))
+ self.flavor_creator.create()
+
+ 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()
+ 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 - ' + e.message)
+
+ for sec_grp_creator in self.sec_grp_creators:
+ try:
+ sec_grp_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning security group with message - ' + e.message)
+
+ if self.keypair_creator:
+ try:
+ self.keypair_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning keypair with message - ' + e.message)
+
+ 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()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning flavor with message - ' + e.message)
+
+ if self.router_creator:
+ try:
+ self.router_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning router with message - ' + e.message)
+
+ if self.network_creator:
+ try:
+ self.network_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning network with message - ' + e.message)
+
+ if self.image_creator:
+ try:
+ self.image_creator.clean()
+ except Exception as e:
+ logger.error('Unexpected exception cleaning image with message - ' + e.message)
+
+ super(self.__class__, self).__clean__()
+
+ def test_add_security_group(self):
+ """
+ Tests the addition of a security group created after the instance.
+ """
+ # Create instance
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name)
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ vm_inst = self.inst_creator.create()
+ 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_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ sec_grp = sec_grp_creator.create()
+ self.sec_grp_creators.append(sec_grp_creator)
+
+ # Check that group has not been added
+ self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name))
+
+ # Add security group to instance after activated
+ self.inst_creator.add_security_group(sec_grp)
+
+ # Validate that security group has been added
+ self.assertTrue(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name))
+
+ def test_add_invalid_security_group(self):
+ """
+ Tests the addition of a security group that no longer exists.
+ """
+ # Create instance
+ instance_settings = VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name)
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ vm_inst = self.inst_creator.create()
+ 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_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ sec_grp = sec_grp_creator.create()
+ sec_grp_creator.clean()
+ self.sec_grp_creators.append(sec_grp_creator)
+
+ # Check that group has not been added
+ self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name))
+
+ # Add security group to instance after activated
+ self.assertFalse(self.inst_creator.add_security_group(sec_grp))
+
+ # Validate that security group has been added
+ self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name))
+
+ def test_remove_security_group(self):
+ """
+ Tests the removal of a security group created before and added to the instance.
+ """
+ # Create security group object to add to instance
+ sec_grp_settings = SecurityGroupSettings(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(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name,
+ security_group_names=[sec_grp_settings.name])
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ vm_inst = self.inst_creator.create()
+ self.assertIsNotNone(vm_inst)
+
+ # Check that group has been added
+ self.assertTrue(inst_has_sec_grp(vm_inst, sec_grp_settings.name))
+
+ # Add security group to instance after activated
+ self.assertTrue(self.inst_creator.remove_security_group(sec_grp))
+
+ # Validate that security group has been added
+ self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name))
+
+ def test_remove_security_group_never_added(self):
+ """
+ Tests the removal of a security group that was never added in the first place.
+ """
+ # Create security group object to add to instance
+ sec_grp_settings = SecurityGroupSettings(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(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name)
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ vm_inst = self.inst_creator.create()
+ self.assertIsNotNone(vm_inst)
+
+ # Check that group has been added
+ self.assertFalse(inst_has_sec_grp(vm_inst, sec_grp_settings.name))
+
+ # Add security group to instance after activated
+ self.assertFalse(self.inst_creator.remove_security_group(sec_grp))
+
+ # Validate that security group has been added
+ self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name))
+
+ def test_add_same_security_group(self):
+ """
+ Tests the addition of a security group created before add added to the instance.
+ """
+ # Create security group object to add to instance
+ sec_grp_settings = SecurityGroupSettings(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(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name,
+ security_group_names=[sec_grp_settings.name])
+ self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings)
+ vm_inst = self.inst_creator.create()
+ self.assertIsNotNone(vm_inst)
+
+ # Check that group has been added
+ self.assertTrue(inst_has_sec_grp(vm_inst, sec_grp_settings.name))
+
+ # Add security group to instance after activated
+ self.assertTrue(self.inst_creator.add_security_group(sec_grp))
+
+ # Validate that security group has been added
+ self.assertTrue(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name))
+
+
+def inst_has_sec_grp(vm_inst, sec_grp_name):
+ """
+ Returns true if instance has a security group of a given name
+ :return:
+ """
+ if not hasattr(vm_inst, 'security_groups'):
+ return False
+
+ found = False
+ for sec_grp_dict in vm_inst.security_groups:
+ if sec_grp_name in sec_grp_dict['name']:
+ found = True
+ break
+ return found
+
+
+def validate_ssh_client(instance_creator):
+ """
+ Returns True if instance_creator returns an SSH client that is valid
+ :param instance_creator: the object responsible for creating the VM instance
+ :return: T/F
+ """
+ ssh_active = instance_creator.vm_ssh_active(block=True)
+
+ if ssh_active:
+ ssh_client = instance_creator.ssh_client()
+ if ssh_client:
+ out = ssh_client.exec_command('pwd')[1]
+ else:
+ return False
+
+ channel = out.channel
+ in_buffer = channel.in_buffer
+ pwd_out = in_buffer.read(1024)
+ if not pwd_out or len(pwd_out) < 10:
+ return False
+ return True
+
+ return False
diff --git a/snaps/openstack/tests/create_keypairs_tests.py b/snaps/openstack/tests/create_keypairs_tests.py
new file mode 100644
index 0000000..e4409a9
--- /dev/null
+++ b/snaps/openstack/tests/create_keypairs_tests.py
@@ -0,0 +1,203 @@
+# 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 os
+import uuid
+import unittest
+
+from Crypto.PublicKey import RSA
+
+from snaps.openstack.create_keypairs import KeypairSettings, OpenStackKeypair
+from snaps.openstack.utils import nova_utils
+from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
+
+__author__ = 'spisarski'
+
+
+class KeypairSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the KeypairSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ KeypairSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ KeypairSettings(config=dict())
+
+ def test_name_only(self):
+ settings = KeypairSettings(name='foo')
+ self.assertEquals('foo', settings.name)
+ self.assertIsNone(settings.public_filepath)
+ self.assertIsNone(settings.private_filepath)
+
+ def test_config_with_name_only(self):
+ settings = KeypairSettings(config={'name': 'foo'})
+ self.assertEquals('foo', settings.name)
+ self.assertIsNone(settings.public_filepath)
+ self.assertIsNone(settings.private_filepath)
+
+ def test_name_pub_only(self):
+ settings = KeypairSettings(name='foo', public_filepath='/foo/bar.pub')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('/foo/bar.pub', settings.public_filepath)
+ self.assertIsNone(settings.private_filepath)
+
+ def test_config_with_name_pub_only(self):
+ settings = KeypairSettings(config={'name': 'foo', 'public_filepath': '/foo/bar.pub'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('/foo/bar.pub', settings.public_filepath)
+ self.assertIsNone(settings.private_filepath)
+
+ def test_name_priv_only(self):
+ settings = KeypairSettings(name='foo', private_filepath='/foo/bar')
+ self.assertEquals('foo', settings.name)
+ self.assertIsNone(settings.public_filepath)
+ self.assertEquals('/foo/bar', settings.private_filepath)
+
+ def test_config_with_name_priv_only(self):
+ settings = KeypairSettings(config={'name': 'foo', 'private_filepath': '/foo/bar'})
+ self.assertEquals('foo', settings.name)
+ self.assertIsNone(settings.public_filepath)
+ self.assertEquals('/foo/bar', settings.private_filepath)
+
+ def test_all(self):
+ settings = KeypairSettings(name='foo', public_filepath='/foo/bar.pub', private_filepath='/foo/bar')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('/foo/bar.pub', settings.public_filepath)
+ self.assertEquals('/foo/bar', settings.private_filepath)
+
+ def test_config_all(self):
+ settings = KeypairSettings(config={'name': 'foo', 'public_filepath': '/foo/bar.pub',
+ 'private_filepath': '/foo/bar'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('/foo/bar.pub', settings.public_filepath)
+ self.assertEquals('/foo/bar', settings.private_filepath)
+
+
+class CreateKeypairsTests(OSIntegrationTestCase):
+ """
+ Tests for the OpenStackKeypair class
+ """
+
+ def setUp(self):
+ super(self.__class__, self).__start__()
+
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.priv_file_path = 'tmp/' + guid
+ self.pub_file_path = self.priv_file_path + '.pub'
+ self.nova = nova_utils.nova_client(self.os_creds)
+ self.keypair_name = guid
+
+ self.keypair_creator = None
+
+ def tearDown(self):
+ """
+ Cleanup of created keypair
+ """
+ if self.keypair_creator:
+ self.keypair_creator.clean()
+
+ try:
+ os.remove(self.pub_file_path)
+ except:
+ pass
+
+ try:
+ os.remove(self.priv_file_path)
+ except:
+ pass
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_keypair_only(self):
+ """
+ Tests the creation of a generated keypair without saving to file
+ :return:
+ """
+ self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(name=self.keypair_name))
+ self.keypair_creator.create()
+
+ keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair())
+ self.assertEquals(self.keypair_creator.get_keypair(), keypair)
+
+ def test_create_delete_keypair(self):
+ """
+ Tests the creation then deletion of an OpenStack keypair to ensure clean() does not raise an Exception.
+ """
+ # Create Image
+ self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(name=self.keypair_name))
+ created_keypair = self.keypair_creator.create()
+ self.assertIsNotNone(created_keypair)
+
+ # Delete Image manually
+ nova_utils.delete_keypair(self.nova, created_keypair)
+
+ self.assertIsNone(nova_utils.get_keypair_by_name(self.nova, self.keypair_name))
+
+ # Must not throw an exception when attempting to cleanup non-existent image
+ self.keypair_creator.clean()
+ self.assertIsNone(self.keypair_creator.get_keypair())
+
+ def test_create_keypair_save_pub_only(self):
+ """
+ Tests the creation of a generated keypair and saves the public key only
+ :return:
+ """
+ self.keypair_creator = OpenStackKeypair(
+ self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path))
+ self.keypair_creator.create()
+
+ keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair())
+ self.assertEquals(self.keypair_creator.get_keypair(), keypair)
+
+ file_key = open(os.path.expanduser(self.pub_file_path)).read()
+ self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key)
+
+ def test_create_keypair_save_both(self):
+ """
+ Tests the creation of a generated keypair and saves both private and public key files[
+ :return:
+ """
+ self.keypair_creator = OpenStackKeypair(
+ self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path,
+ private_filepath=self.priv_file_path))
+ self.keypair_creator.create()
+
+ keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair())
+ self.assertEquals(self.keypair_creator.get_keypair(), keypair)
+
+ file_key = open(os.path.expanduser(self.pub_file_path)).read()
+ self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key)
+
+ self.assertTrue(os.path.isfile(self.priv_file_path))
+
+ def test_create_keypair_from_file(self):
+ """
+ Tests the creation of an existing public keypair from a file
+ :return:
+ """
+ keys = RSA.generate(1024)
+ nova_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.keypair_creator.create()
+
+ keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair())
+ self.assertEquals(self.keypair_creator.get_keypair(), keypair)
+
+ file_key = open(os.path.expanduser(self.pub_file_path)).read()
+ self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key)
diff --git a/snaps/openstack/tests/create_network_tests.py b/snaps/openstack/tests/create_network_tests.py
new file mode 100644
index 0000000..a2b17f8
--- /dev/null
+++ b/snaps/openstack/tests/create_network_tests.py
@@ -0,0 +1,533 @@
+# 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 uuid
+import unittest
+
+from snaps.openstack.create_network import OpenStackNetwork, NetworkSettings, SubnetSettings, PortSettings
+from snaps.openstack import create_router
+from snaps.openstack.tests import openstack_tests
+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
+
+__author__ = 'spisarski'
+
+
+class NetworkSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the NetworkSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ NetworkSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ NetworkSettings(config=dict())
+
+ def test_name_only(self):
+ settings = NetworkSettings(name='foo')
+ self.assertEquals('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.assertEquals(0, len(settings.subnet_settings))
+
+ def test_config_with_name_only(self):
+ settings = NetworkSettings(config={'name': 'foo'})
+ self.assertEquals('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.assertEquals(0, len(settings.subnet_settings))
+
+ def test_all(self):
+ sub_settings = SubnetSettings(name='foo-subnet', cidr='10.0.0.0/24')
+ settings = NetworkSettings(name='foo', admin_state_up=False, shared=True, project_name='bar', external=True,
+ network_type='flat', physical_network='phy', subnet_settings=[sub_settings])
+ self.assertEquals('foo', settings.name)
+ self.assertFalse(settings.admin_state_up)
+ self.assertTrue(settings.shared)
+ self.assertEquals('bar', settings.project_name)
+ self.assertTrue(settings.external)
+ self.assertEquals('flat', settings.network_type)
+ self.assertEquals('phy', settings.physical_network)
+ self.assertEquals(1, len(settings.subnet_settings))
+ self.assertEquals('foo-subnet', settings.subnet_settings[0].name)
+
+ def test_config_all(self):
+ settings = NetworkSettings(config={'name': 'foo', 'admin_state_up': False, 'shared': True,
+ 'project_name': 'bar', 'external': True, 'network_type': 'flat',
+ 'physical_network': 'phy',
+ 'subnets':
+ [{'subnet': {'name': 'foo-subnet', 'cidr': '10.0.0.0/24'}}]})
+ self.assertEquals('foo', settings.name)
+ self.assertFalse(settings.admin_state_up)
+ self.assertTrue(settings.shared)
+ self.assertEquals('bar', settings.project_name)
+ self.assertTrue(settings.external)
+ self.assertEquals('flat', settings.network_type)
+ self.assertEquals('phy', settings.physical_network)
+ self.assertEquals(1, len(settings.subnet_settings))
+ self.assertEquals('foo-subnet', settings.subnet_settings[0].name)
+
+
+class SubnetSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the SubnetSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ SubnetSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ SubnetSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ SubnetSettings(name='foo')
+
+ def test_config_with_name_only(self):
+ with self.assertRaises(Exception):
+ SubnetSettings(config={'name': 'foo'})
+
+ def test_name_cidr_only(self):
+ settings = SubnetSettings(name='foo', cidr='10.0.0.0/24')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('10.0.0.0/24', settings.cidr)
+ self.assertEquals(4, settings.ip_version)
+ self.assertIsNone(settings.project_name)
+ self.assertIsNone(settings.start)
+ self.assertIsNone(settings.end)
+ self.assertIsNone(settings.enable_dhcp)
+ self.assertEquals(1, len(settings.dns_nameservers))
+ self.assertEquals('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 = SubnetSettings(config={'name': 'foo', 'cidr': '10.0.0.0/24'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('10.0.0.0/24', settings.cidr)
+ self.assertEquals(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.assertEquals(1, len(settings.dns_nameservers))
+ self.assertEquals('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(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.assertEquals('foo', settings.name)
+ self.assertEquals('10.0.0.0/24', settings.cidr)
+ self.assertEquals(6, settings.ip_version)
+ self.assertEquals('bar-project', settings.project_name)
+ self.assertEquals('10.0.0.2', settings.start)
+ self.assertEquals('10.0.0.101', settings.end)
+ self.assertEquals('10.0.0.1', settings.gateway_ip)
+ self.assertEquals(False, settings.enable_dhcp)
+ self.assertEquals(1, len(settings.dns_nameservers))
+ self.assertEquals('8.8.8.8', settings.dns_nameservers[0])
+ self.assertEquals(1, len(settings.host_routes))
+ self.assertEquals(host_routes, settings.host_routes[0])
+ self.assertEquals('dest', settings.destination)
+ self.assertEquals('hop', settings.nexthop)
+ self.assertEquals('dhcpv6-stateful', settings.ipv6_ra_mode)
+ self.assertEquals('slaac', settings.ipv6_address_mode)
+
+ def test_config_all(self):
+ host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'}
+ settings = SubnetSettings(config={'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.assertEquals('foo', settings.name)
+ self.assertEquals('10.0.0.0/24', settings.cidr)
+ self.assertEquals(6, settings.ip_version)
+ self.assertEquals('bar-project', settings.project_name)
+ self.assertEquals('10.0.0.2', settings.start)
+ self.assertEquals('10.0.0.101', settings.end)
+ self.assertEquals('10.0.0.1', settings.gateway_ip)
+ self.assertEquals(False, settings.enable_dhcp)
+ self.assertEquals(1, len(settings.dns_nameservers))
+ self.assertEquals('8.8.8.8', settings.dns_nameservers[0])
+ self.assertEquals(1, len(settings.host_routes))
+ self.assertEquals(host_routes, settings.host_routes[0])
+ self.assertEquals('dest', settings.destination)
+ self.assertEquals('hop', settings.nexthop)
+ self.assertEquals('dhcpv6-stateful', settings.ipv6_ra_mode)
+ self.assertEquals('slaac', settings.ipv6_address_mode)
+
+
+class PortSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the PortSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ PortSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ PortSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ PortSettings(name='foo')
+
+ def test_config_name_only(self):
+ with self.assertRaises(Exception):
+ PortSettings(config={'name': 'foo'})
+
+ def test_name_netname_only(self):
+ settings = PortSettings(name='foo', network_name='bar')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('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.fixed_ips)
+ 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 = PortSettings(config={'name': 'foo', 'network_name': 'bar'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('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.fixed_ips)
+ 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_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', device_owner='owner',
+ device_id='device number')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.network_name)
+ self.assertFalse(settings.admin_state_up)
+ self.assertEquals('foo-project', settings.project_name)
+ self.assertEquals('1234', settings.mac_address)
+ self.assertEquals(ip_addrs, settings.ip_addrs)
+ self.assertEquals(fixed_ips, settings.fixed_ips)
+ self.assertEquals(1, len(settings.security_groups))
+ self.assertEquals('foo_grp_id', settings.security_groups[0])
+ self.assertEquals(allowed_address_pairs, settings.allowed_address_pairs)
+ self.assertEquals('opt value', settings.opt_value)
+ self.assertEquals('opt name', settings.opt_name)
+ self.assertEquals('owner', settings.device_owner)
+ self.assertEquals('device number', settings.device_id)
+
+ 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(config={'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', 'device_owner': 'owner', 'device_id': 'device number'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.network_name)
+ self.assertFalse(settings.admin_state_up)
+ self.assertEquals('foo-project', settings.project_name)
+ self.assertEquals('1234', settings.mac_address)
+ self.assertEquals(ip_addrs, settings.ip_addrs)
+ self.assertEquals(fixed_ips, settings.fixed_ips)
+ self.assertEquals(1, len(settings.security_groups))
+ self.assertEquals('foo_grp_id', settings.security_groups[0])
+ self.assertEquals(allowed_address_pairs, settings.allowed_address_pairs)
+ self.assertEquals('opt value', settings.opt_value)
+ self.assertEquals('opt name', settings.opt_name)
+ self.assertEquals('owner', settings.device_owner)
+ self.assertEquals('device number', settings.device_id)
+
+
+class CreateNetworkSuccessTests(OSIntegrationTestCase):
+ """
+ Test for the CreateNework class defined in create_nework.py
+ """
+
+ def setUp(self):
+ """
+ Sets up object for test
+ """
+ super(self.__class__, self).__start__()
+
+ 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)
+
+ 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):
+ """
+ Cleans the network
+ """
+ if self.router_creator:
+ self.router_creator.clean()
+
+ if self.net_creator:
+ if len(self.net_creator.get_subnets()) > 0:
+ # Validate subnet has been deleted
+ neutron_utils_tests.validate_subnet(
+ self.neutron, self.net_creator.network_settings.subnet_settings[0].name,
+ self.net_creator.network_settings.subnet_settings[0].cidr, False)
+
+ if self.net_creator.get_network():
+ # Validate network has been deleted
+ neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name,
+ False)
+ self.net_creator.clean()
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_network_without_router(self):
+ """
+ Tests the creation of an OpenStack network without a router.
+ """
+ # Create Nework
+ self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings)
+ self.net_creator.create()
+
+ # Validate network was created
+ neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True)
+
+ # Validate subnets
+ neutron_utils_tests.validate_subnet(
+ self.neutron, self.net_creator.network_settings.subnet_settings[0].name,
+ self.net_creator.network_settings.subnet_settings[0].cidr, True)
+
+ def test_create_delete_network(self):
+ """
+ Tests the creation of an OpenStack network, it's deletion, then cleanup.
+ """
+ # Create Nework
+ self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings)
+ self.net_creator.create()
+
+ # Validate network was created
+ neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True)
+
+ neutron_utils.delete_network(self.neutron, self.net_creator.get_network())
+ self.assertIsNone(neutron_utils.get_network(self.neutron, self.net_creator.network_settings.name))
+
+ # This shall not throw an exception here
+ self.net_creator.clean()
+
+ def test_create_network_with_router(self):
+ """
+ Tests the creation of an OpenStack network with a router.
+ """
+ # Create Network
+ self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings)
+ self.net_creator.create()
+
+ # Create Router
+ self.router_creator = create_router.OpenStackRouter(self.os_creds, self.net_config.router_settings)
+ self.router_creator.create()
+
+ # Validate network was created
+ neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True)
+
+ # Validate subnets
+ neutron_utils_tests.validate_subnet(
+ self.neutron, self.net_creator.network_settings.subnet_settings[0].name,
+ self.net_creator.network_settings.subnet_settings[0].cidr, True)
+
+ # Validate routers
+ neutron_utils_tests.validate_router(self.neutron, self.router_creator.router_settings.name, True)
+
+ neutron_utils_tests.validate_interface_router(self.router_creator.get_internal_router_interface(),
+ self.router_creator.get_router(),
+ self.net_creator.get_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
+ self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings)
+ self.net_creator.create()
+
+ self.net_creator2 = OpenStackNetwork(self.os_creds, self.net_config.network_settings)
+ self.net_creator2.create()
+
+ self.assertEquals(self.net_creator.get_network()['network']['id'],
+ self.net_creator2.get_network()['network']['id'])
+
+
+class CreateNetworkTypeTests(OSComponentTestCase):
+ """
+ Test for the CreateNework class defined in create_nework.py for testing creating networks of different types
+ """
+
+ def setUp(self):
+ """
+ Sets up object for test
+ """
+ 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')
+
+ self.neutron = neutron_utils.neutron_client(self.os_creds)
+
+ # Initialize for cleanup
+ self.net_creator = None
+ self.neutron = neutron_utils.neutron_client(self.os_creds)
+
+ def tearDown(self):
+ """
+ Cleans the network
+ """
+ if self.net_creator:
+ if len(self.net_creator.get_subnets()) > 0:
+ # Validate subnet has been deleted
+ neutron_utils_tests.validate_subnet(
+ self.neutron, self.net_creator.network_settings.subnet_settings[0].name,
+ self.net_creator.network_settings.subnet_settings[0].cidr, False)
+
+ if self.net_creator.get_network():
+ # Validate network has been deleted
+ neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name,
+ False)
+ self.net_creator.clean()
+ # TODO - determine why this is not working on Newton
+ # - Unable to create the network. No tenant network is available for allocation.
+ # def test_create_network_type_vlan(self):
+ # """
+ # Tests the creation of an OpenStack network of type vlan.
+ # """
+ # # Create Network
+ # network_type = 'vlan'
+ # net_settings = NetworkSettings(name=self.net_config.network_settings.name,
+ # subnet_settings=self.net_config.network_settings.subnet_settings,
+ # network_type=network_type)
+ #
+ # # When setting the network_type, creds must be admin
+ # self.net_creator = OpenStackNetwork(self.os_creds, net_settings)
+ # network = self.net_creator.create()
+ #
+ # # Validate network was created
+ # neutron_utils_tests.validate_network(self.neutron, net_settings.name, True)
+ #
+ # self.assertEquals(network_type, network['network']['provider:network_type'])
+
+ def test_create_network_type_vxlan(self):
+ """
+ Tests the creation of an OpenStack network of type vxlan.
+ """
+ # Create Network
+ network_type = 'vxlan'
+ net_settings = NetworkSettings(name=self.net_config.network_settings.name,
+ subnet_settings=self.net_config.network_settings.subnet_settings,
+ network_type=network_type)
+
+ # When setting the network_type, creds must be admin
+ self.net_creator = OpenStackNetwork(self.os_creds, net_settings)
+ network = self.net_creator.create()
+
+ # Validate network was created
+ neutron_utils_tests.validate_network(self.neutron, net_settings.name, True)
+
+ self.assertEquals(network_type, network['network']['provider:network_type'])
+
+ # TODO - determine what value we need to place into physical_network
+ # - Do not know what vaule to place into the 'physical_network' setting.
+ # def test_create_network_type_flat(self):
+ # """
+ # Tests the creation of an OpenStack network of type flat.
+ # """
+ # # Create Network
+ # network_type = 'flat'
+ #
+ # # Unable to find documentation on how to find a value that will work here.
+ # # https://visibilityspots.org/vlan-flat-neutron-provider.html
+ # # https://community.rackspace.com/products/f/45/t/4225
+ # # It appears that this may be due to how OPNFV is configuring OpenStack.
+ # physical_network = '???'
+ # net_settings = NetworkSettings(name=self.net_config.network_settings.name,
+ # subnet_settings=self.net_config.network_settings.subnet_settings,
+ # network_type=network_type, physical_network=physical_network)
+ # self.net_creator = OpenStackNetwork(self.os_creds, net_settings)
+ # network = self.net_creator.create()
+ #
+ # # Validate network was created
+ # neutron_utils_tests.validate_network(self.neutron, net_settings.name, True)
+ #
+ # self.assertEquals(network_type, network['network']['provider:network_type'])
+
+ def test_create_network_type_foo(self):
+ """
+ Tests the creation of an OpenStack network of type foo which should raise an exception.
+ """
+ # Create Network
+ network_type = 'foo'
+ net_settings = NetworkSettings(name=self.net_config.network_settings.name,
+ subnet_settings=self.net_config.network_settings.subnet_settings,
+ network_type=network_type)
+ self.net_creator = OpenStackNetwork(self.os_creds, net_settings)
+ with self.assertRaises(Exception):
+ self.net_creator.create()
diff --git a/snaps/openstack/tests/create_project_tests.py b/snaps/openstack/tests/create_project_tests.py
new file mode 100644
index 0000000..9d53467
--- /dev/null
+++ b/snaps/openstack/tests/create_project_tests.py
@@ -0,0 +1,228 @@
+# 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 uuid
+import unittest
+
+from snaps.openstack.create_project import 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
+
+__author__ = 'spisarski'
+
+
+class ProjectSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the ProjectSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ ProjectSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ ProjectSettings(config=dict())
+
+ def test_name_only(self):
+ settings = ProjectSettings(name='foo')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('default', settings.domain)
+ self.assertIsNone(settings.description)
+ self.assertTrue(settings.enabled)
+
+ def test_config_with_name_only(self):
+ settings = ProjectSettings(config={'name': 'foo'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('default', settings.domain)
+ self.assertIsNone(settings.description)
+ self.assertTrue(settings.enabled)
+
+ def test_all(self):
+ settings = ProjectSettings(name='foo', domain='bar', description='foobar', enabled=False)
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.domain)
+ self.assertEquals('foobar', settings.description)
+ self.assertFalse(settings.enabled)
+
+ def test_config_all(self):
+ settings = ProjectSettings(config={'name': 'foo', 'domain': 'bar', 'description': 'foobar', 'enabled': False})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.domain)
+ self.assertEquals('foobar', settings.description)
+ self.assertFalse(settings.enabled)
+
+
+class CreateProjectSuccessTests(OSComponentTestCase):
+ """
+ Test for the CreateImage class defined in create_image.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = str(uuid.uuid4())[:-19]
+ guid = self.__class__.__name__ + '-' + guid
+ self.project_settings = ProjectSettings(name=guid + '-name')
+
+ self.keystone = keystone_utils.keystone_client(self.os_creds)
+
+ # Initialize for cleanup
+ self.project_creator = None
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ if self.project_creator:
+ self.project_creator.clean()
+
+ def test_create_project(self):
+ """
+ Tests the creation of an OpenStack project.
+ """
+ self.project_creator = OpenStackProject(self.os_creds, self.project_settings)
+ created_project = self.project_creator.create()
+ self.assertIsNotNone(created_project)
+
+ retrieved_project = keystone_utils.get_project(keystone=self.keystone, project_name=self.project_settings.name)
+ self.assertIsNotNone(retrieved_project)
+ self.assertEquals(created_project, retrieved_project)
+
+ def test_create_project_2x(self):
+ """
+ Tests the creation of an OpenStack project twice to ensure it only creates one.
+ """
+ self.project_creator = OpenStackProject(self.os_creds, self.project_settings)
+ created_project = self.project_creator.create()
+ self.assertIsNotNone(created_project)
+
+ retrieved_project = keystone_utils.get_project(keystone=self.keystone, project_name=self.project_settings.name)
+ self.assertIsNotNone(retrieved_project)
+ self.assertEquals(created_project, retrieved_project)
+
+ project2 = OpenStackProject(self.os_creds, self.project_settings).create()
+ self.assertEquals(retrieved_project, project2)
+
+ def test_create_delete_project(self):
+ """
+ Tests the creation of an OpenStack project, it's deletion, then cleanup.
+ """
+ # Create Image
+ self.project_creator = OpenStackProject(self.os_creds, self.project_settings)
+ created_project = self.project_creator.create()
+ self.assertIsNotNone(created_project)
+
+ keystone_utils.delete_project(self.keystone, created_project)
+
+ self.project_creator.clean()
+
+ self.assertIsNone(self.project_creator.get_project())
+
+ # TODO - Expand tests
+
+
+class CreateProjectUserTests(OSComponentTestCase):
+ """
+ Test for the CreateImage class defined in create_image.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = str(uuid.uuid4())[:-19]
+ self.guid = self.__class__.__name__ + '-' + guid
+ self.project_settings = ProjectSettings(name=self.guid + '-name')
+
+ self.keystone = keystone_utils.keystone_client(self.os_creds)
+
+ # Initialize for cleanup
+ self.project_creator = None
+ self.user_creators = list()
+
+ self.sec_grp_creators = list()
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ for sec_grp_creator in self.sec_grp_creators:
+ sec_grp_creator.clean()
+
+ for user_creator in self.user_creators:
+ user_creator.clean()
+
+ if self.project_creator:
+ self.project_creator.clean()
+
+ def test_create_project_sec_grp_one_user(self):
+ """
+ Tests the creation of an OpenStack object to a project with a new users and to create a security group
+ """
+ self.project_creator = OpenStackProject(self.os_creds, self.project_settings)
+ created_project = self.project_creator.create()
+ self.assertIsNotNone(created_project)
+
+ user_creator = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user', password=self.guid))
+ self.project_creator.assoc_user(user_creator.create())
+ self.user_creators.append(user_creator)
+
+ 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 = sec_grp_creator.create()
+ self.assertIsNotNone(sec_grp)
+ self.sec_grp_creators.append(sec_grp_creator)
+
+ if self.keystone.version == keystone_utils.V2_VERSION:
+ self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['tenant_id'])
+ else:
+ self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['project_id'])
+
+ def test_create_project_sec_grp_two_users(self):
+ """
+ Tests the creation of an OpenStack object to a project with two new users and use each user to create a
+ security group
+ """
+ self.project_creator = OpenStackProject(self.os_creds, self.project_settings)
+ created_project = self.project_creator.create()
+ self.assertIsNotNone(created_project)
+
+ user_creator_1 = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user1', password=self.guid))
+ self.project_creator.assoc_user(user_creator_1.create())
+ self.user_creators.append(user_creator_1)
+
+ user_creator_2 = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user2', password=self.guid))
+ self.project_creator.assoc_user(user_creator_2.create())
+ self.user_creators.append(user_creator_2)
+
+ ctr = 0
+ for user_creator in self.user_creators:
+ ctr += 1
+ 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 = sec_grp_creator.create()
+ self.assertIsNotNone(sec_grp)
+ self.sec_grp_creators.append(sec_grp_creator)
+ self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['tenant_id'])
diff --git a/snaps/openstack/tests/create_router_tests.py b/snaps/openstack/tests/create_router_tests.py
new file mode 100644
index 0000000..3e22714
--- /dev/null
+++ b/snaps/openstack/tests/create_router_tests.py
@@ -0,0 +1,264 @@
+# 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 uuid
+
+from snaps.openstack import create_network
+from snaps.openstack import create_router
+from snaps.openstack.create_network import NetworkSettings
+from snaps.openstack.create_network import OpenStackNetwork
+from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
+from snaps.openstack.create_router import RouterSettings
+from snaps.openstack.utils import neutron_utils
+
+__author__ = 'mmakati'
+
+cidr1 = '10.200.201.0/24'
+cidr2 = '10.200.202.0/24'
+static_gateway_ip1 = '10.200.201.1'
+static_gateway_ip2 = '10.200.202.1'
+
+
+class CreateRouterSuccessTests(OSIntegrationTestCase):
+ """
+ Class for testing routers with various positive scenarios expected to succeed
+ """
+
+ def setUp(self):
+ """
+ Initializes objects used for router testing
+ """
+ super(self.__class__, self).__start__()
+
+ self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.router_creator = None
+ self.network_creator1 = None
+ self.network_creator2 = None
+ self.neutron = neutron_utils.neutron_client(self.os_creds)
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects used for router testing
+ """
+ if self.router_creator:
+ self.router_creator.clean()
+
+ if self.network_creator1:
+ self.network_creator1.clean()
+
+ if self.network_creator2:
+ self.network_creator2.clean()
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_router_vanilla(self):
+ """
+ Test creation of a most basic router with minimal options.
+ """
+ router_settings = RouterSettings(name=self.guid + '-pub-router', external_gateway=self.ext_net_name)
+
+ self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings)
+ self.router_creator.create()
+
+ router = neutron_utils.get_router_by_name(self.neutron, router_settings.name)
+ self.assertIsNotNone(router)
+
+ self.assertTrue(verify_router_attributes(router, self.router_creator, ext_gateway=self.ext_net_name))
+
+ 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(name=self.guid + '-pub-router', external_gateway=self.ext_net_name)
+
+ self.router_creator = create_router.OpenStackRouter(self.os_creds, self.router_settings)
+ created_router = self.router_creator.create()
+ self.assertIsNotNone(created_router)
+ retrieved_router = neutron_utils.get_router_by_name(self.neutron, self.router_settings.name)
+ self.assertIsNotNone(retrieved_router)
+
+ neutron_utils.delete_router(self.neutron, created_router)
+
+ retrieved_router = neutron_utils.get_router_by_name(self.neutron, self.router_settings.name)
+ self.assertIsNone(retrieved_router)
+
+ # Should not raise an exception
+ self.router_creator.clean()
+
+ def test_create_router_admin_state_false(self):
+ """
+ Test creation of a basic router with admin state down.
+ """
+ router_settings = RouterSettings(name=self.guid + '-pub-router', admin_state_up=False)
+
+ self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings)
+ self.router_creator.create()
+
+ router = neutron_utils.get_router_by_name(self.neutron, router_settings.name)
+ self.assertIsNotNone(router)
+
+ self.assertTrue(verify_router_attributes(router, self.router_creator, admin_state=False))
+
+ 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)
+
+ self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings)
+ self.router_creator.create()
+
+ router = neutron_utils.get_router_by_name(self.neutron, router_settings.name)
+ self.assertIsNotNone(router)
+
+ self.assertTrue(verify_router_attributes(router, self.router_creator, admin_state=True))
+
+ def test_create_router_private_network(self):
+ """
+ Test creation of a router connected with two private networks and no external gateway
+ """
+ network_settings1 = NetworkSettings(name=self.guid + '-pub-net1',
+ subnet_settings=[
+ create_network.SubnetSettings(cidr=cidr1,
+ name=self.guid + '-pub-subnet1',
+ gateway_ip=static_gateway_ip1)])
+ network_settings2 = NetworkSettings(name=self.guid + '-pub-net2',
+ subnet_settings=[
+ create_network.SubnetSettings(cidr=cidr2,
+ name=self.guid + '-pub-subnet2',
+ gateway_ip=static_gateway_ip2)])
+
+ self.network_creator1 = OpenStackNetwork(self.os_creds, network_settings1)
+ self.network_creator2 = OpenStackNetwork(self.os_creds, network_settings2)
+
+ self.network_creator1.create()
+ self.network_creator2.create()
+
+ port_settings = [create_network.PortSettings(name=self.guid + '-port1', ip_addrs=[
+ {'subnet_name': network_settings1.subnet_settings[0].name, 'ip': static_gateway_ip1}],
+ network_name=network_settings1.name)
+ , create_network.PortSettings(name=self.guid + '-port2', ip_addrs=[
+ {'subnet_name': network_settings2.subnet_settings[0].name, 'ip': static_gateway_ip2}],
+ network_name=network_settings2.name)]
+
+ router_settings = RouterSettings(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_by_name(self.neutron, router_settings.name)
+
+ self.assertTrue(verify_router_attributes(router, self.router_creator))
+
+ def test_create_router_external_network(self):
+ """
+ Test creation of a router connected to an external network and a private network.
+ """
+ network_settings = NetworkSettings(name=self.guid + '-pub-net1',
+ subnet_settings=[
+ create_network.SubnetSettings(cidr=cidr1,
+ name=self.guid + '-pub-subnet1',
+ gateway_ip=static_gateway_ip1)])
+ self.network_creator1 = OpenStackNetwork(self.os_creds, network_settings)
+ self.network_creator1.create()
+
+ port_settings = [create_network.PortSettings(name=self.guid + '-port1', ip_addrs=[
+ {'subnet_name': network_settings.subnet_settings[0].name, 'ip': static_gateway_ip1}],
+ network_name=network_settings.name)]
+
+ router_settings = RouterSettings(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 = neutron_utils.get_router_by_name(self.neutron, router_settings.name)
+
+ self.assertTrue(verify_router_attributes(router, self.router_creator, ext_gateway=self.ext_net_name))
+
+
+class CreateRouterNegativeTests(OSIntegrationTestCase):
+ """
+ Class for testing routers with various negative scenarios expected to fail.
+ """
+
+ def setUp(self):
+ """
+ Initializes objects used for router testing
+ """
+ super(self.__class__, self).__start__()
+
+ self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.router_creator = None
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects used for router testing
+ """
+ if self.router_creator:
+ self.router_creator.clean()
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_router_noname(self):
+ """
+ Test creating a router without a name.
+ """
+ with self.assertRaises(Exception):
+ router_settings = RouterSettings(name=None, external_gateway=self.ext_net_name)
+ self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings)
+ self.router_creator.create()
+
+ def test_create_router_invalid_gateway_name(self):
+ """
+ Test creating a router without a valid network gateway name.
+ """
+ with self.assertRaises(Exception):
+ router_settings = RouterSettings(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
+ :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 snat: True is enable_snat is True, 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['router']['name'] == router_creator.router_settings.name):
+ return False
+ elif not (router_operational['router']['id'] == router['router']['id']):
+ return False
+ elif not (router_operational['router']['status'] == router['router']['status']):
+ return False
+ elif not (router_operational['router']['tenant_id'] == router['router']['tenant_id']):
+ return False
+ elif not (admin_state == router_operational['router']['admin_state_up']):
+ return False
+ elif (ext_gateway is None) and (router_operational['router']['external_gateway_info'] is not None):
+ return False
+ elif ext_gateway is not None:
+ if router_operational['router']['external_gateway_info'] is None:
+ return False
+ return True
diff --git a/snaps/openstack/tests/create_security_group_tests.py b/snaps/openstack/tests/create_security_group_tests.py
new file mode 100644
index 0000000..079be0c
--- /dev/null
+++ b/snaps/openstack/tests/create_security_group_tests.py
@@ -0,0 +1,355 @@
+# 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 uuid
+import unittest
+
+from snaps.openstack import create_security_group
+from snaps.openstack.create_security_group import SecurityGroupSettings, SecurityGroupRuleSettings, Direction, \
+ Ethertype, 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
+
+__author__ = 'spisarski'
+
+
+class SecurityGroupRuleSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the SecurityGroupRuleSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ SecurityGroupRuleSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ SecurityGroupRuleSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ SecurityGroupRuleSettings(sec_grp_name='foo')
+
+ def test_config_with_name_only(self):
+ with self.assertRaises(Exception):
+ SecurityGroupRuleSettings(config={'sec_grp_name': 'foo'})
+
+ def test_name_and_direction(self):
+ settings = SecurityGroupRuleSettings(sec_grp_name='foo', direction=Direction.ingress)
+ self.assertEquals('foo', settings.sec_grp_name)
+ self.assertEquals(Direction.ingress, settings.direction)
+
+ def test_config_name_and_direction(self):
+ settings = SecurityGroupRuleSettings(config={'sec_grp_name': 'foo', 'direction': 'ingress'})
+ self.assertEquals('foo', settings.sec_grp_name)
+ self.assertEquals(Direction.ingress, settings.direction)
+
+ def test_all(self):
+ settings = SecurityGroupRuleSettings(
+ 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.assertEquals('foo', settings.sec_grp_name)
+ self.assertEquals('fubar', settings.description)
+ self.assertEquals(Direction.egress, settings.direction)
+ self.assertEquals('rgi', settings.remote_group_id)
+ self.assertEquals(Protocol.icmp, settings.protocol)
+ self.assertEquals(Ethertype.IPv6, settings.ethertype)
+ self.assertEquals(1, settings.port_range_min)
+ self.assertEquals(2, settings.port_range_max)
+ self.assertEquals('prfx', settings.remote_ip_prefix)
+
+ def test_config_all(self):
+ settings = SecurityGroupRuleSettings(
+ config={'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.assertEquals('foo', settings.sec_grp_name)
+ self.assertEquals('fubar', settings.description)
+ self.assertEquals(Direction.egress, settings.direction)
+ self.assertEquals('rgi', settings.remote_group_id)
+ self.assertEquals(Protocol.tcp, settings.protocol)
+ self.assertEquals(Ethertype.IPv6, settings.ethertype)
+ self.assertEquals(1, settings.port_range_min)
+ self.assertEquals(2, settings.port_range_max)
+ self.assertEquals('prfx', settings.remote_ip_prefix)
+
+
+class SecurityGroupSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the SecurityGroupSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ SecurityGroupSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ SecurityGroupSettings(config=dict())
+
+ def test_name_only(self):
+ settings = SecurityGroupSettings(name='foo')
+ self.assertEquals('foo', settings.name)
+
+ def test_config_with_name_only(self):
+ settings = SecurityGroupSettings(config={'name': 'foo'})
+ self.assertEquals('foo', settings.name)
+
+ def test_invalid_rule(self):
+ rule_setting = SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.ingress)
+ with self.assertRaises(Exception):
+ SecurityGroupSettings(name='foo', rule_settings=[rule_setting])
+
+ def test_all(self):
+ rule_settings = list()
+ rule_settings.append(SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.egress))
+ rule_settings.append(SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.ingress))
+ settings = SecurityGroupSettings(
+ name='bar', description='fubar', project_name='foo', rule_settings=rule_settings)
+
+ self.assertEquals('bar', settings.name)
+ self.assertEquals('fubar', settings.description)
+ self.assertEquals('foo', settings.project_name)
+ self.assertEquals(rule_settings[0], settings.rule_settings[0])
+ self.assertEquals(rule_settings[1], settings.rule_settings[1])
+
+ def test_config_all(self):
+ settings = SecurityGroupSettings(
+ config={'name': 'bar',
+ 'description': 'fubar',
+ 'project_name': 'foo',
+ 'rules': [{'sec_grp_name': 'bar', 'direction': 'ingress'}]})
+
+ self.assertEquals('bar', settings.name)
+ self.assertEquals('fubar', settings.description)
+ self.assertEquals('foo', settings.project_name)
+ self.assertEquals(1, len(settings.rule_settings))
+ self.assertEquals('bar', settings.rule_settings[0].sec_grp_name)
+ self.assertEquals(Direction.ingress, settings.rule_settings[0].direction)
+
+
+class CreateSecurityGroupTests(OSIntegrationTestCase):
+ """
+ Test for the CreateSecurityGroup class defined in create_security_group.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateSecurityGroup 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.sec_grp_name = guid + 'name'
+ self.neutron = neutron_utils.neutron_client(self.os_creds)
+
+ # Initialize for cleanup
+ self.sec_grp_creator = None
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ if self.sec_grp_creator:
+ self.sec_grp_creator.clean()
+
+ super(self.__class__, self).__clean__()
+
+ def test_create_group_without_rules(self):
+ """
+ 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')
+ self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ self.sec_grp_creator.create()
+
+ sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name)
+ self.assertIsNotNone(sec_grp)
+
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp)
+ rules = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules))
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules)
+
+ def test_create_delete_group(self):
+ """
+ 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')
+ self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ created_sec_grp = self.sec_grp_creator.create()
+ self.assertIsNotNone(created_sec_grp)
+
+ neutron_utils.delete_security_group(self.neutron, created_sec_grp)
+ self.assertIsNone(neutron_utils.get_security_group(self.neutron, self.sec_grp_creator.sec_grp_settings.name))
+
+ self.sec_grp_creator.clean()
+
+ def test_create_group_with_one_simple_rule(self):
+ """
+ Tests the creation of an OpenStack Security Group with one simple custom rule.
+ """
+ # Create Image
+ sec_grp_rule_settings = list()
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.ingress))
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group',
+ rule_settings=sec_grp_rule_settings)
+ self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ self.sec_grp_creator.create()
+
+ sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name)
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp)
+ rules = neutron_utils.get_rules_by_security_group(self.neutron,
+ self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules))
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules)
+
+ def test_create_group_with_several_rules(self):
+ """
+ Tests the creation of an OpenStack Security Group with one simple custom rule.
+ """
+ # Create Image
+ sec_grp_rule_settings = list()
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.ingress))
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.egress,
+ protocol=Protocol.udp,
+ ethertype=Ethertype.IPv6))
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.egress,
+ protocol=Protocol.udp,
+ ethertype=Ethertype.IPv4,
+ port_range_min=10,
+ port_range_max=20))
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group',
+ rule_settings=sec_grp_rule_settings)
+ self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ self.sec_grp_creator.create()
+
+ sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name)
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp)
+ rules = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules))
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules)
+
+ def test_add_rule(self):
+ """
+ Tests the creation of an OpenStack Security Group with one simple custom rule then adds one after creation.
+ """
+ # Create Image
+ sec_grp_rule_settings = list()
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.ingress))
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group',
+ rule_settings=sec_grp_rule_settings)
+ self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ self.sec_grp_creator.create()
+
+ sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name)
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp)
+ rules = neutron_utils.get_rules_by_security_group(self.neutron,
+ self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules))
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules)
+
+ self.sec_grp_creator.add_rule(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_creator.sec_grp_settings.name,
+ direction=Direction.egress, protocol=Protocol.icmp))
+ rules2 = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(rules) + 1, len(rules2))
+
+ def test_remove_rule_by_id(self):
+ """
+ Tests the creation of an OpenStack Security Group with two simple custom rules then removes one by the rule ID.
+ """
+ # Create Image
+ sec_grp_rule_settings = list()
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.ingress))
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.egress,
+ protocol=Protocol.udp,
+ ethertype=Ethertype.IPv6))
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.egress,
+ protocol=Protocol.udp,
+ ethertype=Ethertype.IPv4,
+ port_range_min=10,
+ port_range_max=20))
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group',
+ rule_settings=sec_grp_rule_settings)
+ self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ self.sec_grp_creator.create()
+
+ sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name)
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp)
+ rules = neutron_utils.get_rules_by_security_group(self.neutron,
+ self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules))
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules)
+
+ self.sec_grp_creator.remove_rule(rule_id=rules[0]['security_group_rule']['id'])
+ rules_after_del = neutron_utils.get_rules_by_security_group(self.neutron,
+ self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(rules) - 1, len(rules_after_del))
+
+ def test_remove_rule_by_setting(self):
+ """
+ Tests the creation of an OpenStack Security Group with two simple custom rules then removes one by the rule
+ setting object
+ """
+ # Create Image
+ sec_grp_rule_settings = list()
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.ingress))
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.egress,
+ protocol=Protocol.udp,
+ ethertype=Ethertype.IPv6))
+ sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name,
+ direction=Direction.egress,
+ protocol=Protocol.udp,
+ ethertype=Ethertype.IPv4,
+ port_range_min=10,
+ port_range_max=20))
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group',
+ rule_settings=sec_grp_rule_settings)
+ self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings)
+ self.sec_grp_creator.create()
+
+ sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name)
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp)
+ rules = neutron_utils.get_rules_by_security_group(self.neutron,
+ self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules))
+ validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules)
+
+ self.sec_grp_creator.remove_rule(rule_setting=sec_grp_rule_settings[0])
+ rules_after_del = neutron_utils.get_rules_by_security_group(self.neutron,
+ self.sec_grp_creator.get_security_group())
+ self.assertEquals(len(rules) - 1, len(rules_after_del))
+
+# TODO - Add more tests with different rules. Rule creation parameters can be somewhat complex
diff --git a/snaps/openstack/tests/create_user_tests.py b/snaps/openstack/tests/create_user_tests.py
new file mode 100644
index 0000000..1f7a163
--- /dev/null
+++ b/snaps/openstack/tests/create_user_tests.py
@@ -0,0 +1,155 @@
+# 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 uuid
+import unittest
+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
+
+__author__ = 'spisarski'
+
+
+class UserSettingsUnitTests(unittest.TestCase):
+ """
+ Tests the construction of the UserSettings class
+ """
+
+ def test_no_params(self):
+ with self.assertRaises(Exception):
+ UserSettings()
+
+ def test_empty_config(self):
+ with self.assertRaises(Exception):
+ UserSettings(config=dict())
+
+ def test_name_only(self):
+ with self.assertRaises(Exception):
+ UserSettings(name='foo')
+
+ def test_config_with_name_only(self):
+ with self.assertRaises(Exception):
+ UserSettings(config={'name': 'foo'})
+
+ def test_name_pass_enabled_str(self):
+ with self.assertRaises(Exception):
+ UserSettings(name='foo', password='bar', enabled='true')
+
+ def test_config_with_name_pass_enabled_str(self):
+ with self.assertRaises(Exception):
+ UserSettings(config={'name': 'foo', 'password': 'bar', 'enabled': 'true'})
+
+ def test_name_pass_only(self):
+ settings = UserSettings(name='foo', password='bar')
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('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 = UserSettings(config={'name': 'foo', 'password': 'bar'})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.password)
+ self.assertIsNone(settings.project_name)
+ self.assertIsNone(settings.email)
+ self.assertTrue(settings.enabled)
+
+ def test_all(self):
+ settings = UserSettings(name='foo', password='bar', project_name='proj-foo', email='foo@bar.com', enabled=False)
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.password)
+ self.assertEquals('proj-foo', settings.project_name)
+ self.assertEquals('foo@bar.com', settings.email)
+ self.assertFalse(settings.enabled)
+
+ def test_config_all(self):
+ settings = UserSettings(config={'name': 'foo', 'password': 'bar', 'project_name': 'proj-foo',
+ 'email': 'foo@bar.com', 'enabled': False})
+ self.assertEquals('foo', settings.name)
+ self.assertEquals('bar', settings.password)
+ self.assertEquals('proj-foo', settings.project_name)
+ self.assertEquals('foo@bar.com', settings.email)
+ self.assertFalse(settings.enabled)
+
+
+class CreateUserSuccessTests(OSComponentTestCase):
+ """
+ Test for the CreateImage class defined in create_image.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = str(uuid.uuid4())[:-19]
+ guid = self.__class__.__name__ + '-' + guid
+ self.user_settings = UserSettings(name=guid + '-name', password=guid + '-password')
+
+ self.keystone = keystone_utils.keystone_client(self.os_creds)
+
+ # Initialize for cleanup
+ self.user_creator = None
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ if self.user_creator:
+ self.user_creator.clean()
+
+ def test_create_user(self):
+ """
+ Tests the creation of an OpenStack user.
+ """
+ self.user_creator = OpenStackUser(self.os_creds, self.user_settings)
+ created_user = self.user_creator.create()
+ self.assertIsNotNone(created_user)
+
+ retrieved_user = keystone_utils.get_user(self.keystone, self.user_settings.name)
+ self.assertIsNotNone(retrieved_user)
+ self.assertEquals(created_user, retrieved_user)
+
+ def test_create_user_2x(self):
+ """
+ Tests the creation of an OpenStack user twice to ensure it only creates one.
+ """
+ self.user_creator = OpenStackUser(self.os_creds, self.user_settings)
+ created_user = self.user_creator.create()
+ self.assertIsNotNone(created_user)
+
+ retrieved_user = keystone_utils.get_user(self.keystone, self.user_settings.name)
+ self.assertIsNotNone(retrieved_user)
+ self.assertEquals(created_user, retrieved_user)
+
+ # Create user for the second time to ensure it is the same
+ user2 = OpenStackUser(self.os_creds, self.user_settings).create()
+ self.assertEquals(retrieved_user, user2)
+
+ def test_create_delete_user(self):
+ """
+ Tests the creation of an OpenStack user then delete.
+ """
+ # Create Image
+ self.user_creator = OpenStackUser(self.os_creds, self.user_settings)
+ created_user = self.user_creator.create()
+ self.assertIsNotNone(created_user)
+
+ keystone_utils.delete_user(self.keystone, created_user)
+
+ # Delete user
+ self.user_creator.clean()
+ self.assertIsNone(self.user_creator.get_user())
+
diff --git a/snaps/openstack/tests/openstack_tests.py b/snaps/openstack/tests/openstack_tests.py
new file mode 100644
index 0000000..dab2ea2
--- /dev/null
+++ b/snaps/openstack/tests/openstack_tests.py
@@ -0,0 +1,144 @@
+# 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 re
+
+from snaps import file_utils
+from snaps.openstack.create_network import NetworkSettings, SubnetSettings
+from snaps.openstack.create_router import RouterSettings
+from snaps.openstack.os_credentials import OSCreds, ProxySettings
+from snaps.openstack.create_image import ImageSettings
+import logging
+
+__author__ = 'spisarski'
+
+
+logger = logging.getLogger('openstack_tests')
+
+
+def get_credentials(os_env_file=None, proxy_settings_str=None, ssh_proxy_cmd=None, dev_os_env_file=None):
+ """
+ Returns the OpenStack credentials object. It first attempts to retrieve them from a standard OpenStack source file.
+ If that file is None, it will attempt to retrieve them with a YAML file.
+ it will retrieve them from a
+ :param os_env_file: the OpenStack source file
+ :param proxy_settings_str: proxy settings string <host>:<port> (optional)
+ :param ssh_proxy_cmd: the SSH proxy command for your environment (optional)
+ :param dev_os_env_file: the YAML file to retrieve both the OS credentials and proxy settings
+ :return: the SNAPS credentials object
+ """
+ if os_env_file:
+ logger.debug('Reading RC file - ' + os_env_file)
+ config = file_utils.read_os_env_file(os_env_file)
+ proj_name = config.get('OS_PROJECT_NAME')
+ if not proj_name:
+ proj_name = config.get('OS_TENANT_NAME')
+
+ proj_domain_id = 'default'
+ user_domain_id = 'default'
+
+ if config.get('OS_PROJECT_DOMAIN_ID'):
+ proj_domain_id = config['OS_PROJECT_DOMAIN_ID']
+ if config.get('OS_USER_DOMAIN_ID'):
+ user_domain_id = config['OS_USER_DOMAIN_ID']
+ if config.get('OS_IDENTITY_API_VERSION'):
+ version = int(config['OS_IDENTITY_API_VERSION'])
+ else:
+ version = 2
+
+ proxy_settings = None
+ if proxy_settings_str:
+ tokens = re.split(':', proxy_settings_str)
+ proxy_settings = ProxySettings(tokens[0], tokens[1], ssh_proxy_cmd)
+
+ os_creds = OSCreds(username=config['OS_USERNAME'],
+ password=config['OS_PASSWORD'],
+ auth_url=config['OS_AUTH_URL'],
+ project_name=proj_name,
+ identity_api_version=version,
+ user_domain_id=user_domain_id,
+ project_domain_id=proj_domain_id,
+ proxy_settings=proxy_settings)
+ else:
+ logger.info('Reading development os_env file - ' + dev_os_env_file)
+ config = file_utils.read_yaml(dev_os_env_file)
+ identity_api_version = config.get('identity_api_version')
+ if not identity_api_version:
+ identity_api_version = 2
+
+ proxy_settings = None
+ proxy_str = config.get('http_proxy')
+ if proxy_str:
+ tokens = re.split(':', proxy_str)
+ proxy_settings = ProxySettings(tokens[0], tokens[1], config.get('ssh_proxy_cmd'))
+
+ os_creds = OSCreds(username=config['username'], password=config['password'],
+ auth_url=config['os_auth_url'], project_name=config['project_name'],
+ identity_api_version=identity_api_version,
+ proxy_settings=proxy_settings)
+
+ logger.info('OS Credentials = ' + str(os_creds))
+ return os_creds
+
+
+def cirros_url_image(name):
+ return ImageSettings(name=name, image_user='cirros', img_format='qcow2',
+ url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img')
+
+
+def file_image_test_settings(name, file_path):
+ return ImageSettings(name=name, image_user='cirros', img_format='qcow2',
+ image_file=file_path)
+
+
+def centos_url_image(name):
+ return ImageSettings(name=name, image_user='centos', img_format='qcow2',
+ url='http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2',
+ nic_config_pb_loc='./provisioning/ansible/centos-network-setup/playbooks/configure_host.yml')
+
+
+def ubuntu_url_image(name):
+ return ImageSettings(
+ name=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',
+ nic_config_pb_loc='./provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml')
+
+
+def get_priv_net_config(net_name, subnet_name, router_name=None, cidr='10.55.0.0/24', external_net=None):
+ return OSNetworkConfig(net_name, subnet_name, cidr, router_name, external_gateway=external_net)
+
+
+def get_pub_net_config(net_name, subnet_name=None, router_name=None, cidr='10.55.1.0/24', external_net=None):
+ return OSNetworkConfig(net_name, subnet_name, cidr, router_name, external_gateway=external_net)
+
+
+class OSNetworkConfig:
+ """
+ Represents the settings required for the creation of a network in OpenStack
+ """
+
+ def __init__(self, net_name, subnet_name=None, subnet_cidr=None, router_name=None, external_gateway=None):
+
+ if subnet_name and subnet_cidr:
+ self.network_settings = NetworkSettings(
+ name=net_name, subnet_settings=[SubnetSettings(cidr=subnet_cidr, name=subnet_name)])
+ else:
+ self.network_settings = NetworkSettings(name=net_name)
+
+ if router_name:
+ if subnet_name:
+ self.router_settings = RouterSettings(name=router_name, external_gateway=external_gateway,
+ internal_subnets=[subnet_name])
+ else:
+ self.router_settings = RouterSettings(name=router_name, external_gateway=external_gateway)
diff --git a/snaps/openstack/tests/os_source_file_test.py b/snaps/openstack/tests/os_source_file_test.py
new file mode 100644
index 0000000..fa8d197
--- /dev/null
+++ b/snaps/openstack/tests/os_source_file_test.py
@@ -0,0 +1,131 @@
+# 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 unittest
+import uuid
+
+from snaps import file_utils
+import openstack_tests
+import logging
+
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+# To run these tests from an IDE, the CWD must be set to the python directory of this project
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+from snaps.openstack.create_project import ProjectSettings
+from snaps.openstack.create_user import UserSettings
+from snaps.openstack.utils import deploy_utils, keystone_utils
+
+dev_os_env_file = 'openstack/tests/conf/os_env.yaml'
+
+
+class OSComponentTestCase(unittest.TestCase):
+
+ """
+ Super for test classes requiring a connection to OpenStack
+ """
+ def __init__(self, method_name='runTest', os_env_file=None, ext_net_name=None, http_proxy_str=None,
+ ssh_proxy_cmd=None, log_level=logging.DEBUG):
+ super(OSComponentTestCase, self).__init__(method_name)
+
+ logging.basicConfig(level=log_level)
+
+ self.os_creds = openstack_tests.get_credentials(os_env_file=os_env_file, proxy_settings_str=http_proxy_str,
+ ssh_proxy_cmd=ssh_proxy_cmd, dev_os_env_file=dev_os_env_file)
+ self.ext_net_name = ext_net_name
+
+ if not self.ext_net_name and file_utils.file_exists(dev_os_env_file):
+ test_conf = file_utils.read_yaml(dev_os_env_file)
+ self.ext_net_name = test_conf.get('ext_net')
+
+ @staticmethod
+ def parameterize(testcase_klass, os_env_file, ext_net_name, http_proxy_str=None, ssh_proxy_cmd=None,
+ log_level=logging.DEBUG):
+ """ Create a suite containing all tests taken from the given
+ subclass, passing them the parameter 'param'.
+ """
+ test_loader = unittest.TestLoader()
+ test_names = test_loader.getTestCaseNames(testcase_klass)
+ suite = unittest.TestSuite()
+ for name in test_names:
+ suite.addTest(testcase_klass(name, os_env_file, ext_net_name, http_proxy_str, ssh_proxy_cmd, log_level))
+ return suite
+
+
+class OSIntegrationTestCase(OSComponentTestCase):
+
+ """
+ Super for test classes requiring a connection to OpenStack
+ """
+ def __init__(self, method_name='runTest', os_env_file=None, ext_net_name=None, http_proxy_str=None,
+ ssh_proxy_cmd=None, use_keystone=False, log_level=logging.DEBUG):
+ super(OSIntegrationTestCase, self).__init__(method_name=method_name, os_env_file=os_env_file,
+ ext_net_name=ext_net_name, http_proxy_str=http_proxy_str,
+ ssh_proxy_cmd=ssh_proxy_cmd, log_level=log_level)
+ self.use_keystone = use_keystone
+ self.keystone = None
+
+ @staticmethod
+ def parameterize(testcase_klass, os_env_file, ext_net_name, http_proxy_str=None, ssh_proxy_cmd=None,
+ use_keystone=False, log_level=logging.DEBUG):
+ """ Create a suite containing all tests taken from the given
+ subclass, passing them the parameter 'param'.
+ """
+ test_loader = unittest.TestLoader()
+ test_names = test_loader.getTestCaseNames(testcase_klass)
+ suite = unittest.TestSuite()
+ for name in test_names:
+ suite.addTest(testcase_klass(name, os_env_file, ext_net_name, http_proxy_str, ssh_proxy_cmd, use_keystone,
+ log_level))
+ return suite
+
+ """
+ Super for test classes that should be run within their own project/tenant as they can run for quite some time
+ """
+ def __start__(self):
+ """
+ Creates a project and user to be leveraged by subclass test methods. If implementing class uses this method,
+ it must call __clean__() else you will be left with unwanted users and tenants
+ """
+ self.project_creator = None
+ self.user_creator = None
+ self.admin_os_creds = self.os_creds
+ self.role = None
+
+ if self.use_keystone:
+ self.keystone = keystone_utils.keystone_client(self.os_creds)
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())[:-19]
+ project_name = guid + '-proj'
+ self.project_creator = deploy_utils.create_project(self.admin_os_creds, ProjectSettings(name=project_name))
+
+ self.user_creator = deploy_utils.create_user(
+ self.admin_os_creds, UserSettings(name=guid + '-user', password=guid, project_name=project_name))
+ self.os_creds = self.user_creator.get_os_creds(self.project_creator.project_settings.name)
+
+ # add user to project
+ self.project_creator.assoc_user(self.user_creator.get_user())
+
+ def __clean__(self):
+ """
+ Cleans up test user and project.
+ Must be called at the end of child classes tearDown() if __start__() is called during setUp() else these
+ objects will persist after the test is run
+ """
+ if self.role:
+ keystone_utils.delete_role(self.keystone, self.role)
+
+ if self.project_creator:
+ self.project_creator.clean()
+
+ if self.user_creator:
+ self.user_creator.clean()
diff --git a/snaps/openstack/tests/validation_utils.py b/snaps/openstack/tests/validation_utils.py
new file mode 100644
index 0000000..7c9bd7f
--- /dev/null
+++ b/snaps/openstack/tests/validation_utils.py
@@ -0,0 +1,69 @@
+# 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.
+from neutronclient.v2_0.client import _DictWithMeta
+
+__author__ = 'spisarski'
+
+
+def objects_equivalent(obj1, obj2):
+ """
+ Returns true if both objects are equivalent
+ :param obj1:
+ :param obj2:
+ :return: T/F
+ """
+ if obj1 is None and obj2 is None:
+ return True
+ if type(obj1) is dict or type(obj1) is _DictWithMeta:
+ return dicts_equivalent(obj1, obj2)
+ elif type(obj1) is list:
+ return lists_equivalent(obj1, obj2)
+ else:
+ return obj1 == obj2
+
+
+def dicts_equivalent(dict1, dict2):
+ """
+ Returns true when each key/value pair is equal
+ :param dict1: dict 1
+ :param dict2: dict 2
+ :return: T/F
+ """
+ if (type(dict1) is dict or type(dict1) is _DictWithMeta) and (type(dict2) is dict or type(dict2) is _DictWithMeta):
+ for key, value1 in dict1.iteritems():
+ if not objects_equivalent(value1, dict2.get(key)):
+ return False
+ return True
+ return False
+
+
+def lists_equivalent(list1, list2):
+ """
+ Returns true when an item in list1 is also contained in list2
+ :param list1: list 1
+ :param list2: list 2
+ :return: T/F
+ """
+ if len(list1) == len(list2) and type(list1) is list and type(list2) is list:
+ for item1 in list1:
+ has_equivalent = False
+ for item2 in list2:
+ has_equivalent = objects_equivalent(item1, item2)
+ if has_equivalent:
+ break
+ if not has_equivalent:
+ return False
+ return True
+ return False
diff --git a/snaps/openstack/utils/__init__.py b/snaps/openstack/utils/__init__.py
new file mode 100644
index 0000000..7f92908
--- /dev/null
+++ b/snaps/openstack/utils/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski' \ No newline at end of file
diff --git a/snaps/openstack/utils/deploy_utils.py b/snaps/openstack/utils/deploy_utils.py
new file mode 100644
index 0000000..ade8811
--- /dev/null
+++ b/snaps/openstack/utils/deploy_utils.py
@@ -0,0 +1,151 @@
+#
+# 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
+
+from snaps.openstack.create_project import OpenStackProject
+from snaps.openstack.create_user import OpenStackUser
+from snaps.openstack.create_image import OpenStackImage
+from snaps.openstack.create_network import OpenStackNetwork
+from snaps.openstack.create_router import OpenStackRouter
+from snaps.openstack.create_keypairs import OpenStackKeypair
+from snaps.openstack.create_instance import OpenStackVmInstance
+from snaps.openstack.create_security_group import OpenStackSecurityGroup
+
+logger = logging.getLogger('deploy_utils')
+
+
+def create_image(os_creds, image_settings, cleanup=False):
+ """
+ Creates an image in OpenStack if necessary
+ :param os_creds: The OpenStack credentials object
+ :param image_settings: The image settings object
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: A reference to the image creator object from which the image object can be accessed
+ """
+ image_creator = OpenStackImage(os_creds, image_settings)
+ image_creator.create(cleanup)
+ return image_creator
+
+
+def create_network(os_creds, network_settings, cleanup=False):
+ """
+ Creates a network on which the CMTSs can attach
+ :param os_creds: The OpenStack credentials object
+ :param network_settings: The network settings object
+ :param cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: A reference to the network creator objects for each network from which network elements such as the
+ subnet, router, interface router, and network objects can be accessed.
+ """
+ # Check for OS for network existence
+ # If exists return network instance data
+ # Else, create network and return instance data
+
+ logger.info('Attempting to create network with name - ' + network_settings.name)
+
+ network_creator = OpenStackNetwork(os_creds, network_settings)
+ network_creator.create(cleanup)
+ logger.info('Created network ')
+ return network_creator
+
+
+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 cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: A reference to the network creator objects for each network from which network elements such as the
+ subnet, router, interface router, and network objects can be accessed.
+ """
+ # Check for OS for network existence
+ # If exists return network instance data
+ # Else, create network and return instance data
+ logger.info('Attempting to create router with name - ' + router_settings.name)
+ router_creator = OpenStackRouter(os_creds, router_settings)
+ router_creator.create(cleanup)
+ logger.info('Created router ')
+ return router_creator
+
+
+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 cleanup: Denotes whether or not this is being called for cleanup or not
+ :return: A reference to the keypair creator object
+ """
+ keypair_creator = OpenStackKeypair(os_creds, keypair_settings)
+ keypair_creator.create(cleanup)
+ return keypair_creator
+
+
+def create_vm_instance(os_creds, instance_settings, image_settings, keypair_creator=None, cleanup=False):
+ """
+ Creates a VM instance
+ :param os_creds: The OpenStack credentials
+ :param instance_settings: Instance of VmInstanceSettings
+ :param image_settings: The object containing image settings
+ :param keypair_creator: The object responsible for creating the keypair associated with this VM instance. (optional)
+ :param sg_names: The names of the security groups to apply to VM. (optional)
+ :param cleanup: Denotes whether or not this is being called for cleanup or not (default False)
+ :return: A reference to the VM instance object
+ """
+ kp_settings = None
+ if keypair_creator:
+ kp_settings = keypair_creator.keypair_settings
+ vm_creator = OpenStackVmInstance(os_creds, instance_settings, image_settings, kp_settings)
+ vm_creator.create(cleanup=cleanup)
+ return vm_creator
+
+
+def create_user(os_creds, user_settings):
+ """
+ Creates an OpenStack user
+ :param os_creds: The OpenStack credentials
+ :param user_settings: The user configuration settings
+ :return: A reference to the user instance object
+ """
+ user_creator = OpenStackUser(os_creds, user_settings)
+ user_creator.create()
+ return user_creator
+
+
+def create_project(os_creds, project_settings):
+ """
+ Creates an OpenStack user
+ :param os_creds: The OpenStack credentials
+ :param project_settings: The user project configuration settings
+ :return: A reference to the project instance object
+ """
+ project_creator = OpenStackProject(os_creds, project_settings)
+ project_creator.create()
+ return project_creator
+
+
+def create_security_group(os_creds, sec_grp_settings):
+ """
+ Creates an OpenStack Security Group
+ :param os_creds: The OpenStack credentials
+ :param sec_grp_settings: The security group settings
+ :return: A reference to the project instance object
+ """
+ sg_creator = OpenStackSecurityGroup(os_creds, sec_grp_settings)
+ sg_creator.create()
+ return sg_creator
+
diff --git a/snaps/openstack/utils/glance_utils.py b/snaps/openstack/utils/glance_utils.py
new file mode 100644
index 0000000..6d90d3e
--- /dev/null
+++ b/snaps/openstack/utils/glance_utils.py
@@ -0,0 +1,78 @@
+# 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 logging
+
+from snaps import file_utils
+from glanceclient.client import Client
+from snaps.openstack.utils import keystone_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('glance_utils')
+
+"""
+Utilities for basic neutron API calls
+"""
+
+
+def glance_client(os_creds):
+ """
+ Creates and returns a glance client object
+ :return: the glance client
+ """
+ return Client(version=os_creds.image_api_version, session=keystone_utils.keystone_session(os_creds))
+
+
+def get_image(nova, glance, image_name):
+ """
+ Returns an OpenStack image object for a given name
+ :param nova: the Nova client
+ :param glance: the Glance client
+ :param image_name: the image name to lookup
+ :return: the image object or None
+ """
+ try:
+ image_dict = nova.images.find(name=image_name)
+ if image_dict:
+ return glance.images.get(image_dict.id)
+ except:
+ pass
+ return None
+
+
+def create_image(glance, image_settings):
+ """
+ Creates and returns OpenStack image object with an external URL
+ :param glance: the glance client
+ :param image_settings: the image settings object
+ :return: the OpenStack image object
+ :raise Exception if using a file and it cannot be found
+ """
+ if image_settings.url:
+ return glance.images.create(name=image_settings.name, disk_format=image_settings.format,
+ container_format="bare", location=image_settings.url)
+ elif image_settings.image_file:
+ image_file = file_utils.get_file(image_settings.image_file)
+ return glance.images.create(name=image_settings.name, disk_format=image_settings.format,
+ container_format="bare", data=image_file)
+
+
+def delete_image(glance, image):
+ """
+ Deletes an image from OpenStack
+ :param glance: the glance client
+ :param image: the image to delete
+ """
+ glance.images.delete(image)
diff --git a/snaps/openstack/utils/keystone_utils.py b/snaps/openstack/utils/keystone_utils.py
new file mode 100644
index 0000000..8175b9a
--- /dev/null
+++ b/snaps/openstack/utils/keystone_utils.py
@@ -0,0 +1,204 @@
+# 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 requests
+from keystoneclient.client import Client
+from keystoneauth1.identity import v3, v2
+from keystoneauth1 import session
+import logging
+
+
+logger = logging.getLogger('keystone_utils')
+
+V2_VERSION = 'v2.0'
+
+
+def keystone_session(os_creds):
+ """
+ Creates a keystone session used for authenticating OpenStack clients
+ :param os_creds: The connection credentials to the OpenStack API
+ :return: the client object
+ """
+ logger.debug('Retrieving Keystone Session')
+
+ if os_creds.identity_api_version == 3:
+ auth = v3.Password(auth_url=os_creds.auth_url, username=os_creds.username, password=os_creds.password,
+ project_name=os_creds.project_name, user_domain_id=os_creds.user_domain_id,
+ project_domain_id=os_creds.project_domain_id)
+ else:
+ auth = v2.Password(auth_url=os_creds.auth_url, username=os_creds.username, password=os_creds.password,
+ tenant_name=os_creds.project_name)
+
+ req_session = None
+ if os_creds.proxy_settings:
+ req_session = requests.Session()
+ req_session.proxies = {'http': os_creds.proxy_settings.host + ':' + os_creds.proxy_settings.port}
+ return session.Session(auth=auth, session=req_session)
+
+
+def keystone_client(os_creds):
+ """
+ Returns the keystone client
+ :param os_creds: the OpenStack credentials (OSCreds) object
+ :return: the client
+ """
+ return Client(version=os_creds.identity_api_version, session=keystone_session(os_creds))
+
+
+def get_project(keystone=None, os_creds=None, project_name=None):
+ """
+ Returns the first project object or None if not found
+ :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_name: the name to query
+ :return: the ID or None
+ """
+ if not project_name:
+ return None
+
+ if not keystone:
+ if os_creds:
+ keystone = keystone_client(os_creds)
+ else:
+ raise Exception('Cannot lookup project without the proper credentials')
+
+ if keystone.version == V2_VERSION:
+ projects = keystone.tenants.list()
+ else:
+ projects = keystone.projects.list(**{'name': project_name})
+
+ for project in projects:
+ if project.name == project_name:
+ return project
+
+ return None
+
+
+def create_project(keystone, project_settings):
+ """
+ Creates a project
+ :param keystone: the Keystone client
+ :param project_settings: the project configuration
+ :return:
+ """
+ if keystone.version == V2_VERSION:
+ return keystone.tenants.create(project_settings.name, project_settings.description, project_settings.enabled)
+
+ return keystone.projects.create(project_settings.name, project_settings.domain,
+ description=project_settings.description,
+ enabled=project_settings.enabled)
+
+
+def delete_project(keystone, project):
+ """
+ Deletes a project
+ :param keystone: the Keystone clien
+ :param project: the OpenStack project object
+ """
+ if keystone.version == V2_VERSION:
+ keystone.tenants.delete(project)
+ else:
+ keystone.projects.delete(project)
+
+
+def get_user(keystone, username, project_name=None):
+ """
+ Returns a user for a given name and optionally project
+ :param keystone: the keystone client
+ :param username: the username to lookup
+ :param project_name: the associated project (optional)
+ :return:
+ """
+ project = get_project(keystone=keystone, project_name=project_name)
+
+ if project:
+ users = keystone.users.list(tenant_id=project.id)
+ else:
+ users = keystone.users.list()
+
+ for user in users:
+ if user.name == username:
+ return user
+
+ return None
+
+
+def create_user(keystone, user_settings):
+ """
+ Creates a user
+ :param keystone: the Keystone client
+ :param user_settings: the user configuration
+ :return:
+ """
+ project = None
+ if user_settings.project_name:
+ project = get_project(keystone=keystone, project_name=user_settings.project_name)
+
+ if keystone.version == V2_VERSION:
+ project_id = None
+ if project:
+ project_id = project.id
+ return keystone.users.create(name=user_settings.name, password=user_settings.password,
+ email=user_settings.email, tenant_id=project_id, enabled=user_settings.enabled)
+ else:
+ # TODO - need to support groups
+ return keystone.users.create(name=user_settings.name, password=user_settings.password,
+ email=user_settings.email, project=project,
+ # email=user_settings.email, project=project, group='default',
+ domain=user_settings.domain_name,
+ enabled=user_settings.enabled)
+
+
+def delete_user(keystone, user):
+ """
+ Deletes a user
+ :param keystone: the Keystone client
+ :param user: the OpenStack user object
+ """
+ keystone.users.delete(user)
+
+
+def create_role(keystone, name):
+ """
+ Creates an OpenStack role
+ :param keystone: the keystone client
+ :param name: the role name
+ :return:
+ """
+ return keystone.roles.create(name)
+
+
+def delete_role(keystone, role):
+ """
+ Deletes an OpenStack role
+ :param keystone: the keystone client
+ :param role: the role to delete
+ :return:
+ """
+ keystone.roles.delete(role)
+
+
+def assoc_user_to_project(keystone, role, user, project):
+ """
+ Adds a user to a project
+ :param keystone: the Keystone client
+ :param role: the role used to join a project/user
+ :param user: the user to add to the project
+ :param project: the project to which to add a user
+ :return:
+ """
+ if keystone.version == V2_VERSION:
+ keystone.roles.add_user_role(user, role, tenant=project)
+ else:
+ keystone.roles.grant(role, user=user, project=project)
diff --git a/snaps/openstack/utils/neutron_utils.py b/snaps/openstack/utils/neutron_utils.py
new file mode 100644
index 0000000..6c92d2e
--- /dev/null
+++ b/snaps/openstack/utils/neutron_utils.py
@@ -0,0 +1,405 @@
+# 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 logging
+
+from neutronclient.common.exceptions import NotFound
+from neutronclient.neutron.client import Client
+import keystone_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('neutron_utils')
+
+"""
+Utilities for basic neutron API calls
+"""
+
+
+def neutron_client(os_creds):
+ """
+ Instantiates and returns a client for communications with OpenStack's Neutron server
+ :param os_creds: the credentials for connecting to the OpenStack remote API
+ :return: the client object
+ """
+ return Client(api_version=os_creds.network_api_version, session=keystone_utils.keystone_session(os_creds))
+
+
+def create_network(neutron, os_creds, network_settings):
+ """
+ Creates a network for OpenStack
+ :param neutron: the client
+ :param os_creds: the OpenStack credentials
+ :param network_settings: A dictionary containing the network configuration and is responsible for creating the
+ network request JSON body
+ :return: the network object
+ """
+ if neutron and network_settings:
+ logger.info('Creating network with name ' + network_settings.name)
+ json_body = network_settings.dict_for_neutron(os_creds)
+ return neutron.create_network(body=json_body)
+ else:
+ logger.error("Failed to create network")
+ raise Exception
+
+
+def delete_network(neutron, network):
+ """
+ Deletes a network for OpenStack
+ :param neutron: the client
+ :param network: the network object
+ """
+ if neutron and network:
+ logger.info('Deleting network with name ' + network['network']['name'])
+ neutron.delete_network(network['network']['id'])
+
+
+def get_network(neutron, network_name, project_id=None):
+ """
+ Returns an object (dictionary) of the first network found with a given name and project_id (if included)
+ :param neutron: the client
+ :param network_name: the name of the network to retrieve
+ :param project_id: the id of the network's project
+ :return:
+ """
+ net_filter = dict()
+ if network_name:
+ net_filter['name'] = network_name
+ if project_id:
+ net_filter['project_id'] = project_id
+
+ networks = neutron.list_networks(**net_filter)
+ for network, netInsts in networks.iteritems():
+ for inst in netInsts:
+ if inst.get('name') == network_name:
+ if project_id and inst.get('project_id') == project_id:
+ return {'network': inst}
+ else:
+ return {'network': inst}
+ return None
+
+
+def get_network_by_id(neutron, network_id):
+ """
+ Returns the network object (dictionary) with the given ID
+ :param neutron: the client
+ :param network_id: the id of the network to retrieve
+ :return:
+ """
+ networks = neutron.list_networks(**{'id': network_id})
+ for network, netInsts in networks.iteritems():
+ for inst in netInsts:
+ if inst.get('id') == network_id:
+ return {'network': inst}
+ return None
+
+
+def create_subnet(neutron, subnet_settings, os_creds, network=None):
+ """
+ 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
+ :return: the subnet object
+ """
+ if neutron and network and subnet_settings:
+ json_body = {'subnets': [subnet_settings.dict_for_neutron(os_creds, network=network)]}
+ logger.info('Creating subnet with name ' + subnet_settings.name)
+ subnets = neutron.create_subnet(body=json_body)
+ return {'subnet': subnets['subnets'][0]}
+ else:
+ logger.error("Failed to create subnet.")
+ raise Exception
+
+
+def delete_subnet(neutron, subnet):
+ """
+ Deletes a network subnet for OpenStack
+ :param neutron: the client
+ :param subnet: the subnet object
+ """
+ if neutron and subnet:
+ logger.info('Deleting subnet with name ' + subnet['subnet']['name'])
+ neutron.delete_subnet(subnet['subnet']['id'])
+
+
+def get_subnet_by_name(neutron, subnet_name):
+ """
+ Returns the first subnet object (dictionary) found with a given name
+ :param neutron: the client
+ :param subnet_name: the name of the network to retrieve
+ :return:
+ """
+ subnets = neutron.list_subnets(**{'name': subnet_name})
+ for subnet, subnetInst in subnets.iteritems():
+ for inst in subnetInst:
+ if inst.get('name') == subnet_name:
+ return {'subnet': inst}
+ return None
+
+
+def create_router(neutron, os_creds, router_settings):
+ """
+ Creates a router for OpenStack
+ :param neutron: the client
+ :param os_creds: the OpenStack credentials
+ :param router_settings: A dictionary containing the router configuration and is responsible for creating the subnet
+ request JSON body
+ :return: the router object
+ """
+ if neutron:
+ json_body = router_settings.dict_for_neutron(neutron, os_creds)
+ logger.info('Creating router with name - ' + router_settings.name)
+ return neutron.create_router(json_body)
+ else:
+ logger.error("Failed to create router.")
+ raise Exception
+
+
+def delete_router(neutron, router):
+ """
+ Deletes a router for OpenStack
+ :param neutron: the client
+ :param router: the router object
+ """
+ if neutron and router:
+ logger.info('Deleting router with name - ' + router['router']['name'])
+ neutron.delete_router(router=router['router']['id'])
+ return True
+
+
+def get_router_by_name(neutron, router_name):
+ """
+ Returns the first router object (dictionary) found with a given name
+ :param neutron: the client
+ :param router_name: the name of the network to retrieve
+ :return:
+ """
+ routers = neutron.list_routers(**{'name': router_name})
+ for router, routerInst in routers.iteritems():
+ for inst in routerInst:
+ if inst.get('name') == router_name:
+ return {'router': inst}
+ return None
+
+
+def add_interface_router(neutron, router, subnet=None, port=None):
+ """
+ Adds an interface router for OpenStack for either a subnet or port. Exception will be raised if requesting for both.
+ :param neutron: the client
+ :param router: the router object
+ :param subnet: the subnet object
+ :param port: the port object
+ :return: the interface router object
+ """
+ if subnet and port:
+ raise Exception('Cannot add interface to the router. Both subnet and port were sent in. Either or please.')
+
+ if neutron and router and (router or subnet):
+ logger.info('Adding interface to router with name ' + router['router']['name'])
+ return neutron.add_interface_router(router=router['router']['id'], body=__create_port_json_body(subnet, port))
+ else:
+ raise Exception("Unable to create interface router as neutron client, router or subnet were not created")
+
+
+def remove_interface_router(neutron, router, subnet=None, port=None):
+ """
+ Removes an interface router for OpenStack
+ :param neutron: the client
+ :param router: the router object
+ :param subnet: the subnet object (either subnet or port, not both)
+ :param port: the port object
+ """
+ if router:
+ try:
+ logger.info('Removing router interface from router named ' + router['router']['name'])
+ neutron.remove_interface_router(router=router['router']['id'], body=__create_port_json_body(subnet, port))
+ except NotFound as e:
+ logger.warn('Could not remove router interface. NotFound - ' + e.message)
+ pass
+ else:
+ logger.warn('Could not remove router interface, No router object')
+
+
+def __create_port_json_body(subnet=None, port=None):
+ """
+ Returns the dictionary required for creating and deleting router interfaces. Will only work on a subnet or port
+ object. Will throw and exception if parameters contain both or neither
+ :param subnet: the subnet object
+ :param port: the port object
+ :return: the dict
+ """
+ if subnet and port:
+ raise Exception('Cannot create JSON body with both subnet and port')
+ if not subnet and not port:
+ raise Exception('Cannot create JSON body without subnet or port')
+
+ if subnet:
+ return {"subnet_id": subnet['subnet']['id']}
+ else:
+ return {"port_id": port['port']['id']}
+
+
+def create_port(neutron, os_creds, port_settings):
+ """
+ Creates a port for OpenStack
+ :param neutron: the client
+ :param os_creds: the OpenStack credentials
+ :param port_settings: the settings object for port configuration
+ :return: the port object
+ """
+ json_body = port_settings.dict_for_neutron(neutron, os_creds)
+ logger.info('Creating port for network with name - ' + port_settings.network_name)
+ return neutron.create_port(body=json_body)
+
+
+def delete_port(neutron, port):
+ """
+ Removes an OpenStack port
+ :param neutron: the client
+ :param port: the port object
+ :return:
+ """
+ logger.info('Deleting port with name ' + port['port']['name'])
+ neutron.delete_port(port['port']['id'])
+
+
+def get_port_by_name(neutron, port_name):
+ """
+ Returns the first port object (dictionary) found with a given name
+ :param neutron: the client
+ :param port_name: the name of the port to retrieve
+ :return:
+ """
+ ports = neutron.list_ports(**{'name': port_name})
+ for port in ports['ports']:
+ if port['name'] == port_name:
+ return {'port': port}
+ return None
+
+
+def create_security_group(neutron, keystone, sec_grp_settings):
+ """
+ Creates a security group object in OpenStack
+ :param neutron: the Neutron client
+ :param keystone: the Keystone client
+ :param sec_grp_settings: the security group settings
+ :return: the security group object
+ """
+ logger.info('Creating security group with name - ' + sec_grp_settings.name)
+ return neutron.create_security_group(sec_grp_settings.dict_for_neutron(keystone))
+
+
+def delete_security_group(neutron, sec_grp):
+ """
+ Deletes a security group object from OpenStack
+ :param neutron: the client
+ :param sec_grp: the security group object to delete
+ """
+ logger.info('Deleting security group with name - ' + sec_grp['security_group']['name'])
+ return neutron.delete_security_group(sec_grp['security_group']['id'])
+
+
+def get_security_group(neutron, name):
+ """
+ Returns the first security group object of the given name else None
+ :param neutron: the client
+ :param name: the name of security group object to retrieve
+ """
+ logger.info('Retrieving security group with name - ' + name)
+
+ groups = neutron.list_security_groups(**{'name': name})
+ for group in groups['security_groups']:
+ if group['name'] == name:
+ return {'security_group': group}
+ return None
+
+
+def get_security_group_by_id(neutron, sec_grp_id):
+ """
+ Returns the first security group object of the given name else None
+ :param neutron: the client
+ :param sec_grp_id: the id of the security group to retrieve
+ """
+ logger.info('Retrieving security group with ID - ' + sec_grp_id)
+
+ groups = neutron.list_security_groups(**{'sec_grp_id': sec_grp_id})
+ for group in groups['security_groups']:
+ return {'security_group': group}
+ return None
+
+
+def create_security_group_rule(neutron, sec_grp_rule_settings):
+ """
+ Creates a security group object in OpenStack
+ :param neutron: the client
+ :param sec_grp_rule_settings: the security group rule settings
+ :return: the security group object
+ """
+ logger.info('Creating security group to security group - ' + sec_grp_rule_settings.sec_grp_name)
+ return neutron.create_security_group_rule(sec_grp_rule_settings.dict_for_neutron(neutron))
+
+
+def delete_security_group_rule(neutron, sec_grp_rule):
+ """
+ Deletes a security group object from OpenStack
+ :param neutron: the client
+ :param sec_grp_rule: the security group rule object to delete
+ """
+ logger.info('Deleting security group rule with ID - ' + sec_grp_rule['security_group_rule']['id'])
+ neutron.delete_security_group_rule(sec_grp_rule['security_group_rule']['id'])
+
+
+def get_rules_by_security_group(neutron, sec_grp):
+ """
+ Retrieves all of the rules for a given security group
+ :param neutron: the client
+ :param sec_grp: the security group object
+ """
+ logger.info('Retrieving security group rules associate with the security group - ' +
+ sec_grp['security_group']['name'])
+ out = list()
+ rules = neutron.list_security_group_rules(**{'security_group_id': sec_grp['security_group']['id']})
+ for rule in rules['security_group_rules']:
+ if rule['security_group_id'] == sec_grp['security_group']['id']:
+ out.append({'security_group_rule': rule})
+ return out
+
+
+def get_rule_by_id(neutron, sec_grp, rule_id):
+ """
+ Deletes a security group object from OpenStack
+ :param neutron: the client
+ :param sec_grp: the security group object
+ :param rule_id: the rule's ID
+ """
+ rules = neutron.list_security_group_rules(**{'security_group_id': sec_grp['security_group']['id']})
+ for rule in rules['security_group_rules']:
+ if rule['id'] == rule_id:
+ return {'security_group_rule': rule}
+ return None
+
+
+def get_external_networks(neutron):
+ """
+ Returns a list of external OpenStack network object/dict for all external networks
+ :param neutron: the client
+ :return: a list of external networks (empty list if none configured)
+ """
+ out = list()
+ for network in neutron.list_networks(**{'router:external': True})['networks']:
+ out.append({'network': network})
+ return out
diff --git a/snaps/openstack/utils/nova_utils.py b/snaps/openstack/utils/nova_utils.py
new file mode 100644
index 0000000..9d0f70f
--- /dev/null
+++ b/snaps/openstack/utils/nova_utils.py
@@ -0,0 +1,282 @@
+# 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 os
+import logging
+import keystone_utils
+
+from novaclient.client import Client
+from novaclient.exceptions import NotFound
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('nova_utils')
+
+"""
+Utilities for basic OpenStack Nova API calls
+"""
+
+
+def nova_client(os_creds):
+ """
+ Instantiates and returns a client for communications with OpenStack's Nova server
+ :param os_creds: The connection credentials to the OpenStack API
+ :return: the client object
+ """
+ logger.debug('Retrieving Nova Client')
+ return Client(os_creds.compute_api_version, session=keystone_utils.keystone_session(os_creds))
+
+
+def get_servers_by_name(nova, name):
+ """
+ Returns a list of servers with a given name
+ :param nova: the Nova client
+ :param name: the server name
+ :return: the list of servers
+ """
+ return nova.servers.list(search_opts={'name': name})
+
+
+def get_latest_server_object(nova, server):
+ """
+ Returns a server with a given id
+ :param nova: the Nova client
+ :param server: the old server object
+ :return: the list of servers or None if not found
+ """
+ return nova.servers.get(server)
+
+
+def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None):
+ """
+ Saves the generated RSA generated keys to the filesystem
+ :param keys: the keys to save
+ :param pub_file_path: the path to the public keys
+ :param priv_file_path: the path to the private keys
+ :return: None
+ """
+ if keys:
+ if pub_file_path:
+ pub_dir = os.path.dirname(pub_file_path)
+ if not os.path.isdir(pub_dir):
+ os.mkdir(pub_dir)
+ public_handle = open(pub_file_path, 'wb')
+ public_handle.write(keys.publickey().exportKey('OpenSSH'))
+ public_handle.close()
+ os.chmod(pub_file_path, 0o400)
+ logger.info("Saved public key to - " + pub_file_path)
+ if priv_file_path:
+ priv_dir = os.path.dirname(priv_file_path)
+ if not os.path.isdir(priv_dir):
+ os.mkdir(priv_dir)
+ private_handle = open(priv_file_path, 'wb')
+ private_handle.write(keys.exportKey())
+ private_handle.close()
+ os.chmod(priv_file_path, 0o400)
+ logger.info("Saved private key to - " + priv_file_path)
+
+
+def upload_keypair_file(nova, name, file_path):
+ """
+ Uploads a public key from a file
+ :param nova: the Nova client
+ :param name: the keypair name
+ :param file_path: the path to the public key file
+ :return: the keypair object
+ """
+ with open(os.path.expanduser(file_path)) as fpubkey:
+ logger.info('Saving keypair to - ' + file_path)
+ return upload_keypair(nova, name, fpubkey.read())
+
+
+def upload_keypair(nova, name, key):
+ """
+ Uploads a public key from a file
+ :param nova: the Nova client
+ :param name: the keypair name
+ :param key: the public key object
+ :return: the keypair object
+ """
+ logger.info('Creating keypair with name - ' + name)
+ return nova.keypairs.create(name=name, public_key=key)
+
+
+def keypair_exists(nova, keypair_obj):
+ """
+ Returns a copy of the keypair object if found
+ :param nova: the Nova client
+ :param keypair_obj: the keypair object
+ :return: the keypair object or None if not found
+ """
+ try:
+ return nova.keypairs.get(keypair_obj)
+ except:
+ return None
+
+
+def get_keypair_by_name(nova, name):
+ """
+ Returns a list of all available keypairs
+ :param nova: the Nova client
+ :param name: the name of the keypair to lookup
+ :return: the keypair object or None if not found
+ """
+ keypairs = nova.keypairs.list()
+
+ for keypair in keypairs:
+ if keypair.name == name:
+ return keypair
+
+ return None
+
+
+def delete_keypair(nova, key):
+ """
+ Deletes a keypair object from OpenStack
+ :param nova: the Nova client
+ :param key: the keypair object to delete
+ """
+ logger.debug('Deleting keypair - ' + key.name)
+ nova.keypairs.delete(key)
+
+
+def get_floating_ip_pools(nova):
+ """
+ Returns all of the available floating IP pools
+ :param nova: the Nova client
+ :return: a list of pools
+ """
+ return nova.floating_ip_pools.list()
+
+
+def get_floating_ips(nova):
+ """
+ Returns all of the floating IPs
+ :param nova: the Nova client
+ :return: a list of floating IPs
+ """
+ return nova.floating_ips.list()
+
+
+def create_floating_ip(nova, ext_net_name):
+ """
+ Returns the floating IP object that was created with this call
+ :param nova: the Nova client
+ :param ext_net_name: the name of the external network on which to apply the floating IP address
+ :return: the floating IP object
+ """
+ logger.info('Creating floating ip to external network - ' + ext_net_name)
+ return nova.floating_ips.create(ext_net_name)
+
+
+def get_floating_ip(nova, floating_ip):
+ """
+ Returns a floating IP object that should be identical to the floating_ip parameter
+ :param nova: the Nova client
+ :param floating_ip: the floating IP object to lookup
+ :return: hopefully the same floating IP object input
+ """
+ logger.debug('Attempting to retrieve existing floating ip with IP - ' + floating_ip.ip)
+ return nova.floating_ips.get(floating_ip)
+
+
+def delete_floating_ip(nova, floating_ip):
+ """
+ Responsible for deleting a floating IP
+ :param nova: the Nova client
+ :param floating_ip: the floating IP object to delete
+ :return:
+ """
+ logger.debug('Attempting to delete existing floating ip with IP - ' + floating_ip.ip)
+ return nova.floating_ips.delete(floating_ip)
+
+
+def get_nova_availability_zones(nova):
+ """
+ Returns the names of all nova compute servers
+ :param nova: the Nova client
+ :return: a list of compute server names
+ """
+ out = list()
+ zones = nova.availability_zones.list()
+ for zone in zones:
+ if zone.zoneName == 'nova':
+ for key, host in zone.hosts.iteritems():
+ out.append(zone.zoneName + ':' + key)
+
+ return out
+
+
+def delete_vm_instance(nova, vm_inst):
+ """
+ Deletes a VM instance
+ :param nova: the nova client
+ :param vm_inst: the OpenStack instance object to delete
+ """
+ nova.servers.delete(vm_inst)
+
+
+def get_flavor_by_name(nova, name):
+ """
+ Returns a flavor by name
+ :param nova: the Nova client
+ :param name: the flavor name to return
+ :return: the OpenStack flavor object or None if not exists
+ """
+ try:
+ return nova.flavors.find(name=name)
+ except NotFound:
+ return None
+
+
+def create_flavor(nova, flavor_settings):
+ """
+ Creates and returns and OpenStack flavor object
+ :param nova: the Nova client
+ :param flavor_settings: the flavor settings
+ :return: the Flavor
+ """
+ return nova.flavors.create(name=flavor_settings.name, flavorid=flavor_settings.flavor_id, ram=flavor_settings.ram,
+ vcpus=flavor_settings.vcpus, disk=flavor_settings.disk,
+ ephemeral=flavor_settings.ephemeral, swap=flavor_settings.swap,
+ rxtx_factor=flavor_settings.rxtx_factor, is_public=flavor_settings.is_public)
+
+
+def delete_flavor(nova, flavor):
+ """
+ Deletes a flavor
+ :param nova: the Nova client
+ :param flavor: the OpenStack flavor object
+ """
+ nova.flavors.delete(flavor)
+
+
+def add_security_group(nova, vm, security_group_name):
+ """
+ Adds a security group to an existing VM
+ :param nova: the nova client
+ :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(vm.id, security_group_name)
+
+
+def remove_security_group(nova, vm, security_group):
+ """
+ Removes a security group from an existing VM
+ :param nova: the nova client
+ :param vm: the OpenStack server object (VM) to alter
+ :param security_group: the OpenStack security group object to add
+ """
+ nova.servers.remove_security_group(vm.id, security_group)
diff --git a/snaps/openstack/utils/tests/__init__.py b/snaps/openstack/utils/tests/__init__.py
new file mode 100644
index 0000000..7f92908
--- /dev/null
+++ b/snaps/openstack/utils/tests/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski' \ No newline at end of file
diff --git a/snaps/openstack/utils/tests/glance_utils_tests.py b/snaps/openstack/utils/tests/glance_utils_tests.py
new file mode 100644
index 0000000..d13908b
--- /dev/null
+++ b/snaps/openstack/utils/tests/glance_utils_tests.py
@@ -0,0 +1,115 @@
+# 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 os
+import shutil
+import uuid
+
+from snaps import file_utils
+from snaps.openstack.tests import openstack_tests
+
+from snaps.openstack.utils import nova_utils
+from snaps.openstack.tests import validation_utils
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
+from snaps.openstack.utils import glance_utils
+
+__author__ = 'spisarski'
+
+
+class GlanceSmokeTests(OSComponentTestCase):
+ """
+ Tests to ensure that the neutron client can communicate with the cloud
+ """
+
+ def test_glance_connect_success(self):
+ """
+ Tests to ensure that the proper credentials can connect.
+ """
+ glance = glance_utils.glance_client(self.os_creds)
+
+ users = glance.images.list()
+ self.assertIsNotNone(users)
+
+ def test_glance_connect_fail(self):
+ """
+ Tests to ensure that the improper credentials cannot connect.
+ """
+ from snaps.openstack.os_credentials import OSCreds
+
+ with self.assertRaises(Exception):
+ neutron = glance_utils.glance_client(OSCreds('user', 'pass', 'url', 'project'))
+ neutron.list_networks()
+
+
+class GlanceUtilsTests(OSComponentTestCase):
+ """
+ Test for the CreateImage class defined in create_image.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = uuid.uuid4()
+ self.image_name = self.__class__.__name__ + '-' + str(guid)
+ self.image = None
+ self.nova = nova_utils.nova_client(self.os_creds)
+ self.glance = glance_utils.glance_client(self.os_creds)
+
+ self.tmp_dir = 'tmp/' + str(guid)
+ if not os.path.exists(self.tmp_dir):
+ os.makedirs(self.tmp_dir)
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects
+ """
+ if self.image:
+ glance_utils.delete_image(self.glance, self.image)
+
+ if os.path.exists(self.tmp_dir) and os.path.isdir(self.tmp_dir):
+ shutil.rmtree(self.tmp_dir)
+
+ def test_create_image_minimal_url(self):
+ """
+ Tests the glance_utils.create_image() function with a URL
+ """
+ os_image_settings = openstack_tests.cirros_url_image(name=self.image_name)
+
+ self.image = glance_utils.create_image(self.glance, os_image_settings)
+ self.assertIsNotNone(self.image)
+
+ self.assertEqual(self.image_name, self.image.name)
+
+ image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name)
+ self.assertIsNotNone(image)
+
+ validation_utils.objects_equivalent(self.image, image)
+
+ def test_create_image_minimal_file(self):
+ """
+ Tests the glance_utils.create_image() function with a file
+ """
+ url_image_settings = openstack_tests.cirros_url_image('foo')
+ image_file = file_utils.download(url_image_settings.url, self.tmp_dir)
+ file_image_settings = openstack_tests.file_image_test_settings(name=self.image_name, file_path=image_file.name)
+
+ self.image = glance_utils.create_image(self.glance, file_image_settings)
+ self.assertIsNotNone(self.image)
+ self.assertEqual(self.image_name, self.image.name)
+
+ image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name)
+ self.assertIsNotNone(image)
+ validation_utils.objects_equivalent(self.image, image)
diff --git a/snaps/openstack/utils/tests/keystone_utils_tests.py b/snaps/openstack/utils/tests/keystone_utils_tests.py
new file mode 100644
index 0000000..76a43ef
--- /dev/null
+++ b/snaps/openstack/utils/tests/keystone_utils_tests.py
@@ -0,0 +1,100 @@
+# 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 uuid
+
+from snaps.openstack.create_project import ProjectSettings
+from snaps.openstack.create_user import UserSettings
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
+from snaps.openstack.utils import keystone_utils
+
+__author__ = 'spisarski'
+
+
+class KeystoneSmokeTests(OSComponentTestCase):
+ """
+ Tests to ensure that the neutron client can communicate with the cloud
+ """
+
+ def test_keystone_connect_success(self):
+ """
+ Tests to ensure that the proper credentials can connect.
+ """
+ keystone = keystone_utils.keystone_client(self.os_creds)
+
+ users = keystone.users.list()
+ self.assertIsNotNone(users)
+
+ def test_keystone_connect_fail(self):
+ """
+ Tests to ensure that the improper credentials cannot connect.
+ """
+ from snaps.openstack.os_credentials import OSCreds
+
+ with self.assertRaises(Exception):
+ keystone = keystone_utils.keystone_client(OSCreds('user', 'pass', 'url', 'project'))
+ keystone.users.list()
+
+
+class KeystoneUtilsTests(OSComponentTestCase):
+ """
+ Test for the CreateImage class defined in create_image.py
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = uuid.uuid4()
+ self.username = self.__class__.__name__ + '-' + str(guid)
+ self.user = None
+
+ self.project_name = self.__class__.__name__ + '-' + str(guid)
+ self.project = None
+ self.keystone = keystone_utils.keystone_client(self.os_creds)
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects
+ """
+ if self.project:
+ keystone_utils.delete_project(self.keystone, self.project)
+
+ if self.user:
+ keystone_utils.delete_user(self.keystone, self.user)
+
+ def test_create_user_minimal(self):
+ """
+ Tests the keystone_utils.create_user() function
+ """
+ user_settings = UserSettings(name=self.username, password='test123')
+ self.user = keystone_utils.create_user(self.keystone, user_settings)
+ self.assertEqual(self.username, self.user.name)
+
+ user = keystone_utils.get_user(self.keystone, self.username)
+ self.assertIsNotNone(user)
+ self.assertEqual(self.user, user)
+
+ def test_create_project_minimal(self):
+ """
+ Tests the keyston_utils.create_project() funtion
+ """
+ project_settings = ProjectSettings(name=self.project_name)
+ self.project = keystone_utils.create_project(self.keystone, project_settings)
+ self.assertEquals(self.project_name, self.project.name)
+
+ project = keystone_utils.get_project(keystone=self.keystone, project_name=project_settings.name)
+ self.assertIsNotNone(project)
+ self.assertEquals(self.project_name, self.project.name)
diff --git a/snaps/openstack/utils/tests/neutron_utils_tests.py b/snaps/openstack/utils/tests/neutron_utils_tests.py
new file mode 100644
index 0000000..5f95fc9
--- /dev/null
+++ b/snaps/openstack/utils/tests/neutron_utils_tests.py
@@ -0,0 +1,651 @@
+# 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 uuid
+
+from snaps.openstack.utils import keystone_utils
+from snaps.openstack.create_security_group import SecurityGroupSettings, SecurityGroupRuleSettings, Direction
+from snaps.openstack.tests import openstack_tests
+from snaps.openstack.utils import neutron_utils
+from snaps.openstack.create_network import NetworkSettings, SubnetSettings, PortSettings
+from snaps.openstack import create_router
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
+from snaps.openstack.tests import validation_utils
+
+__author__ = 'spisarski'
+
+ip_1 = '10.55.1.100'
+ip_2 = '10.55.1.200'
+
+
+class NeutronSmokeTests(OSComponentTestCase):
+ """
+ Tests to ensure that the neutron client can communicate with the cloud
+ """
+
+ def test_neutron_connect_success(self):
+ """
+ Tests to ensure that the proper credentials can connect.
+ """
+ neutron = neutron_utils.neutron_client(self.os_creds)
+
+ networks = neutron.list_networks()
+
+ found = False
+ networks = networks.get('networks')
+ for network in networks:
+ if network.get('name') == self.ext_net_name:
+ found = True
+ self.assertTrue(found)
+
+ def test_neutron_connect_fail(self):
+ """
+ Tests to ensure that the improper credentials cannot connect.
+ """
+ from snaps.openstack.os_credentials import OSCreds
+
+ with self.assertRaises(Exception):
+ neutron = neutron_utils.neutron_client(
+ OSCreds(username='user', password='pass', auth_url='url', project_name='project'))
+ neutron.list_networks()
+
+ def test_retrieve_ext_network_name(self):
+ """
+ Tests the neutron_utils.get_external_network_names to ensure the configured self.ext_net_name is contained
+ within the returned list
+ :return:
+ """
+ neutron = neutron_utils.neutron_client(self.os_creds)
+ ext_networks = neutron_utils.get_external_networks(neutron)
+ found = False
+ for network in ext_networks:
+ if network['network']['name'] == self.ext_net_name:
+ found = True
+ break
+ self.assertTrue(found)
+
+
+class NeutronUtilsNetworkTests(OSComponentTestCase):
+ """
+ Test for creating networks via neutron_utils.py
+ """
+
+ def setUp(self):
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.port_name = str(guid) + '-port'
+ self.neutron = neutron_utils.neutron_client(self.os_creds)
+ self.network = None
+ self.net_config = openstack_tests.get_pub_net_config(net_name=guid + '-pub-net')
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects
+ """
+ if self.network:
+ neutron_utils.delete_network(self.neutron, self.network)
+ validate_network(self.neutron, self.network['network']['name'], False)
+
+ def test_create_network(self):
+ """
+ Tests the neutron_utils.create_neutron_net() function
+ """
+ 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ def test_create_network_empty_name(self):
+ """
+ Tests the neutron_utils.create_neutron_net() function with an empty network name
+ """
+ with self.assertRaises(Exception):
+ self.network = neutron_utils.create_network(self.neutron, NetworkSettings(name=''))
+
+ def test_create_network_null_name(self):
+ """
+ Tests the neutron_utils.create_neutron_net() function when the network name is None
+ """
+ with self.assertRaises(Exception):
+ self.network = neutron_utils.create_network(self.neutron, NetworkSettings())
+
+
+class NeutronUtilsSubnetTests(OSComponentTestCase):
+ """
+ Test for creating networks with subnets via neutron_utils.py
+ """
+
+ def setUp(self):
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ 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)
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects
+ """
+ if self.subnet:
+ neutron_utils.delete_subnet(self.neutron, self.subnet)
+ validate_subnet(self.neutron, self.subnet.get('name'),
+ self.net_config.network_settings.subnet_settings[0].cidr, False)
+
+ if self.network:
+ neutron_utils.delete_network(self.neutron, self.network)
+ validate_network(self.neutron, self.network['network']['name'], False)
+
+ def test_create_subnet(self):
+ """
+ Tests the neutron_utils.create_neutron_net() function
+ """
+ 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, network=self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ def test_create_subnet_null_name(self):
+ """
+ Tests the neutron_utils.create_neutron_subnet() function for an Exception when the subnet name 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ with self.assertRaises(Exception):
+ SubnetSettings(cidr=self.net_config.subnet_cidr)
+
+ def test_create_subnet_empty_name(self):
+ """
+ Tests the neutron_utils.create_neutron_net() function with an empty name
+ """
+ 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, network=self.network)
+ validate_subnet(self.neutron, '', self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ 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)
+
+ 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.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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ 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)
+
+
+class NeutronUtilsRouterTests(OSComponentTestCase):
+ """
+ Test for creating routers via neutron_utils.py
+ """
+
+ def setUp(self):
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ 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
+ 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)
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects
+ """
+ if self.interface_router:
+ neutron_utils.remove_interface_router(self.neutron, self.router, self.subnet)
+
+ if self.router:
+ neutron_utils.delete_router(self.neutron, self.router)
+ validate_router(self.neutron, self.router.get('name'), False)
+
+ if self.port:
+ neutron_utils.delete_port(self.neutron, self.port)
+
+ if self.subnet:
+ neutron_utils.delete_subnet(self.neutron, self.subnet)
+ validate_subnet(self.neutron, self.subnet.get('name'),
+ self.net_config.network_settings.subnet_settings[0].cidr, False)
+
+ if self.network:
+ neutron_utils.delete_network(self.neutron, self.network)
+ validate_network(self.neutron, self.network['network']['name'], False)
+
+ def test_create_router_simple(self):
+ """
+ Tests the neutron_utils.create_neutron_net() function when an external gateway is requested
+ """
+ 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)
+
+ def test_create_router_with_public_interface(self):
+ """
+ Tests the neutron_utils.create_neutron_net() function when an external gateway is requested
+ """
+ self.net_config = openstack_tests.OSNetworkConfig(
+ self.net_config.network_settings.name,
+ self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, self.net_config.router_settings.name,
+ self.ext_net_name)
+ 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)
+ # TODO - Add validation that the router gatway has been set
+
+ 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)
+
+ def test_add_interface_router(self):
+ """
+ Tests the neutron_utils.add_interface_router() function
+ """
+ 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, 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)
+
+ self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet)
+ validate_interface_router(self.interface_router, self.router, self.subnet)
+
+ def test_add_interface_router_null_router(self):
+ """
+ Tests the neutron_utils.add_interface_router() function for an Exception when the router 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ with self.assertRaises(Exception):
+ self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet)
+
+ def test_add_interface_router_null_subnet(self):
+ """
+ Tests the neutron_utils.add_interface_router() function for an Exception when the subnet 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['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)
+
+ with self.assertRaises(Exception):
+ self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet)
+
+ def test_create_port(self):
+ """
+ Tests the neutron_utils.create_port() function
+ """
+ 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ self.port = neutron_utils.create_port(
+ self.neutron, self.os_creds, PortSettings(
+ name=self.port_name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}],
+ network_name=self.net_config.network_settings.name))
+ validate_port(self.neutron, self.port, self.port_name)
+
+ def test_create_port_empty_name(self):
+ """
+ Tests the neutron_utils.create_port() function
+ """
+ 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ self.port = neutron_utils.create_port(
+ self.neutron, self.os_creds, PortSettings(
+ name=self.port_name, network_name=self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}]))
+ validate_port(self.neutron, self.port, self.port_name)
+
+ def test_create_port_null_name(self):
+ """
+ Tests the neutron_utils.create_port() function for an Exception when the port name 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ with self.assertRaises(Exception):
+ self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings(
+ network_name=self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': ip_1}]))
+
+ def test_create_port_null_network_object(self):
+ """
+ Tests the neutron_utils.create_port() function for an Exception when the network object 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ with self.assertRaises(Exception):
+ self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings(
+ self.neutron, self.port_name, self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': ip_1}]))
+
+ def test_create_port_null_ip(self):
+ """
+ Tests the neutron_utils.create_port() function for an Exception when the IP 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ with self.assertRaises(Exception):
+ self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings(
+ name=self.port_name, network_name=self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': None}]))
+
+ def test_create_port_invalid_ip(self):
+ """
+ Tests the neutron_utils.create_port() function for an Exception when the IP 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ with self.assertRaises(Exception):
+ self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings(
+ name=self.port_name, network_name=self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': 'foo'}]))
+
+ def test_create_port_invalid_ip_to_subnet(self):
+ """
+ Tests the neutron_utils.create_port() function for an Exception when the IP 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['network']['name'])
+ self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True))
+
+ self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0],
+ self.os_creds, self.network)
+ validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name,
+ self.net_config.network_settings.subnet_settings[0].cidr, True)
+
+ with self.assertRaises(Exception):
+ self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings(
+ name=self.port_name, network_name=self.net_config.network_settings.name,
+ ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name,
+ 'ip': '10.197.123.100'}]))
+
+
+class NeutronUtilsSecurityGroupTests(OSComponentTestCase):
+ """
+ Test for creating security groups via neutron_utils.py
+ """
+
+ def setUp(self):
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.sec_grp_name = guid + 'name'
+
+ self.security_group = None
+ self.security_group_rules = list()
+ self.neutron = neutron_utils.neutron_client(self.os_creds)
+ self.keystone = keystone_utils.keystone_client(self.os_creds)
+
+ def tearDown(self):
+ """
+ Cleans the remote OpenStack objects
+ """
+ for rule in self.security_group_rules:
+ neutron_utils.delete_security_group_rule(self.neutron, rule)
+
+ if self.security_group:
+ neutron_utils.delete_security_group(self.neutron, self.security_group)
+
+ def test_create_delete_simple_sec_grp(self):
+ """
+ Tests the neutron_utils.create_security_group() function
+ """
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name)
+ self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings)
+
+ self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name'])
+
+ sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name)
+ self.assertIsNotNone(sec_grp_get)
+ self.assertTrue(validation_utils.objects_equivalent(
+ self.security_group['security_group'], sec_grp_get['security_group']))
+
+ neutron_utils.delete_security_group(self.neutron, self.security_group)
+ sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name)
+ self.assertIsNone(sec_grp_get)
+ self.security_group = None
+
+ def test_create_sec_grp_no_name(self):
+ """
+ Tests the SecurityGroupSettings 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()
+ self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings)
+
+ def test_create_sec_grp_no_rules(self):
+ """
+ Tests the neutron_utils.create_security_group() function
+ """
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group')
+ self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings)
+
+ self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name'])
+ self.assertTrue(sec_grp_settings.description, self.security_group['security_group']['description'])
+
+ sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name)
+ self.assertIsNotNone(sec_grp_get)
+ self.assertTrue(validation_utils.objects_equivalent(
+ self.security_group['security_group'], sec_grp_get['security_group']))
+
+ def test_create_sec_grp_one_rule(self):
+ """
+ Tests the neutron_utils.create_security_group() function
+ """
+
+ sec_grp_rule_settings = SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, direction=Direction.ingress)
+ sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group',
+ rule_settings=[sec_grp_rule_settings])
+
+ self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings)
+ free_rules = neutron_utils.get_rules_by_security_group(self.neutron, self.security_group)
+ for free_rule in free_rules:
+ self.security_group_rules.append(free_rule)
+
+ self.security_group_rules.append(
+ neutron_utils.create_security_group_rule(self.neutron, sec_grp_settings.rule_settings[0]))
+
+ # Refresh object so it is populated with the newly added rule
+ self.security_group = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name)
+
+ rules = neutron_utils.get_rules_by_security_group(self.neutron, self.security_group)
+
+ self.assertTrue(validation_utils.objects_equivalent(self.security_group_rules, rules))
+
+ self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name'])
+ self.assertTrue(sec_grp_settings.description, self.security_group['security_group']['description'])
+
+ sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name)
+ self.assertIsNotNone(sec_grp_get)
+ self.assertTrue(validation_utils.objects_equivalent(
+ self.security_group['security_group'], sec_grp_get['security_group']))
+
+
+"""
+Validation routines
+"""
+
+
+def validate_network(neutron, name, exists):
+ """
+ Returns true if a network for a given name DOES NOT exist if the exists parameter is false conversely true.
+ Returns false if a network for a given name DOES exist if the exists parameter is true conversely false.
+ :param neutron: The neutron client
+ :param name: The expected network name
+ :param exists: Whether or not the network name should exist or not
+ :return: True/False
+ """
+ network = neutron_utils.get_network(neutron, name)
+ if exists and network:
+ return True
+ if not exists and not network:
+ return True
+ return False
+
+
+def validate_subnet(neutron, name, cidr, exists):
+ """
+ Returns true if a subnet for a given name DOES NOT exist if the exists parameter is false conversely true.
+ Returns false if a subnet for a given name DOES exist if the exists parameter is true conversely false.
+ :param neutron: The neutron client
+ :param name: The expected subnet name
+ :param cidr: The expected CIDR value
+ :param exists: Whether or not the network name should exist or not
+ :return: True/False
+ """
+ subnet = neutron_utils.get_subnet_by_name(neutron, name)
+ if exists and subnet:
+ return subnet.get('cidr') == cidr
+ if not exists and not subnet:
+ return True
+ return False
+
+
+def validate_router(neutron, name, exists):
+ """
+ Returns true if a router for a given name DOES NOT exist if the exists parameter is false conversely true.
+ Returns false if a router for a given name DOES exist if the exists parameter is true conversely false.
+ :param neutron: The neutron client
+ :param name: The expected router name
+ :param exists: Whether or not the network name should exist or not
+ :return: True/False
+ """
+ router = neutron_utils.get_router_by_name(neutron, name)
+ if exists and router:
+ return True
+ return False
+
+
+def validate_interface_router(interface_router, router, subnet):
+ """
+ Returns true if the router ID & subnet ID have been properly included into the interface router object
+ :param interface_router: the object to validate
+ :param router: to validate against the interface_router
+ :param subnet: to validate against the interface_router
+ :return: True if both IDs match else False
+ """
+ subnet_id = interface_router.get('subnet_id')
+ router_id = interface_router.get('port_id')
+
+ return subnet.get('id') == subnet_id and router.get('id') == router_id
+
+
+def validate_port(neutron, port_obj, this_port_name):
+ """
+ Returns true if a port for a given name DOES NOT exist if the exists parameter is false conversely true.
+ Returns false if a port for a given name DOES exist if the exists parameter is true conversely false.
+ :param neutron: The neutron client
+ :param port_obj: The port object to lookup
+ :param this_port_name: The expected router name
+ :return: True/False
+ """
+ ports = neutron.list_ports()
+ for port, port_insts in ports.iteritems():
+ for inst in port_insts:
+ if inst['id'] == port_obj['port']['id']:
+ return inst['name'] == this_port_name
+ return False
diff --git a/snaps/openstack/utils/tests/nova_utils_tests.py b/snaps/openstack/utils/tests/nova_utils_tests.py
new file mode 100644
index 0000000..f6c9156
--- /dev/null
+++ b/snaps/openstack/utils/tests/nova_utils_tests.py
@@ -0,0 +1,208 @@
+# 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 logging
+import os
+import uuid
+
+from Crypto.PublicKey import RSA
+
+from snaps.openstack.utils import nova_utils
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase
+from snaps.openstack.create_flavor import FlavorSettings
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('nova_utils_tests')
+
+
+class NovaSmokeTests(OSComponentTestCase):
+ """
+ Tests to ensure that the nova client can communicate with the cloud
+ """
+
+ def test_nova_connect_success(self):
+ """
+ Tests to ensure that the proper credentials can connect.
+ """
+ nova = nova_utils.nova_client(self.os_creds)
+
+ # This should not throw an exception
+ nova.flavors.list()
+
+ def test_nova_connect_fail(self):
+ """
+ Tests to ensure that the improper credentials cannot connect.
+ """
+ from snaps.openstack.os_credentials import OSCreds
+
+ nova = nova_utils.nova_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))
+
+ # This should throw an exception
+ with self.assertRaises(Exception):
+ nova.flavors.list()
+
+
+class NovaUtilsKeypairTests(OSComponentTestCase):
+ """
+ Test basic nova keypair functionality
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading and creating an OS image file
+ within OpenStack
+ """
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.priv_key_file_path = 'tmp/' + guid
+ self.pub_key_file_path = self.priv_key_file_path + '.pub'
+
+ self.nova = nova_utils.nova_client(self.os_creds)
+ self.keys = RSA.generate(1024)
+ self.public_key = self.keys.publickey().exportKey('OpenSSH')
+ self.keypair_name = guid
+ self.keypair = None
+ self.floating_ip = None
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ if self.keypair:
+ try:
+ nova_utils.delete_keypair(self.nova, self.keypair)
+ except:
+ pass
+
+ try:
+ os.remove(self.priv_key_file_path)
+ except:
+ pass
+
+ try:
+ os.remove(self.pub_key_file_path)
+ except:
+ pass
+
+ if self.floating_ip:
+ nova_utils.delete_floating_ip(self.nova, self.floating_ip)
+
+ def test_create_keypair(self):
+ """
+ Tests the creation of an OpenStack keypair that does not exist.
+ """
+ self.keypair = nova_utils.upload_keypair(self.nova, self.keypair_name, self.public_key)
+ result = nova_utils.keypair_exists(self.nova, self.keypair)
+ self.assertEquals(self.keypair, result)
+ keypair = nova_utils.get_keypair_by_name(self.nova, self.keypair_name)
+ self.assertEquals(self.keypair, keypair)
+
+ def test_create_delete_keypair(self):
+ """
+ Tests the creation of an OpenStack keypair that does not exist.
+ """
+ self.keypair = nova_utils.upload_keypair(self.nova, self.keypair_name, self.public_key)
+ result = nova_utils.keypair_exists(self.nova, self.keypair)
+ self.assertEquals(self.keypair, result)
+ nova_utils.delete_keypair(self.nova, self.keypair)
+ result2 = nova_utils.keypair_exists(self.nova, self.keypair)
+ self.assertIsNone(result2)
+
+ def test_create_key_from_file(self):
+ """
+ Tests that the generated RSA keys are properly saved to files
+ :return:
+ """
+ nova_utils.save_keys_to_files(self.keys, self.pub_key_file_path, self.priv_key_file_path)
+ self.keypair = nova_utils.upload_keypair_file(self.nova, self.keypair_name, self.pub_key_file_path)
+ pub_key = open(os.path.expanduser(self.pub_key_file_path)).read()
+ self.assertEquals(self.keypair.public_key, pub_key)
+
+ def test_floating_ips(self):
+ """
+ Tests the creation of a floating IP
+ :return:
+ """
+ ips = nova_utils.get_floating_ips(self.nova)
+ self.assertIsNotNone(ips)
+
+ self.floating_ip = nova_utils.create_floating_ip(self.nova, self.ext_net_name)
+ returned = nova_utils.get_floating_ip(self.nova, self.floating_ip)
+ self.assertEquals(self.floating_ip, returned)
+
+
+class NovaUtilsFlavorTests(OSComponentTestCase):
+ """
+ Test basic nova flavor functionality
+ """
+
+ def setUp(self):
+ """
+ Instantiates the CreateImage object that is responsible for downloading 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.nova = nova_utils.nova_client(self.os_creds)
+ self.flavor = None
+
+ def tearDown(self):
+ """
+ Cleans the image and downloaded image file
+ """
+ if self.flavor:
+ try:
+ nova_utils.delete_flavor(self.nova, self.flavor)
+ except:
+ pass
+
+ def test_create_flavor(self):
+ """
+ Tests the creation of an OpenStack keypair that does not exist.
+ """
+ self.flavor = nova_utils.create_flavor(self.nova, self.flavor_settings)
+ self.validate_flavor()
+
+ def test_create_delete_flavor(self):
+ """
+ Tests the creation of an OpenStack keypair that does not exist.
+ """
+ self.flavor = nova_utils.create_flavor(self.nova, self.flavor_settings)
+ self.validate_flavor()
+ nova_utils.delete_flavor(self.nova, self.flavor)
+ flavor = nova_utils.get_flavor_by_name(self.nova, self.flavor_settings.name)
+ self.assertIsNone(flavor)
+
+ def validate_flavor(self):
+ """
+ Validates the flavor_settings against the OpenStack flavor object
+ """
+ self.assertIsNotNone(self.flavor)
+ self.assertEquals(self.flavor_settings.name, self.flavor.name)
+ self.assertEquals(self.flavor_settings.flavor_id, self.flavor.id)
+ self.assertEquals(self.flavor_settings.ram, self.flavor.ram)
+ self.assertEquals(self.flavor_settings.disk, self.flavor.disk)
+ self.assertEquals(self.flavor_settings.vcpus, self.flavor.vcpus)
+ self.assertEquals(self.flavor_settings.ephemeral, self.flavor.ephemeral)
+
+ if self.flavor_settings.swap == 0:
+ self.assertEquals('', self.flavor.swap)
+ else:
+ self.assertEquals(self.flavor_settings.swap, self.flavor.swap)
+
+ self.assertEquals(self.flavor_settings.rxtx_factor, self.flavor.rxtx_factor)
+ self.assertEquals(self.flavor_settings.is_public, self.flavor.is_public)
diff --git a/snaps/playbook_runner.py b/snaps/playbook_runner.py
new file mode 100644
index 0000000..3710309
--- /dev/null
+++ b/snaps/playbook_runner.py
@@ -0,0 +1,58 @@
+# 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 argparse
+import logging
+
+import re
+
+from snaps.openstack.os_credentials import ProxySettings
+from snaps.provisioning import ansible_utils
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('playbook_runner')
+
+
+def main(parsed_args):
+ """
+ Uses ansible_utils for applying Ansible Playbooks to machines with a private key
+ """
+ logging.basicConfig(level=logging.DEBUG)
+ logger.info('Starting Playbook Runner')
+
+ proxy_settings = None
+ if parsed_args.http_proxy:
+ tokens = re.split(':', parsed_args.http_proxy)
+ proxy_settings = ProxySettings(tokens[0], tokens[1], parsed_args.ssh_proxy_cmd)
+
+ # Ensure can get an SSH client
+ ansible_utils.ssh_client(parsed_args.ip_addr, parsed_args.host_user, parsed_args.priv_key, proxy_settings)
+
+ retval = ansible_utils.apply_playbook(parsed_args.playbook, [parsed_args.ip_addr], parsed_args.host_user,
+ parsed_args.priv_key, variables={'name': 'Foo'}, proxy_setting=proxy_settings)
+ exit(retval)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-a', '--ip-addr', dest='ip_addr', required=True, help='The Host IP Address')
+ parser.add_argument('-k', '--priv-key', dest='priv_key', required=True, help='The location of the private key file')
+ parser.add_argument('-u', '--host-user', dest='host_user', required=True, help='Host user account')
+ parser.add_argument('-b', '--playbook', dest='playbook', required=True, help='Playbook Location')
+ parser.add_argument('-p', '--http-proxy', dest='http_proxy', required=False, help='<host>:<port>')
+ parser.add_argument('-s', '--ssh-proxy-cmd', dest='ssh_proxy_cmd', required=False)
+ args = parser.parse_args()
+
+ main(args)
diff --git a/snaps/provisioning/__init__.py b/snaps/provisioning/__init__.py
new file mode 100644
index 0000000..7f92908
--- /dev/null
+++ b/snaps/provisioning/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski' \ No newline at end of file
diff --git a/snaps/provisioning/ansible/centos-network-setup/playbooks/configure_host.yml b/snaps/provisioning/ansible/centos-network-setup/playbooks/configure_host.yml
new file mode 100644
index 0000000..8df03cb
--- /dev/null
+++ b/snaps/provisioning/ansible/centos-network-setup/playbooks/configure_host.yml
@@ -0,0 +1,26 @@
+# 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.
+---
+- name: Configure NIC
+ hosts: all
+ become: yes
+ become_method: sudo
+ become_user: root
+
+ 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
diff --git a/snaps/provisioning/ansible/centos-network-setup/templates/ifcfg-interface b/snaps/provisioning/ansible/centos-network-setup/templates/ifcfg-interface
new file mode 100644
index 0000000..47aa3fa
--- /dev/null
+++ b/snaps/provisioning/ansible/centos-network-setup/templates/ifcfg-interface
@@ -0,0 +1,14 @@
+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/ubuntu-network-setup/playbooks/configure_host.yml b/snaps/provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml
new file mode 100644
index 0000000..5d43f96
--- /dev/null
+++ b/snaps/provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml
@@ -0,0 +1,26 @@
+# 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.
+---
+- 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
diff --git a/snaps/provisioning/ansible/ubuntu-network-setup/templates/ethN.cfg b/snaps/provisioning/ansible/ubuntu-network-setup/templates/ethN.cfg
new file mode 100644
index 0000000..3fa7708
--- /dev/null
+++ b/snaps/provisioning/ansible/ubuntu-network-setup/templates/ethN.cfg
@@ -0,0 +1,2 @@
+auto {{ nic_name }}
+iface {{ nic_name }} inet dhcp
diff --git a/snaps/provisioning/ansible_utils.py b/snaps/provisioning/ansible_utils.py
new file mode 100644
index 0000000..36f1efc
--- /dev/null
+++ b/snaps/provisioning/ansible_utils.py
@@ -0,0 +1,114 @@
+# 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 logging
+
+from collections import namedtuple
+
+import os
+import paramiko
+
+from ansible.parsing.dataloader import DataLoader
+from ansible.vars import VariableManager
+from ansible.inventory import Inventory
+from ansible.executor.playbook_executor import PlaybookExecutor
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('ansible_utils')
+
+
+def apply_playbook(playbook_path, hosts_inv, host_user, ssh_priv_key_file_path, variables=None, proxy_setting=None):
+ """
+ Executes an Ansible playbook to the given host
+ :param playbook_path: the (relative) path to the Ansible playbook
+ :param hosts_inv: a list of hostnames/ip addresses to which to apply the Ansible playbook
+ :param host_user: A user for the host instances (must be a password-less sudo user if playbook has "sudo: yes"
+ :param ssh_priv_key_file_path: the file location of the ssh key
+ :param variables: a dictionary containing any substitution variables needed by the Jinga 2 templates
+ :param proxy_setting: instance of os_credentials.ProxySettings class
+ :return: the results
+ """
+ if not os.path.isfile(playbook_path):
+ raise Exception('Requested playbook not found - ' + playbook_path)
+ if not os.path.isfile(ssh_priv_key_file_path):
+ raise Exception('Requested private SSH key not found - ' + ssh_priv_key_file_path)
+
+ import ansible.constants
+ ansible.constants.HOST_KEY_CHECKING = False
+
+ variable_manager = VariableManager()
+ if variables:
+ variable_manager.extra_vars = variables
+
+ loader = DataLoader()
+ inventory = Inventory(loader=loader, variable_manager=variable_manager, host_list=hosts_inv)
+ variable_manager.set_inventory(inventory)
+
+ ssh_extra_args = None
+ if proxy_setting and proxy_setting.ssh_proxy_cmd:
+ ssh_extra_args = '-o ProxyCommand=\'' + proxy_setting.ssh_proxy_cmd + '\''
+
+ options = namedtuple('Options', ['listtags', 'listtasks', 'listhosts', 'syntax', 'connection', 'module_path',
+ 'forks', 'remote_user', 'private_key_file', 'ssh_common_args', 'ssh_extra_args',
+ 'become', 'become_method', 'become_user', 'verbosity', 'check'])
+
+ ansible_opts = options(listtags=False, listtasks=False, listhosts=False, syntax=False, connection='ssh',
+ module_path=None, forks=100, remote_user=host_user, private_key_file=ssh_priv_key_file_path,
+ ssh_common_args=None, ssh_extra_args=ssh_extra_args, become=None, become_method=None,
+ become_user=None, verbosity=11111, check=False)
+
+ logger.debug('Setting up Ansible Playbook Executor for playbook - ' + playbook_path)
+ executor = PlaybookExecutor(
+ playbooks=[playbook_path],
+ inventory=inventory,
+ variable_manager=variable_manager,
+ loader=loader,
+ options=ansible_opts,
+ passwords=None)
+
+ logger.debug('Executing Ansible Playbook - ' + playbook_path)
+ retval = executor.run()
+
+ if retval != 0:
+ logger.error('Playbook application failed [' + playbook_path + '] with return value of - ' + str(retval))
+ raise Exception('Playbook not applied - ' + playbook_path)
+
+ return retval
+
+
+def ssh_client(ip, user, private_key_filepath, proxy_settings=None):
+ """
+ Retrieves and attemts an SSH connection
+ :param ip: the IP of the host to connect
+ :param user: the user with which to connect
+ :param private_key_filepath: the path to the private key file
+ :param proxy_settings: instance of os_credentials.ProxySettings class (optional)
+ :return: the SSH client if can connect else false
+ """
+ logger.debug('Retrieving SSH client')
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy())
+
+ try:
+ proxy_cmd = None
+ if proxy_settings and proxy_settings.ssh_proxy_cmd:
+ proxy_cmd_str = str(proxy_settings.ssh_proxy_cmd.replace('%h', ip))
+ proxy_cmd_str = proxy_cmd_str.replace("%p", '22')
+ proxy_cmd = paramiko.ProxyCommand(proxy_cmd_str)
+
+ ssh.connect(ip, username=user, key_filename=private_key_filepath, sock=proxy_cmd)
+ return ssh
+ except Exception as e:
+ logger.warn('Unable to connect via SSH with message - ' + e.message)
diff --git a/snaps/provisioning/tests/__init__.py b/snaps/provisioning/tests/__init__.py
new file mode 100644
index 0000000..e3e876e
--- /dev/null
+++ b/snaps/provisioning/tests/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski'
diff --git a/snaps/provisioning/tests/ansible_utils_tests.py b/snaps/provisioning/tests/ansible_utils_tests.py
new file mode 100644
index 0000000..dc108e0
--- /dev/null
+++ b/snaps/provisioning/tests/ansible_utils_tests.py
@@ -0,0 +1,217 @@
+# 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 os
+import uuid
+
+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 import create_image
+from snaps.openstack import create_flavor
+from scp import SCPClient
+
+from snaps.provisioning import ansible_utils
+from snaps.openstack.tests import openstack_tests
+from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase
+
+VM_BOOT_TIMEOUT = 600
+
+ip_1 = '10.0.1.100'
+ip_2 = '10.0.1.200'
+
+
+class AnsibleProvisioningTests(OSIntegrationTestCase):
+ """
+ Test for the CreateInstance class with two NIC/Ports, eth0 with floating IP and eth1 w/o
+ """
+
+ 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.keypair_priv_filepath = 'tmp/' + guid
+ self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub'
+ self.keypair_name = guid + '-kp'
+ self.vm_inst_name = guid + '-inst'
+ self.test_file_local_path = 'tmp/' + guid + '-hello.txt'
+ self.port_1_name = guid + '-port-1'
+ self.port_2_name = guid + '-port-2'
+ self.floating_ip_name = guid + 'fip1'
+
+ # Setup members to cleanup just in case they don't get created
+ self.inst_creator = None
+ self.keypair_creator = None
+ self.flavor_creator = None
+ self.router_creator = None
+ self.network_creator = None
+ self.image_creator = None
+
+ try:
+ # Create Image
+ os_image_settings = openstack_tests.ubuntu_url_image(name=guid + '-' + '-image')
+ self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings)
+ self.image_creator.create()
+
+ # First network is public
+ 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)
+
+ self.network_creator = create_network.OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings)
+ self.network_creator.create()
+
+ # Create routers
+ self.router_creator = create_router.OpenStackRouter(self.os_creds, self.pub_net_config.router_settings)
+ self.router_creator.create()
+
+ # 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))
+ self.flavor_creator.create()
+
+ # Create Key/Pair
+ self.keypair_creator = create_keypairs.OpenStackKeypair(
+ self.os_creds, create_keypairs.KeypairSettings(
+ name=self.keypair_name, public_filepath=self.keypair_pub_filepath,
+ private_filepath=self.keypair_priv_filepath))
+ self.keypair_creator.create()
+
+ # Create instance
+ ports_settings = list()
+ ports_settings.append(
+ create_network.PortSettings(name=self.port_1_name,
+ network_name=self.pub_net_config.network_settings.name))
+
+ instance_settings = create_instance.VmInstanceSettings(
+ name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=ports_settings,
+ floating_ip_settings=[create_instance.FloatingIpSettings(
+ name=self.floating_ip_name, port_name=self.port_1_name,
+ router_name=self.pub_net_config.router_settings.name)])
+
+ self.inst_creator = create_instance.OpenStackVmInstance(
+ self.os_creds, instance_settings, self.image_creator.image_settings,
+ keypair_settings=self.keypair_creator.keypair_settings)
+ except Exception as e:
+ self.tearDown()
+ raise Exception(e.message)
+
+ def tearDown(self):
+ """
+ Cleans the created objects
+ """
+ if self.inst_creator:
+ self.inst_creator.clean()
+
+ if self.keypair_creator:
+ self.keypair_creator.clean()
+
+ if self.flavor_creator:
+ self.flavor_creator.clean()
+
+ 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.router_creator:
+ self.router_creator.clean()
+
+ if self.network_creator:
+ self.network_creator.clean()
+
+ if self.image_creator:
+ self.image_creator.clean()
+
+ if os.path.isfile(self.test_file_local_path):
+ os.remove(self.test_file_local_path)
+
+ super(self.__class__, self).__clean__()
+
+ def test_apply_simple_playbook(self):
+ """
+ Tests application of an Ansible playbook that simply copies over a file:
+ 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.
+ """
+ self.inst_creator.create(block=True)
+
+ # Block until VM's ssh port has been opened
+ self.assertTrue(self.inst_creator.vm_ssh_active(block=True))
+
+ ssh_client = self.inst_creator.ssh_client()
+ self.assertIsNotNone(ssh_client)
+ out = ssh_client.exec_command('pwd')[1].channel.in_buffer.read(1024)
+ self.assertIsNotNone(out)
+ self.assertGreater(len(out), 1)
+
+ # Need to use the first floating IP as subsequent ones are currently broken with Apex CO
+ ip = self.inst_creator.get_floating_ip().ip
+ user = self.inst_creator.get_image_user()
+ priv_key = self.inst_creator.keypair_settings.private_filepath
+
+ retval = ansible_utils.apply_playbook('provisioning/tests/playbooks/simple_playbook.yml', [ip], user, priv_key,
+ proxy_setting=self.os_creds.proxy_settings)
+ self.assertEquals(0, retval)
+
+ ssh = ansible_utils.ssh_client(ip, user, priv_key, self.os_creds.proxy_settings)
+ self.assertIsNotNone(ssh)
+ scp = SCPClient(ssh.get_transport())
+ scp.get('~/hello.txt', self.test_file_local_path)
+
+ self.assertTrue(os.path.isfile(self.test_file_local_path))
+
+ with open(self.test_file_local_path) as f:
+ file_contents = f.readline()
+ self.assertEquals('Hello World!', file_contents)
+
+ def test_apply_template_playbook(self):
+ """
+ Tests application of an Ansible playbook that applies a template to a file:
+ 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.
+ """
+ self.inst_creator.create(block=True)
+
+ # Block until VM's ssh port has been opened
+ self.assertTrue(self.inst_creator.vm_ssh_active(block=True))
+
+ # Need to use the first floating IP as subsequent ones are currently broken with Apex CO
+ ip = self.inst_creator.get_floating_ip().ip
+ user = self.inst_creator.get_image_user()
+ priv_key = self.inst_creator.keypair_settings.private_filepath
+
+ ansible_utils.apply_playbook('provisioning/tests/playbooks/template_playbook.yml', [ip], user, priv_key,
+ variables={'name': 'Foo'}, proxy_setting=self.os_creds.proxy_settings)
+
+ ssh = ansible_utils.ssh_client(ip, user, priv_key, self.os_creds.proxy_settings)
+ self.assertIsNotNone(ssh)
+ scp = SCPClient(ssh.get_transport())
+ scp.get('/tmp/hello.txt', self.test_file_local_path)
+
+ self.assertTrue(os.path.isfile(self.test_file_local_path))
+
+ with open(self.test_file_local_path) as f:
+ file_contents = f.readline()
+ self.assertEquals('Hello Foo!', file_contents)
diff --git a/snaps/provisioning/tests/playbooks/simple_playbook.yml b/snaps/provisioning/tests/playbooks/simple_playbook.yml
new file mode 100644
index 0000000..7af169c
--- /dev/null
+++ b/snaps/provisioning/tests/playbooks/simple_playbook.yml
@@ -0,0 +1,21 @@
+# 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.
+---
+- hosts: all
+
+ tasks:
+ - name: Transfer the test file
+ copy: src=../scripts/hello.txt dest=~/hello.txt mode=0777
+
diff --git a/snaps/provisioning/tests/playbooks/template_playbook.yml b/snaps/provisioning/tests/playbooks/template_playbook.yml
new file mode 100644
index 0000000..34d4e95
--- /dev/null
+++ b/snaps/provisioning/tests/playbooks/template_playbook.yml
@@ -0,0 +1,23 @@
+# 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.
+---
+- hosts: all
+ become: yes
+ become_method: sudo
+ become_user: root
+
+ tasks:
+ - name: Apply template and copy file
+ action: template owner=root group=root mode=777 src=../scripts/template.txt dest=/tmp/hello.txt
diff --git a/snaps/provisioning/tests/scripts/hello.txt b/snaps/provisioning/tests/scripts/hello.txt
new file mode 100644
index 0000000..c57eff5
--- /dev/null
+++ b/snaps/provisioning/tests/scripts/hello.txt
@@ -0,0 +1 @@
+Hello World! \ No newline at end of file
diff --git a/snaps/provisioning/tests/scripts/template.txt b/snaps/provisioning/tests/scripts/template.txt
new file mode 100644
index 0000000..c7a43bc
--- /dev/null
+++ b/snaps/provisioning/tests/scripts/template.txt
@@ -0,0 +1 @@
+Hello {{ name }}! \ No newline at end of file
diff --git a/snaps/test_suite_builder.py b/snaps/test_suite_builder.py
new file mode 100644
index 0000000..32e0e35
--- /dev/null
+++ b/snaps/test_suite_builder.py
@@ -0,0 +1,208 @@
+# 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 logging
+import unittest
+
+from snaps.openstack.utils.tests.glance_utils_tests import GlanceSmokeTests, GlanceUtilsTests
+from snaps.openstack.tests.create_flavor_tests import CreateFlavorTests
+from snaps.tests.file_utils_tests import FileUtilsTests
+from snaps.openstack.tests.create_security_group_tests import CreateSecurityGroupTests, \
+ SecurityGroupRuleSettingsUnitTests, SecurityGroupSettingsUnitTests
+from snaps.openstack.tests.create_project_tests import CreateProjectSuccessTests, ProjectSettingsUnitTests, \
+ CreateProjectUserTests
+from snaps.openstack.tests.create_user_tests import UserSettingsUnitTests, CreateUserSuccessTests
+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
+from snaps.openstack.tests.create_image_tests import CreateImageSuccessTests, CreateImageNegativeTests, \
+ ImageSettingsUnitTests
+from snaps.openstack.tests.create_keypairs_tests import CreateKeypairsTests, KeypairSettingsUnitTests
+from snaps.openstack.tests.create_network_tests import CreateNetworkSuccessTests, NetworkSettingsUnitTests, \
+ PortSettingsUnitTests, SubnetSettingsUnitTests, CreateNetworkTypeTests
+from snaps.openstack.tests.create_router_tests import CreateRouterSuccessTests, CreateRouterNegativeTests
+from snaps.openstack.tests.create_instance_tests import CreateInstanceSingleNetworkTests, \
+ CreateInstancePubPrivNetTests, CreateInstanceOnComputeHost, CreateInstanceSimpleTests, \
+ FloatingIpSettingsUnitTests, InstanceSecurityGroupTests, VmInstanceSettingsUnitTests, \
+ CreateInstancePortManipulationTests, SimpleHealthCheck
+from snaps.provisioning.tests.ansible_utils_tests import AnsibleProvisioningTests
+from snaps.openstack.tests.os_source_file_test import OSComponentTestCase, OSIntegrationTestCase
+from snaps.openstack.utils.tests.nova_utils_tests import NovaSmokeTests, NovaUtilsKeypairTests, NovaUtilsFlavorTests
+
+__author__ = 'spisarski'
+
+
+def add_unit_tests(suite):
+ """
+ Adds tests that do not require external resources
+ :param suite: the unittest.TestSuite object to which to add the tests
+ :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(SecurityGroupSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ImageSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(KeypairSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(UserSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ProjectSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(NetworkSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SubnetSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(PortSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(FloatingIpSettingsUnitTests))
+ suite.addTest(unittest.TestLoader().loadTestsFromTestCase(VmInstanceSettingsUnitTests))
+
+
+def add_openstack_client_tests(suite, source_filename, ext_net_name, use_keystone=True, http_proxy_str=None,
+ log_level=logging.INFO):
+ """
+ Adds tests written to exercise OpenStack client retrieval
+ :param suite: the unittest.TestSuite object to which to add the tests
+ :param source_filename: the OpenStack credentials filename
+ :param ext_net_name: the name of an external network on the cloud under test
+ :param http_proxy_str: <host>:<port> of the proxy server (optional)
+ :param use_keystone: when True, tests requiring direct access to Keystone are added as these need to be running on
+ a host that has access to the cloud's private network
+ :param log_level: the logging level
+ :return: None as the tests will be adding to the 'suite' parameter object
+ """
+ # Basic connection tests
+ suite.addTest(OSComponentTestCase.parameterize(GlanceSmokeTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+
+ if use_keystone:
+ suite.addTest(OSComponentTestCase.parameterize(KeystoneSmokeTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+
+ suite.addTest(OSComponentTestCase.parameterize(NeutronSmokeTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(NovaSmokeTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+
+
+def add_openstack_api_tests(suite, source_filename, ext_net_name, http_proxy_str=None, use_keystone=True,
+ log_level=logging.INFO):
+ """
+ Adds tests written to exercise all existing OpenStack APIs
+ :param suite: the unittest.TestSuite object to which to add the tests
+ :param source_filename: the OpenStack credentials filename
+ :param ext_net_name: the name of an external network on the cloud under test
+ :param http_proxy_str: <host>:<port> of the proxy server (optional)
+ :param use_keystone: when True, tests requiring direct access to Keystone are added as these need to be running on
+ a host that has access to the cloud's private network
+ :param log_level: the logging level
+ :return: None as the tests will be adding to the 'suite' parameter object
+ """
+ # Tests the OpenStack API calls
+ if use_keystone:
+ suite.addTest(OSComponentTestCase.parameterize(KeystoneUtilsTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(CreateUserSuccessTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(CreateProjectSuccessTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(CreateProjectUserTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+
+ suite.addTest(OSComponentTestCase.parameterize(GlanceUtilsTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsNetworkTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsSubnetTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsRouterTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(NeutronUtilsSecurityGroupTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(NovaUtilsKeypairTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(NovaUtilsFlavorTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(CreateFlavorTests, source_filename, ext_net_name,
+ http_proxy_str=http_proxy_str, log_level=log_level))
+
+
+def add_openstack_integration_tests(suite, source_filename, ext_net_name, proxy_settings=None, ssh_proxy_cmd=None,
+ use_keystone=True, use_floating_ips=True, log_level=logging.INFO):
+ """
+ Adds tests written to exercise all long-running OpenStack integration tests meaning they will be creating VM
+ instances and potentially performing some SSH functions through floating IPs
+ :param suite: the unittest.TestSuite object to which to add the tests
+ :param source_filename: the OpenStack credentials filename
+ :param ext_net_name: the name of an external network on the cloud under test
+ :param proxy_settings: <host>:<port> of the proxy server (optional)
+ :param ssh_proxy_cmd: the command your environment requires for creating ssh connections through a proxy
+ :param use_keystone: when True, tests requiring direct access to Keystone are added as these need to be running on
+ a host that has access to the cloud's private network
+ :param use_floating_ips: when true, all tests requiring Floating IPs will be added to the suite
+ :param log_level: the logging level
+ :return: None as the tests will be adding to the 'suite' parameter object
+ """
+ # Tests the OpenStack API calls via a creator. If use_keystone, objects will be created with a custom user
+ # and project
+
+ # Creator Object tests
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateSecurityGroupTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateImageSuccessTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateImageNegativeTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateKeypairsTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateNetworkSuccessTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(CreateNetworkTypeTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateRouterSuccessTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateRouterNegativeTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+
+ # VM Instances
+ suite.addTest(OSIntegrationTestCase.parameterize(SimpleHealthCheck, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateInstanceSimpleTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateInstancePortManipulationTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(InstanceSecurityGroupTests, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSComponentTestCase.parameterize(CreateInstanceOnComputeHost, source_filename, ext_net_name,
+ http_proxy_str=proxy_settings, log_level=log_level))
+
+ if use_floating_ips:
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateInstanceSingleNetworkTests, source_filename,
+ ext_net_name, http_proxy_str=proxy_settings,
+ ssh_proxy_cmd=ssh_proxy_cmd, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(CreateInstancePubPrivNetTests, source_filename,
+ ext_net_name, http_proxy_str=proxy_settings,
+ ssh_proxy_cmd=ssh_proxy_cmd, use_keystone=use_keystone,
+ log_level=log_level))
+ suite.addTest(OSIntegrationTestCase.parameterize(AnsibleProvisioningTests, source_filename,
+ ext_net_name, http_proxy_str=proxy_settings,
+ ssh_proxy_cmd=ssh_proxy_cmd, use_keystone=use_keystone,
+ log_level=log_level))
diff --git a/snaps/tests/__init__.py b/snaps/tests/__init__.py
new file mode 100644
index 0000000..e3e876e
--- /dev/null
+++ b/snaps/tests/__init__.py
@@ -0,0 +1,15 @@
+# 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.
+__author__ = 'spisarski'
diff --git a/snaps/tests/file_utils_tests.py b/snaps/tests/file_utils_tests.py
new file mode 100644
index 0000000..d517d5d
--- /dev/null
+++ b/snaps/tests/file_utils_tests.py
@@ -0,0 +1,102 @@
+# 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 os
+import unittest
+import shutil
+import uuid
+
+from snaps import file_utils
+
+__author__ = 'spisarski'
+
+
+class FileUtilsTests(unittest.TestCase):
+ """
+ Tests the methods in file_utils.py
+ """
+
+ def setUp(self):
+ guid = self.__class__.__name__ + '-' + str(uuid.uuid4())
+ self.tmpDir = 'tmp/' + str(guid)
+ if not os.path.exists(self.tmpDir):
+ os.makedirs(self.tmpDir)
+
+ self.tmpFile = self.tmpDir + '/bar.txt'
+ if not os.path.exists(self.tmpFile):
+ open(self.tmpFile, 'wb')
+
+ def tearDown(self):
+ if os.path.exists(self.tmpDir) and os.path.isdir(self.tmpDir):
+ shutil.rmtree(self.tmpDir)
+
+ def testFileIsDirectory(self):
+ """
+ Ensure the file_utils.fileExists() method returns false with a directory
+ """
+ result = file_utils.file_exists(self.tmpDir)
+ self.assertFalse(result)
+ # TODO - Cleanup directory
+
+ def testFileNotExist(self):
+ """
+ Ensure the file_utils.fileExists() method returns false with a bogus file
+ """
+ result = file_utils.file_exists('/foo/bar.txt')
+ self.assertFalse(result)
+
+ def testFileExists(self):
+ """
+ Ensure the file_utils.fileExists() method returns false with a directory
+ """
+ if not os.path.exists(self.tmpFile):
+ os.makedirs(self.tmpFile)
+
+ result = file_utils.file_exists(self.tmpFile)
+ self.assertTrue(result)
+
+ def testDownloadBadUrl(self):
+ """
+ Tests the file_utils.download() method when given a bad URL
+ """
+ with self.assertRaises(Exception):
+ file_utils.download('http://bunkUrl.com/foo/bar.iso', self.tmpDir)
+
+ def testDownloadBadDir(self):
+ """
+ Tests the file_utils.download() method when given a bad URL
+ """
+ with self.assertRaises(Exception):
+ file_utils.download('http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img', '/foo/bar')
+
+ def testCirrosImageDownload(self):
+ """
+ Tests the file_utils.download() method when given a good Cirros QCOW2 URL
+ """
+ image_file = file_utils.download('http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img',
+ self.tmpDir)
+ self.assertIsNotNone(image_file)
+ self.assertTrue(image_file.name.endswith("cirros-0.3.4-x86_64-disk.img"))
+ self.assertTrue(image_file.name.startswith(self.tmpDir))
+
+ def testReadOSEnvFile(self):
+ """
+ Tests that the OS Environment file is correctly parsed
+ :return:
+ """
+ os_env_dict = file_utils.read_os_env_file('openstack/tests/conf/overcloudrc_test')
+ self.assertEquals('test_pw', os_env_dict['OS_PASSWORD'])
+ self.assertEquals('http://foo:5000/v2.0/', os_env_dict['OS_AUTH_URL'])
+ self.assertEquals('admin', os_env_dict['OS_USERNAME'])
+ self.assertEquals('admin', os_env_dict['OS_TENANT_NAME'])
diff --git a/snaps/unit_test_suite.py b/snaps/unit_test_suite.py
new file mode 100644
index 0000000..720d547
--- /dev/null
+++ b/snaps/unit_test_suite.py
@@ -0,0 +1,131 @@
+# 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 argparse
+import logging
+import unittest
+import os
+
+from snaps import test_suite_builder
+
+__author__ = 'spisarski'
+
+logger = logging.getLogger('unit_test_suite')
+
+ARG_NOT_SET = "argument not set"
+LOG_LEVELS = {'FATAL': logging.FATAL, 'CRITICAL': logging.CRITICAL, 'ERROR': logging.ERROR, 'WARN': logging.WARN,
+ 'INFO': logging.INFO, 'DEBUG': logging.DEBUG}
+
+
+def __create_test_suite(source_filename, ext_net_name, proxy_settings, ssh_proxy_cmd, run_unit_tests, use_keystone,
+ use_floating_ips, log_level):
+ """
+ Compiles the tests that should run
+ :param source_filename: the OpenStack credentials file (required)
+ :param ext_net_name: the name of the external network to use for floating IPs (required)
+ :param run_unit_tests: when true, the tests not requiring OpenStack will be added to the test suite
+ :param proxy_settings: <host>:<port> of the proxy server (optional)
+ :param ssh_proxy_cmd: the command used to connect via SSH over some proxy server (optional)
+ :param use_keystone: when true, tests creating users and projects will be exercised and must be run on a host that
+ has access to the cloud's administrative network
+ :param use_floating_ips: when true, tests requiring floating IPs will be executed
+ :param log_level: the logging level
+ :return:
+ """
+ suite = unittest.TestSuite()
+
+ # Tests that do not require a remote connection to an OpenStack cloud
+ if run_unit_tests:
+ test_suite_builder.add_unit_tests(suite)
+
+ # Basic connection tests
+ test_suite_builder.add_openstack_client_tests(suite, source_filename, ext_net_name, use_keystone=use_keystone,
+ http_proxy_str=proxy_settings, log_level=log_level)
+
+ # Tests the OpenStack API calls
+ test_suite_builder.add_openstack_api_tests(suite, source_filename, ext_net_name, use_keystone=use_keystone,
+ http_proxy_str=proxy_settings, log_level=log_level)
+
+ # Long running integration type tests
+ test_suite_builder.add_openstack_integration_tests(suite, source_filename, ext_net_name, use_keystone=use_keystone,
+ proxy_settings=proxy_settings, ssh_proxy_cmd=ssh_proxy_cmd,
+ use_floating_ips=use_floating_ips, log_level=log_level)
+ return suite
+
+
+def main(arguments):
+ """
+ Begins running unit tests.
+ argv[1] if used must be the source filename else os_env.yaml will be leveraged instead
+ argv[2] if used must be the proxy server <host>:<port>
+ """
+ logger.info('Starting test suite')
+
+ log_level = LOG_LEVELS.get(arguments.log_level, logging.DEBUG)
+
+ suite = None
+ if arguments.env and arguments.ext_net:
+ suite = __create_test_suite(arguments.env, arguments.ext_net, arguments.proxy, arguments.ssh_proxy_cmd,
+ arguments.include_units != ARG_NOT_SET,
+ arguments.use_keystone != ARG_NOT_SET,
+ arguments.no_floating_ips == ARG_NOT_SET, log_level)
+ else:
+ logger.error('Environment file or external network not defined')
+ exit(1)
+
+ # To ensure any files referenced via a relative path will begin from the diectory in which this file resides
+ os.chdir(os.path.dirname(os.path.realpath(__file__)))
+
+ result = unittest.TextTestRunner(verbosity=2).run(suite)
+
+ if result.errors:
+ logger.error('Number of errors in test suite - ' + str(len(result.errors)))
+ for test, message in result.errors:
+ logger.error(str(test) + " ERROR with " + message)
+
+ if result.failures:
+ logger.error('Number of failures in test suite - ' + str(len(result.failures)))
+ for test, message in result.failures:
+ logger.error(str(test) + " FAILED with " + message)
+
+ if (result.errors and len(result.errors) > 0) or (result.failures and len(result.failures) > 0):
+ logger.error('See above for test failures')
+ exit(1)
+ else:
+ logger.info('All tests completed successfully')
+
+ exit(0)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-e', '--env', dest='env', required=True, help='OpenStack credentials file')
+ parser.add_argument('-n', '--net', dest='ext_net', required=True, help='External network name')
+ parser.add_argument('-p', '--proxy', dest='proxy', nargs='?', default=None,
+ help='Optonal HTTP proxy socket (<host>:<port>)')
+ parser.add_argument('-s', '--ssh-proxy-cmd', dest='ssh_proxy_cmd', nargs='?', default=None,
+ help='Optonal SSH proxy command value')
+ parser.add_argument('-l', '--log-level', dest='log_level', default='INFO',
+ help='Logging Level (FATAL|CRITICAL|ERROR|WARN|INFO|DEBUG)')
+ parser.add_argument('-k', '--use-keystone', dest='use_keystone', default=ARG_NOT_SET, nargs='?',
+ help='When argument is set, the tests will exercise the keystone APIs and must be run on a ' +
+ 'machine that has access to the admin network' +
+ ' and is able to create users and groups')
+ parser.add_argument('-f', '--no-floating-ips', dest='no_floating_ips', default=ARG_NOT_SET, nargs='?',
+ help='When argument is set, all tests requiring Floating IPs will not be executed')
+ parser.add_argument('-u', '--include-units', dest='include_units', default=ARG_NOT_SET, nargs='?',
+ help='When argument is set, all tests not requiring OpenStack will be executed')
+ args = parser.parse_args()
+
+ main(args)