summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorspisarski <s.pisarski@cablelabs.com>2017-02-15 09:13:54 -0700
committerspisarski <s.pisarski@cablelabs.com>2017-02-15 09:15:34 -0700
commit57777f3df521553a06cd01a3861b415d2905ceca (patch)
treef3b3be457baec7b5231309989aa3ffa9658cd25d
parent73ef791a1cde68e0d8d69cddf63534fbb90f3e2d (diff)
Initial patch with all code from CableLabs repository.
Change-Id: I70a2778718c5e7f21fd14e4ad28c9269d3761cc7 Signed-off-by: spisarski <s.pisarski@cablelabs.com>
-rw-r--r--docs/APITests.md117
-rw-r--r--docs/IntegrationTests.md123
-rw-r--r--docs/Testing.md37
-rw-r--r--docs/UnitTests.md48
-rw-r--r--docs/VirtEnvDeploy.md158
-rw-r--r--docs/examples/complex-network/deploy-complex-network.yaml234
-rw-r--r--docs/examples/complex-network/main.yml16
-rw-r--r--docs/examples/complex-network/playbooks/sample-playbook.yml20
-rw-r--r--docs/examples/external-network/deploy-ext-net.yaml77
-rw-r--r--docs/examples/simple/deploy-simple.yaml101
-rw-r--r--docs/examples/simple/files/motd8
-rw-r--r--docs/examples/simple/main.yml16
-rw-r--r--docs/examples/simple/playbooks/sample-playbook.yml23
-rw-r--r--docs/examples/two-network/deploy-two-net-centos.yaml96
-rw-r--r--docs/examples/two-network/deploy-two-net-ubuntu.yaml96
-rw-r--r--docs/index.md19
-rw-r--r--snaps/__init__.py15
-rw-r--r--snaps/deploy_venv.py535
-rw-r--r--snaps/file_utils.py108
-rw-r--r--snaps/openstack/__init__.py15
-rw-r--r--snaps/openstack/create_flavor.py167
-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.template39
-rw-r--r--snaps/openstack/tests/conf/overcloudrc_test9
-rw-r--r--snaps/openstack/tests/create_flavor_tests.py311
-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, 12245 insertions, 0 deletions
diff --git a/docs/APITests.md b/docs/APITests.md
new file mode 100644
index 0000000..8376203
--- /dev/null
+++ b/docs/APITests.md
@@ -0,0 +1,117 @@
+# 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 Version | 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 Version | Description |
+|--- |:-: |--- |
+|test_create_user|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 - CreateProjectSuccessTests
+| Test Name | Keystone API Version | Description |
+|--- |:-: |--- |
+|test_create_project|2 & 3|Tests the creation of a project via the OpenStackProject class|
+|test_create_project_2x|2 & 3|Tests the creation of a project a second time via the OpenStackProject class to ensure it is only created once|
+|test_create_delete_project|2 & 3|Tests the creation and deletion of a project via the OpenStackProject class to ensure that clean() will not raise an exception|
+
+## create_project_tests.py - CreateProjectUserTests
+| Test Name | Keystone API Version | 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 Version | 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 Version | 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 Version | 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 Version | Description |
+|--- |:-: |--- |
+|test_create_router_simple|2|Ensures neutron_utils.create_router() can properly create a simple OpenStack router object|
+|test_create_router_with_public_interface|2|Ensures neutron_utils.create_router() can properly create 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 Version | 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 Version | 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 Version | 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 Version | 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/IntegrationTests.md b/docs/IntegrationTests.md
new file mode 100644
index 0000000..0433e5a
--- /dev/null
+++ b/docs/IntegrationTests.md
@@ -0,0 +1,123 @@
+# 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|Keystone 2 & 3 Neutron 2|Ensures the OpenStackSecurityGroup class can create a security group without any rules|
+|test_create_delete_group|Keystone 2 & 3 Neutron 2|Ensures the OpenStackSecurityGroup class clean() method will not raise an exception should the group be deleted by some other process|
+|test_create_group_with_one_simple_rule|Keystone 2 & 3 Neutron 2|Ensures the OpenStackSecurityGroup class can create a security group with a single rule|
+|test_create_group_with_several_rules|Keystone 2 & 3 Neutron 2|Ensures the OpenStackSecurityGroup class can create a security group with several rules|
+|test_add_rule|Keystone 2 & 3 Neutron 2|Ensures the OpenStackSecurityGroup#add_rule() method properly creates and associates the new rule|
+|test_remove_rule_by_id|Keystone 2 & 3 Neutron 2|Ensures the OpenStackSecurityGroup#remove_rule() method properly deletes and disassociates the old rule via its ID|
+|test_remove_rule_by_setting|Keystone 2 & 3 Neutron 2|Ensures the OpenStackSecurityGroup#remove_rule() method properly deletes and disassociates the old rule via its setting object|
+
+## create_image_tests.py - CreateImageSuccessTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_create_image_clean_url|Glance 1|Ensures the OpenStackImage class can create an image from a download URL location|
+|test_create_image_clean_file|Glance 1|Ensures the OpenStackImage class can create an image from a locally sourced image file|
+|test_create_delete_image|Glance 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|Glance 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 Version | 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 credentials project name is None|
+|test_none_auth_url|1|Ensures OpenStackImage.create() results in an Exception being raised when credentials URL is None|
+|test_none_password|1|Ensures OpenStackImage.create() results in an Exception being raised when credentials password is None|
+|test_none_user|1|Ensures OpenStackImage.create() results in an Exception being raised when credentials user is None|
+
+## create_keypairs_tests.py - CreateKeypairsTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_create_keypair_only|Nova 2|Ensures that a keypair object can be created simply by only configuring a name|
+|test_create_delete_keypair|Nova 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|Nova 2|Ensures that a keypair object can be created when the only the public key is cached to disk|
+|test_create_keypair_save_both|Nova 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|Nova 2|Ensures that a keypair object can be created with an existing public key file|
+
+## create_network_tests.py - CreateNetworkSuccessTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_create_network_without_router|Neutron 2|Ensures that a network can be created via the OpenStackNetwork class without any routers|
+|test_create_delete_network|Neutron 2|Ensures that a router can be deleted via the OpenStackNetwork.clean() method|
+|test_create_network_with_router|Neutron 2|Ensures that a network can be created via the OpenStackNetwork class with a router|
+|test_create_networks_same_name|Neutron 2|Ensures that the OpenStackNetwork.create() method will not create a network with the same name|
+
+## create_router_tests.py - CreateRouterSuccessTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_create_router_vanilla|Neutron 2|Ensures that a router can be created via the OpenStackRouter class with minimal settings|
+|test_create_delete_router|Neutron 2|Ensures that a router can be deleted via the OpenStackRouter.clean() method|
+|test_create_router_admin_state_false|Neutron 2|Ensures that a router can created with admin_state_up = False|
+|test_create_router_admin_state_True|Neutron 2|Ensures that a router can created with admin_state_up = True|
+|test_create_router_private_network|Neutron 2|Ensures that a router port can be created against a private network|
+|test_create_router_external_network|Neutron 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 | API Versions | Description |
+|--- |:-: |--- |
+|test_create_router_noname|Neutron 2|Ensures that an exception is raised when attempting to create a router without a name|
+|test_create_router_invalid_gateway_name|Neutron 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 and Neutron 2|Ensures that the OpenStackVmInstance.clean() method deletes the instance|
+
+## create_instance_tests.py - SimpleHealthCheck
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_check_vm_ip_dhcp|Nova and Neutron 2|Tests the creation of an OpenStack instance with a single port and it's assigned IP address|
+
+## create_instance_tests.py - CreateInstanceSingleNetworkTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_single_port_static|Nova and Neutron 2|Ensures that an instance with a single port/NIC with a static IP can be created|
+|test_ssh_client_fip_before_active|Nova and Neutron 2|Ensures that an instance can be reached over SSH when the floating IP is assigned prior to the VM becoming ACTIVE|
+|test_ssh_client_fip_after_active|Nova and Neutron 2|Ensures that an instance can be reached over SSH when the 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 and Neutron 2|Ensures that an instance's can have a valid static IP is properly assigned|
+|test_set_custom_invalid_ip_one_subnet|Nova and Neutron 2|Ensures that an instance's port with an invalid static IP raises an exception|
+|test_set_custom_valid_mac|Nova and Neutron 2|Ensures that an instance's port can have a valid MAC address properly assigned|
+|test_set_custom_invalid_mac|Nova and Neutron 2|Ensures that an instance's port with an invalid MAC address raises and exception|
+|test_set_custom_mac_and_ip|Nova and Neutron 2|Ensures that an instance's port with a valid static IP and MAC are properly assigned|
+|test_set_allowed_address_pairs|Nova and Neutron 2|Ensures the configured allowed_address_pairs is properly set on a VMs port|
+|test_set_allowed_address_pairs_bad_mac|Nova and Neutron 2|Ensures the port cannot be created when a bad MAC address format is used in the allowed_address_pairs port attribute|
+|test_set_allowed_address_pairs_bad_ip|Nova and Neutron 2|Ensures the port cannot be created when a bad IP address 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 and Neutron 2|Tests to ensure that one can fire up an instance on each active compute node|
+
+## create_instance_tests.py - CreateInstancePubPrivNetTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_dual_ports_dhcp|Nova and Neutron 2|Ensures that a VM with two ports/NICs can have its second NIC configured via SSH/Ansible after startup|
+
+## create_instance_tests.py - InstanceSecurityGroupTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_add_security_group|Nova and Neutron 2|Ensures that a VM instance can have security group added to it while its running|
+|test_add_invalid_security_group|Nova and Neutron 2|Ensures that a VM instance does not accept the addition of a security group that no longer exists|
+|test_remove_security_group|Nova and Neutron 2|Ensures that a VM instance accepts the removal of a security group|
+|test_remove_security_group_never_added|Nova and Neutron 2|Ensures that a VM instance does not accept the removal of a security group that was never added in the first place|
+|test_add_same_security_group|Nova and Neutron 2|Ensures that a VM instance does not add a security group that has already been added to the instance|
+
+## ansible_utils_tests.py - AnsibleProvisioningTests
+| Test Name | API Versions | Description |
+|--- |:-: |--- |
+|test_apply_simple_playbook|Nova and Neutron 2|Ensures that an instance assigned with a floating IP will apply a simple Ansible playbook|
+|test_apply_template_playbook|Nova and Neutron 2|Ensures that an instance assigned with a floating IP will apply a Ansible playbook containing Jinga2 substitution values|
diff --git a/docs/Testing.md b/docs/Testing.md
new file mode 100644
index 0000000..7fdbbf2
--- /dev/null
+++ b/docs/Testing.md
@@ -0,0 +1,37 @@
+# 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 <host>:<port>
+ * -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.md) - Tests that do not require a connection to OpenStack
+## [OpenStack API Tests] (APITests.md) - Tests many individual OpenStack API calls
+## [Integration Tests] (IntegrationTests.md) - Tests OpenStack object creation in a context. These tests will be run within a custom project as a specific user.
diff --git a/docs/UnitTests.md b/docs/UnitTests.md
new file mode 100644
index 0000000..59fc2d9
--- /dev/null
+++ b/docs/UnitTests.md
@@ -0,0 +1,48 @@
+# 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/VirtEnvDeploy.md b/docs/VirtEnvDeploy.md
new file mode 100644
index 0000000..decec94
--- /dev/null
+++ b/docs/VirtEnvDeploy.md
@@ -0,0 +1,158 @@
+# 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 <path to repo>/
+ * Deploy
+ * cd <path to repo>
+ * python snaps/deploy_venv.py -e <path to deployment configuration YAML file> -d
+ * Working example:
+
+```
+python deploy_venv.py -e <path to repo>/docs/examples/complex-network/deploy-complex-network.yaml -d
+```
+ * Clean
+ * python deploy_venv.py -e <path to deployment configuration YAML file> -c
+ * Working example (cleanup of a previously deployed virtual environment where the VM has Yardstick installed):
+
+```
+python deploy_venv.py -e <path to repo>/docs/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. A fully formed sample can be found in the ./provisioning/ansible/unimgr/deploy-unimgr.yaml that can be dowloaded
+from here.
+
+*** 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 \ No newline at end of file
diff --git a/docs/examples/complex-network/deploy-complex-network.yaml b/docs/examples/complex-network/deploy-complex-network.yaml
new file mode 100644
index 0000000..42559e8
--- /dev/null
+++ b/docs/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/docs/examples/complex-network/main.yml b/docs/examples/complex-network/main.yml
new file mode 100644
index 0000000..7f213ea
--- /dev/null
+++ b/docs/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/docs/examples/complex-network/playbooks/sample-playbook.yml b/docs/examples/complex-network/playbooks/sample-playbook.yml
new file mode 100644
index 0000000..726f213
--- /dev/null
+++ b/docs/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/docs/examples/external-network/deploy-ext-net.yaml b/docs/examples/external-network/deploy-ext-net.yaml
new file mode 100644
index 0000000..31c41ec
--- /dev/null
+++ b/docs/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/docs/examples/simple/deploy-simple.yaml b/docs/examples/simple/deploy-simple.yaml
new file mode 100644
index 0000000..ae946de
--- /dev/null
+++ b/docs/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/docs/examples/simple/files/motd b/docs/examples/simple/files/motd
new file mode 100644
index 0000000..cee5d06
--- /dev/null
+++ b/docs/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/docs/examples/simple/main.yml b/docs/examples/simple/main.yml
new file mode 100644
index 0000000..7f213ea
--- /dev/null
+++ b/docs/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/docs/examples/simple/playbooks/sample-playbook.yml b/docs/examples/simple/playbooks/sample-playbook.yml
new file mode 100644
index 0000000..84c46e4
--- /dev/null
+++ b/docs/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/docs/examples/two-network/deploy-two-net-centos.yaml b/docs/examples/two-network/deploy-two-net-centos.yaml
new file mode 100644
index 0000000..4fae4aa
--- /dev/null
+++ b/docs/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/docs/examples/two-network/deploy-two-net-ubuntu.yaml b/docs/examples/two-network/deploy-two-net-ubuntu.yaml
new file mode 100644
index 0000000..ffcb05d
--- /dev/null
+++ b/docs/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/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..536bc15
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,19 @@
+# Python scripts for creating virtual environments on OpenStack with Ansible playbooks for provisioning.
+
+## 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 &lt;path to repo>/snaps/
+
+## [Testing](Testing.md)
+## [Virtual Environment Deployment](VirtEnvDeploy.md)
+
+Also see the [CableLabs project wiki page](https://community.cablelabs.com/wiki/display/SNAPS/OpenStack+Instantiation%2C+Provisioning%2C+and+Testing)
+for more information on these scripts.
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..05851e2
--- /dev/null
+++ b/snaps/deploy_venv.py
@@ -0,0 +1,535 @@
+#!/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
+
+import file_utils
+from snaps.openstack.create_keypairs import KeypairSettings
+from snaps.openstack.create_router import RouterSettings
+from snaps.openstack.os_credentials import OSCreds, ProxySettings
+from snaps.openstack.create_image import ImageSettings
+from snaps.openstack.create_instance import VmInstanceSettings
+from snaps.openstack.create_network import PortSettings, NetworkSettings
+from snaps.provisioning import ansible_utils
+from snaps.openstack.utils import deploy_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_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)
+ fip = vm.get_floating_ip()
+ if vm and 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
+
+ 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 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, 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,
+ 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, 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()
+
+
+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..60a87cd
--- /dev/null
+++ b/snaps/openstack/create_flavor.py
@@ -0,0 +1,167 @@
+# 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')
+
+
+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)
+ 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):
+ """
+ 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)
+ """
+
+ 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
+ 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
+
+ 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..da6b942
--- /dev/null
+++ b/snaps/openstack/tests/conf/os_env.yaml.template
@@ -0,0 +1,39 @@
+# 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>
+
+
+
+#username: admin
+#password: cable123
+#os_auth_url: http://192.168.67.10:5000/v2.0/
+#project_name: admin
+#ext_net: external
+#http_proxy: 10.197.123.27:3128
+#ssh_proxy_cmd: '/usr/local/bin/corkscrew 10.197.123.27 3128 %h %p'
+
+
+
+
+username: admin
+password: admin
+os_auth_url: http://192.168.0.2:5000/v3
+project_name: admin
+identity_api_version: 3
+ext_net: admin_floating_net
+http_proxy: 10.197.123.27:3128
+ssh_proxy_cmd: '/usr/local/bin/corkscrew localhost 3128 %h %p' \ 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..c75bdf6
--- /dev/null
+++ b/snaps/openstack/tests/create_flavor_tests.py
@@ -0,0 +1,311 @@
+# 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_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)
+
+ 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)
+
+ def test_all(self):
+ settings = FlavorSettings(name='foo', flavor_id='bar', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0,
+ is_public=False)
+ 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)
+
+ def test_config_all(self):
+ 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})
+ 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)
+
+
+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)